From 902cbe59159aa79c11a0ebfc4313963c37d476c0 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Thu, 15 Oct 2026 16:14:34 -0400 Subject: [PATCH] =?UTF-8?q?feat(audiobooks):=20Batch=20A=20=E2=80=94=20fou?= =?UTF-8?q?ndation,=20m4b=20ingest,=20playback=20sync,=20web=20player?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 (foundation): 'audiobooks' library type seeded (schema + DB); audio_metadata JSONB on media_items (the kobo_metadata precedent — only audio paths read it); reading_progress.audio_offset_seconds for the honest {chapter, offset} locator; extensions/mimes registered for ALL breadth formats (m4b/m4a/mp3/flac — deliberate per the DOC/LIT doctrine, m4b scans richly first); classifyFormatGroup 'audiobook' case (gotcha 39 recomputes on rescan); Go mime table extended at boot so media elements accept the streams; ffmpeg/ffprobe in the image (8.1.2 — also the xHE-AAC identification tripwire). Phase 2 (ingest): sidecar-first (Libation/ABS metadata.json — narrators/ASIN/curated chapters), ffprobe fallback/filler (duration, bitrate, codec/profile, embedded chapters). Fixed three real parser brittlenesses the Libation sidecar exposed: series accepts string OR array-of-objects, publishedYear accepts number OR string, ffprobe chapter timings accept number OR string (flexFloat). Folder- structure fallback now runs on the UPDATE path too (rescans were clearing sidecar-absent series/author); audio facts re-derive on every rescan (gotcha 39). Phase 3 (playback + sync): GET /api/media-items/:id/chapters (the format-abstraction list); streaming rides the existing cookie-authed Range /uploads path; PUT progress carries audio_offset_seconds + elapsed_seconds (honest wall-clock playing time) — the reading- history capture prefers player-reported elapsed over the inter-push delta, same 2h ceiling; reading_mode='audio' marks the row. Phase 4 (web player): dedicated audio reader page (foliate not involved) — cover, chapter title, whole-book scrubber, skip ± (chips 1/5/10/15/30s, default 10s), speed (0.75×–2×), chapter list with jump-to, Media Session lockscreen/notification transport, periodic playing-only progress pushes, position restore on open. Verified live: ingest of the Libation sample (Gardens of the Moon — title/author/series/cover/date from sidecar+folders, 34 chapters, 26h duration, aac/LC); chapters endpoint; the reader route serving the player; playback with position restore (0:57 round-trip); progress rows in audio mode; wall-clock session rows in reading_history. --- Dockerfile | 2 +- database/schema/schema.sql | 16 +- internal/database/models.go | 50 ++-- internal/database/queries.sql.go | 247 ++++++++++------- internal/database/queries/queries.sql | 14 +- internal/handlers/media.go | 78 ++++-- internal/router/media.go | 4 + internal/router/reader.go | 13 +- internal/router/router.go | 8 + internal/services/library_service.go | 17 +- internal/services/media_scanner.go | 129 +++++++-- internal/services/media_scanner_audio.go | 278 +++++++++++++++++++ internal/services/series_service_test.go | 60 ++-- internal/services/statistics_service.go | 6 +- internal/services/worker.go | 5 +- internal/sync/progress.go | 22 ++ templates/audio_reader.templ | 167 +++++++++++ templates/audio_reader_templ.go | 183 ++++++++++++ web/src/audio.ts | 337 +++++++++++++++++++++++ web/src/main.ts | 1 + 20 files changed, 1425 insertions(+), 212 deletions(-) create mode 100644 internal/services/media_scanner_audio.go create mode 100644 templates/audio_reader.templ create mode 100644 templates/audio_reader_templ.go create mode 100644 web/src/audio.ts diff --git a/Dockerfile b/Dockerfile index 027e273d..cc45ca17 100644 --- a/Dockerfile +++ b/Dockerfile @@ -84,7 +84,7 @@ CMD ["go", "test", "./cmd/server/tests", "-v", "-timeout", "5m", "-parallel=1", # Final stage FROM alpine:latest -RUN apk --no-cache add ca-certificates curl poppler-utils +RUN apk --no-cache add ca-certificates curl poppler-utils ffmpeg # Install kepubify for EPUB→KEPUB conversion RUN wget -O /usr/bin/kepubify https://github.com/pgaskin/kepubify/releases/latest/download/kepubify-linux-64bit \ diff --git a/database/schema/schema.sql b/database/schema/schema.sql index 57763c56..d775742d 100644 --- a/database/schema/schema.sql +++ b/database/schema/schema.sql @@ -17,7 +17,8 @@ CREATE TABLE IF NOT EXISTS library_types ( 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']) +('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']) ON CONFLICT (name) DO NOTHING; -- Create users table @@ -235,7 +236,15 @@ CREATE TABLE IF NOT EXISTS media_items ( -- } chapter_metadata JSONB, -- Need library_type_name for checks - library_type_name VARCHAR(50) + library_type_name VARCHAR(50), + -- Audiobook metadata (format-scoped JSONB, the kobo_metadata + -- precedent: only the audio paths read it — ebook paths never do). + -- { "narrators": [], "duration_seconds": N, "codec_profile": "LC", + -- "bit_rate": N, "asin": "...", "chapters": [ + -- { "index": 0, "title": "...", "start": 0.0, "end": 168.0 } ] } + -- The chapters array IS the format abstraction: embedded m4b chapters + -- and (later) folder-as-book files both write this exact shape. + audio_metadata JSONB ); -- Add GIN indexes for fast array searches @@ -319,6 +328,9 @@ CREATE TABLE IF NOT EXISTS reading_progress ( context_text TEXT, chapter INTEGER, chapter_progress FLOAT CHECK (chapter_progress >= 0 AND chapter_progress <= 1), + -- Audiobook locator: seconds into the chapter (honest units, not an + -- overloaded fraction — a 2h chapter at float precision drifts). + audio_offset_seconds INTEGER CHECK (audio_offset_seconds >= 0), viewport_x FLOAT DEFAULT 0, viewport_y FLOAT DEFAULT 0, zoom_level FLOAT DEFAULT 1.0, diff --git a/internal/database/models.go b/internal/database/models.go index 03424606..64601125 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -312,6 +312,7 @@ type MediaItems struct { 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"` FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` @@ -416,30 +417,31 @@ type ReadingHistory struct { } type ReadingProgress struct { - ID pgtype.UUID `db:"id" json:"id"` - MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` - UserID pgtype.UUID `db:"user_id" json:"user_id"` - CurrentPage pgtype.Int4 `db:"current_page" json:"current_page"` - TotalPages pgtype.Int4 `db:"total_pages" json:"total_pages"` - LastReadAt pgtype.Timestamptz `db:"last_read_at" json:"last_read_at"` - Percentage pgtype.Float8 `db:"percentage" json:"percentage"` - CharacterOffset pgtype.Int8 `db:"character_offset" json:"character_offset"` - Epubcfi pgtype.Text `db:"epubcfi" json:"epubcfi"` - ContextText pgtype.Text `db:"context_text" json:"context_text"` - Chapter pgtype.Int4 `db:"chapter" json:"chapter"` - ChapterProgress pgtype.Float8 `db:"chapter_progress" json:"chapter_progress"` - ViewportX pgtype.Float8 `db:"viewport_x" json:"viewport_x"` - ViewportY pgtype.Float8 `db:"viewport_y" json:"viewport_y"` - ZoomLevel pgtype.Float8 `db:"zoom_level" json:"zoom_level"` - ScrollPositionX pgtype.Float8 `db:"scroll_position_x" json:"scroll_position_x"` - ScrollPositionY pgtype.Float8 `db:"scroll_position_y" json:"scroll_position_y"` - PanelNumber pgtype.Int4 `db:"panel_number" json:"panel_number"` - ReadingMode pgtype.Text `db:"reading_mode" json:"reading_mode"` - LastSyncDevice pgtype.Text `db:"last_sync_device" json:"last_sync_device"` - LastSyncSource pgtype.Text `db:"last_sync_source" json:"last_sync_source"` - LastSyncTimestamp pgtype.Timestamptz `db:"last_sync_timestamp" json:"last_sync_timestamp"` - ConflictDetected pgtype.Bool `db:"conflict_detected" json:"conflict_detected"` - ConflictResolved pgtype.Bool `db:"conflict_resolved" json:"conflict_resolved"` + ID pgtype.UUID `db:"id" json:"id"` + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + CurrentPage pgtype.Int4 `db:"current_page" json:"current_page"` + TotalPages pgtype.Int4 `db:"total_pages" json:"total_pages"` + LastReadAt pgtype.Timestamptz `db:"last_read_at" json:"last_read_at"` + Percentage pgtype.Float8 `db:"percentage" json:"percentage"` + CharacterOffset pgtype.Int8 `db:"character_offset" json:"character_offset"` + Epubcfi pgtype.Text `db:"epubcfi" json:"epubcfi"` + ContextText pgtype.Text `db:"context_text" json:"context_text"` + Chapter pgtype.Int4 `db:"chapter" json:"chapter"` + ChapterProgress pgtype.Float8 `db:"chapter_progress" json:"chapter_progress"` + AudioOffsetSeconds pgtype.Int4 `db:"audio_offset_seconds" json:"audio_offset_seconds"` + ViewportX pgtype.Float8 `db:"viewport_x" json:"viewport_x"` + ViewportY pgtype.Float8 `db:"viewport_y" json:"viewport_y"` + ZoomLevel pgtype.Float8 `db:"zoom_level" json:"zoom_level"` + ScrollPositionX pgtype.Float8 `db:"scroll_position_x" json:"scroll_position_x"` + ScrollPositionY pgtype.Float8 `db:"scroll_position_y" json:"scroll_position_y"` + PanelNumber pgtype.Int4 `db:"panel_number" json:"panel_number"` + ReadingMode pgtype.Text `db:"reading_mode" json:"reading_mode"` + LastSyncDevice pgtype.Text `db:"last_sync_device" json:"last_sync_device"` + LastSyncSource pgtype.Text `db:"last_sync_source" json:"last_sync_source"` + LastSyncTimestamp pgtype.Timestamptz `db:"last_sync_timestamp" json:"last_sync_timestamp"` + ConflictDetected pgtype.Bool `db:"conflict_detected" json:"conflict_detected"` + ConflictResolved pgtype.Bool `db:"conflict_resolved" json:"conflict_resolved"` } type ReadingSpeed struct { diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index 07556e85..8eb289a7 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -979,10 +979,10 @@ func (q *Queries) CreateMediaHighlightFull(ctx context.Context, arg CreateMediaH } const CreateMediaItem = `-- name: CreateMediaItem :one -INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publisher, 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) -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) +INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publisher, 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) +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) ON CONFLICT (library_id, file_path) DO UPDATE SET updated_at = NOW() -RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence +RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence ` type CreateMediaItemParams struct { @@ -1030,6 +1030,7 @@ type CreateMediaItemParams struct { ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` Summary pgtype.Text `db:"summary" json:"summary"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` + AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` } // Media Items queries @@ -1079,6 +1080,7 @@ func (q *Queries) CreateMediaItem(ctx context.Context, arg CreateMediaItemParams arg.ScanInformation, arg.Summary, arg.LibraryTypeName, + arg.AudioMetadata, ) var i MediaItems err := row.Scan( @@ -1140,6 +1142,7 @@ func (q *Queries) CreateMediaItem(ctx context.Context, arg CreateMediaItemParams &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -1200,10 +1203,10 @@ func (q *Queries) CreateMediaItemFormat(ctx context.Context, arg CreateMediaItem } const CreateMediaItemIfNew = `-- name: CreateMediaItemIfNew :one -INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publisher, 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) -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) +INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publisher, 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) +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) ON CONFLICT (library_id, file_path) DO NOTHING -RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence +RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence ` type CreateMediaItemIfNewParams struct { @@ -1251,6 +1254,7 @@ type CreateMediaItemIfNewParams struct { ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` Summary pgtype.Text `db:"summary" json:"summary"` LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"` + AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` } // API create path: race-safe create-or-detect. On a duplicate @@ -1303,6 +1307,7 @@ func (q *Queries) CreateMediaItemIfNew(ctx context.Context, arg CreateMediaItemI arg.ScanInformation, arg.Summary, arg.LibraryTypeName, + arg.AudioMetadata, ) var i MediaItems err := row.Scan( @@ -1364,6 +1369,7 @@ func (q *Queries) CreateMediaItemIfNew(ctx context.Context, arg CreateMediaItemI &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -2822,7 +2828,7 @@ func (q *Queries) GetBookReadingSessions(ctx context.Context, arg GetBookReading } const GetBooksByTag = `-- name: GetBooksByTag :many -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items +SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE library_id = $1 AND tags @> ARRAY[$2::text] ORDER BY title ASC ` @@ -2900,6 +2906,7 @@ func (q *Queries) GetBooksByTag(ctx context.Context, arg GetBooksByTagParams) ([ &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -3000,7 +3007,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.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.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.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, ci.excluded FROM media_items mi +SELECT mi.id, mi.library_id, mi.title, 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.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.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, ci.excluded FROM media_items mi INNER JOIN collection_items ci ON ci.media_item_id = mi.id WHERE ci.collection_id = $1 AND ($2::uuid IS NULL OR mi.library_id = $2::uuid) @@ -3075,6 +3082,7 @@ type GetCollectionItemsForDashboardRow struct { 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"` FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` @@ -3152,6 +3160,7 @@ func (q *Queries) GetCollectionItemsForDashboard(ctx context.Context, arg GetCol &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -3307,7 +3316,7 @@ func (q *Queries) GetCollectionsForBook(ctx context.Context, mediaItemID pgtype. } const GetContinueReadingItems = `-- name: GetContinueReadingItems :many -SELECT mi.id, mi.library_id, mi.title, 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.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.tags_search, mi.contributors_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.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.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.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi INNER JOIN ( SELECT DISTINCT ON (media_item_id) media_item_id, last_read_at FROM reading_progress @@ -3398,6 +3407,7 @@ func (q *Queries) GetContinueReadingItems(ctx context.Context, arg GetContinueRe &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -3429,7 +3439,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.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.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.tags_search, mi.contributors_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.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.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.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, usp.last_read_at FROM media_items mi JOIN user_series_progress usp ON mi.series = usp.series @@ -3439,7 +3449,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, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence, last_read_at FROM next_books +SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence, last_read_at FROM next_books ORDER BY last_read_at DESC NULLS LAST LIMIT $1 ` @@ -3509,6 +3519,7 @@ type GetContinueSeriesItemsRow struct { 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"` FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` @@ -3586,6 +3597,7 @@ func (q *Queries) GetContinueSeriesItems(ctx context.Context, arg GetContinueSer &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -4785,7 +4797,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.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.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.tags_search, mi.contributors_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.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.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.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence 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 @@ -4860,6 +4872,7 @@ func (q *Queries) GetLibraryItems(ctx context.Context, libraryID pgtype.UUID) ([ &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -5289,7 +5302,7 @@ func (q *Queries) GetMediaHighlights(ctx context.Context, arg GetMediaHighlights } const GetMediaItem = `-- name: GetMediaItem :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE id = $1 +SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE id = $1 ` func (q *Queries) GetMediaItem(ctx context.Context, id pgtype.UUID) (MediaItems, error) { @@ -5354,6 +5367,7 @@ func (q *Queries) GetMediaItem(ctx context.Context, id pgtype.UUID) (MediaItems, &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -5365,7 +5379,7 @@ func (q *Queries) GetMediaItem(ctx context.Context, id pgtype.UUID) (MediaItems, } const GetMediaItemByFilePath = `-- name: GetMediaItemByFilePath :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, tags_search, contributors_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, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_path = $1 AND library_id = $2 ` type GetMediaItemByFilePathParams struct { @@ -5435,6 +5449,7 @@ func (q *Queries) GetMediaItemByFilePath(ctx context.Context, arg GetMediaItemBy &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -5446,7 +5461,7 @@ func (q *Queries) GetMediaItemByFilePath(ctx context.Context, arg GetMediaItemBy } const GetMediaItemByFilePathAnyLibrary = `-- name: GetMediaItemByFilePathAnyLibrary :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_path = $1 LIMIT 1 +SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_path = $1 LIMIT 1 ` func (q *Queries) GetMediaItemByFilePathAnyLibrary(ctx context.Context, filePath string) (MediaItems, error) { @@ -5511,6 +5526,7 @@ func (q *Queries) GetMediaItemByFilePathAnyLibrary(ctx context.Context, filePath &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -5523,7 +5539,7 @@ func (q *Queries) GetMediaItemByFilePathAnyLibrary(ctx context.Context, filePath const GetMediaItemByFilePathForSync = `-- name: GetMediaItemByFilePathForSync :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_path = $1 +SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_path = $1 ` // ============================================ @@ -5591,6 +5607,7 @@ func (q *Queries) GetMediaItemByFilePathForSync(ctx context.Context, filePath st &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -5602,7 +5619,7 @@ func (q *Queries) GetMediaItemByFilePathForSync(ctx context.Context, filePath st } const GetMediaItemByKoboContentId = `-- name: GetMediaItemByKoboContentId :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE kobo_content_id = $1 +SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE kobo_content_id = $1 ` func (q *Queries) GetMediaItemByKoboContentId(ctx context.Context, koboContentID pgtype.Text) (MediaItems, error) { @@ -5667,6 +5684,7 @@ func (q *Queries) GetMediaItemByKoboContentId(ctx context.Context, koboContentID &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -5678,7 +5696,7 @@ func (q *Queries) GetMediaItemByKoboContentId(ctx context.Context, koboContentID } const GetMediaItemByLibraryAndPath = `-- name: GetMediaItemByLibraryAndPath :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, tags_search, contributors_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, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE library_id = $1 AND file_path = $2 ` type GetMediaItemByLibraryAndPathParams struct { @@ -5748,6 +5766,7 @@ func (q *Queries) GetMediaItemByLibraryAndPath(ctx context.Context, arg GetMedia &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -5759,7 +5778,7 @@ func (q *Queries) GetMediaItemByLibraryAndPath(ctx context.Context, arg GetMedia } const GetMediaItemByOPFIdentifier = `-- name: GetMediaItemByOPFIdentifier :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE opf_identifier = $1 +SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE opf_identifier = $1 ` // Get media item by OPF identifier @@ -5825,6 +5844,7 @@ func (q *Queries) GetMediaItemByOPFIdentifier(ctx context.Context, opfIdentifier &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -5836,7 +5856,7 @@ func (q *Queries) GetMediaItemByOPFIdentifier(ctx context.Context, opfIdentifier } const GetMediaItemByOPFUUID = `-- name: GetMediaItemByOPFUUID :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE opf_uuid = $1 +SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE opf_uuid = $1 ` // Get media item by OPF UUID @@ -5902,6 +5922,7 @@ func (q *Queries) GetMediaItemByOPFUUID(ctx context.Context, opfUuid pgtype.Text &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -5913,7 +5934,7 @@ func (q *Queries) GetMediaItemByOPFUUID(ctx context.Context, opfUuid pgtype.Text } const GetMediaItemBySHA256 = `-- name: GetMediaItemBySHA256 :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_sha256 = $1 +SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_sha256 = $1 ` // Get media item by SHA-256 hash @@ -5979,6 +6000,7 @@ func (q *Queries) GetMediaItemBySHA256(ctx context.Context, fileSha256 pgtype.Te &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -5990,7 +6012,7 @@ func (q *Queries) GetMediaItemBySHA256(ctx context.Context, fileSha256 pgtype.Te } const GetMediaItemBySHA256AndLibrary = `-- name: GetMediaItemBySHA256AndLibrary :one -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, tags_search, contributors_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, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_sha256 = $1 AND library_id = $2 ` type GetMediaItemBySHA256AndLibraryParams struct { @@ -6061,6 +6083,7 @@ func (q *Queries) GetMediaItemBySHA256AndLibrary(ctx context.Context, arg GetMed &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -6187,7 +6210,7 @@ func (q *Queries) GetMediaItemUsageCounts(ctx context.Context, mediaItemID pgtyp } const GetMediaItemsBySHA256AndSameLibraryType = `-- name: GetMediaItemsBySHA256AndSameLibraryType :many -SELECT mi.id, mi.library_id, mi.title, 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.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.tags_search, mi.contributors_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.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.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.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, 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 @@ -6264,6 +6287,7 @@ type GetMediaItemsBySHA256AndSameLibraryTypeRow struct { 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"` FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` @@ -6346,6 +6370,7 @@ func (q *Queries) GetMediaItemsBySHA256AndSameLibraryType(ctx context.Context, a &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -6600,7 +6625,7 @@ func (q *Queries) GetNextRetryTime(ctx context.Context) (interface{}, error) { } const GetNotStartedItems = `-- name: GetNotStartedItems :many -SELECT mi.id, mi.library_id, mi.title, 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.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.tags_search, mi.contributors_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.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.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.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence 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 @@ -6688,6 +6713,7 @@ func (q *Queries) GetNotStartedItems(ctx context.Context, arg GetNotStartedItems &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -6937,7 +6963,7 @@ func (q *Queries) GetReadingHistory(ctx context.Context, arg GetReadingHistoryPa } const GetReadingProgress = `-- name: GetReadingProgress :one -SELECT id, media_item_id, user_id, current_page, total_pages, last_read_at, percentage, character_offset, epubcfi, context_text, chapter, chapter_progress, viewport_x, viewport_y, zoom_level, scroll_position_x, scroll_position_y, panel_number, reading_mode, last_sync_device, last_sync_source, last_sync_timestamp, conflict_detected, conflict_resolved FROM reading_progress WHERE media_item_id = $1 AND user_id = $2 +SELECT id, media_item_id, user_id, current_page, total_pages, last_read_at, percentage, character_offset, epubcfi, context_text, chapter, chapter_progress, audio_offset_seconds, viewport_x, viewport_y, zoom_level, scroll_position_x, scroll_position_y, panel_number, reading_mode, last_sync_device, last_sync_source, last_sync_timestamp, conflict_detected, conflict_resolved FROM reading_progress WHERE media_item_id = $1 AND user_id = $2 ` type GetReadingProgressParams struct { @@ -6961,6 +6987,7 @@ func (q *Queries) GetReadingProgress(ctx context.Context, arg GetReadingProgress &i.ContextText, &i.Chapter, &i.ChapterProgress, + &i.AudioOffsetSeconds, &i.ViewportX, &i.ViewportY, &i.ZoomLevel, @@ -7005,7 +7032,7 @@ func (q *Queries) GetReadingSpeed(ctx context.Context, arg GetReadingSpeedParams } const GetRecentlyAddedItems = `-- name: GetRecentlyAddedItems :many -SELECT mi.id, mi.library_id, mi.title, 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.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.tags_search, mi.contributors_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.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.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.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence 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 @@ -7086,6 +7113,7 @@ func (q *Queries) GetRecentlyAddedItems(ctx context.Context, arg GetRecentlyAdde &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -7104,7 +7132,7 @@ func (q *Queries) GetRecentlyAddedItems(ctx context.Context, arg GetRecentlyAdde } const GetRecentlyReadItems = `-- name: GetRecentlyReadItems :many -SELECT mi.id, mi.library_id, mi.title, 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.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.tags_search, mi.contributors_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.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.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.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi INNER JOIN ( SELECT DISTINCT ON (media_item_id) media_item_id, last_read_at FROM reading_progress @@ -7193,6 +7221,7 @@ func (q *Queries) GetRecentlyReadItems(ctx context.Context, arg GetRecentlyReadI &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -7311,7 +7340,7 @@ func (q *Queries) GetSavedFilters(ctx context.Context, arg GetSavedFiltersParams } const GetSeriesBooks = `-- name: GetSeriesBooks :many -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items +SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE series = $1 AND library_id = $2 AND archived_at IS NULL @@ -7395,6 +7424,7 @@ func (q *Queries) GetSeriesBooks(ctx context.Context, arg GetSeriesBooksParams) &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -7910,6 +7940,7 @@ SELECT rp.context_text, rp.chapter, rp.chapter_progress, + rp.audio_offset_seconds, rp.viewport_x, rp.viewport_y, rp.zoom_level, @@ -7939,36 +7970,37 @@ type GetUniversalProgressParams struct { } type GetUniversalProgressRow struct { - ID pgtype.UUID `db:"id" json:"id"` - MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` - UserID pgtype.UUID `db:"user_id" json:"user_id"` - CurrentPage pgtype.Int4 `db:"current_page" json:"current_page"` - TotalPages pgtype.Int4 `db:"total_pages" json:"total_pages"` - LastReadAt pgtype.Timestamptz `db:"last_read_at" json:"last_read_at"` - Percentage pgtype.Float8 `db:"percentage" json:"percentage"` - CharacterOffset pgtype.Int8 `db:"character_offset" json:"character_offset"` - Epubcfi pgtype.Text `db:"epubcfi" json:"epubcfi"` - ContextText pgtype.Text `db:"context_text" json:"context_text"` - Chapter pgtype.Int4 `db:"chapter" json:"chapter"` - ChapterProgress pgtype.Float8 `db:"chapter_progress" json:"chapter_progress"` - ViewportX pgtype.Float8 `db:"viewport_x" json:"viewport_x"` - ViewportY pgtype.Float8 `db:"viewport_y" json:"viewport_y"` - ZoomLevel pgtype.Float8 `db:"zoom_level" json:"zoom_level"` - ScrollPositionX pgtype.Float8 `db:"scroll_position_x" json:"scroll_position_x"` - ScrollPositionY pgtype.Float8 `db:"scroll_position_y" json:"scroll_position_y"` - PanelNumber pgtype.Int4 `db:"panel_number" json:"panel_number"` - ReadingMode pgtype.Text `db:"reading_mode" json:"reading_mode"` - LastSyncDevice pgtype.Text `db:"last_sync_device" json:"last_sync_device"` - LastSyncSource pgtype.Text `db:"last_sync_source" json:"last_sync_source"` - LastSyncTimestamp pgtype.Timestamptz `db:"last_sync_timestamp" json:"last_sync_timestamp"` - ConflictDetected pgtype.Bool `db:"conflict_detected" json:"conflict_detected"` - ConflictResolved pgtype.Bool `db:"conflict_resolved" json:"conflict_resolved"` - 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"` + ID pgtype.UUID `db:"id" json:"id"` + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + CurrentPage pgtype.Int4 `db:"current_page" json:"current_page"` + TotalPages pgtype.Int4 `db:"total_pages" json:"total_pages"` + LastReadAt pgtype.Timestamptz `db:"last_read_at" json:"last_read_at"` + Percentage pgtype.Float8 `db:"percentage" json:"percentage"` + CharacterOffset pgtype.Int8 `db:"character_offset" json:"character_offset"` + Epubcfi pgtype.Text `db:"epubcfi" json:"epubcfi"` + ContextText pgtype.Text `db:"context_text" json:"context_text"` + Chapter pgtype.Int4 `db:"chapter" json:"chapter"` + ChapterProgress pgtype.Float8 `db:"chapter_progress" json:"chapter_progress"` + AudioOffsetSeconds pgtype.Int4 `db:"audio_offset_seconds" json:"audio_offset_seconds"` + ViewportX pgtype.Float8 `db:"viewport_x" json:"viewport_x"` + ViewportY pgtype.Float8 `db:"viewport_y" json:"viewport_y"` + ZoomLevel pgtype.Float8 `db:"zoom_level" json:"zoom_level"` + ScrollPositionX pgtype.Float8 `db:"scroll_position_x" json:"scroll_position_x"` + ScrollPositionY pgtype.Float8 `db:"scroll_position_y" json:"scroll_position_y"` + PanelNumber pgtype.Int4 `db:"panel_number" json:"panel_number"` + ReadingMode pgtype.Text `db:"reading_mode" json:"reading_mode"` + LastSyncDevice pgtype.Text `db:"last_sync_device" json:"last_sync_device"` + LastSyncSource pgtype.Text `db:"last_sync_source" json:"last_sync_source"` + LastSyncTimestamp pgtype.Timestamptz `db:"last_sync_timestamp" json:"last_sync_timestamp"` + ConflictDetected pgtype.Bool `db:"conflict_detected" json:"conflict_detected"` + ConflictResolved pgtype.Bool `db:"conflict_resolved" json:"conflict_resolved"` + 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"` } // Get universal progress for a book @@ -7988,6 +8020,7 @@ func (q *Queries) GetUniversalProgress(ctx context.Context, arg GetUniversalProg &i.ContextText, &i.Chapter, &i.ChapterProgress, + &i.AudioOffsetSeconds, &i.ViewportX, &i.ViewportY, &i.ZoomLevel, @@ -9040,7 +9073,7 @@ func (q *Queries) GetVisibleLibraryMediaCounts(ctx context.Context, userID pgtyp } const GetVisibleLiveMediaForRules = `-- name: GetVisibleLiveMediaForRules :many -SELECT mi.id, mi.library_id, mi.title, 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.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.tags_search, mi.contributors_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.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.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.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence 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 @@ -9126,6 +9159,7 @@ func (q *Queries) GetVisibleLiveMediaForRules(ctx context.Context, arg GetVisibl &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -9646,7 +9680,7 @@ func (q *Queries) ListDevicesByUser(ctx context.Context, userID pgtype.UUID) ([] } const ListHiddenMediaItems = `-- name: ListHiddenMediaItems :many -SELECT mi.id, mi.library_id, mi.title, 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.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.tags_search, mi.contributors_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.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.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.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, 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 @@ -9731,6 +9765,7 @@ type ListHiddenMediaItemsRow struct { 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"` FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` @@ -9818,6 +9853,7 @@ func (q *Queries) ListHiddenMediaItems(ctx context.Context) ([]ListHiddenMediaIt &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -9890,7 +9926,7 @@ func (q *Queries) ListLibraries(ctx context.Context) ([]ListLibrariesRow, error) } const ListMediaItems = `-- name: ListMediaItems :many -SELECT mi.id, mi.library_id, mi.title, 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.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.tags_search, mi.contributors_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.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.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.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, 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 @@ -9965,6 +10001,7 @@ type ListMediaItemsRow struct { 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"` FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` @@ -10043,6 +10080,7 @@ func (q *Queries) ListMediaItems(ctx context.Context, arg ListMediaItemsParams) &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -10063,7 +10101,7 @@ func (q *Queries) ListMediaItems(ctx context.Context, arg ListMediaItemsParams) } const ListMediaItemsByLibrary = `-- name: ListMediaItemsByLibrary :many -SELECT mi.id, mi.library_id, mi.title, 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.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.tags_search, mi.contributors_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.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.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.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, 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 @@ -10132,6 +10170,7 @@ type ListMediaItemsByLibraryRow struct { 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"` FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` @@ -10210,6 +10249,7 @@ func (q *Queries) ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype. &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -10230,7 +10270,7 @@ func (q *Queries) ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype. } const ListMediaItemsByLibraryIncludingArchived = `-- name: ListMediaItemsByLibraryIncludingArchived :many -SELECT mi.id, mi.library_id, mi.title, 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.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.tags_search, mi.contributors_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.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.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.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, 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 @@ -10297,6 +10337,7 @@ type ListMediaItemsByLibraryIncludingArchivedRow struct { 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"` FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` @@ -10375,6 +10416,7 @@ func (q *Queries) ListMediaItemsByLibraryIncludingArchived(ctx context.Context, &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -10395,7 +10437,7 @@ func (q *Queries) ListMediaItemsByLibraryIncludingArchived(ctx context.Context, } const ListMediaItemsBySHA256AndLibrary = `-- name: ListMediaItemsBySHA256AndLibrary :many -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, tags_search, contributors_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, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_sha256 = $1 AND library_id = $2 ORDER BY file_path ` type ListMediaItemsBySHA256AndLibraryParams struct { @@ -10472,6 +10514,7 @@ func (q *Queries) ListMediaItemsBySHA256AndLibrary(ctx context.Context, arg List &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -10490,7 +10533,7 @@ func (q *Queries) ListMediaItemsBySHA256AndLibrary(ctx context.Context, arg List } const ListMediaItemsMissingHash = `-- name: ListMediaItemsMissingHash :many -SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, tags_search, contributors_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, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence 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) @@ -10562,6 +10605,7 @@ func (q *Queries) ListMediaItemsMissingHash(ctx context.Context) ([]MediaItems, &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -10580,7 +10624,7 @@ func (q *Queries) ListMediaItemsMissingHash(ctx context.Context) ([]MediaItems, } const ListMediaItemsSorted = `-- name: ListMediaItemsSorted :many -SELECT mi.id, mi.library_id, mi.title, 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.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.tags_search, mi.contributors_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.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.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.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, 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 @@ -10722,6 +10766,7 @@ type ListMediaItemsSortedRow struct { 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"` FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` @@ -10805,6 +10850,7 @@ func (q *Queries) ListMediaItemsSorted(ctx context.Context, arg ListMediaItemsSo &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -12113,7 +12159,7 @@ func (q *Queries) SearchLanguageValues(ctx context.Context, arg SearchLanguageVa } const SearchMediaItems = `-- name: SearchMediaItems :many -SELECT mi.id, mi.library_id, mi.title, 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.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.tags_search, mi.contributors_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.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.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.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, 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 @@ -12208,6 +12254,7 @@ type SearchMediaItemsRow struct { 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"` FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"` @@ -12293,6 +12340,7 @@ func (q *Queries) SearchMediaItems(ctx context.Context, arg SearchMediaItemsPara &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -13836,9 +13884,10 @@ UPDATE media_items SET scan_information = $36, summary = $37, metadata_overrides = $38, + audio_metadata = $39, 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, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence +RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence ` type UpdateMediaItemParams struct { @@ -13880,6 +13929,7 @@ type UpdateMediaItemParams struct { ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"` Summary pgtype.Text `db:"summary" json:"summary"` MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"` + AudioMetadata []byte `db:"audio_metadata" json:"audio_metadata"` } func (q *Queries) UpdateMediaItem(ctx context.Context, arg UpdateMediaItemParams) (MediaItems, error) { @@ -13922,6 +13972,7 @@ func (q *Queries) UpdateMediaItem(ctx context.Context, arg UpdateMediaItemParams arg.ScanInformation, arg.Summary, arg.MetadataOverrides, + arg.AudioMetadata, ) var i MediaItems err := row.Scan( @@ -13983,6 +14034,7 @@ func (q *Queries) UpdateMediaItem(ctx context.Context, arg UpdateMediaItemParams &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -13997,7 +14049,7 @@ const UpdateMediaItemChapterMetadata = `-- name: UpdateMediaItemChapterMetadata UPDATE media_items SET chapter_metadata = $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, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence +RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence ` type UpdateMediaItemChapterMetadataParams struct { @@ -14067,6 +14119,7 @@ func (q *Queries) UpdateMediaItemChapterMetadata(ctx context.Context, arg Update &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -14172,7 +14225,7 @@ SET hash_confidence = $5, 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, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence +RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence ` type UpdateMediaItemIdentifiersParams struct { @@ -14256,6 +14309,7 @@ func (q *Queries) UpdateMediaItemIdentifiers(ctx context.Context, arg UpdateMedi &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -14274,7 +14328,7 @@ SET entitlement_id = $2, kobo_metadata = $5, 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, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence +RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, 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, file_sha256, opf_identifier, opf_uuid, hash_confidence ` type UpdateMediaItemKoboMetadataParams struct { @@ -14353,6 +14407,7 @@ func (q *Queries) UpdateMediaItemKoboMetadata(ctx context.Context, arg UpdateMed &i.ArchivedAt, &i.ChapterMetadata, &i.LibraryTypeName, + &i.AudioMetadata, &i.TagsSearch, &i.ContributorsSearch, &i.FileSha256, @@ -14538,7 +14593,7 @@ DO UPDATE SET current_page = EXCLUDED.current_page, total_pages = EXCLUDED.total_pages, last_read_at = NOW() -RETURNING id, media_item_id, user_id, current_page, total_pages, last_read_at, percentage, character_offset, epubcfi, context_text, chapter, chapter_progress, viewport_x, viewport_y, zoom_level, scroll_position_x, scroll_position_y, panel_number, reading_mode, last_sync_device, last_sync_source, last_sync_timestamp, conflict_detected, conflict_resolved +RETURNING id, media_item_id, user_id, current_page, total_pages, last_read_at, percentage, character_offset, epubcfi, context_text, chapter, chapter_progress, audio_offset_seconds, viewport_x, viewport_y, zoom_level, scroll_position_x, scroll_position_y, panel_number, reading_mode, last_sync_device, last_sync_source, last_sync_timestamp, conflict_detected, conflict_resolved ` type UpdateReadingProgressParams struct { @@ -14569,6 +14624,7 @@ func (q *Queries) UpdateReadingProgress(ctx context.Context, arg UpdateReadingPr &i.ContextText, &i.Chapter, &i.ChapterProgress, + &i.AudioOffsetSeconds, &i.ViewportX, &i.ViewportY, &i.ZoomLevel, @@ -14739,10 +14795,11 @@ INSERT INTO reading_progress ( last_sync_timestamp, current_page, total_pages, + audio_offset_seconds, last_read_at ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, NOW(), $18, $19, NOW() + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, NOW(), $18, $19, $20, NOW() ) ON CONFLICT (media_item_id, user_id) DO UPDATE SET @@ -14764,30 +14821,32 @@ DO UPDATE SET last_sync_timestamp = EXCLUDED.last_sync_timestamp, current_page = EXCLUDED.current_page, total_pages = EXCLUDED.total_pages, + audio_offset_seconds = EXCLUDED.audio_offset_seconds, last_read_at = NOW() -RETURNING id, media_item_id, user_id, current_page, total_pages, last_read_at, percentage, character_offset, epubcfi, context_text, chapter, chapter_progress, viewport_x, viewport_y, zoom_level, scroll_position_x, scroll_position_y, panel_number, reading_mode, last_sync_device, last_sync_source, last_sync_timestamp, conflict_detected, conflict_resolved +RETURNING id, media_item_id, user_id, current_page, total_pages, last_read_at, percentage, character_offset, epubcfi, context_text, chapter, chapter_progress, audio_offset_seconds, viewport_x, viewport_y, zoom_level, scroll_position_x, scroll_position_y, panel_number, reading_mode, last_sync_device, last_sync_source, last_sync_timestamp, conflict_detected, conflict_resolved ` type UpdateUniversalProgressParams struct { - MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` - UserID pgtype.UUID `db:"user_id" json:"user_id"` - Percentage pgtype.Float8 `db:"percentage" json:"percentage"` - CharacterOffset pgtype.Int8 `db:"character_offset" json:"character_offset"` - Epubcfi pgtype.Text `db:"epubcfi" json:"epubcfi"` - ContextText pgtype.Text `db:"context_text" json:"context_text"` - Chapter pgtype.Int4 `db:"chapter" json:"chapter"` - ChapterProgress pgtype.Float8 `db:"chapter_progress" json:"chapter_progress"` - ViewportX pgtype.Float8 `db:"viewport_x" json:"viewport_x"` - ViewportY pgtype.Float8 `db:"viewport_y" json:"viewport_y"` - ZoomLevel pgtype.Float8 `db:"zoom_level" json:"zoom_level"` - ScrollPositionX pgtype.Float8 `db:"scroll_position_x" json:"scroll_position_x"` - ScrollPositionY pgtype.Float8 `db:"scroll_position_y" json:"scroll_position_y"` - PanelNumber pgtype.Int4 `db:"panel_number" json:"panel_number"` - ReadingMode pgtype.Text `db:"reading_mode" json:"reading_mode"` - LastSyncDevice pgtype.Text `db:"last_sync_device" json:"last_sync_device"` - LastSyncSource pgtype.Text `db:"last_sync_source" json:"last_sync_source"` - CurrentPage pgtype.Int4 `db:"current_page" json:"current_page"` - TotalPages pgtype.Int4 `db:"total_pages" json:"total_pages"` + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + Percentage pgtype.Float8 `db:"percentage" json:"percentage"` + CharacterOffset pgtype.Int8 `db:"character_offset" json:"character_offset"` + Epubcfi pgtype.Text `db:"epubcfi" json:"epubcfi"` + ContextText pgtype.Text `db:"context_text" json:"context_text"` + Chapter pgtype.Int4 `db:"chapter" json:"chapter"` + ChapterProgress pgtype.Float8 `db:"chapter_progress" json:"chapter_progress"` + ViewportX pgtype.Float8 `db:"viewport_x" json:"viewport_x"` + ViewportY pgtype.Float8 `db:"viewport_y" json:"viewport_y"` + ZoomLevel pgtype.Float8 `db:"zoom_level" json:"zoom_level"` + ScrollPositionX pgtype.Float8 `db:"scroll_position_x" json:"scroll_position_x"` + ScrollPositionY pgtype.Float8 `db:"scroll_position_y" json:"scroll_position_y"` + PanelNumber pgtype.Int4 `db:"panel_number" json:"panel_number"` + ReadingMode pgtype.Text `db:"reading_mode" json:"reading_mode"` + LastSyncDevice pgtype.Text `db:"last_sync_device" json:"last_sync_device"` + LastSyncSource pgtype.Text `db:"last_sync_source" json:"last_sync_source"` + CurrentPage pgtype.Int4 `db:"current_page" json:"current_page"` + TotalPages pgtype.Int4 `db:"total_pages" json:"total_pages"` + AudioOffsetSeconds pgtype.Int4 `db:"audio_offset_seconds" json:"audio_offset_seconds"` } // Update universal progress @@ -14812,6 +14871,7 @@ func (q *Queries) UpdateUniversalProgress(ctx context.Context, arg UpdateUnivers arg.LastSyncSource, arg.CurrentPage, arg.TotalPages, + arg.AudioOffsetSeconds, ) var i ReadingProgress err := row.Scan( @@ -14827,6 +14887,7 @@ func (q *Queries) UpdateUniversalProgress(ctx context.Context, arg UpdateUnivers &i.ContextText, &i.Chapter, &i.ChapterProgress, + &i.AudioOffsetSeconds, &i.ViewportX, &i.ViewportY, &i.ZoomLevel, diff --git a/internal/database/queries/queries.sql b/internal/database/queries/queries.sql index 46909b48..9b301a31 100644 --- a/internal/database/queries/queries.sql +++ b/internal/database/queries/queries.sql @@ -149,8 +149,8 @@ GROUP BY l.id; -- Media Items queries -- name: CreateMediaItem :one -INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publisher, 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) -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) +INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publisher, 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) +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) ON CONFLICT (library_id, file_path) DO UPDATE SET updated_at = NOW() RETURNING *; @@ -159,8 +159,8 @@ RETURNING *; -- (library_id, file_path) this inserts NOTHING and returns no row -- (pgx.ErrNoRows) — the handler then answers 200 with the existing row -- and a duplicate flag instead of pretending it created something. -INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publisher, 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) -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) +INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, tags_search, asin, date_published, publisher, 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) +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) ON CONFLICT (library_id, file_path) DO NOTHING RETURNING *; @@ -313,6 +313,7 @@ UPDATE media_items SET scan_information = $36, summary = $37, metadata_overrides = $38, + audio_metadata = $39, updated_at = NOW() WHERE id = $1 RETURNING *; @@ -1338,6 +1339,7 @@ SELECT rp.context_text, rp.chapter, rp.chapter_progress, + rp.audio_offset_seconds, rp.viewport_x, rp.viewport_y, rp.zoom_level, @@ -1383,10 +1385,11 @@ INSERT INTO reading_progress ( last_sync_timestamp, current_page, total_pages, + audio_offset_seconds, last_read_at ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, NOW(), $18, $19, NOW() + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, NOW(), $18, $19, $20, NOW() ) ON CONFLICT (media_item_id, user_id) DO UPDATE SET @@ -1408,6 +1411,7 @@ DO UPDATE SET last_sync_timestamp = EXCLUDED.last_sync_timestamp, current_page = EXCLUDED.current_page, total_pages = EXCLUDED.total_pages, + audio_offset_seconds = EXCLUDED.audio_offset_seconds, last_read_at = NOW() RETURNING *; diff --git a/internal/handlers/media.go b/internal/handlers/media.go index 9ca6f93b..fedf2540 100644 --- a/internal/handlers/media.go +++ b/internal/handlers/media.go @@ -1237,7 +1237,6 @@ func (mh *MediaHandler) UpdateMediaReadingProgress(c *echo.Context) error { } } - var req struct { CurrentPage *int32 `json:"current_page"` TotalPages *int32 `json:"total_pages"` @@ -1251,6 +1250,11 @@ func (mh *MediaHandler) UpdateMediaReadingProgress(c *echo.Context) error { ZoomLevel *float64 `json:"zoom_level"` ScrollX *float64 `json:"scroll_position_x"` ScrollY *float64 `json:"scroll_position_y"` + // Audiobook locator: seconds into the current chapter + AudioOffsetSeconds *int `json:"audio_offset_seconds"` + // Honest wall-clock playing seconds since the last push (audio + // pushes fire while playing; pauses don't count) + ElapsedSeconds *int32 `json:"elapsed_seconds"` } if err := c.Bind(&req); err != nil { return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"}) @@ -1258,25 +1262,27 @@ func (mh *MediaHandler) UpdateMediaReadingProgress(c *echo.Context) error { if mh.progressSvc != nil { saveReq := wsync.SaveProgressRequest{ - MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true}, - UserID: pgtype.UUID{Bytes: userUUID, Valid: true}, - Source: deviceSource, - DeviceID: deviceID, - DeviceType: deviceType, - DeviceName: deviceLabel, - Percentage: req.Percentage, - Epubcfi: req.Epubcfi, - ContextText: req.ContextText, - CharacterOffset: req.CharacterOffset, - Chapter: req.Chapter, - ChapterProgress: req.ChapterProgress, - CurrentPage: nil, - TotalPages: nil, - ZoomLevel: req.ZoomLevel, - ScrollX: req.ScrollX, - ScrollY: req.ScrollY, - ReadingMode: req.ReadingMode, - Broadcast: true, + MediaItemID: pgtype.UUID{Bytes: mediaUUID, Valid: true}, + UserID: pgtype.UUID{Bytes: userUUID, Valid: true}, + Source: deviceSource, + DeviceID: deviceID, + DeviceType: deviceType, + DeviceName: deviceLabel, + Percentage: req.Percentage, + Epubcfi: req.Epubcfi, + ContextText: req.ContextText, + CharacterOffset: req.CharacterOffset, + Chapter: req.Chapter, + ChapterProgress: req.ChapterProgress, + AudioOffsetSeconds: req.AudioOffsetSeconds, + AudioElapsedSeconds: req.ElapsedSeconds, + CurrentPage: nil, + TotalPages: nil, + ZoomLevel: req.ZoomLevel, + ScrollX: req.ScrollX, + ScrollY: req.ScrollY, + ReadingMode: req.ReadingMode, + Broadcast: true, } if req.CurrentPage != nil { cp := int(*req.CurrentPage) @@ -2759,3 +2765,35 @@ func (mh *MediaHandler) saveCoverImage(c echo.Context, mediaUUID uuid.UUID, file return coverRelPath, nil } + +// GetAudioChapters handles GET /api/media-items/:id/chapters — the +// format-abstraction chapter list for audiobooks (media_items +// .audio_metadata). Embedded m4b chapters and folder-as-book files write +// the same shape. Empty list when the item isn't audio or hasn't been +// probed. +func (mh *MediaHandler) GetAudioChapters(c *echo.Context) error { + mediaID := c.Param("id") + mediaUUID, err := uuid.Parse(mediaID) + if err != nil { + return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"}) + } + + item, err := mh.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: mediaUUID, Valid: true}) + if err != nil { + return c.JSON(http.StatusNotFound, map[string]string{"error": "book not found"}) + } + + var payload services.AudioMetadataPayload + if len(item.AudioMetadata) > 0 { + if err := json.Unmarshal(item.AudioMetadata, &payload); err != nil { + return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to parse audio metadata"}) + } + } + + return c.JSON(http.StatusOK, map[string]interface{}{ + "media_item_id": item.ID, + "chapters": payload.Chapters, + "duration_seconds": payload.DurationSeconds, + "codec_profile": payload.CodecProfile, + }) +} diff --git a/internal/router/media.go b/internal/router/media.go index 62bea48b..cdf29716 100644 --- a/internal/router/media.go +++ b/internal/router/media.go @@ -18,6 +18,10 @@ func registerMediaRoutes(cfg *Config) { // Book download endpoint - same ServeFile flow as /uploads/library-:id/* // (JWT + library-visibility gated), addressed by media item ID. protected.GET("/media-items/:id/download", cfg.MediaHandler.ServeFile) + // The format-abstraction chapter list (audio_metadata). Streaming + // itself rides the existing /uploads Range-serving path with cookie + // auth — no separate endpoint. + protected.GET("/media-items/:id/chapters", cfg.MediaHandler.GetAudioChapters) // Media rating routes (all authenticated users) protected.POST("/media-items/:id/rating", cfg.MediaHandler.CreateMediaRating) diff --git a/internal/router/reader.go b/internal/router/reader.go index 2f73509c..78f113ee 100644 --- a/internal/router/reader.go +++ b/internal/router/reader.go @@ -83,7 +83,10 @@ func registerReaderRoutes(cfg *Config) { MediaItemID: mediaUUID.String(), Title: mediaItem.Title, Author: textToString(mediaItem.Author), - CoverImagePath: textToString(mediaItem.CoverImagePath), + // Sidecar covers store a raw library-relative path; resolve to + // the cookie-authed /uploads URL (idempotent for items whose + // stored value is already a servable path). + CoverImagePath: utils.ResolveMediaURL(mediaItem.LibraryID, mediaItem.CoverImagePath), LibraryType: mediaItem.FormatGroup, MimeType: textToString(mediaItem.MimeType), FilePath: mediaItem.FilePath, @@ -100,7 +103,13 @@ func registerReaderRoutes(cfg *Config) { } // Render template var buf bytes.Buffer - err = templates.Reader(user, metadata).Render(c.Request().Context(), &buf) + // Audiobooks get the audio player page — the foliate document + // engine is deliberately not involved (handoff item 3 Batch A). + if mediaItem.FormatGroup == "audiobook" { + err = templates.AudioReader(user, metadata).Render(c.Request().Context(), &buf) + } else { + err = templates.Reader(user, metadata).Render(c.Request().Context(), &buf) + } if err != nil { return renderErrorPage(c, "Error rendering reader", "render_error") } diff --git a/internal/router/router.go b/internal/router/router.go index fe67ddd2..fef890a9 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -12,6 +12,7 @@ import ( "bytes" "context" "log" + "mime" "net/http" "strconv" "strings" @@ -209,6 +210,13 @@ func wantsHTML(header http.Header) bool { // RegisterRoutes registers all application routes func RegisterRoutes(cfg *Config) *handlers.Handler { + // Go's builtin mime table doesn't know the audiobook containers; the + // /uploads file serving and any ServeContent path needs real audio + // content types or media elements refuse the stream. + mime.AddExtensionType(".m4b", "audio/mp4") + mime.AddExtensionType(".m4a", "audio/mp4") + mime.AddExtensionType(".mp3", "audio/mpeg") + mime.AddExtensionType(".flac", "audio/flac") e := cfg.Echo // Set up validator diff --git a/internal/services/library_service.go b/internal/services/library_service.go index 9bd4cc65..b7b91691 100644 --- a/internal/services/library_service.go +++ b/internal/services/library_service.go @@ -24,15 +24,20 @@ func NewLibraryService(db *database.Queries) *LibraryService { // Library type definitions and file extensions const ( - LibraryTypeEbooks = "ebooks" - LibraryTypeComics = "comics" - LibraryTypeManga = "manga" + LibraryTypeEbooks = "ebooks" + LibraryTypeComics = "comics" + LibraryTypeManga = "manga" + LibraryTypeAudiobooks = "audiobooks" ) var AllowedExtensions = map[string][]string{ LibraryTypeEbooks: {".epub", ".pdf", ".mobi", ".azw", ".azw3", ".txt", ".rtf", ".docx", ".fb2", ".pdb"}, LibraryTypeComics: {".cbz", ".cbr", ".cb7", ".cbt", ".epub", ".pdf"}, LibraryTypeManga: {".cbz", ".cbr", ".epub", ".pdf", ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".avif", ".tiff", ".tif"}, + // All four decided breadth formats registered from day one (the + // DOC/LIT doctrine: extensions are a deliberate list); only m4b + // scans richly in Batch A — the chapter abstraction carries the rest. + LibraryTypeAudiobooks: {".m4b", ".m4a", ".mp3", ".flac"}, } var MimeTypes = map[string]string{ @@ -62,6 +67,12 @@ var MimeTypes = map[string]string{ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", ".fb2": "application/x-fictionbook+xml", ".pdb": "application/vnd.palm", + // Audiobooks — audio/mp4 for the MP4-family containers is what + // every platform decoder expects from an