TuneIn Scraper - Radio Stations, Podcasts & Stream URLs
Pricing
from $1.20 / 1,000 directory results
TuneIn Scraper - Radio Stations, Podcasts & Stream URLs
Scrape TuneIn by keyword, category, genre, location or URL. Extract stations and podcasts with artwork, genre, bitrate, frequency, location, language, contact details, playable stream URLs and episodes. Incremental mode tracks changes over time.
Pricing
from $1.20 / 1,000 directory results
Rating
0.0
(0)
Developer
Abot API
Maintained by CommunityActor stats
1
Bookmarked
2
Total users
1
Monthly active users
4 days ago
Last modified
Categories
Share
TuneIn Radio and Podcast Directory Scraper: Stations, Podcasts & Episodes
TuneIn Radio and Podcast Directory Scraper turns 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. Search by keyword, browse the directory's own categories, genres and locations, or paste station, podcast and category links, then export to JSON, CSV or Excel, or read the results straight through the API.
Why This Scraper?
- Three ways to find data. Search by keyword, browse the directory's own categories, genres and locations, or paste station, podcast and category links. No need to build addresses by hand.
- One flat record for every kind. Stations, podcasts and episodes share the same shape, with a field a kind does not have present and null rather than missing, so the dataset opens cleanly as a table.
- Enrichment you switch on. The full published profile, playable stream addresses and episode lists are each a separate toggle, billed only when you turn them on.
- Worldwide coverage. Every genre, category and region the directory publishes, plus a locale-aware Local radio chart for one place's popular stations.
- Built for schedules. Incremental mode classifies each result NEW, UPDATED, REAPPEARED, UNCHANGED or EXPIRED, and ignores what is playing right now so a live station is not re-billed every run.
- Resume without re-paying. Continue one interrupted run and the actor skips everything it already collected there.
- Fails loudly, not silently. A run whose requests were refused stops with a clear message instead of reporting an empty dataset.
Use Cases
- Radio directory and aggregator apps: pull live stations with stream URLs, genre, dial position and location to power your own listings.
- Podcast discovery tools: track podcast catalogs and their episode lists inside one topic, category or search term.
- Local media research: chart the stations available near a place with Local radio and Locale coordinates.
- Directory monitoring: schedule incremental runs to catch new stations and podcasts, or ones going off air, without re-paying for what has not changed.
Data You Get
Sample shape: values are illustrative placeholders, not from a live record.
| Field | Example |
|---|---|
itemType | "station" (also "podcast", "episode") |
guideId | "s900001" |
name / subtitle | "Riverside Jazz Radio" / "Smooth jazz for your afternoon" |
genreId / genreName | "g11" / "Jazz" |
image | station, podcast or episode artwork |
url | the public TuneIn page for the result |
websiteUrl | the broadcaster's own site (profile lookup) |
callSign / frequency / band | "WRVJ-HD3" / "87.9" / "FM" (stations, profile lookup) |
location / language | "Millbrook, US" / "English" |
bitrateKbps / streamFormats | 128 / ["mp3"] (stations) |
reliabilityPercent | uptime score; excluded from incremental change detection |
isAvailable | on-air flag (profile lookup) |
nowPlaying / nowPlayingImage / currentSong / currentArtist / currentAlbum | what is on air right now; never counted as a change |
email / phone / mailingAddress | the broadcaster's own published contact details (profile lookup) |
streams / streamCount | playable stream addresses (stream lookup, stations only) |
episodes / episodeCount | the published episode list (episode lookup, podcasts only) |
durationSeconds / publishedLabel | an episode's running time and publish label |
hosts | a podcast's listed hosts (profile lookup) |
parentShowId | the podcast an episode belongs to |
sourceMode / sourceScope | how this run reached the result, for example "search" / "jazz" |
scrapedAt | the ISO timestamp of the run, the same on every row of one run |
changeType / changedFields | incremental mode only, for example "UPDATED" / ["isAvailable"], alongside firstSeenAt / lastSeenAt |
How to Use
- Pick a mode:
search(by keyword),browse(the directory's own categories, genres and locations) orurl(paste links or directory ids). - Fill in the fields for that mode, and switch on the extra lookups you need: full profiles, playable streams, episode lists.
- Set the filters and Max results that fit your run, then click Start.
- Download the dataset as JSON, CSV or Excel, or read it through the API.
Search by keyword:
{ "mode": "search", "queries": ["jazz", "BBC Radio 4"], "maxItems": 50 }
Browse the popular stations near a city:
{"mode": "browse","browseCategories": ["local"],"localeLatLon": "40.7128,-74.0060","minReliabilityPercent": 90,"maxItems": 100}
Browse podcasts, with their episode lists:
{"mode": "browse","browseCategories": ["podcast"],"fetchEpisodes": true,"maxEpisodesPerPodcast": 10,"maxItems": 50}
Paste a station link and a bare directory id:
{"mode": "url","urls": ["https://tunein.com/station/?stationId=34682","s24777"]}
Run it from your code
Python:
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("abotapi/tunein-radio-podcast-scraper").call(run_input={"mode": "search", "queries": ["jazz"]})for item in client.dataset(run["defaultDatasetId"]).iterate_items():print(item["name"], item["itemType"], item.get("url"))
JavaScript:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });const run = await client.actor('abotapi/tunein-radio-podcast-scraper').call({ mode: 'search', queries: ['jazz'] });const { items } = await client.dataset(run.defaultDatasetId).listItems();
Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the Integrations tab.
Limits and scope
- Max results (
maxItems, default 20) is the run's cap, and the only setting that limits how much it returns or costs. Use0for unlimited. Picking several keywords or categories shares this cap fairly across them: a scope that comes back short hands its unused share to the ones after it. - Max result pages (
maxPages, default 0, no limit) bounds how many listing pages are read per browsed category or pasted category link. Does not apply to keyword search, which the directory answers in a single page. - Sub-category depth (
maxBrowseDepth, default 2) bounds how many levels of sub-categories a browse walk may open. A scope choice, not a cost cap. - Max episodes per podcast (
maxEpisodesPerPodcast, default 20) caps how many episodes are returned inside each podcast record, newest first. Use0for every published episode.
Resume and recurring updates
Resume (resumeFromRunId) continues one interrupted run: paste its run or dataset id and results already collected there are skipped, not re-returned and not re-billed. Incremental mode (incrementalMode) is for scheduling the same scope again and again, for example daily. Each result is then classified NEW, UPDATED (with changedFields), REAPPEARED, UNCHANGED (suppressed and not billed unless emitUnchanged is on) or EXPIRED (only after a run that scanned the whole scope, and only with emitExpired; these rows are added on top of Max results). What is playing right now and the rolling reliability score never count as a change on their own; a station going off air is reported through isAvailable instead. stateKey names or shares the stored monitoring state. With incremental mode off, output is exactly as before.
Input Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
mode | string | search | search (by keyword), browse (the directory's own categories, genres and locations) or url (pasted links or ids). |
queries | array | ["jazz"] | Keywords to look up. Search mode only. |
browseCategories | array | ["local"] | Directory verticals to browse: local, music, talk, sports, podcast, language or location. Browse mode only. |
browseIds | array | (none) | Specific genre, category or region ids, for example g11, c57943 or r101216. Browse mode only. |
localeLatLon | string | (none) | Latitude and longitude for the Local radio category only, for example 51.5074,-0.1278. Leave empty to use the connection's own location. Browse mode only. |
urls | array | sample links | TuneIn links, tun.in share links, or bare directory ids such as s34682. URL mode only. |
itemTypes | array | (all) | Keep only station, podcast and/or episode results. |
minBitrateKbps | integer | 0 | Keep stations streaming at or above this bitrate. Stations only. |
minReliabilityPercent | integer | 0 | Keep stations whose uptime score is at or above this percentage. Stations only. |
streamFormats | array | (any) | Keep stations offering one of these codecs, for example mp3. Stations only. |
genreIds | array | (any) | Keep results filed under one of these genre ids, for example g11. |
fetchDetails | boolean | false | Read each result's full published profile. One extra request per result; always on, and not charged, in url mode. |
fetchStreams | boolean | false | Add each station's playable stream addresses. One extra request per station. |
fetchEpisodes | boolean | false | Add each podcast's published episode list. One extra request per podcast. |
maxEpisodesPerPodcast | integer | 20 | Cap on episodes returned inside each podcast record, newest first. 0 for every episode. |
maxItems | integer | 20 | Maximum results returned across the whole run. 0 for unlimited. |
maxPages | integer | 0 | Maximum listing pages read per browsed or pasted category. 0 means no limit. Does not apply to keyword search. |
maxBrowseDepth | integer | 2 | How many levels of sub-categories a browse walk may open. |
resumeFromRunId | string | (none) | Continue one interrupted run without re-collecting or re-billing its results. |
incrementalMode | boolean | false | Return only new and changed results on scheduled runs over the same scope. |
stateKey | string | (none) | Name or share an incremental-mode monitoring campaign. Auto-derived from the scope when left empty. |
emitUnchanged | boolean | false | Also return (and bill) results that have not changed since the last run. |
emitExpired | boolean | false | Also return (and bill) results no longer found, after a complete scan of the tracked scope. |
proxy | object | Apify Proxy | Connection settings. |
Output Example
Sample shape: values are illustrative placeholders, not from a live record.
{"itemType": "station","guideId": "s900001","name": "Riverside Jazz Radio","subtitle": "Smooth jazz for your afternoon","description": "An illustrative record. Values below are made up, not scraped.","slogan": "Jazz, all day, every day","image": "https://cdn-profiles.tunein.com/s900001/images/logoq.png","url": "https://tunein.com/station/?stationId=900001","websiteUrl": "https://www.riverside-jazz-example.org/","callSign": "WRVJ-HD3","frequency": "87.9","band": "FM","genreId": "g11","genreName": "Jazz","location": "Millbrook, US","latitude": null,"longitude": null,"regionId": "r200099","timezone": "Eastern","timezoneOffsetMinutes": -300,"language": "English","bitrateKbps": 128,"reliabilityPercent": 98,"streamFormats": ["mp3"],"isAvailable": true,"isMusic": true,"contentClassification": "music","nowPlaying": "Sample Track Title","currentArtist": "Sample Artist Name","email": "studio@example-radio.org","phone": "+1-202-555-0100","mailingAddress": "12 Sample Avenue, Millbrook","twitterId": "RiversideJazzFM","streams": [{ "url": "https://stream.riverside-jazz-example.org/live_128k", "bitrateKbps": 128,"mediaType": "mp3", "reliabilityPercent": 98, "isDirect": true, "position": 0 }],"streamCount": 1,"episodes": [],"episodeCount": 0,"sourceMode": "search","sourceScope": "jazz","scrapedAt": "2026-01-01T00:00:00Z"}
A podcast record carries episodes[], episodeCount and hosts, with the bitrate and stream fields left empty. An episode record carries durationSeconds, publishedLabel and parentShowId instead.
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 (or always, in url mode), and they are never written to the run log.
Plan Requirement
The default connection, included on every Apify plan, is enough for this site and is the default. A residential proxy is available if you specifically want one, but the actor never selects a paid option on its own.
FAQ
How much does it cost?
You pay per result returned, with the full profile, stream addresses and episode lists billed only when you switch them on. The Pricing tab shows the current rates. Use Max results to cap the cost of any run.
Is it legal to scrape TuneIn?
This actor collects only publicly available directory data. You are responsible for how you use it: follow TuneIn's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution.
Can I get only new or changed stations and podcasts on a schedule?
Yes. Schedule the actor from the Schedules tab and turn on Incremental mode. Each run then returns only new, updated and reappeared results, and unchanged ones are not billed unless you ask for them.
What counts as a change in incremental mode?
Almost everything, except what is playing right now and the rolling reliability score, which drift on their own and are excluded so a live station is not re-billed every run. A station going off air is still reported, through isAvailable flipping to false.
Why did my run fail instead of returning an empty dataset?
If the directory refuses every request, the run stops with a clear message so "nothing matched" is never confused with "nothing could be read". Run it again in a few minutes.
Can I use it with AI agents or MCP?
Yes. Call it from any Apify integration or MCP client, schedule it from the Schedules tab, or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the Integrations tab.
๐ More scrapers you might like
Pair this actor with these related scrapers from the same team:
| ๐งฉ Anghami Scraper Scrape Anghami's public catalog by keyword or player URL. Rows carry their kind (song... | ๐งฉ Pocket Casts Podcast Scraper Scrape Pocket Casts podcast data, including listener ratings, full episode archives... |
| ๐งฉ WEBTOON Scraper Scrape the WEBTOON catalog into structured data. Extract series, genres, rankings and... | ๐งฉ GoodShort Scraper Scrape the GoodShort short-drama catalog: titles with exact views, follows, genres and... |
| ๐งฉ GasBuddy Scraper Scrape GasBuddy station by station across the US and Canada. Returns brand, address... | ๐งฉ PineDrama Scraper Scrape the PineDrama short-drama catalogue: series with watch counts, episode counts... |
๐ Browse all abotapi scrapers
๐ฌ Support
- ๐ Found a bug or a missing field? Open a ticket on the Issues tab. We usually reply within hours.
- ๐ ๏ธ Need another site, extra fields or a private build? Email contact@abotapi.com.
- โญ Enjoying it? A quick review on the actor page helps other users find it.