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:
@@ -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