refactor: reorganize project structure following go standards

This commit is contained in:
2026-04-20 15:54:35 +02:00
parent 055ec1fca9
commit 6df8788749
70 changed files with 43 additions and 187 deletions

View File

@@ -1,31 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package database
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}

View File

@@ -1,43 +0,0 @@
package database
import (
"context"
"database/sql"
"errors"
"fmt"
)
func NullStringOr(n sql.NullString, fallback string) string {
if n.Valid && n.String != "" {
return n.String
}
return fallback
}
func DisplayTitle(titleEnglish, titleJapanese sql.NullString, titleOriginal string) string {
return NullStringOr(titleEnglish, NullStringOr(titleJapanese, titleOriginal))
}
func (r GetUserWatchListRow) DisplayTitle() string {
return DisplayTitle(r.TitleEnglish, r.TitleJapanese, r.TitleOriginal)
}
func BoolPtr(b sql.NullBool) *bool {
if !b.Valid {
return nil
}
return &b.Bool
}
func BeginTx(ctx context.Context, db *sql.DB) (*Queries, *sql.Tx, error) {
if db == nil {
return nil, nil, errors.New("database unavailable")
}
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return nil, nil, fmt.Errorf("failed to begin transaction: %w", err)
}
return New(tx), tx, nil
}

View File

