TuneIn Scraper: Radio Stations, Podcasts & Stream URLs
Pricing
from $1.20 / 1,000 directory results
TuneIn Scraper: Radio Stations, Podcasts & Stream URLs
Scrape the TuneIn directory by keyword, category, genre, location or pasted link. Returns stations and podcasts with artwork, genre, bitrate, call sign, frequency, location, language, website, contact, playable stream URLs and episode lists. Incremental mode tracks changes.
Pricing
from $1.20 / 1,000 directory results
Rating
0.0
(0)
Developer
Abot API
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
0
Monthly active users
12 days ago
Last modified
Categories
Share
TuneIn Radio and Podcast Directory Scraper
Turn the TuneIn directory into a structured dataset: live radio stations, podcasts and podcast episodes, with artwork, genre, stream quality, dial position, location, language, the broadcaster's own website and published contact details, playable stream addresses and full episode lists.
Works worldwide, on every Apify plan, with no account on the site and no setup.
What you can do with it
| You want | Use |
|---|---|
| Every station matching a keyword or call sign | Search mode, queries: ["jazz"] |
| The popular stations near a city | Browse mode, browseCategories: ["local"] + localeLatLon: "51.5074,-0.1278" |
| A whole genre or category chart | Browse mode, browseCategories: ["music"] or browseIds: ["g11"] |
| Every station in a country or region | Browse mode, browseCategories: ["location"] |
| Podcasts in a topic, with their episodes | Browse mode, browseCategories: ["podcast"] + fetchEpisodes: true |
| One station or podcast you already know | URL mode, paste the link or the guideId |
| Only what changed since yesterday | Any mode + incrementalMode: true |
Modes
Search
One lookup per keyword. Returns the stations, podcasts and episodes TuneIn matches to the
term. A keyword lookup is complete in a single request: the directory returns its whole
match set at once and publishes no further pages for it, so maxPages does not apply here.
{ "mode": "search", "queries": ["jazz", "BBC Radio 4", "true crime"], "maxItems": 50 }
Browse
Walks TuneIn's own verticals and any genre, category or region address. Most top level
categories are pages of sub-categories rather than pages of stations, so the walk opens
sub-categories down to maxBrowseDepth (default 2). A category that lists stations directly,
such as Local radio, is never expanded further.
{"mode": "browse","browseCategories": ["local"],"localeLatLon": "40.7128,-74.0060","minReliabilityPercent": 90,"maxItems": 100}
localeLatLon applies to Local radio only, because that is the one category that is
about a place; the others are worldwide and would silently ignore it. Leave it empty and the
location of the connection the run goes out on is used instead.
URL
Paste TuneIn station, podcast and episode addresses, tun.in share links, or the bare
guideId values this actor returns. Category and genre addresses are walked like a browse
scope. In URL mode the full profile is always read, because a pasted link has nothing
else to read, and that lookup is not charged.
{"mode": "url","urls": ["https://tunein.com/station/?stationId=34682","https://tunein.com/radio/Some-Station-s24777/","s67855","c57943"]}
Filters
Every filter applies in all three modes. TuneIn publishes no filter for any of them on the routes this actor reads, so all five are applied to the results that come back.
| Filter | What it does |
|---|---|
itemTypes | Keep only stations, podcasts and/or episodes |
minBitrateKbps | Keep stations streaming at or above a bitrate |
minReliabilityPercent | Keep stations whose uptime score is at or above a percentage |
streamFormats | Keep stations offering a codec, for example mp3 or aac |
genreIds | Keep results filed under a genre id, for example g11 |
The three stream-quality filters apply to stations only. A podcast publishes no bitrate, no reliability score and no codec, so "podcasts at 128 kbps or better" would otherwise return nothing at all; instead podcasts and episodes pass those three filters untouched.
genreIds excludes episodes while fetchDetails is off, because an episode's listing row
publishes no genre. Its profile does, and the filter is re-applied after enrichment, so
switching fetchDetails on makes episodes filterable by genre like everything else.
Extra lookups and what they cost
Everything TuneIn's own listings carry is returned on every result at no extra cost: name, artwork, genre id, bitrate, reliability score, stream formats, and what is playing right now.
Three toggles each cost one extra request per result:
| Toggle | Adds | Applies to |
|---|---|---|
fetchDetails | Description, slogan, call sign, dial frequency and band, location and coordinates, language, timezone, website, published contact details, on-air status | All results |
fetchStreams | Playable stream addresses with bitrate, codec and reliability | Stations only |
fetchEpisodes | Published episode list with titles, dates and running times | Podcasts only |
A result that receives at least one of them is billed a single Detail enrichment event, whichever combination it received. A listing-only row is never billed one.
Two lookups are never even sent, because they cannot produce real data:
- A podcast is never asked for a stream. TuneIn answers that request with its own spoken "this content is not compatible" clip, at a healthy bitrate and reliability score, which would make every podcast look like a working radio station.
- A station is never asked for an episode list.
Output
One flat record per station, podcast or episode. Fields an entity kind does not have are
present and null rather than missing, so the dataset opens cleanly as a table.
{"itemType": "station","guideId": "s000000","name": "Example Jazz Radio","subtitle": "The city's jazz station","description": "An illustrative record. Values below are made up, not scraped.","slogan": "All jazz, all day","image": "https://cdn-profiles.tunein.com/s000000/images/logoq.png","url": "https://tunein.com/station/?stationId=000000","websiteUrl": "https://www.example-radio.org/","callSign": "KXMP-HD2","frequency": "88","band": "FM","genreId": "g11","genreName": "Jazz","location": "Springfield, US","latitude": null,"longitude": null,"regionId": "r100014","timezone": "Pacific","timezoneOffsetMinutes": -420,"language": "English","bitrateKbps": 128,"reliabilityPercent": 100,"streamFormats": ["mp3"],"isAvailable": true,"isMusic": true,"contentClassification": "music","nowPlaying": "Blue in Green","currentArtist": "Miles Davis","email": "studio@example-radio.org","phone": "+1-202-555-0100","mailingAddress": "1 Example Street, Springfield","twitterId": "ExampleJazz","streams": [{ "url": "https://stream.example-radio.org/live_128k", "bitrateKbps": 128,"mediaType": "mp3", "reliabilityPercent": 100, "isDirect": true, "position": 0 }],"streamCount": 1,"episodes": [],"episodeCount": 0,"sourceMode": "search","sourceScope": "jazz","scrapedAt": "2026-01-01T00:00:00Z"}
The sample above is illustrative. The station, its address, its e-mail and its phone number are invented for the documentation and are not values this actor returned.
A podcast record carries episodes[], episodeCount, hosts and no bitrate. An episode
record carries durationSeconds, publishedLabel and parentShowId.
Broadcaster contact details
email, phone and mailingAddress are the broadcaster's own published business
contact, exactly as TuneIn publishes it on the station's public profile page. They are
returned only when fetchDetails is on, and they are never written to the run log.
Resume and recurring updates
Two different things, in one input section.
resumeFromRunId continues ONE interrupted run. Paste a previous run or dataset id and
results already collected there are skipped, not re-returned and not re-billed.
incrementalMode is for scheduling. The actor remembers the previous run of the same
scope itself, keyed on a hash of the mode, scope, filters and lookup settings (or on your own
stateKey), and classifies each result:
changeType | Meaning |
|---|---|
NEW | First time this scope has seen it |
UPDATED | A tracked field changed; changedFields names them |
REAPPEARED | It was gone, and is listed again |
UNCHANGED | Returned only with emitUnchanged: true |
EXPIRED | Returned only with emitExpired: true |
What deliberately does NOT count as a change
These fields are returned on every row but are excluded from change detection, so they never
appear in changedFields and never re-bill a row on their own:
nowPlaying,nowPlayingImage,currentSong,currentArtist,currentAlbum: what is on air at the instant of the request. On a live station this turns over every few minutes.reliabilityPercent: a rolling uptime score TuneIn recomputes continuously; it drifts by a point or two with no event behind it. The same score inside eachstreams[]row is excluded for the same reason. A station going off air is still reported, throughisAvailableflipping tofalse, which is tracked.scrapedAt,sourceMode,sourceScope: these describe the run, not the entity.
Everything else, including a podcast's episodes list, is tracked: a new episode marks its
podcast UPDATED and names episodes in changedFields.
EXPIRED rows are only produced after a run has fully scanned the tracked scope. A run
that hit maxItems, used Resume, had a request refused, or collected nothing at all never
tombstones anything.
Limits
| Input | Default | Meaning |
|---|---|---|
maxItems | 20 | The run's cap, and the only limit on how much it returns or costs. 0 for unlimited. |
maxPages | 0 (no limit) | Listing pages per browsed or pasted category. Does not apply to keyword search. |
maxBrowseDepth | 2 | How many levels of sub-categories a browse may open. A scope choice, not a cost cap. |
maxEpisodesPerPodcast | 20 | Episodes returned inside each podcast record. 0 for all. |
maxItems is shared fairly across scopes: picking two categories returns results from both,
and a scope that comes back short hands its unused share to the ones after it.
Reliability
- Every request retries at most 4 times with a short timeout, then fails fast. A run that cannot get an answer stops and says so rather than grinding.
- A run that emits nothing because its requests were refused fails loudly with an explanatory status message. It never renders as "succeeded, 0 results".
- TuneIn answers a rejected keyword or an unknown category with a 200 and an in-band error over an empty body. This actor reads that as a rejection and says which value was rejected, rather than reporting it as an empty directory.
- If per-result lookups start failing systematically mid-run, they are switched off for the rest of the run, the remaining results are still returned with their listing fields, and they are not billed the enrichment surcharge.
Connection
The standard connection, included on every Apify plan, is the default and is enough for this site. A residential option is available if you specifically want one, and the actor never selects a paid one on its own.
Local development
python3.12 -m venv .venv.venv/bin/pip install -r requirements.txt.venv/bin/python -m pytest tests/ -qapify run --purge