85d2ea6143
- Data types (SManga, SChapter, Page, MangasPage, all Filter variants) - Source interfaces (Source, CatalogueSource) with MD5-based ID generation matching Tachiyomi/Suwayomi - HTTP client with per-host rate limiting, cookie jar, and 429 retry - FlareSolverr v1 client (FLARESOLVERR_URL env) - Generic GraphQL POST helper - goquery HTML parser wrappers - Source registry (panics on duplicate ID) - Multi-stage Dockerfile (golang:1.26-alpine + distroless) and compose.yml (postgres, flaresolverr, app)
7.3 KiB
7.3 KiB
Phase 2 — Database Layer
Persistent storage using PostgreSQL via pgx. Migrations run on startup. sqlc generates type-safe query code.
Reference schema modeled after Suwayomi-Server:
/Users/achmad/Documents/Belajar/Web/Suwayomi-Server/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/- Migrations:
/Users/achmad/Documents/Belajar/Web/Suwayomi-Server/server/src/main/kotlin/suwayomi/tachidesk/server/database/migration/
2.1 Schema Migration — internal/db/migrations/001_init.sql
sourcestableid BIGINT PRIMARY KEY— generated viaGenerateSourceID(name, lang)same as Tachiyominame VARCHAR(128) NOT NULLlang VARCHAR(32) NOT NULLis_nsfw BOOLEAN NOT NULL DEFAULT FALSE
mangatableid SERIAL PRIMARY KEYsource_id BIGINT NOT NULL REFERENCES sources(id)url VARCHAR(2048) NOT NULLtitle VARCHAR(512) NOT NULLinitialized BOOLEAN NOT NULL DEFAULT FALSEartist TEXT,author TEXT,description TEXT,genre TEXTstatus INTEGER NOT NULL DEFAULT 0thumbnail_url VARCHAR(2048)thumbnail_last_fetched BIGINT NOT NULL DEFAULT 0in_library BOOLEAN NOT NULL DEFAULT FALSEin_library_at BIGINT NOT NULL DEFAULT 0real_url VARCHAR(2048)last_fetched_at BIGINT NOT NULL DEFAULT 0chapters_last_fetched_at BIGINT NOT NULL DEFAULT 0update_strategy VARCHAR(64) NOT NULL DEFAULT 'ALWAYS_UPDATE'UNIQUE (source_id, url)
chapterstableid SERIAL PRIMARY KEYmanga_id INTEGER NOT NULL REFERENCES manga(id) ON DELETE CASCADEurl VARCHAR(2048) NOT NULLname VARCHAR(512) NOT NULLdate_upload BIGINT NOT NULL DEFAULT 0chapter_number REAL NOT NULL DEFAULT -1scanlator VARCHAR(256)source_order INTEGER NOT NULLis_read BOOLEAN NOT NULL DEFAULT FALSEis_bookmarked BOOLEAN NOT NULL DEFAULT FALSElast_page_read INTEGER NOT NULL DEFAULT 0last_read_at BIGINT NOT NULL DEFAULT 0fetched_at BIGINT NOT NULL DEFAULT 0real_url VARCHAR(2048)is_downloaded BOOLEAN NOT NULL DEFAULT FALSEpage_count INTEGER NOT NULL DEFAULT -1UNIQUE (manga_id, url)
pagestableid SERIAL PRIMARY KEYchapter_id INTEGER NOT NULL REFERENCES chapters(id) ON DELETE CASCADE"index" INTEGER NOT NULLurl VARCHAR(2048) NOT NULLimage_url TEXT
source_metatablesource_id BIGINT NOT NULL REFERENCES sources(id)key VARCHAR(256) NOT NULLvalue TEXT NOT NULLPRIMARY KEY (source_id, key)
- Indexes
CREATE INDEX ON manga (source_id)CREATE INDEX ON manga (last_fetched_at)CREATE INDEX ON chapters (manga_id)CREATE INDEX ON pages (chapter_id)
2.2 DB Initialization — internal/db/db.go
pgxpool.Poolinit fromDATABASE_URLenv var- Configurable
MaxConns(default 10) - Configurable
MinConns(default 2) - Connection health check on startup
- Configurable
- Migration runner using
golang-migrate/migrate- Source:
iofs(embed migration SQL files with//go:embed) - Driver:
pgx5 - Run
migrate.Up()on startup; log version before/after - Non-fatal on "no change" (already at latest)
- Source:
Queriesstruct wrapping sqlc-generated query clientsClose()method to drain pool on shutdown
2.3 SQL Queries — internal/db/queries/
Write .sql files, then run sqlc generate to produce type-safe Go.
manga.sql
UpsertManga— INSERT ... ON CONFLICT (source_id, url) DO UPDATE all fields; returns rowGetMangaBySourceURL— SELECT by source_id + urlGetMangaByID— SELECT by primary keyListMangaBySource— SELECT by source_id ORDER BY last_fetched_at DESCUpdateMangaDetails— UPDATE artist/author/description/genre/status/thumbnail/initialized for a manga idUpdateMangaFetchedAt— UPDATE last_fetched_at = $now WHERE id = $idUpdateChaptersFetchedAt— UPDATE chapters_last_fetched_at WHERE id = $id
chapter.sql
UpsertChapter— INSERT ... ON CONFLICT (manga_id, url) DO UPDATE name/date/chapter_number/scanlator/source_orderGetChapterByIDListChaptersByManga— ORDER BY source_orderUpdateChapterFetchedAt
page.sql
UpsertPage— INSERT ... ON CONFLICT (chapter_id, index) DO UPDATE url/image_urlListPagesByChapter— ORDER BY indexUpdatePageImageURL— UPDATE image_url WHERE id = $id
source.sql
UpsertSource— INSERT ... ON CONFLICT (id) DO UPDATE name/lang/is_nsfwListSourcesGetSourceByIDGetSourceMeta— SELECT value WHERE source_id = $id AND key = $keySetSourceMeta— INSERT ... ON CONFLICT DO UPDATE value
2.4 sqlc Configuration — sqlc.yaml
version: "2"- engine:
postgresql - schema path pointing to
internal/db/migrations/ - queries path pointing to
internal/db/queries/*.sql - output package:
internal/db/queries emit_json_tags: trueemit_db_tags: true
2.5 Data Flow & Cache Logic
Four upsert flows — each called from the API handler before returning data.
Manga List (GetPopularManga / GetLatestUpdates / GetSearchManga)
- Check
manga.last_fetched_at: if within TTL (default 10 min) and?refresh=false, return DB rows - Otherwise call source, then
UpsertMangafor each returned SManga - Update
last_fetched_aton upserted rows UpsertSourcefor the source record (idempotent on every list call)
Manga Detail (GetMangaDetails)
- Check
manga.initialized: if true and within TTL (default 1h), return DB row - Otherwise call source, then
UpdateMangaDetails+ setinitialized=true
Chapter List (GetChapterList)
- Check
manga.chapters_last_fetched_at: if within TTL (default 10 min), return DB rows - Otherwise call source, then
UpsertChapterfor each SChapter (preservingsource_order= slice index) - Update
chapters_last_fetched_aton the manga row
Page List (GetPageList)
- Check if pages exist for chapter in DB
- Otherwise call source, then
UpsertPagefor each Page - If source needs a second call to resolve image URLs (via
GetImageURL), call it andUpdatePageImageURL - Store resolved
image_url— do NOT cache for sources with signed/expiring URLs (globalcomix, etc.)
TTL Configuration
MANGA_LIST_TTL_SECONDSenv var (default 600)MANGA_DETAIL_TTL_SECONDSenv var (default 3600)CHAPTER_LIST_TTL_SECONDSenv var (default 600)?refresh=truequery param bypasses TTL on any route
Checklist: Phase 2 Done When
golang-migrateruns001_init.sqlon a fresh DB without errorsqlc generatecompletes without errors; generated files compileUpsertManga+GetMangaBySourceURLround-trip test passesUpsertChaptercorrectly setssource_orderfrom slice position- TTL cache logic returns DB rows on second call (verify no source HTTP call via log)
?refresh=truebypasses TTL and re-fetches from source- All tables visible in
psqlafter API calls