API privée et auto-hébergeable pour interroger Manga News, normaliser les réponses utiles, puis les exposer en JSON avec cache SQLite et ETag.
Le projet est pensé pour deux usages :
- un outil personnel qui a besoin d'une source JSON stable au-dessus du HTML de Manga News ;
- une autre IA ou un autre développeur qui doit pouvoir démarrer rapidement sans relire tout le code.
Routes publiques actuellement disponibles :
GET /healthGET /searchGET /search/resolveGET /series/{slug}GET /series/by-urlGET /series/{slug}/relatedGET /series/by-url/relatedGET /series/{slug}/editionsGET /series/by-url/editionsGET /volume/{series_slug}/{volume_slug}GET /volume/by-urlGET /news/globalGET /news/series/{slug}GET /news/volume/{series_slug}/{volume_slug}GET /news/volume/by-urlGET /planning
Fonctions utiles déjà en place :
- recherche série / volume ;
- résolution du meilleur match ;
- fiches détaillées série et volume ;
- titres alternatifs
title_voettranslated_titlesur les fiches détaillées et dans les résultats de recherche quand l'enrichissement réussit ; - compteurs d'éditions
vf/vosur les fiches série, sur les fiches volume enrichies depuis la série parente, et dans les résultats de recherche enrichis ; - normalisation volume :
number,number_int,edition_label,is_special,is_one_shotsur les fiches volume, le planning, les éditions de série, et les résultats de recherche enrichis ; - projections légères via
blocks,fieldsetinclude_raw_sectionssur les routes détail série / volume ; - cache SQLite persistant avec stale cache et negative cache ;
- ETag /
If-None-Match/304 Not Modified; - documentation OpenAPI native via
/docs,/redoc,/openapi.json; - exemples JSON versionnés dans
docs/examples/.
Important pour éviter les mauvaises hypothèses :
- pas de préfixe
/v1; - pas d'endpoint public de watch / monitoring métier ;
- pas de multi-provider : la source métier est Manga News ;
- pas de pagination métier normalisée dans les réponses ;
- pas d'admin API publique aujourd'hui ;
- pas de recherche dédiée du type
title_vo=...outranslated_title=...: les titres alternatifs sont exposés, pas recherchés séparément.
Pour un humain ou une IA, l'ordre utile est :
- ce
README.md; docs/API_INTEGRATION.md;docs/OPENAPI_AND_AI_USAGE.md;docs/examples/README.mdet quelques JSON réels ;docs/USE_CASES_AND_RECIPES.md;docs/DEPLOYMENT_AND_OPERATIONS.mdsi tu déploies ;docs/ARCHITECTURE.mdsi tu veux comprendre les choix internes.
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reloadPar défaut, Uvicorn servira l'API sur http://localhost:8000.
docker compose up -d --buildLe docker-compose.yml expose le conteneur sur http://localhost:8017.
Si API_TOKEN est vide, l'API est ouverte.
Si API_TOKEN est défini, toutes les routes publiques attendent :
Authorization: Bearer <token>En cas d'échec, la réponse actuelle est :
{
"detail": {
"code": "AUTH_REQUIRED",
"detail": "Missing or invalid bearer token."
}
}Oui, ce format 401 n'est pas identique aux 404/502. La doc le documente tel qu'il est aujourd'hui, sans prétendre qu'il est plus propre qu'il ne l'est.
curl http://localhost:8017/healthcurl --get "http://localhost:8017/search" \
--data-urlencode "q=one piece" \
--data-urlencode "kind=series" \
--data-urlencode "mode=all" \
--data-urlencode "limit=5"Requête la plus légère utile pour ce besoin :
curl --get "http://localhost:8017/search" \
--data-urlencode "q=one piece" \
--data-urlencode "kind=series" \
--data-urlencode "mode=best" \
--data-urlencode "limit=1"Pourquoi cette forme est la plus rapide parmi les routes de recherche publiques :
kind=seriesévite d'interroger aussi les pages de recherche volume ;mode=bestcoupe le résultat final à un seul candidat ;limit=1empêche de conserver plusieurs candidats côté réponse ;- l'API n'enrichit alors qu'un seul résultat retenu, au lieu d'une liste complète.
À lire dans la réponse :
data[0].vf.volumes> 0 : la série a bien des tomes VF connus ;data[0].vfabsent ounull: soit la série n'a pas d'édition VF exposée, soit l'enrichissement détaillé n'a pas pu la lire.
Pour une vérification plus fiable après identification du slug, utilise ensuite une fiche série légère :
curl --get "http://localhost:8017/series/One-piece-Edition-originale" \
--data-urlencode "fields=title,vf.volumes,vf.status"curl --get "http://localhost:8017/search/resolve" \
--data-urlencode "q=one piece tome 91" \
--data-urlencode "kind=volume" \
--data-urlencode "limit=10"curl "http://localhost:8017/series/One-piece-Edition-originale"curl "http://localhost:8017/volume/One-Piece/vol-91"curl --get "http://localhost:8017/planning" \
--data-urlencode "section=manga-vf" \
--data-urlencode "year=2026" \
--data-urlencode "month=4" \
--data-urlencode "publisher=Glénat" \
--data-urlencode "sort=date_asc" \
--data-urlencode "limit=25"curl -H "Authorization: Bearer MON_TOKEN" \
"http://localhost:8017/search?q=one%20piece"La plupart des routes renvoient une enveloppe comme celle-ci :
{
"schema_version": "1.0",
"ok": true,
"found": true,
"source": "manga_news",
"source_url": "https://www.manga-news.com/...",
"cached": false,
"fetched_at": "2026-04-20T12:00:00+00:00",
"cache_expires_at": "2026-04-21T12:00:00+00:00",
"partial": false,
"warnings": [],
"fingerprint": "...",
"data": {}
}Les champs importants :
cached: la réponse vient du cache local ;partial: l'API a servi une entrée stale car l'upstream a échoué ;warnings: détails utiles quandpartial=true;fingerprint: hash métier utilisé pour l'ETag ;source_url: page Manga News réellement utilisée.
Sur les routes enveloppées, l'API peut renvoyer :
ETag: "<fingerprint>"X-Data-Fingerprint: <fingerprint>
Tu peux ensuite rejouer la requête avec :
If-None-Match: "<fingerprint>"Si rien n'a changé, la réponse sera 304 Not Modified.
Les routes détail série et volume acceptent :
blocks: blocs métier prédéfinis ;fields: chemins ciblés ;include_raw_sections=true: inclutraw_sections.
Exemple minimal sur une série :
curl --get "http://localhost:8017/series/One-piece-Edition-originale" \
--data-urlencode "fields=title,vf.volumes,next_release_date"Exemple sur un volume :
curl --get "http://localhost:8017/volume/One-Piece/vol-110" \
--data-urlencode "blocks=identity,release" \
--data-urlencode "fields=cover_image"Voir docs/examples/README.md.
Les exemples les plus utiles pour démarrer sont :
docs/examples/search_response_one_piece.jsondocs/examples/resolve_response_one_piece.jsondocs/examples/series_one_piece.jsondocs/examples/volume_one_piece_110.jsondocs/examples/planning_example.jsondocs/examples/error_upstream_parse.json
Avant de livrer ou consommer l'API :
python scripts/validate_contract_and_docs.py
pytestQuelques variables existent dans la config mais ne pilotent pas encore les routes publiques actuelles :
ADMIN_TOKENREQUEST_MAX_RETRIESREQUEST_BACKOFF_SECONDSDEFAULT_LIMITLOG_FORMATRATE_LIMIT_*
Elles sont présentes parce que le projet a déjà préparé ces concepts, mais la doc n'en fait pas des features actives tant qu'elles ne sont pas réellement branchées au runtime.
Les compteurs vf / vo proviennent en priorité du bloc HTML #numberblock des fiches série Manga-News.
Quand un résultat de recherche cible un volume, l'API relit aussi la fiche série parente pour injecter ces compteurs dans le résultat.
Après un changement de parseur, il faut redémarrer l'API. Les clés de cache métier intègrent désormais une version interne, ce qui évite de relire un ancien payload incompatible après mise à jour.
Les réponses series, volume, search et search/resolve dépendent d'un cache SQLite local. Quand le parseur évolue (par exemple pour mieux remonter vf / vo), l'application ignore automatiquement les anciennes entrées de cache incompatibles grâce à une version interne de schéma de cache. Après déploiement, un simple redémarrage de l'API suffit normalement à voir les nouvelles données. Supprimer le fichier SQLite de cache reste la méthode la plus radicale si vous voulez repartir d'un cache totalement vierge.