FastAPI wrapper around the AnimeKai scraping workflow. This project turns the original Aniyomi/Tachiyomi scraper in into a documented HTTP API with:
- ReDoc at
/redoc - Swagger UI at
/docs - typed JSON responses
- episode token extraction
- AnimeKai search and detail scraping
- stream resolution for playable HLS URLs
The service exposes the same high-level flow a client would use manually:
- search or browse anime
- fetch details for a selected title
- fetch episode tokens for that title
- resolve playable stream URLs for one episode token
The main public endpoints are:
| Endpoint | Purpose |
|---|---|
GET / |
API metadata |
GET /health |
health check |
GET /filters |
supported search filter values |
GET /catalog/popular |
trending/popular anime |
GET /catalog/latest |
latest updated anime |
GET /catalog/search |
keyword + filter search |
GET /anime/details |
metadata for one anime |
GET /anime/episodes |
episodes and episode tokens |
GET /videos |
resolved stream URLs for one episode token |
After starting the server:
- ReDoc:
http://127.0.0.1:8000/redoc - Swagger UI:
http://127.0.0.1:8000/docs - OpenAPI JSON:
http://127.0.0.1:8000/openapi.json
The README below is meant to be the hand-written reference. /docs remains the source of truth for exact schema and query parameters.
uv venv
source .venv/bin/activate
uv pip install -e ".[dev]"
uv run uvicorn app.main:app --reloadWithout activating the virtualenv:
uv venv
uv pip install --python .venv/bin/python -e ".[dev]"
uv run --python .venv/bin/python uvicorn app.main:app --reloadpython3 -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/uvicorn app.main:app --reloadThe app listens on http://127.0.0.1:8000 by default.
| Variable | Default | Meaning |
|---|---|---|
ANIMEKAI_BASE_URL |
https://anikai.to |
default upstream AnimeKai domain |
ANIMEKAI_INCLUDE_ADULT |
false |
include adult-tagged titles in browse/search |
ANIMEKAI_TITLE_LANGUAGE |
en |
en or romaji title preference |
ANIMEKAI_ENABLED_TYPES |
sub,dub,softsub |
which stream groups are resolved by default |
ANIMEKAI_PREFERRED_QUALITY |
1080 |
result sorting preference only |
ANIMEKAI_PREFERRED_TYPE |
Subtitled |
result sorting preference only |
ANIMEKAI_PREFERRED_SERVER |
Server 1 |
result sorting preference only |
ANIMEKAI_USER_AGENT |
browser-like UA | outbound request user-agent |
ANIMEKAI_TIMEOUT_SECONDS |
20 |
upstream request timeout |
anime_pathis the relative AnimeKai watch path, for example/watch/naruto-9r5kwatch_pathin/videosis the same type of relative path returned by/anime/episodesepisode_tokencomes from/anime/episodes/videosreturns stream candidates sorted by your preferred quality, type, and server- upstream failures are returned as HTTP
502 - invalid filter values are returned as HTTP
422
curl "http://127.0.0.1:8000/catalog/search?query=naruto"Example:
"/watch/naruto-9r5k"curl "http://127.0.0.1:8000/anime/details?anime_path=/watch/naruto-9r5k"curl "http://127.0.0.1:8000/anime/episodes?anime_path=/watch/naruto-9r5k"Example:
"NcK_8PHzqEm40nVK0JqH"curl "http://127.0.0.1:8000/videos?watch_path=/watch/naruto-9r5k&episode_token=NcK_8PHzqEm40nVK0JqH&enabled_types=sub"Returns basic API metadata.
Example response:
{
"name": "AnimeKai Scraper API",
"version": "0.1.0",
"docs_url": "/docs",
"redoc_url": "/redoc"
}Returns the current service health and configured upstream base URL.
Example response:
{
"status": "ok",
"upstream_base_url": "https://anikai.to"
}Returns valid values for search filters. Use this when building a frontend or SDK instead of hardcoding ids.
Useful query behavior:
- search accepts either ids or labels for many filter groups
sort=autois converted internally:- to
most_relevancewhenqueryis non-empty - to
trendingwhenqueryis empty
- to
Partial example response:
{
"sort": [
"auto",
"most_relevance",
"updated_date",
"release_date",
"end_date",
"trending"
],
"genres": [
{ "id": "47", "label": "Action" },
{ "id": "1", "label": "Adventure" }
],
"statuses": [
{ "id": "info", "label": "Not Aired Yet" },
{ "id": "releasing", "label": "Releasing" },
{ "id": "completed", "label": "Completed" }
]
}Popular/trending catalog listing.
Query parameters:
| Name | Type | Default | Notes |
|---|---|---|---|
page |
int | 1 |
page number |
base_url |
str | configured default | override AnimeKai domain |
include_adult |
bool | env default | include adult titles |
title_language |
`en | romaji` | env default |
Example request:
curl "http://127.0.0.1:8000/catalog/popular?page=1"Latest-updates listing.
Query parameters are the same as /catalog/popular.
Example request:
curl "http://127.0.0.1:8000/catalog/latest?page=1"Search AnimeKai by keyword and optional filters.
Key query parameters:
| Name | Type | Notes |
|---|---|---|
query |
str | keyword search |
page |
int | page number |
sort |
str | see /filters |
genres |
repeated str | ids or labels |
exclude_genres |
repeated str | ids or labels |
statuses |
repeated str | ids or labels |
types |
repeated str | ids or labels |
seasons |
repeated str | ids or labels |
languages |
repeated str | ids or labels |
countries |
repeated str | ids or labels |
ratings |
repeated str | ids or labels |
years |
repeated str | exact years or buckets like 1990s |
include_adult |
bool | include adult results |
title_language |
`en | romaji` |
Example request:
curl "http://127.0.0.1:8000/catalog/search?query=naruto&sort=auto"Sample response:
{
"page": 1,
"has_next_page": false,
"source_url": "https://anikai.to/browser",
"applied_filters": {
"sort": "auto",
"genres": [],
"exclude_genres": [],
"statuses": [],
"types": [],
"seasons": [],
"languages": [],
"countries": [],
"ratings": [],
"years": []
},
"items": [
{
"title": "Naruto",
"alt_title": "NARUTO",
"path": "/watch/naruto-9r5k",
"thumbnail_url": "https://static.anikai.to/e8/i/9/13/67664948cb6c6@300.jpg",
"adult": false
},
{
"title": "Naruto Shippuden",
"alt_title": "NARUTO: Shippuuden",
"path": "/watch/naruto-shippuden-mv9v",
"thumbnail_url": "https://static.anikai.to/5a/i/9/8a/67664abaf05fa@300.jpg",
"adult": false
}
]
}Returns metadata for one anime page.
Query parameters:
| Name | Type | Required | Notes |
|---|---|---|---|
anime_path |
str | yes | relative AnimeKai watch path |
base_url |
str | no | upstream override |
title_language |
`en | romaji` | no |
Example request:
curl "http://127.0.0.1:8000/anime/details?anime_path=/watch/naruto-9r5k"Sample response:
{
"path": "/watch/naruto-9r5k",
"title": "Naruto",
"alt_title": "NARUTO",
"thumbnail_url": "http://static.1cdn.me/i/9/13/67664948cb6c6.jpg",
"description": "Twelve years ago, a colossal demon fox terrorized the world...",
"genres": [
"Adventure",
"Comedy",
"Fantasy",
"Shounen",
"Action",
"Supernatural",
"Drama",
"Martial Arts"
],
"status": "Completed",
"studios": ["Studio Pierrot"],
"producers": ["Aniplex", "TV Tokyo"]
}Returns episodes for an anime along with the token needed by /videos.
Query parameters:
| Name | Type | Required |
|---|---|---|
anime_path |
str | yes |
base_url |
str | no |
Example request:
curl "http://127.0.0.1:8000/anime/episodes?anime_path=/watch/naruto-9r5k"Sample response:
[
{
"number": 220.0,
"title": "Going on a Journey",
"name": "Episode 220: Going on a Journey",
"labels": ["Sub & Dub"],
"token": "NcK_8PHzqEm40nVK0JqH",
"watch_path": "/watch/naruto-9r5k"
},
{
"number": 219.0,
"title": "The Revived Ultimate Weapon",
"name": "Episode 219: The Revived Ultimate Weapon",
"labels": ["Sub & Dub", "Filler"],
"token": "d8W6rqPx4wXj0W9K2ZSU",
"watch_path": "/watch/naruto-9r5k"
}
]Resolves actual playable stream URLs for one episode token.
Query parameters:
| Name | Type | Required | Notes |
|---|---|---|---|
watch_path |
str | yes | use the value returned from /anime/episodes |
episode_token |
str | yes | use the episode token from /anime/episodes |
base_url |
str | no | upstream override |
enabled_types |
repeated `sub | dub | softsub` |
preferred_quality |
str | no | affects sorting only |
preferred_type_label |
str | no | affects sorting only |
preferred_server |
str | no | affects sorting only |
Example request:
curl "http://127.0.0.1:8000/videos?watch_path=/watch/naruto-9r5k&episode_token=NcK_8PHzqEm40nVK0JqH&enabled_types=sub&preferred_quality=1080&preferred_type_label=Subtitled&preferred_server=Server%201"Sample response:
{
"total": 6,
"preferred_quality": "1080",
"preferred_type_label": "Subtitled",
"preferred_server": "Server 1",
"items": [
{
"quality": "Subtitled | Server 1 | 1080p",
"url": "https://rrr.tech20hub.site/.../1080.m3u8",
"headers": {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
"Referer": "https://anikai.to"
},
"subtitles": [],
"source_type": "sub",
"source_type_label": "Subtitled",
"server_name": "Server 1"
},
{
"quality": "Subtitled | Server 2 | 1080p",
"url": "https://rrr.megaup.cc/.../1080.m3u8",
"headers": {
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36",
"Referer": "https://anikai.to"
},
"subtitles": [],
"source_type": "sub",
"source_type_label": "Subtitled",
"server_name": "Server 2"
}
]
}Typical error responses:
HTTP 502
{
"detail": "Upstream request failed with status 503."
}or
{
"detail": "Unable to extract video streams from the resolved servers. sub/Server 1: ..."
}HTTP 422
{
"detail": "Invalid genre value(s): bad-value. Use the ids or labels from /filters."
}- The sample JSON above is intentionally trimmed to keep the README readable.
- For exact current schema and live responses, prefer
/docsand direct requests to the running API. - Values like stream URLs, thumbnails, available servers, and tokens come from live upstream scraping and can change at any time.