--- name: tvmaze-shows-scraper description: Pull complete TV series metadata from TVmaze via the Apify Actor arman-bd/tvmaze-shows-scraper. Returns one record per show with title, type, language, genres, status, runtime, premiere and end dates, network or streaming channel, community rating, poster, a plain-text summary, and optionally the full episode list with air dates and the full cast with character names. Use when a task needs a watchlist or TV tracking app, catalogue enrichment, air-date monitoring, cast-overlap similarity or clean summaries for a retrieval index. Not for streaming availability by region, box office, subtitles or video. --- # TVmaze Scraper: TV Shows, Episodes & Cast Apify Actor `arman-bd/tvmaze-shows-scraper`. Give it show names, TVmaze show ids, or both, and get one dataset record per show. Episodes and cast are embedded into the same request as the show, so a 200-episode series is still one round trip. It takes no credentials. ## When to use it - Building a watchlist or TV tracker: air dates and episode numbering arrive ready to index. - Enriching a catalogue you already have, once your titles are matched to TVmaze ids. - Nightly schedule monitoring, diffing `airdate` per `episodeId` to catch moved episodes. - Media analysis across a set of shows: genre mix, broadcast against streaming, run length. - Recommendation seeds from cast overlap, which is a cheap and strong similarity signal. ## When not to use it - Regional streaming availability, prices or deep links. `webChannel` names a platform, it does not say where you can watch. - Video, images beyond the poster, subtitles or transcripts. - Films. This is a television database. - Fuzzy catalogue matching at scale. Search returns a relevance ranking, not an identity resolution; for anything that must be exact, resolve the id once and pass `showIds` from then on. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/tvmaze-shows-scraper').call({ searchTerms: ['Severance', 'The Bear'], showIds: ['169'], includeEpisodes: true, includeCast: false, maxShowsPerTerm: 1, }); const { items } = await client.dataset(run.defaultDatasetId).listItems(); const { value: summary } = await client .keyValueStore(run.defaultKeyValueStoreId) .getRecord('RUN_SUMMARY'); ``` One-shot over HTTP, when you want the rows back in the same request: ```bash curl -X POST "https://api.apify.com/v2/acts/arman-bd~tvmaze-shows-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"searchTerms":["Severance"],"includeEpisodes":true,"includeCast":true}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/tvmaze-shows-scraper`, so an MCP-capable agent can call it with no extra wiring. ## Input | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `searchTerms` | string[] | conditional | `[]` | Show titles, resolved through a relevance-ranked search. Trimmed and de-duplicated. Each term costs one extra request before the show itself. | | `showIds` | string[] | conditional | `[]` | Exact TVmaze show ids. A full show URL is accepted and reduced to its numeric id; anything non-numeric after that is dropped silently. | | `includeEpisodes` | boolean | no | `true` | Embed the full episode list, specials included. Costs no extra request. | | `includeCast` | boolean | no | `true` | Embed the full cast list. Costs no extra request. | | `maxShowsPerTerm` | integer | no | `1` | Ranked hits kept per search term. `0` keeps every hit. Range 0 to 10, and a search returns at most 10 anyway. | **At least one of `searchTerms` or `showIds` must be non-empty**, or the run throws before any request. Supplying both is normal and the two are merged on show id, so a show reached by both routes is scraped once. The real trade-off is precision against discovery: `maxShowsPerTerm: 1` gives you the single best match per term, which is what a watchlist wants and which is also how a search quietly returns the wrong show; `maxShowsPerTerm: 10` turns the same call into discovery and multiplies the row count by up to ten. Neither `includeEpisodes` nor `includeCast` adds a request, so leave them on unless record size is the problem. ## Output One record per show resolved. | Field | Type | Notes | |---|---|---| | `showId` | number | TVmaze id. The stable key; pass it back as `showIds` on later runs. | | `name` | string | Show title. | | `type` | string \| null | `Scripted`, `Reality`, `Animation`, `Documentary` and similar. | | `language` | string \| null | Original language. | | `genres` | string[] | Genre list, empty when none are recorded. | | `status` | string \| null | `Running`, `Ended`, `To Be Determined`, `In Development`. | | `runtime` | number \| null | Typical episode length in minutes, falling back to the average when episode lengths vary. Per-episode `runtime` is the exact figure. | | `premiered` | string \| null | `YYYY-MM-DD`. | | `ended` | string \| null | `null` for a show still running or not yet finished. | | `officialSite` | string \| null | The show's own site, when recorded. | | `network` | string \| null | Broadcast network. `null` for streaming originals. | | `webChannel` | string \| null | Streaming platform. `null` for broadcast shows. **Read both.** | | `rating` | number \| null | Community rating out of 10. `null` when too few votes. | | `summary` | string | Synopsis as plain text: entities decoded, markup stripped. Empty string when the database has no summary. | | `imageUrl` | string \| null | Full-resolution poster, falling back to the medium size. | | `url` | string \| null | Public show page. | | `episodes` | object[] | **Key is absent entirely when `includeEpisodes` is false.** Keys: `episodeId`, `season`, `number`, `name`, `type`, `airdate`, `airstamp`, `runtime`, `rating`, `url`, `summary`. | | `cast` | object[] | **Key is absent entirely when `includeCast` is false.** Keys: `personId`, `personName`, `personUrl`, `country`, `birthday`, `characterId`, `characterName`, `characterUrl`, `self`, `voice`. | | `scrapedAt` | string | Run timestamp, ISO 8601. | A real record, lists trimmed to one entry each: ```json { "showId": 44933, "name": "Severance", "type": "Scripted", "language": "English", "genres": ["Drama", "Science-Fiction", "Mystery"], "status": "Running", "runtime": 49, "premiered": "2022-02-18", "ended": null, "officialSite": "https://tv.apple.com/show/severance/…", "network": null, "webChannel": "Apple TV", "rating": 7.7, "summary": "Mark Scout leads a team at Lumon Industries, whose employees have undergone a severance procedure …", "imageUrl": "https://static.tvmaze.com/uploads/images/original_untouched/548/1371406.jpg", "url": "https://www.tvmaze.com/shows/44933/severance", "episodes": [ { "episodeId": 2238231, "season": 1, "number": 1, "name": "Good News About Hell", "type": "regular", "airdate": "2022-02-18", "airstamp": "2022-02-18T12:00:00+00:00", "runtime": 57, "rating": 7.2, "url": "https://www.tvmaze.com/episodes/2238231/severance-1x01-good-news-about-hell", "summary": "Mark is promoted to lead a team who have had their memories surgically divided …" } ], "cast": [ { "personId": 31964, "personName": "Adam Scott", "personUrl": "https://www.tvmaze.com/people/31964/adam-scott", "country": "United States", "birthday": "1973-04-03", "characterId": 673312, "characterName": "Mark Scout", "characterUrl": "https://www.tvmaze.com/characters/673312/severance-mark-scout", "self": false, "voice": false } ], "scrapedAt": "2026-08-06T12:00:00.000Z" } ``` ## RUN_SUMMARY Written to the run's key-value store under the key `RUN_SUMMARY`. **Read it.** It is where a partial run admits that it was partial. ```json { "searchTermsRequested": 3, "showIdsRequested": 1, "showsResolved": 3, "showsSaved": 3, "episodesSaved": 271, "castMembersSaved": 0, "sourcesFailed": 1, "failures": [ { "source": "search:zzzq not a show", "error": "no results" } ], "filters": { "searchTerms": ["Severance", "The Bear", "zzzq not a show"], "showIds": [169], "includeEpisodes": true, "includeCast": false, "maxShowsPerTerm": 1 }, "finishedAt": "2026-08-06T12:00:04.311Z" } ``` `showsResolved` is the count of **distinct** shows the search terms and ids resolved to, so it is lower than `searchTermsRequested + showIdsRequested` whenever a term resolved to a show you also listed by id. `failures[].source` is prefixed: `search:` means a term matched nothing or the lookup failed, `show:` means the show itself could not be read. `castMembersSaved: 0` alongside `includeCast: false` is expected, not a shortfall. Note that this Actor throws when **no** show at all was saved, so a run that finishes normally always has at least one row. ## Behaviour to plan around - **`episodes` and `cast` are absent, not empty**, when their flags are off. Code that reads `show.episodes.length` throws rather than seeing `0`. Check for the key. - **A search can confidently return the wrong show.** With the default `maxShowsPerTerm: 1` you get the top-ranked hit and no indication of how close the runner-up was. For anything that must be right, raise it and choose, or resolve the id once and use `showIds`. - **Show-level `runtime` may be an average.** Where episode lengths vary the show value falls back to the average, so it can disagree with every single `episodes[].runtime`. Use the per-episode value for arithmetic. - **`network` and `webChannel` are mutually exclusive in practice.** A streaming original leaves `network` null. Reading only one of them silently loses half your catalogue. - **Specials are in `episodes`.** Every episode the database holds is returned, with `type` distinguishing `regular`, `special` and `insignificant`. Filter on `type` before computing a season count. - **`airdate` moves.** Schedules change, which is the whole reason to re-run. Diff on `episodeId` plus `airdate`, not on the episode count. - **`rating` is null rather than zero** when a show or episode has too few votes. Do not average nulls into a score. - **Summaries are plain text already.** They are decoded before and after tags are stripped, so no HTML cleanup step is needed downstream. - **A missing show never aborts the run.** Searches with no hits and unknown ids land in `RUN_SUMMARY.failures` and the run continues. Not-found is final; transient failures get three attempts with exponential backoff. ## Recipes **Watchlist refresh.** Known ids, episodes on, cast off, cheap enough to run nightly. ```json { "showIds": ["44933", "169", "https://www.tvmaze.com/shows/82/game-of-thrones"], "includeEpisodes": true, "includeCast": false } ``` Diff `episodes[].airdate` per `episodeId` and alert on anything that moved. **Resolve titles to ids once.** Metadata only, then keep the ids. ```json { "searchTerms": ["Severance", "Breaking Bad", "The Bear"], "maxShowsPerTerm": 3, "includeEpisodes": false, "includeCast": false } ``` Pick the right `showId` per title by comparing `premiered` and `network` or `webChannel`, then use `showIds` from then on. **Discovery sweep.** Broad terms, every ranked hit, small records. ```json { "searchTerms": ["detective", "space"], "maxShowsPerTerm": 10, "includeEpisodes": false, "includeCast": true } ``` Group by `genres` and `webChannel`, and use `cast[].personId` overlap as a similarity edge. **Full series snapshot for a retrieval index.** Everything on, one show. ```json { "showIds": ["44933"], "includeEpisodes": true, "includeCast": true } ``` Chunk `summary` and each `episodes[].summary`, citing `url` and `episodes[].url`.