@@ -1,117 +0,0 @@
package database
import (
"database/sql"
"fmt"
"log"
"os"
"path/filepath"
"sort"
"strings"
)
func RunMigrations(db *sql.DB, migrationsDir string) error {
if migrationsDir == "" {
return fmt.Errorf("migrations directory is required")
}
// Create migration tracking table
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS migration_version (
name TEXT PRIMARY KEY,
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
)
`)
if err != nil {
return err
}
migrations, err := filepath.Glob(filepath.Join(migrationsDir, "*.sql"))
if err != nil {
return err
}
if len(migrations) == 0 {
return fmt.Errorf("no migration files found in %s", migrationsDir)
}
sort.Strings(migrations)
appliedNames, err := loadAppliedMigrationNames(db)
if err != nil {
return err
}
for _, migrationFile := range migrations {
migrationName := filepath.Base(migrationFile)
if migrationApplied(appliedNames, migrationName) {
// already applied, skipping silently
continue
}
// Read and execute migration
migrationSQL, err := os.ReadFile(migrationFile)
if err != nil {
return err
}
// Strict execution: if it fails, it halts.
if _, err := db.Exec(string(migrationSQL)); err != nil {
return err
}
// Mark as applied
_, err = db.Exec("INSERT INTO migration_version (name) VALUES (?)", migrationName)
if err != nil {
return err
}
appliedNames[migrationName] = struct{}{}
log.Printf("migration %s applied successfully", migrationName)
}
return nil
}
func loadAppliedMigrationNames(db *sql.DB) (map[string]struct{}, error) {
rows, err := db.Query("SELECT name FROM migration_version")
if err != nil {
return nil, err
}
defer rows.Close()
applied := make(map[string]struct{})
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
return nil, err
}
applied[name] = struct{}{}
}
if err := rows.Err(); err != nil {
return nil, err
}
return applied, nil
}
func migrationApplied(appliedNames map[string]struct{}, migrationName string) bool {
if _, exists := appliedNames[migrationName]; exists {
return true
}
legacyName := filepath.ToSlash(filepath.Join("migrations", migrationName))
if _, exists := appliedNames[legacyName]; exists {
return true
}
for appliedName := range appliedNames {
if strings.EqualFold(filepath.Base(appliedName), migrationName) {
return true
}
}
return false
}

View File

@@ -1,95 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package database
import (
"database/sql"
"time"
)
type Account struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Provider string `json:"provider"`
ProviderAccountID string `json:"provider_account_id"`
CreatedAt time.Time `json:"created_at"`
}
type Anime struct {
ID int64 `json:"id"`
TitleOriginal string `json:"title_original"`
ImageUrl string `json:"image_url"`
CreatedAt time.Time `json:"created_at"`
TitleEnglish sql.NullString `json:"title_english"`
TitleJapanese sql.NullString `json:"title_japanese"`
Airing sql.NullBool `json:"airing"`
Status sql.NullString `json:"status"`
RelationsSyncedAt sql.NullTime `json:"relations_synced_at"`
}
type AnimeFetchRetry struct {
AnimeID int64 `json:"anime_id"`
Attempts int64 `json:"attempts"`
NextRetryAt time.Time `json:"next_retry_at"`
LastError string `json:"last_error"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type AnimeRelation struct {
AnimeID int64 `json:"anime_id"`
RelatedAnimeID int64 `json:"related_anime_id"`
RelationType string `json:"relation_type"`
}
type ContinueWatchingEntry struct {
ID string `json:"id"`
UserID string `json:"user_id"`
AnimeID int64 `json:"anime_id"`
CurrentEpisode sql.NullInt64 `json:"current_episode"`
CurrentTimeSeconds float64 `json:"current_time_seconds"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type JikanCache struct {
Key string `json:"key"`
Data string `json:"data"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
}
type NotificationPreference struct {
ID string `json:"id"`
UserID string `json:"user_id"`
NotifyNewEpisodes bool `json:"notify_new_episodes"`
CreatedAt time.Time `json:"created_at"`
}
type Session struct {
ID string `json:"id"`
UserID string `json:"user_id"`
ExpiresAt time.Time `json:"expires_at"`
CreatedAt time.Time `json:"created_at"`
}
type User struct {
ID string `json:"id"`
Username string `json:"username"`
PasswordHash string `json:"password_hash"`
CreatedAt time.Time `json:"created_at"`
}
type WatchListEntry struct {
ID string `json:"id"`
UserID string `json:"user_id"`
AnimeID int64 `json:"anime_id"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CurrentEpisode sql.NullInt64 `json:"current_episode"`
LastEpisodeAt sql.NullTime `json:"last_episode_at"`
CurrentTimeSeconds float64 `json:"current_time_seconds"`
}

View File

@@ -1,47 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
package database
import (
"context"
)
type Querier interface {
CountPendingAnimeFetchRetries(ctx context.Context) (int64, error)
CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error)
CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
DeleteAnimeFetchRetry(ctx context.Context, animeID int64) error
DeleteContinueWatchingEntry(ctx context.Context, arg DeleteContinueWatchingEntryParams) error
DeleteExpiredJikanCache(ctx context.Context) error
DeleteSession(ctx context.Context, id string) error
DeleteUserSessions(ctx context.Context, userID string) error
DeleteWatchListEntry(ctx context.Context, arg DeleteWatchListEntryParams) error
EnqueueAnimeFetchRetry(ctx context.Context, arg EnqueueAnimeFetchRetryParams) error
GetAnime(ctx context.Context, id int64) (Anime, error)
GetAnimeNeedingRelationSync(ctx context.Context) ([]GetAnimeNeedingRelationSyncRow, error)
GetContinueWatchingEntries(ctx context.Context, userID string) ([]GetContinueWatchingEntriesRow, error)
GetContinueWatchingEntry(ctx context.Context, arg GetContinueWatchingEntryParams) (ContinueWatchingEntry, error)
GetDueAnimeFetchRetries(ctx context.Context, limit int64) ([]AnimeFetchRetry, error)
GetJikanCache(ctx context.Context, key string) (string, error)
GetJikanCacheStale(ctx context.Context, key string) (string, error)
GetSession(ctx context.Context, id string) (Session, error)
GetUpcomingSeasons(ctx context.Context, userID string) ([]GetUpcomingSeasonsRow, error)
GetUser(ctx context.Context, id string) (User, error)
GetUserByUsername(ctx context.Context, username string) (User, error)
GetUserWatchList(ctx context.Context, userID string) ([]GetUserWatchListRow, error)
GetWatchListEntry(ctx context.Context, arg GetWatchListEntryParams) (WatchListEntry, error)
GetWatchingAnime(ctx context.Context, userID string) ([]GetWatchingAnimeRow, error)
MarkAnimeFetchRetryFailed(ctx context.Context, arg MarkAnimeFetchRetryFailedParams) error
MarkRelationsSynced(ctx context.Context, id int64) error
SaveWatchProgress(ctx context.Context, arg SaveWatchProgressParams) error
SetJikanCache(ctx context.Context, arg SetJikanCacheParams) error
UpdateAnimeStatus(ctx context.Context, arg UpdateAnimeStatusParams) error
UpsertAnime(ctx context.Context, arg UpsertAnimeParams) (Anime, error)
UpsertAnimeRelation(ctx context.Context, arg UpsertAnimeRelationParams) error
UpsertContinueWatchingEntry(ctx context.Context, arg UpsertContinueWatchingEntryParams) (ContinueWatchingEntry, error)
UpsertWatchListEntry(ctx context.Context, arg UpsertWatchListEntryParams) (WatchListEntry, error)
}
var _ Querier = (*Queries)(nil)

View File

@@ -1,250 +0,0 @@
-- name: GetUser :one
SELECT * FROM user WHERE id = ? LIMIT 1;
-- name: GetUserByUsername :one
SELECT * FROM user WHERE username = ? LIMIT 1;
-- name: CreateUser :one
INSERT INTO user (id, username, password_hash)
VALUES (?, ?, ?)
RETURNING *;
-- name: CreateSession :one
INSERT INTO session (id, user_id, expires_at)
VALUES (?, ?, ?)
RETURNING *;
-- name: GetSession :one
SELECT * FROM session WHERE id = ? LIMIT 1;
-- name: DeleteSession :exec
DELETE FROM session WHERE id = ?;
-- name: DeleteUserSessions :exec
DELETE FROM session WHERE user_id = ?;
-- name: UpsertAnime :one
INSERT INTO anime (id, title_original, title_english, title_japanese, image_url, airing)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT (id) DO UPDATE SET
title_original = excluded.title_original,
title_english = excluded.title_english,
title_japanese = excluded.title_japanese,
image_url = excluded.image_url,
airing = excluded.airing
RETURNING *;
-- name: GetAnime :one
SELECT * FROM anime WHERE id = ? LIMIT 1;
-- name: UpsertWatchListEntry :one
INSERT INTO watch_list_entry (id, user_id, anime_id, status, current_episode, current_time_seconds, updated_at)
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
ON CONFLICT (user_id, anime_id) DO UPDATE SET
status = excluded.status,
current_episode = excluded.current_episode,
current_time_seconds = excluded.current_time_seconds,
updated_at = CURRENT_TIMESTAMP
RETURNING *;
-- name: SaveWatchProgress :exec
UPDATE watch_list_entry
SET current_episode = ?,
current_time_seconds = ?,
updated_at = CURRENT_TIMESTAMP
WHERE user_id = ? AND anime_id = ?;
-- name: UpsertContinueWatchingEntry :one
INSERT INTO continue_watching_entry (id, user_id, anime_id, current_episode, current_time_seconds, updated_at)
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
ON CONFLICT (user_id, anime_id) DO UPDATE SET
current_episode = excluded.current_episode,
current_time_seconds = excluded.current_time_seconds,
updated_at = CURRENT_TIMESTAMP
RETURNING *;
-- name: GetContinueWatchingEntry :one
SELECT * FROM continue_watching_entry
WHERE user_id = ? AND anime_id = ? LIMIT 1;
-- name: GetContinueWatchingEntries :many
SELECT
c.id,
c.user_id,
c.anime_id,
c.current_episode,
c.current_time_seconds,
c.created_at,
c.updated_at,
a.title_original,
a.title_english,
a.title_japanese,
a.image_url
FROM continue_watching_entry c
JOIN anime a ON c.anime_id = a.id
WHERE c.user_id = ?
ORDER BY c.updated_at DESC;
-- name: DeleteContinueWatchingEntry :exec
DELETE FROM continue_watching_entry
WHERE user_id = ? AND anime_id = ?;
-- name: GetWatchListEntry :one
SELECT * FROM watch_list_entry
WHERE user_id = ? AND anime_id = ? LIMIT 1;
-- name: GetUserWatchList :many
SELECT
e.*,
a.title_original,
a.title_english,
a.title_japanese,
a.image_url,
a.airing
FROM watch_list_entry e
JOIN anime a ON e.anime_id = a.id
WHERE e.user_id = ?
ORDER BY e.updated_at DESC;
-- name: DeleteWatchListEntry :exec
DELETE FROM watch_list_entry
WHERE user_id = ? AND anime_id = ?;
-- name: GetWatchingAnime :many
SELECT
e.*,
a.title_original,
a.title_english,
a.title_japanese,
a.image_url,
a.airing
FROM watch_list_entry e
JOIN anime a ON e.anime_id = a.id
WHERE e.user_id = ? AND e.status IN ('watching', 'plan_to_watch') AND a.airing = 1
ORDER BY e.updated_at DESC;
-- name: UpsertAnimeRelation :exec
INSERT INTO anime_relation (anime_id, related_anime_id, relation_type)
VALUES (?, ?, ?)
ON CONFLICT (anime_id, related_anime_id) DO UPDATE SET
relation_type = excluded.relation_type;
-- name: UpdateAnimeStatus :exec
UPDATE anime SET status = ? WHERE id = ?;
-- name: MarkRelationsSynced :exec
UPDATE anime SET relations_synced_at = CURRENT_TIMESTAMP WHERE id = ?;
-- name: GetAnimeNeedingRelationSync :many
WITH RECURSIVE sequel_chain AS (
SELECT a.id, a.title_original, a.relations_synced_at, w.updated_at as base_updated_at, 0 as depth
FROM watch_list_entry w
JOIN anime a ON w.anime_id = a.id
WHERE w.status IN ('completed', 'watching')
UNION
SELECT a.id, a.title_original, a.relations_synced_at, sc.base_updated_at, sc.depth + 1
FROM sequel_chain sc
JOIN anime_relation r ON sc.id = r.anime_id AND r.relation_type = 'Sequel'
JOIN anime a ON r.related_anime_id = a.id
WHERE sc.depth < 10
)
SELECT id, title_original
FROM sequel_chain
WHERE relations_synced_at IS NULL OR relations_synced_at < datetime('now', '-7 days')
GROUP BY id, title_original
ORDER BY MAX(base_updated_at) DESC, MIN(depth) ASC
LIMIT 50;
-- name: GetUpcomingSeasons :many
WITH RECURSIVE sequel_chain AS (
SELECT
w.anime_id as root_id,
a.title_original as root_title,
r.related_anime_id as current_id,
1 as depth,
w.user_id
FROM watch_list_entry w
JOIN anime a ON w.anime_id = a.id
JOIN anime_relation r ON w.anime_id = r.anime_id
WHERE w.user_id = ?
AND w.status IN ('completed', 'watching')
AND r.relation_type = 'Sequel'
UNION
SELECT
sc.root_id,
sc.root_title,
r.related_anime_id,
sc.depth + 1,
sc.user_id
FROM sequel_chain sc
JOIN anime_relation r ON sc.current_id = r.anime_id
WHERE r.relation_type = 'Sequel' AND sc.depth < 10
)
SELECT DISTINCT
related.*,
sc.root_title AS prequel_title
FROM sequel_chain sc
JOIN anime related ON sc.current_id = related.id
WHERE related.status IN ('Not yet aired', 'Currently Airing')
AND NOT EXISTS (
SELECT 1 FROM watch_list_entry we
WHERE we.user_id = sc.user_id AND we.anime_id = related.id
)
ORDER BY related.id DESC;
-- name: GetJikanCache :one
SELECT data FROM jikan_cache
WHERE key = ? AND expires_at > CURRENT_TIMESTAMP LIMIT 1;
-- name: GetJikanCacheStale :one
SELECT data FROM jikan_cache
WHERE key = ? LIMIT 1;
-- name: SetJikanCache :exec
INSERT INTO jikan_cache (key, data, expires_at)
VALUES (?, ?, ?)
ON CONFLICT (key) DO UPDATE SET
data = excluded.data,
expires_at = excluded.expires_at,
created_at = CURRENT_TIMESTAMP;
-- name: DeleteExpiredJikanCache :exec
DELETE FROM jikan_cache WHERE expires_at <= CURRENT_TIMESTAMP;
-- name: EnqueueAnimeFetchRetry :exec
INSERT INTO anime_fetch_retry (anime_id, attempts, next_retry_at, last_error, updated_at)
VALUES (?, 0, CURRENT_TIMESTAMP, ?, CURRENT_TIMESTAMP)
ON CONFLICT (anime_id) DO UPDATE SET
next_retry_at = CASE
WHEN anime_fetch_retry.next_retry_at > CURRENT_TIMESTAMP THEN anime_fetch_retry.next_retry_at
ELSE CURRENT_TIMESTAMP
END,
last_error = excluded.last_error,
updated_at = CURRENT_TIMESTAMP;
-- name: GetDueAnimeFetchRetries :many
SELECT anime_id, attempts, next_retry_at, last_error, created_at, updated_at
FROM anime_fetch_retry
WHERE next_retry_at <= CURRENT_TIMESTAMP
ORDER BY next_retry_at ASC
LIMIT ?;
-- name: MarkAnimeFetchRetryFailed :exec
UPDATE anime_fetch_retry
SET attempts = attempts + 1,
next_retry_at = datetime(CURRENT_TIMESTAMP, ?),
last_error = ?,
updated_at = CURRENT_TIMESTAMP
WHERE anime_id = ?;
-- name: DeleteAnimeFetchRetry :exec
DELETE FROM anime_fetch_retry
WHERE anime_id = ?;
-- name: CountPendingAnimeFetchRetries :one
SELECT COUNT(*)
FROM anime_fetch_retry
WHERE next_retry_at <= CURRENT_TIMESTAMP;

View File

@@ -1,923 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.30.0
// source: queries.sql
package database
import (
"context"
"database/sql"
"time"
)
const countPendingAnimeFetchRetries = `-- name: CountPendingAnimeFetchRetries :one
SELECT COUNT(*)
FROM anime_fetch_retry
WHERE next_retry_at <= CURRENT_TIMESTAMP
`
func (q *Queries) CountPendingAnimeFetchRetries(ctx context.Context) (int64, error) {
row := q.db.QueryRowContext(ctx, countPendingAnimeFetchRetries)
var count int64
err := row.Scan(&count)
return count, err
}
const createSession = `-- name: CreateSession :one
INSERT INTO session (id, user_id, expires_at)
VALUES (?, ?, ?)
RETURNING id, user_id, expires_at, created_at
`
type CreateSessionParams struct {
ID string `json:"id"`
UserID string `json:"user_id"`
ExpiresAt time.Time `json:"expires_at"`
}
func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) {
row := q.db.QueryRowContext(ctx, createSession, arg.ID, arg.UserID, arg.ExpiresAt)
var i Session
err := row.Scan(
&i.ID,
&i.UserID,
&i.ExpiresAt,
&i.CreatedAt,
)
return i, err
}
const createUser = `-- name: CreateUser :one
INSERT INTO user (id, username, password_hash)
VALUES (?, ?, ?)
RETURNING id, username, password_hash, created_at
`
type CreateUserParams struct {
ID string `json:"id"`
Username string `json:"username"`
PasswordHash string `json:"password_hash"`
}
func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) {
row := q.db.QueryRowContext(ctx, createUser, arg.ID, arg.Username, arg.PasswordHash)
var i User
err := row.Scan(
&i.ID,
&i.Username,
&i.PasswordHash,
&i.CreatedAt,
)
return i, err
}
const deleteAnimeFetchRetry = `-- name: DeleteAnimeFetchRetry :exec
DELETE FROM anime_fetch_retry
WHERE anime_id = ?
`
func (q *Queries) DeleteAnimeFetchRetry(ctx context.Context, animeID int64) error {
_, err := q.db.ExecContext(ctx, deleteAnimeFetchRetry, animeID)
return err
}
const deleteContinueWatchingEntry = `-- name: DeleteContinueWatchingEntry :exec
DELETE FROM continue_watching_entry
WHERE user_id = ? AND anime_id = ?
`
type DeleteContinueWatchingEntryParams struct {
UserID string `json:"user_id"`
AnimeID int64 `json:"anime_id"`
}
func (q *Queries) DeleteContinueWatchingEntry(ctx context.Context, arg DeleteContinueWatchingEntryParams) error {
_, err := q.db.ExecContext(ctx, deleteContinueWatchingEntry, arg.UserID, arg.AnimeID)
return err
}
const deleteExpiredJikanCache = `-- name: DeleteExpiredJikanCache :exec
DELETE FROM jikan_cache WHERE expires_at <= CURRENT_TIMESTAMP
`
func (q *Queries) DeleteExpiredJikanCache(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, deleteExpiredJikanCache)
return err
}
const deleteSession = `-- name: DeleteSession :exec
DELETE FROM session WHERE id = ?
`
func (q *Queries) DeleteSession(ctx context.Context, id string) error {
_, err := q.db.ExecContext(ctx, deleteSession, id)
return err
}
const deleteUserSessions = `-- name: DeleteUserSessions :exec
DELETE FROM session WHERE user_id = ?
`
func (q *Queries) DeleteUserSessions(ctx context.Context, userID string) error {
_, err := q.db.ExecContext(ctx, deleteUserSessions, userID)
return err
}
const deleteWatchListEntry = `-- name: DeleteWatchListEntry :exec
DELETE FROM watch_list_entry
WHERE user_id = ? AND anime_id = ?
`
type DeleteWatchListEntryParams struct {
UserID string `json:"user_id"`
AnimeID int64 `json:"anime_id"`
}
func (q *Queries) DeleteWatchListEntry(ctx context.Context, arg DeleteWatchListEntryParams) error {
_, err := q.db.ExecContext(ctx, deleteWatchListEntry, arg.UserID, arg.AnimeID)
return err
}
const enqueueAnimeFetchRetry = `-- name: EnqueueAnimeFetchRetry :exec
INSERT INTO anime_fetch_retry (anime_id, attempts, next_retry_at, last_error, updated_at)
VALUES (?, 0, CURRENT_TIMESTAMP, ?, CURRENT_TIMESTAMP)
ON CONFLICT (anime_id) DO UPDATE SET
next_retry_at = CASE
WHEN anime_fetch_retry.next_retry_at > CURRENT_TIMESTAMP THEN anime_fetch_retry.next_retry_at
ELSE CURRENT_TIMESTAMP
END,
last_error = excluded.last_error,
updated_at = CURRENT_TIMESTAMP
`
type EnqueueAnimeFetchRetryParams struct {
AnimeID int64 `json:"anime_id"`
LastError string `json:"last_error"`
}
func (q *Queries) EnqueueAnimeFetchRetry(ctx context.Context, arg EnqueueAnimeFetchRetryParams) error {
_, err := q.db.ExecContext(ctx, enqueueAnimeFetchRetry, arg.AnimeID, arg.LastError)
return err
}
const getAnime = `-- name: GetAnime :one
SELECT id, title_original, image_url, created_at, title_english, title_japanese, airing, status, relations_synced_at FROM anime WHERE id = ? LIMIT 1
`
func (q *Queries) GetAnime(ctx context.Context, id int64) (Anime, error) {
row := q.db.QueryRowContext(ctx, getAnime, id)
var i Anime
err := row.Scan(
&i.ID,
&i.TitleOriginal,
&i.ImageUrl,
&i.CreatedAt,
&i.TitleEnglish,
&i.TitleJapanese,
&i.Airing,
&i.Status,
&i.RelationsSyncedAt,
)
return i, err
}
const getAnimeNeedingRelationSync = `-- name: GetAnimeNeedingRelationSync :many
WITH RECURSIVE sequel_chain AS (
SELECT a.id, a.title_original, a.relations_synced_at, w.updated_at as base_updated_at, 0 as depth
FROM watch_list_entry w
JOIN anime a ON w.anime_id = a.id
WHERE w.status IN ('completed', 'watching')
UNION
SELECT a.id, a.title_original, a.relations_synced_at, sc.base_updated_at, sc.depth + 1
FROM sequel_chain sc
JOIN anime_relation r ON sc.id = r.anime_id AND r.relation_type = 'Sequel'
JOIN anime a ON r.related_anime_id = a.id
WHERE sc.depth < 10
)
SELECT id, title_original
FROM sequel_chain
WHERE relations_synced_at IS NULL OR relations_synced_at < datetime('now', '-7 days')
GROUP BY id, title_original
ORDER BY MAX(base_updated_at) DESC, MIN(depth) ASC
LIMIT 50
`
type GetAnimeNeedingRelationSyncRow struct {
ID int64 `json:"id"`
TitleOriginal string `json:"title_original"`
}
func (q *Queries) GetAnimeNeedingRelationSync(ctx context.Context) ([]GetAnimeNeedingRelationSyncRow, error) {
rows, err := q.db.QueryContext(ctx, getAnimeNeedingRelationSync)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetAnimeNeedingRelationSyncRow
for rows.Next() {
var i GetAnimeNeedingRelationSyncRow
if err := rows.Scan(&i.ID, &i.TitleOriginal); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getContinueWatchingEntries = `-- name: GetContinueWatchingEntries :many
SELECT
c.id,
c.user_id,
c.anime_id,
c.current_episode,
c.current_time_seconds,
c.created_at,
c.updated_at,
a.title_original,
a.title_english,
a.title_japanese,
a.image_url
FROM continue_watching_entry c
JOIN anime a ON c.anime_id = a.id
WHERE c.user_id = ?
ORDER BY c.updated_at DESC
`
type GetContinueWatchingEntriesRow struct {
ID string `json:"id"`
UserID string `json:"user_id"`
AnimeID int64 `json:"anime_id"`
CurrentEpisode sql.NullInt64 `json:"current_episode"`
CurrentTimeSeconds float64 `json:"current_time_seconds"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
TitleOriginal string `json:"title_original"`
TitleEnglish sql.NullString `json:"title_english"`
TitleJapanese sql.NullString `json:"title_japanese"`
ImageUrl string `json:"image_url"`
}
func (q *Queries) GetContinueWatchingEntries(ctx context.Context, userID string) ([]GetContinueWatchingEntriesRow, error) {
rows, err := q.db.QueryContext(ctx, getContinueWatchingEntries, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetContinueWatchingEntriesRow
for rows.Next() {
var i GetContinueWatchingEntriesRow
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.AnimeID,
&i.CurrentEpisode,
&i.CurrentTimeSeconds,
&i.CreatedAt,
&i.UpdatedAt,
&i.TitleOriginal,
&i.TitleEnglish,
&i.TitleJapanese,
&i.ImageUrl,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getContinueWatchingEntry = `-- name: GetContinueWatchingEntry :one
SELECT id, user_id, anime_id, current_episode, current_time_seconds, created_at, updated_at FROM continue_watching_entry
WHERE user_id = ? AND anime_id = ? LIMIT 1
`
type GetContinueWatchingEntryParams struct {
UserID string `json:"user_id"`
AnimeID int64 `json:"anime_id"`
}
func (q *Queries) GetContinueWatchingEntry(ctx context.Context, arg GetContinueWatchingEntryParams) (ContinueWatchingEntry, error) {
row := q.db.QueryRowContext(ctx, getContinueWatchingEntry, arg.UserID, arg.AnimeID)
var i ContinueWatchingEntry
err := row.Scan(
&i.ID,
&i.UserID,
&i.AnimeID,
&i.CurrentEpisode,
&i.CurrentTimeSeconds,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const getDueAnimeFetchRetries = `-- name: GetDueAnimeFetchRetries :many
SELECT anime_id, attempts, next_retry_at, last_error, created_at, updated_at
FROM anime_fetch_retry
WHERE next_retry_at <= CURRENT_TIMESTAMP
ORDER BY next_retry_at ASC
LIMIT ?
`
func (q *Queries) GetDueAnimeFetchRetries(ctx context.Context, limit int64) ([]AnimeFetchRetry, error) {
rows, err := q.db.QueryContext(ctx, getDueAnimeFetchRetries, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []AnimeFetchRetry
for rows.Next() {
var i AnimeFetchRetry
if err := rows.Scan(
&i.AnimeID,
&i.Attempts,
&i.NextRetryAt,
&i.LastError,
&i.CreatedAt,
&i.UpdatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getJikanCache = `-- name: GetJikanCache :one
SELECT data FROM jikan_cache
WHERE key = ? AND expires_at > CURRENT_TIMESTAMP LIMIT 1
`
func (q *Queries) GetJikanCache(ctx context.Context, key string) (string, error) {
row := q.db.QueryRowContext(ctx, getJikanCache, key)
var data string
err := row.Scan(&data)
return data, err
}
const getJikanCacheStale = `-- name: GetJikanCacheStale :one
SELECT data FROM jikan_cache
WHERE key = ? LIMIT 1
`
func (q *Queries) GetJikanCacheStale(ctx context.Context, key string) (string, error) {
row := q.db.QueryRowContext(ctx, getJikanCacheStale, key)
var data string
err := row.Scan(&data)
return data, err
}
const getSession = `-- name: GetSession :one
SELECT id, user_id, expires_at, created_at FROM session WHERE id = ? LIMIT 1
`
func (q *Queries) GetSession(ctx context.Context, id string) (Session, error) {
row := q.db.QueryRowContext(ctx, getSession, id)
var i Session
err := row.Scan(
&i.ID,
&i.UserID,
&i.ExpiresAt,
&i.CreatedAt,
)
return i, err
}
const getUpcomingSeasons = `-- name: GetUpcomingSeasons :many
WITH RECURSIVE sequel_chain AS (
SELECT
w.anime_id as root_id,
a.title_original as root_title,
r.related_anime_id as current_id,
1 as depth,
w.user_id
FROM watch_list_entry w
JOIN anime a ON w.anime_id = a.id
JOIN anime_relation r ON w.anime_id = r.anime_id
WHERE w.user_id = ?
AND w.status IN ('completed', 'watching')
AND r.relation_type = 'Sequel'
UNION
SELECT
sc.root_id,
sc.root_title,
r.related_anime_id,
sc.depth + 1,
sc.user_id
FROM sequel_chain sc
JOIN anime_relation r ON sc.current_id = r.anime_id
WHERE r.relation_type = 'Sequel' AND sc.depth < 10
)
SELECT DISTINCT
related.id, related.title_original, related.image_url, related.created_at, related.title_english, related.title_japanese, related.airing, related.status, related.relations_synced_at,
sc.root_title AS prequel_title
FROM sequel_chain sc
JOIN anime related ON sc.current_id = related.id
WHERE related.status IN ('Not yet aired', 'Currently Airing')
AND NOT EXISTS (
SELECT 1 FROM watch_list_entry we
WHERE we.user_id = sc.user_id AND we.anime_id = related.id
)
ORDER BY related.id DESC
`
type GetUpcomingSeasonsRow struct {
ID int64 `json:"id"`
TitleOriginal string `json:"title_original"`
ImageUrl string `json:"image_url"`
CreatedAt time.Time `json:"created_at"`
TitleEnglish sql.NullString `json:"title_english"`
TitleJapanese sql.NullString `json:"title_japanese"`
Airing sql.NullBool `json:"airing"`
Status sql.NullString `json:"status"`
RelationsSyncedAt sql.NullTime `json:"relations_synced_at"`
PrequelTitle string `json:"prequel_title"`
}
func (q *Queries) GetUpcomingSeasons(ctx context.Context, userID string) ([]GetUpcomingSeasonsRow, error) {
rows, err := q.db.QueryContext(ctx, getUpcomingSeasons, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetUpcomingSeasonsRow
for rows.Next() {
var i GetUpcomingSeasonsRow
if err := rows.Scan(
&i.ID,
&i.TitleOriginal,
&i.ImageUrl,
&i.CreatedAt,
&i.TitleEnglish,
&i.TitleJapanese,
&i.Airing,
&i.Status,
&i.RelationsSyncedAt,
&i.PrequelTitle,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getUser = `-- name: GetUser :one
SELECT id, username, password_hash, created_at FROM user WHERE id = ? LIMIT 1
`
func (q *Queries) GetUser(ctx context.Context, id string) (User, error) {
row := q.db.QueryRowContext(ctx, getUser, id)
var i User
err := row.Scan(
&i.ID,
&i.Username,
&i.PasswordHash,
&i.CreatedAt,
)
return i, err
}
const getUserByUsername = `-- name: GetUserByUsername :one
SELECT id, username, password_hash, created_at FROM user WHERE username = ? LIMIT 1
`
func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, error) {
row := q.db.QueryRowContext(ctx, getUserByUsername, username)
var i User
err := row.Scan(
&i.ID,
&i.Username,
&i.PasswordHash,
&i.CreatedAt,
)
return i, err
}
const getUserWatchList = `-- name: GetUserWatchList :many
SELECT
e.id, e.user_id, e.anime_id, e.status, e.created_at, e.updated_at, e.current_episode, e.last_episode_at, e.current_time_seconds,
a.title_original,
a.title_english,
a.title_japanese,
a.image_url,
a.airing
FROM watch_list_entry e
JOIN anime a ON e.anime_id = a.id
WHERE e.user_id = ?
ORDER BY e.updated_at DESC
`
type GetUserWatchListRow struct {
ID string `json:"id"`
UserID string `json:"user_id"`
AnimeID int64 `json:"anime_id"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CurrentEpisode sql.NullInt64 `json:"current_episode"`
LastEpisodeAt sql.NullTime `json:"last_episode_at"`
CurrentTimeSeconds float64 `json:"current_time_seconds"`
TitleOriginal string `json:"title_original"`
TitleEnglish sql.NullString `json:"title_english"`
TitleJapanese sql.NullString `json:"title_japanese"`
ImageUrl string `json:"image_url"`
Airing sql.NullBool `json:"airing"`
}
func (q *Queries) GetUserWatchList(ctx context.Context, userID string) ([]GetUserWatchListRow, error) {
rows, err := q.db.QueryContext(ctx, getUserWatchList, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetUserWatchListRow
for rows.Next() {
var i GetUserWatchListRow
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.AnimeID,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
&i.CurrentEpisode,
&i.LastEpisodeAt,
&i.CurrentTimeSeconds,
&i.TitleOriginal,
&i.TitleEnglish,
&i.TitleJapanese,
&i.ImageUrl,
&i.Airing,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const getWatchListEntry = `-- name: GetWatchListEntry :one
SELECT id, user_id, anime_id, status, created_at, updated_at, current_episode, last_episode_at, current_time_seconds FROM watch_list_entry
WHERE user_id = ? AND anime_id = ? LIMIT 1
`
type GetWatchListEntryParams struct {
UserID string `json:"user_id"`
AnimeID int64 `json:"anime_id"`
}
func (q *Queries) GetWatchListEntry(ctx context.Context, arg GetWatchListEntryParams) (WatchListEntry, error) {
row := q.db.QueryRowContext(ctx, getWatchListEntry, arg.UserID, arg.AnimeID)
var i WatchListEntry
err := row.Scan(
&i.ID,
&i.UserID,
&i.AnimeID,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
&i.CurrentEpisode,
&i.LastEpisodeAt,
&i.CurrentTimeSeconds,
)
return i, err
}
const getWatchingAnime = `-- name: GetWatchingAnime :many
SELECT
e.id, e.user_id, e.anime_id, e.status, e.created_at, e.updated_at, e.current_episode, e.last_episode_at, e.current_time_seconds,
a.title_original,
a.title_english,
a.title_japanese,
a.image_url,
a.airing
FROM watch_list_entry e
JOIN anime a ON e.anime_id = a.id
WHERE e.user_id = ? AND e.status IN ('watching', 'plan_to_watch') AND a.airing = 1
ORDER BY e.updated_at DESC
`
type GetWatchingAnimeRow struct {
ID string `json:"id"`
UserID string `json:"user_id"`
AnimeID int64 `json:"anime_id"`
Status string `json:"status"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
CurrentEpisode sql.NullInt64 `json:"current_episode"`
LastEpisodeAt sql.NullTime `json:"last_episode_at"`
CurrentTimeSeconds float64 `json:"current_time_seconds"`
TitleOriginal string `json:"title_original"`
TitleEnglish sql.NullString `json:"title_english"`
TitleJapanese sql.NullString `json:"title_japanese"`
ImageUrl string `json:"image_url"`
Airing sql.NullBool `json:"airing"`
}
func (q *Queries) GetWatchingAnime(ctx context.Context, userID string) ([]GetWatchingAnimeRow, error) {
rows, err := q.db.QueryContext(ctx, getWatchingAnime, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []GetWatchingAnimeRow
for rows.Next() {
var i GetWatchingAnimeRow
if err := rows.Scan(
&i.ID,
&i.UserID,
&i.AnimeID,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
&i.CurrentEpisode,
&i.LastEpisodeAt,
&i.CurrentTimeSeconds,
&i.TitleOriginal,
&i.TitleEnglish,
&i.TitleJapanese,
&i.ImageUrl,
&i.Airing,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const markAnimeFetchRetryFailed = `-- name: MarkAnimeFetchRetryFailed :exec
UPDATE anime_fetch_retry
SET attempts = attempts + 1,
next_retry_at = datetime(CURRENT_TIMESTAMP, ?),
last_error = ?,
updated_at = CURRENT_TIMESTAMP
WHERE anime_id = ?
`
type MarkAnimeFetchRetryFailedParams struct {
Datetime string `json:"datetime"`
LastError string `json:"last_error"`
AnimeID int64 `json:"anime_id"`
}
func (q *Queries) MarkAnimeFetchRetryFailed(ctx context.Context, arg MarkAnimeFetchRetryFailedParams) error {
_, err := q.db.ExecContext(ctx, markAnimeFetchRetryFailed, arg.Datetime, arg.LastError, arg.AnimeID)
return err
}
const markRelationsSynced = `-- name: MarkRelationsSynced :exec
UPDATE anime SET relations_synced_at = CURRENT_TIMESTAMP WHERE id = ?
`
func (q *Queries) MarkRelationsSynced(ctx context.Context, id int64) error {
_, err := q.db.ExecContext(ctx, markRelationsSynced, id)
return err
}
const saveWatchProgress = `-- name: SaveWatchProgress :exec
UPDATE watch_list_entry
SET current_episode = ?,
current_time_seconds = ?,
updated_at = CURRENT_TIMESTAMP
WHERE user_id = ? AND anime_id = ?
`
type SaveWatchProgressParams struct {
CurrentEpisode sql.NullInt64 `json:"current_episode"`
CurrentTimeSeconds float64 `json:"current_time_seconds"`
UserID string `json:"user_id"`
AnimeID int64 `json:"anime_id"`
}
func (q *Queries) SaveWatchProgress(ctx context.Context, arg SaveWatchProgressParams) error {
_, err := q.db.ExecContext(ctx, saveWatchProgress,
arg.CurrentEpisode,
arg.CurrentTimeSeconds,
arg.UserID,
arg.AnimeID,
)
return err
}
const setJikanCache = `-- name: SetJikanCache :exec
INSERT INTO jikan_cache (key, data, expires_at)
VALUES (?, ?, ?)
ON CONFLICT (key) DO UPDATE SET
data = excluded.data,
expires_at = excluded.expires_at,
created_at = CURRENT_TIMESTAMP
`
type SetJikanCacheParams struct {
Key string `json:"key"`
Data string `json:"data"`
ExpiresAt time.Time `json:"expires_at"`
}
func (q *Queries) SetJikanCache(ctx context.Context, arg SetJikanCacheParams) error {
_, err := q.db.ExecContext(ctx, setJikanCache, arg.Key, arg.Data, arg.ExpiresAt)
return err
}
const updateAnimeStatus = `-- name: UpdateAnimeStatus :exec
UPDATE anime SET status = ? WHERE id = ?
`
type UpdateAnimeStatusParams struct {
Status sql.NullString `json:"status"`
ID int64 `json:"id"`
}
func (q *Queries) UpdateAnimeStatus(ctx context.Context, arg UpdateAnimeStatusParams) error {
_, err := q.db.ExecContext(ctx, updateAnimeStatus, arg.Status, arg.ID)
return err
}
const upsertAnime = `-- name: UpsertAnime :one
INSERT INTO anime (id, title_original, title_english, title_japanese, image_url, airing)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT (id) DO UPDATE SET
title_original = excluded.title_original,
title_english = excluded.title_english,
title_japanese = excluded.title_japanese,
image_url = excluded.image_url,
airing = excluded.airing
RETURNING id, title_original, image_url, created_at, title_english, title_japanese, airing, status, relations_synced_at
`
type UpsertAnimeParams struct {
ID int64 `json:"id"`
TitleOriginal string `json:"title_original"`
TitleEnglish sql.NullString `json:"title_english"`
TitleJapanese sql.NullString `json:"title_japanese"`
ImageUrl string `json:"image_url"`
Airing sql.NullBool `json:"airing"`
}
func (q *Queries) UpsertAnime(ctx context.Context, arg UpsertAnimeParams) (Anime, error) {
row := q.db.QueryRowContext(ctx, upsertAnime,
arg.ID,
arg.TitleOriginal,
arg.TitleEnglish,
arg.TitleJapanese,
arg.ImageUrl,
arg.Airing,
)
var i Anime
err := row.Scan(
&i.ID,
&i.TitleOriginal,
&i.ImageUrl,
&i.CreatedAt,
&i.TitleEnglish,
&i.TitleJapanese,
&i.Airing,
&i.Status,
&i.RelationsSyncedAt,
)
return i, err
}
const upsertAnimeRelation = `-- name: UpsertAnimeRelation :exec
INSERT INTO anime_relation (anime_id, related_anime_id, relation_type)
VALUES (?, ?, ?)
ON CONFLICT (anime_id, related_anime_id) DO UPDATE SET
relation_type = excluded.relation_type
`
type UpsertAnimeRelationParams struct {
AnimeID int64 `json:"anime_id"`
RelatedAnimeID int64 `json:"related_anime_id"`
RelationType string `json:"relation_type"`
}
func (q *Queries) UpsertAnimeRelation(ctx context.Context, arg UpsertAnimeRelationParams) error {
_, err := q.db.ExecContext(ctx, upsertAnimeRelation, arg.AnimeID, arg.RelatedAnimeID, arg.RelationType)
return err
}
const upsertContinueWatchingEntry = `-- name: UpsertContinueWatchingEntry :one
INSERT INTO continue_watching_entry (id, user_id, anime_id, current_episode, current_time_seconds, updated_at)
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
ON CONFLICT (user_id, anime_id) DO UPDATE SET
current_episode = excluded.current_episode,
current_time_seconds = excluded.current_time_seconds,
updated_at = CURRENT_TIMESTAMP
RETURNING id, user_id, anime_id, current_episode, current_time_seconds, created_at, updated_at
`
type UpsertContinueWatchingEntryParams struct {
ID string `json:"id"`
UserID string `json:"user_id"`
AnimeID int64 `json:"anime_id"`
CurrentEpisode sql.NullInt64 `json:"current_episode"`
CurrentTimeSeconds float64 `json:"current_time_seconds"`
}
func (q *Queries) UpsertContinueWatchingEntry(ctx context.Context, arg UpsertContinueWatchingEntryParams) (ContinueWatchingEntry, error) {
row := q.db.QueryRowContext(ctx, upsertContinueWatchingEntry,
arg.ID,
arg.UserID,
arg.AnimeID,
arg.CurrentEpisode,
arg.CurrentTimeSeconds,
)
var i ContinueWatchingEntry
err := row.Scan(
&i.ID,
&i.UserID,
&i.AnimeID,
&i.CurrentEpisode,
&i.CurrentTimeSeconds,
&i.CreatedAt,
&i.UpdatedAt,
)
return i, err
}
const upsertWatchListEntry = `-- name: UpsertWatchListEntry :one
INSERT INTO watch_list_entry (id, user_id, anime_id, status, current_episode, current_time_seconds, updated_at)
VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
ON CONFLICT (user_id, anime_id) DO UPDATE SET
status = excluded.status,
current_episode = excluded.current_episode,
current_time_seconds = excluded.current_time_seconds,
updated_at = CURRENT_TIMESTAMP
RETURNING id, user_id, anime_id, status, created_at, updated_at, current_episode, last_episode_at, current_time_seconds
`
type UpsertWatchListEntryParams struct {
ID string `json:"id"`
UserID string `json:"user_id"`
AnimeID int64 `json:"anime_id"`
Status string `json:"status"`
CurrentEpisode sql.NullInt64 `json:"current_episode"`
CurrentTimeSeconds float64 `json:"current_time_seconds"`
}
func (q *Queries) UpsertWatchListEntry(ctx context.Context, arg UpsertWatchListEntryParams) (WatchListEntry, error) {
row := q.db.QueryRowContext(ctx, upsertWatchListEntry,
arg.ID,
arg.UserID,
arg.AnimeID,
arg.Status,
arg.CurrentEpisode,
arg.CurrentTimeSeconds,
)
var i WatchListEntry
err := row.Scan(
&i.ID,
&i.UserID,
&i.AnimeID,
&i.Status,
&i.CreatedAt,
&i.UpdatedAt,
&i.CurrentEpisode,
&i.LastEpisodeAt,
&i.CurrentTimeSeconds,
)
return i, err
}