TuneIn Scraper: Radio Stations, Podcasts & Stream URLs avatar

TuneIn Scraper: Radio Stations, Podcasts & Stream URLs

Pricing

from $1.20 / 1,000 directory results

Go to Apify Store
TuneIn Scraper: Radio Stations, Podcasts & Stream URLs

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

Abot API

Maintained by Community

Actor 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 wantUse
Every station matching a keyword or call signSearch mode, queries: ["jazz"]
The popular stations near a cityBrowse mode, browseCategories: ["local"] + localeLatLon: "51.5074,-0.1278"
A whole genre or category chartBrowse mode, browseCategories: ["music"] or browseIds: ["g11"]
Every station in a country or regionBrowse mode, browseCategories: ["location"]
Podcasts in a topic, with their episodesBrowse mode, browseCategories: ["podcast"] + fetchEpisodes: true
One station or podcast you already knowURL mode, paste the link or the guideId
Only what changed since yesterdayAny mode + incrementalMode: true

Modes

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.

FilterWhat it does
itemTypesKeep only stations, podcasts and/or episodes
minBitrateKbpsKeep stations streaming at or above a bitrate
minReliabilityPercentKeep stations whose uptime score is at or above a percentage
streamFormatsKeep stations offering a codec, for example mp3 or aac
genreIdsKeep 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:

ToggleAddsApplies to
fetchDetailsDescription, slogan, call sign, dial frequency and band, location and coordinates, language, timezone, website, published contact details, on-air statusAll results
fetchStreamsPlayable stream addresses with bitrate, codec and reliabilityStations only
fetchEpisodesPublished episode list with titles, dates and running timesPodcasts 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:

changeTypeMeaning
NEWFirst time this scope has seen it
UPDATEDA tracked field changed; changedFields names them
REAPPEAREDIt was gone, and is listed again
UNCHANGEDReturned only with emitUnchanged: true
EXPIREDReturned 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 each streams[] row is excluded for the same reason. A station going off air is still reported, through isAvailable flipping to false, 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

InputDefaultMeaning
maxItems20The run's cap, and the only limit on how much it returns or costs. 0 for unlimited.
maxPages0 (no limit)Listing pages per browsed or pasted category. Does not apply to keyword search.
maxBrowseDepth2How many levels of sub-categories a browse may open. A scope choice, not a cost cap.
maxEpisodesPerPodcast20Episodes 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/ -q
apify run --purge