1134 lines
41 KiB
Go
1134 lines
41 KiB
Go
package watchsync
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
type Repository interface {
|
|
GetServerSetting(ctx context.Context, key string) (string, error)
|
|
UpsertAuthSession(ctx context.Context, session DeviceAuthSession) (DeviceAuthSession, error)
|
|
GetAuthSession(ctx context.Context, id string) (DeviceAuthSession, error)
|
|
UpsertConnection(ctx context.Context, conn Connection) (Connection, error)
|
|
GetConnection(ctx context.Context, provider string, userID int, profileID string) (Connection, bool, error)
|
|
GetConnectionByID(ctx context.Context, id string) (Connection, bool, error)
|
|
DeleteConnection(ctx context.Context, provider string, userID int, profileID string) error
|
|
ListConnectionsDueForSync(ctx context.Context, now time.Time) ([]Connection, error)
|
|
CreateSyncRun(ctx context.Context, run SyncRun) (SyncRun, error)
|
|
CompleteSyncRun(ctx context.Context, run SyncRun) (SyncRun, error)
|
|
GetLatestSyncRun(ctx context.Context, connectionID string) (SyncRun, bool, error)
|
|
GetActiveSyncRun(ctx context.Context, connectionID string) (SyncRun, bool, error)
|
|
ListSyncRuns(ctx context.Context, connectionID string, limit int) ([]SyncRun, error)
|
|
ListLocalWatchEventConnections(ctx context.Context, userID int, profileID string, kind LocalWatchEventKind) ([]Connection, error)
|
|
ListFavoriteEventConnections(ctx context.Context, userID int, profileID string, kind LocalFavoriteEventKind) ([]Connection, error)
|
|
UpsertHistoryExports(ctx context.Context, exports []HistoryExport) error
|
|
ListPendingHistoryExports(ctx context.Context, connectionID string, limit int) ([]HistoryExport, error)
|
|
MarkHistoryExportStatus(ctx context.Context, id string, status string, lastError string) error
|
|
UpsertFavoriteStates(ctx context.Context, states []FavoriteState) error
|
|
ListFavoriteStates(ctx context.Context, connectionID string) ([]FavoriteState, error)
|
|
ListPendingFavoriteExports(ctx context.Context, connectionID string, limit int) ([]FavoriteState, error)
|
|
ListPendingFavoriteRemovals(ctx context.Context, connectionID string, limit int) ([]FavoriteState, error)
|
|
MarkFavoriteExported(ctx context.Context, connectionID, mediaItemID string, exportedAt time.Time) error
|
|
MarkFavoriteRemoteRemoved(ctx context.Context, connectionID, mediaItemID string, removedAt time.Time) error
|
|
MarkFavoriteLocalRemoved(ctx context.Context, connectionID, mediaItemID string, removedAt time.Time) error
|
|
MarkFavoriteError(ctx context.Context, connectionID, mediaItemID, lastError string) error
|
|
ListScrobbleConnections(ctx context.Context, userID int, profileID string) ([]Connection, error)
|
|
UpsertScrobbleSession(ctx context.Context, event ScrobbleEvent, connectionID string, action string) error
|
|
UpdateScrobbleSession(ctx context.Context, playbackSessionID string, connectionID string, action string, progress float64, historyID string, lastError string, stopSentAt *time.Time) error
|
|
ListOpenScrobbleSessions(ctx context.Context) ([]ScrobbleSession, error)
|
|
}
|
|
|
|
type PostgresRepository struct {
|
|
pool *pgxpool.Pool
|
|
}
|
|
|
|
func NewPostgresRepository(pool *pgxpool.Pool) *PostgresRepository {
|
|
return &PostgresRepository{pool: pool}
|
|
}
|
|
|
|
func (r *PostgresRepository) GetServerSetting(ctx context.Context, key string) (string, error) {
|
|
var value string
|
|
err := r.pool.QueryRow(ctx, `SELECT value FROM server_settings WHERE key = $1`, key).Scan(&value)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return "", nil
|
|
}
|
|
if err != nil {
|
|
return "", fmt.Errorf("server_settings get %q: %w", key, err)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func (r *PostgresRepository) UpsertAuthSession(
|
|
ctx context.Context,
|
|
session DeviceAuthSession,
|
|
) (DeviceAuthSession, error) {
|
|
row := r.pool.QueryRow(ctx, `
|
|
INSERT INTO watch_provider_auth_sessions (
|
|
id, provider, user_id, profile_id, device_code, user_code,
|
|
verification_url, interval_seconds, expires_at, completed_at
|
|
)
|
|
VALUES (
|
|
COALESCE(NULLIF($1, '')::uuid, gen_random_uuid()),
|
|
$2, $3, $4, $5, $6, $7, $8, $9, $10
|
|
)
|
|
ON CONFLICT (id) DO UPDATE SET
|
|
provider = EXCLUDED.provider,
|
|
user_id = EXCLUDED.user_id,
|
|
profile_id = EXCLUDED.profile_id,
|
|
device_code = EXCLUDED.device_code,
|
|
user_code = EXCLUDED.user_code,
|
|
verification_url = EXCLUDED.verification_url,
|
|
interval_seconds = EXCLUDED.interval_seconds,
|
|
expires_at = EXCLUDED.expires_at,
|
|
completed_at = EXCLUDED.completed_at,
|
|
updated_at = now()
|
|
RETURNING
|
|
id::text, provider, user_id, profile_id, device_code, user_code,
|
|
verification_url, interval_seconds, expires_at, completed_at
|
|
`,
|
|
session.ID,
|
|
session.Provider,
|
|
session.UserID,
|
|
session.ProfileID,
|
|
session.DeviceCode,
|
|
session.UserCode,
|
|
session.VerificationURL,
|
|
session.IntervalSeconds,
|
|
session.ExpiresAt,
|
|
session.CompletedAt,
|
|
)
|
|
saved, err := scanDeviceAuthSession(row)
|
|
if err != nil {
|
|
return DeviceAuthSession{}, fmt.Errorf("upsert watch provider auth session: %w", err)
|
|
}
|
|
return saved, nil
|
|
}
|
|
|
|
func (r *PostgresRepository) GetAuthSession(ctx context.Context, id string) (DeviceAuthSession, error) {
|
|
row := r.pool.QueryRow(ctx, `
|
|
SELECT
|
|
id::text, provider, user_id, profile_id, device_code, user_code,
|
|
verification_url, interval_seconds, expires_at, completed_at
|
|
FROM watch_provider_auth_sessions
|
|
WHERE id = $1::uuid
|
|
`, id)
|
|
session, err := scanDeviceAuthSession(row)
|
|
if err != nil {
|
|
return DeviceAuthSession{}, fmt.Errorf("get watch provider auth session %q: %w", id, err)
|
|
}
|
|
return session, nil
|
|
}
|
|
|
|
func (r *PostgresRepository) UpsertConnection(ctx context.Context, conn Connection) (Connection, error) {
|
|
row := r.pool.QueryRow(ctx, `
|
|
INSERT INTO watch_provider_connections (
|
|
id, provider, user_id, profile_id, provider_account_id, provider_username,
|
|
access_token, refresh_token, token_expires_at, import_watched_enabled,
|
|
import_progress_enabled, export_watched_enabled, export_unwatched_enabled,
|
|
import_favorites_enabled, export_favorites_enabled, sync_favorite_removals_enabled,
|
|
scrobble_enabled, last_inbound_sync_at, last_progress_sync_at, last_outbound_sync_at,
|
|
last_favorites_sync_at, last_scrobble_error_at, last_error, sync_cursors
|
|
)
|
|
VALUES (
|
|
COALESCE(NULLIF($1, '')::uuid, gen_random_uuid()),
|
|
$2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13,
|
|
$14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24::jsonb
|
|
)
|
|
ON CONFLICT (provider, user_id, profile_id) DO UPDATE SET
|
|
provider_account_id = EXCLUDED.provider_account_id,
|
|
provider_username = EXCLUDED.provider_username,
|
|
access_token = EXCLUDED.access_token,
|
|
refresh_token = EXCLUDED.refresh_token,
|
|
token_expires_at = EXCLUDED.token_expires_at,
|
|
import_watched_enabled = EXCLUDED.import_watched_enabled,
|
|
import_progress_enabled = EXCLUDED.import_progress_enabled,
|
|
export_watched_enabled = EXCLUDED.export_watched_enabled,
|
|
export_unwatched_enabled = EXCLUDED.export_unwatched_enabled,
|
|
import_favorites_enabled = EXCLUDED.import_favorites_enabled,
|
|
export_favorites_enabled = EXCLUDED.export_favorites_enabled,
|
|
sync_favorite_removals_enabled = EXCLUDED.sync_favorite_removals_enabled,
|
|
scrobble_enabled = EXCLUDED.scrobble_enabled,
|
|
last_inbound_sync_at = EXCLUDED.last_inbound_sync_at,
|
|
last_progress_sync_at = EXCLUDED.last_progress_sync_at,
|
|
last_outbound_sync_at = EXCLUDED.last_outbound_sync_at,
|
|
last_favorites_sync_at = EXCLUDED.last_favorites_sync_at,
|
|
last_scrobble_error_at = EXCLUDED.last_scrobble_error_at,
|
|
last_error = EXCLUDED.last_error,
|
|
sync_cursors = EXCLUDED.sync_cursors,
|
|
updated_at = now()
|
|
RETURNING
|
|
id::text, provider, user_id, profile_id, provider_account_id, provider_username,
|
|
access_token, refresh_token, token_expires_at, import_watched_enabled,
|
|
import_progress_enabled, export_watched_enabled, export_unwatched_enabled,
|
|
import_favorites_enabled, export_favorites_enabled, sync_favorite_removals_enabled,
|
|
scrobble_enabled, last_inbound_sync_at, last_progress_sync_at, last_outbound_sync_at,
|
|
last_favorites_sync_at, last_scrobble_error_at, last_error, sync_cursors, created_at, updated_at
|
|
`,
|
|
conn.ID,
|
|
conn.Provider,
|
|
conn.UserID,
|
|
conn.ProfileID,
|
|
conn.ProviderAccountID,
|
|
conn.ProviderUsername,
|
|
conn.AccessToken,
|
|
conn.RefreshToken,
|
|
conn.TokenExpiresAt,
|
|
conn.ImportWatchedEnabled,
|
|
conn.ImportProgressEnabled,
|
|
conn.ExportWatchedEnabled,
|
|
conn.ExportUnwatchedEnabled,
|
|
conn.ImportFavoritesEnabled,
|
|
conn.ExportFavoritesEnabled,
|
|
conn.SyncFavoriteRemovalsEnabled,
|
|
conn.ScrobbleEnabled,
|
|
conn.LastInboundSyncAt,
|
|
conn.LastProgressSyncAt,
|
|
conn.LastOutboundSyncAt,
|
|
conn.LastFavoritesSyncAt,
|
|
conn.LastScrobbleErrorAt,
|
|
conn.LastError,
|
|
encodeSyncCursors(conn.SyncCursors),
|
|
)
|
|
saved, err := scanConnection(row)
|
|
if err != nil {
|
|
return Connection{}, fmt.Errorf("upsert watch provider connection: %w", err)
|
|
}
|
|
return saved, nil
|
|
}
|
|
|
|
func (r *PostgresRepository) GetConnection(
|
|
ctx context.Context,
|
|
provider string,
|
|
userID int,
|
|
profileID string,
|
|
) (Connection, bool, error) {
|
|
row := r.pool.QueryRow(ctx, `
|
|
SELECT
|
|
id::text, provider, user_id, profile_id, provider_account_id, provider_username,
|
|
access_token, refresh_token, token_expires_at, import_watched_enabled,
|
|
import_progress_enabled, export_watched_enabled, export_unwatched_enabled,
|
|
import_favorites_enabled, export_favorites_enabled, sync_favorite_removals_enabled,
|
|
scrobble_enabled, last_inbound_sync_at, last_progress_sync_at, last_outbound_sync_at,
|
|
last_favorites_sync_at, last_scrobble_error_at, last_error, sync_cursors, created_at, updated_at
|
|
FROM watch_provider_connections
|
|
WHERE provider = $1 AND user_id = $2 AND profile_id = $3
|
|
`, provider, userID, profileID)
|
|
conn, err := scanConnection(row)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return Connection{}, false, nil
|
|
}
|
|
if err != nil {
|
|
return Connection{}, false, fmt.Errorf("get watch provider connection: %w", err)
|
|
}
|
|
return conn, true, nil
|
|
}
|
|
|
|
func (r *PostgresRepository) GetConnectionByID(ctx context.Context, id string) (Connection, bool, error) {
|
|
row := r.pool.QueryRow(ctx, `
|
|
SELECT
|
|
id::text, provider, user_id, profile_id, provider_account_id, provider_username,
|
|
access_token, refresh_token, token_expires_at, import_watched_enabled,
|
|
import_progress_enabled, export_watched_enabled, export_unwatched_enabled,
|
|
import_favorites_enabled, export_favorites_enabled, sync_favorite_removals_enabled,
|
|
scrobble_enabled, last_inbound_sync_at, last_progress_sync_at, last_outbound_sync_at,
|
|
last_favorites_sync_at, last_scrobble_error_at, last_error, sync_cursors, created_at, updated_at
|
|
FROM watch_provider_connections
|
|
WHERE id = $1::uuid
|
|
`, id)
|
|
conn, err := scanConnection(row)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return Connection{}, false, nil
|
|
}
|
|
if err != nil {
|
|
return Connection{}, false, fmt.Errorf("get watch provider connection by id: %w", err)
|
|
}
|
|
return conn, true, nil
|
|
}
|
|
|
|
func (r *PostgresRepository) DeleteConnection(
|
|
ctx context.Context,
|
|
provider string,
|
|
userID int,
|
|
profileID string,
|
|
) error {
|
|
_, err := r.pool.Exec(ctx, `
|
|
DELETE FROM watch_provider_connections
|
|
WHERE provider = $1 AND user_id = $2 AND profile_id = $3
|
|
`, provider, userID, profileID)
|
|
if err != nil {
|
|
return fmt.Errorf("delete watch provider connection: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *PostgresRepository) ListConnectionsDueForSync(
|
|
ctx context.Context,
|
|
_ time.Time,
|
|
) ([]Connection, error) {
|
|
rows, err := r.pool.Query(ctx, `
|
|
SELECT
|
|
id::text, provider, user_id, profile_id, provider_account_id, provider_username,
|
|
access_token, refresh_token, token_expires_at, import_watched_enabled,
|
|
import_progress_enabled, export_watched_enabled, export_unwatched_enabled,
|
|
import_favorites_enabled, export_favorites_enabled, sync_favorite_removals_enabled,
|
|
scrobble_enabled, last_inbound_sync_at, last_progress_sync_at, last_outbound_sync_at,
|
|
last_favorites_sync_at, last_scrobble_error_at, last_error, sync_cursors, created_at, updated_at
|
|
FROM watch_provider_connections
|
|
WHERE provider <> ''
|
|
AND (
|
|
import_watched_enabled
|
|
OR import_progress_enabled
|
|
OR export_watched_enabled
|
|
OR export_unwatched_enabled
|
|
OR import_favorites_enabled
|
|
OR export_favorites_enabled
|
|
OR sync_favorite_removals_enabled
|
|
OR scrobble_enabled
|
|
)
|
|
ORDER BY provider, user_id, profile_id
|
|
`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list due watch provider connections: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var conns []Connection
|
|
for rows.Next() {
|
|
conn, scanErr := scanConnection(rows)
|
|
if scanErr != nil {
|
|
return nil, fmt.Errorf("scan due watch provider connection: %w", scanErr)
|
|
}
|
|
conns = append(conns, conn)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("iterate due watch provider connections: %w", err)
|
|
}
|
|
return conns, nil
|
|
}
|
|
|
|
func (r *PostgresRepository) CreateSyncRun(ctx context.Context, run SyncRun) (SyncRun, error) {
|
|
if run.Status == "" {
|
|
run.Status = string(SyncRunStatusRunning)
|
|
}
|
|
if run.StartedAt.IsZero() {
|
|
run.StartedAt = time.Now().UTC()
|
|
}
|
|
row := r.pool.QueryRow(ctx, `
|
|
INSERT INTO watch_provider_sync_runs (
|
|
connection_id, trigger, status, provider,
|
|
inbound_watched_found, inbound_watched_imported,
|
|
inbound_progress_found, inbound_progress_imported,
|
|
outbound_found, outbound_sent, inbound_favorites_found,
|
|
inbound_favorites_imported, outbound_favorites_found,
|
|
outbound_favorites_sent, favorite_removals_sent,
|
|
warning, error, started_at, completed_at
|
|
)
|
|
VALUES (
|
|
$1::uuid, $2, $3, $4,
|
|
$5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19
|
|
)
|
|
RETURNING id::text, connection_id::text, trigger, status, provider,
|
|
inbound_watched_found, inbound_watched_imported,
|
|
inbound_progress_found, inbound_progress_imported,
|
|
outbound_found, outbound_sent, inbound_favorites_found,
|
|
inbound_favorites_imported, outbound_favorites_found,
|
|
outbound_favorites_sent, favorite_removals_sent,
|
|
warning, error, started_at, completed_at, created_at
|
|
`, run.ConnectionID, run.Trigger, run.Status, run.Provider,
|
|
run.InboundWatchedFound, run.InboundWatchedImported,
|
|
run.InboundProgressFound, run.InboundProgressImported,
|
|
run.OutboundFound, run.OutboundSent, run.InboundFavoritesFound,
|
|
run.InboundFavoritesImported, run.OutboundFavoritesFound,
|
|
run.OutboundFavoritesSent, run.FavoriteRemovalsSent, run.Warning, run.Error,
|
|
run.StartedAt, run.CompletedAt)
|
|
created, err := scanSyncRun(row)
|
|
if err != nil {
|
|
return SyncRun{}, fmt.Errorf("scan created watch provider sync run: %w", err)
|
|
}
|
|
return created, nil
|
|
}
|
|
|
|
func (r *PostgresRepository) CompleteSyncRun(ctx context.Context, run SyncRun) (SyncRun, error) {
|
|
row := r.pool.QueryRow(ctx, `
|
|
UPDATE watch_provider_sync_runs
|
|
SET status = $2,
|
|
inbound_watched_found = $3,
|
|
inbound_watched_imported = $4,
|
|
inbound_progress_found = $5,
|
|
inbound_progress_imported = $6,
|
|
outbound_found = $7,
|
|
outbound_sent = $8,
|
|
inbound_favorites_found = $9,
|
|
inbound_favorites_imported = $10,
|
|
outbound_favorites_found = $11,
|
|
outbound_favorites_sent = $12,
|
|
favorite_removals_sent = $13,
|
|
warning = $14,
|
|
error = $15,
|
|
completed_at = $16
|
|
WHERE id = $1::uuid
|
|
RETURNING id::text, connection_id::text, trigger, status, provider,
|
|
inbound_watched_found, inbound_watched_imported,
|
|
inbound_progress_found, inbound_progress_imported,
|
|
outbound_found, outbound_sent, inbound_favorites_found,
|
|
inbound_favorites_imported, outbound_favorites_found,
|
|
outbound_favorites_sent, favorite_removals_sent,
|
|
warning, error, started_at, completed_at, created_at
|
|
`, run.ID, run.Status, run.InboundWatchedFound, run.InboundWatchedImported,
|
|
run.InboundProgressFound, run.InboundProgressImported, run.OutboundFound, run.OutboundSent,
|
|
run.InboundFavoritesFound, run.InboundFavoritesImported, run.OutboundFavoritesFound,
|
|
run.OutboundFavoritesSent, run.FavoriteRemovalsSent, run.Warning, run.Error, run.CompletedAt)
|
|
completed, err := scanSyncRun(row)
|
|
if err != nil {
|
|
return SyncRun{}, fmt.Errorf("complete watch provider sync run: %w", err)
|
|
}
|
|
return completed, nil
|
|
}
|
|
|
|
func (r *PostgresRepository) GetLatestSyncRun(ctx context.Context, connectionID string) (SyncRun, bool, error) {
|
|
row := r.pool.QueryRow(ctx, `
|
|
SELECT id::text, connection_id::text, trigger, status, provider,
|
|
inbound_watched_found, inbound_watched_imported,
|
|
inbound_progress_found, inbound_progress_imported,
|
|
outbound_found, outbound_sent, inbound_favorites_found,
|
|
inbound_favorites_imported, outbound_favorites_found,
|
|
outbound_favorites_sent, favorite_removals_sent,
|
|
warning, error, started_at, completed_at, created_at
|
|
FROM watch_provider_sync_runs
|
|
WHERE connection_id = $1::uuid
|
|
ORDER BY started_at DESC, created_at DESC
|
|
LIMIT 1
|
|
`, connectionID)
|
|
run, err := scanSyncRun(row)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return SyncRun{}, false, nil
|
|
}
|
|
if err != nil {
|
|
return SyncRun{}, false, fmt.Errorf("get latest watch provider sync run: %w", err)
|
|
}
|
|
return run, true, nil
|
|
}
|
|
|
|
func (r *PostgresRepository) GetActiveSyncRun(ctx context.Context, connectionID string) (SyncRun, bool, error) {
|
|
row := r.pool.QueryRow(ctx, `
|
|
SELECT id::text, connection_id::text, trigger, status, provider,
|
|
inbound_watched_found, inbound_watched_imported,
|
|
inbound_progress_found, inbound_progress_imported,
|
|
outbound_found, outbound_sent, inbound_favorites_found,
|
|
inbound_favorites_imported, outbound_favorites_found,
|
|
outbound_favorites_sent, favorite_removals_sent,
|
|
warning, error, started_at, completed_at, created_at
|
|
FROM watch_provider_sync_runs
|
|
WHERE connection_id = $1::uuid
|
|
AND status IN ('queued', 'running')
|
|
ORDER BY started_at DESC, created_at DESC
|
|
LIMIT 1
|
|
`, connectionID)
|
|
run, err := scanSyncRun(row)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return SyncRun{}, false, nil
|
|
}
|
|
if err != nil {
|
|
return SyncRun{}, false, fmt.Errorf("get active watch provider sync run: %w", err)
|
|
}
|
|
return run, true, nil
|
|
}
|
|
|
|
func (r *PostgresRepository) ListSyncRuns(ctx context.Context, connectionID string, limit int) ([]SyncRun, error) {
|
|
if limit <= 0 || limit > 50 {
|
|
limit = 10
|
|
}
|
|
rows, err := r.pool.Query(ctx, `
|
|
SELECT id::text, connection_id::text, trigger, status, provider,
|
|
inbound_watched_found, inbound_watched_imported,
|
|
inbound_progress_found, inbound_progress_imported,
|
|
outbound_found, outbound_sent, inbound_favorites_found,
|
|
inbound_favorites_imported, outbound_favorites_found,
|
|
outbound_favorites_sent, favorite_removals_sent,
|
|
warning, error, started_at, completed_at, created_at
|
|
FROM watch_provider_sync_runs
|
|
WHERE connection_id = $1::uuid
|
|
ORDER BY started_at DESC, created_at DESC
|
|
LIMIT $2
|
|
`, connectionID, limit)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list watch provider sync runs: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
var runs []SyncRun
|
|
for rows.Next() {
|
|
run, err := scanSyncRun(rows)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("scan watch provider sync run: %w", err)
|
|
}
|
|
runs = append(runs, run)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("iterate watch provider sync runs: %w", err)
|
|
}
|
|
return runs, nil
|
|
}
|
|
|
|
func (r *PostgresRepository) ListLocalWatchEventConnections(
|
|
ctx context.Context,
|
|
userID int,
|
|
profileID string,
|
|
kind LocalWatchEventKind,
|
|
) ([]Connection, error) {
|
|
var predicate string
|
|
switch kind {
|
|
case LocalWatchEventMarkedWatched:
|
|
predicate = "export_watched_enabled = true"
|
|
case LocalWatchEventMarkedUnwatched:
|
|
predicate = "export_unwatched_enabled = true"
|
|
default:
|
|
return nil, nil
|
|
}
|
|
rows, err := r.pool.Query(ctx, `
|
|
SELECT
|
|
id::text, provider, user_id, profile_id, provider_account_id, provider_username,
|
|
access_token, refresh_token, token_expires_at, import_watched_enabled,
|
|
import_progress_enabled, export_watched_enabled, export_unwatched_enabled,
|
|
import_favorites_enabled, export_favorites_enabled, sync_favorite_removals_enabled,
|
|
scrobble_enabled, last_inbound_sync_at, last_progress_sync_at, last_outbound_sync_at,
|
|
last_favorites_sync_at, last_scrobble_error_at, last_error, sync_cursors, created_at, updated_at
|
|
FROM watch_provider_connections
|
|
WHERE user_id = $1 AND profile_id = $2 AND `+predicate+`
|
|
ORDER BY provider
|
|
`, userID, profileID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list local watch event connections: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var conns []Connection
|
|
for rows.Next() {
|
|
conn, scanErr := scanConnection(rows)
|
|
if scanErr != nil {
|
|
return nil, fmt.Errorf("scan local watch event connection: %w", scanErr)
|
|
}
|
|
conns = append(conns, conn)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("iterate local watch event connections: %w", err)
|
|
}
|
|
return conns, nil
|
|
}
|
|
|
|
func (r *PostgresRepository) ListFavoriteEventConnections(
|
|
ctx context.Context,
|
|
userID int,
|
|
profileID string,
|
|
kind LocalFavoriteEventKind,
|
|
) ([]Connection, error) {
|
|
var predicate string
|
|
switch kind {
|
|
case LocalFavoriteEventAdded:
|
|
predicate = "export_favorites_enabled = true"
|
|
case LocalFavoriteEventRemoved:
|
|
predicate = "export_favorites_enabled = true"
|
|
default:
|
|
return nil, nil
|
|
}
|
|
rows, err := r.pool.Query(ctx, `
|
|
SELECT
|
|
id::text, provider, user_id, profile_id, provider_account_id, provider_username,
|
|
access_token, refresh_token, token_expires_at, import_watched_enabled,
|
|
import_progress_enabled, export_watched_enabled, export_unwatched_enabled,
|
|
import_favorites_enabled, export_favorites_enabled, sync_favorite_removals_enabled,
|
|
scrobble_enabled, last_inbound_sync_at, last_progress_sync_at, last_outbound_sync_at,
|
|
last_favorites_sync_at, last_scrobble_error_at, last_error, sync_cursors, created_at, updated_at
|
|
FROM watch_provider_connections
|
|
WHERE user_id = $1 AND profile_id = $2 AND `+predicate+`
|
|
ORDER BY provider
|
|
`, userID, profileID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list favorite event connections: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var conns []Connection
|
|
for rows.Next() {
|
|
conn, scanErr := scanConnection(rows)
|
|
if scanErr != nil {
|
|
return nil, fmt.Errorf("scan favorite event connection: %w", scanErr)
|
|
}
|
|
conns = append(conns, conn)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("iterate favorite event connections: %w", err)
|
|
}
|
|
return conns, nil
|
|
}
|
|
|
|
func (r *PostgresRepository) GetMediaDuration(ctx context.Context, mediaItemID string) (float64, error) {
|
|
var duration float64
|
|
err := r.pool.QueryRow(ctx, mediaDurationQuery, mediaItemID).Scan(&duration)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("get media duration: %w", err)
|
|
}
|
|
return duration, nil
|
|
}
|
|
|
|
func (r *PostgresRepository) GetFavoriteMediaItems(ctx context.Context, mediaItemIDs []string) (map[string]LocalFavorite, error) {
|
|
result := make(map[string]LocalFavorite, len(mediaItemIDs))
|
|
if len(mediaItemIDs) == 0 {
|
|
return result, nil
|
|
}
|
|
rows, err := r.pool.Query(ctx, `
|
|
SELECT content_id, type, title, COALESCE(year, 0), COALESCE(imdb_id, ''), COALESCE(tmdb_id, ''), COALESCE(tvdb_id, '')
|
|
FROM media_items
|
|
WHERE content_id = ANY($1)
|
|
`, mediaItemIDs)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get favorite media items: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var fav LocalFavorite
|
|
if err := rows.Scan(&fav.MediaItemID, &fav.Kind, &fav.Title, &fav.Year, &fav.IMDbID, &fav.TMDBID, &fav.TVDBID); err != nil {
|
|
return nil, fmt.Errorf("scan favorite media item: %w", err)
|
|
}
|
|
fav.ProviderItemKey = providerItemKeyForLocalFavorite(fav)
|
|
result[fav.MediaItemID] = fav
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("iterate favorite media items: %w", err)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
const mediaDurationQuery = `
|
|
SELECT COALESCE(MAX(duration), 0)
|
|
FROM media_files
|
|
WHERE content_id = $1 AND missing_since IS NULL
|
|
`
|
|
|
|
func (r *PostgresRepository) UpsertHistoryExports(ctx context.Context, exports []HistoryExport) error {
|
|
for _, export := range exports {
|
|
_, err := r.pool.Exec(ctx, `
|
|
INSERT INTO watch_provider_history_exports (
|
|
connection_id, history_id, media_item_id, watched_at, provider_item_key, status,
|
|
attempt_count, last_attempt_at, last_error
|
|
)
|
|
VALUES ($1::uuid, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
ON CONFLICT (connection_id, history_id) DO UPDATE SET
|
|
provider_item_key = EXCLUDED.provider_item_key,
|
|
status = CASE
|
|
WHEN watch_provider_history_exports.status IN ('sent', 'satisfied_by_scrobble') THEN watch_provider_history_exports.status
|
|
ELSE EXCLUDED.status
|
|
END,
|
|
updated_at = now()
|
|
`, export.ConnectionID, export.HistoryID, export.MediaItemID, export.WatchedAt, export.ProviderItemKey,
|
|
export.Status, export.AttemptCount, export.LastAttemptAt, export.LastError)
|
|
if err != nil {
|
|
return fmt.Errorf("upsert history export: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *PostgresRepository) ListPendingHistoryExports(ctx context.Context, connectionID string, limit int) ([]HistoryExport, error) {
|
|
if limit <= 0 || limit > 100 {
|
|
limit = 100
|
|
}
|
|
rows, err := r.pool.Query(ctx, `
|
|
SELECT id::text, connection_id::text, history_id, media_item_id, watched_at,
|
|
provider_item_key, status, attempt_count, last_attempt_at, last_error, created_at, updated_at
|
|
FROM watch_provider_history_exports
|
|
WHERE connection_id = $1::uuid
|
|
AND status IN ('pending', 'failed')
|
|
AND attempt_count < 5
|
|
ORDER BY watched_at ASC
|
|
LIMIT $2
|
|
`, connectionID, limit)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list pending history exports: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
var exports []HistoryExport
|
|
for rows.Next() {
|
|
var export HistoryExport
|
|
if err := rows.Scan(
|
|
&export.ID,
|
|
&export.ConnectionID,
|
|
&export.HistoryID,
|
|
&export.MediaItemID,
|
|
&export.WatchedAt,
|
|
&export.ProviderItemKey,
|
|
&export.Status,
|
|
&export.AttemptCount,
|
|
&export.LastAttemptAt,
|
|
&export.LastError,
|
|
&export.CreatedAt,
|
|
&export.UpdatedAt,
|
|
); err != nil {
|
|
return nil, fmt.Errorf("scan pending history export: %w", err)
|
|
}
|
|
exports = append(exports, export)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("iterate pending history exports: %w", err)
|
|
}
|
|
return exports, nil
|
|
}
|
|
|
|
func (r *PostgresRepository) MarkHistoryExportStatus(ctx context.Context, id string, status string, lastError string) error {
|
|
_, err := r.pool.Exec(ctx, `
|
|
UPDATE watch_provider_history_exports
|
|
SET status = $2,
|
|
attempt_count = attempt_count + 1,
|
|
last_attempt_at = now(),
|
|
last_error = $3,
|
|
updated_at = now()
|
|
WHERE id = $1::uuid
|
|
`, id, status, lastError)
|
|
if err != nil {
|
|
return fmt.Errorf("mark history export status: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *PostgresRepository) UpsertFavoriteStates(ctx context.Context, states []FavoriteState) error {
|
|
for _, state := range states {
|
|
_, err := r.pool.Exec(ctx, `
|
|
INSERT INTO watch_provider_favorite_items (
|
|
connection_id, media_item_id, provider_item_key, kind, title, year,
|
|
remote_present, local_present, last_seen_remote_at, last_seen_local_at,
|
|
last_exported_at, last_removed_remote_at, last_removed_local_at, last_error
|
|
)
|
|
VALUES ($1::uuid, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
|
ON CONFLICT (connection_id, media_item_id) DO UPDATE SET
|
|
provider_item_key = CASE
|
|
WHEN EXCLUDED.provider_item_key <> '' THEN EXCLUDED.provider_item_key
|
|
ELSE watch_provider_favorite_items.provider_item_key
|
|
END,
|
|
kind = CASE WHEN EXCLUDED.kind <> '' THEN EXCLUDED.kind ELSE watch_provider_favorite_items.kind END,
|
|
title = CASE WHEN EXCLUDED.title <> '' THEN EXCLUDED.title ELSE watch_provider_favorite_items.title END,
|
|
year = CASE WHEN EXCLUDED.year <> 0 THEN EXCLUDED.year ELSE watch_provider_favorite_items.year END,
|
|
remote_present = watch_provider_favorite_items.remote_present OR EXCLUDED.remote_present,
|
|
local_present = EXCLUDED.local_present,
|
|
last_seen_remote_at = COALESCE(EXCLUDED.last_seen_remote_at, watch_provider_favorite_items.last_seen_remote_at),
|
|
last_seen_local_at = COALESCE(EXCLUDED.last_seen_local_at, watch_provider_favorite_items.last_seen_local_at),
|
|
last_exported_at = COALESCE(EXCLUDED.last_exported_at, watch_provider_favorite_items.last_exported_at),
|
|
last_removed_remote_at = COALESCE(EXCLUDED.last_removed_remote_at, watch_provider_favorite_items.last_removed_remote_at),
|
|
last_removed_local_at = COALESCE(EXCLUDED.last_removed_local_at, watch_provider_favorite_items.last_removed_local_at),
|
|
last_error = EXCLUDED.last_error,
|
|
updated_at = now()
|
|
`, state.ConnectionID, state.MediaItemID, state.ProviderItemKey, state.Kind, state.Title, state.Year,
|
|
state.RemotePresent, state.LocalPresent, state.LastSeenRemoteAt, state.LastSeenLocalAt,
|
|
state.LastExportedAt, state.LastRemovedRemoteAt, state.LastRemovedLocalAt, state.LastError)
|
|
if err != nil {
|
|
return fmt.Errorf("upsert favorite state: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *PostgresRepository) ListFavoriteStates(ctx context.Context, connectionID string) ([]FavoriteState, error) {
|
|
rows, err := r.pool.Query(ctx, `
|
|
SELECT id::text, connection_id::text, media_item_id, provider_item_key, kind, title, year,
|
|
remote_present, local_present, last_seen_remote_at, last_seen_local_at,
|
|
last_exported_at, last_removed_remote_at, last_removed_local_at, last_error, created_at, updated_at
|
|
FROM watch_provider_favorite_items
|
|
WHERE connection_id = $1::uuid
|
|
`, connectionID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list favorite states: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
return scanFavoriteStates(rows)
|
|
}
|
|
|
|
func (r *PostgresRepository) ListPendingFavoriteExports(ctx context.Context, connectionID string, limit int) ([]FavoriteState, error) {
|
|
if limit <= 0 || limit > 100 {
|
|
limit = 100
|
|
}
|
|
rows, err := r.pool.Query(ctx, `
|
|
SELECT id::text, connection_id::text, media_item_id, provider_item_key, kind, title, year,
|
|
remote_present, local_present, last_seen_remote_at, last_seen_local_at,
|
|
last_exported_at, last_removed_remote_at, last_removed_local_at, last_error, created_at, updated_at
|
|
FROM watch_provider_favorite_items
|
|
WHERE connection_id = $1::uuid
|
|
AND local_present = true
|
|
AND remote_present = false
|
|
AND last_error = ''
|
|
ORDER BY last_seen_local_at ASC NULLS LAST, created_at ASC
|
|
LIMIT $2
|
|
`, connectionID, limit)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list pending favorite exports: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
return scanFavoriteStates(rows)
|
|
}
|
|
|
|
func (r *PostgresRepository) ListPendingFavoriteRemovals(ctx context.Context, connectionID string, limit int) ([]FavoriteState, error) {
|
|
if limit <= 0 || limit > 100 {
|
|
limit = 100
|
|
}
|
|
rows, err := r.pool.Query(ctx, `
|
|
SELECT id::text, connection_id::text, media_item_id, provider_item_key, kind, title, year,
|
|
remote_present, local_present, last_seen_remote_at, last_seen_local_at,
|
|
last_exported_at, last_removed_remote_at, last_removed_local_at, last_error, created_at, updated_at
|
|
FROM watch_provider_favorite_items
|
|
WHERE connection_id = $1::uuid
|
|
AND local_present = false
|
|
AND remote_present = true
|
|
AND last_error = ''
|
|
ORDER BY last_removed_local_at ASC NULLS LAST, updated_at ASC
|
|
LIMIT $2
|
|
`, connectionID, limit)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list pending favorite removals: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
return scanFavoriteStates(rows)
|
|
}
|
|
|
|
func (r *PostgresRepository) MarkFavoriteExported(ctx context.Context, connectionID, mediaItemID string, exportedAt time.Time) error {
|
|
return r.updateFavoriteState(ctx, connectionID, mediaItemID, `
|
|
remote_present = true,
|
|
local_present = true,
|
|
last_exported_at = $3,
|
|
last_seen_remote_at = COALESCE(last_seen_remote_at, $3),
|
|
last_error = ''
|
|
`, exportedAt)
|
|
}
|
|
|
|
func (r *PostgresRepository) MarkFavoriteRemoteRemoved(ctx context.Context, connectionID, mediaItemID string, removedAt time.Time) error {
|
|
return r.updateFavoriteState(ctx, connectionID, mediaItemID, `
|
|
remote_present = false,
|
|
last_removed_remote_at = $3,
|
|
last_error = ''
|
|
`, removedAt)
|
|
}
|
|
|
|
func (r *PostgresRepository) MarkFavoriteLocalRemoved(ctx context.Context, connectionID, mediaItemID string, removedAt time.Time) error {
|
|
return r.updateFavoriteState(ctx, connectionID, mediaItemID, `
|
|
local_present = false,
|
|
last_removed_local_at = $3,
|
|
last_error = ''
|
|
`, removedAt)
|
|
}
|
|
|
|
func (r *PostgresRepository) MarkFavoriteError(ctx context.Context, connectionID, mediaItemID, lastError string) error {
|
|
_, err := r.pool.Exec(ctx, `
|
|
UPDATE watch_provider_favorite_items
|
|
SET last_error = $3, updated_at = now()
|
|
WHERE connection_id = $1::uuid AND media_item_id = $2
|
|
`, connectionID, mediaItemID, lastError)
|
|
if err != nil {
|
|
return fmt.Errorf("mark favorite error: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *PostgresRepository) updateFavoriteState(ctx context.Context, connectionID, mediaItemID, setClause string, at time.Time) error {
|
|
_, err := r.pool.Exec(ctx, `
|
|
UPDATE watch_provider_favorite_items
|
|
SET `+setClause+`,
|
|
updated_at = now()
|
|
WHERE connection_id = $1::uuid AND media_item_id = $2
|
|
`, connectionID, mediaItemID, at)
|
|
if err != nil {
|
|
return fmt.Errorf("update favorite state: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *PostgresRepository) ListScrobbleConnections(ctx context.Context, userID int, profileID string) ([]Connection, error) {
|
|
rows, err := r.pool.Query(ctx, `
|
|
SELECT
|
|
id::text, provider, user_id, profile_id, provider_account_id, provider_username,
|
|
access_token, refresh_token, token_expires_at, import_watched_enabled,
|
|
import_progress_enabled, export_watched_enabled, export_unwatched_enabled,
|
|
import_favorites_enabled, export_favorites_enabled, sync_favorite_removals_enabled,
|
|
scrobble_enabled, last_inbound_sync_at, last_progress_sync_at, last_outbound_sync_at,
|
|
last_favorites_sync_at, last_scrobble_error_at, last_error, sync_cursors, created_at, updated_at
|
|
FROM watch_provider_connections
|
|
WHERE user_id = $1 AND profile_id = $2 AND scrobble_enabled = true
|
|
ORDER BY provider
|
|
`, userID, profileID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list scrobble connections: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
var conns []Connection
|
|
for rows.Next() {
|
|
conn, err := scanConnection(rows)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("scan scrobble connection: %w", err)
|
|
}
|
|
conns = append(conns, conn)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("iterate scrobble connections: %w", err)
|
|
}
|
|
return conns, nil
|
|
}
|
|
|
|
func (r *PostgresRepository) UpsertScrobbleSession(ctx context.Context, event ScrobbleEvent, connectionID string, action string) error {
|
|
_, err := r.pool.Exec(ctx, `
|
|
INSERT INTO watch_provider_scrobble_sessions (
|
|
playback_session_id, connection_id, media_item_id, provider_item_key, kind,
|
|
imdb_id, tmdb_id, tvdb_id, series_imdb_id, series_tmdb_id, series_tvdb_id,
|
|
season_number, episode_number, history_id, started_at, last_progress,
|
|
duration_seconds, completed, last_action, last_error
|
|
)
|
|
VALUES (
|
|
$1, $2::uuid, $3, $4, $5, $6, $7, $8, $9, $10,
|
|
$11, $12, $13, $14, $15, $16, $17, $18, $19, ''
|
|
)
|
|
ON CONFLICT (playback_session_id, connection_id) DO UPDATE SET
|
|
media_item_id = EXCLUDED.media_item_id,
|
|
provider_item_key = EXCLUDED.provider_item_key,
|
|
kind = EXCLUDED.kind,
|
|
imdb_id = EXCLUDED.imdb_id,
|
|
tmdb_id = EXCLUDED.tmdb_id,
|
|
tvdb_id = EXCLUDED.tvdb_id,
|
|
series_imdb_id = EXCLUDED.series_imdb_id,
|
|
series_tmdb_id = EXCLUDED.series_tmdb_id,
|
|
series_tvdb_id = EXCLUDED.series_tvdb_id,
|
|
season_number = EXCLUDED.season_number,
|
|
episode_number = EXCLUDED.episode_number,
|
|
history_id = COALESCE(NULLIF(EXCLUDED.history_id, ''), watch_provider_scrobble_sessions.history_id),
|
|
last_progress = EXCLUDED.last_progress,
|
|
duration_seconds = EXCLUDED.duration_seconds,
|
|
completed = EXCLUDED.completed,
|
|
last_action = EXCLUDED.last_action,
|
|
last_error = '',
|
|
updated_at = now()
|
|
`, event.PlaybackSessionID, connectionID, event.MediaItemID, event.ProviderItemKey, event.Kind,
|
|
event.IMDbID, event.TMDBID, event.TVDBID, event.SeriesIMDbID, event.SeriesTMDBID,
|
|
event.SeriesTVDBID, event.SeasonNumber, event.EpisodeNumber, event.HistoryID,
|
|
event.OccurredAt, event.PositionSeconds, event.DurationSeconds, event.Completed, action)
|
|
if err != nil {
|
|
return fmt.Errorf("upsert scrobble session: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *PostgresRepository) UpdateScrobbleSession(ctx context.Context, playbackSessionID string, connectionID string, action string, progress float64, historyID string, lastError string, stopSentAt *time.Time) error {
|
|
_, err := r.pool.Exec(ctx, `
|
|
UPDATE watch_provider_scrobble_sessions
|
|
SET last_action = $3,
|
|
last_progress = $4,
|
|
history_id = COALESCE(NULLIF($5, ''), history_id),
|
|
last_error = $6,
|
|
stop_sent_at = COALESCE($7, stop_sent_at),
|
|
updated_at = now()
|
|
WHERE playback_session_id = $1 AND connection_id = $2::uuid
|
|
`, playbackSessionID, connectionID, action, progress, historyID, lastError, stopSentAt)
|
|
if err != nil {
|
|
return fmt.Errorf("update scrobble session: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *PostgresRepository) ListOpenScrobbleSessions(ctx context.Context) ([]ScrobbleSession, error) {
|
|
rows, err := r.pool.Query(ctx, `
|
|
SELECT playback_session_id, connection_id::text, media_item_id, provider_item_key, kind,
|
|
imdb_id, tmdb_id, tvdb_id, series_imdb_id, series_tmdb_id, series_tvdb_id,
|
|
season_number, episode_number, history_id, started_at, last_progress,
|
|
duration_seconds, completed, last_action, stop_sent_at, last_error
|
|
FROM watch_provider_scrobble_sessions
|
|
WHERE stop_sent_at IS NULL
|
|
ORDER BY started_at ASC
|
|
`)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("list open scrobble sessions: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
var sessions []ScrobbleSession
|
|
for rows.Next() {
|
|
var session ScrobbleSession
|
|
if err := rows.Scan(
|
|
&session.PlaybackSessionID,
|
|
&session.ConnectionID,
|
|
&session.MediaItemID,
|
|
&session.ProviderItemKey,
|
|
&session.Kind,
|
|
&session.IMDbID,
|
|
&session.TMDBID,
|
|
&session.TVDBID,
|
|
&session.SeriesIMDbID,
|
|
&session.SeriesTMDBID,
|
|
&session.SeriesTVDBID,
|
|
&session.SeasonNumber,
|
|
&session.EpisodeNumber,
|
|
&session.HistoryID,
|
|
&session.StartedAt,
|
|
&session.LastProgress,
|
|
&session.DurationSeconds,
|
|
&session.Completed,
|
|
&session.LastAction,
|
|
&session.StopSentAt,
|
|
&session.LastError,
|
|
); err != nil {
|
|
return nil, fmt.Errorf("scan open scrobble session: %w", err)
|
|
}
|
|
sessions = append(sessions, session)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("iterate open scrobble sessions: %w", err)
|
|
}
|
|
return sessions, nil
|
|
}
|
|
|
|
func scanDeviceAuthSession(row pgx.Row) (DeviceAuthSession, error) {
|
|
var session DeviceAuthSession
|
|
err := row.Scan(
|
|
&session.ID,
|
|
&session.Provider,
|
|
&session.UserID,
|
|
&session.ProfileID,
|
|
&session.DeviceCode,
|
|
&session.UserCode,
|
|
&session.VerificationURL,
|
|
&session.IntervalSeconds,
|
|
&session.ExpiresAt,
|
|
&session.CompletedAt,
|
|
)
|
|
if err != nil {
|
|
return DeviceAuthSession{}, err
|
|
}
|
|
return session, nil
|
|
}
|
|
|
|
func scanSyncRun(row pgx.Row) (SyncRun, error) {
|
|
var run SyncRun
|
|
err := row.Scan(
|
|
&run.ID,
|
|
&run.ConnectionID,
|
|
&run.Trigger,
|
|
&run.Status,
|
|
&run.Provider,
|
|
&run.InboundWatchedFound,
|
|
&run.InboundWatchedImported,
|
|
&run.InboundProgressFound,
|
|
&run.InboundProgressImported,
|
|
&run.OutboundFound,
|
|
&run.OutboundSent,
|
|
&run.InboundFavoritesFound,
|
|
&run.InboundFavoritesImported,
|
|
&run.OutboundFavoritesFound,
|
|
&run.OutboundFavoritesSent,
|
|
&run.FavoriteRemovalsSent,
|
|
&run.Warning,
|
|
&run.Error,
|
|
&run.StartedAt,
|
|
&run.CompletedAt,
|
|
&run.CreatedAt,
|
|
)
|
|
if err != nil {
|
|
return SyncRun{}, err
|
|
}
|
|
return run, nil
|
|
}
|
|
|
|
func scanConnection(row pgx.Row) (Connection, error) {
|
|
var conn Connection
|
|
var rawSyncCursors []byte
|
|
err := row.Scan(
|
|
&conn.ID,
|
|
&conn.Provider,
|
|
&conn.UserID,
|
|
&conn.ProfileID,
|
|
&conn.ProviderAccountID,
|
|
&conn.ProviderUsername,
|
|
&conn.AccessToken,
|
|
&conn.RefreshToken,
|
|
&conn.TokenExpiresAt,
|
|
&conn.ImportWatchedEnabled,
|
|
&conn.ImportProgressEnabled,
|
|
&conn.ExportWatchedEnabled,
|
|
&conn.ExportUnwatchedEnabled,
|
|
&conn.ImportFavoritesEnabled,
|
|
&conn.ExportFavoritesEnabled,
|
|
&conn.SyncFavoriteRemovalsEnabled,
|
|
&conn.ScrobbleEnabled,
|
|
&conn.LastInboundSyncAt,
|
|
&conn.LastProgressSyncAt,
|
|
&conn.LastOutboundSyncAt,
|
|
&conn.LastFavoritesSyncAt,
|
|
&conn.LastScrobbleErrorAt,
|
|
&conn.LastError,
|
|
&rawSyncCursors,
|
|
&conn.CreatedAt,
|
|
&conn.UpdatedAt,
|
|
)
|
|
if err != nil {
|
|
return Connection{}, err
|
|
}
|
|
conn.SyncCursors = decodeSyncCursors(rawSyncCursors)
|
|
return conn, nil
|
|
}
|
|
|
|
type favoriteStateRows interface {
|
|
Next() bool
|
|
Scan(dest ...any) error
|
|
Err() error
|
|
}
|
|
|
|
func scanFavoriteStates(rows favoriteStateRows) ([]FavoriteState, error) {
|
|
var states []FavoriteState
|
|
for rows.Next() {
|
|
var state FavoriteState
|
|
if err := rows.Scan(
|
|
&state.ID,
|
|
&state.ConnectionID,
|
|
&state.MediaItemID,
|
|
&state.ProviderItemKey,
|
|
&state.Kind,
|
|
&state.Title,
|
|
&state.Year,
|
|
&state.RemotePresent,
|
|
&state.LocalPresent,
|
|
&state.LastSeenRemoteAt,
|
|
&state.LastSeenLocalAt,
|
|
&state.LastExportedAt,
|
|
&state.LastRemovedRemoteAt,
|
|
&state.LastRemovedLocalAt,
|
|
&state.LastError,
|
|
&state.CreatedAt,
|
|
&state.UpdatedAt,
|
|
); err != nil {
|
|
return nil, fmt.Errorf("scan favorite state: %w", err)
|
|
}
|
|
states = append(states, state)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("iterate favorite states: %w", err)
|
|
}
|
|
return states, nil
|
|
}
|
|
|
|
func encodeSyncCursors(cursors map[string]string) []byte {
|
|
if len(cursors) == 0 {
|
|
return []byte(`{}`)
|
|
}
|
|
data, err := json.Marshal(cursors)
|
|
if err != nil {
|
|
return []byte(`{}`)
|
|
}
|
|
return data
|
|
}
|
|
|
|
func decodeSyncCursors(data []byte) map[string]string {
|
|
if len(data) == 0 {
|
|
return map[string]string{}
|
|
}
|
|
var cursors map[string]string
|
|
if err := json.Unmarshal(data, &cursors); err != nil || cursors == nil {
|
|
return map[string]string{}
|
|
}
|
|
return cursors
|
|
}
|