From 0b7ad0f376801f43dd6a121e435bea912d4d1e35 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Thu, 29 Oct 2026 15:36:11 -0400 Subject: [PATCH] =?UTF-8?q?feat(podcasts):=20schema,=20RLS=20policy=20exte?= =?UTF-8?q?nsion,=20sqlc=20queries=20=E2=80=94=20feed=20core=20foundations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Podcast integration task 1.1 (plan: podcast-integration.md §6/§8, D20). WHY: podcasts are a new first-class media domain — RSS feeds users subscribe to personally, with episodes as media_items rows so progress, stats and the audio player work unchanged. Visibility is per-user (strict: only your subscriptions), which after the RLS migration (2026-10-29) belongs in the POLICIES, not Go helpers. Schema (idempotent — schema.sql re-executes every boot): - 'podcasts' library type seeded (ON CONFLICT DO NOTHING, ARRAY[]::TEXT[] cast required — bare ARRAY[] 42802s under ON_ERROR_STOP) - podcast_feeds: per-library feed entity (UNIQUE(library_id, feed_url)), full RSS/iTunes/Podcasting-2.0 metadata inventory, conditional-GET validators, is_complete poll backoff - podcast_subscriptions: the ONLY user-writable podcast state (display name, auto-download policy); deliberately UNGOVERNED — it IS the read policy's lookup table - media_items episode columns: source/source_feed_id/episode_guid/ feed_position/season/episode/enclosure_url/declared_duration/ transcript_url/chapters_url/hosts/guests + download state machine (pending|queued|downloading|downloaded|failed + error + attempts), all partial-indexed so scan rows never touch them - libraries.allow_member_feeds (first per-library knob); library_folders .purpose ('podcast' download roots, scanner-skipped) RLS (inside the labeled policy block): - p_media_items_visible gains the podcast-episode subscription predicate + admin escape (source IS DISTINCT FROM 'podcast_episode' guard keeps scan rows on the cheap path); unset identity still fails loud - podcast_feeds governed identically: library-visibility join AND subscriber-or-admin SELECT; permissive write stand-ins (mandatory at ENABLE time; fetcher/worker declare first-admin, add-feed upsert uses ON CONFLICT DO UPDATE RETURNING so creators never need SELECT on foreign rows) - Verified against dev DB: migration identity sees nothing, admin sees all, member sees feeds/episodes only post-subscribe (probed in a rolled-back tx — no rows persisted) sqlc: 30 new queries (feed upsert/CRUD, subscriptions, episode upsert-with-write-after-success-guard, download state transitions, poller worklist, subscription-scoped search) — no visibility clauses anywhere, per the D20 scoping model. --- database/schema/schema.sql | 197 +- internal/database/models.go | 82 +- internal/database/querier.go | 102 + internal/database/queries.sql.go | 3747 ++++++++++++++++++++----- internal/database/queries/queries.sql | 335 +++ internal/services/library_service.go | 9 + 6 files changed, 3799 insertions(+), 673 deletions(-) diff --git a/database/schema/schema.sql b/database/schema/schema.sql index f13e2b06..e84419ec 100644 --- a/database/schema/schema.sql +++ b/database/schema/schema.sql @@ -18,7 +18,8 @@ INSERT INTO library_types (name, description, allowed_extensions) VALUES ('ebooks', 'Ebook files including EPUB, PDF, MOBI, etc.', ARRAY['.epub', '.pdf', '.mobi', '.azw', '.azw3', '.txt', '.rtf', '.doc', '.docx', '.lit', '.fb2', '.pdb']), ('comics', 'Comic book archives and image formats', ARRAY['.cbz', '.cbr', '.cb7', '.cbt', '.epub', '.pdf']), ('manga', 'Manga files including archives and image folders', ARRAY['.cbz', '.cbr', '.epub', '.pdf', '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.avif', '.tiff', '.tif']), -('audiobooks', 'Audiobook files; m4b first, breadth formats registered from day one', ARRAY['.m4b', '.m4a', '.mp3', '.flac']) +('audiobooks', 'Audiobook files; m4b first, breadth formats registered from day one', ARRAY['.m4b', '.m4a', '.mp3', '.flac']), +('podcasts', 'RSS podcast/feed libraries: feeds subscribe-and-download; the scanner never walks these folders', ARRAY[]::TEXT[]) ON CONFLICT (name) DO NOTHING; -- Create users table @@ -1878,6 +1879,126 @@ CREATE INDEX IF NOT EXISTS idx_hash_conflicts_status ON hash_conflicts(status); -- Widen for databases created before the resolution format settled (no-op otherwise) ALTER TABLE hash_conflicts ALTER COLUMN resolution TYPE VARCHAR(50); +-- ============================================================ +-- PODCASTS / RSS FEEDS +-- Plan: ~/.opencode/plan/podcast-integration.md (§6). +-- Feeds are per-library entities (UNIQUE(library_id, feed_url)); users +-- subscribe personally (podcast_subscriptions). Episodes are +-- media_items rows with source='podcast_episode' — per-episode +-- progress/stats for free — grouped by source_feed_id. Visibility is +-- RLS-enforced (see the policy block at the end of this file): an +-- episode/feed row is SELECT-visible only to subscribers of the feed +-- and to admins. The fetcher/download worker is the SOLE writer of +-- feed/episode metadata; users own only their subscription row and the +-- reader_settings order override. ALL DDL IDEMPOTENT: schema.sql +-- re-executes on EVERY boot. +-- ============================================================ + +-- First per-library boolean knob (plain column, not KV): when false, +-- only admins may add feeds to the library (members keep subscribe +-- rights to existing feeds). +ALTER TABLE libraries ADD COLUMN IF NOT EXISTS allow_member_feeds BOOLEAN NOT NULL DEFAULT TRUE; + +-- Folder purpose: 'scan' (default; the FS walker's root) vs 'podcast' +-- (a podcasts library's download root — scanner skips it wholesale, +-- belt-and-braces on top of the library-type skip). +ALTER TABLE library_folders ADD COLUMN IF NOT EXISTS purpose VARCHAR(20) NOT NULL DEFAULT 'scan'; + +CREATE TABLE IF NOT EXISTS podcast_feeds ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + library_id UUID NOT NULL REFERENCES libraries(id) ON DELETE CASCADE, + feed_url TEXT NOT NULL, + -- podcast:guid — the feed's canonical identity; dedupes across URL + -- migrations (fetcher honors itunes:new-feed-url + redirects). + podcast_guid TEXT, + title VARCHAR(500) NOT NULL, + description TEXT, + -- Channel itunes:author (semantically the host) and itunes:owner. + host VARCHAR(500), + publisher VARCHAR(500), + owner_name VARCHAR(500), + owner_email VARCHAR(500), + language VARCHAR(50), + tags TEXT[] DEFAULT '{}', + explicit BOOLEAN NOT NULL DEFAULT FALSE, + artwork_path VARCHAR(500), + -- itunes:type: 'serial' | 'episodic' + feed_type VARCHAR(20), + -- podcast:medium: 'audiobook' | 'podcast' | 'music' | ... + medium VARCHAR(20), + -- itunes:complete: poller backs off once the publisher ends the feed. + is_complete BOOLEAN NOT NULL DEFAULT FALSE, + -- Conditional-GET validators (opaque; echoed verbatim). + etag VARCHAR(500), + last_modified VARCHAR(500), + last_fetched_at TIMESTAMPTZ, + last_build_date TIMESTAMPTZ, + created_by_admin_id UUID REFERENCES users(id) ON DELETE SET NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(library_id, feed_url) +); + +CREATE INDEX IF NOT EXISTS idx_podcast_feeds_library ON podcast_feeds(library_id); +CREATE INDEX IF NOT EXISTS idx_podcast_feeds_guid ON podcast_feeds(podcast_guid); + +-- User-owned subscription row: the ONLY podcast state a user writes. +-- display_name NULL renders as the feed title. auto_download: +-- 'off' | 'all' | 'newest' (+ auto_download_count for newest N). +CREATE TABLE IF NOT EXISTS podcast_subscriptions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + feed_id UUID NOT NULL REFERENCES podcast_feeds(id) ON DELETE CASCADE, + display_name VARCHAR(500), + auto_download VARCHAR(20) NOT NULL DEFAULT 'off', + auto_download_count INT, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(user_id, feed_id) +); + +CREATE INDEX IF NOT EXISTS idx_podcast_subs_user ON podcast_subscriptions(user_id); +CREATE INDEX IF NOT EXISTS idx_podcast_subs_feed ON podcast_subscriptions(feed_id); + +-- Episode columns (NULL for scan-sourced items). See the plan §5 for +-- the full RSS/iTunes/Podcasting-2.0 field mapping — existing columns +-- (title, subtitle, description, authors, explicit, date_published, +-- cover_image_path, file_size, mime_type, language, genre/tags) carry +-- the shared fields; only genuinely podcast-only storage lives here. +ALTER TABLE media_items + ADD COLUMN IF NOT EXISTS source VARCHAR(20) NOT NULL DEFAULT 'scan', + ADD COLUMN IF NOT EXISTS source_feed_id UUID REFERENCES podcast_feeds(id) ON DELETE CASCADE, + -- Stable reorder-proof addressing key (invariant 3): progress + -- anchors on guid, never ordinal. + ADD COLUMN IF NOT EXISTS episode_guid TEXT, + -- Native order as of the latest fetch (renumbered per fetch). + ADD COLUMN IF NOT EXISTS feed_position INTEGER, + ADD COLUMN IF NOT EXISTS season_number INTEGER, + ADD COLUMN IF NOT EXISTS episode_number INTEGER, + ADD COLUMN IF NOT EXISTS enclosure_url TEXT, + -- itunes:duration claim (often HH:MM:SS or prose); display until + -- ffprobe reconciles after download. + ADD COLUMN IF NOT EXISTS declared_duration_seconds INTEGER, + ADD COLUMN IF NOT EXISTS transcript_url TEXT, + ADD COLUMN IF NOT EXISTS chapters_url TEXT, + ADD COLUMN IF NOT EXISTS hosts TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS guests TEXT[] DEFAULT '{}', + -- Download state machine: pending | queued | downloading | + -- downloaded | failed (+ reason text + attempt count). The episode + -- list renders this inline (D8/D19) — never Processing Issues. + ADD COLUMN IF NOT EXISTS download_state VARCHAR(20) NOT NULL DEFAULT 'pending', + ADD COLUMN IF NOT EXISTS download_error TEXT, + ADD COLUMN IF NOT EXISTS download_attempts INT NOT NULL DEFAULT 0; + +-- Identity + ordering + worker-worklist indexes (all partial: scan +-- rows never touch these columns). +CREATE UNIQUE INDEX IF NOT EXISTS idx_media_items_feed_guid + ON media_items(source_feed_id, episode_guid) WHERE source_feed_id IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_media_items_feed_pos + ON media_items(source_feed_id, feed_position) WHERE source_feed_id IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_media_items_feed_undownloaded + ON media_items(source_feed_id) WHERE source_feed_id IS NOT NULL AND download_state <> 'downloaded'; + + -- ============================================================ -- RLS: PER-USER LIBRARY VISIBILITY (ONE SOURCE OF TRUTH) -- Plan: ~/.opencode/plan/server-rls-library-visibility.md REV 2. @@ -1912,6 +2033,13 @@ ALTER TABLE media_items FORCE ROW LEVEL SECURITY; -- semantics — row absent -> visible; row true -> visible; row false -- -> hidden. No new index needed: library_visibility's -- UNIQUE(user_id, library_id) constraint already indexes the join. +-- PODCAST EPISODES (plan D20): source='podcast_episode' rows carry the +-- ADDITIONAL subscription predicate — visible only to subscribers of +-- source_feed_id, or to admins (admins manage feeds regardless of +-- subscription; the escape also satisfies background jobs, which +-- declare a first-admin identity). Scan rows pass unchanged via the +-- IS DISTINCT FROM guard — the predicate costs them one NULL compare. +-- Unset identity FAILS LOUD, never fails open (see the block header). CREATE POLICY p_media_items_visible ON media_items FOR SELECT USING ( @@ -1922,6 +2050,19 @@ CREATE POLICY p_media_items_visible ON media_items AND lv.library_id = media_items.library_id AND lv.is_visible = false ) + AND ( + media_items.source IS DISTINCT FROM 'podcast_episode' + OR EXISTS ( + SELECT 1 FROM podcast_subscriptions s + WHERE s.user_id = current_setting('app.user_id')::uuid + AND s.feed_id = media_items.source_feed_id + ) + OR EXISTS ( + SELECT 1 FROM users u + WHERE u.id = current_setting('app.user_id')::uuid + AND u.role = 'admin' + ) + ) ); -- WRITE policies: PERMISSIVE STAND-INS for the deferred scoped write @@ -1942,7 +2083,59 @@ CREATE POLICY p_media_items_update ON media_items CREATE POLICY p_media_items_delete ON media_items FOR DELETE USING (true); --- Other tables: NONE. collection_items reaches media only via INNER +-- Other tables: collection_items reaches media only via INNER -- JOIN media_items (the join drops filtered rows). collections -- metadata (book_count) is a separate counts question — plan A13. + +-- PODCAST FEEDS (plan D20): governed the same way. SELECT-visible to +-- subscribers of the feed (their subscription IS the grant — this is +-- the predicate's lookup table, podcast_subscriptions itself stays +-- UNGOVERNED) plus the library-visibility join (a hidden library +-- hides its feeds too), and to admins. WRITE policies are permissive +-- stand-ins for the same reason as media_items above: mandatory at +-- ENABLE time, and the fetcher/worker (first-admin identity) plus the +-- add-feed upsert (ON CONFLICT DO UPDATE needs no separate SELECT) +-- cover every legitimate writer. +DROP POLICY IF EXISTS p_podcast_feeds_visible ON podcast_feeds; +DROP POLICY IF EXISTS p_podcast_feeds_insert ON podcast_feeds; +DROP POLICY IF EXISTS p_podcast_feeds_update ON podcast_feeds; +DROP POLICY IF EXISTS p_podcast_feeds_delete ON podcast_feeds; + +ALTER TABLE podcast_feeds ENABLE ROW LEVEL SECURITY; +ALTER TABLE podcast_feeds FORCE ROW LEVEL SECURITY; + +CREATE POLICY p_podcast_feeds_visible ON podcast_feeds + FOR SELECT + USING ( + current_setting('app.user_id') IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM library_visibility lv + WHERE lv.user_id = current_setting('app.user_id')::uuid + AND lv.library_id = podcast_feeds.library_id + AND lv.is_visible = false + ) + AND ( + EXISTS ( + SELECT 1 FROM podcast_subscriptions s + WHERE s.user_id = current_setting('app.user_id')::uuid + AND s.feed_id = podcast_feeds.id + ) + OR EXISTS ( + SELECT 1 FROM users u + WHERE u.id = current_setting('app.user_id')::uuid + AND u.role = 'admin' + ) + ) + ); + +CREATE POLICY p_podcast_feeds_insert ON podcast_feeds + FOR INSERT WITH CHECK (true); +CREATE POLICY p_podcast_feeds_update ON podcast_feeds + FOR UPDATE USING (true) WITH CHECK (true); +CREATE POLICY p_podcast_feeds_delete ON podcast_feeds + FOR DELETE USING (true); + +-- podcast_subscriptions: UNGOVERNED, deliberately — it IS the read +-- policy's lookup table, and a user must always see their own +-- subscription rows (subscribe -> visibility is a single INSERT). -- ============================================================ diff --git a/internal/database/models.go b/internal/database/models.go index 56d5358e..a4ed38b3 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -150,6 +150,7 @@ type Libraries struct { CreatedByAdminID pgtype.UUID `db:"created_by_admin_id" json:"created_by_admin_id"` CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + AllowMemberFeeds bool `db:"allow_member_feeds" json:"allow_member_feeds"` } type LibraryFolders struct { @@ -157,6 +158,7 @@ type LibraryFolders struct { LibraryID pgtype.UUID `db:"library_id" json:"library_id"` FolderPath string `db:"folder_path" json:"folder_path"` CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + Purpose string `db:"purpose" json:"purpose"` } type LibraryTypes struct { @@ -324,20 +326,35 @@ type MediaItems struct { // Scan information from ComicInfo.xml (scanner group, resolution, etc.) ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` // Summary from ComicInfo.xml (may be merged with description from Calibre) - Summary pgtype.Text `db:"summary" json:"summary"` - MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` - MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` - ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` - ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` - LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` - AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` - TagsSearch []string `db:"tags_search" json:"tags_search"` - ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` - NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` - FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` - OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"` - OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"` - HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"` + Summary pgtype.Text `db:"summary" json:"summary"` + MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` + MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` + ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` + ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` + LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` + AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` + TagsSearch []string `db:"tags_search" json:"tags_search"` + ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` + NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` + FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` + OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"` + OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"` + HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"` + Source string `db:"source" json:"source"` + SourceFeedID pgtype.UUID `db:"source_feed_id" json:"source_feed_id"` + EpisodeGuid pgtype.Text `db:"episode_guid" json:"episode_guid"` + FeedPosition pgtype.Int4 `db:"feed_position" json:"feed_position"` + SeasonNumber pgtype.Int4 `db:"season_number" json:"season_number"` + EpisodeNumber pgtype.Int4 `db:"episode_number" json:"episode_number"` + EnclosureUrl pgtype.Text `db:"enclosure_url" json:"enclosure_url"` + DeclaredDurationSeconds pgtype.Int4 `db:"declared_duration_seconds" json:"declared_duration_seconds"` + TranscriptUrl pgtype.Text `db:"transcript_url" json:"transcript_url"` + ChaptersUrl pgtype.Text `db:"chapters_url" json:"chapters_url"` + Hosts []string `db:"hosts" json:"hosts"` + Guests []string `db:"guests" json:"guests"` + DownloadState string `db:"download_state" json:"download_state"` + DownloadError pgtype.Text `db:"download_error" json:"download_error"` + DownloadAttempts int32 `db:"download_attempts" json:"download_attempts"` } type MediaNotes struct { @@ -405,6 +422,43 @@ type PanelData struct { UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` } +type PodcastFeeds struct { + ID pgtype.UUID `db:"id" json:"id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + FeedUrl string `db:"feed_url" json:"feed_url"` + PodcastGuid pgtype.Text `db:"podcast_guid" json:"podcast_guid"` + Title string `db:"title" json:"title"` + Description pgtype.Text `db:"description" json:"description"` + Host pgtype.Text `db:"host" json:"host"` + Publisher pgtype.Text `db:"publisher" json:"publisher"` + OwnerName pgtype.Text `db:"owner_name" json:"owner_name"` + OwnerEmail pgtype.Text `db:"owner_email" json:"owner_email"` + Language pgtype.Text `db:"language" json:"language"` + Tags []string `db:"tags" json:"tags"` + Explicit bool `db:"explicit" json:"explicit"` + ArtworkPath pgtype.Text `db:"artwork_path" json:"artwork_path"` + FeedType pgtype.Text `db:"feed_type" json:"feed_type"` + Medium pgtype.Text `db:"medium" json:"medium"` + IsComplete bool `db:"is_complete" json:"is_complete"` + Etag pgtype.Text `db:"etag" json:"etag"` + LastModified pgtype.Text `db:"last_modified" json:"last_modified"` + LastFetchedAt pgtype.Timestamptz `db:"last_fetched_at" json:"last_fetched_at"` + LastBuildDate pgtype.Timestamptz `db:"last_build_date" json:"last_build_date"` + CreatedByAdminID pgtype.UUID `db:"created_by_admin_id" json:"created_by_admin_id"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` +} + +type PodcastSubscriptions struct { + ID pgtype.UUID `db:"id" json:"id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + FeedID pgtype.UUID `db:"feed_id" json:"feed_id"` + DisplayName pgtype.Text `db:"display_name" json:"display_name"` + AutoDownload string `db:"auto_download" json:"auto_download"` + AutoDownloadCount pgtype.Int4 `db:"auto_download_count" json:"auto_download_count"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` +} + // Tracks media that failed processing in a library: item-backed issues (library-type mismatches, metadata extraction failures) and row-less import failures (no media_items row exists; keyed by library + file_path). Auto-resolved by later successful scans. type ProcessingIssues struct { ID pgtype.UUID `db:"id" json:"id"` diff --git a/internal/database/querier.go b/internal/database/querier.go index 23a2be96..74d26112 100644 --- a/internal/database/querier.go +++ b/internal/database/querier.go @@ -53,11 +53,17 @@ type Querier interface { // collections page); same live filter as GetCollectionItems, plus the // future-hybrid excluded exclusion and an optional library scope. CountCollectionLiveItems(ctx context.Context, arg CountCollectionLiveItemsParams) (int32, error) + CountDownloadedEpisodes(ctx context.Context, sourceFeedID pgtype.UUID) (int64, error) // Sibling-book count for sidecar naming (MetadataWriter): a dir-scoped // metadata.opf may only be written when NO other non-archived item // shares the directory; otherwise the sidecar goes to // .opf so one book's metadata can never poison dir-mates. CountOtherMediaItemsInDir(ctx context.Context, arg CountOtherMediaItemsInDirParams) (int32, error) + CountPodcastFeedSubscribers(ctx context.Context, feedID pgtype.UUID) (int64, error) + // Sidebar/page gate (D13): >=1 podcasts-type library the user can see + // (COALESCE(is_visible,true) semantics — mirrors GetUserVisibleLibraries). + CountPodcastLibrariesForUser(ctx context.Context, userID pgtype.UUID) (int64, error) + CountUndownloadedEpisodes(ctx context.Context, sourceFeedID pgtype.UUID) (int64, error) // Count unlinked books for a device (ignored rows stay hidden) CountUnlinkedBooks(ctx context.Context, deviceID pgtype.UUID) (int64, error) // Banner count: unresolved, non-ignored queue rows across the caller's own @@ -116,6 +122,22 @@ type Querier interface { // ============================================ CreateOrUpdateKoboEntitlement(ctx context.Context, arg CreateOrUpdateKoboEntitlementParams) (KoboEntitlements, error) // ============================================================================ + // PODCASTS / RSS FEEDS (plan ~/.opencode/plan/podcast-integration.md §6/§8) + // Visibility is RLS-enforced (schema policy block) — NO query here carries + // visibility clauses. Feed reads run under the acting user's identity + // (subscriber-or-admin policy); the fetcher/worker run as first-admin. + // The fetcher/download worker is the SOLE writer of feed/episode metadata; + // users write only podcast_subscriptions rows. + // ============================================================================ + // Add-feed upsert keyed on the per-library UNIQUE(library_id, feed_url): + // a second subscriber adding the same URL attaches to the existing row + // instead of erroring. DO UPDATE (not DO NOTHING) so the RETURNING works + // even when the conflicting row is invisible to the actor under RLS + // (non-admin creators cannot SELECT other users' feeds). Metadata columns + // are filled by the fetcher right after; this only establishes identity. + CreatePodcastFeed(ctx context.Context, arg CreatePodcastFeedParams) (PodcastFeeds, error) + CreatePodcastLibraryFolder(ctx context.Context, arg CreatePodcastLibraryFolderParams) (LibraryFolders, error) + // ============================================================================ // PROCESSING ISSUES QUERIES // ============================================================================ // Widened upsert (faceted plan Phase 11): media_item_id and file_path @@ -160,6 +182,11 @@ type Querier interface { DeleteMediaItemFormat(ctx context.Context, id pgtype.UUID) error DeleteMediaRating(ctx context.Context, arg DeleteMediaRatingParams) error DeleteMediaReview(ctx context.Context, arg DeleteMediaReviewParams) error + // Admin-only removal (handler checks role); episodes cascade via + // media_items.source_feed_id ON DELETE CASCADE. + DeletePodcastFeed(ctx context.Context, id pgtype.UUID) (int64, error) + // Unsubscribe NEVER deletes feed/episode/file state (D6). + DeletePodcastSubscription(ctx context.Context, arg DeletePodcastSubscriptionParams) (int64, error) DeleteProcessingIssue(ctx context.Context, id pgtype.UUID) (ProcessingIssues, error) DeleteReadingProgress(ctx context.Context, arg DeleteReadingProgressParams) error DeleteSavedFilter(ctx context.Context, arg DeleteSavedFilterParams) (SavedFilters, error) @@ -421,6 +448,17 @@ type Querier interface { // Get OPDS tokens by device GetOpdsTokensByDevice(ctx context.Context, deviceID pgtype.UUID) ([]OpdsTokens, error) GetPanelData(ctx context.Context, arg GetPanelDataParams) (PanelData, error) + GetPodcastEpisode(ctx context.Context, id pgtype.UUID) (MediaItems, error) + GetPodcastFeed(ctx context.Context, id pgtype.UUID) (PodcastFeeds, error) + // URL-migration dedupe: same canonical guid within the library reuses + // the existing row. + GetPodcastFeedByGuid(ctx context.Context, arg GetPodcastFeedByGuidParams) (PodcastFeeds, error) + // The download root (purpose='podcast') of a podcasts library. + GetPodcastLibraryFolder(ctx context.Context, libraryID pgtype.UUID) ([]LibraryFolders, error) + GetPodcastSubscription(ctx context.Context, arg GetPodcastSubscriptionParams) (PodcastSubscriptions, error) + // The podcasts library/ies visible to the user (add-feed target list; + // a self-hosted instance typically has exactly one). + GetPodcastsLibraryForUser(ctx context.Context, userID pgtype.UUID) ([]GetPodcastsLibraryForUserRow, error) // Get most popular books for a user GetPopularBooks(ctx context.Context, arg GetPopularBooksParams) ([]GetPopularBooksRow, error) // Single-issue read for the resolve/delete endpoints' family-lens gate. @@ -552,6 +590,9 @@ type Querier interface { ListDeletedAnnotationsForBook(ctx context.Context, arg ListDeletedAnnotationsForBookParams) ([]ListDeletedAnnotationsForBookRow, error) ListDevicesByType(ctx context.Context, deviceType string) ([]Devices, error) ListDevicesByUser(ctx context.Context, userID pgtype.UUID) ([]Devices, error) + // Download-worker policy union (D6): most aggressive policy wins for + // what arrives. Runs under an admin identity (background job). + ListFeedSubscribers(ctx context.Context, feedID pgtype.UUID) ([]ListFeedSubscribersRow, error) // Items hidden from libraries: either missing from disk (waiting on the // second scan that archives them) or already archived and pending purge. // Feeds the admin archived-items page. match_* columns identify an active @@ -571,11 +612,21 @@ type Querier interface { // List media items that have no stored SHA-256 (imported before hashing existed) ListMediaItemsMissingHash(ctx context.Context) ([]MediaItems, error) ListMediaItemsSorted(ctx context.Context, arg ListMediaItemsSortedParams) ([]ListMediaItemsSortedRow, error) + // The podcasts page: the acting user's subscriptions with feed metadata + // and the user-owned subscription fields. Facet values derive in Go. + ListMyPodcastFeeds(ctx context.Context, userID pgtype.UUID) ([]ListMyPodcastFeedsRow, error) ListPendingHashConflicts(ctx context.Context) ([]ListPendingHashConflictsRow, error) // DeviceID NULL = every device's pending items (the queue processor's // enumerator passes NULL; a bare `device_id = NULL` never matches, which // silently disabled the offline flush until 2026-10-17). ListPendingSyncQueueItems(ctx context.Context, arg ListPendingSyncQueueItemsParams) ([]SyncQueue, error) + // Feed detail / sequence source. RLS scopes to subscriber-or-admin; + // ordering: native position, then pub date, then guid (stable across + // re-fetches for historical rows whose position went stale). + ListPodcastEpisodes(ctx context.Context, sourceFeedID pgtype.UUID) ([]MediaItems, error) + // Poller worklist (admin identity): every feed with its validators and + // the back-off flag (is_complete feeds poll rarely, never disappear). + ListPodcastFeedsForPolling(ctx context.Context) ([]PodcastFeeds, error) // LEFT JOIN (Phase 11): row-less import failures have NO media_items // row — an inner join would silently drop the ledger's newest kind. // Item-side projections COALESCE to keep them non-nullable strings; @@ -583,6 +634,8 @@ type Querier interface { ListProcessingIssuesByLibrary(ctx context.Context, libraryID pgtype.UUID) ([]ListProcessingIssuesByLibraryRow, error) ListSyncConflictsByMediaItem(ctx context.Context, arg ListSyncConflictsByMediaItemParams) ([]SyncConflicts, error) ListSyncConflictsByUser(ctx context.Context, userID pgtype.UUID) ([]ListSyncConflictsByUserRow, error) + // Download All + policy union worklist. + ListUndownloadedEpisodes(ctx context.Context, sourceFeedID pgtype.UUID) ([]MediaItems, error) // List unresolved unlinked books with pagination ListUnresolvedUnlinkedBooks(ctx context.Context, arg ListUnresolvedUnlinkedBooksParams) ([]ListUnresolvedUnlinkedBooksRow, error) // Page list: unresolved queue rows for the caller's own devices (ignored @@ -591,6 +644,14 @@ type Querier interface { ListUsers(ctx context.Context) ([]ListUsersRow, error) // First consecutive scan that cannot find the file on disk. MarkMediaItemMissing(ctx context.Context, id pgtype.UUID) error + // SUCCESS-only write (invariant 7): the file is on disk and probed. + // ffprobe facts (real duration via audio_metadata, precise size/mime) + // reconcile the feed's declared values. + MarkPodcastEpisodeDownloaded(ctx context.Context, arg MarkPodcastEpisodeDownloadedParams) (MediaItems, error) + // Failure/rejection write: state + reason + whether a human already + // intervened (manual_retry resets the auto-retry budget). Existing + // metadata/file facts are never cleared. + MarkPodcastEpisodeFailed(ctx context.Context, arg MarkPodcastEpisodeFailedParams) error // Content reappeared at a new location while the old path vanished: treat as // a move. Repoints the row (and refreshes file-level fields) so reading // history follows the book; archive/missing state is cleared separately. @@ -654,14 +715,30 @@ type Querier interface { // Search Media Items queries SearchMediaItems(ctx context.Context, arg SearchMediaItemsParams) ([]SearchMediaItemsRow, error) SearchMediaItemsUnified(ctx context.Context, arg SearchMediaItemsUnifiedParams) ([]SearchMediaItemsUnifiedRow, error) + // Subscription-scoped episode search (D14): title + description cover + // un-downloaded AND historical rows (fetcher persists metadata-only). + // RLS already restricts to the caller's subscriptions — the explicit + // source filter keeps scan items out of podcast search. + SearchMyPodcastEpisodes(ctx context.Context, dollar_1 interface{}) ([]MediaItems, error) + // Feed-title AND subscription-display-name search (D14) + the facet + // fields the podcasts page filters on. + SearchMyPodcastFeeds(ctx context.Context, arg SearchMyPodcastFeedsParams) ([]SearchMyPodcastFeedsRow, error) SearchNarratorValues(ctx context.Context, arg SearchNarratorValuesParams) ([]SearchNarratorValuesRow, error) SearchSeriesValues(ctx context.Context, arg SearchSeriesValuesParams) ([]SearchSeriesValuesRow, error) SearchTagsValues(ctx context.Context, arg SearchTagsValuesParams) ([]SearchTagsValuesRow, error) SetDeviceSyncEnabled(ctx context.Context, arg SetDeviceSyncEnabledParams) (Devices, error) // Library Visibility queries SetLibraryVisibility(ctx context.Context, arg SetLibraryVisibilityParams) (LibraryVisibility, error) + // Worker claim: pending|failed -> queued. Attempts incremented at + // claim time so a crashed worker still counts the try. + SetPodcastEpisodeDownloadQueued(ctx context.Context, id pgtype.UUID) (int64, error) + SetPodcastEpisodeDownloading(ctx context.Context, id pgtype.UUID) error + // Manual retry (same endpoint as download): reset to pending so the + // worker re-claims it; the reason text clears with the fresh attempt. + SetPodcastEpisodePending(ctx context.Context, id pgtype.UUID) error // Set system config SetSystemConfig(ctx context.Context, arg SetSystemConfigParams) (SystemConfig, error) + SubscribePodcastFeed(ctx context.Context, arg SubscribePodcastFeedParams) (PodcastSubscriptions, error) SyncLibraryTypeExtensions(ctx context.Context, arg SyncLibraryTypeExtensionsParams) error TombstoneMediaBookmarkByDedupKey(ctx context.Context, arg TombstoneMediaBookmarkByDedupKeyParams) error TombstoneMediaBookmarkByID(ctx context.Context, arg TombstoneMediaBookmarkByIDParams) error @@ -726,6 +803,20 @@ type Querier interface { UpdateMediaNoteForSync(ctx context.Context, arg UpdateMediaNoteForSyncParams) (MediaNotes, error) UpdateMediaRating(ctx context.Context, arg UpdateMediaRatingParams) (MediaRatings, error) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error + UpdatePodcastFeedArtwork(ctx context.Context, arg UpdatePodcastFeedArtworkParams) error + // Post-fetch bookkeeping: validators for conditional GETs, the fetch + // timestamp, and the feed's own last-build marker. + UpdatePodcastFeedFetch(ctx context.Context, arg UpdatePodcastFeedFetchParams) (PodcastFeeds, error) + // Fetcher-only full metadata refresh (parse result). podcast_guid is + // set once a feed declares it; new_feed_url handling is a dedicated + // query below. + UpdatePodcastFeedMetadata(ctx context.Context, arg UpdatePodcastFeedMetadataParams) (PodcastFeeds, error) + // itunes:new-feed-url (or a permanent HTTP redirect chain): the feed has + // MOVED. Rewrite feed_url so future polls hit the new location; the row + // (and every subscription/episode) stays put. + UpdatePodcastFeedRedirect(ctx context.Context, arg UpdatePodcastFeedRedirectParams) error + // User-owned fields only (D9): display name + auto-download policy. + UpdatePodcastSubscription(ctx context.Context, arg UpdatePodcastSubscriptionParams) (PodcastSubscriptions, error) UpdateReadingProgress(ctx context.Context, arg UpdateReadingProgressParams) (ReadingProgress, error) UpdateReadingSpeed(ctx context.Context, arg UpdateReadingSpeedParams) (ReadingSpeed, error) UpdateSavedFilter(ctx context.Context, arg UpdateSavedFilterParams) (SavedFilters, error) @@ -751,6 +842,17 @@ type Querier interface { // content identical), and after a manifest-gated re-extract. UpsertMediaScanState(ctx context.Context, arg UpsertMediaScanStateParams) error UpsertPanelData(ctx context.Context, arg UpsertPanelDataParams) (PanelData, error) + // ============================================================================ + // Podcast episodes — media_items rows with source='podcast_episode' + // ============================================================================ + // Fetcher-only upsert keyed on the partial unique index + // (source_feed_id, episode_guid). DO UPDATE refreshes FETCH-OWNED + // metadata only. Write-after-success guard (AGENTS.md gotcha #7): + // download state/error/attempts are NEVER touched here, and the + // downloaded file's facts (path/size/mime/cover) survive renumbering — + // only un-downloaded rows follow the re-derived file_path. + // authors_search mirrors authors (trigram search column). + UpsertPodcastEpisode(ctx context.Context, arg UpsertPodcastEpisodeParams) (MediaItems, error) UpsertReaderSettings(ctx context.Context, arg UpsertReaderSettingsParams) (ReaderSettings, error) UpsertSystemSetting(ctx context.Context, arg UpsertSystemSettingParams) (SystemSettings, error) // Ingest: insert or refresh the queue row for a device file that could not diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index e332857e..f150dd45 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -123,7 +123,7 @@ func (q *Queries) AddBookToKoboShelf(ctx context.Context, arg AddBookToKoboShelf } const AddLibraryFolder = `-- name: AddLibraryFolder :one -INSERT INTO library_folders (library_id, folder_path) VALUES ($1, $2) RETURNING id, library_id, folder_path, created_at +INSERT INTO library_folders (library_id, folder_path) VALUES ($1, $2) RETURNING id, library_id, folder_path, created_at, purpose ` type AddLibraryFolderParams struct { @@ -140,6 +140,7 @@ func (q *Queries) AddLibraryFolder(ctx context.Context, arg AddLibraryFolderPara &i.LibraryID, &i.FolderPath, &i.CreatedAt, + &i.Purpose, ) return i, err } @@ -369,6 +370,20 @@ func (q *Queries) CountCollectionLiveItems(ctx context.Context, arg CountCollect return column_1, err } +const CountDownloadedEpisodes = `-- name: CountDownloadedEpisodes :one +SELECT COUNT(*) FROM media_items +WHERE source_feed_id = $1 + AND source = 'podcast_episode' + AND download_state = 'downloaded' +` + +func (q *Queries) CountDownloadedEpisodes(ctx context.Context, sourceFeedID pgtype.UUID) (int64, error) { + row := q.db.QueryRow(ctx, CountDownloadedEpisodes, sourceFeedID) + var count int64 + err := row.Scan(&count) + return count, err +} + const CountOtherMediaItemsInDir = `-- name: CountOtherMediaItemsInDir :one SELECT COUNT(*)::int FROM media_items @@ -395,6 +410,48 @@ func (q *Queries) CountOtherMediaItemsInDir(ctx context.Context, arg CountOtherM return column_1, err } +const CountPodcastFeedSubscribers = `-- name: CountPodcastFeedSubscribers :one +SELECT COUNT(*) FROM podcast_subscriptions WHERE feed_id = $1 +` + +func (q *Queries) CountPodcastFeedSubscribers(ctx context.Context, feedID pgtype.UUID) (int64, error) { + row := q.db.QueryRow(ctx, CountPodcastFeedSubscribers, feedID) + var count int64 + err := row.Scan(&count) + return count, err +} + +const CountPodcastLibrariesForUser = `-- name: CountPodcastLibrariesForUser :one +SELECT COUNT(*) +FROM libraries l +JOIN library_types lt ON lt.id = l.library_type_id AND lt.name = 'podcasts' +LEFT JOIN library_visibility lv ON lv.library_id = l.id AND lv.user_id = $1 +WHERE COALESCE(lv.is_visible, true) = true +` + +// Sidebar/page gate (D13): >=1 podcasts-type library the user can see +// (COALESCE(is_visible,true) semantics — mirrors GetUserVisibleLibraries). +func (q *Queries) CountPodcastLibrariesForUser(ctx context.Context, userID pgtype.UUID) (int64, error) { + row := q.db.QueryRow(ctx, CountPodcastLibrariesForUser, userID) + var count int64 + err := row.Scan(&count) + return count, err +} + +const CountUndownloadedEpisodes = `-- name: CountUndownloadedEpisodes :one +SELECT COUNT(*) FROM media_items +WHERE source_feed_id = $1 + AND source = 'podcast_episode' + AND download_state <> 'downloaded' +` + +func (q *Queries) CountUndownloadedEpisodes(ctx context.Context, sourceFeedID pgtype.UUID) (int64, error) { + row := q.db.QueryRow(ctx, CountUndownloadedEpisodes, sourceFeedID) + var count int64 + err := row.Scan(&count) + return count, err +} + const CountUnlinkedBooks = `-- name: CountUnlinkedBooks :one SELECT COUNT(*) as count FROM unlinked_books @@ -767,7 +824,7 @@ func (q *Queries) CreateHashConflict(ctx context.Context, arg CreateHashConflict const CreateLibrary = `-- name: CreateLibrary :one INSERT INTO libraries (name, description, library_type_id, created_by_admin_id) VALUES ($1, $2, $3, $4) -RETURNING id, name, description, library_type_id, created_by_admin_id, created_at, updated_at +RETURNING id, name, description, library_type_id, created_by_admin_id, created_at, updated_at, allow_member_feeds ` type CreateLibraryParams struct { @@ -794,6 +851,7 @@ func (q *Queries) CreateLibrary(ctx context.Context, arg CreateLibraryParams) (L &i.CreatedByAdminID, &i.CreatedAt, &i.UpdatedAt, + &i.AllowMemberFeeds, ) return i, err } @@ -963,7 +1021,7 @@ const CreateMediaItem = `-- name: CreateMediaItem :one INSERT INTO media_items (library_id, title, subtitle, authors, authors_search, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publishers, publishers_search, contributors, contributors_search, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, library_type_name, audio_metadata, narrators, narrators_search, abridged, explicit) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51, $52) ON CONFLICT (library_id, file_path) DO UPDATE SET updated_at = NOW() -RETURNING id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence +RETURNING id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts ` type CreateMediaItemParams struct { @@ -1156,6 +1214,21 @@ func (q *Queries) CreateMediaItem(ctx context.Context, arg CreateMediaItemParams &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ) return i, err } @@ -1213,7 +1286,7 @@ const CreateMediaItemIfNew = `-- name: CreateMediaItemIfNew :one INSERT INTO media_items (library_id, title, subtitle, authors, authors_search, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publishers, publishers_search, contributors, contributors_search, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, library_type_name, audio_metadata, narrators, narrators_search, abridged, explicit) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29, $30, $31, $32, $33, $34, $35, $36, $37, $38, $39, $40, $41, $42, $43, $44, $45, $46, $47, $48, $49, $50, $51, $52) ON CONFLICT (library_id, file_path) DO NOTHING -RETURNING id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence +RETURNING id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts ` type CreateMediaItemIfNewParams struct { @@ -1408,6 +1481,21 @@ func (q *Queries) CreateMediaItemIfNew(ctx context.Context, arg CreateMediaItemI &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ) return i, err } @@ -1606,6 +1694,96 @@ func (q *Queries) CreateOrUpdateKoboEntitlement(ctx context.Context, arg CreateO return i, err } +const CreatePodcastFeed = `-- name: CreatePodcastFeed :one + +INSERT INTO podcast_feeds (library_id, feed_url, title, created_by_admin_id) +VALUES ($1, $2, $3, $4) +ON CONFLICT (library_id, feed_url) DO UPDATE SET updated_at = NOW() +RETURNING id, library_id, feed_url, podcast_guid, title, description, host, publisher, owner_name, owner_email, language, tags, explicit, artwork_path, feed_type, medium, is_complete, etag, last_modified, last_fetched_at, last_build_date, created_by_admin_id, created_at, updated_at +` + +type CreatePodcastFeedParams struct { + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + FeedUrl string `db:"feed_url" json:"feed_url"` + Title string `db:"title" json:"title"` + CreatedByAdminID pgtype.UUID `db:"created_by_admin_id" json:"created_by_admin_id"` +} + +// ============================================================================ +// PODCASTS / RSS FEEDS (plan ~/.opencode/plan/podcast-integration.md §6/§8) +// Visibility is RLS-enforced (schema policy block) — NO query here carries +// visibility clauses. Feed reads run under the acting user's identity +// (subscriber-or-admin policy); the fetcher/worker run as first-admin. +// The fetcher/download worker is the SOLE writer of feed/episode metadata; +// users write only podcast_subscriptions rows. +// ============================================================================ +// Add-feed upsert keyed on the per-library UNIQUE(library_id, feed_url): +// a second subscriber adding the same URL attaches to the existing row +// instead of erroring. DO UPDATE (not DO NOTHING) so the RETURNING works +// even when the conflicting row is invisible to the actor under RLS +// (non-admin creators cannot SELECT other users' feeds). Metadata columns +// are filled by the fetcher right after; this only establishes identity. +func (q *Queries) CreatePodcastFeed(ctx context.Context, arg CreatePodcastFeedParams) (PodcastFeeds, error) { + row := q.db.QueryRow(ctx, CreatePodcastFeed, + arg.LibraryID, + arg.FeedUrl, + arg.Title, + arg.CreatedByAdminID, + ) + var i PodcastFeeds + err := row.Scan( + &i.ID, + &i.LibraryID, + &i.FeedUrl, + &i.PodcastGuid, + &i.Title, + &i.Description, + &i.Host, + &i.Publisher, + &i.OwnerName, + &i.OwnerEmail, + &i.Language, + &i.Tags, + &i.Explicit, + &i.ArtworkPath, + &i.FeedType, + &i.Medium, + &i.IsComplete, + &i.Etag, + &i.LastModified, + &i.LastFetchedAt, + &i.LastBuildDate, + &i.CreatedByAdminID, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const CreatePodcastLibraryFolder = `-- name: CreatePodcastLibraryFolder :one +INSERT INTO library_folders (library_id, folder_path, purpose) +VALUES ($1, $2, 'podcast') +RETURNING id, library_id, folder_path, created_at, purpose +` + +type CreatePodcastLibraryFolderParams struct { + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + FolderPath string `db:"folder_path" json:"folder_path"` +} + +func (q *Queries) CreatePodcastLibraryFolder(ctx context.Context, arg CreatePodcastLibraryFolderParams) (LibraryFolders, error) { + row := q.db.QueryRow(ctx, CreatePodcastLibraryFolder, arg.LibraryID, arg.FolderPath) + var i LibraryFolders + err := row.Scan( + &i.ID, + &i.LibraryID, + &i.FolderPath, + &i.CreatedAt, + &i.Purpose, + ) + return i, err +} + const CreateProcessingIssue = `-- name: CreateProcessingIssue :one INSERT INTO processing_issues (media_item_id, library_id, issue_type, issue_description, severity, file_path) @@ -2167,7 +2345,7 @@ func (q *Queries) DeleteLibrary(ctx context.Context, id pgtype.UUID) error { } const DeleteLibraryFolder = `-- name: DeleteLibraryFolder :one -DELETE FROM library_folders WHERE library_id = $1 AND folder_path = $2 RETURNING id, library_id, folder_path, created_at +DELETE FROM library_folders WHERE library_id = $1 AND folder_path = $2 RETURNING id, library_id, folder_path, created_at, purpose ` type DeleteLibraryFolderParams struct { @@ -2183,6 +2361,7 @@ func (q *Queries) DeleteLibraryFolder(ctx context.Context, arg DeleteLibraryFold &i.LibraryID, &i.FolderPath, &i.CreatedAt, + &i.Purpose, ) return i, err } @@ -2234,6 +2413,38 @@ func (q *Queries) DeleteMediaReview(ctx context.Context, arg DeleteMediaReviewPa return err } +const DeletePodcastFeed = `-- name: DeletePodcastFeed :execrows +DELETE FROM podcast_feeds WHERE id = $1 +` + +// Admin-only removal (handler checks role); episodes cascade via +// media_items.source_feed_id ON DELETE CASCADE. +func (q *Queries) DeletePodcastFeed(ctx context.Context, id pgtype.UUID) (int64, error) { + result, err := q.db.Exec(ctx, DeletePodcastFeed, id) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil +} + +const DeletePodcastSubscription = `-- name: DeletePodcastSubscription :execrows +DELETE FROM podcast_subscriptions WHERE user_id = $1 AND feed_id = $2 +` + +type DeletePodcastSubscriptionParams struct { + UserID pgtype.UUID `db:"user_id" json:"user_id"` + FeedID pgtype.UUID `db:"feed_id" json:"feed_id"` +} + +// Unsubscribe NEVER deletes feed/episode/file state (D6). +func (q *Queries) DeletePodcastSubscription(ctx context.Context, arg DeletePodcastSubscriptionParams) (int64, error) { + result, err := q.db.Exec(ctx, DeletePodcastSubscription, arg.UserID, arg.FeedID) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil +} + const DeleteProcessingIssue = `-- name: DeleteProcessingIssue :one DELETE FROM processing_issues WHERE id = $1 @@ -3882,7 +4093,7 @@ func (q *Queries) GetBookReadingSessions(ctx context.Context, arg GetBookReading } const GetBooksByTag = `-- name: GetBooksByTag :many -SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts FROM media_items WHERE library_id = $1 AND tags @> ARRAY[$2::text] ORDER BY title ASC ` @@ -3976,6 +4187,21 @@ func (q *Queries) GetBooksByTag(ctx context.Context, arg GetBooksByTagParams) ([ &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ); err != nil { return nil, err } @@ -4148,7 +4374,7 @@ func (q *Queries) GetCollectionItems(ctx context.Context, collectionID pgtype.UU } const GetCollectionItemsForDashboard = `-- name: GetCollectionItemsForDashboard :many -SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, rp.percentage AS viewer_percentage FROM media_items mi +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, mi.source, mi.source_feed_id, mi.episode_guid, mi.feed_position, mi.season_number, mi.episode_number, mi.enclosure_url, mi.declared_duration_seconds, mi.transcript_url, mi.chapters_url, mi.hosts, mi.guests, mi.download_state, mi.download_error, mi.download_attempts, rp.percentage AS viewer_percentage FROM media_items mi INNER JOIN collection_items ci ON ci.media_item_id = mi.id LEFT JOIN reading_progress rp ON rp.media_item_id = mi.id AND rp.user_id = $1::uuid WHERE ci.collection_id = $2 @@ -4273,6 +4499,21 @@ func (q *Queries) GetCollectionItemsForDashboard(ctx context.Context, arg GetCol &i.MediaItems.OpfIdentifier, &i.MediaItems.OpfUuid, &i.MediaItems.HashConfidence, + &i.MediaItems.Source, + &i.MediaItems.SourceFeedID, + &i.MediaItems.EpisodeGuid, + &i.MediaItems.FeedPosition, + &i.MediaItems.SeasonNumber, + &i.MediaItems.EpisodeNumber, + &i.MediaItems.EnclosureUrl, + &i.MediaItems.DeclaredDurationSeconds, + &i.MediaItems.TranscriptUrl, + &i.MediaItems.ChaptersUrl, + &i.MediaItems.Hosts, + &i.MediaItems.Guests, + &i.MediaItems.DownloadState, + &i.MediaItems.DownloadError, + &i.MediaItems.DownloadAttempts, &i.ViewerPercentage, ); err != nil { return nil, err @@ -4286,7 +4527,7 @@ func (q *Queries) GetCollectionItemsForDashboard(ctx context.Context, arg GetCol } const GetCollectionUpNext = `-- name: GetCollectionUpNext :many -SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, rp.percentage AS viewer_percentage FROM media_items mi +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, mi.source, mi.source_feed_id, mi.episode_guid, mi.feed_position, mi.season_number, mi.episode_number, mi.enclosure_url, mi.declared_duration_seconds, mi.transcript_url, mi.chapters_url, mi.hosts, mi.guests, mi.download_state, mi.download_error, mi.download_attempts, rp.percentage AS viewer_percentage FROM media_items mi INNER JOIN collection_items ci ON ci.media_item_id = mi.id LEFT JOIN reading_progress rp ON rp.media_item_id = mi.id AND rp.user_id = $1::uuid WHERE ci.collection_id = $2 @@ -4404,6 +4645,21 @@ func (q *Queries) GetCollectionUpNext(ctx context.Context, arg GetCollectionUpNe &i.MediaItems.OpfIdentifier, &i.MediaItems.OpfUuid, &i.MediaItems.HashConfidence, + &i.MediaItems.Source, + &i.MediaItems.SourceFeedID, + &i.MediaItems.EpisodeGuid, + &i.MediaItems.FeedPosition, + &i.MediaItems.SeasonNumber, + &i.MediaItems.EpisodeNumber, + &i.MediaItems.EnclosureUrl, + &i.MediaItems.DeclaredDurationSeconds, + &i.MediaItems.TranscriptUrl, + &i.MediaItems.ChaptersUrl, + &i.MediaItems.Hosts, + &i.MediaItems.Guests, + &i.MediaItems.DownloadState, + &i.MediaItems.DownloadError, + &i.MediaItems.DownloadAttempts, &i.ViewerPercentage, ); err != nil { return nil, err @@ -4553,7 +4809,7 @@ func (q *Queries) GetCollectionsForBook(ctx context.Context, mediaItemID pgtype. } const GetContinueReadingItems = `-- name: GetContinueReadingItems :many -SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, mi.source, mi.source_feed_id, mi.episode_guid, mi.feed_position, mi.season_number, mi.episode_number, mi.enclosure_url, mi.declared_duration_seconds, mi.transcript_url, mi.chapters_url, mi.hosts, mi.guests, mi.download_state, mi.download_error, mi.download_attempts FROM media_items mi INNER JOIN ( SELECT DISTINCT ON (media_item_id) media_item_id, last_read_at FROM reading_progress @@ -4660,6 +4916,21 @@ func (q *Queries) GetContinueReadingItems(ctx context.Context, arg GetContinueRe &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ); err != nil { return nil, err } @@ -4685,7 +4956,7 @@ WITH user_series_progress AS ( GROUP BY mi.series ), next_books AS ( - SELECT DISTINCT ON (mi.series) mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, + SELECT DISTINCT ON (mi.series) mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, mi.source, mi.source_feed_id, mi.episode_guid, mi.feed_position, mi.season_number, mi.episode_number, mi.enclosure_url, mi.declared_duration_seconds, mi.transcript_url, mi.chapters_url, mi.hosts, mi.guests, mi.download_state, mi.download_error, mi.download_attempts, usp.last_read_at FROM media_items mi JOIN user_series_progress usp ON mi.series = usp.series @@ -4695,7 +4966,7 @@ next_books AS ( AND (mi.series_number > usp.max_read_number OR usp.max_read_number IS NULL) ORDER BY mi.series, mi.series_number ASC NULLS LAST ) -SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, last_read_at FROM next_books +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts, last_read_at FROM next_books ORDER BY last_read_at DESC NULLS LAST LIMIT $1 ` @@ -4707,81 +4978,96 @@ type GetContinueSeriesItemsParams struct { } type GetContinueSeriesItemsRow struct { - ID pgtype.UUID `db:"id" json:"id"` - LibraryID pgtype.UUID `db:"library_id" json:"library_id"` - Title string `db:"title" json:"title"` - Subtitle pgtype.Text `db:"subtitle" json:"subtitle"` - Author pgtype.Text `db:"author" json:"author"` - Isbn pgtype.Text `db:"isbn" json:"isbn"` - Description pgtype.Text `db:"description" json:"description"` - FilePath string `db:"file_path" json:"file_path"` - FileSize pgtype.Int8 `db:"file_size" json:"file_size"` - MimeType pgtype.Text `db:"mime_type" json:"mime_type"` - CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` - Series pgtype.Text `db:"series" json:"series"` - SeriesNumber pgtype.Float8 `db:"series_number" json:"series_number"` - Tags []string `db:"tags" json:"tags"` - Narrators []string `db:"narrators" json:"narrators"` - Authors []string `db:"authors" json:"authors"` - AuthorsSearch []string `db:"authors_search" json:"authors_search"` - Publishers []string `db:"publishers" json:"publishers"` - PublishersSearch []string `db:"publishers_search" json:"publishers_search"` - Abridged bool `db:"abridged" json:"abridged"` - Explicit bool `db:"explicit" json:"explicit"` - Asin pgtype.Text `db:"asin" json:"asin"` - DatePublished pgtype.Date `db:"date_published" json:"date_published"` - Publisher pgtype.Text `db:"publisher" json:"publisher"` - Contributors []string `db:"contributors" json:"contributors"` - Language pgtype.Text `db:"language" json:"language"` - Edition pgtype.Text `db:"edition" json:"edition"` - PageCount pgtype.Int4 `db:"page_count" json:"page_count"` - Genre pgtype.Text `db:"genre" json:"genre"` - CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"` - GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"` - OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"` - GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"` - AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"` - CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` - ImportedAt pgtype.Timestamptz `db:"imported_at" json:"imported_at"` - UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` - FormatGroup string `db:"format_group" json:"format_group"` - FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"` - IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"` - HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"` - TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"` - ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"` - EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"` - RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"` - KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"` - KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"` - MangaType pgtype.Text `db:"manga_type" json:"manga_type"` - ReadingDirection pgtype.Text `db:"reading_direction" json:"reading_direction"` - SeriesCount pgtype.Int4 `db:"series_count" json:"series_count"` - Volume pgtype.Int4 `db:"volume" json:"volume"` - Imprint pgtype.Text `db:"imprint" json:"imprint"` - AgeRating pgtype.Text `db:"age_rating" json:"age_rating"` - WebUrl pgtype.Text `db:"web_url" json:"web_url"` - StoryArc pgtype.Text `db:"story_arc" json:"story_arc"` - IsBlackAndWhite pgtype.Bool `db:"is_black_and_white" json:"is_black_and_white"` - MetadataNotes pgtype.Text `db:"metadata_notes" json:"metadata_notes"` - CommunityRating pgtype.Float8 `db:"community_rating" json:"community_rating"` - AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` - ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` - Summary pgtype.Text `db:"summary" json:"summary"` - MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` - MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` - ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` - ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` - LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` - AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` - TagsSearch []string `db:"tags_search" json:"tags_search"` - ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` - NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` - FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` - OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"` - OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"` - HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"` - LastReadAt interface{} `db:"last_read_at" json:"last_read_at"` + ID pgtype.UUID `db:"id" json:"id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + Title string `db:"title" json:"title"` + Subtitle pgtype.Text `db:"subtitle" json:"subtitle"` + Author pgtype.Text `db:"author" json:"author"` + Isbn pgtype.Text `db:"isbn" json:"isbn"` + Description pgtype.Text `db:"description" json:"description"` + FilePath string `db:"file_path" json:"file_path"` + FileSize pgtype.Int8 `db:"file_size" json:"file_size"` + MimeType pgtype.Text `db:"mime_type" json:"mime_type"` + CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` + Series pgtype.Text `db:"series" json:"series"` + SeriesNumber pgtype.Float8 `db:"series_number" json:"series_number"` + Tags []string `db:"tags" json:"tags"` + Narrators []string `db:"narrators" json:"narrators"` + Authors []string `db:"authors" json:"authors"` + AuthorsSearch []string `db:"authors_search" json:"authors_search"` + Publishers []string `db:"publishers" json:"publishers"` + PublishersSearch []string `db:"publishers_search" json:"publishers_search"` + Abridged bool `db:"abridged" json:"abridged"` + Explicit bool `db:"explicit" json:"explicit"` + Asin pgtype.Text `db:"asin" json:"asin"` + DatePublished pgtype.Date `db:"date_published" json:"date_published"` + Publisher pgtype.Text `db:"publisher" json:"publisher"` + Contributors []string `db:"contributors" json:"contributors"` + Language pgtype.Text `db:"language" json:"language"` + Edition pgtype.Text `db:"edition" json:"edition"` + PageCount pgtype.Int4 `db:"page_count" json:"page_count"` + Genre pgtype.Text `db:"genre" json:"genre"` + CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"` + GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"` + OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"` + GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"` + AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + ImportedAt pgtype.Timestamptz `db:"imported_at" json:"imported_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + FormatGroup string `db:"format_group" json:"format_group"` + FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"` + IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"` + HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"` + TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"` + ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"` + EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"` + RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"` + KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"` + KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"` + MangaType pgtype.Text `db:"manga_type" json:"manga_type"` + ReadingDirection pgtype.Text `db:"reading_direction" json:"reading_direction"` + SeriesCount pgtype.Int4 `db:"series_count" json:"series_count"` + Volume pgtype.Int4 `db:"volume" json:"volume"` + Imprint pgtype.Text `db:"imprint" json:"imprint"` + AgeRating pgtype.Text `db:"age_rating" json:"age_rating"` + WebUrl pgtype.Text `db:"web_url" json:"web_url"` + StoryArc pgtype.Text `db:"story_arc" json:"story_arc"` + IsBlackAndWhite pgtype.Bool `db:"is_black_and_white" json:"is_black_and_white"` + MetadataNotes pgtype.Text `db:"metadata_notes" json:"metadata_notes"` + CommunityRating pgtype.Float8 `db:"community_rating" json:"community_rating"` + AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` + ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` + Summary pgtype.Text `db:"summary" json:"summary"` + MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` + MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` + ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` + ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` + LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` + AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` + TagsSearch []string `db:"tags_search" json:"tags_search"` + ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` + NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` + FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` + OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"` + OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"` + HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"` + Source string `db:"source" json:"source"` + SourceFeedID pgtype.UUID `db:"source_feed_id" json:"source_feed_id"` + EpisodeGuid pgtype.Text `db:"episode_guid" json:"episode_guid"` + FeedPosition pgtype.Int4 `db:"feed_position" json:"feed_position"` + SeasonNumber pgtype.Int4 `db:"season_number" json:"season_number"` + EpisodeNumber pgtype.Int4 `db:"episode_number" json:"episode_number"` + EnclosureUrl pgtype.Text `db:"enclosure_url" json:"enclosure_url"` + DeclaredDurationSeconds pgtype.Int4 `db:"declared_duration_seconds" json:"declared_duration_seconds"` + TranscriptUrl pgtype.Text `db:"transcript_url" json:"transcript_url"` + ChaptersUrl pgtype.Text `db:"chapters_url" json:"chapters_url"` + Hosts []string `db:"hosts" json:"hosts"` + Guests []string `db:"guests" json:"guests"` + DownloadState string `db:"download_state" json:"download_state"` + DownloadError pgtype.Text `db:"download_error" json:"download_error"` + DownloadAttempts int32 `db:"download_attempts" json:"download_attempts"` + LastReadAt interface{} `db:"last_read_at" json:"last_read_at"` } func (q *Queries) GetContinueSeriesItems(ctx context.Context, arg GetContinueSeriesItemsParams) ([]GetContinueSeriesItemsRow, error) { @@ -4868,6 +5154,21 @@ func (q *Queries) GetContinueSeriesItems(ctx context.Context, arg GetContinueSer &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, &i.LastReadAt, ); err != nil { return nil, err @@ -6038,7 +6339,7 @@ func (q *Queries) GetKoreaderStatWatermark(ctx context.Context, arg GetKoreaderS } const GetLibrary = `-- name: GetLibrary :one -SELECT l.id, l.name, l.description, l.library_type_id, l.created_by_admin_id, l.created_at, l.updated_at, lt.name as type_name, lt.description as type_description +SELECT l.id, l.name, l.description, l.library_type_id, l.created_by_admin_id, l.created_at, l.updated_at, l.allow_member_feeds, lt.name as type_name, lt.description as type_description FROM libraries l JOIN library_types lt ON l.library_type_id = lt.id WHERE l.id = $1 @@ -6052,6 +6353,7 @@ type GetLibraryRow struct { CreatedByAdminID pgtype.UUID `db:"created_by_admin_id" json:"created_by_admin_id"` CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + AllowMemberFeeds bool `db:"allow_member_feeds" json:"allow_member_feeds"` TypeName string `db:"type_name" json:"type_name"` TypeDescription pgtype.Text `db:"type_description" json:"type_description"` } @@ -6067,6 +6369,7 @@ func (q *Queries) GetLibrary(ctx context.Context, id pgtype.UUID) (GetLibraryRow &i.CreatedByAdminID, &i.CreatedAt, &i.UpdatedAt, + &i.AllowMemberFeeds, &i.TypeName, &i.TypeDescription, ) @@ -6074,7 +6377,7 @@ func (q *Queries) GetLibrary(ctx context.Context, id pgtype.UUID) (GetLibraryRow } const GetLibraryByFolder = `-- name: GetLibraryByFolder :one -SELECT lf.library_id, l.id, l.name, l.description, l.library_type_id, l.created_by_admin_id, l.created_at, l.updated_at FROM library_folders lf +SELECT lf.library_id, l.id, l.name, l.description, l.library_type_id, l.created_by_admin_id, l.created_at, l.updated_at, l.allow_member_feeds FROM library_folders lf JOIN libraries l ON lf.library_id = l.id WHERE lf.folder_path = $1 ` @@ -6088,6 +6391,7 @@ type GetLibraryByFolderRow struct { CreatedByAdminID pgtype.UUID `db:"created_by_admin_id" json:"created_by_admin_id"` CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + AllowMemberFeeds bool `db:"allow_member_feeds" json:"allow_member_feeds"` } func (q *Queries) GetLibraryByFolder(ctx context.Context, folderPath string) (GetLibraryByFolderRow, error) { @@ -6102,12 +6406,13 @@ func (q *Queries) GetLibraryByFolder(ctx context.Context, folderPath string) (Ge &i.CreatedByAdminID, &i.CreatedAt, &i.UpdatedAt, + &i.AllowMemberFeeds, ) return i, err } const GetLibraryByFolderPathPrefix = `-- name: GetLibraryByFolderPathPrefix :one -SELECT lf.library_id, lf.folder_path, l.id, l.name, l.description, l.library_type_id, l.created_by_admin_id, l.created_at, l.updated_at FROM library_folders lf +SELECT lf.library_id, lf.folder_path, l.id, l.name, l.description, l.library_type_id, l.created_by_admin_id, l.created_at, l.updated_at, l.allow_member_feeds FROM library_folders lf JOIN libraries l ON lf.library_id = l.id WHERE $1 LIKE lf.folder_path || '%' ORDER BY LENGTH(lf.folder_path) DESC @@ -6124,6 +6429,7 @@ type GetLibraryByFolderPathPrefixRow struct { CreatedByAdminID pgtype.UUID `db:"created_by_admin_id" json:"created_by_admin_id"` CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + AllowMemberFeeds bool `db:"allow_member_feeds" json:"allow_member_feeds"` } func (q *Queries) GetLibraryByFolderPathPrefix(ctx context.Context, folderPath string) (GetLibraryByFolderPathPrefixRow, error) { @@ -6139,12 +6445,13 @@ func (q *Queries) GetLibraryByFolderPathPrefix(ctx context.Context, folderPath s &i.CreatedByAdminID, &i.CreatedAt, &i.UpdatedAt, + &i.AllowMemberFeeds, ) return i, err } const GetLibraryFolders = `-- name: GetLibraryFolders :many -SELECT id, library_id, folder_path, created_at FROM library_folders WHERE library_id = $1 ORDER BY created_at +SELECT id, library_id, folder_path, created_at, purpose FROM library_folders WHERE library_id = $1 ORDER BY created_at ` func (q *Queries) GetLibraryFolders(ctx context.Context, libraryID pgtype.UUID) ([]LibraryFolders, error) { @@ -6161,6 +6468,7 @@ func (q *Queries) GetLibraryFolders(ctx context.Context, libraryID pgtype.UUID) &i.LibraryID, &i.FolderPath, &i.CreatedAt, + &i.Purpose, ); err != nil { return nil, err } @@ -6173,7 +6481,7 @@ func (q *Queries) GetLibraryFolders(ctx context.Context, libraryID pgtype.UUID) } const GetLibraryItems = `-- name: GetLibraryItems :many -SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, mi.source, mi.source_feed_id, mi.episode_guid, mi.feed_position, mi.season_number, mi.episode_number, mi.enclosure_url, mi.declared_duration_seconds, mi.transcript_url, mi.chapters_url, mi.hosts, mi.guests, mi.download_state, mi.download_error, mi.download_attempts FROM media_items mi WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid) AND mi.archived_at IS NULL AND mi.missing_scan_count = 0 @@ -6264,6 +6572,21 @@ func (q *Queries) GetLibraryItems(ctx context.Context, libraryID pgtype.UUID) ([ &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ); err != nil { return nil, err } @@ -6651,7 +6974,7 @@ func (q *Queries) GetLibraryVisibility(ctx context.Context, arg GetLibraryVisibi const GetLibraryWithType = `-- name: GetLibraryWithType :one SELECT - l.id, l.name, l.description, l.library_type_id, l.created_by_admin_id, l.created_at, l.updated_at, + l.id, l.name, l.description, l.library_type_id, l.created_by_admin_id, l.created_at, l.updated_at, l.allow_member_feeds, lt.name as type_name, lt.description as type_description, lt.allowed_extensions @@ -6668,6 +6991,7 @@ type GetLibraryWithTypeRow struct { CreatedByAdminID pgtype.UUID `db:"created_by_admin_id" json:"created_by_admin_id"` CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + AllowMemberFeeds bool `db:"allow_member_feeds" json:"allow_member_feeds"` TypeName string `db:"type_name" json:"type_name"` TypeDescription pgtype.Text `db:"type_description" json:"type_description"` AllowedExtensions []string `db:"allowed_extensions" json:"allowed_extensions"` @@ -6687,6 +7011,7 @@ func (q *Queries) GetLibraryWithType(ctx context.Context, id pgtype.UUID) (GetLi &i.CreatedByAdminID, &i.CreatedAt, &i.UpdatedAt, + &i.AllowMemberFeeds, &i.TypeName, &i.TypeDescription, &i.AllowedExtensions, @@ -6973,7 +7298,7 @@ func (q *Queries) GetMediaHighlights(ctx context.Context, arg GetMediaHighlights } const GetMediaItem = `-- name: GetMediaItem :one -SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE id = $1 +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts FROM media_items WHERE id = $1 ` func (q *Queries) GetMediaItem(ctx context.Context, id pgtype.UUID) (MediaItems, error) { @@ -7054,12 +7379,27 @@ func (q *Queries) GetMediaItem(ctx context.Context, id pgtype.UUID) (MediaItems, &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ) return i, err } const GetMediaItemByFilePath = `-- name: GetMediaItemByFilePath :one -SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_path = $1 AND library_id = $2 +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts FROM media_items WHERE file_path = $1 AND library_id = $2 ` type GetMediaItemByFilePathParams struct { @@ -7145,12 +7485,27 @@ func (q *Queries) GetMediaItemByFilePath(ctx context.Context, arg GetMediaItemBy &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ) return i, err } const GetMediaItemByFilePathAnyLibrary = `-- name: GetMediaItemByFilePathAnyLibrary :one -SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_path = $1 LIMIT 1 +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts FROM media_items WHERE file_path = $1 LIMIT 1 ` func (q *Queries) GetMediaItemByFilePathAnyLibrary(ctx context.Context, filePath string) (MediaItems, error) { @@ -7231,13 +7586,28 @@ func (q *Queries) GetMediaItemByFilePathAnyLibrary(ctx context.Context, filePath &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ) return i, err } const GetMediaItemByFilePathForSync = `-- name: GetMediaItemByFilePathForSync :one -SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_path = $1 +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts FROM media_items WHERE file_path = $1 ` // ============================================ @@ -7321,12 +7691,27 @@ func (q *Queries) GetMediaItemByFilePathForSync(ctx context.Context, filePath st &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ) return i, err } const GetMediaItemByKoboContentId = `-- name: GetMediaItemByKoboContentId :one -SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE kobo_content_id = $1 +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts FROM media_items WHERE kobo_content_id = $1 ` func (q *Queries) GetMediaItemByKoboContentId(ctx context.Context, koboContentID pgtype.Text) (MediaItems, error) { @@ -7407,12 +7792,27 @@ func (q *Queries) GetMediaItemByKoboContentId(ctx context.Context, koboContentID &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ) return i, err } const GetMediaItemByLibraryAndPath = `-- name: GetMediaItemByLibraryAndPath :one -SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE library_id = $1 AND file_path = $2 +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts FROM media_items WHERE library_id = $1 AND file_path = $2 ` type GetMediaItemByLibraryAndPathParams struct { @@ -7498,12 +7898,27 @@ func (q *Queries) GetMediaItemByLibraryAndPath(ctx context.Context, arg GetMedia &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ) return i, err } const GetMediaItemByOPFIdentifier = `-- name: GetMediaItemByOPFIdentifier :one -SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE opf_identifier = $1 +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts FROM media_items WHERE opf_identifier = $1 ` // Get media item by OPF identifier @@ -7585,12 +8000,27 @@ func (q *Queries) GetMediaItemByOPFIdentifier(ctx context.Context, opfIdentifier &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ) return i, err } const GetMediaItemByOPFUUID = `-- name: GetMediaItemByOPFUUID :one -SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE opf_uuid = $1 +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts FROM media_items WHERE opf_uuid = $1 ` // Get media item by OPF UUID @@ -7672,12 +8102,27 @@ func (q *Queries) GetMediaItemByOPFUUID(ctx context.Context, opfUuid pgtype.Text &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ) return i, err } const GetMediaItemBySHA256 = `-- name: GetMediaItemBySHA256 :one -SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_sha256 = $1 +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts FROM media_items WHERE file_sha256 = $1 ` // Get media item by SHA-256 hash @@ -7759,12 +8204,27 @@ func (q *Queries) GetMediaItemBySHA256(ctx context.Context, fileSha256 pgtype.Te &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ) return i, err } const GetMediaItemBySHA256AndLibrary = `-- name: GetMediaItemBySHA256AndLibrary :one -SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_sha256 = $1 AND library_id = $2 +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts FROM media_items WHERE file_sha256 = $1 AND library_id = $2 ` type GetMediaItemBySHA256AndLibraryParams struct { @@ -7851,6 +8311,21 @@ func (q *Queries) GetMediaItemBySHA256AndLibrary(ctx context.Context, arg GetMed &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ) return i, err } @@ -7971,7 +8446,7 @@ func (q *Queries) GetMediaItemUsageCounts(ctx context.Context, mediaItemID pgtyp } const GetMediaItemsBySHA256AndSameLibraryType = `-- name: GetMediaItemsBySHA256AndSameLibraryType :many -SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, lt.name AS library_type_name +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, mi.source, mi.source_feed_id, mi.episode_guid, mi.feed_position, mi.season_number, mi.episode_number, mi.enclosure_url, mi.declared_duration_seconds, mi.transcript_url, mi.chapters_url, mi.hosts, mi.guests, mi.download_state, mi.download_error, mi.download_attempts, lt.name AS library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id @@ -7990,81 +8465,96 @@ type GetMediaItemsBySHA256AndSameLibraryTypeParams struct { } type GetMediaItemsBySHA256AndSameLibraryTypeRow struct { - ID pgtype.UUID `db:"id" json:"id"` - LibraryID pgtype.UUID `db:"library_id" json:"library_id"` - Title string `db:"title" json:"title"` - Subtitle pgtype.Text `db:"subtitle" json:"subtitle"` - Author pgtype.Text `db:"author" json:"author"` - Isbn pgtype.Text `db:"isbn" json:"isbn"` - Description pgtype.Text `db:"description" json:"description"` - FilePath string `db:"file_path" json:"file_path"` - FileSize pgtype.Int8 `db:"file_size" json:"file_size"` - MimeType pgtype.Text `db:"mime_type" json:"mime_type"` - CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` - Series pgtype.Text `db:"series" json:"series"` - SeriesNumber pgtype.Float8 `db:"series_number" json:"series_number"` - Tags []string `db:"tags" json:"tags"` - Narrators []string `db:"narrators" json:"narrators"` - Authors []string `db:"authors" json:"authors"` - AuthorsSearch []string `db:"authors_search" json:"authors_search"` - Publishers []string `db:"publishers" json:"publishers"` - PublishersSearch []string `db:"publishers_search" json:"publishers_search"` - Abridged bool `db:"abridged" json:"abridged"` - Explicit bool `db:"explicit" json:"explicit"` - Asin pgtype.Text `db:"asin" json:"asin"` - DatePublished pgtype.Date `db:"date_published" json:"date_published"` - Publisher pgtype.Text `db:"publisher" json:"publisher"` - Contributors []string `db:"contributors" json:"contributors"` - Language pgtype.Text `db:"language" json:"language"` - Edition pgtype.Text `db:"edition" json:"edition"` - PageCount pgtype.Int4 `db:"page_count" json:"page_count"` - Genre pgtype.Text `db:"genre" json:"genre"` - CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"` - GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"` - OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"` - GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"` - AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"` - CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` - ImportedAt pgtype.Timestamptz `db:"imported_at" json:"imported_at"` - UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` - FormatGroup string `db:"format_group" json:"format_group"` - FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"` - IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"` - HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"` - TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"` - ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"` - EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"` - RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"` - KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"` - KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"` - MangaType pgtype.Text `db:"manga_type" json:"manga_type"` - ReadingDirection pgtype.Text `db:"reading_direction" json:"reading_direction"` - SeriesCount pgtype.Int4 `db:"series_count" json:"series_count"` - Volume pgtype.Int4 `db:"volume" json:"volume"` - Imprint pgtype.Text `db:"imprint" json:"imprint"` - AgeRating pgtype.Text `db:"age_rating" json:"age_rating"` - WebUrl pgtype.Text `db:"web_url" json:"web_url"` - StoryArc pgtype.Text `db:"story_arc" json:"story_arc"` - IsBlackAndWhite pgtype.Bool `db:"is_black_and_white" json:"is_black_and_white"` - MetadataNotes pgtype.Text `db:"metadata_notes" json:"metadata_notes"` - CommunityRating pgtype.Float8 `db:"community_rating" json:"community_rating"` - AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` - ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` - Summary pgtype.Text `db:"summary" json:"summary"` - MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` - MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` - ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` - ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` - LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` - AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` - TagsSearch []string `db:"tags_search" json:"tags_search"` - ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` - NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` - FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` - OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"` - OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"` - HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"` - LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"` + ID pgtype.UUID `db:"id" json:"id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + Title string `db:"title" json:"title"` + Subtitle pgtype.Text `db:"subtitle" json:"subtitle"` + Author pgtype.Text `db:"author" json:"author"` + Isbn pgtype.Text `db:"isbn" json:"isbn"` + Description pgtype.Text `db:"description" json:"description"` + FilePath string `db:"file_path" json:"file_path"` + FileSize pgtype.Int8 `db:"file_size" json:"file_size"` + MimeType pgtype.Text `db:"mime_type" json:"mime_type"` + CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` + Series pgtype.Text `db:"series" json:"series"` + SeriesNumber pgtype.Float8 `db:"series_number" json:"series_number"` + Tags []string `db:"tags" json:"tags"` + Narrators []string `db:"narrators" json:"narrators"` + Authors []string `db:"authors" json:"authors"` + AuthorsSearch []string `db:"authors_search" json:"authors_search"` + Publishers []string `db:"publishers" json:"publishers"` + PublishersSearch []string `db:"publishers_search" json:"publishers_search"` + Abridged bool `db:"abridged" json:"abridged"` + Explicit bool `db:"explicit" json:"explicit"` + Asin pgtype.Text `db:"asin" json:"asin"` + DatePublished pgtype.Date `db:"date_published" json:"date_published"` + Publisher pgtype.Text `db:"publisher" json:"publisher"` + Contributors []string `db:"contributors" json:"contributors"` + Language pgtype.Text `db:"language" json:"language"` + Edition pgtype.Text `db:"edition" json:"edition"` + PageCount pgtype.Int4 `db:"page_count" json:"page_count"` + Genre pgtype.Text `db:"genre" json:"genre"` + CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"` + GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"` + OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"` + GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"` + AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + ImportedAt pgtype.Timestamptz `db:"imported_at" json:"imported_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + FormatGroup string `db:"format_group" json:"format_group"` + FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"` + IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"` + HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"` + TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"` + ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"` + EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"` + RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"` + KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"` + KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"` + MangaType pgtype.Text `db:"manga_type" json:"manga_type"` + ReadingDirection pgtype.Text `db:"reading_direction" json:"reading_direction"` + SeriesCount pgtype.Int4 `db:"series_count" json:"series_count"` + Volume pgtype.Int4 `db:"volume" json:"volume"` + Imprint pgtype.Text `db:"imprint" json:"imprint"` + AgeRating pgtype.Text `db:"age_rating" json:"age_rating"` + WebUrl pgtype.Text `db:"web_url" json:"web_url"` + StoryArc pgtype.Text `db:"story_arc" json:"story_arc"` + IsBlackAndWhite pgtype.Bool `db:"is_black_and_white" json:"is_black_and_white"` + MetadataNotes pgtype.Text `db:"metadata_notes" json:"metadata_notes"` + CommunityRating pgtype.Float8 `db:"community_rating" json:"community_rating"` + AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` + ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` + Summary pgtype.Text `db:"summary" json:"summary"` + MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` + MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` + ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` + ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` + LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` + AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` + TagsSearch []string `db:"tags_search" json:"tags_search"` + ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` + NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` + FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` + OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"` + OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"` + HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"` + Source string `db:"source" json:"source"` + SourceFeedID pgtype.UUID `db:"source_feed_id" json:"source_feed_id"` + EpisodeGuid pgtype.Text `db:"episode_guid" json:"episode_guid"` + FeedPosition pgtype.Int4 `db:"feed_position" json:"feed_position"` + SeasonNumber pgtype.Int4 `db:"season_number" json:"season_number"` + EpisodeNumber pgtype.Int4 `db:"episode_number" json:"episode_number"` + EnclosureUrl pgtype.Text `db:"enclosure_url" json:"enclosure_url"` + DeclaredDurationSeconds pgtype.Int4 `db:"declared_duration_seconds" json:"declared_duration_seconds"` + TranscriptUrl pgtype.Text `db:"transcript_url" json:"transcript_url"` + ChaptersUrl pgtype.Text `db:"chapters_url" json:"chapters_url"` + Hosts []string `db:"hosts" json:"hosts"` + Guests []string `db:"guests" json:"guests"` + DownloadState string `db:"download_state" json:"download_state"` + DownloadError pgtype.Text `db:"download_error" json:"download_error"` + DownloadAttempts int32 `db:"download_attempts" json:"download_attempts"` + LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"` } // Cross-library move detection: identical content registered in a different @@ -8156,6 +8646,21 @@ func (q *Queries) GetMediaItemsBySHA256AndSameLibraryType(ctx context.Context, a &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, &i.LibraryTypeName_2, ); err != nil { return nil, err @@ -8418,7 +8923,7 @@ func (q *Queries) GetNextRetryTime(ctx context.Context) (interface{}, error) { } const GetNotStartedItems = `-- name: GetNotStartedItems :many -SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, mi.source, mi.source_feed_id, mi.episode_guid, mi.feed_position, mi.season_number, mi.episode_number, mi.enclosure_url, mi.declared_duration_seconds, mi.transcript_url, mi.chapters_url, mi.hosts, mi.guests, mi.download_state, mi.download_error, mi.download_attempts FROM media_items mi WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid) AND mi.archived_at IS NULL AND mi.missing_scan_count = 0 @@ -8522,6 +9027,21 @@ func (q *Queries) GetNotStartedItems(ctx context.Context, arg GetNotStartedItems &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ); err != nil { return nil, err } @@ -8625,6 +9145,282 @@ func (q *Queries) GetPanelData(ctx context.Context, arg GetPanelDataParams) (Pan return i, err } +const GetPodcastEpisode = `-- name: GetPodcastEpisode :one +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts FROM media_items +WHERE id = $1 AND source = 'podcast_episode' +` + +func (q *Queries) GetPodcastEpisode(ctx context.Context, id pgtype.UUID) (MediaItems, error) { + row := q.db.QueryRow(ctx, GetPodcastEpisode, id) + var i MediaItems + err := row.Scan( + &i.ID, + &i.LibraryID, + &i.Title, + &i.Subtitle, + &i.Author, + &i.Isbn, + &i.Description, + &i.FilePath, + &i.FileSize, + &i.MimeType, + &i.CoverImagePath, + &i.Series, + &i.SeriesNumber, + &i.Tags, + &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, + &i.Abridged, + &i.Explicit, + &i.Asin, + &i.DatePublished, + &i.Publisher, + &i.Contributors, + &i.Language, + &i.Edition, + &i.PageCount, + &i.Genre, + &i.CopyrightYear, + &i.GoodreadsID, + &i.OpenlibraryID, + &i.GoogleBooksID, + &i.AddedByAdminID, + &i.CreatedAt, + &i.ImportedAt, + &i.UpdatedAt, + &i.FormatGroup, + &i.FormatMimetype, + &i.IsReflowable, + &i.HasFixedLayout, + &i.TotalCharacters, + &i.ChapterCount, + &i.EntitlementID, + &i.RevisionNumber, + &i.KoboContentID, + &i.KoboMetadata, + &i.MangaType, + &i.ReadingDirection, + &i.SeriesCount, + &i.Volume, + &i.Imprint, + &i.AgeRating, + &i.WebUrl, + &i.StoryArc, + &i.IsBlackAndWhite, + &i.MetadataNotes, + &i.CommunityRating, + &i.AlternateInfo, + &i.ScanInformation, + &i.Summary, + &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, + &i.ChapterMetadata, + &i.LibraryTypeName, + &i.AudioMetadata, + &i.TagsSearch, + &i.ContributorsSearch, + &i.NarratorsSearch, + &i.FileSha256, + &i.OpfIdentifier, + &i.OpfUuid, + &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, + ) + return i, err +} + +const GetPodcastFeed = `-- name: GetPodcastFeed :one +SELECT id, library_id, feed_url, podcast_guid, title, description, host, publisher, owner_name, owner_email, language, tags, explicit, artwork_path, feed_type, medium, is_complete, etag, last_modified, last_fetched_at, last_build_date, created_by_admin_id, created_at, updated_at FROM podcast_feeds WHERE id = $1 +` + +func (q *Queries) GetPodcastFeed(ctx context.Context, id pgtype.UUID) (PodcastFeeds, error) { + row := q.db.QueryRow(ctx, GetPodcastFeed, id) + var i PodcastFeeds + err := row.Scan( + &i.ID, + &i.LibraryID, + &i.FeedUrl, + &i.PodcastGuid, + &i.Title, + &i.Description, + &i.Host, + &i.Publisher, + &i.OwnerName, + &i.OwnerEmail, + &i.Language, + &i.Tags, + &i.Explicit, + &i.ArtworkPath, + &i.FeedType, + &i.Medium, + &i.IsComplete, + &i.Etag, + &i.LastModified, + &i.LastFetchedAt, + &i.LastBuildDate, + &i.CreatedByAdminID, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const GetPodcastFeedByGuid = `-- name: GetPodcastFeedByGuid :one +SELECT id, library_id, feed_url, podcast_guid, title, description, host, publisher, owner_name, owner_email, language, tags, explicit, artwork_path, feed_type, medium, is_complete, etag, last_modified, last_fetched_at, last_build_date, created_by_admin_id, created_at, updated_at FROM podcast_feeds +WHERE library_id = $1 AND podcast_guid = $2 +LIMIT 1 +` + +type GetPodcastFeedByGuidParams struct { + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + PodcastGuid pgtype.Text `db:"podcast_guid" json:"podcast_guid"` +} + +// URL-migration dedupe: same canonical guid within the library reuses +// the existing row. +func (q *Queries) GetPodcastFeedByGuid(ctx context.Context, arg GetPodcastFeedByGuidParams) (PodcastFeeds, error) { + row := q.db.QueryRow(ctx, GetPodcastFeedByGuid, arg.LibraryID, arg.PodcastGuid) + var i PodcastFeeds + err := row.Scan( + &i.ID, + &i.LibraryID, + &i.FeedUrl, + &i.PodcastGuid, + &i.Title, + &i.Description, + &i.Host, + &i.Publisher, + &i.OwnerName, + &i.OwnerEmail, + &i.Language, + &i.Tags, + &i.Explicit, + &i.ArtworkPath, + &i.FeedType, + &i.Medium, + &i.IsComplete, + &i.Etag, + &i.LastModified, + &i.LastFetchedAt, + &i.LastBuildDate, + &i.CreatedByAdminID, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const GetPodcastLibraryFolder = `-- name: GetPodcastLibraryFolder :many +SELECT lf.id, lf.library_id, lf.folder_path, lf.created_at, lf.purpose +FROM library_folders lf +WHERE lf.library_id = $1 AND lf.purpose = 'podcast' +` + +// The download root (purpose='podcast') of a podcasts library. +func (q *Queries) GetPodcastLibraryFolder(ctx context.Context, libraryID pgtype.UUID) ([]LibraryFolders, error) { + rows, err := q.db.Query(ctx, GetPodcastLibraryFolder, libraryID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []LibraryFolders{} + for rows.Next() { + var i LibraryFolders + if err := rows.Scan( + &i.ID, + &i.LibraryID, + &i.FolderPath, + &i.CreatedAt, + &i.Purpose, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetPodcastSubscription = `-- name: GetPodcastSubscription :one +SELECT id, user_id, feed_id, display_name, auto_download, auto_download_count, created_at FROM podcast_subscriptions WHERE user_id = $1 AND feed_id = $2 +` + +type GetPodcastSubscriptionParams struct { + UserID pgtype.UUID `db:"user_id" json:"user_id"` + FeedID pgtype.UUID `db:"feed_id" json:"feed_id"` +} + +func (q *Queries) GetPodcastSubscription(ctx context.Context, arg GetPodcastSubscriptionParams) (PodcastSubscriptions, error) { + row := q.db.QueryRow(ctx, GetPodcastSubscription, arg.UserID, arg.FeedID) + var i PodcastSubscriptions + err := row.Scan( + &i.ID, + &i.UserID, + &i.FeedID, + &i.DisplayName, + &i.AutoDownload, + &i.AutoDownloadCount, + &i.CreatedAt, + ) + return i, err +} + +const GetPodcastsLibraryForUser = `-- name: GetPodcastsLibraryForUser :many +SELECT l.id, l.name +FROM libraries l +JOIN library_types lt ON lt.id = l.library_type_id AND lt.name = 'podcasts' +LEFT JOIN library_visibility lv ON lv.library_id = l.id AND lv.user_id = $1 +WHERE COALESCE(lv.is_visible, true) = true +ORDER BY l.created_at ASC +` + +type GetPodcastsLibraryForUserRow struct { + ID pgtype.UUID `db:"id" json:"id"` + Name string `db:"name" json:"name"` +} + +// The podcasts library/ies visible to the user (add-feed target list; +// a self-hosted instance typically has exactly one). +func (q *Queries) GetPodcastsLibraryForUser(ctx context.Context, userID pgtype.UUID) ([]GetPodcastsLibraryForUserRow, error) { + rows, err := q.db.Query(ctx, GetPodcastsLibraryForUser, userID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetPodcastsLibraryForUserRow{} + for rows.Next() { + var i GetPodcastsLibraryForUserRow + if err := rows.Scan(&i.ID, &i.Name); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const GetPopularBooks = `-- name: GetPopularBooks :many SELECT mi.id, @@ -8857,7 +9653,7 @@ func (q *Queries) GetReadingSpeed(ctx context.Context, arg GetReadingSpeedParams } const GetRecentlyAddedItems = `-- name: GetRecentlyAddedItems :many -SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, mi.source, mi.source_feed_id, mi.episode_guid, mi.feed_position, mi.season_number, mi.episode_number, mi.enclosure_url, mi.declared_duration_seconds, mi.transcript_url, mi.chapters_url, mi.hosts, mi.guests, mi.download_state, mi.download_error, mi.download_attempts FROM media_items mi WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid) AND mi.archived_at IS NULL AND mi.missing_scan_count = 0 @@ -8954,6 +9750,21 @@ func (q *Queries) GetRecentlyAddedItems(ctx context.Context, arg GetRecentlyAdde &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ); err != nil { return nil, err } @@ -8966,7 +9777,7 @@ func (q *Queries) GetRecentlyAddedItems(ctx context.Context, arg GetRecentlyAdde } const GetRecentlyReadItems = `-- name: GetRecentlyReadItems :many -SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, mi.source, mi.source_feed_id, mi.episode_guid, mi.feed_position, mi.season_number, mi.episode_number, mi.enclosure_url, mi.declared_duration_seconds, mi.transcript_url, mi.chapters_url, mi.hosts, mi.guests, mi.download_state, mi.download_error, mi.download_attempts FROM media_items mi INNER JOIN ( SELECT DISTINCT ON (media_item_id) media_item_id, last_read_at FROM reading_progress @@ -9071,6 +9882,21 @@ func (q *Queries) GetRecentlyReadItems(ctx context.Context, arg GetRecentlyReadI &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ); err != nil { return nil, err } @@ -9245,7 +10071,7 @@ func (q *Queries) GetScanStateByLibrary(ctx context.Context, libraryID pgtype.UU } const GetSeriesBooks = `-- name: GetSeriesBooks :many -SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts FROM media_items WHERE series = $1 AND library_id = $2 AND archived_at IS NULL @@ -9345,6 +10171,21 @@ func (q *Queries) GetSeriesBooks(ctx context.Context, arg GetSeriesBooksParams) &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ); err != nil { return nil, err } @@ -10966,7 +11807,7 @@ func (q *Queries) GetUserReadingSessionsRange(ctx context.Context, arg GetUserRe } const GetUserVisibleLibraries = `-- name: GetUserVisibleLibraries :many -SELECT l.id, l.name, l.description, l.library_type_id, l.created_by_admin_id, l.created_at, l.updated_at, lt.name as type_name, lt.description as type_description, +SELECT l.id, l.name, l.description, l.library_type_id, l.created_by_admin_id, l.created_at, l.updated_at, l.allow_member_feeds, lt.name as type_name, lt.description as type_description, COALESCE(lv.is_visible, true) as is_visible FROM libraries l JOIN library_types lt ON l.library_type_id = lt.id @@ -10983,6 +11824,7 @@ type GetUserVisibleLibrariesRow struct { CreatedByAdminID pgtype.UUID `db:"created_by_admin_id" json:"created_by_admin_id"` CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + AllowMemberFeeds bool `db:"allow_member_feeds" json:"allow_member_feeds"` TypeName string `db:"type_name" json:"type_name"` TypeDescription pgtype.Text `db:"type_description" json:"type_description"` IsVisible bool `db:"is_visible" json:"is_visible"` @@ -11005,6 +11847,7 @@ func (q *Queries) GetUserVisibleLibraries(ctx context.Context, userID pgtype.UUI &i.CreatedByAdminID, &i.CreatedAt, &i.UpdatedAt, + &i.AllowMemberFeeds, &i.TypeName, &i.TypeDescription, &i.IsVisible, @@ -11056,7 +11899,7 @@ func (q *Queries) GetVisibleLibraryMediaCounts(ctx context.Context, userID pgtyp } const GetVisibleLiveMediaForRules = `-- name: GetVisibleLiveMediaForRules :many -SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, mi.source, mi.source_feed_id, mi.episode_guid, mi.feed_position, mi.season_number, mi.episode_number, mi.enclosure_url, mi.declared_duration_seconds, mi.transcript_url, mi.chapters_url, mi.hosts, mi.guests, mi.download_state, mi.download_error, mi.download_attempts FROM media_items mi WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid) AND ($2::uuid[] IS NULL OR mi.library_id = ANY($2::uuid[])) AND mi.archived_at IS NULL @@ -11158,6 +12001,21 @@ func (q *Queries) GetVisibleLiveMediaForRules(ctx context.Context, arg GetVisibl &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ); err != nil { return nil, err } @@ -11683,8 +12541,42 @@ func (q *Queries) ListDevicesByUser(ctx context.Context, userID pgtype.UUID) ([] return items, nil } +const ListFeedSubscribers = `-- name: ListFeedSubscribers :many +SELECT user_id, auto_download, auto_download_count +FROM podcast_subscriptions +WHERE feed_id = $1 +` + +type ListFeedSubscribersRow struct { + UserID pgtype.UUID `db:"user_id" json:"user_id"` + AutoDownload string `db:"auto_download" json:"auto_download"` + AutoDownloadCount pgtype.Int4 `db:"auto_download_count" json:"auto_download_count"` +} + +// Download-worker policy union (D6): most aggressive policy wins for +// what arrives. Runs under an admin identity (background job). +func (q *Queries) ListFeedSubscribers(ctx context.Context, feedID pgtype.UUID) ([]ListFeedSubscribersRow, error) { + rows, err := q.db.Query(ctx, ListFeedSubscribers, feedID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListFeedSubscribersRow{} + for rows.Next() { + var i ListFeedSubscribersRow + if err := rows.Scan(&i.UserID, &i.AutoDownload, &i.AutoDownloadCount); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const ListHiddenMediaItems = `-- name: ListHiddenMediaItems :many -SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name, +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, mi.source, mi.source_feed_id, mi.episode_guid, mi.feed_position, mi.season_number, mi.episode_number, mi.enclosure_url, mi.declared_duration_seconds, mi.transcript_url, mi.chapters_url, mi.hosts, mi.guests, mi.download_state, mi.download_error, mi.download_attempts, l.name as library_name, lt.name as library_type_name, twin.id AS match_id, COALESCE(twin.title, '') AS match_title, twin_library.name AS match_library_name FROM media_items mi @@ -11711,85 +12603,100 @@ ORDER BY mi.archived_at NULLS LAST, mi.updated_at DESC ` type ListHiddenMediaItemsRow struct { - ID pgtype.UUID `db:"id" json:"id"` - LibraryID pgtype.UUID `db:"library_id" json:"library_id"` - Title string `db:"title" json:"title"` - Subtitle pgtype.Text `db:"subtitle" json:"subtitle"` - Author pgtype.Text `db:"author" json:"author"` - Isbn pgtype.Text `db:"isbn" json:"isbn"` - Description pgtype.Text `db:"description" json:"description"` - FilePath string `db:"file_path" json:"file_path"` - FileSize pgtype.Int8 `db:"file_size" json:"file_size"` - MimeType pgtype.Text `db:"mime_type" json:"mime_type"` - CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` - Series pgtype.Text `db:"series" json:"series"` - SeriesNumber pgtype.Float8 `db:"series_number" json:"series_number"` - Tags []string `db:"tags" json:"tags"` - Narrators []string `db:"narrators" json:"narrators"` - Authors []string `db:"authors" json:"authors"` - AuthorsSearch []string `db:"authors_search" json:"authors_search"` - Publishers []string `db:"publishers" json:"publishers"` - PublishersSearch []string `db:"publishers_search" json:"publishers_search"` - Abridged bool `db:"abridged" json:"abridged"` - Explicit bool `db:"explicit" json:"explicit"` - Asin pgtype.Text `db:"asin" json:"asin"` - DatePublished pgtype.Date `db:"date_published" json:"date_published"` - Publisher pgtype.Text `db:"publisher" json:"publisher"` - Contributors []string `db:"contributors" json:"contributors"` - Language pgtype.Text `db:"language" json:"language"` - Edition pgtype.Text `db:"edition" json:"edition"` - PageCount pgtype.Int4 `db:"page_count" json:"page_count"` - Genre pgtype.Text `db:"genre" json:"genre"` - CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"` - GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"` - OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"` - GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"` - AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"` - CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` - ImportedAt pgtype.Timestamptz `db:"imported_at" json:"imported_at"` - UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` - FormatGroup string `db:"format_group" json:"format_group"` - FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"` - IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"` - HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"` - TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"` - ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"` - EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"` - RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"` - KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"` - KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"` - MangaType pgtype.Text `db:"manga_type" json:"manga_type"` - ReadingDirection pgtype.Text `db:"reading_direction" json:"reading_direction"` - SeriesCount pgtype.Int4 `db:"series_count" json:"series_count"` - Volume pgtype.Int4 `db:"volume" json:"volume"` - Imprint pgtype.Text `db:"imprint" json:"imprint"` - AgeRating pgtype.Text `db:"age_rating" json:"age_rating"` - WebUrl pgtype.Text `db:"web_url" json:"web_url"` - StoryArc pgtype.Text `db:"story_arc" json:"story_arc"` - IsBlackAndWhite pgtype.Bool `db:"is_black_and_white" json:"is_black_and_white"` - MetadataNotes pgtype.Text `db:"metadata_notes" json:"metadata_notes"` - CommunityRating pgtype.Float8 `db:"community_rating" json:"community_rating"` - AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` - ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` - Summary pgtype.Text `db:"summary" json:"summary"` - MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` - MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` - ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` - ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` - LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` - AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` - TagsSearch []string `db:"tags_search" json:"tags_search"` - ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` - NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` - FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` - OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"` - OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"` - HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"` - LibraryName string `db:"library_name" json:"library_name"` - LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"` - MatchID pgtype.UUID `db:"match_id" json:"match_id"` - MatchTitle string `db:"match_title" json:"match_title"` - MatchLibraryName pgtype.Text `db:"match_library_name" json:"match_library_name"` + ID pgtype.UUID `db:"id" json:"id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + Title string `db:"title" json:"title"` + Subtitle pgtype.Text `db:"subtitle" json:"subtitle"` + Author pgtype.Text `db:"author" json:"author"` + Isbn pgtype.Text `db:"isbn" json:"isbn"` + Description pgtype.Text `db:"description" json:"description"` + FilePath string `db:"file_path" json:"file_path"` + FileSize pgtype.Int8 `db:"file_size" json:"file_size"` + MimeType pgtype.Text `db:"mime_type" json:"mime_type"` + CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` + Series pgtype.Text `db:"series" json:"series"` + SeriesNumber pgtype.Float8 `db:"series_number" json:"series_number"` + Tags []string `db:"tags" json:"tags"` + Narrators []string `db:"narrators" json:"narrators"` + Authors []string `db:"authors" json:"authors"` + AuthorsSearch []string `db:"authors_search" json:"authors_search"` + Publishers []string `db:"publishers" json:"publishers"` + PublishersSearch []string `db:"publishers_search" json:"publishers_search"` + Abridged bool `db:"abridged" json:"abridged"` + Explicit bool `db:"explicit" json:"explicit"` + Asin pgtype.Text `db:"asin" json:"asin"` + DatePublished pgtype.Date `db:"date_published" json:"date_published"` + Publisher pgtype.Text `db:"publisher" json:"publisher"` + Contributors []string `db:"contributors" json:"contributors"` + Language pgtype.Text `db:"language" json:"language"` + Edition pgtype.Text `db:"edition" json:"edition"` + PageCount pgtype.Int4 `db:"page_count" json:"page_count"` + Genre pgtype.Text `db:"genre" json:"genre"` + CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"` + GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"` + OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"` + GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"` + AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + ImportedAt pgtype.Timestamptz `db:"imported_at" json:"imported_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + FormatGroup string `db:"format_group" json:"format_group"` + FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"` + IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"` + HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"` + TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"` + ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"` + EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"` + RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"` + KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"` + KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"` + MangaType pgtype.Text `db:"manga_type" json:"manga_type"` + ReadingDirection pgtype.Text `db:"reading_direction" json:"reading_direction"` + SeriesCount pgtype.Int4 `db:"series_count" json:"series_count"` + Volume pgtype.Int4 `db:"volume" json:"volume"` + Imprint pgtype.Text `db:"imprint" json:"imprint"` + AgeRating pgtype.Text `db:"age_rating" json:"age_rating"` + WebUrl pgtype.Text `db:"web_url" json:"web_url"` + StoryArc pgtype.Text `db:"story_arc" json:"story_arc"` + IsBlackAndWhite pgtype.Bool `db:"is_black_and_white" json:"is_black_and_white"` + MetadataNotes pgtype.Text `db:"metadata_notes" json:"metadata_notes"` + CommunityRating pgtype.Float8 `db:"community_rating" json:"community_rating"` + AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` + ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` + Summary pgtype.Text `db:"summary" json:"summary"` + MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` + MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` + ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` + ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` + LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` + AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` + TagsSearch []string `db:"tags_search" json:"tags_search"` + ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` + NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` + FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` + OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"` + OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"` + HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"` + Source string `db:"source" json:"source"` + SourceFeedID pgtype.UUID `db:"source_feed_id" json:"source_feed_id"` + EpisodeGuid pgtype.Text `db:"episode_guid" json:"episode_guid"` + FeedPosition pgtype.Int4 `db:"feed_position" json:"feed_position"` + SeasonNumber pgtype.Int4 `db:"season_number" json:"season_number"` + EpisodeNumber pgtype.Int4 `db:"episode_number" json:"episode_number"` + EnclosureUrl pgtype.Text `db:"enclosure_url" json:"enclosure_url"` + DeclaredDurationSeconds pgtype.Int4 `db:"declared_duration_seconds" json:"declared_duration_seconds"` + TranscriptUrl pgtype.Text `db:"transcript_url" json:"transcript_url"` + ChaptersUrl pgtype.Text `db:"chapters_url" json:"chapters_url"` + Hosts []string `db:"hosts" json:"hosts"` + Guests []string `db:"guests" json:"guests"` + DownloadState string `db:"download_state" json:"download_state"` + DownloadError pgtype.Text `db:"download_error" json:"download_error"` + DownloadAttempts int32 `db:"download_attempts" json:"download_attempts"` + LibraryName string `db:"library_name" json:"library_name"` + LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"` + MatchID pgtype.UUID `db:"match_id" json:"match_id"` + MatchTitle string `db:"match_title" json:"match_title"` + MatchLibraryName pgtype.Text `db:"match_library_name" json:"match_library_name"` } // Items hidden from libraries: either missing from disk (waiting on the @@ -11882,6 +12789,21 @@ func (q *Queries) ListHiddenMediaItems(ctx context.Context) ([]ListHiddenMediaIt &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, &i.LibraryName, &i.LibraryTypeName_2, &i.MatchID, @@ -11965,7 +12887,7 @@ func (q *Queries) ListIgnoredUnlinkedBooksForUser(ctx context.Context, userID pg } const ListLibraries = `-- name: ListLibraries :many -SELECT l.id, l.name, l.description, l.library_type_id, l.created_by_admin_id, l.created_at, l.updated_at, lt.name as type_name, lt.description as type_description +SELECT l.id, l.name, l.description, l.library_type_id, l.created_by_admin_id, l.created_at, l.updated_at, l.allow_member_feeds, lt.name as type_name, lt.description as type_description FROM libraries l JOIN library_types lt ON l.library_type_id = lt.id ORDER BY l.created_at DESC @@ -11979,6 +12901,7 @@ type ListLibrariesRow struct { CreatedByAdminID pgtype.UUID `db:"created_by_admin_id" json:"created_by_admin_id"` CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + AllowMemberFeeds bool `db:"allow_member_feeds" json:"allow_member_feeds"` TypeName string `db:"type_name" json:"type_name"` TypeDescription pgtype.Text `db:"type_description" json:"type_description"` } @@ -12000,6 +12923,7 @@ func (q *Queries) ListLibraries(ctx context.Context) ([]ListLibrariesRow, error) &i.CreatedByAdminID, &i.CreatedAt, &i.UpdatedAt, + &i.AllowMemberFeeds, &i.TypeName, &i.TypeDescription, ); err != nil { @@ -12014,7 +12938,7 @@ func (q *Queries) ListLibraries(ctx context.Context) ([]ListLibrariesRow, error) } const ListMediaItems = `-- name: ListMediaItems :many -SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, mi.source, mi.source_feed_id, mi.episode_guid, mi.feed_position, mi.season_number, mi.episode_number, mi.enclosure_url, mi.declared_duration_seconds, mi.transcript_url, mi.chapters_url, mi.hosts, mi.guests, mi.download_state, mi.download_error, mi.download_attempts, l.name as library_name, lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id @@ -12031,82 +12955,97 @@ type ListMediaItemsParams struct { } type ListMediaItemsRow struct { - ID pgtype.UUID `db:"id" json:"id"` - LibraryID pgtype.UUID `db:"library_id" json:"library_id"` - Title string `db:"title" json:"title"` - Subtitle pgtype.Text `db:"subtitle" json:"subtitle"` - Author pgtype.Text `db:"author" json:"author"` - Isbn pgtype.Text `db:"isbn" json:"isbn"` - Description pgtype.Text `db:"description" json:"description"` - FilePath string `db:"file_path" json:"file_path"` - FileSize pgtype.Int8 `db:"file_size" json:"file_size"` - MimeType pgtype.Text `db:"mime_type" json:"mime_type"` - CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` - Series pgtype.Text `db:"series" json:"series"` - SeriesNumber pgtype.Float8 `db:"series_number" json:"series_number"` - Tags []string `db:"tags" json:"tags"` - Narrators []string `db:"narrators" json:"narrators"` - Authors []string `db:"authors" json:"authors"` - AuthorsSearch []string `db:"authors_search" json:"authors_search"` - Publishers []string `db:"publishers" json:"publishers"` - PublishersSearch []string `db:"publishers_search" json:"publishers_search"` - Abridged bool `db:"abridged" json:"abridged"` - Explicit bool `db:"explicit" json:"explicit"` - Asin pgtype.Text `db:"asin" json:"asin"` - DatePublished pgtype.Date `db:"date_published" json:"date_published"` - Publisher pgtype.Text `db:"publisher" json:"publisher"` - Contributors []string `db:"contributors" json:"contributors"` - Language pgtype.Text `db:"language" json:"language"` - Edition pgtype.Text `db:"edition" json:"edition"` - PageCount pgtype.Int4 `db:"page_count" json:"page_count"` - Genre pgtype.Text `db:"genre" json:"genre"` - CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"` - GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"` - OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"` - GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"` - AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"` - CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` - ImportedAt pgtype.Timestamptz `db:"imported_at" json:"imported_at"` - UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` - FormatGroup string `db:"format_group" json:"format_group"` - FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"` - IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"` - HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"` - TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"` - ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"` - EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"` - RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"` - KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"` - KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"` - MangaType pgtype.Text `db:"manga_type" json:"manga_type"` - ReadingDirection pgtype.Text `db:"reading_direction" json:"reading_direction"` - SeriesCount pgtype.Int4 `db:"series_count" json:"series_count"` - Volume pgtype.Int4 `db:"volume" json:"volume"` - Imprint pgtype.Text `db:"imprint" json:"imprint"` - AgeRating pgtype.Text `db:"age_rating" json:"age_rating"` - WebUrl pgtype.Text `db:"web_url" json:"web_url"` - StoryArc pgtype.Text `db:"story_arc" json:"story_arc"` - IsBlackAndWhite pgtype.Bool `db:"is_black_and_white" json:"is_black_and_white"` - MetadataNotes pgtype.Text `db:"metadata_notes" json:"metadata_notes"` - CommunityRating pgtype.Float8 `db:"community_rating" json:"community_rating"` - AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` - ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` - Summary pgtype.Text `db:"summary" json:"summary"` - MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` - MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` - ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` - ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` - LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` - AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` - TagsSearch []string `db:"tags_search" json:"tags_search"` - ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` - NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` - FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` - OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"` - OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"` - HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"` - LibraryName string `db:"library_name" json:"library_name"` - LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"` + ID pgtype.UUID `db:"id" json:"id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + Title string `db:"title" json:"title"` + Subtitle pgtype.Text `db:"subtitle" json:"subtitle"` + Author pgtype.Text `db:"author" json:"author"` + Isbn pgtype.Text `db:"isbn" json:"isbn"` + Description pgtype.Text `db:"description" json:"description"` + FilePath string `db:"file_path" json:"file_path"` + FileSize pgtype.Int8 `db:"file_size" json:"file_size"` + MimeType pgtype.Text `db:"mime_type" json:"mime_type"` + CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` + Series pgtype.Text `db:"series" json:"series"` + SeriesNumber pgtype.Float8 `db:"series_number" json:"series_number"` + Tags []string `db:"tags" json:"tags"` + Narrators []string `db:"narrators" json:"narrators"` + Authors []string `db:"authors" json:"authors"` + AuthorsSearch []string `db:"authors_search" json:"authors_search"` + Publishers []string `db:"publishers" json:"publishers"` + PublishersSearch []string `db:"publishers_search" json:"publishers_search"` + Abridged bool `db:"abridged" json:"abridged"` + Explicit bool `db:"explicit" json:"explicit"` + Asin pgtype.Text `db:"asin" json:"asin"` + DatePublished pgtype.Date `db:"date_published" json:"date_published"` + Publisher pgtype.Text `db:"publisher" json:"publisher"` + Contributors []string `db:"contributors" json:"contributors"` + Language pgtype.Text `db:"language" json:"language"` + Edition pgtype.Text `db:"edition" json:"edition"` + PageCount pgtype.Int4 `db:"page_count" json:"page_count"` + Genre pgtype.Text `db:"genre" json:"genre"` + CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"` + GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"` + OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"` + GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"` + AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + ImportedAt pgtype.Timestamptz `db:"imported_at" json:"imported_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + FormatGroup string `db:"format_group" json:"format_group"` + FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"` + IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"` + HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"` + TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"` + ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"` + EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"` + RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"` + KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"` + KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"` + MangaType pgtype.Text `db:"manga_type" json:"manga_type"` + ReadingDirection pgtype.Text `db:"reading_direction" json:"reading_direction"` + SeriesCount pgtype.Int4 `db:"series_count" json:"series_count"` + Volume pgtype.Int4 `db:"volume" json:"volume"` + Imprint pgtype.Text `db:"imprint" json:"imprint"` + AgeRating pgtype.Text `db:"age_rating" json:"age_rating"` + WebUrl pgtype.Text `db:"web_url" json:"web_url"` + StoryArc pgtype.Text `db:"story_arc" json:"story_arc"` + IsBlackAndWhite pgtype.Bool `db:"is_black_and_white" json:"is_black_and_white"` + MetadataNotes pgtype.Text `db:"metadata_notes" json:"metadata_notes"` + CommunityRating pgtype.Float8 `db:"community_rating" json:"community_rating"` + AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` + ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` + Summary pgtype.Text `db:"summary" json:"summary"` + MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` + MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` + ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` + ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` + LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` + AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` + TagsSearch []string `db:"tags_search" json:"tags_search"` + ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` + NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` + FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` + OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"` + OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"` + HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"` + Source string `db:"source" json:"source"` + SourceFeedID pgtype.UUID `db:"source_feed_id" json:"source_feed_id"` + EpisodeGuid pgtype.Text `db:"episode_guid" json:"episode_guid"` + FeedPosition pgtype.Int4 `db:"feed_position" json:"feed_position"` + SeasonNumber pgtype.Int4 `db:"season_number" json:"season_number"` + EpisodeNumber pgtype.Int4 `db:"episode_number" json:"episode_number"` + EnclosureUrl pgtype.Text `db:"enclosure_url" json:"enclosure_url"` + DeclaredDurationSeconds pgtype.Int4 `db:"declared_duration_seconds" json:"declared_duration_seconds"` + TranscriptUrl pgtype.Text `db:"transcript_url" json:"transcript_url"` + ChaptersUrl pgtype.Text `db:"chapters_url" json:"chapters_url"` + Hosts []string `db:"hosts" json:"hosts"` + Guests []string `db:"guests" json:"guests"` + DownloadState string `db:"download_state" json:"download_state"` + DownloadError pgtype.Text `db:"download_error" json:"download_error"` + DownloadAttempts int32 `db:"download_attempts" json:"download_attempts"` + LibraryName string `db:"library_name" json:"library_name"` + LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"` } func (q *Queries) ListMediaItems(ctx context.Context, arg ListMediaItemsParams) ([]ListMediaItemsRow, error) { @@ -12193,6 +13132,21 @@ func (q *Queries) ListMediaItems(ctx context.Context, arg ListMediaItemsParams) &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, &i.LibraryName, &i.LibraryTypeName_2, ); err != nil { @@ -12207,7 +13161,7 @@ func (q *Queries) ListMediaItems(ctx context.Context, arg ListMediaItemsParams) } const ListMediaItemsByLibrary = `-- name: ListMediaItemsByLibrary :many -SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, mi.source, mi.source_feed_id, mi.episode_guid, mi.feed_position, mi.season_number, mi.episode_number, mi.enclosure_url, mi.declared_duration_seconds, mi.transcript_url, mi.chapters_url, mi.hosts, mi.guests, mi.download_state, mi.download_error, mi.download_attempts, l.name as library_name, lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id @@ -12218,82 +13172,97 @@ ORDER BY mi.created_at DESC ` type ListMediaItemsByLibraryRow struct { - ID pgtype.UUID `db:"id" json:"id"` - LibraryID pgtype.UUID `db:"library_id" json:"library_id"` - Title string `db:"title" json:"title"` - Subtitle pgtype.Text `db:"subtitle" json:"subtitle"` - Author pgtype.Text `db:"author" json:"author"` - Isbn pgtype.Text `db:"isbn" json:"isbn"` - Description pgtype.Text `db:"description" json:"description"` - FilePath string `db:"file_path" json:"file_path"` - FileSize pgtype.Int8 `db:"file_size" json:"file_size"` - MimeType pgtype.Text `db:"mime_type" json:"mime_type"` - CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` - Series pgtype.Text `db:"series" json:"series"` - SeriesNumber pgtype.Float8 `db:"series_number" json:"series_number"` - Tags []string `db:"tags" json:"tags"` - Narrators []string `db:"narrators" json:"narrators"` - Authors []string `db:"authors" json:"authors"` - AuthorsSearch []string `db:"authors_search" json:"authors_search"` - Publishers []string `db:"publishers" json:"publishers"` - PublishersSearch []string `db:"publishers_search" json:"publishers_search"` - Abridged bool `db:"abridged" json:"abridged"` - Explicit bool `db:"explicit" json:"explicit"` - Asin pgtype.Text `db:"asin" json:"asin"` - DatePublished pgtype.Date `db:"date_published" json:"date_published"` - Publisher pgtype.Text `db:"publisher" json:"publisher"` - Contributors []string `db:"contributors" json:"contributors"` - Language pgtype.Text `db:"language" json:"language"` - Edition pgtype.Text `db:"edition" json:"edition"` - PageCount pgtype.Int4 `db:"page_count" json:"page_count"` - Genre pgtype.Text `db:"genre" json:"genre"` - CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"` - GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"` - OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"` - GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"` - AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"` - CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` - ImportedAt pgtype.Timestamptz `db:"imported_at" json:"imported_at"` - UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` - FormatGroup string `db:"format_group" json:"format_group"` - FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"` - IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"` - HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"` - TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"` - ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"` - EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"` - RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"` - KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"` - KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"` - MangaType pgtype.Text `db:"manga_type" json:"manga_type"` - ReadingDirection pgtype.Text `db:"reading_direction" json:"reading_direction"` - SeriesCount pgtype.Int4 `db:"series_count" json:"series_count"` - Volume pgtype.Int4 `db:"volume" json:"volume"` - Imprint pgtype.Text `db:"imprint" json:"imprint"` - AgeRating pgtype.Text `db:"age_rating" json:"age_rating"` - WebUrl pgtype.Text `db:"web_url" json:"web_url"` - StoryArc pgtype.Text `db:"story_arc" json:"story_arc"` - IsBlackAndWhite pgtype.Bool `db:"is_black_and_white" json:"is_black_and_white"` - MetadataNotes pgtype.Text `db:"metadata_notes" json:"metadata_notes"` - CommunityRating pgtype.Float8 `db:"community_rating" json:"community_rating"` - AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` - ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` - Summary pgtype.Text `db:"summary" json:"summary"` - MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` - MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` - ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` - ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` - LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` - AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` - TagsSearch []string `db:"tags_search" json:"tags_search"` - ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` - NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` - FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` - OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"` - OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"` - HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"` - LibraryName string `db:"library_name" json:"library_name"` - LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"` + ID pgtype.UUID `db:"id" json:"id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + Title string `db:"title" json:"title"` + Subtitle pgtype.Text `db:"subtitle" json:"subtitle"` + Author pgtype.Text `db:"author" json:"author"` + Isbn pgtype.Text `db:"isbn" json:"isbn"` + Description pgtype.Text `db:"description" json:"description"` + FilePath string `db:"file_path" json:"file_path"` + FileSize pgtype.Int8 `db:"file_size" json:"file_size"` + MimeType pgtype.Text `db:"mime_type" json:"mime_type"` + CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` + Series pgtype.Text `db:"series" json:"series"` + SeriesNumber pgtype.Float8 `db:"series_number" json:"series_number"` + Tags []string `db:"tags" json:"tags"` + Narrators []string `db:"narrators" json:"narrators"` + Authors []string `db:"authors" json:"authors"` + AuthorsSearch []string `db:"authors_search" json:"authors_search"` + Publishers []string `db:"publishers" json:"publishers"` + PublishersSearch []string `db:"publishers_search" json:"publishers_search"` + Abridged bool `db:"abridged" json:"abridged"` + Explicit bool `db:"explicit" json:"explicit"` + Asin pgtype.Text `db:"asin" json:"asin"` + DatePublished pgtype.Date `db:"date_published" json:"date_published"` + Publisher pgtype.Text `db:"publisher" json:"publisher"` + Contributors []string `db:"contributors" json:"contributors"` + Language pgtype.Text `db:"language" json:"language"` + Edition pgtype.Text `db:"edition" json:"edition"` + PageCount pgtype.Int4 `db:"page_count" json:"page_count"` + Genre pgtype.Text `db:"genre" json:"genre"` + CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"` + GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"` + OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"` + GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"` + AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + ImportedAt pgtype.Timestamptz `db:"imported_at" json:"imported_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + FormatGroup string `db:"format_group" json:"format_group"` + FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"` + IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"` + HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"` + TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"` + ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"` + EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"` + RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"` + KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"` + KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"` + MangaType pgtype.Text `db:"manga_type" json:"manga_type"` + ReadingDirection pgtype.Text `db:"reading_direction" json:"reading_direction"` + SeriesCount pgtype.Int4 `db:"series_count" json:"series_count"` + Volume pgtype.Int4 `db:"volume" json:"volume"` + Imprint pgtype.Text `db:"imprint" json:"imprint"` + AgeRating pgtype.Text `db:"age_rating" json:"age_rating"` + WebUrl pgtype.Text `db:"web_url" json:"web_url"` + StoryArc pgtype.Text `db:"story_arc" json:"story_arc"` + IsBlackAndWhite pgtype.Bool `db:"is_black_and_white" json:"is_black_and_white"` + MetadataNotes pgtype.Text `db:"metadata_notes" json:"metadata_notes"` + CommunityRating pgtype.Float8 `db:"community_rating" json:"community_rating"` + AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` + ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` + Summary pgtype.Text `db:"summary" json:"summary"` + MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` + MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` + ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` + ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` + LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` + AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` + TagsSearch []string `db:"tags_search" json:"tags_search"` + ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` + NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` + FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` + OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"` + OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"` + HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"` + Source string `db:"source" json:"source"` + SourceFeedID pgtype.UUID `db:"source_feed_id" json:"source_feed_id"` + EpisodeGuid pgtype.Text `db:"episode_guid" json:"episode_guid"` + FeedPosition pgtype.Int4 `db:"feed_position" json:"feed_position"` + SeasonNumber pgtype.Int4 `db:"season_number" json:"season_number"` + EpisodeNumber pgtype.Int4 `db:"episode_number" json:"episode_number"` + EnclosureUrl pgtype.Text `db:"enclosure_url" json:"enclosure_url"` + DeclaredDurationSeconds pgtype.Int4 `db:"declared_duration_seconds" json:"declared_duration_seconds"` + TranscriptUrl pgtype.Text `db:"transcript_url" json:"transcript_url"` + ChaptersUrl pgtype.Text `db:"chapters_url" json:"chapters_url"` + Hosts []string `db:"hosts" json:"hosts"` + Guests []string `db:"guests" json:"guests"` + DownloadState string `db:"download_state" json:"download_state"` + DownloadError pgtype.Text `db:"download_error" json:"download_error"` + DownloadAttempts int32 `db:"download_attempts" json:"download_attempts"` + LibraryName string `db:"library_name" json:"library_name"` + LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"` } func (q *Queries) ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype.UUID) ([]ListMediaItemsByLibraryRow, error) { @@ -12380,6 +13349,21 @@ func (q *Queries) ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype. &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, &i.LibraryName, &i.LibraryTypeName_2, ); err != nil { @@ -12394,7 +13378,7 @@ func (q *Queries) ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype. } const ListMediaItemsByLibraryIncludingArchived = `-- name: ListMediaItemsByLibraryIncludingArchived :many -SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, mi.source, mi.source_feed_id, mi.episode_guid, mi.feed_position, mi.season_number, mi.episode_number, mi.enclosure_url, mi.declared_duration_seconds, mi.transcript_url, mi.chapters_url, mi.hosts, mi.guests, mi.download_state, mi.download_error, mi.download_attempts, l.name as library_name, lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id @@ -12403,82 +13387,97 @@ ORDER BY mi.created_at DESC ` type ListMediaItemsByLibraryIncludingArchivedRow struct { - ID pgtype.UUID `db:"id" json:"id"` - LibraryID pgtype.UUID `db:"library_id" json:"library_id"` - Title string `db:"title" json:"title"` - Subtitle pgtype.Text `db:"subtitle" json:"subtitle"` - Author pgtype.Text `db:"author" json:"author"` - Isbn pgtype.Text `db:"isbn" json:"isbn"` - Description pgtype.Text `db:"description" json:"description"` - FilePath string `db:"file_path" json:"file_path"` - FileSize pgtype.Int8 `db:"file_size" json:"file_size"` - MimeType pgtype.Text `db:"mime_type" json:"mime_type"` - CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` - Series pgtype.Text `db:"series" json:"series"` - SeriesNumber pgtype.Float8 `db:"series_number" json:"series_number"` - Tags []string `db:"tags" json:"tags"` - Narrators []string `db:"narrators" json:"narrators"` - Authors []string `db:"authors" json:"authors"` - AuthorsSearch []string `db:"authors_search" json:"authors_search"` - Publishers []string `db:"publishers" json:"publishers"` - PublishersSearch []string `db:"publishers_search" json:"publishers_search"` - Abridged bool `db:"abridged" json:"abridged"` - Explicit bool `db:"explicit" json:"explicit"` - Asin pgtype.Text `db:"asin" json:"asin"` - DatePublished pgtype.Date `db:"date_published" json:"date_published"` - Publisher pgtype.Text `db:"publisher" json:"publisher"` - Contributors []string `db:"contributors" json:"contributors"` - Language pgtype.Text `db:"language" json:"language"` - Edition pgtype.Text `db:"edition" json:"edition"` - PageCount pgtype.Int4 `db:"page_count" json:"page_count"` - Genre pgtype.Text `db:"genre" json:"genre"` - CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"` - GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"` - OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"` - GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"` - AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"` - CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` - ImportedAt pgtype.Timestamptz `db:"imported_at" json:"imported_at"` - UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` - FormatGroup string `db:"format_group" json:"format_group"` - FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"` - IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"` - HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"` - TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"` - ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"` - EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"` - RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"` - KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"` - KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"` - MangaType pgtype.Text `db:"manga_type" json:"manga_type"` - ReadingDirection pgtype.Text `db:"reading_direction" json:"reading_direction"` - SeriesCount pgtype.Int4 `db:"series_count" json:"series_count"` - Volume pgtype.Int4 `db:"volume" json:"volume"` - Imprint pgtype.Text `db:"imprint" json:"imprint"` - AgeRating pgtype.Text `db:"age_rating" json:"age_rating"` - WebUrl pgtype.Text `db:"web_url" json:"web_url"` - StoryArc pgtype.Text `db:"story_arc" json:"story_arc"` - IsBlackAndWhite pgtype.Bool `db:"is_black_and_white" json:"is_black_and_white"` - MetadataNotes pgtype.Text `db:"metadata_notes" json:"metadata_notes"` - CommunityRating pgtype.Float8 `db:"community_rating" json:"community_rating"` - AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` - ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` - Summary pgtype.Text `db:"summary" json:"summary"` - MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` - MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` - ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` - ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` - LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` - AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` - TagsSearch []string `db:"tags_search" json:"tags_search"` - ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` - NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` - FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` - OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"` - OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"` - HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"` - LibraryName string `db:"library_name" json:"library_name"` - LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"` + ID pgtype.UUID `db:"id" json:"id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + Title string `db:"title" json:"title"` + Subtitle pgtype.Text `db:"subtitle" json:"subtitle"` + Author pgtype.Text `db:"author" json:"author"` + Isbn pgtype.Text `db:"isbn" json:"isbn"` + Description pgtype.Text `db:"description" json:"description"` + FilePath string `db:"file_path" json:"file_path"` + FileSize pgtype.Int8 `db:"file_size" json:"file_size"` + MimeType pgtype.Text `db:"mime_type" json:"mime_type"` + CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` + Series pgtype.Text `db:"series" json:"series"` + SeriesNumber pgtype.Float8 `db:"series_number" json:"series_number"` + Tags []string `db:"tags" json:"tags"` + Narrators []string `db:"narrators" json:"narrators"` + Authors []string `db:"authors" json:"authors"` + AuthorsSearch []string `db:"authors_search" json:"authors_search"` + Publishers []string `db:"publishers" json:"publishers"` + PublishersSearch []string `db:"publishers_search" json:"publishers_search"` + Abridged bool `db:"abridged" json:"abridged"` + Explicit bool `db:"explicit" json:"explicit"` + Asin pgtype.Text `db:"asin" json:"asin"` + DatePublished pgtype.Date `db:"date_published" json:"date_published"` + Publisher pgtype.Text `db:"publisher" json:"publisher"` + Contributors []string `db:"contributors" json:"contributors"` + Language pgtype.Text `db:"language" json:"language"` + Edition pgtype.Text `db:"edition" json:"edition"` + PageCount pgtype.Int4 `db:"page_count" json:"page_count"` + Genre pgtype.Text `db:"genre" json:"genre"` + CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"` + GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"` + OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"` + GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"` + AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + ImportedAt pgtype.Timestamptz `db:"imported_at" json:"imported_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + FormatGroup string `db:"format_group" json:"format_group"` + FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"` + IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"` + HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"` + TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"` + ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"` + EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"` + RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"` + KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"` + KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"` + MangaType pgtype.Text `db:"manga_type" json:"manga_type"` + ReadingDirection pgtype.Text `db:"reading_direction" json:"reading_direction"` + SeriesCount pgtype.Int4 `db:"series_count" json:"series_count"` + Volume pgtype.Int4 `db:"volume" json:"volume"` + Imprint pgtype.Text `db:"imprint" json:"imprint"` + AgeRating pgtype.Text `db:"age_rating" json:"age_rating"` + WebUrl pgtype.Text `db:"web_url" json:"web_url"` + StoryArc pgtype.Text `db:"story_arc" json:"story_arc"` + IsBlackAndWhite pgtype.Bool `db:"is_black_and_white" json:"is_black_and_white"` + MetadataNotes pgtype.Text `db:"metadata_notes" json:"metadata_notes"` + CommunityRating pgtype.Float8 `db:"community_rating" json:"community_rating"` + AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` + ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` + Summary pgtype.Text `db:"summary" json:"summary"` + MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` + MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` + ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` + ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` + LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` + AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` + TagsSearch []string `db:"tags_search" json:"tags_search"` + ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` + NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` + FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` + OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"` + OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"` + HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"` + Source string `db:"source" json:"source"` + SourceFeedID pgtype.UUID `db:"source_feed_id" json:"source_feed_id"` + EpisodeGuid pgtype.Text `db:"episode_guid" json:"episode_guid"` + FeedPosition pgtype.Int4 `db:"feed_position" json:"feed_position"` + SeasonNumber pgtype.Int4 `db:"season_number" json:"season_number"` + EpisodeNumber pgtype.Int4 `db:"episode_number" json:"episode_number"` + EnclosureUrl pgtype.Text `db:"enclosure_url" json:"enclosure_url"` + DeclaredDurationSeconds pgtype.Int4 `db:"declared_duration_seconds" json:"declared_duration_seconds"` + TranscriptUrl pgtype.Text `db:"transcript_url" json:"transcript_url"` + ChaptersUrl pgtype.Text `db:"chapters_url" json:"chapters_url"` + Hosts []string `db:"hosts" json:"hosts"` + Guests []string `db:"guests" json:"guests"` + DownloadState string `db:"download_state" json:"download_state"` + DownloadError pgtype.Text `db:"download_error" json:"download_error"` + DownloadAttempts int32 `db:"download_attempts" json:"download_attempts"` + LibraryName string `db:"library_name" json:"library_name"` + LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"` } func (q *Queries) ListMediaItemsByLibraryIncludingArchived(ctx context.Context, libraryID pgtype.UUID) ([]ListMediaItemsByLibraryIncludingArchivedRow, error) { @@ -12565,6 +13564,21 @@ func (q *Queries) ListMediaItemsByLibraryIncludingArchived(ctx context.Context, &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, &i.LibraryName, &i.LibraryTypeName_2, ); err != nil { @@ -12579,7 +13593,7 @@ func (q *Queries) ListMediaItemsByLibraryIncludingArchived(ctx context.Context, } const ListMediaItemsBySHA256AndLibrary = `-- name: ListMediaItemsBySHA256AndLibrary :many -SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_sha256 = $1 AND library_id = $2 ORDER BY file_path +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts FROM media_items WHERE file_sha256 = $1 AND library_id = $2 ORDER BY file_path ` type ListMediaItemsBySHA256AndLibraryParams struct { @@ -12672,6 +13686,21 @@ func (q *Queries) ListMediaItemsBySHA256AndLibrary(ctx context.Context, arg List &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ); err != nil { return nil, err } @@ -12684,7 +13713,7 @@ func (q *Queries) ListMediaItemsBySHA256AndLibrary(ctx context.Context, arg List } const ListMediaItemsMissingHash = `-- name: ListMediaItemsMissingHash :many -SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_sha256 IS NULL ORDER BY created_at +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts FROM media_items WHERE file_sha256 IS NULL ORDER BY created_at ` // List media items that have no stored SHA-256 (imported before hashing existed) @@ -12772,6 +13801,21 @@ func (q *Queries) ListMediaItemsMissingHash(ctx context.Context) ([]MediaItems, &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ); err != nil { return nil, err } @@ -12784,7 +13828,7 @@ func (q *Queries) ListMediaItemsMissingHash(ctx context.Context) ([]MediaItems, } const ListMediaItemsSorted = `-- name: ListMediaItemsSorted :many -SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, mi.source, mi.source_feed_id, mi.episode_guid, mi.feed_position, mi.season_number, mi.episode_number, mi.enclosure_url, mi.declared_duration_seconds, mi.transcript_url, mi.chapters_url, mi.hosts, mi.guests, mi.download_state, mi.download_error, mi.download_attempts, l.name as library_name, lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id @@ -12868,82 +13912,97 @@ type ListMediaItemsSortedParams struct { } type ListMediaItemsSortedRow struct { - ID pgtype.UUID `db:"id" json:"id"` - LibraryID pgtype.UUID `db:"library_id" json:"library_id"` - Title string `db:"title" json:"title"` - Subtitle pgtype.Text `db:"subtitle" json:"subtitle"` - Author pgtype.Text `db:"author" json:"author"` - Isbn pgtype.Text `db:"isbn" json:"isbn"` - Description pgtype.Text `db:"description" json:"description"` - FilePath string `db:"file_path" json:"file_path"` - FileSize pgtype.Int8 `db:"file_size" json:"file_size"` - MimeType pgtype.Text `db:"mime_type" json:"mime_type"` - CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` - Series pgtype.Text `db:"series" json:"series"` - SeriesNumber pgtype.Float8 `db:"series_number" json:"series_number"` - Tags []string `db:"tags" json:"tags"` - Narrators []string `db:"narrators" json:"narrators"` - Authors []string `db:"authors" json:"authors"` - AuthorsSearch []string `db:"authors_search" json:"authors_search"` - Publishers []string `db:"publishers" json:"publishers"` - PublishersSearch []string `db:"publishers_search" json:"publishers_search"` - Abridged bool `db:"abridged" json:"abridged"` - Explicit bool `db:"explicit" json:"explicit"` - Asin pgtype.Text `db:"asin" json:"asin"` - DatePublished pgtype.Date `db:"date_published" json:"date_published"` - Publisher pgtype.Text `db:"publisher" json:"publisher"` - Contributors []string `db:"contributors" json:"contributors"` - Language pgtype.Text `db:"language" json:"language"` - Edition pgtype.Text `db:"edition" json:"edition"` - PageCount pgtype.Int4 `db:"page_count" json:"page_count"` - Genre pgtype.Text `db:"genre" json:"genre"` - CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"` - GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"` - OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"` - GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"` - AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"` - CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` - ImportedAt pgtype.Timestamptz `db:"imported_at" json:"imported_at"` - UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` - FormatGroup string `db:"format_group" json:"format_group"` - FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"` - IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"` - HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"` - TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"` - ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"` - EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"` - RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"` - KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"` - KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"` - MangaType pgtype.Text `db:"manga_type" json:"manga_type"` - ReadingDirection pgtype.Text `db:"reading_direction" json:"reading_direction"` - SeriesCount pgtype.Int4 `db:"series_count" json:"series_count"` - Volume pgtype.Int4 `db:"volume" json:"volume"` - Imprint pgtype.Text `db:"imprint" json:"imprint"` - AgeRating pgtype.Text `db:"age_rating" json:"age_rating"` - WebUrl pgtype.Text `db:"web_url" json:"web_url"` - StoryArc pgtype.Text `db:"story_arc" json:"story_arc"` - IsBlackAndWhite pgtype.Bool `db:"is_black_and_white" json:"is_black_and_white"` - MetadataNotes pgtype.Text `db:"metadata_notes" json:"metadata_notes"` - CommunityRating pgtype.Float8 `db:"community_rating" json:"community_rating"` - AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` - ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` - Summary pgtype.Text `db:"summary" json:"summary"` - MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` - MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` - ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` - ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` - LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` - AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` - TagsSearch []string `db:"tags_search" json:"tags_search"` - ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` - NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` - FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` - OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"` - OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"` - HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"` - LibraryName string `db:"library_name" json:"library_name"` - LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"` + ID pgtype.UUID `db:"id" json:"id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + Title string `db:"title" json:"title"` + Subtitle pgtype.Text `db:"subtitle" json:"subtitle"` + Author pgtype.Text `db:"author" json:"author"` + Isbn pgtype.Text `db:"isbn" json:"isbn"` + Description pgtype.Text `db:"description" json:"description"` + FilePath string `db:"file_path" json:"file_path"` + FileSize pgtype.Int8 `db:"file_size" json:"file_size"` + MimeType pgtype.Text `db:"mime_type" json:"mime_type"` + CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` + Series pgtype.Text `db:"series" json:"series"` + SeriesNumber pgtype.Float8 `db:"series_number" json:"series_number"` + Tags []string `db:"tags" json:"tags"` + Narrators []string `db:"narrators" json:"narrators"` + Authors []string `db:"authors" json:"authors"` + AuthorsSearch []string `db:"authors_search" json:"authors_search"` + Publishers []string `db:"publishers" json:"publishers"` + PublishersSearch []string `db:"publishers_search" json:"publishers_search"` + Abridged bool `db:"abridged" json:"abridged"` + Explicit bool `db:"explicit" json:"explicit"` + Asin pgtype.Text `db:"asin" json:"asin"` + DatePublished pgtype.Date `db:"date_published" json:"date_published"` + Publisher pgtype.Text `db:"publisher" json:"publisher"` + Contributors []string `db:"contributors" json:"contributors"` + Language pgtype.Text `db:"language" json:"language"` + Edition pgtype.Text `db:"edition" json:"edition"` + PageCount pgtype.Int4 `db:"page_count" json:"page_count"` + Genre pgtype.Text `db:"genre" json:"genre"` + CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"` + GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"` + OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"` + GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"` + AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + ImportedAt pgtype.Timestamptz `db:"imported_at" json:"imported_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + FormatGroup string `db:"format_group" json:"format_group"` + FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"` + IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"` + HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"` + TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"` + ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"` + EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"` + RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"` + KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"` + KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"` + MangaType pgtype.Text `db:"manga_type" json:"manga_type"` + ReadingDirection pgtype.Text `db:"reading_direction" json:"reading_direction"` + SeriesCount pgtype.Int4 `db:"series_count" json:"series_count"` + Volume pgtype.Int4 `db:"volume" json:"volume"` + Imprint pgtype.Text `db:"imprint" json:"imprint"` + AgeRating pgtype.Text `db:"age_rating" json:"age_rating"` + WebUrl pgtype.Text `db:"web_url" json:"web_url"` + StoryArc pgtype.Text `db:"story_arc" json:"story_arc"` + IsBlackAndWhite pgtype.Bool `db:"is_black_and_white" json:"is_black_and_white"` + MetadataNotes pgtype.Text `db:"metadata_notes" json:"metadata_notes"` + CommunityRating pgtype.Float8 `db:"community_rating" json:"community_rating"` + AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` + ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` + Summary pgtype.Text `db:"summary" json:"summary"` + MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` + MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` + ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` + ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` + LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` + AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` + TagsSearch []string `db:"tags_search" json:"tags_search"` + ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` + NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` + FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` + OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"` + OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"` + HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"` + Source string `db:"source" json:"source"` + SourceFeedID pgtype.UUID `db:"source_feed_id" json:"source_feed_id"` + EpisodeGuid pgtype.Text `db:"episode_guid" json:"episode_guid"` + FeedPosition pgtype.Int4 `db:"feed_position" json:"feed_position"` + SeasonNumber pgtype.Int4 `db:"season_number" json:"season_number"` + EpisodeNumber pgtype.Int4 `db:"episode_number" json:"episode_number"` + EnclosureUrl pgtype.Text `db:"enclosure_url" json:"enclosure_url"` + DeclaredDurationSeconds pgtype.Int4 `db:"declared_duration_seconds" json:"declared_duration_seconds"` + TranscriptUrl pgtype.Text `db:"transcript_url" json:"transcript_url"` + ChaptersUrl pgtype.Text `db:"chapters_url" json:"chapters_url"` + Hosts []string `db:"hosts" json:"hosts"` + Guests []string `db:"guests" json:"guests"` + DownloadState string `db:"download_state" json:"download_state"` + DownloadError pgtype.Text `db:"download_error" json:"download_error"` + DownloadAttempts int32 `db:"download_attempts" json:"download_attempts"` + LibraryName string `db:"library_name" json:"library_name"` + LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"` } func (q *Queries) ListMediaItemsSorted(ctx context.Context, arg ListMediaItemsSortedParams) ([]ListMediaItemsSortedRow, error) { @@ -13035,6 +14094,21 @@ func (q *Queries) ListMediaItemsSorted(ctx context.Context, arg ListMediaItemsSo &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, &i.LibraryName, &i.LibraryTypeName_2, ); err != nil { @@ -13048,6 +14122,100 @@ func (q *Queries) ListMediaItemsSorted(ctx context.Context, arg ListMediaItemsSo return items, nil } +const ListMyPodcastFeeds = `-- name: ListMyPodcastFeeds :many +SELECT f.id, f.library_id, f.feed_url, f.podcast_guid, f.title, f.description, f.host, f.publisher, f.owner_name, f.owner_email, f.language, f.tags, f.explicit, f.artwork_path, f.feed_type, f.medium, f.is_complete, f.etag, f.last_modified, f.last_fetched_at, f.last_build_date, f.created_by_admin_id, f.created_at, f.updated_at, + s.display_name, + s.auto_download, + s.auto_download_count, + s.created_at AS subscribed_at +FROM podcast_subscriptions s +JOIN podcast_feeds f ON f.id = s.feed_id +WHERE s.user_id = $1 +ORDER BY COALESCE(s.display_name, f.title) ASC +` + +type ListMyPodcastFeedsRow struct { + ID pgtype.UUID `db:"id" json:"id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + FeedUrl string `db:"feed_url" json:"feed_url"` + PodcastGuid pgtype.Text `db:"podcast_guid" json:"podcast_guid"` + Title string `db:"title" json:"title"` + Description pgtype.Text `db:"description" json:"description"` + Host pgtype.Text `db:"host" json:"host"` + Publisher pgtype.Text `db:"publisher" json:"publisher"` + OwnerName pgtype.Text `db:"owner_name" json:"owner_name"` + OwnerEmail pgtype.Text `db:"owner_email" json:"owner_email"` + Language pgtype.Text `db:"language" json:"language"` + Tags []string `db:"tags" json:"tags"` + Explicit bool `db:"explicit" json:"explicit"` + ArtworkPath pgtype.Text `db:"artwork_path" json:"artwork_path"` + FeedType pgtype.Text `db:"feed_type" json:"feed_type"` + Medium pgtype.Text `db:"medium" json:"medium"` + IsComplete bool `db:"is_complete" json:"is_complete"` + Etag pgtype.Text `db:"etag" json:"etag"` + LastModified pgtype.Text `db:"last_modified" json:"last_modified"` + LastFetchedAt pgtype.Timestamptz `db:"last_fetched_at" json:"last_fetched_at"` + LastBuildDate pgtype.Timestamptz `db:"last_build_date" json:"last_build_date"` + CreatedByAdminID pgtype.UUID `db:"created_by_admin_id" json:"created_by_admin_id"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + DisplayName pgtype.Text `db:"display_name" json:"display_name"` + AutoDownload string `db:"auto_download" json:"auto_download"` + AutoDownloadCount pgtype.Int4 `db:"auto_download_count" json:"auto_download_count"` + SubscribedAt pgtype.Timestamptz `db:"subscribed_at" json:"subscribed_at"` +} + +// The podcasts page: the acting user's subscriptions with feed metadata +// and the user-owned subscription fields. Facet values derive in Go. +func (q *Queries) ListMyPodcastFeeds(ctx context.Context, userID pgtype.UUID) ([]ListMyPodcastFeedsRow, error) { + rows, err := q.db.Query(ctx, ListMyPodcastFeeds, userID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListMyPodcastFeedsRow{} + for rows.Next() { + var i ListMyPodcastFeedsRow + if err := rows.Scan( + &i.ID, + &i.LibraryID, + &i.FeedUrl, + &i.PodcastGuid, + &i.Title, + &i.Description, + &i.Host, + &i.Publisher, + &i.OwnerName, + &i.OwnerEmail, + &i.Language, + &i.Tags, + &i.Explicit, + &i.ArtworkPath, + &i.FeedType, + &i.Medium, + &i.IsComplete, + &i.Etag, + &i.LastModified, + &i.LastFetchedAt, + &i.LastBuildDate, + &i.CreatedByAdminID, + &i.CreatedAt, + &i.UpdatedAt, + &i.DisplayName, + &i.AutoDownload, + &i.AutoDownloadCount, + &i.SubscribedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const ListPendingHashConflicts = `-- name: ListPendingHashConflicts :many SELECT hc.id, hc.library_id, hc.file_sha256, hc.created_at, l.name AS library_name, @@ -13144,6 +14312,176 @@ func (q *Queries) ListPendingSyncQueueItems(ctx context.Context, arg ListPending return items, nil } +const ListPodcastEpisodes = `-- name: ListPodcastEpisodes :many +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts FROM media_items +WHERE source_feed_id = $1 AND source = 'podcast_episode' +ORDER BY feed_position ASC NULLS LAST, date_published ASC NULLS LAST, episode_guid ASC +` + +// Feed detail / sequence source. RLS scopes to subscriber-or-admin; +// ordering: native position, then pub date, then guid (stable across +// re-fetches for historical rows whose position went stale). +func (q *Queries) ListPodcastEpisodes(ctx context.Context, sourceFeedID pgtype.UUID) ([]MediaItems, error) { + rows, err := q.db.Query(ctx, ListPodcastEpisodes, sourceFeedID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []MediaItems{} + for rows.Next() { + var i MediaItems + if err := rows.Scan( + &i.ID, + &i.LibraryID, + &i.Title, + &i.Subtitle, + &i.Author, + &i.Isbn, + &i.Description, + &i.FilePath, + &i.FileSize, + &i.MimeType, + &i.CoverImagePath, + &i.Series, + &i.SeriesNumber, + &i.Tags, + &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, + &i.Abridged, + &i.Explicit, + &i.Asin, + &i.DatePublished, + &i.Publisher, + &i.Contributors, + &i.Language, + &i.Edition, + &i.PageCount, + &i.Genre, + &i.CopyrightYear, + &i.GoodreadsID, + &i.OpenlibraryID, + &i.GoogleBooksID, + &i.AddedByAdminID, + &i.CreatedAt, + &i.ImportedAt, + &i.UpdatedAt, + &i.FormatGroup, + &i.FormatMimetype, + &i.IsReflowable, + &i.HasFixedLayout, + &i.TotalCharacters, + &i.ChapterCount, + &i.EntitlementID, + &i.RevisionNumber, + &i.KoboContentID, + &i.KoboMetadata, + &i.MangaType, + &i.ReadingDirection, + &i.SeriesCount, + &i.Volume, + &i.Imprint, + &i.AgeRating, + &i.WebUrl, + &i.StoryArc, + &i.IsBlackAndWhite, + &i.MetadataNotes, + &i.CommunityRating, + &i.AlternateInfo, + &i.ScanInformation, + &i.Summary, + &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, + &i.ChapterMetadata, + &i.LibraryTypeName, + &i.AudioMetadata, + &i.TagsSearch, + &i.ContributorsSearch, + &i.NarratorsSearch, + &i.FileSha256, + &i.OpfIdentifier, + &i.OpfUuid, + &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const ListPodcastFeedsForPolling = `-- name: ListPodcastFeedsForPolling :many +SELECT id, library_id, feed_url, podcast_guid, title, description, host, publisher, owner_name, owner_email, language, tags, explicit, artwork_path, feed_type, medium, is_complete, etag, last_modified, last_fetched_at, last_build_date, created_by_admin_id, created_at, updated_at FROM podcast_feeds ORDER BY last_fetched_at ASC NULLS FIRST +` + +// Poller worklist (admin identity): every feed with its validators and +// the back-off flag (is_complete feeds poll rarely, never disappear). +func (q *Queries) ListPodcastFeedsForPolling(ctx context.Context) ([]PodcastFeeds, error) { + rows, err := q.db.Query(ctx, ListPodcastFeedsForPolling) + if err != nil { + return nil, err + } + defer rows.Close() + items := []PodcastFeeds{} + for rows.Next() { + var i PodcastFeeds + if err := rows.Scan( + &i.ID, + &i.LibraryID, + &i.FeedUrl, + &i.PodcastGuid, + &i.Title, + &i.Description, + &i.Host, + &i.Publisher, + &i.OwnerName, + &i.OwnerEmail, + &i.Language, + &i.Tags, + &i.Explicit, + &i.ArtworkPath, + &i.FeedType, + &i.Medium, + &i.IsComplete, + &i.Etag, + &i.LastModified, + &i.LastFetchedAt, + &i.LastBuildDate, + &i.CreatedByAdminID, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const ListProcessingIssuesByLibrary = `-- name: ListProcessingIssuesByLibrary :many SELECT pi.id, @@ -13323,6 +14661,125 @@ func (q *Queries) ListSyncConflictsByUser(ctx context.Context, userID pgtype.UUI return items, nil } +const ListUndownloadedEpisodes = `-- name: ListUndownloadedEpisodes :many +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts FROM media_items +WHERE source_feed_id = $1 + AND source = 'podcast_episode' + AND download_state <> 'downloaded' +ORDER BY feed_position ASC NULLS LAST, date_published ASC NULLS LAST, episode_guid ASC +` + +// Download All + policy union worklist. +func (q *Queries) ListUndownloadedEpisodes(ctx context.Context, sourceFeedID pgtype.UUID) ([]MediaItems, error) { + rows, err := q.db.Query(ctx, ListUndownloadedEpisodes, sourceFeedID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []MediaItems{} + for rows.Next() { + var i MediaItems + if err := rows.Scan( + &i.ID, + &i.LibraryID, + &i.Title, + &i.Subtitle, + &i.Author, + &i.Isbn, + &i.Description, + &i.FilePath, + &i.FileSize, + &i.MimeType, + &i.CoverImagePath, + &i.Series, + &i.SeriesNumber, + &i.Tags, + &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, + &i.Abridged, + &i.Explicit, + &i.Asin, + &i.DatePublished, + &i.Publisher, + &i.Contributors, + &i.Language, + &i.Edition, + &i.PageCount, + &i.Genre, + &i.CopyrightYear, + &i.GoodreadsID, + &i.OpenlibraryID, + &i.GoogleBooksID, + &i.AddedByAdminID, + &i.CreatedAt, + &i.ImportedAt, + &i.UpdatedAt, + &i.FormatGroup, + &i.FormatMimetype, + &i.IsReflowable, + &i.HasFixedLayout, + &i.TotalCharacters, + &i.ChapterCount, + &i.EntitlementID, + &i.RevisionNumber, + &i.KoboContentID, + &i.KoboMetadata, + &i.MangaType, + &i.ReadingDirection, + &i.SeriesCount, + &i.Volume, + &i.Imprint, + &i.AgeRating, + &i.WebUrl, + &i.StoryArc, + &i.IsBlackAndWhite, + &i.MetadataNotes, + &i.CommunityRating, + &i.AlternateInfo, + &i.ScanInformation, + &i.Summary, + &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, + &i.ChapterMetadata, + &i.LibraryTypeName, + &i.AudioMetadata, + &i.TagsSearch, + &i.ContributorsSearch, + &i.NarratorsSearch, + &i.FileSha256, + &i.OpfIdentifier, + &i.OpfUuid, + &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const ListUnresolvedUnlinkedBooks = `-- name: ListUnresolvedUnlinkedBooks :many SELECT ub.id, ub.device_id, ub.content_id, ub.file_path, ub.title, ub.author, ub.confidence_score, ub.resolved, ub.media_item_id, ub.resolved_at, ub.resolution_method, ub.last_seen_at, ub.created_at, ub.ignored_at, d.device_name, d.device_type FROM unlinked_books ub @@ -13534,6 +14991,162 @@ func (q *Queries) MarkMediaItemMissing(ctx context.Context, id pgtype.UUID) erro return err } +const MarkPodcastEpisodeDownloaded = `-- name: MarkPodcastEpisodeDownloaded :one +UPDATE media_items +SET download_state = 'downloaded', + download_error = NULL, + file_path = $2, + file_size = $3, + mime_type = $4, + cover_image_path = COALESCE($5, cover_image_path), + audio_metadata = $6, + chapter_count = $7, + updated_at = NOW() +WHERE id = $1 AND source = 'podcast_episode' +RETURNING id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts +` + +type MarkPodcastEpisodeDownloadedParams struct { + ID pgtype.UUID `db:"id" json:"id"` + FilePath string `db:"file_path" json:"file_path"` + FileSize pgtype.Int8 `db:"file_size" json:"file_size"` + MimeType pgtype.Text `db:"mime_type" json:"mime_type"` + CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` + AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` + ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"` +} + +// SUCCESS-only write (invariant 7): the file is on disk and probed. +// ffprobe facts (real duration via audio_metadata, precise size/mime) +// reconcile the feed's declared values. +func (q *Queries) MarkPodcastEpisodeDownloaded(ctx context.Context, arg MarkPodcastEpisodeDownloadedParams) (MediaItems, error) { + row := q.db.QueryRow(ctx, MarkPodcastEpisodeDownloaded, + arg.ID, + arg.FilePath, + arg.FileSize, + arg.MimeType, + arg.CoverImagePath, + arg.AudioMetadata, + arg.ChapterCount, + ) + var i MediaItems + err := row.Scan( + &i.ID, + &i.LibraryID, + &i.Title, + &i.Subtitle, + &i.Author, + &i.Isbn, + &i.Description, + &i.FilePath, + &i.FileSize, + &i.MimeType, + &i.CoverImagePath, + &i.Series, + &i.SeriesNumber, + &i.Tags, + &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, + &i.Abridged, + &i.Explicit, + &i.Asin, + &i.DatePublished, + &i.Publisher, + &i.Contributors, + &i.Language, + &i.Edition, + &i.PageCount, + &i.Genre, + &i.CopyrightYear, + &i.GoodreadsID, + &i.OpenlibraryID, + &i.GoogleBooksID, + &i.AddedByAdminID, + &i.CreatedAt, + &i.ImportedAt, + &i.UpdatedAt, + &i.FormatGroup, + &i.FormatMimetype, + &i.IsReflowable, + &i.HasFixedLayout, + &i.TotalCharacters, + &i.ChapterCount, + &i.EntitlementID, + &i.RevisionNumber, + &i.KoboContentID, + &i.KoboMetadata, + &i.MangaType, + &i.ReadingDirection, + &i.SeriesCount, + &i.Volume, + &i.Imprint, + &i.AgeRating, + &i.WebUrl, + &i.StoryArc, + &i.IsBlackAndWhite, + &i.MetadataNotes, + &i.CommunityRating, + &i.AlternateInfo, + &i.ScanInformation, + &i.Summary, + &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, + &i.ChapterMetadata, + &i.LibraryTypeName, + &i.AudioMetadata, + &i.TagsSearch, + &i.ContributorsSearch, + &i.NarratorsSearch, + &i.FileSha256, + &i.OpfIdentifier, + &i.OpfUuid, + &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, + ) + return i, err +} + +const MarkPodcastEpisodeFailed = `-- name: MarkPodcastEpisodeFailed :exec +UPDATE media_items +SET download_state = 'failed', + download_error = $2, + download_attempts = CASE WHEN $3::boolean THEN 0 ELSE download_attempts END, + updated_at = NOW() +WHERE id = $1 AND source = 'podcast_episode' +` + +type MarkPodcastEpisodeFailedParams struct { + ID pgtype.UUID `db:"id" json:"id"` + DownloadError pgtype.Text `db:"download_error" json:"download_error"` + Column3 bool `db:"column_3" json:"column_3"` +} + +// Failure/rejection write: state + reason + whether a human already +// intervened (manual_retry resets the auto-retry budget). Existing +// metadata/file facts are never cleared. +func (q *Queries) MarkPodcastEpisodeFailed(ctx context.Context, arg MarkPodcastEpisodeFailedParams) error { + _, err := q.db.Exec(ctx, MarkPodcastEpisodeFailed, arg.ID, arg.DownloadError, arg.Column3) + return err +} + const MoveMediaItemFilePath = `-- name: MoveMediaItemFilePath :exec UPDATE media_items SET file_path = $2, file_size = $3, missing_scan_count = 0, archived_at = NULL, updated_at = NOW() @@ -14477,7 +16090,7 @@ func (q *Queries) SearchLanguageValues(ctx context.Context, arg SearchLanguageVa } const SearchMediaItems = `-- name: SearchMediaItems :many -SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name +SELECT mi.id, mi.library_id, mi.title, mi.subtitle, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.narrators, mi.authors, mi.authors_search, mi.publishers, mi.publishers_search, mi.abridged, mi.explicit, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.audio_metadata, mi.tags_search, mi.contributors_search, mi.narrators_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, mi.source, mi.source_feed_id, mi.episode_guid, mi.feed_position, mi.season_number, mi.episode_number, mi.enclosure_url, mi.declared_duration_seconds, mi.transcript_url, mi.chapters_url, mi.hosts, mi.guests, mi.download_state, mi.download_error, mi.download_attempts, l.name as library_name, lt.name as library_type_name FROM media_items mi JOIN libraries l ON mi.library_id = l.id JOIN library_types lt ON l.library_type_id = lt.id @@ -14514,82 +16127,97 @@ type SearchMediaItemsParams struct { } type SearchMediaItemsRow struct { - ID pgtype.UUID `db:"id" json:"id"` - LibraryID pgtype.UUID `db:"library_id" json:"library_id"` - Title string `db:"title" json:"title"` - Subtitle pgtype.Text `db:"subtitle" json:"subtitle"` - Author pgtype.Text `db:"author" json:"author"` - Isbn pgtype.Text `db:"isbn" json:"isbn"` - Description pgtype.Text `db:"description" json:"description"` - FilePath string `db:"file_path" json:"file_path"` - FileSize pgtype.Int8 `db:"file_size" json:"file_size"` - MimeType pgtype.Text `db:"mime_type" json:"mime_type"` - CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` - Series pgtype.Text `db:"series" json:"series"` - SeriesNumber pgtype.Float8 `db:"series_number" json:"series_number"` - Tags []string `db:"tags" json:"tags"` - Narrators []string `db:"narrators" json:"narrators"` - Authors []string `db:"authors" json:"authors"` - AuthorsSearch []string `db:"authors_search" json:"authors_search"` - Publishers []string `db:"publishers" json:"publishers"` - PublishersSearch []string `db:"publishers_search" json:"publishers_search"` - Abridged bool `db:"abridged" json:"abridged"` - Explicit bool `db:"explicit" json:"explicit"` - Asin pgtype.Text `db:"asin" json:"asin"` - DatePublished pgtype.Date `db:"date_published" json:"date_published"` - Publisher pgtype.Text `db:"publisher" json:"publisher"` - Contributors []string `db:"contributors" json:"contributors"` - Language pgtype.Text `db:"language" json:"language"` - Edition pgtype.Text `db:"edition" json:"edition"` - PageCount pgtype.Int4 `db:"page_count" json:"page_count"` - Genre pgtype.Text `db:"genre" json:"genre"` - CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"` - GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"` - OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"` - GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"` - AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"` - CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` - ImportedAt pgtype.Timestamptz `db:"imported_at" json:"imported_at"` - UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` - FormatGroup string `db:"format_group" json:"format_group"` - FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"` - IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"` - HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"` - TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"` - ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"` - EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"` - RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"` - KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"` - KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"` - MangaType pgtype.Text `db:"manga_type" json:"manga_type"` - ReadingDirection pgtype.Text `db:"reading_direction" json:"reading_direction"` - SeriesCount pgtype.Int4 `db:"series_count" json:"series_count"` - Volume pgtype.Int4 `db:"volume" json:"volume"` - Imprint pgtype.Text `db:"imprint" json:"imprint"` - AgeRating pgtype.Text `db:"age_rating" json:"age_rating"` - WebUrl pgtype.Text `db:"web_url" json:"web_url"` - StoryArc pgtype.Text `db:"story_arc" json:"story_arc"` - IsBlackAndWhite pgtype.Bool `db:"is_black_and_white" json:"is_black_and_white"` - MetadataNotes pgtype.Text `db:"metadata_notes" json:"metadata_notes"` - CommunityRating pgtype.Float8 `db:"community_rating" json:"community_rating"` - AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` - ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` - Summary pgtype.Text `db:"summary" json:"summary"` - MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` - MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` - ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` - ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` - LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` - AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` - TagsSearch []string `db:"tags_search" json:"tags_search"` - ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` - NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` - FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` - OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"` - OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"` - HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"` - LibraryName string `db:"library_name" json:"library_name"` - LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"` + ID pgtype.UUID `db:"id" json:"id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + Title string `db:"title" json:"title"` + Subtitle pgtype.Text `db:"subtitle" json:"subtitle"` + Author pgtype.Text `db:"author" json:"author"` + Isbn pgtype.Text `db:"isbn" json:"isbn"` + Description pgtype.Text `db:"description" json:"description"` + FilePath string `db:"file_path" json:"file_path"` + FileSize pgtype.Int8 `db:"file_size" json:"file_size"` + MimeType pgtype.Text `db:"mime_type" json:"mime_type"` + CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` + Series pgtype.Text `db:"series" json:"series"` + SeriesNumber pgtype.Float8 `db:"series_number" json:"series_number"` + Tags []string `db:"tags" json:"tags"` + Narrators []string `db:"narrators" json:"narrators"` + Authors []string `db:"authors" json:"authors"` + AuthorsSearch []string `db:"authors_search" json:"authors_search"` + Publishers []string `db:"publishers" json:"publishers"` + PublishersSearch []string `db:"publishers_search" json:"publishers_search"` + Abridged bool `db:"abridged" json:"abridged"` + Explicit bool `db:"explicit" json:"explicit"` + Asin pgtype.Text `db:"asin" json:"asin"` + DatePublished pgtype.Date `db:"date_published" json:"date_published"` + Publisher pgtype.Text `db:"publisher" json:"publisher"` + Contributors []string `db:"contributors" json:"contributors"` + Language pgtype.Text `db:"language" json:"language"` + Edition pgtype.Text `db:"edition" json:"edition"` + PageCount pgtype.Int4 `db:"page_count" json:"page_count"` + Genre pgtype.Text `db:"genre" json:"genre"` + CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"` + GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"` + OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"` + GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"` + AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + ImportedAt pgtype.Timestamptz `db:"imported_at" json:"imported_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + FormatGroup string `db:"format_group" json:"format_group"` + FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"` + IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"` + HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"` + TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"` + ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"` + EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"` + RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"` + KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"` + KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"` + MangaType pgtype.Text `db:"manga_type" json:"manga_type"` + ReadingDirection pgtype.Text `db:"reading_direction" json:"reading_direction"` + SeriesCount pgtype.Int4 `db:"series_count" json:"series_count"` + Volume pgtype.Int4 `db:"volume" json:"volume"` + Imprint pgtype.Text `db:"imprint" json:"imprint"` + AgeRating pgtype.Text `db:"age_rating" json:"age_rating"` + WebUrl pgtype.Text `db:"web_url" json:"web_url"` + StoryArc pgtype.Text `db:"story_arc" json:"story_arc"` + IsBlackAndWhite pgtype.Bool `db:"is_black_and_white" json:"is_black_and_white"` + MetadataNotes pgtype.Text `db:"metadata_notes" json:"metadata_notes"` + CommunityRating pgtype.Float8 `db:"community_rating" json:"community_rating"` + AlternateInfo []byte `db:"alternate_info" json:"alternate_info"` + ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` + Summary pgtype.Text `db:"summary" json:"summary"` + MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` + MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"` + ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"` + ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"` + LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` + AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` + TagsSearch []string `db:"tags_search" json:"tags_search"` + ContributorsSearch []string `db:"contributors_search" json:"contributors_search"` + NarratorsSearch []string `db:"narrators_search" json:"narrators_search"` + FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` + OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"` + OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"` + HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"` + Source string `db:"source" json:"source"` + SourceFeedID pgtype.UUID `db:"source_feed_id" json:"source_feed_id"` + EpisodeGuid pgtype.Text `db:"episode_guid" json:"episode_guid"` + FeedPosition pgtype.Int4 `db:"feed_position" json:"feed_position"` + SeasonNumber pgtype.Int4 `db:"season_number" json:"season_number"` + EpisodeNumber pgtype.Int4 `db:"episode_number" json:"episode_number"` + EnclosureUrl pgtype.Text `db:"enclosure_url" json:"enclosure_url"` + DeclaredDurationSeconds pgtype.Int4 `db:"declared_duration_seconds" json:"declared_duration_seconds"` + TranscriptUrl pgtype.Text `db:"transcript_url" json:"transcript_url"` + ChaptersUrl pgtype.Text `db:"chapters_url" json:"chapters_url"` + Hosts []string `db:"hosts" json:"hosts"` + Guests []string `db:"guests" json:"guests"` + DownloadState string `db:"download_state" json:"download_state"` + DownloadError pgtype.Text `db:"download_error" json:"download_error"` + DownloadAttempts int32 `db:"download_attempts" json:"download_attempts"` + LibraryName string `db:"library_name" json:"library_name"` + LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"` } // Search Media Items queries @@ -14683,6 +16311,21 @@ func (q *Queries) SearchMediaItems(ctx context.Context, arg SearchMediaItemsPara &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, &i.LibraryName, &i.LibraryTypeName_2, ); err != nil { @@ -15137,6 +16780,231 @@ func (q *Queries) SearchMediaItemsUnified(ctx context.Context, arg SearchMediaIt return items, nil } +const SearchMyPodcastEpisodes = `-- name: SearchMyPodcastEpisodes :many +SELECT id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts FROM media_items +WHERE source = 'podcast_episode' + AND ($1 = '' OR title ILIKE '%' || $1 || '%' OR description ILIKE '%' || $1 || '%') +ORDER BY feed_position ASC NULLS LAST, date_published ASC NULLS LAST +LIMIT 50 +` + +// Subscription-scoped episode search (D14): title + description cover +// un-downloaded AND historical rows (fetcher persists metadata-only). +// RLS already restricts to the caller's subscriptions — the explicit +// source filter keeps scan items out of podcast search. +func (q *Queries) SearchMyPodcastEpisodes(ctx context.Context, dollar_1 interface{}) ([]MediaItems, error) { + rows, err := q.db.Query(ctx, SearchMyPodcastEpisodes, dollar_1) + if err != nil { + return nil, err + } + defer rows.Close() + items := []MediaItems{} + for rows.Next() { + var i MediaItems + if err := rows.Scan( + &i.ID, + &i.LibraryID, + &i.Title, + &i.Subtitle, + &i.Author, + &i.Isbn, + &i.Description, + &i.FilePath, + &i.FileSize, + &i.MimeType, + &i.CoverImagePath, + &i.Series, + &i.SeriesNumber, + &i.Tags, + &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, + &i.Abridged, + &i.Explicit, + &i.Asin, + &i.DatePublished, + &i.Publisher, + &i.Contributors, + &i.Language, + &i.Edition, + &i.PageCount, + &i.Genre, + &i.CopyrightYear, + &i.GoodreadsID, + &i.OpenlibraryID, + &i.GoogleBooksID, + &i.AddedByAdminID, + &i.CreatedAt, + &i.ImportedAt, + &i.UpdatedAt, + &i.FormatGroup, + &i.FormatMimetype, + &i.IsReflowable, + &i.HasFixedLayout, + &i.TotalCharacters, + &i.ChapterCount, + &i.EntitlementID, + &i.RevisionNumber, + &i.KoboContentID, + &i.KoboMetadata, + &i.MangaType, + &i.ReadingDirection, + &i.SeriesCount, + &i.Volume, + &i.Imprint, + &i.AgeRating, + &i.WebUrl, + &i.StoryArc, + &i.IsBlackAndWhite, + &i.MetadataNotes, + &i.CommunityRating, + &i.AlternateInfo, + &i.ScanInformation, + &i.Summary, + &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, + &i.ChapterMetadata, + &i.LibraryTypeName, + &i.AudioMetadata, + &i.TagsSearch, + &i.ContributorsSearch, + &i.NarratorsSearch, + &i.FileSha256, + &i.OpfIdentifier, + &i.OpfUuid, + &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const SearchMyPodcastFeeds = `-- name: SearchMyPodcastFeeds :many +SELECT f.id, f.library_id, f.feed_url, f.podcast_guid, f.title, f.description, f.host, f.publisher, f.owner_name, f.owner_email, f.language, f.tags, f.explicit, f.artwork_path, f.feed_type, f.medium, f.is_complete, f.etag, f.last_modified, f.last_fetched_at, f.last_build_date, f.created_by_admin_id, f.created_at, f.updated_at, + s.display_name, + s.auto_download, + s.auto_download_count, + s.created_at AS subscribed_at +FROM podcast_subscriptions s +JOIN podcast_feeds f ON f.id = s.feed_id +WHERE s.user_id = $1 + AND ($2 = '' OR f.title ILIKE '%' || $2 || '%' + OR COALESCE(s.display_name, '') ILIKE '%' || $2 || '%' + OR COALESCE(f.host, '') ILIKE '%' || $2 || '%' + OR COALESCE(f.publisher, '') ILIKE '%' || $2 || '%') +ORDER BY COALESCE(s.display_name, f.title) ASC +` + +type SearchMyPodcastFeedsParams struct { + UserID pgtype.UUID `db:"user_id" json:"user_id"` + Column2 interface{} `db:"column_2" json:"column_2"` +} + +type SearchMyPodcastFeedsRow struct { + ID pgtype.UUID `db:"id" json:"id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + FeedUrl string `db:"feed_url" json:"feed_url"` + PodcastGuid pgtype.Text `db:"podcast_guid" json:"podcast_guid"` + Title string `db:"title" json:"title"` + Description pgtype.Text `db:"description" json:"description"` + Host pgtype.Text `db:"host" json:"host"` + Publisher pgtype.Text `db:"publisher" json:"publisher"` + OwnerName pgtype.Text `db:"owner_name" json:"owner_name"` + OwnerEmail pgtype.Text `db:"owner_email" json:"owner_email"` + Language pgtype.Text `db:"language" json:"language"` + Tags []string `db:"tags" json:"tags"` + Explicit bool `db:"explicit" json:"explicit"` + ArtworkPath pgtype.Text `db:"artwork_path" json:"artwork_path"` + FeedType pgtype.Text `db:"feed_type" json:"feed_type"` + Medium pgtype.Text `db:"medium" json:"medium"` + IsComplete bool `db:"is_complete" json:"is_complete"` + Etag pgtype.Text `db:"etag" json:"etag"` + LastModified pgtype.Text `db:"last_modified" json:"last_modified"` + LastFetchedAt pgtype.Timestamptz `db:"last_fetched_at" json:"last_fetched_at"` + LastBuildDate pgtype.Timestamptz `db:"last_build_date" json:"last_build_date"` + CreatedByAdminID pgtype.UUID `db:"created_by_admin_id" json:"created_by_admin_id"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + DisplayName pgtype.Text `db:"display_name" json:"display_name"` + AutoDownload string `db:"auto_download" json:"auto_download"` + AutoDownloadCount pgtype.Int4 `db:"auto_download_count" json:"auto_download_count"` + SubscribedAt pgtype.Timestamptz `db:"subscribed_at" json:"subscribed_at"` +} + +// Feed-title AND subscription-display-name search (D14) + the facet +// fields the podcasts page filters on. +func (q *Queries) SearchMyPodcastFeeds(ctx context.Context, arg SearchMyPodcastFeedsParams) ([]SearchMyPodcastFeedsRow, error) { + rows, err := q.db.Query(ctx, SearchMyPodcastFeeds, arg.UserID, arg.Column2) + if err != nil { + return nil, err + } + defer rows.Close() + items := []SearchMyPodcastFeedsRow{} + for rows.Next() { + var i SearchMyPodcastFeedsRow + if err := rows.Scan( + &i.ID, + &i.LibraryID, + &i.FeedUrl, + &i.PodcastGuid, + &i.Title, + &i.Description, + &i.Host, + &i.Publisher, + &i.OwnerName, + &i.OwnerEmail, + &i.Language, + &i.Tags, + &i.Explicit, + &i.ArtworkPath, + &i.FeedType, + &i.Medium, + &i.IsComplete, + &i.Etag, + &i.LastModified, + &i.LastFetchedAt, + &i.LastBuildDate, + &i.CreatedByAdminID, + &i.CreatedAt, + &i.UpdatedAt, + &i.DisplayName, + &i.AutoDownload, + &i.AutoDownloadCount, + &i.SubscribedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const SearchNarratorValues = `-- name: SearchNarratorValues :many SELECT narrator::TEXT as value, @@ -15380,6 +17248,54 @@ func (q *Queries) SetLibraryVisibility(ctx context.Context, arg SetLibraryVisibi return i, err } +const SetPodcastEpisodeDownloadQueued = `-- name: SetPodcastEpisodeDownloadQueued :execrows +UPDATE media_items +SET download_state = 'queued', + download_error = NULL, + download_attempts = download_attempts + 1, + updated_at = NOW() +WHERE id = $1 + AND source = 'podcast_episode' + AND download_state IN ('pending', 'failed') +` + +// Worker claim: pending|failed -> queued. Attempts incremented at +// claim time so a crashed worker still counts the try. +func (q *Queries) SetPodcastEpisodeDownloadQueued(ctx context.Context, id pgtype.UUID) (int64, error) { + result, err := q.db.Exec(ctx, SetPodcastEpisodeDownloadQueued, id) + if err != nil { + return 0, err + } + return result.RowsAffected(), nil +} + +const SetPodcastEpisodeDownloading = `-- name: SetPodcastEpisodeDownloading :exec +UPDATE media_items +SET download_state = 'downloading', updated_at = NOW() +WHERE id = $1 AND source = 'podcast_episode' +` + +func (q *Queries) SetPodcastEpisodeDownloading(ctx context.Context, id pgtype.UUID) error { + _, err := q.db.Exec(ctx, SetPodcastEpisodeDownloading, id) + return err +} + +const SetPodcastEpisodePending = `-- name: SetPodcastEpisodePending :exec +UPDATE media_items +SET download_state = 'pending', + download_error = NULL, + download_attempts = 0, + updated_at = NOW() +WHERE id = $1 AND source = 'podcast_episode' +` + +// Manual retry (same endpoint as download): reset to pending so the +// worker re-claims it; the reason text clears with the fresh attempt. +func (q *Queries) SetPodcastEpisodePending(ctx context.Context, id pgtype.UUID) error { + _, err := q.db.Exec(ctx, SetPodcastEpisodePending, id) + return err +} + const SetSystemConfig = `-- name: SetSystemConfig :one INSERT INTO system_config (key, value, updated_by) VALUES ($1, $2, $3) @@ -15410,6 +17326,33 @@ func (q *Queries) SetSystemConfig(ctx context.Context, arg SetSystemConfigParams return i, err } +const SubscribePodcastFeed = `-- name: SubscribePodcastFeed :one +INSERT INTO podcast_subscriptions (user_id, feed_id) +VALUES ($1, $2) +ON CONFLICT (user_id, feed_id) DO NOTHING +RETURNING id, user_id, feed_id, display_name, auto_download, auto_download_count, created_at +` + +type SubscribePodcastFeedParams struct { + UserID pgtype.UUID `db:"user_id" json:"user_id"` + FeedID pgtype.UUID `db:"feed_id" json:"feed_id"` +} + +func (q *Queries) SubscribePodcastFeed(ctx context.Context, arg SubscribePodcastFeedParams) (PodcastSubscriptions, error) { + row := q.db.QueryRow(ctx, SubscribePodcastFeed, arg.UserID, arg.FeedID) + var i PodcastSubscriptions + err := row.Scan( + &i.ID, + &i.UserID, + &i.FeedID, + &i.DisplayName, + &i.AutoDownload, + &i.AutoDownloadCount, + &i.CreatedAt, + ) + return i, err +} + const SyncLibraryTypeExtensions = `-- name: SyncLibraryTypeExtensions :exec UPDATE library_types SET allowed_extensions = $2 WHERE name = $1 ` @@ -16093,7 +18036,7 @@ UPDATE libraries SET description = $3, updated_at = NOW() WHERE id = $1 -RETURNING id, name, description, library_type_id, created_by_admin_id, created_at, updated_at +RETURNING id, name, description, library_type_id, created_by_admin_id, created_at, updated_at, allow_member_feeds ` type UpdateLibraryParams struct { @@ -16113,6 +18056,7 @@ func (q *Queries) UpdateLibrary(ctx context.Context, arg UpdateLibraryParams) (L &i.CreatedByAdminID, &i.CreatedAt, &i.UpdatedAt, + &i.AllowMemberFeeds, ) return i, err } @@ -16338,7 +18282,7 @@ UPDATE media_items SET explicit = $46, updated_at = NOW() WHERE id = $1 -RETURNING id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence +RETURNING id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts ` type UpdateMediaItemParams struct { @@ -16515,6 +18459,21 @@ func (q *Queries) UpdateMediaItem(ctx context.Context, arg UpdateMediaItemParams &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ) return i, err } @@ -16699,7 +18658,7 @@ const UpdateMediaItemChapterMetadata = `-- name: UpdateMediaItemChapterMetadata UPDATE media_items SET chapter_metadata = $2, updated_at = NOW() WHERE id = $1 -RETURNING id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence +RETURNING id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts ` type UpdateMediaItemChapterMetadataParams struct { @@ -16785,6 +18744,21 @@ func (q *Queries) UpdateMediaItemChapterMetadata(ctx context.Context, arg Update &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ) return i, err } @@ -16884,7 +18858,7 @@ SET hash_confidence = $5, updated_at = NOW() WHERE id = $1 -RETURNING id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence +RETURNING id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts ` type UpdateMediaItemIdentifiersParams struct { @@ -16984,6 +18958,21 @@ func (q *Queries) UpdateMediaItemIdentifiers(ctx context.Context, arg UpdateMedi &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ) return i, err } @@ -16996,7 +18985,7 @@ SET entitlement_id = $2, kobo_metadata = $5, updated_at = NOW() WHERE id = $1 -RETURNING id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence +RETURNING id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts ` type UpdateMediaItemKoboMetadataParams struct { @@ -17091,6 +19080,21 @@ func (q *Queries) UpdateMediaItemKoboMetadata(ctx context.Context, arg UpdateMed &i.OpfIdentifier, &i.OpfUuid, &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, ) return i, err } @@ -17214,6 +19218,223 @@ func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) return err } +const UpdatePodcastFeedArtwork = `-- name: UpdatePodcastFeedArtwork :exec +UPDATE podcast_feeds SET artwork_path = $2, updated_at = NOW() WHERE id = $1 +` + +type UpdatePodcastFeedArtworkParams struct { + ID pgtype.UUID `db:"id" json:"id"` + ArtworkPath pgtype.Text `db:"artwork_path" json:"artwork_path"` +} + +func (q *Queries) UpdatePodcastFeedArtwork(ctx context.Context, arg UpdatePodcastFeedArtworkParams) error { + _, err := q.db.Exec(ctx, UpdatePodcastFeedArtwork, arg.ID, arg.ArtworkPath) + return err +} + +const UpdatePodcastFeedFetch = `-- name: UpdatePodcastFeedFetch :one +UPDATE podcast_feeds +SET etag = $2, + last_modified = $3, + last_fetched_at = NOW(), + last_build_date = $4, + updated_at = NOW() +WHERE id = $1 +RETURNING id, library_id, feed_url, podcast_guid, title, description, host, publisher, owner_name, owner_email, language, tags, explicit, artwork_path, feed_type, medium, is_complete, etag, last_modified, last_fetched_at, last_build_date, created_by_admin_id, created_at, updated_at +` + +type UpdatePodcastFeedFetchParams struct { + ID pgtype.UUID `db:"id" json:"id"` + Etag pgtype.Text `db:"etag" json:"etag"` + LastModified pgtype.Text `db:"last_modified" json:"last_modified"` + LastBuildDate pgtype.Timestamptz `db:"last_build_date" json:"last_build_date"` +} + +// Post-fetch bookkeeping: validators for conditional GETs, the fetch +// timestamp, and the feed's own last-build marker. +func (q *Queries) UpdatePodcastFeedFetch(ctx context.Context, arg UpdatePodcastFeedFetchParams) (PodcastFeeds, error) { + row := q.db.QueryRow(ctx, UpdatePodcastFeedFetch, + arg.ID, + arg.Etag, + arg.LastModified, + arg.LastBuildDate, + ) + var i PodcastFeeds + err := row.Scan( + &i.ID, + &i.LibraryID, + &i.FeedUrl, + &i.PodcastGuid, + &i.Title, + &i.Description, + &i.Host, + &i.Publisher, + &i.OwnerName, + &i.OwnerEmail, + &i.Language, + &i.Tags, + &i.Explicit, + &i.ArtworkPath, + &i.FeedType, + &i.Medium, + &i.IsComplete, + &i.Etag, + &i.LastModified, + &i.LastFetchedAt, + &i.LastBuildDate, + &i.CreatedByAdminID, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const UpdatePodcastFeedMetadata = `-- name: UpdatePodcastFeedMetadata :one +UPDATE podcast_feeds +SET title = $2, + description = $3, + host = $4, + publisher = $5, + owner_name = $6, + owner_email = $7, + language = $8, + tags = $9, + explicit = $10, + feed_type = $11, + medium = $12, + is_complete = $13, + podcast_guid = COALESCE($14, podcast_guid), + updated_at = NOW() +WHERE id = $1 +RETURNING id, library_id, feed_url, podcast_guid, title, description, host, publisher, owner_name, owner_email, language, tags, explicit, artwork_path, feed_type, medium, is_complete, etag, last_modified, last_fetched_at, last_build_date, created_by_admin_id, created_at, updated_at +` + +type UpdatePodcastFeedMetadataParams struct { + ID pgtype.UUID `db:"id" json:"id"` + Title string `db:"title" json:"title"` + Description pgtype.Text `db:"description" json:"description"` + Host pgtype.Text `db:"host" json:"host"` + Publisher pgtype.Text `db:"publisher" json:"publisher"` + OwnerName pgtype.Text `db:"owner_name" json:"owner_name"` + OwnerEmail pgtype.Text `db:"owner_email" json:"owner_email"` + Language pgtype.Text `db:"language" json:"language"` + Tags []string `db:"tags" json:"tags"` + Explicit bool `db:"explicit" json:"explicit"` + FeedType pgtype.Text `db:"feed_type" json:"feed_type"` + Medium pgtype.Text `db:"medium" json:"medium"` + IsComplete bool `db:"is_complete" json:"is_complete"` + PodcastGuid pgtype.Text `db:"podcast_guid" json:"podcast_guid"` +} + +// Fetcher-only full metadata refresh (parse result). podcast_guid is +// set once a feed declares it; new_feed_url handling is a dedicated +// query below. +func (q *Queries) UpdatePodcastFeedMetadata(ctx context.Context, arg UpdatePodcastFeedMetadataParams) (PodcastFeeds, error) { + row := q.db.QueryRow(ctx, UpdatePodcastFeedMetadata, + arg.ID, + arg.Title, + arg.Description, + arg.Host, + arg.Publisher, + arg.OwnerName, + arg.OwnerEmail, + arg.Language, + arg.Tags, + arg.Explicit, + arg.FeedType, + arg.Medium, + arg.IsComplete, + arg.PodcastGuid, + ) + var i PodcastFeeds + err := row.Scan( + &i.ID, + &i.LibraryID, + &i.FeedUrl, + &i.PodcastGuid, + &i.Title, + &i.Description, + &i.Host, + &i.Publisher, + &i.OwnerName, + &i.OwnerEmail, + &i.Language, + &i.Tags, + &i.Explicit, + &i.ArtworkPath, + &i.FeedType, + &i.Medium, + &i.IsComplete, + &i.Etag, + &i.LastModified, + &i.LastFetchedAt, + &i.LastBuildDate, + &i.CreatedByAdminID, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const UpdatePodcastFeedRedirect = `-- name: UpdatePodcastFeedRedirect :exec +UPDATE podcast_feeds +SET feed_url = $2, + updated_at = NOW() +WHERE id = $1 +` + +type UpdatePodcastFeedRedirectParams struct { + ID pgtype.UUID `db:"id" json:"id"` + FeedUrl string `db:"feed_url" json:"feed_url"` +} + +// itunes:new-feed-url (or a permanent HTTP redirect chain): the feed has +// MOVED. Rewrite feed_url so future polls hit the new location; the row +// (and every subscription/episode) stays put. +func (q *Queries) UpdatePodcastFeedRedirect(ctx context.Context, arg UpdatePodcastFeedRedirectParams) error { + _, err := q.db.Exec(ctx, UpdatePodcastFeedRedirect, arg.ID, arg.FeedUrl) + return err +} + +const UpdatePodcastSubscription = `-- name: UpdatePodcastSubscription :one +UPDATE podcast_subscriptions +SET display_name = $3, + auto_download = $4, + auto_download_count = $5 +WHERE user_id = $1 AND feed_id = $2 +RETURNING id, user_id, feed_id, display_name, auto_download, auto_download_count, created_at +` + +type UpdatePodcastSubscriptionParams struct { + UserID pgtype.UUID `db:"user_id" json:"user_id"` + FeedID pgtype.UUID `db:"feed_id" json:"feed_id"` + DisplayName pgtype.Text `db:"display_name" json:"display_name"` + AutoDownload string `db:"auto_download" json:"auto_download"` + AutoDownloadCount pgtype.Int4 `db:"auto_download_count" json:"auto_download_count"` +} + +// User-owned fields only (D9): display name + auto-download policy. +func (q *Queries) UpdatePodcastSubscription(ctx context.Context, arg UpdatePodcastSubscriptionParams) (PodcastSubscriptions, error) { + row := q.db.QueryRow(ctx, UpdatePodcastSubscription, + arg.UserID, + arg.FeedID, + arg.DisplayName, + arg.AutoDownload, + arg.AutoDownloadCount, + ) + var i PodcastSubscriptions + err := row.Scan( + &i.ID, + &i.UserID, + &i.FeedID, + &i.DisplayName, + &i.AutoDownload, + &i.AutoDownloadCount, + &i.CreatedAt, + ) + return i, err +} + const UpdateReadingProgress = `-- name: UpdateReadingProgress :one INSERT INTO reading_progress (media_item_id, user_id, current_page, total_pages, last_read_at) VALUES ($1, $2, $3, $4, NOW()) @@ -17867,6 +20088,218 @@ func (q *Queries) UpsertPanelData(ctx context.Context, arg UpsertPanelDataParams return i, err } +const UpsertPodcastEpisode = `-- name: UpsertPodcastEpisode :one + +INSERT INTO media_items ( + library_id, title, subtitle, description, authors, authors_search, + file_path, file_size, mime_type, cover_image_path, explicit, + date_published, language, tags, genre, + format_group, format_mimetype, is_reflowable, library_type_name, + source, source_feed_id, episode_guid, feed_position, + season_number, episode_number, enclosure_url, + declared_duration_seconds, transcript_url, chapters_url, + hosts, guests +) VALUES ( + $1, $2, $3, $4, $5, $6, + $7, $8, $9, $10, $11, + $12, $13, $14, $15, + 'audiobook', $9, FALSE, 'podcasts', + 'podcast_episode', $16, $17, $18, + $19, $20, $21, + $22, $23, $24, + $25, $26 +) +ON CONFLICT (source_feed_id, episode_guid) WHERE source_feed_id IS NOT NULL DO UPDATE SET + title = EXCLUDED.title, + subtitle = EXCLUDED.subtitle, + description = EXCLUDED.description, + authors = EXCLUDED.authors, + authors_search = EXCLUDED.authors_search, + explicit = EXCLUDED.explicit, + date_published = EXCLUDED.date_published, + language = EXCLUDED.language, + tags = EXCLUDED.tags, + genre = EXCLUDED.genre, + season_number = EXCLUDED.season_number, + episode_number = EXCLUDED.episode_number, + enclosure_url = EXCLUDED.enclosure_url, + declared_duration_seconds = EXCLUDED.declared_duration_seconds, + transcript_url = EXCLUDED.transcript_url, + chapters_url = EXCLUDED.chapters_url, + hosts = EXCLUDED.hosts, + guests = EXCLUDED.guests, + feed_position = EXCLUDED.feed_position, + cover_image_path = CASE WHEN media_items.download_state = 'downloaded' THEN media_items.cover_image_path ELSE EXCLUDED.cover_image_path END, + file_path = CASE WHEN media_items.download_state = 'downloaded' THEN media_items.file_path ELSE EXCLUDED.file_path END, + file_size = CASE WHEN media_items.download_state = 'downloaded' THEN media_items.file_size ELSE EXCLUDED.file_size END, + mime_type = CASE WHEN media_items.download_state = 'downloaded' THEN media_items.mime_type ELSE EXCLUDED.mime_type END, + updated_at = NOW() +RETURNING id, library_id, title, subtitle, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, authors, authors_search, publishers, publishers_search, abridged, explicit, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, source, source_feed_id, episode_guid, feed_position, season_number, episode_number, enclosure_url, declared_duration_seconds, transcript_url, chapters_url, hosts, guests, download_state, download_error, download_attempts +` + +type UpsertPodcastEpisodeParams struct { + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + Title string `db:"title" json:"title"` + Subtitle pgtype.Text `db:"subtitle" json:"subtitle"` + Description pgtype.Text `db:"description" json:"description"` + Authors []string `db:"authors" json:"authors"` + AuthorsSearch []string `db:"authors_search" json:"authors_search"` + FilePath string `db:"file_path" json:"file_path"` + FileSize pgtype.Int8 `db:"file_size" json:"file_size"` + MimeType pgtype.Text `db:"mime_type" json:"mime_type"` + CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"` + Explicit bool `db:"explicit" json:"explicit"` + DatePublished pgtype.Date `db:"date_published" json:"date_published"` + Language pgtype.Text `db:"language" json:"language"` + Tags []string `db:"tags" json:"tags"` + Genre pgtype.Text `db:"genre" json:"genre"` + SourceFeedID pgtype.UUID `db:"source_feed_id" json:"source_feed_id"` + EpisodeGuid pgtype.Text `db:"episode_guid" json:"episode_guid"` + FeedPosition pgtype.Int4 `db:"feed_position" json:"feed_position"` + SeasonNumber pgtype.Int4 `db:"season_number" json:"season_number"` + EpisodeNumber pgtype.Int4 `db:"episode_number" json:"episode_number"` + EnclosureUrl pgtype.Text `db:"enclosure_url" json:"enclosure_url"` + DeclaredDurationSeconds pgtype.Int4 `db:"declared_duration_seconds" json:"declared_duration_seconds"` + TranscriptUrl pgtype.Text `db:"transcript_url" json:"transcript_url"` + ChaptersUrl pgtype.Text `db:"chapters_url" json:"chapters_url"` + Hosts []string `db:"hosts" json:"hosts"` + Guests []string `db:"guests" json:"guests"` +} + +// ============================================================================ +// Podcast episodes — media_items rows with source='podcast_episode' +// ============================================================================ +// Fetcher-only upsert keyed on the partial unique index +// (source_feed_id, episode_guid). DO UPDATE refreshes FETCH-OWNED +// metadata only. Write-after-success guard (AGENTS.md gotcha #7): +// download state/error/attempts are NEVER touched here, and the +// downloaded file's facts (path/size/mime/cover) survive renumbering — +// only un-downloaded rows follow the re-derived file_path. +// authors_search mirrors authors (trigram search column). +func (q *Queries) UpsertPodcastEpisode(ctx context.Context, arg UpsertPodcastEpisodeParams) (MediaItems, error) { + row := q.db.QueryRow(ctx, UpsertPodcastEpisode, + arg.LibraryID, + arg.Title, + arg.Subtitle, + arg.Description, + arg.Authors, + arg.AuthorsSearch, + arg.FilePath, + arg.FileSize, + arg.MimeType, + arg.CoverImagePath, + arg.Explicit, + arg.DatePublished, + arg.Language, + arg.Tags, + arg.Genre, + arg.SourceFeedID, + arg.EpisodeGuid, + arg.FeedPosition, + arg.SeasonNumber, + arg.EpisodeNumber, + arg.EnclosureUrl, + arg.DeclaredDurationSeconds, + arg.TranscriptUrl, + arg.ChaptersUrl, + arg.Hosts, + arg.Guests, + ) + var i MediaItems + err := row.Scan( + &i.ID, + &i.LibraryID, + &i.Title, + &i.Subtitle, + &i.Author, + &i.Isbn, + &i.Description, + &i.FilePath, + &i.FileSize, + &i.MimeType, + &i.CoverImagePath, + &i.Series, + &i.SeriesNumber, + &i.Tags, + &i.Narrators, + &i.Authors, + &i.AuthorsSearch, + &i.Publishers, + &i.PublishersSearch, + &i.Abridged, + &i.Explicit, + &i.Asin, + &i.DatePublished, + &i.Publisher, + &i.Contributors, + &i.Language, + &i.Edition, + &i.PageCount, + &i.Genre, + &i.CopyrightYear, + &i.GoodreadsID, + &i.OpenlibraryID, + &i.GoogleBooksID, + &i.AddedByAdminID, + &i.CreatedAt, + &i.ImportedAt, + &i.UpdatedAt, + &i.FormatGroup, + &i.FormatMimetype, + &i.IsReflowable, + &i.HasFixedLayout, + &i.TotalCharacters, + &i.ChapterCount, + &i.EntitlementID, + &i.RevisionNumber, + &i.KoboContentID, + &i.KoboMetadata, + &i.MangaType, + &i.ReadingDirection, + &i.SeriesCount, + &i.Volume, + &i.Imprint, + &i.AgeRating, + &i.WebUrl, + &i.StoryArc, + &i.IsBlackAndWhite, + &i.MetadataNotes, + &i.CommunityRating, + &i.AlternateInfo, + &i.ScanInformation, + &i.Summary, + &i.MetadataOverrides, + &i.MissingScanCount, + &i.ArchivedAt, + &i.ChapterMetadata, + &i.LibraryTypeName, + &i.AudioMetadata, + &i.TagsSearch, + &i.ContributorsSearch, + &i.NarratorsSearch, + &i.FileSha256, + &i.OpfIdentifier, + &i.OpfUuid, + &i.HashConfidence, + &i.Source, + &i.SourceFeedID, + &i.EpisodeGuid, + &i.FeedPosition, + &i.SeasonNumber, + &i.EpisodeNumber, + &i.EnclosureUrl, + &i.DeclaredDurationSeconds, + &i.TranscriptUrl, + &i.ChaptersUrl, + &i.Hosts, + &i.Guests, + &i.DownloadState, + &i.DownloadError, + &i.DownloadAttempts, + ) + return i, err +} + const UpsertReaderSettings = `-- name: UpsertReaderSettings :one INSERT INTO reader_settings (user_id, setting_key, setting_value) VALUES ($1, 'reader_settings', $2) diff --git a/internal/database/queries/queries.sql b/internal/database/queries/queries.sql index 19baf96e..1f7e2907 100644 --- a/internal/database/queries/queries.sql +++ b/internal/database/queries/queries.sql @@ -4590,3 +4590,338 @@ SET file_size = $2, updated_at = NOW() WHERE id = $1 RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, narrators, abridged, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, imported_at, updated_at, format_group, format_mimetype, is_reflowable, has_fixed_layout, total_characters, chapter_count, entitlement_id, revision_number, kobo_content_id, kobo_metadata, manga_type, reading_direction, series_count, volume, imprint, age_rating, web_url, story_arc, is_black_and_white, metadata_notes, community_rating, alternate_info, scan_information, summary, metadata_overrides, missing_scan_count, archived_at, chapter_metadata, library_type_name, audio_metadata, tags_search, contributors_search, narrators_search, file_sha256, opf_identifier, opf_uuid, hash_confidence; + +-- ============================================================================ +-- PODCASTS / RSS FEEDS (plan ~/.opencode/plan/podcast-integration.md §6/§8) +-- Visibility is RLS-enforced (schema policy block) — NO query here carries +-- visibility clauses. Feed reads run under the acting user's identity +-- (subscriber-or-admin policy); the fetcher/worker run as first-admin. +-- The fetcher/download worker is the SOLE writer of feed/episode metadata; +-- users write only podcast_subscriptions rows. +-- ============================================================================ + +-- name: CreatePodcastFeed :one +-- Add-feed upsert keyed on the per-library UNIQUE(library_id, feed_url): +-- a second subscriber adding the same URL attaches to the existing row +-- instead of erroring. DO UPDATE (not DO NOTHING) so the RETURNING works +-- even when the conflicting row is invisible to the actor under RLS +-- (non-admin creators cannot SELECT other users' feeds). Metadata columns +-- are filled by the fetcher right after; this only establishes identity. +INSERT INTO podcast_feeds (library_id, feed_url, title, created_by_admin_id) +VALUES ($1, $2, $3, $4) +ON CONFLICT (library_id, feed_url) DO UPDATE SET updated_at = NOW() +RETURNING *; + +-- name: GetPodcastFeed :one +SELECT * FROM podcast_feeds WHERE id = $1; + +-- name: ListMyPodcastFeeds :many +-- The podcasts page: the acting user's subscriptions with feed metadata +-- and the user-owned subscription fields. Facet values derive in Go. +SELECT f.*, + s.display_name, + s.auto_download, + s.auto_download_count, + s.created_at AS subscribed_at +FROM podcast_subscriptions s +JOIN podcast_feeds f ON f.id = s.feed_id +WHERE s.user_id = $1 +ORDER BY COALESCE(s.display_name, f.title) ASC; + +-- name: GetPodcastSubscription :one +SELECT * FROM podcast_subscriptions WHERE user_id = $1 AND feed_id = $2; + +-- name: SubscribePodcastFeed :one +INSERT INTO podcast_subscriptions (user_id, feed_id) +VALUES ($1, $2) +ON CONFLICT (user_id, feed_id) DO NOTHING +RETURNING *; + +-- name: UpdatePodcastSubscription :one +-- User-owned fields only (D9): display name + auto-download policy. +UPDATE podcast_subscriptions +SET display_name = $3, + auto_download = $4, + auto_download_count = $5 +WHERE user_id = $1 AND feed_id = $2 +RETURNING *; + +-- name: DeletePodcastSubscription :execrows +-- Unsubscribe NEVER deletes feed/episode/file state (D6). +DELETE FROM podcast_subscriptions WHERE user_id = $1 AND feed_id = $2; + +-- name: ListFeedSubscribers :many +-- Download-worker policy union (D6): most aggressive policy wins for +-- what arrives. Runs under an admin identity (background job). +SELECT user_id, auto_download, auto_download_count +FROM podcast_subscriptions +WHERE feed_id = $1; + +-- name: CountPodcastFeedSubscribers :one +SELECT COUNT(*) FROM podcast_subscriptions WHERE feed_id = $1; + +-- name: ListPodcastFeedsForPolling :many +-- Poller worklist (admin identity): every feed with its validators and +-- the back-off flag (is_complete feeds poll rarely, never disappear). +SELECT * FROM podcast_feeds ORDER BY last_fetched_at ASC NULLS FIRST; + +-- name: UpdatePodcastFeedFetch :one +-- Post-fetch bookkeeping: validators for conditional GETs, the fetch +-- timestamp, and the feed's own last-build marker. +UPDATE podcast_feeds +SET etag = $2, + last_modified = $3, + last_fetched_at = NOW(), + last_build_date = $4, + updated_at = NOW() +WHERE id = $1 +RETURNING *; + +-- name: UpdatePodcastFeedMetadata :one +-- Fetcher-only full metadata refresh (parse result). podcast_guid is +-- set once a feed declares it; new_feed_url handling is a dedicated +-- query below. +UPDATE podcast_feeds +SET title = $2, + description = $3, + host = $4, + publisher = $5, + owner_name = $6, + owner_email = $7, + language = $8, + tags = $9, + explicit = $10, + feed_type = $11, + medium = $12, + is_complete = $13, + podcast_guid = COALESCE($14, podcast_guid), + updated_at = NOW() +WHERE id = $1 +RETURNING *; + +-- name: UpdatePodcastFeedArtwork :exec +UPDATE podcast_feeds SET artwork_path = $2, updated_at = NOW() WHERE id = $1; + +-- name: UpdatePodcastFeedRedirect :exec +-- itunes:new-feed-url (or a permanent HTTP redirect chain): the feed has +-- MOVED. Rewrite feed_url so future polls hit the new location; the row +-- (and every subscription/episode) stays put. +UPDATE podcast_feeds +SET feed_url = $2, + updated_at = NOW() +WHERE id = $1; + +-- name: GetPodcastFeedByGuid :one +-- URL-migration dedupe: same canonical guid within the library reuses +-- the existing row. +SELECT * FROM podcast_feeds +WHERE library_id = $1 AND podcast_guid = $2 +LIMIT 1; + +-- name: DeletePodcastFeed :execrows +-- Admin-only removal (handler checks role); episodes cascade via +-- media_items.source_feed_id ON DELETE CASCADE. +DELETE FROM podcast_feeds WHERE id = $1; + +-- name: CountPodcastLibrariesForUser :one +-- Sidebar/page gate (D13): >=1 podcasts-type library the user can see +-- (COALESCE(is_visible,true) semantics — mirrors GetUserVisibleLibraries). +SELECT COUNT(*) +FROM libraries l +JOIN library_types lt ON lt.id = l.library_type_id AND lt.name = 'podcasts' +LEFT JOIN library_visibility lv ON lv.library_id = l.id AND lv.user_id = $1 +WHERE COALESCE(lv.is_visible, true) = true; + +-- name: GetPodcastsLibraryForUser :many +-- The podcasts library/ies visible to the user (add-feed target list; +-- a self-hosted instance typically has exactly one). +SELECT l.id, l.name +FROM libraries l +JOIN library_types lt ON lt.id = l.library_type_id AND lt.name = 'podcasts' +LEFT JOIN library_visibility lv ON lv.library_id = l.id AND lv.user_id = $1 +WHERE COALESCE(lv.is_visible, true) = true +ORDER BY l.created_at ASC; + +-- name: GetPodcastLibraryFolder :many +-- The download root (purpose='podcast') of a podcasts library. +SELECT lf.* +FROM library_folders lf +WHERE lf.library_id = $1 AND lf.purpose = 'podcast'; + +-- name: CreatePodcastLibraryFolder :one +INSERT INTO library_folders (library_id, folder_path, purpose) +VALUES ($1, $2, 'podcast') +RETURNING *; + +-- ============================================================================ +-- Podcast episodes — media_items rows with source='podcast_episode' +-- ============================================================================ + +-- name: UpsertPodcastEpisode :one +-- Fetcher-only upsert keyed on the partial unique index +-- (source_feed_id, episode_guid). DO UPDATE refreshes FETCH-OWNED +-- metadata only. Write-after-success guard (AGENTS.md gotcha #7): +-- download state/error/attempts are NEVER touched here, and the +-- downloaded file's facts (path/size/mime/cover) survive renumbering — +-- only un-downloaded rows follow the re-derived file_path. +-- authors_search mirrors authors (trigram search column). +INSERT INTO media_items ( + library_id, title, subtitle, description, authors, authors_search, + file_path, file_size, mime_type, cover_image_path, explicit, + date_published, language, tags, genre, + format_group, format_mimetype, is_reflowable, library_type_name, + source, source_feed_id, episode_guid, feed_position, + season_number, episode_number, enclosure_url, + declared_duration_seconds, transcript_url, chapters_url, + hosts, guests +) VALUES ( + $1, $2, $3, $4, $5, $6, + $7, $8, $9, $10, $11, + $12, $13, $14, $15, + 'audiobook', $9, FALSE, 'podcasts', + 'podcast_episode', $16, $17, $18, + $19, $20, $21, + $22, $23, $24, + $25, $26 +) +ON CONFLICT (source_feed_id, episode_guid) WHERE source_feed_id IS NOT NULL DO UPDATE SET + title = EXCLUDED.title, + subtitle = EXCLUDED.subtitle, + description = EXCLUDED.description, + authors = EXCLUDED.authors, + authors_search = EXCLUDED.authors_search, + explicit = EXCLUDED.explicit, + date_published = EXCLUDED.date_published, + language = EXCLUDED.language, + tags = EXCLUDED.tags, + genre = EXCLUDED.genre, + season_number = EXCLUDED.season_number, + episode_number = EXCLUDED.episode_number, + enclosure_url = EXCLUDED.enclosure_url, + declared_duration_seconds = EXCLUDED.declared_duration_seconds, + transcript_url = EXCLUDED.transcript_url, + chapters_url = EXCLUDED.chapters_url, + hosts = EXCLUDED.hosts, + guests = EXCLUDED.guests, + feed_position = EXCLUDED.feed_position, + cover_image_path = CASE WHEN media_items.download_state = 'downloaded' THEN media_items.cover_image_path ELSE EXCLUDED.cover_image_path END, + file_path = CASE WHEN media_items.download_state = 'downloaded' THEN media_items.file_path ELSE EXCLUDED.file_path END, + file_size = CASE WHEN media_items.download_state = 'downloaded' THEN media_items.file_size ELSE EXCLUDED.file_size END, + mime_type = CASE WHEN media_items.download_state = 'downloaded' THEN media_items.mime_type ELSE EXCLUDED.mime_type END, + updated_at = NOW() +RETURNING *; + +-- name: ListPodcastEpisodes :many +-- Feed detail / sequence source. RLS scopes to subscriber-or-admin; +-- ordering: native position, then pub date, then guid (stable across +-- re-fetches for historical rows whose position went stale). +SELECT * FROM media_items +WHERE source_feed_id = $1 AND source = 'podcast_episode' +ORDER BY feed_position ASC NULLS LAST, date_published ASC NULLS LAST, episode_guid ASC; + +-- name: GetPodcastEpisode :one +SELECT * FROM media_items +WHERE id = $1 AND source = 'podcast_episode'; + +-- name: SetPodcastEpisodeDownloadQueued :execrows +-- Worker claim: pending|failed -> queued. Attempts incremented at +-- claim time so a crashed worker still counts the try. +UPDATE media_items +SET download_state = 'queued', + download_error = NULL, + download_attempts = download_attempts + 1, + updated_at = NOW() +WHERE id = $1 + AND source = 'podcast_episode' + AND download_state IN ('pending', 'failed'); + +-- name: SetPodcastEpisodeDownloading :exec +UPDATE media_items +SET download_state = 'downloading', updated_at = NOW() +WHERE id = $1 AND source = 'podcast_episode'; + +-- name: MarkPodcastEpisodeDownloaded :one +-- SUCCESS-only write (invariant 7): the file is on disk and probed. +-- ffprobe facts (real duration via audio_metadata, precise size/mime) +-- reconcile the feed's declared values. +UPDATE media_items +SET download_state = 'downloaded', + download_error = NULL, + file_path = $2, + file_size = $3, + mime_type = $4, + cover_image_path = COALESCE($5, cover_image_path), + audio_metadata = $6, + chapter_count = $7, + updated_at = NOW() +WHERE id = $1 AND source = 'podcast_episode' +RETURNING *; + +-- name: MarkPodcastEpisodeFailed :exec +-- Failure/rejection write: state + reason + whether a human already +-- intervened (manual_retry resets the auto-retry budget). Existing +-- metadata/file facts are never cleared. +UPDATE media_items +SET download_state = 'failed', + download_error = $2, + download_attempts = CASE WHEN $3::boolean THEN 0 ELSE download_attempts END, + updated_at = NOW() +WHERE id = $1 AND source = 'podcast_episode'; + +-- name: SetPodcastEpisodePending :exec +-- Manual retry (same endpoint as download): reset to pending so the +-- worker re-claims it; the reason text clears with the fresh attempt. +UPDATE media_items +SET download_state = 'pending', + download_error = NULL, + download_attempts = 0, + updated_at = NOW() +WHERE id = $1 AND source = 'podcast_episode'; + +-- name: ListUndownloadedEpisodes :many +-- Download All + policy union worklist. +SELECT * FROM media_items +WHERE source_feed_id = $1 + AND source = 'podcast_episode' + AND download_state <> 'downloaded' +ORDER BY feed_position ASC NULLS LAST, date_published ASC NULLS LAST, episode_guid ASC; + +-- name: CountUndownloadedEpisodes :one +SELECT COUNT(*) FROM media_items +WHERE source_feed_id = $1 + AND source = 'podcast_episode' + AND download_state <> 'downloaded'; + +-- name: CountDownloadedEpisodes :one +SELECT COUNT(*) FROM media_items +WHERE source_feed_id = $1 + AND source = 'podcast_episode' + AND download_state = 'downloaded'; + +-- name: SearchMyPodcastEpisodes :many +-- Subscription-scoped episode search (D14): title + description cover +-- un-downloaded AND historical rows (fetcher persists metadata-only). +-- RLS already restricts to the caller's subscriptions — the explicit +-- source filter keeps scan items out of podcast search. +SELECT * FROM media_items +WHERE source = 'podcast_episode' + AND ($1 = '' OR title ILIKE '%' || $1 || '%' OR description ILIKE '%' || $1 || '%') +ORDER BY feed_position ASC NULLS LAST, date_published ASC NULLS LAST +LIMIT 50; + +-- name: SearchMyPodcastFeeds :many +-- Feed-title AND subscription-display-name search (D14) + the facet +-- fields the podcasts page filters on. +SELECT f.*, + s.display_name, + s.auto_download, + s.auto_download_count, + s.created_at AS subscribed_at +FROM podcast_subscriptions s +JOIN podcast_feeds f ON f.id = s.feed_id +WHERE s.user_id = $1 + AND ($2 = '' OR f.title ILIKE '%' || $2 || '%' + OR COALESCE(s.display_name, '') ILIKE '%' || $2 || '%' + OR COALESCE(f.host, '') ILIKE '%' || $2 || '%' + OR COALESCE(f.publisher, '') ILIKE '%' || $2 || '%') +ORDER BY COALESCE(s.display_name, f.title) ASC; diff --git a/internal/services/library_service.go b/internal/services/library_service.go index 05fc6a60..afbf9b1d 100644 --- a/internal/services/library_service.go +++ b/internal/services/library_service.go @@ -29,6 +29,12 @@ const ( LibraryTypeComics = "comics" LibraryTypeManga = "manga" LibraryTypeAudiobooks = "audiobooks" + // Podcasts libraries are feed containers (RSS subscribe+download), + // not FS-scanned media: AllowedExtensions is deliberately empty — + // the scanner skips these folders wholesale (podcast downloads are + // written by the fetcher's download worker, never discovered by + // the walker). See podcast_service.go. + LibraryTypePodcasts = "podcasts" ) var AllowedExtensions = map[string][]string{ @@ -43,6 +49,9 @@ var AllowedExtensions = map[string][]string{ // browser-playable lossless/next-gen containers join for Batch B // metadata parity (.wma deliberately absent: browsers can't play it). LibraryTypeAudiobooks: {".m4b", ".m4a", ".mp3", ".flac", ".zip", ".opus", ".ogg", ".oga", ".aac", ".wav"}, + // Empty BY DESIGN: nothing in a podcasts library is scanner-facing. + // The scanner additionally skips these folders outright. + LibraryTypePodcasts: {}, } var MimeTypes = map[string]string{