{"id":50818732,"url":"https://github.com/risqinf/apiku","last_synced_at":"2026-06-13T12:01:19.968Z","repository":{"id":360480593,"uuid":"1250355128","full_name":"risqinf/apiku","owner":"risqinf","description":"RESTful scraping API in Rust for Mangaball, Anichin, Cosplaytele, nhentai, and NovelID. Opaque HMAC-signed IDs, signed image proxy, browser fingerprint rotation, single-flight cache, adaptive runtime. Live demo at api.risqinf.web.id.","archived":false,"fork":false,"pushed_at":"2026-06-04T21:23:46.000Z","size":743,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-04T23:07:38.646Z","etag":null,"topics":["axum","cosplay","donghua","doujin","hmac","image-proxy","manga","manhua","manhwa","nhentai","novel","opaque-id","open-source","reqwest","rust","toxio","vibe-coding","web-scraping"],"latest_commit_sha":null,"homepage":"https://api.farellvpn.engineer","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/risqinf.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-26T14:49:46.000Z","updated_at":"2026-06-04T21:22:34.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/risqinf/apiku","commit_stats":null,"previous_names":["risqinf/apiku"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/risqinf/apiku","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/risqinf%2Fapiku","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/risqinf%2Fapiku/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/risqinf%2Fapiku/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/risqinf%2Fapiku/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/risqinf","download_url":"https://codeload.github.com/risqinf/apiku/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/risqinf%2Fapiku/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34283391,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-13T02:00:06.617Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["axum","cosplay","donghua","doujin","hmac","image-proxy","manga","manhua","manhwa","nhentai","novel","opaque-id","open-source","reqwest","rust","toxio","vibe-coding","web-scraping"],"created_at":"2026-06-13T12:01:05.074Z","updated_at":"2026-06-13T12:01:19.952Z","avatar_url":"https://github.com/risqinf.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# apiku\r\n\r\n[![CI](https://github.com/risqinf/apiku/actions/workflows/ci.yml/badge.svg)](https://github.com/risqinf/apiku/actions/workflows/ci.yml)\r\n[![Release](https://github.com/risqinf/apiku/actions/workflows/release.yml/badge.svg)](https://github.com/risqinf/apiku/actions/workflows/release.yml)\r\n[![Live](https://img.shields.io/website?url=https%3A%2F%2Fapi.farellvpn.engineer%2Fapi%2Fv1%2Fhealth\u0026label=api.farellvpn.engineer)](https://api.farellvpn.engineer)\r\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\r\n\r\n\u003e RESTful scraping API for **Mangaball**, **Anichin**, **Otakudesu** (.fit + .blog), **lmanime**, **LayarKaca21**, **Cosplaytele**, **nhentai**, **NovelID**, and **NekoPoi** — one HTTP service that other developers can build manga readers, anime / donghua / movie players, cosplay galleries, doujinshi browsers, and novel readers against, without ever seeing the upstream URLs.\r\n\r\n- **Live demo:** \u003chttps://api.farellvpn.engineer\u003e (hosted on AWS — same binary, same endpoints)\r\n- **Repo:** \u003chttps://github.com/risqinf/apiku\u003e\r\n- **Releases:** \u003chttps://github.com/risqinf/apiku/releases\u003e (pre-built binaries for Linux x86_64 / ARM64, macOS Intel / Apple Silicon, Windows x86_64 / ARM64)\r\n- **Author:** [@risqinf](https://github.com/risqinf)\r\n- **License:** MIT\r\n- **Version:** 0.2.6 (see `Cargo.toml`)\r\n\r\n---\r\n\r\n## Highlights\r\n\r\n- **One API, many providers.** Anime (Otakudesu .fit + .blog merged), English-subbed anime/donghua (lmanime), donghua (Anichin), movies (LayarKaca21), manga/komik (Mangaball), cosplay archives (Cosplaytele), doujinshi (nhentai), Indonesian novels (NovelID), and adult anime (NekoPoi) — all behind one uniform JSON envelope.\r\n- **Opaque IDs.** Resource IDs are HMAC-SHA256-signed tokens. Consumers never see upstream URLs. The signing secret is **persisted by default**, so IDs (and the signed image URLs) stay valid across restarts.\r\n- **Image proxy.** Every cover, page and thumbnail is rewritten to a signed local proxy. Source CDNs stay hidden.\r\n- **Movie \u0026 adult-anime players, server-resolved.** LayarKaca21 exposes switchable players (P2P / TURBOVIP / CAST / HYDRAX): the P2P chain is **sniffed server-side to a proxied HLS stream** (the segment CDN rotates its host on every chunk — a private-host SSRF guard keeps it working), the rest are unwrapped to embeddable inner players. NekoPoi handles both single video posts and multi-episode series pages.\r\n- **Cosplay video → HLS, server-resolved.** Cosplaytele videos are served via an encrypted third-party embed (`cossora.stream`) that blocks plain iframes. apiku fetches the embed with the right Referer, **decrypts the real `.m3u8` URL server-side (AES-256-CBC)**, and hands the client a playable HLS stream. Only the tiny playlists are proxied — the heavy `.ts` segments stream **directly from the CDN to the client** to save bandwidth.\r\n- **Resumable library.** Favorites and browsing history are keyed by a **secret-independent stable content key** (they survive ID rotation and restarts), track reading/watching **progress**, and a history entry **resumes straight to your last episode/chapter**. A `/api/v1/resolve` endpoint re-signs a known provider URL to self-heal any stale saved ID.\r\n- **High-precision search.** Cosplaytele's loose WordPress search and its recommendation carousels are stripped out, then results are relevance-filtered. Cosplayer names and doujin tags are **clickable** and jump to a filtered search; nhentai surfaces cumulative `[parody] [tag]` suggestions.\r\n- **Browser fingerprint rotation.** Outbound requests pick a coherent identity (Windows/Chrome, macOS/Safari, Android/Chrome, iPhone/Safari, Linux/Firefox, ...) per upstream URL — coupled with proper Sec-CH-UA, Sec-Fetch-* and Referer spoofing so origin checks and hotlink protection see a real browser visiting the source site.\r\n- **Adaptive runtime.** CPU and RAM are detected at startup; tokio threads, HTTP concurrency, and cache sizes are tuned automatically.\r\n- **Benchmarked.** Criterion micro-benchmarks for the per-request hot paths (opaque crypto, fingerprint, HTML parse) plus end-to-end throughput — see [Benchmarks](#benchmarks) / [BENCHMARKS.md](BENCHMARKS.md).\r\n- **Client-side prefetch.** The web app warms the next likely detail/episode/chapter/page during idle time, so navigation feels instant.\r\n- **Single-flight cache.** Concurrent requests for the same URL collapse into one upstream fetch.\r\n- **Browse + search + detail + paged chapter list** for every provider.\r\n- **Consumer web app at `/`.** A dependency-free SPA streaming/reading platform with a modern UI: animated aurora + color-flow grid background, home rows, per-provider browse with feed filters, search with per-source filter chips, anime/donghua player with server switching, a movie player with a server switcher, manga/doujin reader with fullscreen, novel text reader, cosplay galleries with inline HLS video, and per-detail recommendations. Manga detail pages **group chapters by language** with one-tap language tabs that persist across pagination. Includes a responsive navbar (desktop bar + mobile drawer with real-time toggle switches), light/dark theme toggle, an in-app **API Docs** page, an inline **API Explorer** with copy-ready multi-language code samples, and an **18+ toggle** (clear age-verification modal) that hides the adult providers (Cosplay, Doujin, Hentai) until explicitly enabled.\r\n- **Configurable branding, no recompile.** Site name, tagline, logo, footer, ad slots, and SEO/ad-network verification snippets are driven by a `[web]` block in `config.toml` and/or environment variables — see [Branding \u0026 customization](#branding--customization). Drop a `logo.*` into `public/` and it's auto-detected.\r\n- **Developer API console at `/tester`.** Live request playground, multi-language code examples, full reference, security notes.\r\n\r\n---\r\n\r\n## Table of contents\r\n\r\n1. [Quick start](#quick-start)\r\n2. [CLI reference](#cli-reference)\r\n3. [HTTP API at a glance](#http-api-at-a-glance)\r\n4. [Browse feeds](#browse-feeds)\r\n5. [Search](#search)\r\n6. [Series and chapter pagination](#series-and-chapter-pagination)\r\n7. [Response envelope](#response-envelope)\r\n8. [Sample payloads](#sample-payloads)\r\n9. [Status codes and error codes](#status-codes-and-error-codes)\r\n10. [Opaque ID format](#opaque-id-format)\r\n11. [Image proxy](#image-proxy)\r\n12. [Browser fingerprint rotation](#browser-fingerprint-rotation)\r\n13. [Code examples](#code-examples)\r\n14. [Adaptive tuning](#adaptive-tuning)\r\n15. [Benchmarks](#benchmarks)\r\n16. [Security model](#security-model)\r\n17. [Configuration](#configuration)\r\n18. [Branding \u0026 customization](#branding--customization)\r\n19. [Deployment](#deployment)\r\n20. [Logging](#logging)\r\n21. [Project layout](#project-layout)\r\n22. [Roadmap](#roadmap)\r\n\r\n---\r\n\r\n## Quick start\r\n\r\n```bash\r\n# Build (release)\r\ncargo build --release\r\n\r\n# Run (auto-tunes for the host)\r\n./target/release/apiku serve\r\n\r\n# Custom bind + verbose logs + log to file\r\n./target/release/apiku serve --bind 0.0.0.0:8080 --log debug --log-file apiku.log\r\n```\r\n\r\nOpen `http://127.0.0.1:3000/` for the **web app** — a full streaming/reading platform (browse, search, watch donghua, read manga \u0026 novels, view cosplay galleries) with a light/dark theme toggle, an inline API Explorer, and an 18+ toggle. The developer API console lives at `http://127.0.0.1:3000/tester`. No API key required.\r\n\r\n### Try it without building\r\n\r\nThe same binary is hosted at **\u003chttps://api.farellvpn.engineer\u003e** (AWS). Every endpoint shown in this README also works there:\r\n\r\n```bash\r\ncurl 'https://api.farellvpn.engineer/api/v1/info'\r\ncurl 'https://api.farellvpn.engineer/api/v1/search?q=Martial+Universe\u0026source=novel'\r\ncurl 'https://api.farellvpn.engineer/api/v1/browse/nhentai?feed=popular-today'\r\n```\r\n\r\nThe tester website is also live at \u003chttps://api.farellvpn.engineer/\u003e.\r\n\r\n### Pre-built binaries\r\n\r\nReleases are auto-built by GitHub Actions on every `v*.*.*` tag. Download from \u003chttps://github.com/risqinf/apiku/releases\u003e for:\r\n\r\n- `x86_64-unknown-linux-gnu` (Linux glibc, generic)\r\n- `x86_64-unknown-linux-musl` (Linux static, portable)\r\n- `aarch64-unknown-linux-gnu` (Linux ARM64 — Raspberry Pi 4/5, AWS Graviton)\r\n- `x86_64-apple-darwin` (macOS Intel)\r\n- `aarch64-apple-darwin` (macOS Apple Silicon)\r\n- `x86_64-pc-windows-msvc` (Windows 64-bit)\r\n- `aarch64-pc-windows-msvc` (Windows ARM64)\r\n\r\nEach archive ships with a SHA-256 checksum; a combined `SHA256SUMS` file is also attached to the release.\r\n\r\n---\r\n\r\n## CLI reference\r\n\r\n```\r\napiku [OPTIONS] [COMMAND]\r\n\r\nCommands:\r\n  serve         Run as an HTTP API server (recommended)\r\n  scrape        Scrape one or more URLs (CLI)\r\n  batch         Read URLs from a file and scrape them all\r\n  info          Print version and adapter list\r\n\r\nGlobal options:\r\n  -c, --config \u003cPATH\u003e            Path to TOML config file (default: config.toml)\r\n      --log \u003cLEVEL\u003e              error|warn|info|debug|trace (default: info)\r\n      --log-format \u003cFMT\u003e         pretty|json|compact (default: pretty)\r\n      --log-file \u003cPATH\u003e          Tee logs to a file as well\r\n      --concurrency \u003cN\u003e          Override engine concurrency (1-100)\r\n      --timeout \u003cSECONDS\u003e        Override request timeout (1-300)\r\n      --rate-limit \u003cMS\u003e          Override rate-limit delay (100-60000)\r\n      --max-retries \u003cN\u003e          Override max retry attempts\r\n  -H, --header 'Name: Value'     Add custom HTTP header (repeatable)\r\n      --user-agent \u003cSTRING\u003e      Override User-Agent\r\n      --referer \u003cURL\u003e            Override Referer\r\n      --no-deep                  Skip deep page extraction\r\n\r\nCLI mode only:\r\n  -u, --url \u003cURL\u003e                Single target URL\r\n      --urls \u003cURL\u003e...            Multiple target URLs\r\n  -o, --output \u003cPATH\u003e            Output file (use '-' for stdout)\r\n      --stdout                   Write JSON to stdout\r\n      --indent \u003c0-8\u003e             JSON indentation (0 = compact)\r\n      --flat                     Bare content object instead of full envelope\r\n      --clean \u003cMODE\u003e             none|clean|minimal\r\n      --summary                  Print summary table to stderr\r\n\r\nserve options:\r\n      --bind \u003cADDR\u003e              Bind address (default: 127.0.0.1:3000)\r\n```\r\n\r\n---\r\n\r\n## HTTP API at a glance\r\n\r\nBase URL: `http://127.0.0.1:3000` (local) — base path: `/api/v1`.\r\n\r\n| Method | Path | Description |\r\n|---|---|---|\r\n| `GET` | `/api/v1/health` | Liveness probe |\r\n| `GET` | `/api/v1/info` | Server info, system tuning, providers, endpoints |\r\n| `GET` | `/api/v1/search?q=...\u0026source=...\u0026page=N` | Cross-provider search |\r\n| `GET` | `/api/v1/browse/{provider}?feed=...\u0026page=N\u0026size=N` | Provider home / popular / latest feed |\r\n| `GET` | `/api/v1/manga/{id}?page=N\u0026size=N` | Manga series detail (Mangaball) — chapter list paginated |\r\n| `GET` | `/api/v1/manga/chapter/{id}` | Manga chapter pages |\r\n| `GET` | `/api/v1/donghua/{id}?page=N\u0026size=N` | Donghua series detail (Anichin) — episode list paginated |\r\n| `GET` | `/api/v1/donghua/episode/{id}` | Donghua episode (servers + downloads) |\r\n| `GET` | `/api/v1/anime/{id}` | Anime series detail (Otakudesu .fit/.blog) — full metadata + episode list |\r\n| `GET` | `/api/v1/anime/episode/{id}` | Anime episode — quality-grouped streaming mirrors + downloads |\r\n| `GET` | `/api/v1/anime-stream?id=...` | Resolve an anime mirror token into a playable embed URL |\r\n| `GET` | `/api/v1/lmanime/{id}` | English-subbed anime/donghua series detail (lmanime) |\r\n| `GET` | `/api/v1/lmanime/episode/{id}` | lmanime episode — streaming mirrors + downloads |\r\n| `GET` | `/api/v1/lmanime-stream?id=...` | Resolve an lmanime mirror token into a playable embed URL |\r\n| `GET` | `/api/v1/movie/{id}` | Movie detail (LayarKaca21) — switchable servers + related movies |\r\n| `GET` | `/api/v1/movie-stream/{id}?server=...` | Resolve a movie server: proxied HLS (P2P) or an embeddable inner player |\r\n| `GET` | `/api/v1/cosplay/{id}` | Cosplay post (gallery + resolved video + downloads) |\r\n| `GET` | `/api/v1/cosplay-video?p=...\u0026s=...` | Resolve a Cosplaytele embed into a playable HLS stream URL |\r\n| `GET` | `/api/v1/novel/{id}?page=N\u0026size=N` | Novel series detail (NovelID) — chapter list paginated, supports upstream-paginated novels with thousands of chapters |\r\n| `GET` | `/api/v1/novel/chapter/{id}` | Novel chapter (text body, plus prev/next IDs) |\r\n| `GET` | `/api/v1/nhentai/{id}` | nhentai gallery (browser-fingerprint spoofed) |\r\n| `GET` | `/api/v1/nhentai/chapter/{id}` | nhentai gallery as a chapter (proxied page list) |\r\n| `GET` | `/api/v1/nekopoi/{id}` | NekoPoi post (18+): streaming servers + downloads, or a series episode list |\r\n| `GET` | `/api/v1/resolve?source=...\u0026kind=...\u0026u=...` | Re-sign a known provider URL into a fresh opaque ID (saved-item self-heal) |\r\n| `GET` | `/img?p={payload}\u0026s={signature}` | Signed image proxy |\r\n| `GET` | `/hls?p={payload}\u0026s={signature}` | HLS playlist + segment proxy |\r\n\r\nEvery response carries a generated `X-Request-Id` header echoed in `meta.request_id`.\r\n\r\nProviders are: `mangaball` | `anichin` | `anime` (otakudesu) | `lmanime` | `movie` (lk21) | `cosplaytele` | `nhentai` | `novelid` | `nekopoi`.\r\n\r\n---\r\n\r\n## Browse feeds\r\n\r\n`GET /api/v1/browse/{provider}?feed={feed}\u0026page={n}\u0026size={N}` surfaces home / popular / latest content for any provider, with the same envelope shape as `/search`.\r\n\r\n| Provider | Feed values |\r\n|---|---|\r\n| `mangaball` | `home` (featured), `popular`, `latest`, `recommend` (page-sliced from a single API response, `size` defaults to 30, max 60) |\r\n| `anichin` | `home` (= latest update), `popular`, `rating`, `title` (A-Z), `latest-added` |\r\n| `anime` (otakudesu) | `ongoing`, `complete`, or any genre slug — merges otakudesu.fit + .blog and dedups the same series listed under different romanizations |\r\n| `lmanime` | `ongoing`, `all` (A-Z), or any genre slug (`action`, `fantasy`, `romance`, `isekai`, `cultivation`, ...) |\r\n| `movie` (lk21) | `populer`, `latest`, `rating`, `release` (by year), `nontondrama` (series), or any genre slug (`action`, `drama`, `horror`, ...) |\r\n| `cosplaytele` | `home` (latest), or any category slug (e.g. `genshin-impact`, `azur-lane`) |\r\n| `nhentai` | `home` (recent), `popular-today`, `popular-week`, `popular` (all-time) |\r\n| `novelid` | `home` (semua), `popular` (alias of `tamat`), or any genre slug: `novel-translate`, `fantasi`, `romantis`, `religi`, `motivasi`, `horror`, `aksi`, `komedi`, `sastra`, `novel-anak` |\r\n| `nekopoi` (18+) | `latest`, `hentai`, `3d`, `2d`, `jav`, `jav-cosplay`, or any `category:\u003cslug\u003e` |\r\n\r\nPagination is page-based: `?page=2`, `?page=3`, ...\r\n\r\n```bash\r\n# Today's popular nhentai galleries\r\ncurl 'http://127.0.0.1:3000/api/v1/browse/nhentai?feed=popular-today'\r\n\r\n# Anichin most popular donghua, page 2\r\ncurl 'http://127.0.0.1:3000/api/v1/browse/anichin?feed=popular\u0026page=2'\r\n\r\n# Cosplaytele latest posts\r\ncurl 'http://127.0.0.1:3000/api/v1/browse/cosplaytele?feed=home'\r\n\r\n# NovelID Romantis genre\r\ncurl 'http://127.0.0.1:3000/api/v1/browse/novelid?feed=romantis'\r\n\r\n# Mangaball popular, with explicit page size\r\ncurl 'http://127.0.0.1:3000/api/v1/browse/mangaball?feed=popular\u0026size=30\u0026page=1'\r\n```\r\n\r\n---\r\n\r\n## Search\r\n\r\n`GET /api/v1/search?q={query}\u0026source={source}\u0026page={n}`\r\n\r\n- `source`: `all` (default) | `manga` | `donghua` | `anime` | `lmanime` | `movie` | `cosplay` | `nhentai` | `novel` | `nekopoi`\r\n- `page`: 1-based, applies to providers that support upstream pagination\r\n- nhentai accepts inline `[tag]` syntax — e.g. `?q=Genshin+Impact+%5Bfull+color%5D\u0026source=nhentai`\r\n- results are relevance-ranked (closest title matches first) across all providers\r\n- `nekopoi` (18+) is excluded from the default `all` search and only returned when requested explicitly\r\n\r\n```bash\r\ncurl 'http://127.0.0.1:3000/api/v1/search?q=one+piece\u0026source=manga'\r\ncurl 'http://127.0.0.1:3000/api/v1/search?q=peerless\u0026source=donghua'\r\ncurl 'http://127.0.0.1:3000/api/v1/search?q=Martial+Universe\u0026source=novel'\r\n```\r\n\r\nSort options for nhentai (popular today / week / all-time) live under `/browse/nhentai` instead of `/search`, since they apply to discovery rather than keyword queries.\r\n\r\n---\r\n\r\n## Series and chapter pagination\r\n\r\nSeries detail endpoints (`/manga/{id}`, `/donghua/{id}`, `/novel/{id}`) accept `page` and `size` query parameters to paginate the chapter / episode list inside the response. The series metadata (title, cover, synopsis, ...) is unchanged on every page; only the `chapters[]` / `episodes[]` array contains the requested window.\r\n\r\nThe response includes pagination metadata so a client can build prev/next navigation without a second round-trip:\r\n\r\n| Field | Meaning |\r\n|---|---|\r\n| `chapter_count` / `episode_count` | Total count across all pages |\r\n| `chapter_page` / `episode_page` | Current page (1-indexed) |\r\n| `chapter_page_size` / `episode_page_size` | Items per page |\r\n| `chapter_total_pages` / `episode_total_pages` | Total pages |\r\n\r\n```bash\r\n# Page 3 of a novel's chapters, 20 per page\r\ncurl 'http://127.0.0.1:3000/api/v1/novel/\u003cid\u003e?page=3\u0026size=20'\r\n```\r\n\r\n### Upstream-paginated novels (NovelID)\r\n\r\nNovelID itself paginates the chapter list at the source — each `?page=N` upstream returns ~30 chapters out of potentially thousands. `apiku` handles this transparently:\r\n\r\n- Page 1 always fetches the canonical URL (gives metadata + first 30 chapters) and the **last** upstream page in parallel, so `chapter_count` is exactly accurate even on the first request.\r\n- Any API page request computes which upstream pages cover its window, fetches them concurrently (via the per-URL single-flight cache), and slices by chapter number.\r\n- Subsequent pages are sub-millisecond because every upstream page is cached.\r\n\r\nTested against *Martial Universe* (1,309 chapters, 44 upstream pages):\r\n\r\n| Request | Result |\r\n|---|---|\r\n| `?page=1\u0026size=30` | chapters 1-30, `chapter_count: 1309`, `chapter_total_pages: 44` |\r\n| `?page=2\u0026size=30` | chapters 31-60 |\r\n| `?page=20\u0026size=30` | chapters 571-600 |\r\n| `?page=44\u0026size=30` | chapters 1291-1309 (last page, 19 items) |\r\n| `?page=1\u0026size=50` | chapters 1-50, `chapter_total_pages: 27` (spans 2 upstream pages) |\r\n\r\n---\r\n\r\n## Response envelope\r\n\r\nEvery endpoint shares the same JSON envelope.\r\n\r\n### Success\r\n\r\n```json\r\n{\r\n  \"status\": 200,\r\n  \"ok\": true,\r\n  \"data\": { /* endpoint-specific payload */ },\r\n  \"meta\": {\r\n    \"took_ms\": 123,\r\n    \"cached\": false,\r\n    \"request_id\": \"1f8b2c4d-...\"\r\n  }\r\n}\r\n```\r\n\r\n### Error\r\n\r\n```json\r\n{\r\n  \"status\": 404,\r\n  \"ok\": false,\r\n  \"error\": {\r\n    \"code\": \"not_found\",\r\n    \"message\": \"Route not found: /api/v1/nope\"\r\n  },\r\n  \"meta\": {\r\n    \"took_ms\": 0,\r\n    \"cached\": false,\r\n    \"request_id\": \"...\"\r\n  }\r\n}\r\n```\r\n\r\n---\r\n\r\n## Sample payloads\r\n\r\n### `GET /api/v1/search?q=Martial+Universe\u0026source=novel`\r\n\r\n```json\r\n{\r\n  \"status\": 200, \"ok\": true,\r\n  \"data\": {\r\n    \"query\": \"Martial Universe\",\r\n    \"source\": \"novel\",\r\n    \"page\": 1,\r\n    \"total\": 1,\r\n    \"items\": [\r\n      {\r\n        \"id\": \"nvsxyz....\",\r\n        \"source\": \"novelid\",\r\n        \"kind\": \"novel\",\r\n        \"title\": \"Martial Universe (Wu Dong Qian Kun Terjemah Indo)\",\r\n        \"thumbnail\": \"/img?p=...\u0026s=...\",\r\n        \"snippet\": null,\r\n        \"tags\": [\"Novel Translate\", \"Tamat\"]\r\n      }\r\n    ]\r\n  },\r\n  \"meta\": { \"took_ms\": 320, \"cached\": false, \"request_id\": \"...\" }\r\n}\r\n```\r\n\r\n### `GET /api/v1/manga/{id}?page=1\u0026size=60`\r\n\r\n```json\r\n{\r\n  \"status\": 200, \"ok\": true,\r\n  \"data\": {\r\n    \"id\": \"mbsabc....\",\r\n    \"title\": \"Dark Mortal\",\r\n    \"description\": \"...\",\r\n    \"author\": null,\r\n    \"artist\": null,\r\n    \"genres\": [],\r\n    \"cover\": \"/img?p=...\u0026s=...\",\r\n    \"chapter_count\": 85,\r\n    \"chapter_page\": 1,\r\n    \"chapter_page_size\": 60,\r\n    \"chapter_total_pages\": 2,\r\n    \"chapters\": [\r\n      {\r\n        \"id\": \"mbiabc....\",\r\n        \"number\": 1.0,\r\n        \"title\": \"Family\",\r\n        \"translations\": [\r\n          { \"id\": \"mbixyz....\", \"language\": \"English\", \"group\": \"Articuno\",\r\n            \"date\": \"2026-02-10\", \"pages\": 71 }\r\n        ]\r\n      }\r\n    ]\r\n  },\r\n  \"meta\": { \"took_ms\": 180, \"cached\": false, \"request_id\": \"...\" }\r\n}\r\n```\r\n\r\n### `GET /api/v1/manga/chapter/{id}`\r\n\r\n```json\r\n{\r\n  \"status\": 200, \"ok\": true,\r\n  \"data\": {\r\n    \"id\": \"mbiabc....\",\r\n    \"series_title\": \"Dark Mortal Vol. 1\",\r\n    \"chapter_number\": 1.0,\r\n    \"page_count\": 71,\r\n    \"pages\": [\r\n      { \"index\": 1, \"url\": \"/img?p=...\u0026s=...\" },\r\n      { \"index\": 2, \"url\": \"/img?p=...\u0026s=...\" }\r\n    ]\r\n  },\r\n  \"meta\": { \"took_ms\": 270, \"cached\": false, \"request_id\": \"...\" }\r\n}\r\n```\r\n\r\n### `GET /api/v1/donghua/episode/{id}`\r\n\r\n```json\r\n{\r\n  \"status\": 200, \"ok\": true,\r\n  \"data\": {\r\n    \"id\": \"aciabc....\",\r\n    \"series_title\": \"Peerless Martial Spirit\",\r\n    \"series_id\": \"acsdef....\",\r\n    \"episode_number\": 440,\r\n    \"prev_id\": \"aci...\",\r\n    \"next_id\": null,\r\n    \"servers\": [\r\n      { \"label\": \"Dailymotion\", \"embed_url\": \"https://geo.dailymotion.com/...\", \"format\": \"embed\" }\r\n    ],\r\n    \"downloads\": [\r\n      {\r\n        \"quality\": \"720p\",\r\n        \"mirrors\": [\r\n          { \"name\": \"Mirrored\", \"url\": \"https://www.mirrored.to/multilinks/...\" }\r\n        ]\r\n      }\r\n    ]\r\n  },\r\n  \"meta\": { \"took_ms\": 410, \"cached\": false, \"request_id\": \"...\" }\r\n}\r\n```\r\n\r\n### `GET /api/v1/cosplay/{id}`\r\n\r\n```json\r\n{\r\n  \"status\": 200, \"ok\": true,\r\n  \"data\": {\r\n    \"id\": \"ctpghi....\",\r\n    \"title\": \"ChuChu Magic cosplay Raiden Shogun ...\",\r\n    \"cosplayer\": \"ChuChu Magic\",\r\n    \"character\": \"Raiden Shogun\",\r\n    \"series\": \"Genshin Impact\",\r\n    \"photo_count\": 23,\r\n    \"video_count\": 1,\r\n    \"categories\": [\"Cosplay Game\", \"Genshin Impact\"],\r\n    \"tags\": [\"Raiden Shogun\"],\r\n    \"published_at\": \"2026-05-25T16:16:34+08:00\",\r\n    \"cover\": \"/img?p=...\u0026s=...\",\r\n    \"images\": [\"/img?p=...\u0026s=...\", \"/img?p=...\u0026s=...\"],\r\n    \"videos\": [\"/api/v1/cosplay-video?p=...\u0026s=...\"],\r\n    \"downloads\": [\r\n      { \"name\": \"Download Telegram\", \"url\": \"https://t.me/+...\" }\r\n    ],\r\n    \"unzip_password\": \"cosplaytele\"\r\n  },\r\n  \"meta\": { \"took_ms\": 220, \"cached\": false, \"request_id\": \"...\" }\r\n}\r\n```\r\n\r\n`videos[]` entries that point at `/api/v1/cosplay-video?...` resolve to a playable HLS stream — call that endpoint to get `{ \"type\": \"hls\", \"url\": \"/hls?...\" }`, then play the `/hls` URL with hls.js (or natively on Safari). Heavy video segments stream straight from the CDN to the client; only the playlist passes through the server.\r\n\r\n### `GET /api/v1/novel/{id}?page=1\u0026size=30`\r\n\r\n```json\r\n{\r\n  \"status\": 200, \"ok\": true,\r\n  \"data\": {\r\n    \"id\": \"nvsabc....\",\r\n    \"title\": \"Martial Universe (Wu Dong Qian Kun Terjemah Indo)\",\r\n    \"author\": \"Fight007\",\r\n    \"status\": \"Tamat\",\r\n    \"genres\": [\"Romantis\"],\r\n    \"synopsis\": \"Lin Dong, dia menjalani kehidupan berat penuh dengan hinaan ...\",\r\n    \"cover\": \"/img?p=...\u0026s=...\",\r\n    \"rating\": \"8.00\",\r\n    \"chapter_count\": 1309,\r\n    \"chapter_page\": 1,\r\n    \"chapter_page_size\": 30,\r\n    \"chapter_total_pages\": 44,\r\n    \"chapters\": [\r\n      { \"id\": \"nviabc....\", \"number\": 1, \"title\": \"Lin Dong - Bagian 1\" },\r\n      { \"id\": \"nvixyz....\", \"number\": 2, \"title\": \"Tinju Penetrasi - 2\" }\r\n    ]\r\n  },\r\n  \"meta\": { \"took_ms\": 310, \"cached\": false, \"request_id\": \"...\" }\r\n}\r\n```\r\n\r\n### `GET /api/v1/novel/chapter/{id}`\r\n\r\n```json\r\n{\r\n  \"status\": 200, \"ok\": true,\r\n  \"data\": {\r\n    \"id\": \"nviabc....\",\r\n    \"series_title\": \"Martial Universe (Wu Dong Qian Kun Terjemah Indo)\",\r\n    \"series_id\": \"nvsabc....\",\r\n    \"chapter_number\": 1,\r\n    \"chapter_title\": \"Lin Dong - Bagian 1\",\r\n    \"body\": \"\\u201CWuu.\\u201D\\n\\nKetika Lin Dong mengumpulkan setiap ons kekuatan...\",\r\n    \"body_html\": \"\u003cp\u003e...\u003c/p\u003e\u003cp\u003e...\u003c/p\u003e\",\r\n    \"prev_id\": null,\r\n    \"next_id\": \"nvinext....\",\r\n    \"word_count\": 1842\r\n  },\r\n  \"meta\": { \"took_ms\": 280, \"cached\": false, \"request_id\": \"...\" }\r\n}\r\n```\r\n\r\n---\r\n\r\n## Status codes and error codes\r\n\r\n### HTTP status codes\r\n\r\n| Code | Meaning |\r\n|---|---|\r\n| `200 OK` | Request succeeded |\r\n| `400 Bad Request` | Malformed query, invalid opaque ID, query too long |\r\n| `403 Forbidden` | Bad image-proxy signature, host not on allowlist |\r\n| `404 Not Found` | Unknown route |\r\n| `502 Bad Gateway` | Upstream provider returned an error or unparseable content |\r\n\r\n### Error codes (in `error.code`)\r\n\r\n| Code | When |\r\n|---|---|\r\n| `missing_query` | `/api/v1/search` called without `q` |\r\n| `query_too_long` | `q` longer than 200 chars |\r\n| `invalid_id` | Opaque ID malformed or has bad signature |\r\n| `wrong_source` | ID belongs to a different provider than the endpoint |\r\n| `wrong_kind` | Scraped page does not match endpoint expectation |\r\n| `scrape_failed` | Upstream scrape returned no content |\r\n| `upstream_error` | Network or 5xx from upstream |\r\n| `upstream_status` | Upstream returned a non-success HTTP status |\r\n| `bad_signature` | Image proxy signature failed verification |\r\n| `host_not_allowed` | Image URL host not on the proxy allowlist |\r\n| `bad_payload` | Image proxy payload is not valid base64url / utf-8 |\r\n| `not_found` | Unknown route (404) |\r\n\r\n---\r\n\r\n## Opaque ID format\r\n\r\nEvery resource ID returned by the API is HMAC-SHA256 signed.\r\n\r\n```\r\n\u003csource 2 chars\u003e\u003ckind 1 char\u003e\u003cnonce 3 chars\u003e.\u003cbase64url-payload\u003e.\u003cbase64url-mac-16-bytes\u003e\r\n```\r\n\r\nExample: `mbsijk.aHR0cHM6Ly9tYW5nYWJhbGwubmV0L3RpdGxlLWRldGFpbC8.J9k1Nz5pQq3v7L2HjT7M5w`\r\n\r\n- **source** — `mb` (mangaball), `ac` (anichin), `ct` (cosplaytele), `nh` (nhentai), `nv` (novelid)\r\n- **kind** — `s` (series), `i` (item: chapter or episode), `p` (post)\r\n- **nonce** — 3 random base32-ish chars to prevent identical IDs for the same URL\r\n- **payload** — base64url-encoded raw URL\r\n- **mac** — first 16 bytes of `HMAC-SHA256(secret, header || \".\" || payload)`, base64url-encoded (~22 chars, 128-bit security)\r\n\r\nConstant-time comparison rejects any tampering. By default the server **persists an auto-generated secret** (in `$APIKU_DATA_DIR/secret`, else `$HOME/.apiku/secret`) so opaque IDs — and the signed image-proxy URLs — stay valid across restarts, keeping saved favorites / history and their thumbnails working. Set `APIKU_SECRET` to override (recommended for multi-instance deployments so every instance shares one secret).\r\n\r\n---\r\n\r\n## Image proxy\r\n\r\nAll cover, thumbnail, gallery and page-image URLs in API responses are rewritten to:\r\n\r\n```\r\n/img?p={base64url-encoded-url}\u0026s={hmac-signature}\r\n```\r\n\r\nFour layers of defence:\r\n\r\n1. **HMAC-SHA256 signature** — verified server-side per request, in constant time.\r\n2. **Host allowlist** — even with a forged signature the proxy will only fetch from these hosts:\r\n   - `*.poke-black-and-white.net`, `*.red-and-blue.net`, `*.pokemon-gold-silver.net`, `*.pokemon-ruby-sapphire.net` (Mangaball CDNs)\r\n   - `anichin.cafe`, `anichin.care`, `anichin.cloud`, `i0..i3.wp.com` (Anichin)\r\n   - `cosplaytele.com`, `*.cosplaytele.com`\r\n   - `nhentai.net`, `nhentai.xxx`, `nhentai.to`, `i1..i4.nhentai.net`, `t1..t4.nhentai.net`\r\n   - `novelid.org` and the wp.com mirror it uses\r\n   - `otakudesu.blog` and its mirror domains (anime covers)\r\n3. **Referer spoofing** — outgoing requests carry the source domain as `Referer` to bypass hotlink protection.\r\n4. **Browser fingerprint rotation** — every outgoing image request applies a coherent browser identity (User-Agent, Sec-CH-UA, Sec-Fetch-* tailored for an `\u003cimg\u003e` request, narrow image-Accept) picked deterministically per upstream URL.\r\n\r\nResponses set `Cache-Control: public, max-age=86400, immutable` so browsers cache aggressively.\r\n\r\n---\r\n\r\n## Browser fingerprint rotation\r\n\r\napiku ships a curated catalogue of internally consistent browser identities:\r\n\r\n- Windows / Chrome 121\r\n- Windows / Edge 121\r\n- macOS / Safari 17\r\n- macOS / Chrome 121\r\n- Linux / Firefox 122\r\n- Linux / Chrome 121\r\n- Android / Chrome 121\r\n- Android / Samsung Internet 23\r\n- iOS / Safari 17 (iPhone)\r\n- iPadOS / Safari 17\r\n\r\nEach entry sets `User-Agent`, `Accept`, `Accept-Language`, `Accept-Encoding`, `Sec-CH-UA*` and `Sec-Fetch-*` as a unit so the request is internally coherent. Selection is deterministic: a SHA-256 of the upstream URL picks the index, so the same URL always uses the same identity (defeats simple rotation-detection heuristics).\r\n\r\nUsed wherever apiku makes outbound requests to fingerprint-sensitive hosts: nhentai's JSON API and CDN, the image proxy, and the cross-provider search calls.\r\n\r\n---\r\n\r\n## Code examples\r\n\r\nAll examples make the same request: search \"one piece\" on Mangaball.\r\n\r\n### cURL\r\n\r\n```bash\r\n# Local\r\ncurl 'http://127.0.0.1:3000/api/v1/search?q=one+piece\u0026source=manga'\r\n\r\n# Live demo (same shape, hosted on AWS)\r\ncurl 'https://api.farellvpn.engineer/api/v1/search?q=one+piece\u0026source=manga' | jq .\r\n```\r\n\r\n### JavaScript (browser / Node 18+)\r\n\r\n```js\r\nconst res = await fetch('http://127.0.0.1:3000/api/v1/search?q=one piece\u0026source=manga');\r\nconst json = await res.json();\r\n\r\nif (!json.ok) {\r\n  throw new Error(`${json.error.code}: ${json.error.message}`);\r\n}\r\n\r\nconsole.log(`Found ${json.data.total} (${json.meta.took_ms}ms)`);\r\nfor (const item of json.data.items) {\r\n  console.log(`- [${item.source}] ${item.title}`);\r\n}\r\n```\r\n\r\n### TypeScript\r\n\r\n```ts\r\ninterface Envelope\u003cT\u003e {\r\n  status: number;\r\n  ok: boolean;\r\n  data: T;\r\n  meta: { took_ms: number; cached: boolean; request_id: string };\r\n}\r\ninterface ApiErr {\r\n  status: number;\r\n  ok: false;\r\n  error: { code: string; message: string };\r\n  meta: { took_ms: number; request_id: string };\r\n}\r\ntype Response\u003cT\u003e = Envelope\u003cT\u003e | ApiErr;\r\n\r\ninterface SearchData {\r\n  query: string;\r\n  source: string;\r\n  page: number;\r\n  total: number;\r\n  items: Array\u003c{\r\n    id: string;\r\n    source: 'mangaball' | 'anichin' | 'cosplaytele' | 'nhentai' | 'novelid';\r\n    kind: 'manga' | 'donghua' | 'cosplay' | 'doujin' | 'novel';\r\n    title: string;\r\n    thumbnail?: string;\r\n    snippet?: string;\r\n    tags: string[];\r\n  }\u003e;\r\n}\r\n\r\nconst res = await fetch('http://127.0.0.1:3000/api/v1/search?q=one+piece\u0026source=manga');\r\nconst json: Response\u003cSearchData\u003e = await res.json();\r\nif (!json.ok) throw new Error(`${json.error.code}: ${json.error.message}`);\r\nconsole.log(json.data.total, json.meta.took_ms);\r\n```\r\n\r\n### Python\r\n\r\n```python\r\nimport requests\r\n\r\nBASE = 'http://127.0.0.1:3000'\r\n\r\ndef api_get(path, params=None):\r\n    r = requests.get(f'{BASE}{path}', params=params)\r\n    r.raise_for_status()\r\n    body = r.json()\r\n    if not body.get('ok'):\r\n        raise RuntimeError(f\"{body['error']['code']}: {body['error']['message']}\")\r\n    return body['data']\r\n\r\n# Search\r\nresults = api_get('/api/v1/search', {'q': 'one piece', 'source': 'manga'})\r\nprint(f\"Found {results['total']} items\")\r\n\r\n# Detail with chapter pagination\r\nif results['items']:\r\n    series = api_get(f\"/api/v1/manga/{results['items'][0]['id']}\", {'page': 1, 'size': 30})\r\n    print(f\"{series['title']}: {series['chapter_count']} chapters across {series['chapter_total_pages']} pages\")\r\n```\r\n\r\n### PHP\r\n\r\n```php\r\n\u003c?php\r\nconst BASE = 'http://127.0.0.1:3000';\r\n\r\nfunction api_get(string $path): array {\r\n    $ch = curl_init(BASE . $path);\r\n    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);\r\n    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json']);\r\n    $body = curl_exec($ch);\r\n    curl_close($ch);\r\n\r\n    $json = json_decode($body, true);\r\n    if (!$json['ok']) {\r\n        throw new RuntimeException(\"{$json['error']['code']}: {$json['error']['message']}\");\r\n    }\r\n    return $json['data'];\r\n}\r\n\r\n$search = api_get('/api/v1/search?' . http_build_query([\r\n    'q' =\u003e 'one piece', 'source' =\u003e 'manga',\r\n]));\r\necho \"Found {$search['total']} results\\n\";\r\n```\r\n\r\n### Go\r\n\r\n```go\r\npackage main\r\n\r\nimport (\r\n    \"encoding/json\"\r\n    \"fmt\"\r\n    \"io\"\r\n    \"net/http\"\r\n    \"net/url\"\r\n)\r\n\r\nconst Base = \"http://127.0.0.1:3000\"\r\n\r\ntype Envelope[T any] struct {\r\n    Status int  `json:\"status\"`\r\n    Ok     bool `json:\"ok\"`\r\n    Data   T    `json:\"data,omitempty\"`\r\n    Error  *struct {\r\n        Code, Message string\r\n    } `json:\"error,omitempty\"`\r\n    Meta struct {\r\n        TookMs    int    `json:\"took_ms\"`\r\n        Cached    bool   `json:\"cached\"`\r\n        RequestID string `json:\"request_id\"`\r\n    } `json:\"meta\"`\r\n}\r\n\r\ntype SearchData struct {\r\n    Total int `json:\"total\"`\r\n    Items []struct {\r\n        ID, Source, Kind, Title string\r\n    } `json:\"items\"`\r\n}\r\n\r\nfunc apiGet[T any](path string) (*Envelope[T], error) {\r\n    resp, err := http.Get(Base + path)\r\n    if err != nil { return nil, err }\r\n    defer resp.Body.Close()\r\n    body, _ := io.ReadAll(resp.Body)\r\n    var env Envelope[T]\r\n    if err := json.Unmarshal(body, \u0026env); err != nil { return nil, err }\r\n    if !env.Ok { return nil, fmt.Errorf(\"%s: %s\", env.Error.Code, env.Error.Message) }\r\n    return \u0026env, nil\r\n}\r\n\r\nfunc main() {\r\n    qs := url.Values{\"q\": {\"one piece\"}, \"source\": {\"manga\"}}\r\n    res, err := apiGet[SearchData](\"/api/v1/search?\" + qs.Encode())\r\n    if err != nil { panic(err) }\r\n    fmt.Printf(\"Found %d (took %dms)\\n\", res.Data.Total, res.Meta.TookMs)\r\n}\r\n```\r\n\r\n### C++\r\n\r\n```cpp\r\n// Requires libcurl + nlohmann::json\r\n#include \u003ciostream\u003e\r\n#include \u003cstring\u003e\r\n#include \u003ccurl/curl.h\u003e\r\n#include \u003cnlohmann/json.hpp\u003e\r\n\r\nusing json = nlohmann::json;\r\n\r\nstatic size_t cb(void* p, size_t s, size_t n, std::string* out) {\r\n    out-\u003eappend((char*)p, s * n); return s * n;\r\n}\r\n\r\njson api_get(const std::string\u0026 path) {\r\n    CURL* curl = curl_easy_init();\r\n    std::string body;\r\n    curl_easy_setopt(curl, CURLOPT_URL, (\"http://127.0.0.1:3000\" + path).c_str());\r\n    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, cb);\r\n    curl_easy_setopt(curl, CURLOPT_WRITEDATA, \u0026body);\r\n    curl_easy_perform(curl);\r\n    curl_easy_cleanup(curl);\r\n\r\n    auto j = json::parse(body);\r\n    if (!j[\"ok\"].get\u003cbool\u003e())\r\n        throw std::runtime_error(j[\"error\"][\"code\"].get\u003cstd::string\u003e());\r\n    return j[\"data\"];\r\n}\r\n\r\nint main() {\r\n    auto data = api_get(\"/api/v1/search?q=one+piece\u0026source=manga\");\r\n    std::cout \u003c\u003c \"Total: \" \u003c\u003c data[\"total\"] \u003c\u003c \"\\n\";\r\n}\r\n```\r\n\r\n### Rust\r\n\r\n```rust\r\n// reqwest = { version = \"0.12\", features = [\"json\"] }\r\n// serde = { version = \"1\", features = [\"derive\"] }\r\n// tokio = { version = \"1\", features = [\"full\"] }\r\nuse serde::Deserialize;\r\n\r\n#[derive(Deserialize)] struct Envelope\u003cT\u003e { ok: bool, data: Option\u003cT\u003e, error: Option\u003cApiError\u003e, meta: Meta }\r\n#[derive(Deserialize)] struct ApiError { code: String, message: String }\r\n#[derive(Deserialize)] struct Meta { took_ms: u64, request_id: String, cached: bool }\r\n#[derive(Deserialize)] struct SearchData { total: usize, items: Vec\u003cSearchItem\u003e }\r\n#[derive(Deserialize)] struct SearchItem { id: String, source: String, title: String }\r\n\r\n#[tokio::main]\r\nasync fn main() -\u003e Result\u003c(), Box\u003cdyn std::error::Error\u003e\u003e {\r\n    let resp: Envelope\u003cSearchData\u003e = reqwest::get(\r\n        \"http://127.0.0.1:3000/api/v1/search?q=one+piece\u0026source=manga\"\r\n    ).await?.json().await?;\r\n\r\n    if !resp.ok { let e = resp.error.unwrap(); return Err(format!(\"{}: {}\", e.code, e.message).into()); }\r\n    let data = resp.data.unwrap();\r\n    println!(\"Total: {} ({}ms)\", data.total, resp.meta.took_ms);\r\n    Ok(())\r\n}\r\n```\r\n\r\n---\r\n\r\n## Adaptive tuning\r\n\r\nOn startup, the server detects CPU cores and RAM and chooses values from this table:\r\n\r\n| Parameter | Logic | Min | Max |\r\n|---|---|---|---|\r\n| Tokio worker threads | `= cores` | 2 | 32 |\r\n| HTTP concurrency | `= cores * 4` | 8 | 100 |\r\n| TCP pool / host | `= cores * 8` | 16 | 256 |\r\n| Scrape cache capacity | scaled with RAM | 500 | 50,000 |\r\n| Search cache capacity | `= scrape_cache / 4` | - | - |\r\n\r\nProfile tier:\r\n\r\n- **minimal** (≤ 1 GB RAM)\r\n- **small** (1-4 GB)\r\n- **standard** (4-16 GB)\r\n- **production** (\u003e 16 GB)\r\n\r\nSample startup log:\r\n\r\n```\r\nINFO  apiku                    system detected cores=8 ram_mib=7455 threads=8 profile=\"standard (workstation / 8-16GB)\"\r\nINFO  apiku::server            server listening addr=127.0.0.1:3000\r\n```\r\n\r\n---\r\n\r\n## Benchmarks\r\n\r\nHot-path micro-benchmarks and end-to-end HTTP throughput. Full methodology,\r\ntables, and reproduction commands are in **[BENCHMARKS.md](BENCHMARKS.md)**.\r\n\r\nMeasured on an AMD Ryzen 3 7320U (4c/8t), 7 GiB RAM, `--release`:\r\n\r\n| Path | Result |\r\n|---|---:|\r\n| Opaque ID `encode` / `decode` (HMAC-SHA256) | 952 ns / 570 ns |\r\n| Image-URL `sign` / `verify` | 307 ns / 325 ns |\r\n| Browser fingerprint pick (`for_url`) | 110 ns |\r\n| HTML parse + extract (30-card listing) | 444 µs |\r\n| `GET /api/v1/health` throughput (c=50) | **15,270 req/s** (p50 2.8 ms) |\r\n| `GET /img` warm cache hit (c=50) | **24,647 req/s**, 1.1 GiB/s (p50 1.7 ms) |\r\n\r\nThe crypto envelope is sub-microsecond per item, so scrape latency is\r\nnetwork-bound rather than CPU-bound; the in-memory image cache turns repeat\r\ncover/thumbnail loads (~124 ms cold here, up to seconds on slow upstreams) into\r\n~1.7 ms warm hits.\r\n\r\nReproduce with `cargo bench` (Criterion) and `oha` (see\r\n[BENCHMARKS.md](BENCHMARKS.md)).\r\n\r\n---\r\n\r\n## Security model\r\n\r\n### Opaque IDs\r\n\r\n- 128-bit MAC truncated from HMAC-SHA256\r\n- Constant-time comparison\r\n- 3-character nonce in the header\r\n- Tamper-detected with `invalid_id` 400 errors\r\n\r\n### Image proxy\r\n\r\n- HMAC-SHA256 96-bit signature, verified per request\r\n- Host allowlist (see [Image proxy](#image-proxy))\r\n- `Referer` spoofed to source domain to defeat hotlink protection\r\n- Browser fingerprint applied per request\r\n- Long-lived `Cache-Control` for browser caching\r\n\r\n### Rate limiting\r\n\r\n- Per-domain delay (default 400 ms in server mode)\r\n- Single-flight cache: simultaneous requests for the same URL collapse into a single upstream fetch\r\n- Scrape result TTL: 10 minutes — Search TTL: 5 minutes\r\n\r\n### Hardening recommendations for production\r\n\r\n- Set `APIKU_SECRET=\u003clong-random\u003e` so every instance shares one signing secret (IDs are stable across restarts by default via a persisted secret, but multiple instances each generate their own unless this is set)\r\n- Run behind a reverse proxy (nginx / caddy) for TLS, IP rate limiting, access logs\r\n- Restrict `Access-Control-Allow-Origin` instead of `*`\r\n- Add HTTP basic auth or API keys at the reverse proxy layer if exposed publicly\r\n\r\n---\r\n\r\n## Configuration\r\n\r\nOptional `config.toml`:\r\n\r\n```toml\r\n# Default headers applied to every outbound request\r\n[headers]\r\n\"User-Agent\" = \"Mozilla/5.0 (compatible; apiku/0.2)\"\r\n\r\n# Per-domain rate limits in milliseconds\r\n[rate_limits]\r\n\"mangaball.net\"   = 400\r\n\"anichin.cafe\"    = 800\r\n\"cosplaytele.com\" = 600\r\n\"nhentai.net\"     = 600\r\n\"novelid.org\"     = 400\r\n\r\n# Per-site overrides (referer, user-agent, headers)\r\n[sites.\"mangaball.net\"]\r\nreferer = \"https://mangaball.net/\"\r\n```\r\n\r\nAll values are optional and have sensible defaults; the file is not required.\r\n\r\n---\r\n\r\n## Branding \u0026 customization\r\n\r\nThe consumer web app served at `/` can be fully rebranded and monetized **without recompiling** — everything is read at server start and injected into the SPA. Configure it in the `[web]` block of `config.toml`:\r\n\r\n```toml\r\n[web]\r\n# Shown in the header, drawer, and browser tab title.\r\nsite_name = \"NontonKu\"\r\n\r\n# Home hero tagline.\r\ntagline = \"Streaming donghua, baca komik \u0026 novel, galeri cosplay - semua dalam satu platform.\"\r\n\r\n# Custom logo. Leave empty to auto-detect (see below) or use the built-in mark.\r\n# Absolute URL or a root path served from `static_dir` (e.g. \"/logo.svg\").\r\nlogo_url = \"\"\r\n\r\n# Footer HTML. Empty -\u003e a minimal \"\u003csite_name\u003e (c) \u003cyear\u003e\" line.\r\nfooter_html = \"\"\r\n\r\n# Raw HTML injected into \u003chead\u003e (SEO / ad-network verification, analytics).\r\nhead_html = '\u003cmeta name=\"google-site-verification\" content=\"XXXX\"\u003e'\r\n\r\n# Raw HTML injected just before \u003c/body\u003e (deferred scripts).\r\nbody_html = \"\"\r\n\r\n# Directory served at the site root for verification files, ads.txt,\r\n# sitemap.xml, robots.txt, favicons, and custom logos.\r\nstatic_dir = \"public\"\r\n\r\n# Named ad slots rendered at fixed positions. Known slots: home, browse, reader.\r\n[web.ads]\r\nhome   = '\u003cins class=\"adsbygoogle\" ...\u003e\u003c/ins\u003e'\r\nreader = ''\r\n```\r\n\r\n### Changing the site name (no rebuild)\r\n\r\nTwo ways, both outside the binary:\r\n\r\n- **Config file:** set `site_name` in `[web]` and restart `apiku serve`.\r\n- **Environment variables** (win over the config file — handy for Docker / systemd):\r\n\r\n  | Variable | Overrides |\r\n  |---|---|\r\n  | `APIKU_SITE_NAME` | site name |\r\n  | `APIKU_TAGLINE` | hero tagline |\r\n  | `APIKU_LOGO_URL` | logo |\r\n  | `APIKU_STATIC_DIR` | static directory |\r\n\r\n  ```bash\r\n  APIKU_SITE_NAME=\"NontonKu\" apiku serve\r\n  ```\r\n\r\n### Custom logo\r\n\r\n- **Auto-detect (easiest):** drop a `logo.*` (or `favicon.*`) file into `public/` — `logo.svg`, `logo.png`, `logo.webp`, `logo.jpg`, `logo.gif`, `logo.ico` are detected in that priority order, no config needed. Restart and it appears in the header, drawer, and as the favicon.\r\n- **Manual:** set `logo_url = \"/brand.png\"` (file in `public/`) or an absolute URL. Manual value always wins over auto-detect.\r\n\r\n### Static files \u0026 verification (`public/`)\r\n\r\nAnything in `static_dir` (default `public/`) is served at the site root for a single path segment — e.g. `public/google1234.html` → `https://your-domain/google1234.html`. Use it for `ads.txt` / `app-ads.txt`, search-engine verification files, `robots.txt`, `sitemap.xml`, favicons, and logos. Path traversal is rejected, and API / SPA / proxy routes always take precedence. See [`public/README.md`](public/README.md) for the full guide.\r\n\r\n---\r\n\r\n## Deployment\r\n\r\nThe reference deployment runs on AWS at \u003chttps://api.farellvpn.engineer\u003e. Architecture:\r\n\r\n- **Compute:** EC2 instance (`apiku serve --bind 127.0.0.1:3000` behind systemd)\r\n- **Reverse proxy:** nginx terminates TLS, forwards `/` and `/api/v1/*` and `/img` to the local apiku\r\n- **TLS:** Let's Encrypt via Certbot, auto-renewed\r\n- **Domain:** `api.farellvpn.engineer` → AWS via Route 53 / external DNS provider\r\n- **Logs:** systemd journald (`apiku --log-format json --log-file /var/log/apiku/apiku.log`)\r\n- **Image proxy:** the host allowlist already covers every upstream CDN we use, so no extra firewall rules are needed\r\n\r\n### Suggested systemd unit\r\n\r\n```ini\r\n# /etc/systemd/system/apiku.service\r\n[Unit]\r\nDescription=apiku - RESTful scraping API\r\nAfter=network.target\r\n\r\n[Service]\r\nUser=apiku\r\nGroup=apiku\r\nEnvironment=APIKU_SECRET=\u003clong-random-secret\u003e\r\nExecStart=/usr/local/bin/apiku serve --bind 127.0.0.1:3000 --log info --log-format json\r\nRestart=on-failure\r\nRestartSec=2s\r\nLimitNOFILE=65536\r\n\r\n[Install]\r\nWantedBy=multi-user.target\r\n```\r\n\r\n### Suggested nginx site\r\n\r\n```nginx\r\nserver {\r\n    listen 443 ssl http2;\r\n    server_name api.farellvpn.engineer;\r\n\r\n    ssl_certificate     /etc/letsencrypt/live/api.farellvpn.engineer/fullchain.pem;\r\n    ssl_certificate_key /etc/letsencrypt/live/api.farellvpn.engineer/privkey.pem;\r\n\r\n    # Long-lived image-proxy responses can be safely cached at the edge\r\n    location /img {\r\n        proxy_pass http://127.0.0.1:3000;\r\n        proxy_set_header Host $host;\r\n        proxy_buffering on;\r\n        proxy_cache_valid 200 1d;\r\n    }\r\n\r\n    location / {\r\n        proxy_pass http://127.0.0.1:3000;\r\n        proxy_http_version 1.1;\r\n        proxy_set_header Host              $host;\r\n        proxy_set_header X-Real-IP         $remote_addr;\r\n        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;\r\n        proxy_set_header X-Forwarded-Proto https;\r\n    }\r\n}\r\n```\r\n\r\nHardening that the live deployment uses (and you should consider too):\r\n\r\n- `APIKU_SECRET` env set to a 64+ character random value so opaque IDs survive restarts\r\n- nginx-level rate limiting (`limit_req_zone`) on `/api/v1/search`\r\n- IP allowlist on `/api/v1/info` if you don't want server tuning info public\r\n- Cloudflare (or another CDN) in front for DDoS protection and TLS termination if you prefer\r\n\r\n### Continuous delivery\r\n\r\nPushing a tag like `v0.3.0` runs `.github/workflows/release.yml`, which:\r\n\r\n1. Cross-builds the binary for 7 targets (Linux x86_64 glibc + musl, Linux ARM64, macOS Intel + Apple Silicon, Windows x86_64 + ARM64) using `cross` for non-native ones\r\n2. Strips the binary on Unix targets\r\n3. Packs each target as `apiku-vX.Y.Z-\u003ctarget\u003e.{tar.gz,zip}` together with `README.md`, `LICENSE`, and `config.toml`\r\n4. Generates a per-archive `.sha256` plus a combined `SHA256SUMS`\r\n5. Publishes a GitHub Release with auto-generated notes\r\n\r\n`.github/workflows/ci.yml` runs on every push and PR (Linux, macOS, Windows) and gates merges on `cargo fmt --check`, `cargo clippy -D warnings`, `cargo build`, and `cargo test`.\r\n\r\nTo cut a release locally:\r\n\r\n```bash\r\ngit tag v0.3.0\r\ngit push origin v0.3.0\r\n# Or: trigger the \"Release\" workflow manually from the Actions tab\r\n```\r\n\r\n---\r\n\r\n## Logging\r\n\r\nLogs go to stderr by default. Three formats:\r\n\r\n| `--log-format` | Use case |\r\n|---|---|\r\n| `pretty` (default) | Coloured, aligned, human-readable |\r\n| `json` | Machine-readable, ideal for log aggregation |\r\n| `compact` | Single-line, no colours |\r\n\r\nUse `--log-file \u003cpath\u003e` to tee logs to a file in addition to stderr.\r\n\r\nSample colourised output:\r\n\r\n```\r\n2026-05-25T20:11:46Z  INFO  apiku::engine            Starting scrape of 1 URL(s) with concurrency 32\r\n2026-05-25T20:11:46Z  INFO  apiku::engine            HTTP 200 for https://mangaball.net/title-detail/dark-mortal/\r\n2026-05-25T20:11:47Z  INFO  apiku::engine            [1/1] Completed | Remaining: 0\r\n```\r\n\r\n---\r\n\r\n## Project layout\r\n\r\n```\r\nassets/                Front-end assets, compiled into the binary via include_str!\r\n├── webapp/\r\n│   ├── app.css        Consumer web app stylesheet\r\n│   └── app.js         Consumer web app SPA router + views\r\n└── tester/\r\n    ├── tester.css     Developer API console stylesheet\r\n    └── tester.js      Developer API console client-side script\r\n\r\nsrc/\r\n├── main.rs            CLI entry point, argument parsing, runtime setup\r\n├── log.rs             Coloured tracing-subscriber setup, banner\r\n├── opaque.rs          HMAC-SHA256 opaque ID + image-proxy signing\r\n├── fingerprint.rs     Browser fingerprint catalogue (Win/macOS/Linux/Android/iOS)\r\n├── sysspec.rs         CPU/RAM detection and tuning\r\n├── engine.rs          Scraping orchestrator (used by CLI and API)\r\n├── pipeline.rs        Outbound request header pipeline\r\n├── rate_limiter.rs    Per-domain rate limiter\r\n├── retry.rs           Retry handler with exponential backoff\r\n├── deep_extractor.rs  Generic page extractor (links, images, OG, JSON-LD)\r\n├── parser.rs          scraper wrapper with infallible CSS queries\r\n├── models.rs          Domain models (ContentModel and friends)\r\n├── error.rs           ScraperError variants\r\n├── config.rs          TOML configuration loading + validation\r\n├── web/               HTTP-serving layer (separate from engine internals)\r\n│   ├── mod.rs         Web module registry\r\n│   ├── server.rs      axum router, middleware (request-id, CORS, compression)\r\n│   ├── api.rs         REST handlers, DTOs, response envelopes, browse + paging\r\n│   ├── webapp.rs      Consumer streaming/reading SPA shell\r\n│   ├── tester.rs      Developer API console (maud)\r\n│   ├── search.rs      Cross-provider search abstraction + relevance ranking\r\n│   └── cossora.rs     Cosplaytele video resolver (AES-256-CBC decrypt -\u003e HLS)\r\n└── adapters/\r\n    ├── mod.rs         SiteAdapter trait + registry\r\n    ├── mangaball.rs   Mangaball SPA adapter (multi-step API + browse search_types)\r\n    ├── anichin.rs     Anichin donghua streaming adapter (HTML + browse orders)\r\n    ├── otakudesu.rs   Otakudesu anime streaming adapter (HTML + AJAX mirror resolver + genre feeds)\r\n    ├── cosplaytele.rs Cosplaytele cosplay archive adapter (HTML + categories)\r\n    ├── nhentai.rs     nhentai doujinshi adapter (JSON API + sharded CDN + popular feeds)\r\n    └── novelid.rs     NovelID Indonesian novel adapter (HTML, upstream-paginated chapter lists)\r\n```\r\n\r\n---\r\n\r\n## Roadmap\r\n\r\nPlanned work, roughly in priority order. Contributions and suggestions welcome via [issues](https://github.com/risqinf/apiku/issues).\r\n\r\n### Next up\r\n\r\n- [ ] **Nekopoi** — add the Nekopoi provider (adult anime / hentai streaming) behind the 18+ toggle, reusing the existing HLS resolver + episode/mirror player pipeline. **This is the next major target.**\r\n\r\n### Backlog\r\n\r\n- [ ] More providers (additional manga / donghua / novel sources) behind the same envelope.\r\n- [ ] Watch/read history \u0026 resume (client-side, then optional sync).\r\n- [ ] Favorites / bookmarks with import-export.\r\n- [ ] Server-rendered meta tags per detail page for richer link previews \u0026 SEO.\r\n- [ ] Optional API-key / rate-tier layer for public deployments.\r\n- [ ] PWA: offline shell + installable web app.\r\n- [ ] More language samples and an OpenAPI spec for the Explorer.\r\n\r\n### Done\r\n\r\n- [x] **Instant client-side navigation (0.2.4)** — the app shell (header / nav / drawer / footer) is built once and persists; navigation only swaps the content area instead of rebuilding the whole page, so moving between pages no longer flashes like a reload. Detail/watch/read pages render from a hover/touch-warmed cache with no spinner when the data is already in hand, and a 140 ms GPU-only fade keeps swaps smooth.\r\n- [x] **Performance pass for low-end devices (0.2.3)** — lightweight static background (no animated orbs / blur / blend modes), automatic \"lite mode\" on weak hardware (\u003c=2 GiB RAM / \u003c=2 cores, data-saver, 2G, or reduced-motion) that disables animations and background prefetch, plus a manual toggle. Targets a 2008 PC or a 2015 phone.\r\n- [x] **Tidier project layout (0.2.3)** — front-end assets split into `assets/webapp` + `assets/tester`; HTTP-serving code grouped under `src/web`, kept separate from the engine and adapters.\r\n- [x] **Stream Anime (Otakudesu)** — full anime streaming provider: search, rich detail metadata, episode list, quality-grouped streaming mirrors resolved on demand, downloads, and genre feeds.\r\n- [x] Cosplay video playback via server-side HLS resolution (no iframe embeds).\r\n- [x] High-precision Cosplaytele search + clickable cosplayer/tag pills.\r\n- [x] Relevance-ranked cross-provider search with per-source filter counts.\r\n- [x] Configurable branding (name/tagline/logo/footer/ads) via config + env, with logo auto-detection.\r\n- [x] Reworked API Explorer with grouped endpoints and copy-ready multi-language samples.\r\n- [x] Clean light/dark theme with real-time toggle switches and an overflow nav menu.\r\n- [x] Full episode lists for donghua, language-grouped manga chapters, NovelID upstream pagination.\r\n\r\n---\r\n\r\n## License\r\n\r\nMIT\r\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frisqinf%2Fapiku","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frisqinf%2Fapiku","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frisqinf%2Fapiku/lists"}