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
2
Bookmarked
2
Total users
0
Monthly active users
a day ago
Last modified
Categories
Features
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"]}
Supported link shapes (all tunein.com):
| Shape | Example |
|---|---|
| Station, legacy | https://tunein.com/station/?stationId=34682 |
| Station, slug | https://tunein.com/radio/Example-Station-s34682/ |
| Podcast | https://tunein.com/podcasts/Talk-p1008289/ |
| Search page | https://tunein.com/search/?query=jazz |
| Search page, podcasts only | https://tunein.com/search/?query=jazz&filter=p |
| Category page | https://tunein.com/radio/Jazz-c57943/ |
| Genre page | https://tunein.com/radio/Jazz-g11/ |
| Category page, paginated | https://tunein.com/radio/Jazz-g11/?page=2 (the walk always starts at the first page and follows the source's own paging) |
| Podcasts hub | https://tunein.com/podcasts/ |
| Top-level music or local radio | https://tunein.com/radio/music/, https://tunein.com/radio/local/ |
| Embed player | https://tunein.com/embed/player/s34682/ |
| Podcast, legacy | https://tunein.com/program/?programId=1008289 |
| Episode | https://tunein.com/topic/?topicId=572686392 |
| Region page | https://tunein.com/radio/Asia-r101216/ |
| Directory id on its own | p1008289 (podcast), t572686392 (episode), r101216 (region) |
A search link ending in filter=p narrows that link to podcasts. It never widens your Result types selection: with only stations selected, that link is skipped and the log says so; with both selected, it returns podcasts only.
Default behaviour: in url mode, leaving Links or ids untouched now scrapes one example of each shape above instead of returning nothing. Set Max results or replace the list to narrow it. The suggested list the editor shows also includes the podcast, episode, id-only and region shapes from the table. If the list is emptied, the run reads one example station link. tun.in share links are not read: paste the tunein.com address they open instead.
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 on how much it returns. Your maximum cost per run also stops the run early, and Max result pages (below) bounds browsed categories. 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. ignoreFieldsForChanges lists extra field names (dotted paths such as streams.url reach nested fields) that must not count as a change; they stay in the output and are only left out of the comparison and changedFields, and changing the list never re-bills existing results or changes the state key. A run with nothing to scan in its mode (no keyword, no category or id, or no link) uses an example (the keyword jazz, Local radio or one station link) so a default run still returns results; that example run never saves state and never marks anything EXPIRED. With incremental mode off, output is exactly as before.
Send results into your apps (MCP connectors)
Optionally pipe the returned results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step after the scrape: the Apify dataset is never changed.
What gets written to the connector: a condensed, human-readable summary of each result, not the full JSON. Each result becomes one entry with a title and its key fields flattened to plain text. The complete record always stays in the Apify dataset.
- Authorize a connector once under Apify, Settings, API & Integrations (Notion, Linear, Airtable, or Apify).
- Select it in the "Pipe results into your apps" input field (
mcpConnectors). If the picker is empty, you have not authorized a connector yet. - For Notion, also set
notionParentPageUrlto the page where result pages should be created.maxNotifyListingscaps how many results are written per connector.
The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.
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 | (none; the editor suggests ["jazz"]) | Keywords to look up. Search mode only. Left empty, the run searches the example keyword jazz instead of returning nothing. |
browseCategories | array | (none; the editor suggests ["local"]) | Directory verticals to browse: local, music, talk, sports, podcast, language or location. Browse mode only. With no category and no id, the run browses Local radio. |
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 | one example of each supported link shape | TuneIn station, podcast, embed-player, search, category, genre and hub links, or bare directory ids such as s34682. URL mode only. Left empty, the run reads one example station link. |
itemTypes | array | (all) | Keep only station, podcast and/or episode results. |
minBitrateKbps | integer | 0 | Keep stations streaming at or above this bitrate. Stations only. |
maxBitrateKbps | integer | 0 | Keep stations streaming at or below this bitrate. Stations only; 0 means no upper limit. |
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. With incremental mode on and your own State key, the mode needs a keyword, category, id or link: with none the run stops with an input message and collects nothing. |
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. |
ignoreFieldsForChanges | array | (none) | Extra field names (dotted paths such as streams.url) that must not count as a change in incremental mode. They stay in the output; only the comparison and changedFields skip them. |
mcpConnectors | array | (none) | Optional. Send results into the apps you already use through Apify-authorized MCP connectors. Never changes the dataset. |
notionParentPageUrl | string | (none) | Notion parent page URL or id; needed only for the Notion connector. |
maxNotifyListings | integer | 50 | Cap on results written to each connector per run. |
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 is the Apify datacenter proxy pool, included on every plan with proxy access. A plain request with a browser user agent was served real content by the site and its directory API in our checks, but that check was not made through an Apify datacenter exit, so datacenter access is not guaranteed for every exit. A residential proxy is available if you select it in the proxy settings, and the actor never selects a paid option on its own.
If the default connection is unavailable to the run, the log shows once a boxed notice: "The selected connection is unavailable to this run. Apify free plans do not include residential proxies. Continuing on a backup connection; some pages may still be unavailable." and later "Served over the backup connection (the default connection was unavailable)." The backup connection is billed to your Apify account as proxy usage. It is used only when the proxy gateway refuses the connection itself; a site refusal or timeout does not switch it on. Custom proxy URLs and direct mode never get a backup connection. To turn the backup connection off, set the environment variable TUNEIN_ALTERNATE_CONNECTION=0 on the run; the run then uses only the connection you selected.
If nothing can be loaded, the run fails with a clear message instead of an empty dataset: "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required", "No results: residential proxies are unavailable on this run (Apify free plans do not include them), and the backup connection could not load the requested TuneIn pages", or "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search. Please try again later."
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. Each result returned is charged as the dataset-item event, and the run start as the actor-start event. The profile, stream and episode lookups are charged as the detail-enrichment event, one unit per returned result that received a lookup, charged row by row as each result is returned (not once at the end of the run); if your maximum cost per run is reached the run stops early. 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:
| ๐งฉ Pocket Casts Podcast Scraper Scrape Pocket Casts podcast data, including listener ratings, full episode archives... | ๐งฉ Anghami Scraper Scrape Anghami's public catalog by keyword or player URL. Rows carry their kind (song... |
| ๐งฉ GoodShort Scraper Scrape the GoodShort short-drama catalog: titles with exact views, follows, genres and... | ๐งฉ WEBTOON Scraper Scrape the WEBTOON catalog into structured data. Extract series, genres, rankings and... |
| ๐งฉ NetShort Scraper Scrape the NetShort short-drama catalog: series with synopses, like and follow counters... | โ๏ธ Fuel Price Scraper Scrape current petrol and fuel prices across Australia and New Zealand. Search by address... |
๐ 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.