--- name: anilist-anime-scraper description: Read AniList anime and manga catalogue entries as structured records via the Apify Actor arman-bd/anilist-anime-scraper. Returns titles in romaji, English and native script, format, airing status, episode or chapter counts, weighted community score, popularity, favourites, genres, community tags, studios, start and end dates, season, cover art and a plain-text synopsis. Use when a task needs to enrich a list of known titles, pull a whole airing season as a chart, or filter a catalogue by genre for trend and licensing research. Not for staff or character credits, relations, streaming links, user lists or per-episode airing schedules. --- # AniList Scraper: Anime, Manga & Studio Data Apify Actor `arman-bd/anilist-anime-scraper`. Give it search terms, or give it season and genre filters instead, and get one dataset record per title. It runs without credentials and there is nothing to configure beyond the query itself. ## When to use it - A list of title names has to become rows: scores, popularity, format, studios, dates. - You want the full chart for one airing season, most popular first, as a seed catalogue. - You are comparing seasons or genres and need averages over a few hundred titles. - Manga research: chapter counts, publication status and favourites-to-popularity ratio. - Any of the above on a schedule, diffing `averageScore`, `popularity` and `status`. ## When not to use it - Staff, voice actors, characters, relations or recommendations. The query shape is fixed and none of those edges are fetched. - Per-episode airing times or streaming availability. Not returned. - Anything tied to a signed-in AniList account: user lists, ratings, activity feeds. - Fetching one title you already know the ID of. A single lookup of your own is cheaper. ## Call it ```js import { ApifyClient } from 'apify-client'; const client = new ApifyClient({ token: process.env.APIFY_TOKEN }); const run = await client.actor('arman-bd/anilist-anime-scraper').call({ searchTerms: ['attack on titan', 'cowboy bebop'], mediaType: 'ANIME', genres: ['Action'], maxResults: 12, }); 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~anilist-anime-scraper/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"mediaType":"ANIME","season":"SPRING","seasonYear":2024,"maxResults":75}' ``` The Actor is also exposed through Apify's MCP server as `arman-bd/anilist-anime-scraper`, so an MCP-capable agent can call it with no extra wiring. ## Input | Field | Type | Required | Default | Notes | |---|---|---|---|---| | `searchTerms` | string[] | no | `[]` | Title searches, one query per entry, ranked by relevance to the term. Blank and duplicate entries are dropped. Leave empty to browse with the filters below instead. | | `mediaType` | string | no | `"ANIME"` | `ANIME` or `MANGA`. Decides which catalogue is queried and therefore whether `episodes` or `chapters` is populated. | | `season` | string | no | `""` | One of `WINTER`, `SPRING`, `SUMMER`, `FALL`, or empty for any. Anime only; manga carries no season. | | `seasonYear` | integer | no | | Airing year, minimum 1940, for example `2024`. Empty means all years. | | `genres` | string[] | no | `[]` | Exact AniList genre names, ANDed: a title must carry every one. Valid names are Action, Adventure, Comedy, Drama, Ecchi, Fantasy, Horror, Mahou Shoujo, Mecha, Music, Mystery, Psychological, Romance, Sci-Fi, Slice of Life, Sports, Supernatural, Thriller. | | `maxResults` | integer | no | `100` | Total titles saved across the whole run, minimum 1. Split evenly between search terms. | **Search or browse, not both in spirit.** With `searchTerms` set, each term is its own relevance-ranked query and the filters merely narrow it. With `searchTerms` empty, the filters alone define one popularity-ranked browse query, which is the mode worth scheduling. `season` without `seasonYear` matches that season in every year, which is almost never the intent; set the pair together. Genres being ANDed is the other trap: two genres cuts the result set hard, and a term returning fewer titles than `maxResults` allowed is usually the genre filter rather than a thin search. ## Output One record per title saved. | Field | Type | Notes | |---|---|---| | `id` | number | AniList media ID. Stable, use it as the join key across runs. | | `titleRomaji` | string \| null | Romanised title. In practice always present; the safe fallback when the English title is missing. | | `titleEnglish` | string \| null | `null` unless an official English title has been licensed. | | `titleNative` | string \| null | Native-script title. | | `type` | string | `ANIME` or `MANGA`, echoing `mediaType`. | | `format` | string \| null | `TV`, `MOVIE`, `OVA`, `ONA`, `SPECIAL`, `MANGA`, `NOVEL`, `ONE_SHOT`. | | `status` | string \| null | `FINISHED`, `RELEASING`, `NOT_YET_RELEASED`, `CANCELLED`, `HIATUS`. | | `episodes` | number \| null | Anime only. `null` for manga and for anime still unannounced. | | `chapters` | number \| null | Manga only. `null` for anime and for ongoing series without a final count. | | `averageScore` | number \| null | Weighted community score out of 100, not out of 10. `null` with too few votes. | | `popularity` | number \| null | Users with the title on any list. | | `favourites` | number \| null | Users who favourited it. A much smaller number than `popularity`. | | `genres` | string[] | AniList's curated genre list. `[]` when none are set. | | `tags` | string[] | Community tag names only, ranked most relevant first. Ranks and spoiler flags are dropped. | | `studios` | string[] | Producing studios, main studio first, then the rest. Includes producers, not only animation studios. | | `startDate` | string \| null | `YYYY-MM-DD`, degrading to `YYYY-MM` or `YYYY` when only part of the date is known. Not ISO 8601 in every case. | | `endDate` | string \| null | Same partial-date rule. `null` while a title is still running. | | `season` | string \| null | Airing season and year joined, for example `SPRING 2013`. Falls back to the bare season when the year is unknown. | | `coverImage` | string \| null | Highest-resolution cover art URL AniList offers. | | `description` | string \| null | Synopsis as plain text. Stray markup and HTML entities are stripped and decoded. | | `scrapedAt` | string | Run timestamp, ISO 8601. | A real record, long strings trimmed: ```json { "id": 16498, "titleRomaji": "Shingeki no Kyojin", "titleEnglish": "Attack on Titan", "titleNative": "進撃の巨人", "type": "ANIME", "format": "TV", "status": "FINISHED", "episodes": 25, "chapters": null, "averageScore": 85, "popularity": 1038684, "favourites": 85824, "genres": ["Action", "Drama", "Fantasy", "Mystery"], "tags": ["Kaiju", "Revenge", "Tragedy", "Military", "Henshin", "…"], "studios": ["WIT STUDIO", "Pony Canyon", "Kodansha", "Production I.G", "…"], "startDate": "2013-04-07", "endDate": "2013-09-28", "season": "SPRING 2013", "coverImage": "https://s4.anilist.co/file/anilistcdn/media/anime/cover/large/bx16498-buvcRTBx4NSm.jpg", "description": "Several hundred years ago, humans were nearly exterminated by titans. …", "scrapedAt": "2026-08-06T11:29:28.483Z" } ``` ## 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 { "sourcesRequested": 2, "sourcesFailed": 0, "failures": [], "titlesSaved": 9, "titlesPerSource": { "attack on titan": 6, "cowboy bebop": 3 }, "apiRequests": 2, "rateLimitPerMin": 30, "filters": { "searchTerms": ["attack on titan", "cowboy bebop"], "mediaType": "ANIME", "season": null, "seasonYear": null, "genres": ["Action"], "maxResults": 12 }, "finishedAt": "2026-08-06T11:29:30.148Z" } ``` `titlesSaved` below `maxResults` is normal and `titlesPerSource` says which term came up short. A non-empty `failures` array means a query aborted mid-run; each entry carries `source` and `error`. When `sourcesFailed` equals `sourcesRequested` the run throws instead of finishing, so a completed run always saved something. ## Behaviour to plan around - **`maxResults` is a total, split evenly and never redistributed.** Two search terms with `maxResults: 12` gives each a budget of 6. If the first matches only 3, the spare 3 do not roll over to the second. Ask for more than you need on multi-term runs. - **Sources are processed in order and the cap can cut the tail.** Once the total cap is reached, later search terms are skipped entirely and simply do not appear in `titlesPerSource`. Compare its key count against the terms you sent. - **`averageScore` is out of 100.** An 85 is a strong title, not a near-perfect one. Dividing by 10 to get a 0 to 10 score is the intended conversion. - **Dates are not always full dates.** `startDate` can be `2024`, `2024-10` or `2024-10-04` depending on what is known. Parse defensively rather than passing straight to a strict date constructor. - **`titleEnglish` is frequently null.** Only licensed English titles are carried. Key displays on `titleEnglish ?? titleRomaji`. - **`episodes` and `chapters` are mutually exclusive.** The one that does not apply to `mediaType` is always `null`, and the applicable one is also `null` for unfinished or unannounced works. A null episode count is not an error. - **`tags` are community-submitted and unbounded.** They are not the same vocabulary as `genres` and should not be mixed into the same facet. - **A failing query never aborts the run.** It is recorded in `failures` and the next source proceeds. Retries are three attempts with linear backoff for transient errors; a malformed query fails immediately, because retrying it never helps. - **The Actor paces itself.** Large browse runs take real wall-clock time by design, so set the run timeout accordingly rather than assuming a few hundred titles arrive in seconds. ## Recipes **Enrich a known list of titles.** One query per term, relevance ranked, so the first record for each term is usually the one you meant. ```json { "searchTerms": ["frieren", "chainsaw man", "one piece"], "mediaType": "ANIME", "maxResults": 30 } ``` Take the top record per term from `titlesPerSource` ordering, then join your list on `id`. **Seasonal chart, scheduled weekly.** No search terms, so the filters define one popularity-ordered browse. ```json { "mediaType": "ANIME", "season": "SPRING", "seasonYear": 2024, "maxResults": 300 } ``` Diff `averageScore`, `popularity` and `episodes` against last week's run, keyed on `id`. **Genre trend comparison across seasons.** Run once per season with the same genre filter and compare `studios` frequency and mean `averageScore`. ```json { "mediaType": "ANIME", "season": "FALL", "seasonYear": 2024, "genres": ["Action", "Fantasy"], "maxResults": 200 } ``` **Manga licensing shortlist.** Completed series with a high favourites-to-popularity ratio are the ones with a devoted readership relative to reach. ```json { "mediaType": "MANGA", "genres": ["Psychological", "Thriller"], "maxResults": 150 } ``` Filter to `status: "FINISHED"` locally, then rank on `favourites / popularity`.