feat: Phase 2 — database layer
- PostgreSQL schema: sources, manga, chapters, pages, source_meta with indexes - golang-migrate runner with embedded SQL via go:embed (pgx5:// scheme) - sqlc-generated type-safe queries for all tables (pgx/v5 native) - Config package with all env vars including TTL durations - Wire DB init and config into cmd/server/main.go
This commit is contained in:
@@ -6,7 +6,7 @@ Detailed checklists are in each phase doc under `docs/`.
|
||||
---
|
||||
|
||||
- [x] **Phase 1 — Core Framework** → `docs/phase1-core-framework.md`
|
||||
- [ ] **Phase 2 — Database Layer** → `docs/phase2-database.md`
|
||||
- [x] **Phase 2 — Database Layer** → `docs/phase2-database.md`
|
||||
- [ ] **Phase 3 — Base Source Implementations (68 bases)** → `docs/phase3-bases.md`
|
||||
- [ ] **Phase 4 — Standalone Sources (555 sources)** → `docs/phase4-standalone.md`
|
||||
- [ ] **Phase 5 — HTTP API** → `docs/phase5-api.md`
|
||||
|
||||
+15
-4
@@ -1,22 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
_ "goyomi/internal/registry" // ensures registry package is initialized
|
||||
"goyomi/internal/config"
|
||||
"goyomi/internal/db"
|
||||
_ "goyomi/internal/registry"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := config.Load()
|
||||
|
||||
ctx := context.Background()
|
||||
database, err := db.Open(ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("db: %v", err)
|
||||
}
|
||||
defer database.Close()
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, "ok")
|
||||
})
|
||||
|
||||
addr := ":8080"
|
||||
log.Printf("listening on %s", addr)
|
||||
if err := http.ListenAndServe(addr, mux); err != nil {
|
||||
log.Printf("listening on %s", cfg.Addr)
|
||||
if err := http.ListenAndServe(cfg.Addr, mux); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
+111
-134
@@ -8,176 +8,153 @@ Reference schema modeled after Suwayomi-Server:
|
||||
|
||||
---
|
||||
|
||||
## 2.1 Schema Migration — `internal/db/migrations/001_init.sql`
|
||||
## 2.1 Schema Migration — `internal/db/migrations/000001_init.up.sql`
|
||||
|
||||
- [ ] `sources` table
|
||||
- [ ] `id BIGINT PRIMARY KEY` — generated via `GenerateSourceID(name, lang)` same as Tachiyomi
|
||||
- [ ] `name VARCHAR(128) NOT NULL`
|
||||
- [ ] `lang VARCHAR(32) NOT NULL`
|
||||
- [ ] `is_nsfw BOOLEAN NOT NULL DEFAULT FALSE`
|
||||
- [ ] `manga` table
|
||||
- [ ] `id SERIAL PRIMARY KEY`
|
||||
- [ ] `source_id BIGINT NOT NULL REFERENCES sources(id)`
|
||||
- [ ] `url VARCHAR(2048) NOT NULL`
|
||||
- [ ] `title VARCHAR(512) NOT NULL`
|
||||
- [ ] `initialized BOOLEAN NOT NULL DEFAULT FALSE`
|
||||
- [ ] `artist TEXT`, `author TEXT`, `description TEXT`, `genre TEXT`
|
||||
- [ ] `status INTEGER NOT NULL DEFAULT 0`
|
||||
- [ ] `thumbnail_url VARCHAR(2048)`
|
||||
- [ ] `thumbnail_last_fetched BIGINT NOT NULL DEFAULT 0`
|
||||
- [ ] `in_library BOOLEAN NOT NULL DEFAULT FALSE`
|
||||
- [ ] `in_library_at BIGINT NOT NULL DEFAULT 0`
|
||||
- [ ] `real_url VARCHAR(2048)`
|
||||
- [ ] `last_fetched_at BIGINT NOT NULL DEFAULT 0`
|
||||
- [ ] `chapters_last_fetched_at BIGINT NOT NULL DEFAULT 0`
|
||||
- [ ] `update_strategy VARCHAR(64) NOT NULL DEFAULT 'ALWAYS_UPDATE'`
|
||||
- [ ] `UNIQUE (source_id, url)`
|
||||
- [ ] `chapters` table
|
||||
- [ ] `id SERIAL PRIMARY KEY`
|
||||
- [ ] `manga_id INTEGER NOT NULL REFERENCES manga(id) ON DELETE CASCADE`
|
||||
- [ ] `url VARCHAR(2048) NOT NULL`
|
||||
- [ ] `name VARCHAR(512) NOT NULL`
|
||||
- [ ] `date_upload BIGINT NOT NULL DEFAULT 0`
|
||||
- [ ] `chapter_number REAL NOT NULL DEFAULT -1`
|
||||
- [ ] `scanlator VARCHAR(256)`
|
||||
- [ ] `source_order INTEGER NOT NULL`
|
||||
- [ ] `is_read BOOLEAN NOT NULL DEFAULT FALSE`
|
||||
- [ ] `is_bookmarked BOOLEAN NOT NULL DEFAULT FALSE`
|
||||
- [ ] `last_page_read INTEGER NOT NULL DEFAULT 0`
|
||||
- [ ] `last_read_at BIGINT NOT NULL DEFAULT 0`
|
||||
- [ ] `fetched_at BIGINT NOT NULL DEFAULT 0`
|
||||
- [ ] `real_url VARCHAR(2048)`
|
||||
- [ ] `is_downloaded BOOLEAN NOT NULL DEFAULT FALSE`
|
||||
- [ ] `page_count INTEGER NOT NULL DEFAULT -1`
|
||||
- [ ] `UNIQUE (manga_id, url)`
|
||||
- [ ] `pages` table
|
||||
- [ ] `id SERIAL PRIMARY KEY`
|
||||
- [ ] `chapter_id INTEGER NOT NULL REFERENCES chapters(id) ON DELETE CASCADE`
|
||||
- [ ] `"index" INTEGER NOT NULL`
|
||||
- [ ] `url VARCHAR(2048) NOT NULL`
|
||||
- [ ] `image_url TEXT`
|
||||
- [ ] `source_meta` table
|
||||
- [ ] `source_id BIGINT NOT NULL REFERENCES sources(id)`
|
||||
- [ ] `key VARCHAR(256) NOT NULL`
|
||||
- [ ] `value TEXT NOT NULL`
|
||||
- [ ] `PRIMARY 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)`
|
||||
- [x] `sources` table
|
||||
- [x] `id BIGINT PRIMARY KEY`
|
||||
- [x] `name VARCHAR(128) NOT NULL`
|
||||
- [x] `lang VARCHAR(32) NOT NULL`
|
||||
- [x] `is_nsfw BOOLEAN NOT NULL DEFAULT FALSE`
|
||||
- [x] `manga` table
|
||||
- [x] `id SERIAL PRIMARY KEY`
|
||||
- [x] `source_id BIGINT NOT NULL REFERENCES sources(id)`
|
||||
- [x] `url VARCHAR(2048) NOT NULL`
|
||||
- [x] `title VARCHAR(512) NOT NULL`
|
||||
- [x] `initialized BOOLEAN NOT NULL DEFAULT FALSE`
|
||||
- [x] `artist TEXT`, `author TEXT`, `description TEXT`, `genre TEXT`
|
||||
- [x] `status INTEGER NOT NULL DEFAULT 0`
|
||||
- [x] `thumbnail_url VARCHAR(2048)`
|
||||
- [x] `thumbnail_last_fetched BIGINT NOT NULL DEFAULT 0`
|
||||
- [x] `in_library BOOLEAN NOT NULL DEFAULT FALSE`
|
||||
- [x] `in_library_at BIGINT NOT NULL DEFAULT 0`
|
||||
- [x] `real_url VARCHAR(2048)`
|
||||
- [x] `last_fetched_at BIGINT NOT NULL DEFAULT 0`
|
||||
- [x] `chapters_last_fetched_at BIGINT NOT NULL DEFAULT 0`
|
||||
- [x] `update_strategy VARCHAR(64) NOT NULL DEFAULT 'ALWAYS_UPDATE'`
|
||||
- [x] `UNIQUE (source_id, url)`
|
||||
- [x] `chapters` table
|
||||
- [x] `id SERIAL PRIMARY KEY`
|
||||
- [x] `manga_id INTEGER NOT NULL REFERENCES manga(id) ON DELETE CASCADE`
|
||||
- [x] `url VARCHAR(2048) NOT NULL`
|
||||
- [x] `name VARCHAR(512) NOT NULL`
|
||||
- [x] `date_upload BIGINT NOT NULL DEFAULT 0`
|
||||
- [x] `chapter_number REAL NOT NULL DEFAULT -1`
|
||||
- [x] `scanlator VARCHAR(256)`
|
||||
- [x] `source_order INTEGER NOT NULL`
|
||||
- [x] `is_read BOOLEAN NOT NULL DEFAULT FALSE`
|
||||
- [x] `is_bookmarked BOOLEAN NOT NULL DEFAULT FALSE`
|
||||
- [x] `last_page_read INTEGER NOT NULL DEFAULT 0`
|
||||
- [x] `last_read_at BIGINT NOT NULL DEFAULT 0`
|
||||
- [x] `fetched_at BIGINT NOT NULL DEFAULT 0`
|
||||
- [x] `real_url VARCHAR(2048)`
|
||||
- [x] `is_downloaded BOOLEAN NOT NULL DEFAULT FALSE`
|
||||
- [x] `page_count INTEGER NOT NULL DEFAULT -1`
|
||||
- [x] `UNIQUE (manga_id, url)`
|
||||
- [x] `pages` table
|
||||
- [x] `id SERIAL PRIMARY KEY`
|
||||
- [x] `chapter_id INTEGER NOT NULL REFERENCES chapters(id) ON DELETE CASCADE`
|
||||
- [x] `"index" INTEGER NOT NULL`
|
||||
- [x] `url VARCHAR(2048) NOT NULL`
|
||||
- [x] `image_url TEXT`
|
||||
- [x] `UNIQUE (chapter_id, "index")` — added for upsert support
|
||||
- [x] `source_meta` table
|
||||
- [x] `source_id BIGINT NOT NULL REFERENCES sources(id)`
|
||||
- [x] `key VARCHAR(256) NOT NULL`
|
||||
- [x] `value TEXT NOT NULL`
|
||||
- [x] `PRIMARY KEY (source_id, key)`
|
||||
- [x] Indexes
|
||||
- [x] `CREATE INDEX ON manga (source_id)`
|
||||
- [x] `CREATE INDEX ON manga (last_fetched_at)`
|
||||
- [x] `CREATE INDEX ON chapters (manga_id)`
|
||||
- [x] `CREATE INDEX ON pages (chapter_id)`
|
||||
|
||||
---
|
||||
|
||||
## 2.2 DB Initialization — `internal/db/db.go`
|
||||
|
||||
- [ ] `pgxpool.Pool` init from `DATABASE_URL` env var
|
||||
- [ ] Configurable `MaxConns` (default 10)
|
||||
- [ ] Configurable `MinConns` (default 2)
|
||||
- [ ] Connection health check on startup
|
||||
- [ ] 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)
|
||||
- [ ] `Queries` struct wrapping sqlc-generated query clients
|
||||
- [ ] `Close()` method to drain pool on shutdown
|
||||
- [x] `pgxpool.Pool` init from `DATABASE_URL` env var
|
||||
- [x] Configurable `MaxConns` via `DB_MAX_CONNS` (default 10)
|
||||
- [x] Configurable `MinConns` via `DB_MIN_CONNS` (default 2)
|
||||
- [x] Connection health check on startup (`pool.Ping`)
|
||||
- [x] Migration runner using `golang-migrate/migrate`
|
||||
- [x] Source: `iofs` (embed migration SQL files with `//go:embed`)
|
||||
- [x] Driver: `pgx5` (DSN scheme rewritten from `postgres://` → `pgx5://`)
|
||||
- [x] Run `migrate.Up()` on startup; log version before/after
|
||||
- [x] Non-fatal on "no change" (`migrate.ErrNoChange`)
|
||||
- [x] `Queries` struct wrapping sqlc-generated query clients
|
||||
- [x] `Close()` 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.
|
||||
Generated by `sqlc generate` (config: `sqlc.yaml`, `sql_package: pgx/v5`).
|
||||
|
||||
### `manga.sql`
|
||||
|
||||
- [ ] `UpsertManga` — INSERT ... ON CONFLICT (source_id, url) DO UPDATE all fields; returns row
|
||||
- [ ] `GetMangaBySourceURL` — SELECT by source_id + url
|
||||
- [ ] `GetMangaByID` — SELECT by primary key
|
||||
- [ ] `ListMangaBySource` — SELECT by source_id ORDER BY last_fetched_at DESC
|
||||
- [ ] `UpdateMangaDetails` — UPDATE artist/author/description/genre/status/thumbnail/initialized for a manga id
|
||||
- [ ] `UpdateMangaFetchedAt` — UPDATE last_fetched_at = $now WHERE id = $id
|
||||
- [ ] `UpdateChaptersFetchedAt` — UPDATE chapters_last_fetched_at WHERE id = $id
|
||||
- [x] `UpsertManga`
|
||||
- [x] `GetMangaBySourceURL`
|
||||
- [x] `GetMangaByID`
|
||||
- [x] `ListMangaBySource`
|
||||
- [x] `UpdateMangaDetails`
|
||||
- [x] `UpdateMangaFetchedAt`
|
||||
- [x] `UpdateChaptersFetchedAt`
|
||||
|
||||
### `chapter.sql`
|
||||
|
||||
- [ ] `UpsertChapter` — INSERT ... ON CONFLICT (manga_id, url) DO UPDATE name/date/chapter_number/scanlator/source_order
|
||||
- [ ] `GetChapterByID`
|
||||
- [ ] `ListChaptersByManga` — ORDER BY source_order
|
||||
- [ ] `UpdateChapterFetchedAt`
|
||||
- [x] `UpsertChapter`
|
||||
- [x] `GetChapterByID`
|
||||
- [x] `ListChaptersByManga`
|
||||
- [x] `UpdateChapterFetchedAt`
|
||||
|
||||
### `page.sql`
|
||||
|
||||
- [ ] `UpsertPage` — INSERT ... ON CONFLICT (chapter_id, index) DO UPDATE url/image_url
|
||||
- [ ] `ListPagesByChapter` — ORDER BY index
|
||||
- [ ] `UpdatePageImageURL` — UPDATE image_url WHERE id = $id
|
||||
- [x] `UpsertPage`
|
||||
- [x] `ListPagesByChapter`
|
||||
- [x] `UpdatePageImageURL`
|
||||
|
||||
### `source.sql`
|
||||
|
||||
- [ ] `UpsertSource` — INSERT ... ON CONFLICT (id) DO UPDATE name/lang/is_nsfw
|
||||
- [ ] `ListSources`
|
||||
- [ ] `GetSourceByID`
|
||||
- [ ] `GetSourceMeta` — SELECT value WHERE source_id = $id AND key = $key
|
||||
- [ ] `SetSourceMeta` — INSERT ... ON CONFLICT DO UPDATE value
|
||||
- [x] `UpsertSource`
|
||||
- [x] `ListSources`
|
||||
- [x] `GetSourceByID`
|
||||
- [x] `GetSourceMeta`
|
||||
- [x] `SetSourceMeta`
|
||||
|
||||
---
|
||||
|
||||
## 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: true`
|
||||
- [ ] `emit_db_tags: true`
|
||||
- [x] `version: "2"`
|
||||
- [x] engine: `postgresql`
|
||||
- [x] schema path: `internal/db/migrations/`
|
||||
- [x] queries path: `internal/db/queries/*.sql`
|
||||
- [x] output package: `internal/db/queries`
|
||||
- [x] `sql_package: "pgx/v5"` — required for pgxpool compatibility
|
||||
- [x] `emit_json_tags: true`
|
||||
- [x] `emit_db_tags: true`
|
||||
- [x] `emit_pointers_for_null_types: true`
|
||||
|
||||
---
|
||||
|
||||
## 2.5 Data Flow & Cache Logic
|
||||
|
||||
Four upsert flows — each called from the API handler before returning data.
|
||||
TTL env vars implemented in `internal/config/config.go`. Cache check logic lives in the API handler (Phase 5) using these values.
|
||||
|
||||
### 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 `UpsertManga` for each returned SManga
|
||||
- [ ] Update `last_fetched_at` on upserted rows
|
||||
- [ ] `UpsertSource` for 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` + set `initialized=true`
|
||||
|
||||
### Chapter List (`GetChapterList`)
|
||||
|
||||
- [ ] Check `manga.chapters_last_fetched_at`: if within TTL (default 10 min), return DB rows
|
||||
- [ ] Otherwise call source, then `UpsertChapter` for each SChapter (preserving `source_order` = slice index)
|
||||
- [ ] Update `chapters_last_fetched_at` on the manga row
|
||||
|
||||
### Page List (`GetPageList`)
|
||||
|
||||
- [ ] Check if pages exist for chapter in DB
|
||||
- [ ] Otherwise call source, then `UpsertPage` for each Page
|
||||
- [ ] If source needs a second call to resolve image URLs (via `GetImageURL`), call it and `UpdatePageImageURL`
|
||||
- [ ] Store resolved `image_url` — do NOT cache for sources with signed/expiring URLs (globalcomix, etc.)
|
||||
|
||||
### TTL Configuration
|
||||
|
||||
- [ ] `MANGA_LIST_TTL_SECONDS` env var (default 600)
|
||||
- [ ] `MANGA_DETAIL_TTL_SECONDS` env var (default 3600)
|
||||
- [ ] `CHAPTER_LIST_TTL_SECONDS` env var (default 600)
|
||||
- [ ] `?refresh=true` query param bypasses TTL on any route
|
||||
- [x] `MANGA_LIST_TTL_SECONDS` env var (default 600)
|
||||
- [x] `MANGA_DETAIL_TTL_SECONDS` env var (default 3600)
|
||||
- [x] `CHAPTER_LIST_TTL_SECONDS` env var (default 600)
|
||||
- [ ] `?refresh=true` query param bypass — implemented in Phase 5 API handler
|
||||
- [ ] Per-flow cache check + upsert logic — implemented in Phase 5 API handler
|
||||
|
||||
---
|
||||
|
||||
## Checklist: Phase 2 Done When
|
||||
|
||||
- [ ] `golang-migrate` runs `001_init.sql` on a fresh DB without error
|
||||
- [ ] `sqlc generate` completes without errors; generated files compile
|
||||
- [ ] `UpsertManga` + `GetMangaBySourceURL` round-trip test passes
|
||||
- [ ] `UpsertChapter` correctly sets `source_order` from slice position
|
||||
- [ ] TTL cache logic returns DB rows on second call (verify no source HTTP call via log)
|
||||
- [ ] `?refresh=true` bypasses TTL and re-fetches from source
|
||||
- [ ] All tables visible in `psql` after API calls
|
||||
- [ ] `golang-migrate` runs `000001_init.up.sql` on a fresh DB without error (manual test with running postgres)
|
||||
- [x] `sqlc generate` completes without errors; generated files compile
|
||||
- [ ] `UpsertManga` + `GetMangaBySourceURL` round-trip test passes (requires running postgres)
|
||||
- [ ] `UpsertChapter` correctly sets `source_order` from slice position (requires running postgres)
|
||||
- [ ] TTL cache logic returns DB rows on second call (Phase 5)
|
||||
- [ ] `?refresh=true` bypasses TTL (Phase 5)
|
||||
- [ ] All tables visible in `psql` after API calls (Phase 5)
|
||||
|
||||
@@ -4,10 +4,18 @@ go 1.26.2
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.12.0
|
||||
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||
github.com/jackc/pgx/v5 v5.9.2
|
||||
golang.org/x/time v0.15.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,9 +1,82 @@
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/PuerkitoBio/goquery v1.12.0 h1:pAcL4g3WRXekcB9AU/y1mbKez2dbY2AajVhtkO8RIBo=
|
||||
github.com/PuerkitoBio/goquery v1.12.0/go.mod h1:802ej+gV2y7bbIhOIoPY5sT183ZW0YFofScC4q/hIpQ=
|
||||
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
|
||||
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
|
||||
github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
|
||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4=
|
||||
github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjYkaKubwuBdxEI=
|
||||
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA=
|
||||
github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw=
|
||||
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
|
||||
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
@@ -33,6 +106,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -44,6 +119,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -62,6 +139,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -71,3 +150,7 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
DatabaseURL string
|
||||
FlareSolverrURL string
|
||||
Addr string
|
||||
MangaListTTL time.Duration
|
||||
MangaDetailTTL time.Duration
|
||||
ChapterListTTL time.Duration
|
||||
DBMaxConns int
|
||||
DBMinConns int
|
||||
}
|
||||
|
||||
func Load() Config {
|
||||
return Config{
|
||||
DatabaseURL: os.Getenv("DATABASE_URL"),
|
||||
FlareSolverrURL: os.Getenv("FLARESOLVERR_URL"),
|
||||
Addr: envStr("ADDR", ":8080"),
|
||||
MangaListTTL: time.Duration(envInt("MANGA_LIST_TTL_SECONDS", 600)) * time.Second,
|
||||
MangaDetailTTL: time.Duration(envInt("MANGA_DETAIL_TTL_SECONDS", 3600)) * time.Second,
|
||||
ChapterListTTL: time.Duration(envInt("CHAPTER_LIST_TTL_SECONDS", 600)) * time.Second,
|
||||
DBMaxConns: envInt("DB_MAX_CONNS", 10),
|
||||
DBMinConns: envInt("DB_MIN_CONNS", 2),
|
||||
}
|
||||
}
|
||||
|
||||
func envStr(key, def string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func envInt(key string, def int) int {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/golang-migrate/migrate/v4"
|
||||
_ "github.com/golang-migrate/migrate/v4/database/pgx/v5"
|
||||
"github.com/golang-migrate/migrate/v4/source/iofs"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"goyomi/internal/db/queries"
|
||||
)
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationFS embed.FS
|
||||
|
||||
// DB holds the connection pool and sqlc query clients.
|
||||
type DB struct {
|
||||
Pool *pgxpool.Pool
|
||||
Queries *queries.Queries
|
||||
}
|
||||
|
||||
func Open(ctx context.Context) (*DB, error) {
|
||||
dsn := os.Getenv("DATABASE_URL")
|
||||
if dsn == "" {
|
||||
return nil, fmt.Errorf("DATABASE_URL not set")
|
||||
}
|
||||
|
||||
cfg, err := pgxpool.ParseConfig(dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse DATABASE_URL: %w", err)
|
||||
}
|
||||
cfg.MaxConns = int32(envInt("DB_MAX_CONNS", 10))
|
||||
cfg.MinConns = int32(envInt("DB_MIN_CONNS", 2))
|
||||
cfg.HealthCheckPeriod = 30 * time.Second
|
||||
|
||||
pool, err := pgxpool.NewWithConfig(ctx, cfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open pool: %w", err)
|
||||
}
|
||||
if err := pool.Ping(ctx); err != nil {
|
||||
pool.Close()
|
||||
return nil, fmt.Errorf("ping db: %w", err)
|
||||
}
|
||||
|
||||
if err := runMigrations(dsn); err != nil {
|
||||
pool.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &DB{
|
||||
Pool: pool,
|
||||
Queries: queries.New(pool),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *DB) Close() {
|
||||
d.Pool.Close()
|
||||
}
|
||||
|
||||
func runMigrations(dsn string) error {
|
||||
src, err := iofs.New(migrationFS, "migrations")
|
||||
if err != nil {
|
||||
return fmt.Errorf("migration source: %w", err)
|
||||
}
|
||||
|
||||
// golang-migrate pgx/v5 driver expects pgx5:// scheme
|
||||
driverDSN := dsnToMigrateScheme(dsn)
|
||||
|
||||
m, err := migrate.NewWithSourceInstance("iofs", src, driverDSN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("migrate init: %w", err)
|
||||
}
|
||||
defer m.Close()
|
||||
|
||||
before, _, _ := m.Version()
|
||||
if err := m.Up(); err != nil && err != migrate.ErrNoChange {
|
||||
return fmt.Errorf("migrate up: %w", err)
|
||||
}
|
||||
after, _, _ := m.Version()
|
||||
if before != after {
|
||||
log.Printf("db: migrated from version %d to %d", before, after)
|
||||
} else {
|
||||
log.Printf("db: schema up to date at version %d", after)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// dsnToMigrateScheme converts a postgres:// or postgresql:// DSN to pgx5://
|
||||
// as required by the golang-migrate pgx/v5 driver.
|
||||
func dsnToMigrateScheme(dsn string) string {
|
||||
for _, prefix := range []string{"postgresql://", "postgres://"} {
|
||||
if len(dsn) >= len(prefix) && dsn[:len(prefix)] == prefix {
|
||||
return "pgx5://" + dsn[len(prefix):]
|
||||
}
|
||||
}
|
||||
return dsn
|
||||
}
|
||||
|
||||
func envInt(key string, def int) int {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return def
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
DROP TABLE IF EXISTS source_meta;
|
||||
DROP TABLE IF EXISTS pages;
|
||||
DROP TABLE IF EXISTS chapters;
|
||||
DROP TABLE IF EXISTS manga;
|
||||
DROP TABLE IF EXISTS sources;
|
||||
@@ -0,0 +1,71 @@
|
||||
CREATE TABLE sources (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name VARCHAR(128) NOT NULL,
|
||||
lang VARCHAR(32) NOT NULL,
|
||||
is_nsfw BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
CREATE TABLE manga (
|
||||
id SERIAL PRIMARY KEY,
|
||||
source_id BIGINT NOT NULL REFERENCES sources(id),
|
||||
url VARCHAR(2048) NOT NULL,
|
||||
title VARCHAR(512) NOT NULL,
|
||||
initialized BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
artist TEXT,
|
||||
author TEXT,
|
||||
description TEXT,
|
||||
genre TEXT,
|
||||
status INTEGER NOT NULL DEFAULT 0,
|
||||
thumbnail_url VARCHAR(2048),
|
||||
thumbnail_last_fetched BIGINT NOT NULL DEFAULT 0,
|
||||
in_library BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
in_library_at BIGINT NOT NULL DEFAULT 0,
|
||||
real_url VARCHAR(2048),
|
||||
last_fetched_at BIGINT NOT NULL DEFAULT 0,
|
||||
chapters_last_fetched_at BIGINT NOT NULL DEFAULT 0,
|
||||
update_strategy VARCHAR(64) NOT NULL DEFAULT 'ALWAYS_UPDATE',
|
||||
UNIQUE (source_id, url)
|
||||
);
|
||||
|
||||
CREATE INDEX ON manga (source_id);
|
||||
CREATE INDEX ON manga (last_fetched_at);
|
||||
|
||||
CREATE TABLE chapters (
|
||||
id SERIAL PRIMARY KEY,
|
||||
manga_id INTEGER NOT NULL REFERENCES manga(id) ON DELETE CASCADE,
|
||||
url VARCHAR(2048) NOT NULL,
|
||||
name VARCHAR(512) NOT NULL,
|
||||
date_upload BIGINT NOT NULL DEFAULT 0,
|
||||
chapter_number REAL NOT NULL DEFAULT -1,
|
||||
scanlator VARCHAR(256),
|
||||
source_order INTEGER NOT NULL,
|
||||
is_read BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_bookmarked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
last_page_read INTEGER NOT NULL DEFAULT 0,
|
||||
last_read_at BIGINT NOT NULL DEFAULT 0,
|
||||
fetched_at BIGINT NOT NULL DEFAULT 0,
|
||||
real_url VARCHAR(2048),
|
||||
is_downloaded BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
page_count INTEGER NOT NULL DEFAULT -1,
|
||||
UNIQUE (manga_id, url)
|
||||
);
|
||||
|
||||
CREATE INDEX ON chapters (manga_id);
|
||||
|
||||
CREATE TABLE pages (
|
||||
id SERIAL PRIMARY KEY,
|
||||
chapter_id INTEGER NOT NULL REFERENCES chapters(id) ON DELETE CASCADE,
|
||||
"index" INTEGER NOT NULL,
|
||||
url VARCHAR(2048) NOT NULL,
|
||||
image_url TEXT,
|
||||
UNIQUE (chapter_id, "index")
|
||||
);
|
||||
|
||||
CREATE INDEX ON pages (chapter_id);
|
||||
|
||||
CREATE TABLE source_meta (
|
||||
source_id BIGINT NOT NULL REFERENCES sources(id),
|
||||
key VARCHAR(256) NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
PRIMARY KEY (source_id, key)
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
-- name: UpsertChapter :one
|
||||
INSERT INTO chapters (
|
||||
manga_id, url, name, date_upload, chapter_number, scanlator, source_order, fetched_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8
|
||||
)
|
||||
ON CONFLICT (manga_id, url) DO UPDATE
|
||||
SET name = EXCLUDED.name,
|
||||
date_upload = EXCLUDED.date_upload,
|
||||
chapter_number = EXCLUDED.chapter_number,
|
||||
scanlator = EXCLUDED.scanlator,
|
||||
source_order = EXCLUDED.source_order,
|
||||
fetched_at = EXCLUDED.fetched_at
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetChapterByID :one
|
||||
SELECT * FROM chapters WHERE id = $1;
|
||||
|
||||
-- name: ListChaptersByManga :many
|
||||
SELECT * FROM chapters WHERE manga_id = $1 ORDER BY source_order;
|
||||
|
||||
-- name: UpdateChapterFetchedAt :exec
|
||||
UPDATE chapters SET fetched_at = $2 WHERE id = $1;
|
||||
@@ -0,0 +1,153 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
// source: chapter.sql
|
||||
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const getChapterByID = `-- name: GetChapterByID :one
|
||||
SELECT id, manga_id, url, name, date_upload, chapter_number, scanlator, source_order, is_read, is_bookmarked, last_page_read, last_read_at, fetched_at, real_url, is_downloaded, page_count FROM chapters WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetChapterByID(ctx context.Context, id int32) (Chapter, error) {
|
||||
row := q.db.QueryRow(ctx, getChapterByID, id)
|
||||
var i Chapter
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.MangaID,
|
||||
&i.Url,
|
||||
&i.Name,
|
||||
&i.DateUpload,
|
||||
&i.ChapterNumber,
|
||||
&i.Scanlator,
|
||||
&i.SourceOrder,
|
||||
&i.IsRead,
|
||||
&i.IsBookmarked,
|
||||
&i.LastPageRead,
|
||||
&i.LastReadAt,
|
||||
&i.FetchedAt,
|
||||
&i.RealUrl,
|
||||
&i.IsDownloaded,
|
||||
&i.PageCount,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listChaptersByManga = `-- name: ListChaptersByManga :many
|
||||
SELECT id, manga_id, url, name, date_upload, chapter_number, scanlator, source_order, is_read, is_bookmarked, last_page_read, last_read_at, fetched_at, real_url, is_downloaded, page_count FROM chapters WHERE manga_id = $1 ORDER BY source_order
|
||||
`
|
||||
|
||||
func (q *Queries) ListChaptersByManga(ctx context.Context, mangaID int32) ([]Chapter, error) {
|
||||
rows, err := q.db.Query(ctx, listChaptersByManga, mangaID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Chapter
|
||||
for rows.Next() {
|
||||
var i Chapter
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.MangaID,
|
||||
&i.Url,
|
||||
&i.Name,
|
||||
&i.DateUpload,
|
||||
&i.ChapterNumber,
|
||||
&i.Scanlator,
|
||||
&i.SourceOrder,
|
||||
&i.IsRead,
|
||||
&i.IsBookmarked,
|
||||
&i.LastPageRead,
|
||||
&i.LastReadAt,
|
||||
&i.FetchedAt,
|
||||
&i.RealUrl,
|
||||
&i.IsDownloaded,
|
||||
&i.PageCount,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateChapterFetchedAt = `-- name: UpdateChapterFetchedAt :exec
|
||||
UPDATE chapters SET fetched_at = $2 WHERE id = $1
|
||||
`
|
||||
|
||||
type UpdateChapterFetchedAtParams struct {
|
||||
ID int32 `db:"id" json:"id"`
|
||||
FetchedAt int64 `db:"fetched_at" json:"fetched_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateChapterFetchedAt(ctx context.Context, arg UpdateChapterFetchedAtParams) error {
|
||||
_, err := q.db.Exec(ctx, updateChapterFetchedAt, arg.ID, arg.FetchedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
const upsertChapter = `-- name: UpsertChapter :one
|
||||
INSERT INTO chapters (
|
||||
manga_id, url, name, date_upload, chapter_number, scanlator, source_order, fetched_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8
|
||||
)
|
||||
ON CONFLICT (manga_id, url) DO UPDATE
|
||||
SET name = EXCLUDED.name,
|
||||
date_upload = EXCLUDED.date_upload,
|
||||
chapter_number = EXCLUDED.chapter_number,
|
||||
scanlator = EXCLUDED.scanlator,
|
||||
source_order = EXCLUDED.source_order,
|
||||
fetched_at = EXCLUDED.fetched_at
|
||||
RETURNING id, manga_id, url, name, date_upload, chapter_number, scanlator, source_order, is_read, is_bookmarked, last_page_read, last_read_at, fetched_at, real_url, is_downloaded, page_count
|
||||
`
|
||||
|
||||
type UpsertChapterParams struct {
|
||||
MangaID int32 `db:"manga_id" json:"manga_id"`
|
||||
Url string `db:"url" json:"url"`
|
||||
Name string `db:"name" json:"name"`
|
||||
DateUpload int64 `db:"date_upload" json:"date_upload"`
|
||||
ChapterNumber float32 `db:"chapter_number" json:"chapter_number"`
|
||||
Scanlator *string `db:"scanlator" json:"scanlator"`
|
||||
SourceOrder int32 `db:"source_order" json:"source_order"`
|
||||
FetchedAt int64 `db:"fetched_at" json:"fetched_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertChapter(ctx context.Context, arg UpsertChapterParams) (Chapter, error) {
|
||||
row := q.db.QueryRow(ctx, upsertChapter,
|
||||
arg.MangaID,
|
||||
arg.Url,
|
||||
arg.Name,
|
||||
arg.DateUpload,
|
||||
arg.ChapterNumber,
|
||||
arg.Scanlator,
|
||||
arg.SourceOrder,
|
||||
arg.FetchedAt,
|
||||
)
|
||||
var i Chapter
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.MangaID,
|
||||
&i.Url,
|
||||
&i.Name,
|
||||
&i.DateUpload,
|
||||
&i.ChapterNumber,
|
||||
&i.Scanlator,
|
||||
&i.SourceOrder,
|
||||
&i.IsRead,
|
||||
&i.IsBookmarked,
|
||||
&i.LastPageRead,
|
||||
&i.LastReadAt,
|
||||
&i.FetchedAt,
|
||||
&i.RealUrl,
|
||||
&i.IsDownloaded,
|
||||
&i.PageCount,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
type DBTX interface {
|
||||
Exec(context.Context, string, ...interface{}) (pgconn.CommandTag, error)
|
||||
Query(context.Context, string, ...interface{}) (pgx.Rows, error)
|
||||
QueryRow(context.Context, string, ...interface{}) pgx.Row
|
||||
}
|
||||
|
||||
func New(db DBTX) *Queries {
|
||||
return &Queries{db: db}
|
||||
}
|
||||
|
||||
type Queries struct {
|
||||
db DBTX
|
||||
}
|
||||
|
||||
func (q *Queries) WithTx(tx pgx.Tx) *Queries {
|
||||
return &Queries{
|
||||
db: tx,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
-- name: UpsertManga :one
|
||||
INSERT INTO manga (
|
||||
source_id, url, title, artist, author, description, genre,
|
||||
status, thumbnail_url, initialized, last_fetched_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
|
||||
)
|
||||
ON CONFLICT (source_id, url) DO UPDATE
|
||||
SET title = EXCLUDED.title,
|
||||
artist = EXCLUDED.artist,
|
||||
author = EXCLUDED.author,
|
||||
description = EXCLUDED.description,
|
||||
genre = EXCLUDED.genre,
|
||||
status = EXCLUDED.status,
|
||||
thumbnail_url = EXCLUDED.thumbnail_url,
|
||||
initialized = EXCLUDED.initialized,
|
||||
last_fetched_at = EXCLUDED.last_fetched_at
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetMangaBySourceURL :one
|
||||
SELECT * FROM manga WHERE source_id = $1 AND url = $2;
|
||||
|
||||
-- name: GetMangaByID :one
|
||||
SELECT * FROM manga WHERE id = $1;
|
||||
|
||||
-- name: ListMangaBySource :many
|
||||
SELECT * FROM manga
|
||||
WHERE source_id = $1
|
||||
ORDER BY last_fetched_at DESC;
|
||||
|
||||
-- name: UpdateMangaDetails :exec
|
||||
UPDATE manga
|
||||
SET artist = $2,
|
||||
author = $3,
|
||||
description = $4,
|
||||
genre = $5,
|
||||
status = $6,
|
||||
thumbnail_url = $7,
|
||||
initialized = TRUE
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: UpdateMangaFetchedAt :exec
|
||||
UPDATE manga SET last_fetched_at = $2 WHERE id = $1;
|
||||
|
||||
-- name: UpdateChaptersFetchedAt :exec
|
||||
UPDATE manga SET chapters_last_fetched_at = $2 WHERE id = $1;
|
||||
@@ -0,0 +1,255 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
// source: manga.sql
|
||||
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const getMangaByID = `-- name: GetMangaByID :one
|
||||
SELECT id, source_id, url, title, initialized, artist, author, description, genre, status, thumbnail_url, thumbnail_last_fetched, in_library, in_library_at, real_url, last_fetched_at, chapters_last_fetched_at, update_strategy FROM manga WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetMangaByID(ctx context.Context, id int32) (Manga, error) {
|
||||
row := q.db.QueryRow(ctx, getMangaByID, id)
|
||||
var i Manga
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SourceID,
|
||||
&i.Url,
|
||||
&i.Title,
|
||||
&i.Initialized,
|
||||
&i.Artist,
|
||||
&i.Author,
|
||||
&i.Description,
|
||||
&i.Genre,
|
||||
&i.Status,
|
||||
&i.ThumbnailUrl,
|
||||
&i.ThumbnailLastFetched,
|
||||
&i.InLibrary,
|
||||
&i.InLibraryAt,
|
||||
&i.RealUrl,
|
||||
&i.LastFetchedAt,
|
||||
&i.ChaptersLastFetchedAt,
|
||||
&i.UpdateStrategy,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getMangaBySourceURL = `-- name: GetMangaBySourceURL :one
|
||||
SELECT id, source_id, url, title, initialized, artist, author, description, genre, status, thumbnail_url, thumbnail_last_fetched, in_library, in_library_at, real_url, last_fetched_at, chapters_last_fetched_at, update_strategy FROM manga WHERE source_id = $1 AND url = $2
|
||||
`
|
||||
|
||||
type GetMangaBySourceURLParams struct {
|
||||
SourceID int64 `db:"source_id" json:"source_id"`
|
||||
Url string `db:"url" json:"url"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetMangaBySourceURL(ctx context.Context, arg GetMangaBySourceURLParams) (Manga, error) {
|
||||
row := q.db.QueryRow(ctx, getMangaBySourceURL, arg.SourceID, arg.Url)
|
||||
var i Manga
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SourceID,
|
||||
&i.Url,
|
||||
&i.Title,
|
||||
&i.Initialized,
|
||||
&i.Artist,
|
||||
&i.Author,
|
||||
&i.Description,
|
||||
&i.Genre,
|
||||
&i.Status,
|
||||
&i.ThumbnailUrl,
|
||||
&i.ThumbnailLastFetched,
|
||||
&i.InLibrary,
|
||||
&i.InLibraryAt,
|
||||
&i.RealUrl,
|
||||
&i.LastFetchedAt,
|
||||
&i.ChaptersLastFetchedAt,
|
||||
&i.UpdateStrategy,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listMangaBySource = `-- name: ListMangaBySource :many
|
||||
SELECT id, source_id, url, title, initialized, artist, author, description, genre, status, thumbnail_url, thumbnail_last_fetched, in_library, in_library_at, real_url, last_fetched_at, chapters_last_fetched_at, update_strategy FROM manga
|
||||
WHERE source_id = $1
|
||||
ORDER BY last_fetched_at DESC
|
||||
`
|
||||
|
||||
func (q *Queries) ListMangaBySource(ctx context.Context, sourceID int64) ([]Manga, error) {
|
||||
rows, err := q.db.Query(ctx, listMangaBySource, sourceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Manga
|
||||
for rows.Next() {
|
||||
var i Manga
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SourceID,
|
||||
&i.Url,
|
||||
&i.Title,
|
||||
&i.Initialized,
|
||||
&i.Artist,
|
||||
&i.Author,
|
||||
&i.Description,
|
||||
&i.Genre,
|
||||
&i.Status,
|
||||
&i.ThumbnailUrl,
|
||||
&i.ThumbnailLastFetched,
|
||||
&i.InLibrary,
|
||||
&i.InLibraryAt,
|
||||
&i.RealUrl,
|
||||
&i.LastFetchedAt,
|
||||
&i.ChaptersLastFetchedAt,
|
||||
&i.UpdateStrategy,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateChaptersFetchedAt = `-- name: UpdateChaptersFetchedAt :exec
|
||||
UPDATE manga SET chapters_last_fetched_at = $2 WHERE id = $1
|
||||
`
|
||||
|
||||
type UpdateChaptersFetchedAtParams struct {
|
||||
ID int32 `db:"id" json:"id"`
|
||||
ChaptersLastFetchedAt int64 `db:"chapters_last_fetched_at" json:"chapters_last_fetched_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateChaptersFetchedAt(ctx context.Context, arg UpdateChaptersFetchedAtParams) error {
|
||||
_, err := q.db.Exec(ctx, updateChaptersFetchedAt, arg.ID, arg.ChaptersLastFetchedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateMangaDetails = `-- name: UpdateMangaDetails :exec
|
||||
UPDATE manga
|
||||
SET artist = $2,
|
||||
author = $3,
|
||||
description = $4,
|
||||
genre = $5,
|
||||
status = $6,
|
||||
thumbnail_url = $7,
|
||||
initialized = TRUE
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
type UpdateMangaDetailsParams struct {
|
||||
ID int32 `db:"id" json:"id"`
|
||||
Artist *string `db:"artist" json:"artist"`
|
||||
Author *string `db:"author" json:"author"`
|
||||
Description *string `db:"description" json:"description"`
|
||||
Genre *string `db:"genre" json:"genre"`
|
||||
Status int32 `db:"status" json:"status"`
|
||||
ThumbnailUrl *string `db:"thumbnail_url" json:"thumbnail_url"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateMangaDetails(ctx context.Context, arg UpdateMangaDetailsParams) error {
|
||||
_, err := q.db.Exec(ctx, updateMangaDetails,
|
||||
arg.ID,
|
||||
arg.Artist,
|
||||
arg.Author,
|
||||
arg.Description,
|
||||
arg.Genre,
|
||||
arg.Status,
|
||||
arg.ThumbnailUrl,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateMangaFetchedAt = `-- name: UpdateMangaFetchedAt :exec
|
||||
UPDATE manga SET last_fetched_at = $2 WHERE id = $1
|
||||
`
|
||||
|
||||
type UpdateMangaFetchedAtParams struct {
|
||||
ID int32 `db:"id" json:"id"`
|
||||
LastFetchedAt int64 `db:"last_fetched_at" json:"last_fetched_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateMangaFetchedAt(ctx context.Context, arg UpdateMangaFetchedAtParams) error {
|
||||
_, err := q.db.Exec(ctx, updateMangaFetchedAt, arg.ID, arg.LastFetchedAt)
|
||||
return err
|
||||
}
|
||||
|
||||
const upsertManga = `-- name: UpsertManga :one
|
||||
INSERT INTO manga (
|
||||
source_id, url, title, artist, author, description, genre,
|
||||
status, thumbnail_url, initialized, last_fetched_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
|
||||
)
|
||||
ON CONFLICT (source_id, url) DO UPDATE
|
||||
SET title = EXCLUDED.title,
|
||||
artist = EXCLUDED.artist,
|
||||
author = EXCLUDED.author,
|
||||
description = EXCLUDED.description,
|
||||
genre = EXCLUDED.genre,
|
||||
status = EXCLUDED.status,
|
||||
thumbnail_url = EXCLUDED.thumbnail_url,
|
||||
initialized = EXCLUDED.initialized,
|
||||
last_fetched_at = EXCLUDED.last_fetched_at
|
||||
RETURNING id, source_id, url, title, initialized, artist, author, description, genre, status, thumbnail_url, thumbnail_last_fetched, in_library, in_library_at, real_url, last_fetched_at, chapters_last_fetched_at, update_strategy
|
||||
`
|
||||
|
||||
type UpsertMangaParams struct {
|
||||
SourceID int64 `db:"source_id" json:"source_id"`
|
||||
Url string `db:"url" json:"url"`
|
||||
Title string `db:"title" json:"title"`
|
||||
Artist *string `db:"artist" json:"artist"`
|
||||
Author *string `db:"author" json:"author"`
|
||||
Description *string `db:"description" json:"description"`
|
||||
Genre *string `db:"genre" json:"genre"`
|
||||
Status int32 `db:"status" json:"status"`
|
||||
ThumbnailUrl *string `db:"thumbnail_url" json:"thumbnail_url"`
|
||||
Initialized bool `db:"initialized" json:"initialized"`
|
||||
LastFetchedAt int64 `db:"last_fetched_at" json:"last_fetched_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertManga(ctx context.Context, arg UpsertMangaParams) (Manga, error) {
|
||||
row := q.db.QueryRow(ctx, upsertManga,
|
||||
arg.SourceID,
|
||||
arg.Url,
|
||||
arg.Title,
|
||||
arg.Artist,
|
||||
arg.Author,
|
||||
arg.Description,
|
||||
arg.Genre,
|
||||
arg.Status,
|
||||
arg.ThumbnailUrl,
|
||||
arg.Initialized,
|
||||
arg.LastFetchedAt,
|
||||
)
|
||||
var i Manga
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SourceID,
|
||||
&i.Url,
|
||||
&i.Title,
|
||||
&i.Initialized,
|
||||
&i.Artist,
|
||||
&i.Author,
|
||||
&i.Description,
|
||||
&i.Genre,
|
||||
&i.Status,
|
||||
&i.ThumbnailUrl,
|
||||
&i.ThumbnailLastFetched,
|
||||
&i.InLibrary,
|
||||
&i.InLibraryAt,
|
||||
&i.RealUrl,
|
||||
&i.LastFetchedAt,
|
||||
&i.ChaptersLastFetchedAt,
|
||||
&i.UpdateStrategy,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
|
||||
package queries
|
||||
|
||||
type Chapter struct {
|
||||
ID int32 `db:"id" json:"id"`
|
||||
MangaID int32 `db:"manga_id" json:"manga_id"`
|
||||
Url string `db:"url" json:"url"`
|
||||
Name string `db:"name" json:"name"`
|
||||
DateUpload int64 `db:"date_upload" json:"date_upload"`
|
||||
ChapterNumber float32 `db:"chapter_number" json:"chapter_number"`
|
||||
Scanlator *string `db:"scanlator" json:"scanlator"`
|
||||
SourceOrder int32 `db:"source_order" json:"source_order"`
|
||||
IsRead bool `db:"is_read" json:"is_read"`
|
||||
IsBookmarked bool `db:"is_bookmarked" json:"is_bookmarked"`
|
||||
LastPageRead int32 `db:"last_page_read" json:"last_page_read"`
|
||||
LastReadAt int64 `db:"last_read_at" json:"last_read_at"`
|
||||
FetchedAt int64 `db:"fetched_at" json:"fetched_at"`
|
||||
RealUrl *string `db:"real_url" json:"real_url"`
|
||||
IsDownloaded bool `db:"is_downloaded" json:"is_downloaded"`
|
||||
PageCount int32 `db:"page_count" json:"page_count"`
|
||||
}
|
||||
|
||||
type Manga struct {
|
||||
ID int32 `db:"id" json:"id"`
|
||||
SourceID int64 `db:"source_id" json:"source_id"`
|
||||
Url string `db:"url" json:"url"`
|
||||
Title string `db:"title" json:"title"`
|
||||
Initialized bool `db:"initialized" json:"initialized"`
|
||||
Artist *string `db:"artist" json:"artist"`
|
||||
Author *string `db:"author" json:"author"`
|
||||
Description *string `db:"description" json:"description"`
|
||||
Genre *string `db:"genre" json:"genre"`
|
||||
Status int32 `db:"status" json:"status"`
|
||||
ThumbnailUrl *string `db:"thumbnail_url" json:"thumbnail_url"`
|
||||
ThumbnailLastFetched int64 `db:"thumbnail_last_fetched" json:"thumbnail_last_fetched"`
|
||||
InLibrary bool `db:"in_library" json:"in_library"`
|
||||
InLibraryAt int64 `db:"in_library_at" json:"in_library_at"`
|
||||
RealUrl *string `db:"real_url" json:"real_url"`
|
||||
LastFetchedAt int64 `db:"last_fetched_at" json:"last_fetched_at"`
|
||||
ChaptersLastFetchedAt int64 `db:"chapters_last_fetched_at" json:"chapters_last_fetched_at"`
|
||||
UpdateStrategy string `db:"update_strategy" json:"update_strategy"`
|
||||
}
|
||||
|
||||
type Page struct {
|
||||
ID int32 `db:"id" json:"id"`
|
||||
ChapterID int32 `db:"chapter_id" json:"chapter_id"`
|
||||
Index int32 `db:"index" json:"index"`
|
||||
Url string `db:"url" json:"url"`
|
||||
ImageUrl *string `db:"image_url" json:"image_url"`
|
||||
}
|
||||
|
||||
type Source struct {
|
||||
ID int64 `db:"id" json:"id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Lang string `db:"lang" json:"lang"`
|
||||
IsNsfw bool `db:"is_nsfw" json:"is_nsfw"`
|
||||
}
|
||||
|
||||
type SourceMetum struct {
|
||||
SourceID int64 `db:"source_id" json:"source_id"`
|
||||
Key string `db:"key" json:"key"`
|
||||
Value string `db:"value" json:"value"`
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
-- name: UpsertPage :one
|
||||
INSERT INTO pages (chapter_id, "index", url, image_url)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (chapter_id, "index") DO UPDATE
|
||||
SET url = EXCLUDED.url,
|
||||
image_url = EXCLUDED.image_url
|
||||
RETURNING *;
|
||||
|
||||
-- name: ListPagesByChapter :many
|
||||
SELECT * FROM pages WHERE chapter_id = $1 ORDER BY "index";
|
||||
|
||||
-- name: UpdatePageImageURL :exec
|
||||
UPDATE pages SET image_url = $2 WHERE id = $1;
|
||||
@@ -0,0 +1,88 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
// source: page.sql
|
||||
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const listPagesByChapter = `-- name: ListPagesByChapter :many
|
||||
SELECT id, chapter_id, index, url, image_url FROM pages WHERE chapter_id = $1 ORDER BY "index"
|
||||
`
|
||||
|
||||
func (q *Queries) ListPagesByChapter(ctx context.Context, chapterID int32) ([]Page, error) {
|
||||
rows, err := q.db.Query(ctx, listPagesByChapter, chapterID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Page
|
||||
for rows.Next() {
|
||||
var i Page
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.ChapterID,
|
||||
&i.Index,
|
||||
&i.Url,
|
||||
&i.ImageUrl,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updatePageImageURL = `-- name: UpdatePageImageURL :exec
|
||||
UPDATE pages SET image_url = $2 WHERE id = $1
|
||||
`
|
||||
|
||||
type UpdatePageImageURLParams struct {
|
||||
ID int32 `db:"id" json:"id"`
|
||||
ImageUrl *string `db:"image_url" json:"image_url"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdatePageImageURL(ctx context.Context, arg UpdatePageImageURLParams) error {
|
||||
_, err := q.db.Exec(ctx, updatePageImageURL, arg.ID, arg.ImageUrl)
|
||||
return err
|
||||
}
|
||||
|
||||
const upsertPage = `-- name: UpsertPage :one
|
||||
INSERT INTO pages (chapter_id, "index", url, image_url)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (chapter_id, "index") DO UPDATE
|
||||
SET url = EXCLUDED.url,
|
||||
image_url = EXCLUDED.image_url
|
||||
RETURNING id, chapter_id, index, url, image_url
|
||||
`
|
||||
|
||||
type UpsertPageParams struct {
|
||||
ChapterID int32 `db:"chapter_id" json:"chapter_id"`
|
||||
Index int32 `db:"index" json:"index"`
|
||||
Url string `db:"url" json:"url"`
|
||||
ImageUrl *string `db:"image_url" json:"image_url"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertPage(ctx context.Context, arg UpsertPageParams) (Page, error) {
|
||||
row := q.db.QueryRow(ctx, upsertPage,
|
||||
arg.ChapterID,
|
||||
arg.Index,
|
||||
arg.Url,
|
||||
arg.ImageUrl,
|
||||
)
|
||||
var i Page
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.ChapterID,
|
||||
&i.Index,
|
||||
&i.Url,
|
||||
&i.ImageUrl,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
-- name: UpsertSource :one
|
||||
INSERT INTO sources (id, name, lang, is_nsfw)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (id) DO UPDATE
|
||||
SET name = EXCLUDED.name,
|
||||
lang = EXCLUDED.lang,
|
||||
is_nsfw = EXCLUDED.is_nsfw
|
||||
RETURNING *;
|
||||
|
||||
-- name: ListSources :many
|
||||
SELECT * FROM sources ORDER BY id;
|
||||
|
||||
-- name: GetSourceByID :one
|
||||
SELECT * FROM sources WHERE id = $1;
|
||||
|
||||
-- name: GetSourceMeta :one
|
||||
SELECT value FROM source_meta
|
||||
WHERE source_id = $1 AND key = $2;
|
||||
|
||||
-- name: SetSourceMeta :exec
|
||||
INSERT INTO source_meta (source_id, key, value)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (source_id, key) DO UPDATE
|
||||
SET value = EXCLUDED.value;
|
||||
@@ -0,0 +1,124 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
// source: source.sql
|
||||
|
||||
package queries
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
const getSourceByID = `-- name: GetSourceByID :one
|
||||
SELECT id, name, lang, is_nsfw FROM sources WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetSourceByID(ctx context.Context, id int64) (Source, error) {
|
||||
row := q.db.QueryRow(ctx, getSourceByID, id)
|
||||
var i Source
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Lang,
|
||||
&i.IsNsfw,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getSourceMeta = `-- name: GetSourceMeta :one
|
||||
SELECT value FROM source_meta
|
||||
WHERE source_id = $1 AND key = $2
|
||||
`
|
||||
|
||||
type GetSourceMetaParams struct {
|
||||
SourceID int64 `db:"source_id" json:"source_id"`
|
||||
Key string `db:"key" json:"key"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSourceMeta(ctx context.Context, arg GetSourceMetaParams) (string, error) {
|
||||
row := q.db.QueryRow(ctx, getSourceMeta, arg.SourceID, arg.Key)
|
||||
var value string
|
||||
err := row.Scan(&value)
|
||||
return value, err
|
||||
}
|
||||
|
||||
const listSources = `-- name: ListSources :many
|
||||
SELECT id, name, lang, is_nsfw FROM sources ORDER BY id
|
||||
`
|
||||
|
||||
func (q *Queries) ListSources(ctx context.Context) ([]Source, error) {
|
||||
rows, err := q.db.Query(ctx, listSources)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Source
|
||||
for rows.Next() {
|
||||
var i Source
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Lang,
|
||||
&i.IsNsfw,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const setSourceMeta = `-- name: SetSourceMeta :exec
|
||||
INSERT INTO source_meta (source_id, key, value)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (source_id, key) DO UPDATE
|
||||
SET value = EXCLUDED.value
|
||||
`
|
||||
|
||||
type SetSourceMetaParams struct {
|
||||
SourceID int64 `db:"source_id" json:"source_id"`
|
||||
Key string `db:"key" json:"key"`
|
||||
Value string `db:"value" json:"value"`
|
||||
}
|
||||
|
||||
func (q *Queries) SetSourceMeta(ctx context.Context, arg SetSourceMetaParams) error {
|
||||
_, err := q.db.Exec(ctx, setSourceMeta, arg.SourceID, arg.Key, arg.Value)
|
||||
return err
|
||||
}
|
||||
|
||||
const upsertSource = `-- name: UpsertSource :one
|
||||
INSERT INTO sources (id, name, lang, is_nsfw)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (id) DO UPDATE
|
||||
SET name = EXCLUDED.name,
|
||||
lang = EXCLUDED.lang,
|
||||
is_nsfw = EXCLUDED.is_nsfw
|
||||
RETURNING id, name, lang, is_nsfw
|
||||
`
|
||||
|
||||
type UpsertSourceParams struct {
|
||||
ID int64 `db:"id" json:"id"`
|
||||
Name string `db:"name" json:"name"`
|
||||
Lang string `db:"lang" json:"lang"`
|
||||
IsNsfw bool `db:"is_nsfw" json:"is_nsfw"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertSource(ctx context.Context, arg UpsertSourceParams) (Source, error) {
|
||||
row := q.db.QueryRow(ctx, upsertSource,
|
||||
arg.ID,
|
||||
arg.Name,
|
||||
arg.Lang,
|
||||
arg.IsNsfw,
|
||||
)
|
||||
var i Source
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Lang,
|
||||
&i.IsNsfw,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
Reference in New Issue
Block a user