7.3 KiB
Executable File
7.3 KiB
Executable File
Phase 5 — HTTP API (REST)
Plain REST/JSON API over HTTP using github.com/go-chi/chi/v5. No GraphQL on our end.
All handlers live in api/handler.go. Server bootstrap in cmd/server/main.go.
Reference:
- Suwayomi API patterns:
/Users/achmad/Documents/Belajar/Web/Suwayomi-Server/server/src/main/kotlin/suwayomi/tachidesk/manga/api/
5.1 Server Bootstrap — cmd/server/main.go
- Parse config from env vars:
DATABASE_URL(required)PORT(default8080)FLARESOLVERR_URL(optional; disables CF-dependent sources if empty)- TTL vars:
MANGA_LIST_TTL_SECONDS,MANGA_DETAIL_TTL_SECONDS,CHAPTER_LIST_TTL_SECONDS
- Initialize
pgxpool.Poolviadb.New() - Run DB migrations via
db.Migrate() - Blank-import all source packages so their
init()registers them:import ( _ "tachiyomi-go/sources/all/mangadex" _ "tachiyomi-go/sources/all/kemono" _ "tachiyomi-go/sources/en/allanime" // ... all sources ) UpsertSourcefor every registered source on startup (sync source table with compiled sources)- Mount router and
http.ListenAndServe - Graceful shutdown on SIGINT/SIGTERM (drain pool, close server)
5.2 Router — api/handler.go
NewRouter(db *pgxpool.Pool) http.Handlerusing chi- Mount all routes under
/api - Middleware:
chi.Logger— request loggingchi.Recoverer— panic recovery → 500 JSON errorrequestIDmiddleware- CORS headers (configurable origin, default
*)
5.3 Source Routes
GET /api/sources
- Call
registry.All() - Return
[]{id, name, lang, supportsLatest, isNsfw}sorted by ID - No DB call required (registry is in-memory)
GET /api/sources/{id}/popular?page=1
- Parse
idas int64,pageas int (default 1),refreshas bool - Validate source exists in registry
- Check manga list TTL (see Phase 2 cache logic)
- If cache miss or
?refresh=true: callsource.GetPopularManga(page), upsert results - Return
{mangas: [...], hasNextPage: bool}
GET /api/sources/{id}/latest?page=1
- Same as popular but calls
source.GetLatestUpdates(page) - Return 405 if
source.SupportsLatest()is false
GET /api/sources/{id}/search?q=&page=1
- Parse
q(query string),page,refresh - Pass
filtersfrom query params (see Filter parsing below) - Call
source.GetSearchManga(page, q, filters) - Upsert results, return
{mangas: [...], hasNextPage: bool}
GET /api/sources/{id}/filters
- Call
source.GetFilterList() - Return filter descriptors as JSON:
[{type, name, values}] - No DB involvement
Filter Param Parsing
- Parse filter values from query params:
filter_{name}={value} SelectFilter: single string valueCheckboxFilter: "true"/"false" → boolTriStateFilter: "0"/"1"/"2" → intTextFilter: raw stringSortFilter:{name}:{asc|desc}
5.4 Manga Routes (Source-aware)
GET /api/sources/{id}/manga?url={encodedURL}
- URL-decode
urlparam - Look up manga in DB by
(source_id, url)—GetMangaBySourceURL - If not found or
initialized=falseor TTL expired or?refresh=true:- Call
source.GetMangaDetails(SManga{URL: url}) UpsertManga+UpdateMangaDetails+ setinitialized=true
- Call
- Return full manga JSON (all fields)
GET /api/sources/{id}/manga?url={encodedURL}&resource=chapters
OR GET /api/sources/{id}/chapters?url={encodedURL}
- Look up manga_id from DB
- Check
chapters_last_fetched_atTTL - If miss: call
source.GetChapterList(manga),UpsertChapterfor each - Return
[{url, name, chapterNumber, dateUpload, scanlator, sourceOrder}]
GET /api/sources/{id}/pages?chapterUrl={encodedURL}
- Look up chapter by URL
- Check if pages exist in DB for chapter
- If miss: call
source.GetPageList(chapter), then optionallysource.GetImageURL(page)for each UpsertPagefor each; storeimage_url(except sources with expiring URLs)- Return
[{index, url, imageUrl}]
5.5 DB-Backed Routes (no source call)
GET /api/manga/{id}
GetMangaByIDfrom DB- Return manga JSON or 404
GET /api/manga/{id}/chapters
ListChaptersByMangafrom DB- Return chapter array ordered by
source_order
GET /api/chapters/{id}
GetChapterByIDfrom DB- Return chapter JSON or 404
GET /api/chapters/{id}/pages
ListPagesByChapterfrom DB- Return page array ordered by
index
5.6 Image Proxy — GET /api/image?url={encodedURL}&source_id={id}
- URL-decode
urlparam - Validate
source_idexists in registry - Fetch image using source's configured HTTP client (respects source-specific headers/cookies)
- Set
Refererif required (e.g. Pixiv) - Use FlareSolverr-cleared cookies if source needs CF bypass
- Set
- Stream response bytes directly to client
- Forward
Content-Typeheader from upstream - Set
Cache-Control: public, max-age=86400for non-expiring images - Do not set long cache for signed/expiring URLs (globalcomix, etc.)
- Return 502 on upstream error
5.7 Error Handling
- All errors:
{"error": "human-readable message"}with correct HTTP status - 400: invalid params (bad ID, missing required param, bad page number)
- 404: source not found, manga not found, chapter not found
- 405: method not supported (e.g.
GetLatestUpdateson source withSupportsLatest=false) - 500: DB error, unexpected source error
- 502: upstream source returned error (connection refused, non-200, parse failure)
- 503: FlareSolverr unavailable (when source requires it but
FLARESOLVERR_URLis unset) - Structured error logging: include
source_id,url,errorin log fields
5.8 ?refresh=true Bypass
- Accepted on all source-facing routes:
/popular,/latest,/search,/manga,/chapters,/pages - Bypasses TTL — always calls source regardless of
last_fetched_at - Updates DB with fresh data after fetch
- NOT accepted on DB-only routes (
/api/manga/{id},/api/chapters/{id}, etc.)
Checklist: Phase 5 Done When
golang-migrateruns000001_init.up.sqlon a fresh DB without errorUpsertManga+GetMangaBySourceURLround-trip succeeds (confirm viapsql)UpsertChaptercorrectly setssource_orderfrom slice position- All tables (
manga,chapters,pages) populated after API calls — confirm viapsql GET /api/sourcesreturns all registered sourcesGET /api/sources/{heancmsID}/popular?page=1returns ≥1 manga; rows inmangatable confirmed viapsql- Second call to same URL returns from DB (no source HTTP call — confirm via log)
?refresh=trueforces re-fetch and updateslast_fetched_atin DBGET /api/sources/{madaraID}/pages?chapterUrl=...resolves image URLs via FlareSolverrGET /api/image?url=...&source_id=...proxies image bytes with correctContent-Type- 404 response for unknown source ID:
{"error": "source not found"} - 405 response when
GetLatestUpdatescalled on source withSupportsLatest=false go build ./cmd/server/succeeds- Server starts, logs migration version, logs registered source count