Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf2c2825ac | ||
|
|
cd119a74da | ||
|
|
14445a7c3f | ||
|
|
e6aceae0da | ||
|
|
44b98f3fc3 | ||
|
|
61681aac23 |
@@ -35,5 +35,9 @@ DBPASS=your-secure-database-password-here
|
|||||||
# BOOKHOARD_CONVERSION_TOOL=/usr/bin/ebook-convert
|
# BOOKHOARD_CONVERSION_TOOL=/usr/bin/ebook-convert
|
||||||
# Conversion Cache TTL: Override default 24h
|
# Conversion Cache TTL: Override default 24h
|
||||||
# BOOKHOARD_CONVERSION_CACHE_TTL=48h
|
# BOOKHOARD_CONVERSION_CACHE_TTL=48h
|
||||||
|
# Archive Retention: days to keep archived items (files missing from disk for
|
||||||
|
# 2+ scans) before they are purged, deleting their reading history with them.
|
||||||
|
# Set 0 to keep archived items until purged manually on the library admin page.
|
||||||
|
# ARCHIVE_RETENTION_DAYS=90
|
||||||
# System timezone (fallback for server-side time operations, defaults to UTC)
|
# System timezone (fallback for server-side time operations, defaults to UTC)
|
||||||
# TZ=America/New_York
|
# TZ=America/New_York
|
||||||
@@ -213,6 +213,13 @@ CREATE TABLE IF NOT EXISTS media_items (
|
|||||||
-- reset-to-scanned-defaults action clears them.
|
-- reset-to-scanned-defaults action clears them.
|
||||||
metadata_overrides TEXT[] NOT NULL DEFAULT '{}',
|
metadata_overrides TEXT[] NOT NULL DEFAULT '{}',
|
||||||
|
|
||||||
|
-- Archive lifecycle: a file missing from disk for two consecutive scans
|
||||||
|
-- gets archived_at set (hidden from all browsing, reading history kept).
|
||||||
|
-- If the file returns, the row is un-archived. Archived rows are purged
|
||||||
|
-- after ARCHIVE_RETENTION_DAYS (0 = manual purge only).
|
||||||
|
missing_scan_count INT NOT NULL DEFAULT 0,
|
||||||
|
archived_at TIMESTAMPTZ,
|
||||||
|
|
||||||
-- Chapter metadata for reader navigation and progress tracking
|
-- Chapter metadata for reader navigation and progress tracking
|
||||||
-- Caches detected chapter structure to avoid re-parsing files
|
-- Caches detected chapter structure to avoid re-parsing files
|
||||||
-- Populated by ReaderService.DetectChapters() on first read
|
-- Populated by ReaderService.DetectChapters() on first read
|
||||||
@@ -243,6 +250,11 @@ ALTER TABLE media_items ADD COLUMN IF NOT EXISTS contributors_search TEXT[];
|
|||||||
-- definition above for fresh databases)
|
-- definition above for fresh databases)
|
||||||
ALTER TABLE media_items ADD COLUMN IF NOT EXISTS metadata_overrides TEXT[] NOT NULL DEFAULT '{}';
|
ALTER TABLE media_items ADD COLUMN IF NOT EXISTS metadata_overrides TEXT[] NOT NULL DEFAULT '{}';
|
||||||
|
|
||||||
|
-- Archive lifecycle (idempotent backfill for existing installs)
|
||||||
|
ALTER TABLE media_items ADD COLUMN IF NOT EXISTS missing_scan_count INT NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE media_items ADD COLUMN IF NOT EXISTS archived_at TIMESTAMPTZ;
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_media_items_archived ON media_items (archived_at) WHERE archived_at IS NOT NULL;
|
||||||
|
|
||||||
-- Create GIN indexes for fast search field searches
|
-- Create GIN indexes for fast search field searches
|
||||||
CREATE INDEX IF NOT EXISTS idx_media_items_tags_search ON media_items USING GIN (tags_search);
|
CREATE INDEX IF NOT EXISTS idx_media_items_tags_search ON media_items USING GIN (tags_search);
|
||||||
CREATE INDEX IF NOT EXISTS idx_media_items_contributors_search ON media_items USING GIN (contributors_search);
|
CREATE INDEX IF NOT EXISTS idx_media_items_contributors_search ON media_items USING GIN (contributors_search);
|
||||||
|
|||||||
@@ -61,6 +61,12 @@ services:
|
|||||||
BOOKHOARD_CONVERSION_TOOL: ${BOOKHOARD_CONVERSION_TOOL:-/usr/bin/kepubify}
|
BOOKHOARD_CONVERSION_TOOL: ${BOOKHOARD_CONVERSION_TOOL:-/usr/bin/kepubify}
|
||||||
BOOKHOARD_CONVERSION_CACHE_TTL: ${BOOKHOARD_CONVERSION_CACHE_TTL:-24h}
|
BOOKHOARD_CONVERSION_CACHE_TTL: ${BOOKHOARD_CONVERSION_CACHE_TTL:-24h}
|
||||||
|
|
||||||
|
# Library Maintenance
|
||||||
|
# Days an archived item (file missing from disk for 2+ scans) is kept,
|
||||||
|
# with its reading history, before library scans purge it for good.
|
||||||
|
# Set 0 to keep archived items until purged manually on the library admin page.
|
||||||
|
ARCHIVE_RETENTION_DAYS: ${ARCHIVE_RETENTION_DAYS:-90}
|
||||||
|
|
||||||
# System timezone (fallback for server-side time operations)
|
# System timezone (fallback for server-side time operations)
|
||||||
TZ: ${TZ:-UTC}
|
TZ: ${TZ:-UTC}
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -84,3 +84,14 @@ func getEnvInt(key string, defaultValue int) int {
|
|||||||
}
|
}
|
||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ArchiveRetentionDays returns how many days a media item stays archived
|
||||||
|
// (file missing from disk for two consecutive scans) before library scans
|
||||||
|
// purge it for good. Reading progress, notes, and highlights survive the
|
||||||
|
// archive window and are restored if the file returns; the purge deletes
|
||||||
|
// them along with the row.
|
||||||
|
// Configure via ARCHIVE_RETENTION_DAYS (default 90); 0 keeps archived items
|
||||||
|
// until an admin purges them manually from the library admin page.
|
||||||
|
func ArchiveRetentionDays() int {
|
||||||
|
return getEnvInt("ARCHIVE_RETENTION_DAYS", 90)
|
||||||
|
}
|
||||||
|
|||||||
+12
-10
@@ -305,16 +305,18 @@ type MediaItems struct {
|
|||||||
// Scan information from ComicInfo.xml (scanner group, resolution, etc.)
|
// Scan information from ComicInfo.xml (scanner group, resolution, etc.)
|
||||||
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
|
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
|
||||||
// Summary from ComicInfo.xml (may be merged with description from Calibre)
|
// Summary from ComicInfo.xml (may be merged with description from Calibre)
|
||||||
Summary pgtype.Text `db:"summary" json:"summary"`
|
Summary pgtype.Text `db:"summary" json:"summary"`
|
||||||
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
|
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
|
||||||
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
|
MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"`
|
||||||
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
|
ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"`
|
||||||
TagsSearch []string `db:"tags_search" json:"tags_search"`
|
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
|
||||||
ContributorsSearch []string `db:"contributors_search" json:"contributors_search"`
|
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
|
||||||
FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"`
|
TagsSearch []string `db:"tags_search" json:"tags_search"`
|
||||||
OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"`
|
ContributorsSearch []string `db:"contributors_search" json:"contributors_search"`
|
||||||
OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"`
|
FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"`
|
||||||
HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"`
|
OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"`
|
||||||
|
OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"`
|
||||||
|
HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MediaNotes struct {
|
type MediaNotes struct {
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ type Querier interface {
|
|||||||
AddBookToKoboShelf(ctx context.Context, arg AddBookToKoboShelfParams) (KoboShelves, error)
|
AddBookToKoboShelf(ctx context.Context, arg AddBookToKoboShelfParams) (KoboShelves, error)
|
||||||
// Library Folders queries
|
// Library Folders queries
|
||||||
AddLibraryFolder(ctx context.Context, arg AddLibraryFolderParams) (LibraryFolders, error)
|
AddLibraryFolder(ctx context.Context, arg AddLibraryFolderParams) (LibraryFolders, error)
|
||||||
|
// Second consecutive missing scan: hide the item from all browsing while
|
||||||
|
// preserving reading history in case the file returns.
|
||||||
|
ArchiveMediaItem(ctx context.Context, id pgtype.UUID) error
|
||||||
// Bulk update format group for all media items
|
// Bulk update format group for all media items
|
||||||
BulkUpdateFormatGroups(ctx context.Context) error
|
BulkUpdateFormatGroups(ctx context.Context) error
|
||||||
BulkUpdateProgressFromSync(ctx context.Context, arg BulkUpdateProgressFromSyncParams) ([]interface{}, error)
|
BulkUpdateProgressFromSync(ctx context.Context, arg BulkUpdateProgressFromSyncParams) ([]interface{}, error)
|
||||||
@@ -30,10 +33,14 @@ type Querier interface {
|
|||||||
ClearDeviceSyncQueue(ctx context.Context, deviceID pgtype.UUID) error
|
ClearDeviceSyncQueue(ctx context.Context, deviceID pgtype.UUID) error
|
||||||
ClearKoboShelf(ctx context.Context, deviceID pgtype.UUID) error
|
ClearKoboShelf(ctx context.Context, deviceID pgtype.UUID) error
|
||||||
ClearKoboShelfByName(ctx context.Context, arg ClearKoboShelfByNameParams) error
|
ClearKoboShelfByName(ctx context.Context, arg ClearKoboShelfByNameParams) error
|
||||||
|
// File is back on disk (by path or content hash): restore visibility and
|
||||||
|
// reset the missing-scan counter. No-op for items that were never archived.
|
||||||
|
ClearMediaItemArchive(ctx context.Context, id pgtype.UUID) error
|
||||||
// Reset an item to scanned defaults: clears per-field user overrides so the
|
// Reset an item to scanned defaults: clears per-field user overrides so the
|
||||||
// next rescan can freely overwrite user-customized metadata.
|
// next rescan can freely overwrite user-customized metadata.
|
||||||
ClearMediaItemMetadataOverrides(ctx context.Context, id pgtype.UUID) error
|
ClearMediaItemMetadataOverrides(ctx context.Context, id pgtype.UUID) error
|
||||||
CountAdmins(ctx context.Context) (int64, error)
|
CountAdmins(ctx context.Context) (int64, error)
|
||||||
|
CountArchivedMediaItems(ctx context.Context) (int64, error)
|
||||||
// Count unlinked books for a device
|
// Count unlinked books for a device
|
||||||
CountUnlinkedBooks(ctx context.Context, deviceID pgtype.UUID) (int64, error)
|
CountUnlinkedBooks(ctx context.Context, deviceID pgtype.UUID) (int64, error)
|
||||||
CountUserDevices(ctx context.Context, userID pgtype.UUID) (int64, error)
|
CountUserDevices(ctx context.Context, userID pgtype.UUID) (int64, error)
|
||||||
@@ -343,6 +350,7 @@ type Querier interface {
|
|||||||
ListLibraries(ctx context.Context) ([]ListLibrariesRow, error)
|
ListLibraries(ctx context.Context) ([]ListLibrariesRow, error)
|
||||||
ListMediaItems(ctx context.Context, arg ListMediaItemsParams) ([]ListMediaItemsRow, error)
|
ListMediaItems(ctx context.Context, arg ListMediaItemsParams) ([]ListMediaItemsRow, error)
|
||||||
ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype.UUID) ([]ListMediaItemsByLibraryRow, error)
|
ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype.UUID) ([]ListMediaItemsByLibraryRow, error)
|
||||||
|
ListMediaItemsByLibraryIncludingArchived(ctx context.Context, libraryID pgtype.UUID) ([]ListMediaItemsByLibraryIncludingArchivedRow, error)
|
||||||
// List all media items sharing a SHA-256 hash within a library (hash conflict group)
|
// List all media items sharing a SHA-256 hash within a library (hash conflict group)
|
||||||
ListMediaItemsBySHA256AndLibrary(ctx context.Context, arg ListMediaItemsBySHA256AndLibraryParams) ([]MediaItems, error)
|
ListMediaItemsBySHA256AndLibrary(ctx context.Context, arg ListMediaItemsBySHA256AndLibraryParams) ([]MediaItems, error)
|
||||||
// List media items that have no stored SHA-256 (imported before hashing existed)
|
// List media items that have no stored SHA-256 (imported before hashing existed)
|
||||||
@@ -356,6 +364,13 @@ type Querier interface {
|
|||||||
// List unresolved unlinked books with pagination
|
// List unresolved unlinked books with pagination
|
||||||
ListUnresolvedUnlinkedBooks(ctx context.Context, arg ListUnresolvedUnlinkedBooksParams) ([]ListUnresolvedUnlinkedBooksRow, error)
|
ListUnresolvedUnlinkedBooks(ctx context.Context, arg ListUnresolvedUnlinkedBooksParams) ([]ListUnresolvedUnlinkedBooksRow, error)
|
||||||
ListUsers(ctx context.Context) ([]ListUsersRow, error)
|
ListUsers(ctx context.Context) ([]ListUsersRow, error)
|
||||||
|
// First consecutive scan that cannot find the file on disk.
|
||||||
|
MarkMediaItemMissing(ctx context.Context, id pgtype.UUID) error
|
||||||
|
// Manual bulk purge from the library admin page.
|
||||||
|
PurgeAllArchivedMediaItems(ctx context.Context) ([]PurgeAllArchivedMediaItemsRow, error)
|
||||||
|
// Retention sweep at library-scan time: hard-delete archived items older
|
||||||
|
// than the cutoff. Cascades remove reading history with the row.
|
||||||
|
PurgeExpiredArchivedMediaItems(ctx context.Context, archivedAt pgtype.Timestamptz) ([]PurgeExpiredArchivedMediaItemsRow, error)
|
||||||
PurgeExpiredBookmarkTombstones(ctx context.Context, deletedAt pgtype.Timestamptz) error
|
PurgeExpiredBookmarkTombstones(ctx context.Context, deletedAt pgtype.Timestamptz) error
|
||||||
PurgeExpiredHighlightTombstones(ctx context.Context, deletedAt pgtype.Timestamptz) error
|
PurgeExpiredHighlightTombstones(ctx context.Context, deletedAt pgtype.Timestamptz) error
|
||||||
PurgeExpiredNoteTombstones(ctx context.Context, deletedAt pgtype.Timestamptz) error
|
PurgeExpiredNoteTombstones(ctx context.Context, deletedAt pgtype.Timestamptz) error
|
||||||
|
|||||||
@@ -107,6 +107,18 @@ func (q *Queries) AddLibraryFolder(ctx context.Context, arg AddLibraryFolderPara
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ArchiveMediaItem = `-- name: ArchiveMediaItem :exec
|
||||||
|
UPDATE media_items SET archived_at = NOW(), updated_at = NOW()
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
// Second consecutive missing scan: hide the item from all browsing while
|
||||||
|
// preserving reading history in case the file returns.
|
||||||
|
func (q *Queries) ArchiveMediaItem(ctx context.Context, id pgtype.UUID) error {
|
||||||
|
_, err := q.db.Exec(ctx, ArchiveMediaItem, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
const BulkUpdateFormatGroups = `-- name: BulkUpdateFormatGroups :exec
|
const BulkUpdateFormatGroups = `-- name: BulkUpdateFormatGroups :exec
|
||||||
UPDATE media_items m
|
UPDATE media_items m
|
||||||
SET
|
SET
|
||||||
@@ -226,6 +238,18 @@ func (q *Queries) ClearKoboShelfByName(ctx context.Context, arg ClearKoboShelfBy
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ClearMediaItemArchive = `-- name: ClearMediaItemArchive :exec
|
||||||
|
UPDATE media_items SET archived_at = NULL, missing_scan_count = 0, updated_at = NOW()
|
||||||
|
WHERE id = $1 AND (archived_at IS NOT NULL OR missing_scan_count > 0)
|
||||||
|
`
|
||||||
|
|
||||||
|
// File is back on disk (by path or content hash): restore visibility and
|
||||||
|
// reset the missing-scan counter. No-op for items that were never archived.
|
||||||
|
func (q *Queries) ClearMediaItemArchive(ctx context.Context, id pgtype.UUID) error {
|
||||||
|
_, err := q.db.Exec(ctx, ClearMediaItemArchive, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
const ClearMediaItemMetadataOverrides = `-- name: ClearMediaItemMetadataOverrides :exec
|
const ClearMediaItemMetadataOverrides = `-- name: ClearMediaItemMetadataOverrides :exec
|
||||||
UPDATE media_items SET metadata_overrides = '{}', updated_at = NOW()
|
UPDATE media_items SET metadata_overrides = '{}', updated_at = NOW()
|
||||||
WHERE id = $1
|
WHERE id = $1
|
||||||
@@ -249,6 +273,17 @@ func (q *Queries) CountAdmins(ctx context.Context) (int64, error) {
|
|||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CountArchivedMediaItems = `-- name: CountArchivedMediaItems :one
|
||||||
|
SELECT COUNT(*) FROM media_items WHERE archived_at IS NOT NULL
|
||||||
|
`
|
||||||
|
|
||||||
|
func (q *Queries) CountArchivedMediaItems(ctx context.Context) (int64, error) {
|
||||||
|
row := q.db.QueryRow(ctx, CountArchivedMediaItems)
|
||||||
|
var count int64
|
||||||
|
err := row.Scan(&count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
const CountUnlinkedBooks = `-- name: CountUnlinkedBooks :one
|
const CountUnlinkedBooks = `-- name: CountUnlinkedBooks :one
|
||||||
SELECT COUNT(*) as count
|
SELECT COUNT(*) as count
|
||||||
FROM unlinked_books
|
FROM unlinked_books
|
||||||
@@ -912,7 +947,7 @@ 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)
|
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)
|
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)
|
||||||
ON CONFLICT (library_id, file_path) DO UPDATE SET updated_at = NOW()
|
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, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateMediaItemParams struct {
|
type CreateMediaItemParams struct {
|
||||||
@@ -1066,6 +1101,8 @@ func (q *Queries) CreateMediaItem(ctx context.Context, arg CreateMediaItemParams
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -2460,7 +2497,7 @@ func (q *Queries) GetAnnotationsForBook(ctx context.Context, arg GetAnnotationsF
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GetBooksByTag = `-- name: GetBooksByTag :many
|
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, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items
|
||||||
WHERE library_id = $1 AND tags @> ARRAY[$2::text]
|
WHERE library_id = $1 AND tags @> ARRAY[$2::text]
|
||||||
ORDER BY title ASC
|
ORDER BY title ASC
|
||||||
`
|
`
|
||||||
@@ -2534,6 +2571,8 @@ func (q *Queries) GetBooksByTag(ctx context.Context, arg GetBooksByTagParams) ([
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -2633,7 +2672,7 @@ func (q *Queries) GetCollectionItems(ctx context.Context, collectionID pgtype.UU
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GetCollectionItemsForDashboard = `-- name: GetCollectionItemsForDashboard :many
|
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.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.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
|
INNER JOIN collection_items ci ON ci.media_item_id = mi.id
|
||||||
WHERE ci.collection_id = $1
|
WHERE ci.collection_id = $1
|
||||||
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid)
|
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid)
|
||||||
@@ -2702,6 +2741,8 @@ type GetCollectionItemsForDashboardRow struct {
|
|||||||
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
|
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
|
||||||
Summary pgtype.Text `db:"summary" json:"summary"`
|
Summary pgtype.Text `db:"summary" json:"summary"`
|
||||||
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
|
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
|
||||||
|
MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"`
|
||||||
|
ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"`
|
||||||
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
|
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
|
||||||
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
|
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
|
||||||
TagsSearch []string `db:"tags_search" json:"tags_search"`
|
TagsSearch []string `db:"tags_search" json:"tags_search"`
|
||||||
@@ -2777,6 +2818,8 @@ func (q *Queries) GetCollectionItemsForDashboard(ctx context.Context, arg GetCol
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -2929,7 +2972,7 @@ func (q *Queries) GetCollectionsForBook(ctx context.Context, mediaItemID pgtype.
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GetContinueReadingItems = `-- name: GetContinueReadingItems :many
|
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.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.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi
|
||||||
INNER JOIN (
|
INNER JOIN (
|
||||||
SELECT DISTINCT ON (media_item_id) media_item_id, last_read_at
|
SELECT DISTINCT ON (media_item_id) media_item_id, last_read_at
|
||||||
FROM reading_progress
|
FROM reading_progress
|
||||||
@@ -3014,6 +3057,8 @@ func (q *Queries) GetContinueReadingItems(ctx context.Context, arg GetContinueRe
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -3047,15 +3092,16 @@ WITH user_series_progress AS (
|
|||||||
GROUP BY mi.series
|
GROUP BY mi.series
|
||||||
),
|
),
|
||||||
next_books AS (
|
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.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.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence,
|
||||||
usp.last_read_at
|
usp.last_read_at
|
||||||
FROM media_items mi
|
FROM media_items mi
|
||||||
JOIN user_series_progress usp ON mi.series = usp.series
|
JOIN user_series_progress usp ON mi.series = usp.series
|
||||||
WHERE ($3::uuid IS NULL OR mi.library_id = $3::uuid)
|
WHERE mi.archived_at IS NULL
|
||||||
|
AND ($3::uuid IS NULL OR mi.library_id = $3::uuid)
|
||||||
AND (mi.series_number > usp.max_read_number OR usp.max_read_number IS NULL)
|
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
|
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, 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, 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
|
ORDER BY last_read_at DESC NULLS LAST
|
||||||
LIMIT $1
|
LIMIT $1
|
||||||
`
|
`
|
||||||
@@ -3121,6 +3167,8 @@ type GetContinueSeriesItemsRow struct {
|
|||||||
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
|
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
|
||||||
Summary pgtype.Text `db:"summary" json:"summary"`
|
Summary pgtype.Text `db:"summary" json:"summary"`
|
||||||
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
|
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
|
||||||
|
MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"`
|
||||||
|
ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"`
|
||||||
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
|
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
|
||||||
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
|
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
|
||||||
TagsSearch []string `db:"tags_search" json:"tags_search"`
|
TagsSearch []string `db:"tags_search" json:"tags_search"`
|
||||||
@@ -3196,6 +3244,8 @@ func (q *Queries) GetContinueSeriesItems(ctx context.Context, arg GetContinueSer
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -4325,7 +4375,7 @@ func (q *Queries) GetLibraryFolders(ctx context.Context, libraryID pgtype.UUID)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GetLibraryItems = `-- name: GetLibraryItems :many
|
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.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.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)
|
WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid)
|
||||||
ORDER BY mi.created_at DESC
|
ORDER BY mi.created_at DESC
|
||||||
`
|
`
|
||||||
@@ -4394,6 +4444,8 @@ func (q *Queries) GetLibraryItems(ctx context.Context, libraryID pgtype.UUID) ([
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -4825,7 +4877,7 @@ func (q *Queries) GetMediaHighlights(ctx context.Context, arg GetMediaHighlights
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GetMediaItem = `-- name: GetMediaItem :one
|
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, 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, 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) {
|
func (q *Queries) GetMediaItem(ctx context.Context, id pgtype.UUID) (MediaItems, error) {
|
||||||
@@ -4886,6 +4938,8 @@ func (q *Queries) GetMediaItem(ctx context.Context, id pgtype.UUID) (MediaItems,
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -4899,7 +4953,7 @@ func (q *Queries) GetMediaItem(ctx context.Context, id pgtype.UUID) (MediaItems,
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GetMediaItemByFilePath = `-- name: GetMediaItemByFilePath :one
|
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, 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, 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 {
|
type GetMediaItemByFilePathParams struct {
|
||||||
@@ -4965,6 +5019,8 @@ func (q *Queries) GetMediaItemByFilePath(ctx context.Context, arg GetMediaItemBy
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -4978,7 +5034,7 @@ func (q *Queries) GetMediaItemByFilePath(ctx context.Context, arg GetMediaItemBy
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GetMediaItemByFilePathAnyLibrary = `-- name: GetMediaItemByFilePathAnyLibrary :one
|
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, 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, 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) {
|
func (q *Queries) GetMediaItemByFilePathAnyLibrary(ctx context.Context, filePath string) (MediaItems, error) {
|
||||||
@@ -5039,6 +5095,8 @@ func (q *Queries) GetMediaItemByFilePathAnyLibrary(ctx context.Context, filePath
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -5053,7 +5111,7 @@ func (q *Queries) GetMediaItemByFilePathAnyLibrary(ctx context.Context, filePath
|
|||||||
|
|
||||||
const GetMediaItemByFilePathForSync = `-- name: GetMediaItemByFilePathForSync :one
|
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, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items WHERE file_path = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
// ============================================
|
// ============================================
|
||||||
@@ -5117,6 +5175,8 @@ func (q *Queries) GetMediaItemByFilePathForSync(ctx context.Context, filePath st
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -5130,7 +5190,7 @@ func (q *Queries) GetMediaItemByFilePathForSync(ctx context.Context, filePath st
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GetMediaItemByKoboContentId = `-- name: GetMediaItemByKoboContentId :one
|
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, 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, 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) {
|
func (q *Queries) GetMediaItemByKoboContentId(ctx context.Context, koboContentID pgtype.Text) (MediaItems, error) {
|
||||||
@@ -5191,6 +5251,8 @@ func (q *Queries) GetMediaItemByKoboContentId(ctx context.Context, koboContentID
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -5204,7 +5266,7 @@ func (q *Queries) GetMediaItemByKoboContentId(ctx context.Context, koboContentID
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GetMediaItemByOPFIdentifier = `-- name: GetMediaItemByOPFIdentifier :one
|
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, 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, 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
|
// Get media item by OPF identifier
|
||||||
@@ -5266,6 +5328,8 @@ func (q *Queries) GetMediaItemByOPFIdentifier(ctx context.Context, opfIdentifier
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -5279,7 +5343,7 @@ func (q *Queries) GetMediaItemByOPFIdentifier(ctx context.Context, opfIdentifier
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GetMediaItemByOPFUUID = `-- name: GetMediaItemByOPFUUID :one
|
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, 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, 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
|
// Get media item by OPF UUID
|
||||||
@@ -5341,6 +5405,8 @@ func (q *Queries) GetMediaItemByOPFUUID(ctx context.Context, opfUuid pgtype.Text
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -5354,7 +5420,7 @@ func (q *Queries) GetMediaItemByOPFUUID(ctx context.Context, opfUuid pgtype.Text
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GetMediaItemBySHA256 = `-- name: GetMediaItemBySHA256 :one
|
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, 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, 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
|
// Get media item by SHA-256 hash
|
||||||
@@ -5416,6 +5482,8 @@ func (q *Queries) GetMediaItemBySHA256(ctx context.Context, fileSha256 pgtype.Te
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -5429,7 +5497,7 @@ func (q *Queries) GetMediaItemBySHA256(ctx context.Context, fileSha256 pgtype.Te
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GetMediaItemBySHA256AndLibrary = `-- name: GetMediaItemBySHA256AndLibrary :one
|
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, 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, 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 {
|
type GetMediaItemBySHA256AndLibraryParams struct {
|
||||||
@@ -5496,6 +5564,8 @@ func (q *Queries) GetMediaItemBySHA256AndLibrary(ctx context.Context, arg GetMed
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -5836,7 +5906,7 @@ func (q *Queries) GetNextRetryTime(ctx context.Context) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GetNotStartedItems = `-- name: GetNotStartedItems :many
|
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.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.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)
|
WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid)
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM reading_progress rp
|
SELECT 1 FROM reading_progress rp
|
||||||
@@ -5918,6 +5988,8 @@ func (q *Queries) GetNotStartedItems(ctx context.Context, arg GetNotStartedItems
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -6237,7 +6309,7 @@ func (q *Queries) GetReadingSpeed(ctx context.Context, arg GetReadingSpeedParams
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GetRecentlyAddedItems = `-- name: GetRecentlyAddedItems :many
|
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.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.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)
|
WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid)
|
||||||
ORDER BY mi.imported_at DESC NULLS LAST, mi.created_at DESC
|
ORDER BY mi.imported_at DESC NULLS LAST, mi.created_at DESC
|
||||||
LIMIT $2
|
LIMIT $2
|
||||||
@@ -6312,6 +6384,8 @@ func (q *Queries) GetRecentlyAddedItems(ctx context.Context, arg GetRecentlyAdde
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -6332,7 +6406,7 @@ func (q *Queries) GetRecentlyAddedItems(ctx context.Context, arg GetRecentlyAdde
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GetRecentlyReadItems = `-- name: GetRecentlyReadItems :many
|
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.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.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi
|
||||||
INNER JOIN (
|
INNER JOIN (
|
||||||
SELECT DISTINCT ON (media_item_id) media_item_id, last_read_at
|
SELECT DISTINCT ON (media_item_id) media_item_id, last_read_at
|
||||||
FROM reading_progress
|
FROM reading_progress
|
||||||
@@ -6415,6 +6489,8 @@ func (q *Queries) GetRecentlyReadItems(ctx context.Context, arg GetRecentlyReadI
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -6535,7 +6611,7 @@ func (q *Queries) GetSavedFilters(ctx context.Context, arg GetSavedFiltersParams
|
|||||||
}
|
}
|
||||||
|
|
||||||
const GetSeriesBooks = `-- name: GetSeriesBooks :many
|
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, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence FROM media_items
|
||||||
WHERE series = $1
|
WHERE series = $1
|
||||||
ORDER BY series_number ASC NULLS LAST
|
ORDER BY series_number ASC NULLS LAST
|
||||||
`
|
`
|
||||||
@@ -6604,6 +6680,8 @@ func (q *Queries) GetSeriesBooks(ctx context.Context, series pgtype.Text) ([]Med
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -7805,7 +7883,7 @@ const GetVisibleLibraryMediaCounts = `-- name: GetVisibleLibraryMediaCounts :man
|
|||||||
SELECT l.id, COUNT(mi.id) as media_count
|
SELECT l.id, COUNT(mi.id) as media_count
|
||||||
FROM libraries l
|
FROM libraries l
|
||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
|
||||||
LEFT JOIN media_items mi ON mi.library_id = l.id
|
LEFT JOIN media_items mi ON mi.library_id = l.id AND mi.archived_at IS NULL
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
GROUP BY l.id
|
GROUP BY l.id
|
||||||
`
|
`
|
||||||
@@ -8387,10 +8465,11 @@ func (q *Queries) ListLibraries(ctx context.Context) ([]ListLibrariesRow, error)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ListMediaItems = `-- name: ListMediaItems :many
|
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.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.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
|
FROM media_items mi
|
||||||
JOIN libraries l ON mi.library_id = l.id
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
JOIN library_types lt ON l.library_type_id = lt.id
|
JOIN library_types lt ON l.library_type_id = lt.id
|
||||||
|
WHERE mi.archived_at IS NULL
|
||||||
ORDER BY mi.created_at DESC LIMIT $1 OFFSET $2
|
ORDER BY mi.created_at DESC LIMIT $1 OFFSET $2
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -8454,6 +8533,8 @@ type ListMediaItemsRow struct {
|
|||||||
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
|
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
|
||||||
Summary pgtype.Text `db:"summary" json:"summary"`
|
Summary pgtype.Text `db:"summary" json:"summary"`
|
||||||
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
|
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
|
||||||
|
MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"`
|
||||||
|
ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"`
|
||||||
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
|
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
|
||||||
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
|
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
|
||||||
TagsSearch []string `db:"tags_search" json:"tags_search"`
|
TagsSearch []string `db:"tags_search" json:"tags_search"`
|
||||||
@@ -8530,6 +8611,8 @@ func (q *Queries) ListMediaItems(ctx context.Context, arg ListMediaItemsParams)
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -8552,11 +8635,12 @@ func (q *Queries) ListMediaItems(ctx context.Context, arg ListMediaItemsParams)
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ListMediaItemsByLibrary = `-- name: ListMediaItemsByLibrary :many
|
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.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.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
|
FROM media_items mi
|
||||||
JOIN libraries l ON mi.library_id = l.id
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
JOIN library_types lt ON l.library_type_id = lt.id
|
JOIN library_types lt ON l.library_type_id = lt.id
|
||||||
WHERE mi.library_id = $1
|
WHERE mi.library_id = $1
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
ORDER BY mi.created_at DESC
|
ORDER BY mi.created_at DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -8615,6 +8699,8 @@ type ListMediaItemsByLibraryRow struct {
|
|||||||
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
|
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
|
||||||
Summary pgtype.Text `db:"summary" json:"summary"`
|
Summary pgtype.Text `db:"summary" json:"summary"`
|
||||||
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
|
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
|
||||||
|
MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"`
|
||||||
|
ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"`
|
||||||
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
|
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
|
||||||
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
|
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
|
||||||
TagsSearch []string `db:"tags_search" json:"tags_search"`
|
TagsSearch []string `db:"tags_search" json:"tags_search"`
|
||||||
@@ -8691,6 +8777,173 @@ func (q *Queries) ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype.
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
|
&i.ChapterMetadata,
|
||||||
|
&i.LibraryTypeName,
|
||||||
|
&i.TagsSearch,
|
||||||
|
&i.ContributorsSearch,
|
||||||
|
&i.FileSha256,
|
||||||
|
&i.OpfIdentifier,
|
||||||
|
&i.OpfUuid,
|
||||||
|
&i.HashConfidence,
|
||||||
|
&i.LibraryName,
|
||||||
|
&i.LibraryTypeName_2,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
FROM media_items mi
|
||||||
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
|
JOIN library_types lt ON l.library_type_id = lt.id
|
||||||
|
WHERE mi.library_id = $1
|
||||||
|
ORDER BY mi.created_at DESC
|
||||||
|
`
|
||||||
|
|
||||||
|
type ListMediaItemsByLibraryIncludingArchivedRow struct {
|
||||||
|
ID pgtype.UUID `db:"id" json:"id"`
|
||||||
|
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
|
||||||
|
Title string `db:"title" json:"title"`
|
||||||
|
Author pgtype.Text `db:"author" json:"author"`
|
||||||
|
Isbn pgtype.Text `db:"isbn" json:"isbn"`
|
||||||
|
Description pgtype.Text `db:"description" json:"description"`
|
||||||
|
FilePath string `db:"file_path" json:"file_path"`
|
||||||
|
FileSize pgtype.Int8 `db:"file_size" json:"file_size"`
|
||||||
|
MimeType pgtype.Text `db:"mime_type" json:"mime_type"`
|
||||||
|
CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"`
|
||||||
|
Series pgtype.Text `db:"series" json:"series"`
|
||||||
|
SeriesNumber pgtype.Int4 `db:"series_number" json:"series_number"`
|
||||||
|
Tags []string `db:"tags" json:"tags"`
|
||||||
|
Asin pgtype.Text `db:"asin" json:"asin"`
|
||||||
|
DatePublished pgtype.Date `db:"date_published" json:"date_published"`
|
||||||
|
Publisher pgtype.Text `db:"publisher" json:"publisher"`
|
||||||
|
Contributors []string `db:"contributors" json:"contributors"`
|
||||||
|
Language pgtype.Text `db:"language" json:"language"`
|
||||||
|
Edition pgtype.Text `db:"edition" json:"edition"`
|
||||||
|
PageCount pgtype.Int4 `db:"page_count" json:"page_count"`
|
||||||
|
Genre pgtype.Text `db:"genre" json:"genre"`
|
||||||
|
CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"`
|
||||||
|
GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"`
|
||||||
|
OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"`
|
||||||
|
GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"`
|
||||||
|
AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"`
|
||||||
|
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
|
||||||
|
ImportedAt pgtype.Timestamptz `db:"imported_at" json:"imported_at"`
|
||||||
|
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
|
||||||
|
FormatGroup string `db:"format_group" json:"format_group"`
|
||||||
|
FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"`
|
||||||
|
IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"`
|
||||||
|
HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"`
|
||||||
|
TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"`
|
||||||
|
ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"`
|
||||||
|
EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"`
|
||||||
|
RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"`
|
||||||
|
KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"`
|
||||||
|
KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"`
|
||||||
|
MangaType pgtype.Text `db:"manga_type" json:"manga_type"`
|
||||||
|
ReadingDirection pgtype.Text `db:"reading_direction" json:"reading_direction"`
|
||||||
|
SeriesCount pgtype.Int4 `db:"series_count" json:"series_count"`
|
||||||
|
Volume pgtype.Int4 `db:"volume" json:"volume"`
|
||||||
|
Imprint pgtype.Text `db:"imprint" json:"imprint"`
|
||||||
|
AgeRating pgtype.Text `db:"age_rating" json:"age_rating"`
|
||||||
|
WebUrl pgtype.Text `db:"web_url" json:"web_url"`
|
||||||
|
StoryArc pgtype.Text `db:"story_arc" json:"story_arc"`
|
||||||
|
IsBlackAndWhite pgtype.Bool `db:"is_black_and_white" json:"is_black_and_white"`
|
||||||
|
MetadataNotes pgtype.Text `db:"metadata_notes" json:"metadata_notes"`
|
||||||
|
CommunityRating pgtype.Float8 `db:"community_rating" json:"community_rating"`
|
||||||
|
AlternateInfo []byte `db:"alternate_info" json:"alternate_info"`
|
||||||
|
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
|
||||||
|
Summary pgtype.Text `db:"summary" json:"summary"`
|
||||||
|
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
|
||||||
|
MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"`
|
||||||
|
ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"`
|
||||||
|
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
|
||||||
|
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
|
||||||
|
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"`
|
||||||
|
OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"`
|
||||||
|
OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"`
|
||||||
|
HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"`
|
||||||
|
LibraryName string `db:"library_name" json:"library_name"`
|
||||||
|
LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *Queries) ListMediaItemsByLibraryIncludingArchived(ctx context.Context, libraryID pgtype.UUID) ([]ListMediaItemsByLibraryIncludingArchivedRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, ListMediaItemsByLibraryIncludingArchived, libraryID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []ListMediaItemsByLibraryIncludingArchivedRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i ListMediaItemsByLibraryIncludingArchivedRow
|
||||||
|
if err := rows.Scan(
|
||||||
|
&i.ID,
|
||||||
|
&i.LibraryID,
|
||||||
|
&i.Title,
|
||||||
|
&i.Author,
|
||||||
|
&i.Isbn,
|
||||||
|
&i.Description,
|
||||||
|
&i.FilePath,
|
||||||
|
&i.FileSize,
|
||||||
|
&i.MimeType,
|
||||||
|
&i.CoverImagePath,
|
||||||
|
&i.Series,
|
||||||
|
&i.SeriesNumber,
|
||||||
|
&i.Tags,
|
||||||
|
&i.Asin,
|
||||||
|
&i.DatePublished,
|
||||||
|
&i.Publisher,
|
||||||
|
&i.Contributors,
|
||||||
|
&i.Language,
|
||||||
|
&i.Edition,
|
||||||
|
&i.PageCount,
|
||||||
|
&i.Genre,
|
||||||
|
&i.CopyrightYear,
|
||||||
|
&i.GoodreadsID,
|
||||||
|
&i.OpenlibraryID,
|
||||||
|
&i.GoogleBooksID,
|
||||||
|
&i.AddedByAdminID,
|
||||||
|
&i.CreatedAt,
|
||||||
|
&i.ImportedAt,
|
||||||
|
&i.UpdatedAt,
|
||||||
|
&i.FormatGroup,
|
||||||
|
&i.FormatMimetype,
|
||||||
|
&i.IsReflowable,
|
||||||
|
&i.HasFixedLayout,
|
||||||
|
&i.TotalCharacters,
|
||||||
|
&i.ChapterCount,
|
||||||
|
&i.EntitlementID,
|
||||||
|
&i.RevisionNumber,
|
||||||
|
&i.KoboContentID,
|
||||||
|
&i.KoboMetadata,
|
||||||
|
&i.MangaType,
|
||||||
|
&i.ReadingDirection,
|
||||||
|
&i.SeriesCount,
|
||||||
|
&i.Volume,
|
||||||
|
&i.Imprint,
|
||||||
|
&i.AgeRating,
|
||||||
|
&i.WebUrl,
|
||||||
|
&i.StoryArc,
|
||||||
|
&i.IsBlackAndWhite,
|
||||||
|
&i.MetadataNotes,
|
||||||
|
&i.CommunityRating,
|
||||||
|
&i.AlternateInfo,
|
||||||
|
&i.ScanInformation,
|
||||||
|
&i.Summary,
|
||||||
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -8713,7 +8966,7 @@ func (q *Queries) ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype.
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ListMediaItemsBySHA256AndLibrary = `-- name: ListMediaItemsBySHA256AndLibrary :many
|
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, 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, 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 {
|
type ListMediaItemsBySHA256AndLibraryParams struct {
|
||||||
@@ -8786,6 +9039,8 @@ func (q *Queries) ListMediaItemsBySHA256AndLibrary(ctx context.Context, arg List
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -8806,7 +9061,7 @@ func (q *Queries) ListMediaItemsBySHA256AndLibrary(ctx context.Context, arg List
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ListMediaItemsMissingHash = `-- name: ListMediaItemsMissingHash :many
|
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, 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, 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)
|
// List media items that have no stored SHA-256 (imported before hashing existed)
|
||||||
@@ -8874,6 +9129,8 @@ func (q *Queries) ListMediaItemsMissingHash(ctx context.Context) ([]MediaItems,
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -8894,11 +9151,12 @@ func (q *Queries) ListMediaItemsMissingHash(ctx context.Context) ([]MediaItems,
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ListMediaItemsSorted = `-- name: ListMediaItemsSorted :many
|
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.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.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
|
FROM media_items mi
|
||||||
JOIN libraries l ON mi.library_id = l.id
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
JOIN library_types lt ON l.library_type_id = lt.id
|
JOIN library_types lt ON l.library_type_id = lt.id
|
||||||
WHERE mi.library_id = $1
|
WHERE mi.archived_at IS NULL
|
||||||
|
AND mi.library_id = $1
|
||||||
ORDER BY
|
ORDER BY
|
||||||
CASE
|
CASE
|
||||||
WHEN $2 = 'title ASC' THEN mi.title
|
WHEN $2 = 'title ASC' THEN mi.title
|
||||||
@@ -9030,6 +9288,8 @@ type ListMediaItemsSortedRow struct {
|
|||||||
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
|
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
|
||||||
Summary pgtype.Text `db:"summary" json:"summary"`
|
Summary pgtype.Text `db:"summary" json:"summary"`
|
||||||
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
|
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
|
||||||
|
MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"`
|
||||||
|
ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"`
|
||||||
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
|
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
|
||||||
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
|
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
|
||||||
TagsSearch []string `db:"tags_search" json:"tags_search"`
|
TagsSearch []string `db:"tags_search" json:"tags_search"`
|
||||||
@@ -9111,6 +9371,8 @@ func (q *Queries) ListMediaItemsSorted(ctx context.Context, arg ListMediaItemsSo
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -9532,6 +9794,82 @@ func (q *Queries) ListUsers(ctx context.Context) ([]ListUsersRow, error) {
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MarkMediaItemMissing = `-- name: MarkMediaItemMissing :exec
|
||||||
|
UPDATE media_items SET missing_scan_count = missing_scan_count + 1, updated_at = NOW()
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
// First consecutive scan that cannot find the file on disk.
|
||||||
|
func (q *Queries) MarkMediaItemMissing(ctx context.Context, id pgtype.UUID) error {
|
||||||
|
_, err := q.db.Exec(ctx, MarkMediaItemMissing, id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
const PurgeAllArchivedMediaItems = `-- name: PurgeAllArchivedMediaItems :many
|
||||||
|
DELETE FROM media_items
|
||||||
|
WHERE archived_at IS NOT NULL
|
||||||
|
RETURNING id, title
|
||||||
|
`
|
||||||
|
|
||||||
|
type PurgeAllArchivedMediaItemsRow struct {
|
||||||
|
ID pgtype.UUID `db:"id" json:"id"`
|
||||||
|
Title string `db:"title" json:"title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual bulk purge from the library admin page.
|
||||||
|
func (q *Queries) PurgeAllArchivedMediaItems(ctx context.Context) ([]PurgeAllArchivedMediaItemsRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, PurgeAllArchivedMediaItems)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []PurgeAllArchivedMediaItemsRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i PurgeAllArchivedMediaItemsRow
|
||||||
|
if err := rows.Scan(&i.ID, &i.Title); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
const PurgeExpiredArchivedMediaItems = `-- name: PurgeExpiredArchivedMediaItems :many
|
||||||
|
DELETE FROM media_items
|
||||||
|
WHERE archived_at IS NOT NULL AND archived_at < $1
|
||||||
|
RETURNING id, title
|
||||||
|
`
|
||||||
|
|
||||||
|
type PurgeExpiredArchivedMediaItemsRow struct {
|
||||||
|
ID pgtype.UUID `db:"id" json:"id"`
|
||||||
|
Title string `db:"title" json:"title"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retention sweep at library-scan time: hard-delete archived items older
|
||||||
|
// than the cutoff. Cascades remove reading history with the row.
|
||||||
|
func (q *Queries) PurgeExpiredArchivedMediaItems(ctx context.Context, archivedAt pgtype.Timestamptz) ([]PurgeExpiredArchivedMediaItemsRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, PurgeExpiredArchivedMediaItems, archivedAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []PurgeExpiredArchivedMediaItemsRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i PurgeExpiredArchivedMediaItemsRow
|
||||||
|
if err := rows.Scan(&i.ID, &i.Title); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, i)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
const PurgeExpiredBookmarkTombstones = `-- name: PurgeExpiredBookmarkTombstones :exec
|
const PurgeExpiredBookmarkTombstones = `-- name: PurgeExpiredBookmarkTombstones :exec
|
||||||
DELETE FROM media_bookmarks WHERE deleted = TRUE AND deleted_at < $1
|
DELETE FROM media_bookmarks WHERE deleted = TRUE AND deleted_at < $1
|
||||||
`
|
`
|
||||||
@@ -10068,6 +10406,7 @@ FROM media_items mi
|
|||||||
JOIN libraries l ON mi.library_id = l.id
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
AND mi.library_id = $3
|
AND mi.library_id = $3
|
||||||
AND word_similarity($1, COALESCE(mi.author, '')) > 0.3
|
AND word_similarity($1, COALESCE(mi.author, '')) > 0.3
|
||||||
AND mi.author IS NOT NULL
|
AND mi.author IS NOT NULL
|
||||||
@@ -10126,6 +10465,7 @@ FROM media_items mi
|
|||||||
JOIN libraries l ON mi.library_id = l.id
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
AND mi.library_id = $3
|
AND mi.library_id = $3
|
||||||
AND word_similarity($1, COALESCE(mi.genre, '')) > 0.3
|
AND word_similarity($1, COALESCE(mi.genre, '')) > 0.3
|
||||||
AND mi.genre IS NOT NULL
|
AND mi.genre IS NOT NULL
|
||||||
@@ -10184,6 +10524,7 @@ FROM media_items mi
|
|||||||
JOIN libraries l ON mi.library_id = l.id
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
AND mi.library_id = $3
|
AND mi.library_id = $3
|
||||||
AND word_similarity($1, COALESCE(mi.language, '')) > 0.3
|
AND word_similarity($1, COALESCE(mi.language, '')) > 0.3
|
||||||
AND mi.language IS NOT NULL
|
AND mi.language IS NOT NULL
|
||||||
@@ -10234,12 +10575,13 @@ func (q *Queries) SearchLanguageValues(ctx context.Context, arg SearchLanguageVa
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SearchMediaItems = `-- name: SearchMediaItems :many
|
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.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.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
|
FROM media_items mi
|
||||||
JOIN libraries l ON mi.library_id = l.id
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
JOIN library_types lt ON l.library_type_id = lt.id
|
JOIN library_types lt ON l.library_type_id = lt.id
|
||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid)
|
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid)
|
||||||
AND (
|
AND (
|
||||||
mi.title ILIKE $3 OR
|
mi.title ILIKE $3 OR
|
||||||
@@ -10323,6 +10665,8 @@ type SearchMediaItemsRow struct {
|
|||||||
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
|
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
|
||||||
Summary pgtype.Text `db:"summary" json:"summary"`
|
Summary pgtype.Text `db:"summary" json:"summary"`
|
||||||
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
|
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
|
||||||
|
MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"`
|
||||||
|
ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"`
|
||||||
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
|
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
|
||||||
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
|
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
|
||||||
TagsSearch []string `db:"tags_search" json:"tags_search"`
|
TagsSearch []string `db:"tags_search" json:"tags_search"`
|
||||||
@@ -10406,6 +10750,8 @@ func (q *Queries) SearchMediaItems(ctx context.Context, arg SearchMediaItemsPara
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -10480,6 +10826,7 @@ JOIN libraries l ON mi.library_id = l.id
|
|||||||
JOIN library_types lt ON l.library_type_id = lt.id
|
JOIN library_types lt ON l.library_type_id = lt.id
|
||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
AND ($2::uuid IS NULL
|
AND ($2::uuid IS NULL
|
||||||
OR mi.library_id = $2::uuid)
|
OR mi.library_id = $2::uuid)
|
||||||
-- Fuzzy author filter
|
-- Fuzzy author filter
|
||||||
@@ -10764,6 +11111,7 @@ FROM media_items mi
|
|||||||
JOIN libraries l ON mi.library_id = l.id
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $2
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
AND mi.library_id = $3
|
AND mi.library_id = $3
|
||||||
AND word_similarity($1, COALESCE(mi.series, '')) > 0.3
|
AND word_similarity($1, COALESCE(mi.series, '')) > 0.3
|
||||||
AND mi.series IS NOT NULL
|
AND mi.series IS NOT NULL
|
||||||
@@ -11880,7 +12228,7 @@ UPDATE media_items SET
|
|||||||
metadata_overrides = $38,
|
metadata_overrides = $38,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $1
|
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, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateMediaItemParams struct {
|
type UpdateMediaItemParams struct {
|
||||||
@@ -12021,6 +12369,8 @@ func (q *Queries) UpdateMediaItem(ctx context.Context, arg UpdateMediaItemParams
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -12037,7 +12387,7 @@ const UpdateMediaItemChapterMetadata = `-- name: UpdateMediaItemChapterMetadata
|
|||||||
UPDATE media_items
|
UPDATE media_items
|
||||||
SET chapter_metadata = $2, updated_at = NOW()
|
SET chapter_metadata = $2, updated_at = NOW()
|
||||||
WHERE id = $1
|
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, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateMediaItemChapterMetadataParams struct {
|
type UpdateMediaItemChapterMetadataParams struct {
|
||||||
@@ -12103,6 +12453,8 @@ func (q *Queries) UpdateMediaItemChapterMetadata(ctx context.Context, arg Update
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -12210,7 +12562,7 @@ SET
|
|||||||
hash_confidence = $5,
|
hash_confidence = $5,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $1
|
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, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateMediaItemIdentifiersParams struct {
|
type UpdateMediaItemIdentifiersParams struct {
|
||||||
@@ -12290,6 +12642,8 @@ func (q *Queries) UpdateMediaItemIdentifiers(ctx context.Context, arg UpdateMedi
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
@@ -12310,7 +12664,7 @@ SET entitlement_id = $2,
|
|||||||
kobo_metadata = $5,
|
kobo_metadata = $5,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = $1
|
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, 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, tags_search, contributors_search, file_sha256, opf_identifier, opf_uuid, hash_confidence
|
||||||
`
|
`
|
||||||
|
|
||||||
type UpdateMediaItemKoboMetadataParams struct {
|
type UpdateMediaItemKoboMetadataParams struct {
|
||||||
@@ -12385,6 +12739,8 @@ func (q *Queries) UpdateMediaItemKoboMetadata(ctx context.Context, arg UpdateMed
|
|||||||
&i.ScanInformation,
|
&i.ScanInformation,
|
||||||
&i.Summary,
|
&i.Summary,
|
||||||
&i.MetadataOverrides,
|
&i.MetadataOverrides,
|
||||||
|
&i.MissingScanCount,
|
||||||
|
&i.ArchivedAt,
|
||||||
&i.ChapterMetadata,
|
&i.ChapterMetadata,
|
||||||
&i.LibraryTypeName,
|
&i.LibraryTypeName,
|
||||||
&i.TagsSearch,
|
&i.TagsSearch,
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ ORDER BY l.created_at ASC;
|
|||||||
SELECT l.id, COUNT(mi.id) as media_count
|
SELECT l.id, COUNT(mi.id) as media_count
|
||||||
FROM libraries l
|
FROM libraries l
|
||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1
|
||||||
LEFT JOIN media_items mi ON mi.library_id = l.id
|
LEFT JOIN media_items mi ON mi.library_id = l.id AND mi.archived_at IS NULL
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
GROUP BY l.id;
|
GROUP BY l.id;
|
||||||
|
|
||||||
@@ -160,6 +160,7 @@ SELECT mi.*, l.name as library_name, lt.name as library_type_name
|
|||||||
FROM media_items mi
|
FROM media_items mi
|
||||||
JOIN libraries l ON mi.library_id = l.id
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
JOIN library_types lt ON l.library_type_id = lt.id
|
JOIN library_types lt ON l.library_type_id = lt.id
|
||||||
|
WHERE mi.archived_at IS NULL
|
||||||
ORDER BY mi.created_at DESC LIMIT $1 OFFSET $2;
|
ORDER BY mi.created_at DESC LIMIT $1 OFFSET $2;
|
||||||
|
|
||||||
-- name: ListMediaItemsByLibrary :many
|
-- name: ListMediaItemsByLibrary :many
|
||||||
@@ -167,6 +168,15 @@ SELECT mi.*, l.name as library_name, lt.name as library_type_name
|
|||||||
FROM media_items mi
|
FROM media_items mi
|
||||||
JOIN libraries l ON mi.library_id = l.id
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
JOIN library_types lt ON l.library_type_id = lt.id
|
JOIN library_types lt ON l.library_type_id = lt.id
|
||||||
|
WHERE mi.library_id = $1
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
|
ORDER BY mi.created_at DESC;
|
||||||
|
|
||||||
|
-- name: ListMediaItemsByLibraryIncludingArchived :many
|
||||||
|
SELECT mi.*, 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
|
||||||
WHERE mi.library_id = $1
|
WHERE mi.library_id = $1
|
||||||
ORDER BY mi.created_at DESC;
|
ORDER BY mi.created_at DESC;
|
||||||
|
|
||||||
@@ -175,7 +185,8 @@ SELECT mi.*, l.name as library_name, lt.name as library_type_name
|
|||||||
FROM media_items mi
|
FROM media_items mi
|
||||||
JOIN libraries l ON mi.library_id = l.id
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
JOIN library_types lt ON l.library_type_id = lt.id
|
JOIN library_types lt ON l.library_type_id = lt.id
|
||||||
WHERE mi.library_id = sqlc.narg('library_id')
|
WHERE mi.archived_at IS NULL
|
||||||
|
AND mi.library_id = sqlc.narg('library_id')
|
||||||
ORDER BY
|
ORDER BY
|
||||||
CASE
|
CASE
|
||||||
WHEN sqlc.narg('sort') = 'title ASC' THEN mi.title
|
WHEN sqlc.narg('sort') = 'title ASC' THEN mi.title
|
||||||
@@ -293,6 +304,39 @@ RETURNING *;
|
|||||||
UPDATE media_items SET metadata_overrides = '{}', updated_at = NOW()
|
UPDATE media_items SET metadata_overrides = '{}', updated_at = NOW()
|
||||||
WHERE id = $1;
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: MarkMediaItemMissing :exec
|
||||||
|
-- First consecutive scan that cannot find the file on disk.
|
||||||
|
UPDATE media_items SET missing_scan_count = missing_scan_count + 1, updated_at = NOW()
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: ArchiveMediaItem :exec
|
||||||
|
-- Second consecutive missing scan: hide the item from all browsing while
|
||||||
|
-- preserving reading history in case the file returns.
|
||||||
|
UPDATE media_items SET archived_at = NOW(), updated_at = NOW()
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: ClearMediaItemArchive :exec
|
||||||
|
-- File is back on disk (by path or content hash): restore visibility and
|
||||||
|
-- reset the missing-scan counter. No-op for items that were never archived.
|
||||||
|
UPDATE media_items SET archived_at = NULL, missing_scan_count = 0, updated_at = NOW()
|
||||||
|
WHERE id = $1 AND (archived_at IS NOT NULL OR missing_scan_count > 0);
|
||||||
|
|
||||||
|
-- name: PurgeExpiredArchivedMediaItems :many
|
||||||
|
-- Retention sweep at library-scan time: hard-delete archived items older
|
||||||
|
-- than the cutoff. Cascades remove reading history with the row.
|
||||||
|
DELETE FROM media_items
|
||||||
|
WHERE archived_at IS NOT NULL AND archived_at < $1
|
||||||
|
RETURNING id, title;
|
||||||
|
|
||||||
|
-- name: PurgeAllArchivedMediaItems :many
|
||||||
|
-- Manual bulk purge from the library admin page.
|
||||||
|
DELETE FROM media_items
|
||||||
|
WHERE archived_at IS NOT NULL
|
||||||
|
RETURNING id, title;
|
||||||
|
|
||||||
|
-- name: CountArchivedMediaItems :one
|
||||||
|
SELECT COUNT(*) FROM media_items WHERE archived_at IS NOT NULL;
|
||||||
|
|
||||||
-- name: DeleteMediaItem :exec
|
-- name: DeleteMediaItem :exec
|
||||||
DELETE FROM media_items WHERE id = $1;
|
DELETE FROM media_items WHERE id = $1;
|
||||||
|
|
||||||
@@ -441,6 +485,7 @@ JOIN libraries l ON mi.library_id = l.id
|
|||||||
JOIN library_types lt ON l.library_type_id = lt.id
|
JOIN library_types lt ON l.library_type_id = lt.id
|
||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
AND (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
AND (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
||||||
AND (
|
AND (
|
||||||
mi.title ILIKE sqlc.narg('search_pattern') OR
|
mi.title ILIKE sqlc.narg('search_pattern') OR
|
||||||
@@ -513,6 +558,7 @@ JOIN libraries l ON mi.library_id = l.id
|
|||||||
JOIN library_types lt ON l.library_type_id = lt.id
|
JOIN library_types lt ON l.library_type_id = lt.id
|
||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
AND (sqlc.narg('library_id')::uuid IS NULL
|
AND (sqlc.narg('library_id')::uuid IS NULL
|
||||||
OR mi.library_id = sqlc.narg('library_id')::uuid)
|
OR mi.library_id = sqlc.narg('library_id')::uuid)
|
||||||
-- Fuzzy author filter
|
-- Fuzzy author filter
|
||||||
@@ -646,6 +692,7 @@ FROM media_items mi
|
|||||||
JOIN libraries l ON mi.library_id = l.id
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
AND mi.library_id = sqlc.narg('library_id')
|
AND mi.library_id = sqlc.narg('library_id')
|
||||||
AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.author, '')) > 0.3
|
AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.author, '')) > 0.3
|
||||||
AND mi.author IS NOT NULL
|
AND mi.author IS NOT NULL
|
||||||
@@ -663,6 +710,7 @@ FROM media_items mi
|
|||||||
JOIN libraries l ON mi.library_id = l.id
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
AND mi.library_id = sqlc.narg('library_id')
|
AND mi.library_id = sqlc.narg('library_id')
|
||||||
AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.genre, '')) > 0.3
|
AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.genre, '')) > 0.3
|
||||||
AND mi.genre IS NOT NULL
|
AND mi.genre IS NOT NULL
|
||||||
@@ -697,6 +745,7 @@ FROM media_items mi
|
|||||||
JOIN libraries l ON mi.library_id = l.id
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
AND mi.library_id = sqlc.narg('library_id')
|
AND mi.library_id = sqlc.narg('library_id')
|
||||||
AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.series, '')) > 0.3
|
AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.series, '')) > 0.3
|
||||||
AND mi.series IS NOT NULL
|
AND mi.series IS NOT NULL
|
||||||
@@ -714,6 +763,7 @@ FROM media_items mi
|
|||||||
JOIN libraries l ON mi.library_id = l.id
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id')
|
||||||
WHERE COALESCE(lv.is_visible, true) = true
|
WHERE COALESCE(lv.is_visible, true) = true
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
AND mi.library_id = sqlc.narg('library_id')
|
AND mi.library_id = sqlc.narg('library_id')
|
||||||
AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.language, '')) > 0.3
|
AND word_similarity(sqlc.narg('search_query'), COALESCE(mi.language, '')) > 0.3
|
||||||
AND mi.language IS NOT NULL
|
AND mi.language IS NOT NULL
|
||||||
@@ -2461,7 +2511,8 @@ next_books AS (
|
|||||||
usp.last_read_at
|
usp.last_read_at
|
||||||
FROM media_items mi
|
FROM media_items mi
|
||||||
JOIN user_series_progress usp ON mi.series = usp.series
|
JOIN user_series_progress usp ON mi.series = usp.series
|
||||||
WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
WHERE mi.archived_at IS NULL
|
||||||
|
AND (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
||||||
AND (mi.series_number > usp.max_read_number OR usp.max_read_number IS NULL)
|
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
|
ORDER BY mi.series, mi.series_number ASC NULLS LAST
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1330,6 +1330,27 @@ func (mh *MediaHandler) UpdateMediaItem(c *echo.Context) error {
|
|||||||
return c.JSON(http.StatusOK, item)
|
return c.JSON(http.StatusOK, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PurgeArchivedMediaItems handles POST /api/media-items/purge-archived
|
||||||
|
// (admin only). Hard-deletes every archived item (files missing from disk for
|
||||||
|
// 2+ scans) together with its reading history. The archive retention window
|
||||||
|
// eventually does the same automatically; this is the manual bulk escape hatch.
|
||||||
|
func (mh *MediaHandler) PurgeArchivedMediaItems(c *echo.Context) error {
|
||||||
|
user := MustGetAuthenticatedUser(c)
|
||||||
|
|
||||||
|
if user.Role != "admin" {
|
||||||
|
return c.JSON(http.StatusForbidden, map[string]string{"error": "admin access required"})
|
||||||
|
}
|
||||||
|
|
||||||
|
purged, err := mh.db.PurgeAllArchivedMediaItems(c.Request().Context())
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||||
|
"purged": len(purged),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// RescanMediaItem handles POST /api/media-items/:id/rescan (admin only)
|
// RescanMediaItem handles POST /api/media-items/:id/rescan (admin only)
|
||||||
func (mh *MediaHandler) RescanMediaItem(c *echo.Context) error {
|
func (mh *MediaHandler) RescanMediaItem(c *echo.Context) error {
|
||||||
user := MustGetAuthenticatedUser(c)
|
user := MustGetAuthenticatedUser(c)
|
||||||
|
|||||||
@@ -884,7 +884,8 @@ func registerFrontendRoutes(cfg *Config) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
err = templates.AdminLibrary(user, libData, userData).Render(c.Request().Context(), &buf)
|
archivedCount, _ := cfg.Queries.CountArchivedMediaItems(c.Request().Context())
|
||||||
|
err = templates.AdminLibrary(user, libData, userData, archivedCount).Render(c.Request().Context(), &buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ func registerMediaRoutes(cfg *Config) {
|
|||||||
admin.PUT("/media-items/:id", cfg.MediaHandler.UpdateMediaItem)
|
admin.PUT("/media-items/:id", cfg.MediaHandler.UpdateMediaItem)
|
||||||
admin.POST("/media-items/:id/rescan", cfg.MediaHandler.RescanMediaItem)
|
admin.POST("/media-items/:id/rescan", cfg.MediaHandler.RescanMediaItem)
|
||||||
admin.DELETE("/media-items/:id", cfg.MediaHandler.DeleteMediaItem)
|
admin.DELETE("/media-items/:id", cfg.MediaHandler.DeleteMediaItem)
|
||||||
|
admin.POST("/media-items/purge-archived", cfg.MediaHandler.PurgeArchivedMediaItems)
|
||||||
|
|
||||||
// Shelf management (protected)
|
// Shelf management (protected)
|
||||||
protected.POST("/devices/:id/shelves", cfg.MediaHandler.AddToShelf)
|
protected.POST("/devices/:id/shelves", cfg.MediaHandler.AddToShelf)
|
||||||
|
|||||||
+287
-165
@@ -5,6 +5,7 @@ package services
|
|||||||
import (
|
import (
|
||||||
"archive/tar"
|
"archive/tar"
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
|
"bookhoard/internal/config"
|
||||||
"bookhoard/internal/database"
|
"bookhoard/internal/database"
|
||||||
"bookhoard/internal/utils"
|
"bookhoard/internal/utils"
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -105,22 +106,23 @@ type FormatInfo struct {
|
|||||||
|
|
||||||
// MediaScanner scans library folders for media files (ebooks, comics, manga)
|
// MediaScanner scans library folders for media files (ebooks, comics, manga)
|
||||||
type MediaScanner struct {
|
type MediaScanner struct {
|
||||||
db *database.Queries
|
db *database.Queries
|
||||||
watcher *fsnotify.Watcher
|
watcher *fsnotify.Watcher
|
||||||
folders []string
|
folders []string
|
||||||
adminID pgtype.UUID
|
adminID pgtype.UUID
|
||||||
defaultLibraryID pgtype.UUID
|
defaultLibraryID pgtype.UUID
|
||||||
libraryTypes map[string][]string
|
libraryTypes map[string][]string
|
||||||
forceRescan bool
|
forceRescan bool
|
||||||
logger *ScannerLogger
|
archiveRetentionDays int
|
||||||
dirtyDirs map[string]time.Time
|
logger *ScannerLogger
|
||||||
dirtyDirsMu sync.RWMutex
|
dirtyDirs map[string]time.Time
|
||||||
fileStability map[string]*atomic.Bool
|
dirtyDirsMu sync.RWMutex
|
||||||
fileStabilityMu sync.RWMutex
|
fileStability map[string]*atomic.Bool
|
||||||
scanMutex sync.Mutex
|
fileStabilityMu sync.RWMutex
|
||||||
scanInProgress atomic.Bool
|
scanMutex sync.Mutex
|
||||||
watching atomic.Bool
|
scanInProgress atomic.Bool
|
||||||
settingsCache *SettingsCache
|
watching atomic.Bool
|
||||||
|
settingsCache *SettingsCache
|
||||||
|
|
||||||
totalFiles int
|
totalFiles int
|
||||||
newItems int
|
newItems int
|
||||||
@@ -157,18 +159,19 @@ type CalibreOPFMetadata struct {
|
|||||||
// never closed.
|
// never closed.
|
||||||
func NewMediaScanner(db *database.Queries) *MediaScanner {
|
func NewMediaScanner(db *database.Queries) *MediaScanner {
|
||||||
return &MediaScanner{
|
return &MediaScanner{
|
||||||
db: db,
|
db: db,
|
||||||
watcher: nil,
|
watcher: nil,
|
||||||
settingsCache: NewSettingsCache(30 * time.Second),
|
archiveRetentionDays: config.ArchiveRetentionDays(),
|
||||||
dirtyDirs: make(map[string]time.Time),
|
settingsCache: NewSettingsCache(30 * time.Second),
|
||||||
fileStability: make(map[string]*atomic.Bool),
|
dirtyDirs: make(map[string]time.Time),
|
||||||
watching: atomic.Bool{},
|
fileStability: make(map[string]*atomic.Bool),
|
||||||
scanInProgress: atomic.Bool{},
|
watching: atomic.Bool{},
|
||||||
folders: []string{},
|
scanInProgress: atomic.Bool{},
|
||||||
adminID: pgtype.UUID{},
|
folders: []string{},
|
||||||
defaultLibraryID: pgtype.UUID{Valid: false},
|
adminID: pgtype.UUID{},
|
||||||
libraryTypes: make(map[string][]string),
|
defaultLibraryID: pgtype.UUID{Valid: false},
|
||||||
logger: NewScannerLogger(),
|
libraryTypes: make(map[string][]string),
|
||||||
|
logger: NewScannerLogger(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,17 +479,23 @@ func (s *MediaScanner) ScanFolders(ctx context.Context) error {
|
|||||||
fmt.Printf("Scan completed: %d total files scanned, %d media files found, %d new items, %d errors\n",
|
fmt.Printf("Scan completed: %d total files scanned, %d media files found, %d new items, %d errors\n",
|
||||||
processedFiles, mediaFiles, s.newItems, s.errors)
|
processedFiles, mediaFiles, s.newItems, s.errors)
|
||||||
|
|
||||||
// Clean up: Find media items in DB that no longer exist on filesystem
|
// Archive lifecycle pass. Items whose files vanished from disk are
|
||||||
|
// archived after two consecutive missing scans (reading history kept,
|
||||||
|
// item hidden), and purged for good once archived older than the
|
||||||
|
// retention window (ARCHIVE_RETENTION_DAYS; 0 = manual purge only).
|
||||||
|
// Every branch logs - the previous hard-delete cleanup failed silently
|
||||||
|
// and left orphaned rows undetected.
|
||||||
for _, folder := range s.folders {
|
for _, folder := range s.folders {
|
||||||
lib, err := s.db.GetLibraryByFolder(ctx, folder)
|
lib, err := s.db.GetLibraryByFolder(ctx, folder)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
fmt.Printf("[ARCHIVE] Warning: no library found for folder %s, skipping archive pass: %v\n", folder, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
libraryID := lib.LibraryID
|
libraryID := lib.LibraryID
|
||||||
|
|
||||||
dbItems, err := s.db.ListMediaItemsByLibrary(ctx, libraryID)
|
dbItems, err := s.db.ListMediaItemsByLibraryIncludingArchived(ctx, libraryID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Warning: failed to get library items for cleanup: %v\n", err)
|
fmt.Printf("[ARCHIVE] Warning: failed to get library items for archive pass: %v\n", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,28 +510,46 @@ func (s *MediaScanner) ScanFolders(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
fmt.Printf("[RESCAN-CLEANUP] Warning: failed to walk directory %s, skipping orphan cleanup: %v\n", folder, err)
|
fmt.Printf("[ARCHIVE] Warning: failed to walk directory %s, skipping archive pass: %v\n", folder, err)
|
||||||
continue // Skip to next folder to avoid false deletions
|
continue // Skip to next folder to avoid false archivals
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete items whose files no longer exist - with safety logging
|
|
||||||
for _, item := range dbItems {
|
for _, item := range dbItems {
|
||||||
filePath := item.FilePath
|
if item.FilePath == "" || scannedPaths[item.FilePath] {
|
||||||
if filePath != "" && !scannedPaths[filePath] {
|
continue // File present; unarchive is handled in processMediaFile
|
||||||
msg := fmt.Sprintf("[RESCAN-CLEANUP] Orphaned media item found: ID=%s, Title=%s, Path=%s",
|
}
|
||||||
item.ID, item.Title, filePath)
|
|
||||||
s.logger.LogDelete(msg)
|
|
||||||
|
|
||||||
delMsg := fmt.Sprintf("[RESCAN-CLEANUP] Deleting orphaned item '%s' (file no longer exists at %s)",
|
if item.ArchivedAt.Valid {
|
||||||
item.Title, filePath)
|
// Still missing and already archived: purge once past the
|
||||||
s.logger.LogDelete(delMsg)
|
// retention window (0 = keep until manual purge).
|
||||||
|
if s.archiveRetentionDays > 0 && time.Now().AddDate(0, 0, -s.archiveRetentionDays).After(item.ArchivedAt.Time) {
|
||||||
|
if err := s.db.DeleteMediaItem(ctx, item.ID); err != nil {
|
||||||
|
fmt.Printf("[ARCHIVE] Error: failed to purge archived item %s: %v\n", item.Title, err)
|
||||||
|
s.logger.LogError(fmt.Sprintf("[ARCHIVE] ERROR: failed to purge archived item '%s': %v", item.Title, err))
|
||||||
|
} else {
|
||||||
|
fmt.Printf("[ARCHIVE] Purged archived item '%s' (retention %d days): %s\n", item.Title, s.archiveRetentionDays, item.FilePath)
|
||||||
|
s.logger.LogDelete(fmt.Sprintf("[ARCHIVE] Purged archived item '%s' after retention window (file missing at %s)", item.Title, item.FilePath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.db.DeleteMediaItem(ctx, item.ID); err != nil {
|
// Missing but not yet archived.
|
||||||
errMsg := fmt.Sprintf("[RESCAN-CLEANUP] ERROR: failed to delete orphaned item %s: %v", item.Title, err)
|
if item.MissingScanCount >= 1 {
|
||||||
s.logger.LogDelete(errMsg)
|
// Second consecutive missing scan: archive it.
|
||||||
s.logger.LogError(errMsg)
|
if err := s.db.ArchiveMediaItem(ctx, item.ID); err != nil {
|
||||||
|
fmt.Printf("[ARCHIVE] Error: failed to archive item %s: %v\n", item.Title, err)
|
||||||
|
s.logger.LogError(fmt.Sprintf("[ARCHIVE] ERROR: failed to archive item '%s': %v", item.Title, err))
|
||||||
} else {
|
} else {
|
||||||
s.logger.LogDelete(fmt.Sprintf("[RESCAN-CLEANUP] SUCCESS: deleted orphaned item '%s'", item.Title))
|
fmt.Printf("[ARCHIVE] Archived item '%s' (missing from disk for %d scans): %s\n", item.Title, item.MissingScanCount+1, item.FilePath)
|
||||||
|
s.logger.LogDelete(fmt.Sprintf("[ARCHIVE] Archived item '%s' (file missing from disk at %s)", item.Title, item.FilePath))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// First missing scan: mark, archive on the next one.
|
||||||
|
if err := s.db.MarkMediaItemMissing(ctx, item.ID); err != nil {
|
||||||
|
fmt.Printf("[ARCHIVE] Warning: failed to mark item missing %s: %v\n", item.Title, err)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("[ARCHIVE] Item missing from disk (1/2 scans before archiving): %s\n", item.FilePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -693,6 +720,17 @@ func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool,
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
fmt.Printf("Media item already exists in database: %s (size: %d vs %d)\n", path, existingItem.FileSize.Int64, info.Size())
|
fmt.Printf("Media item already exists in database: %s (size: %d vs %d)\n", path, existingItem.FileSize.Int64, info.Size())
|
||||||
|
|
||||||
|
// The file is back on disk: lift any archive/missing state so the
|
||||||
|
// item reappears in libraries and future missing scans start fresh.
|
||||||
|
if existingItem.ArchivedAt.Valid || existingItem.MissingScanCount > 0 {
|
||||||
|
if err := s.db.ClearMediaItemArchive(ctx, existingItem.ID); err != nil {
|
||||||
|
fmt.Printf("Warning: failed to unarchive media item %s: %v\n", existingItem.FilePath, err)
|
||||||
|
} else if existingItem.ArchivedAt.Valid {
|
||||||
|
fmt.Printf("[ARCHIVE] Restored from archive, file is back: %s\n", existingItem.FilePath)
|
||||||
|
s.logger.LogDelete(fmt.Sprintf("[ARCHIVE] Restored archived item '%s' (file returned at %s)", existingItem.Title, existingItem.FilePath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If force rescan is enabled, always re-process
|
// If force rescan is enabled, always re-process
|
||||||
if s.forceRescan {
|
if s.forceRescan {
|
||||||
fmt.Printf("Force rescan enabled, updating existing media item: %s\n", path)
|
fmt.Printf("Force rescan enabled, updating existing media item: %s\n", path)
|
||||||
@@ -757,6 +795,15 @@ func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool,
|
|||||||
if err == nil && existingByHash.ID.Valid {
|
if err == nil && existingByHash.ID.Valid {
|
||||||
fmt.Printf("Media item with same SHA-256 already exists in library (path %q), skipping duplicate: %s\n",
|
fmt.Printf("Media item with same SHA-256 already exists in library (path %q), skipping duplicate: %s\n",
|
||||||
existingByHash.FilePath, path)
|
existingByHash.FilePath, path)
|
||||||
|
// Content returned (possibly at a new path): restore archived rows.
|
||||||
|
if existingByHash.ArchivedAt.Valid || existingByHash.MissingScanCount > 0 {
|
||||||
|
if err := s.db.ClearMediaItemArchive(ctx, existingByHash.ID); err != nil {
|
||||||
|
fmt.Printf("Warning: failed to unarchive media item %s: %v\n", existingByHash.FilePath, err)
|
||||||
|
} else if existingByHash.ArchivedAt.Valid {
|
||||||
|
fmt.Printf("[ARCHIVE] Restored from archive, identical content found at %s\n", path)
|
||||||
|
s.logger.LogDelete(fmt.Sprintf("[ARCHIVE] Restored archived item '%s' (identical content found at %s)", existingByHash.Title, path))
|
||||||
|
}
|
||||||
|
}
|
||||||
if s.forceRescan {
|
if s.forceRescan {
|
||||||
_ = s.updateMediaItem(ctx, existingByHash, path)
|
_ = s.updateMediaItem(ctx, existingByHash, path)
|
||||||
}
|
}
|
||||||
@@ -958,6 +1005,88 @@ func (s *MediaScanner) extractCalibreSidecar(path string) *MediaMetadata {
|
|||||||
return metadata
|
return metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractAudiobookshelfSidecar checks for and parses an Audiobookshelf-style
|
||||||
|
// metadata.json sidecar next to the media file. Only fields with a matching
|
||||||
|
// media_items column are mapped; narrators, subtitle, explicit, abridged and
|
||||||
|
// chapters are deliberately skipped. Returns nil when no sidecar exists.
|
||||||
|
func extractAudiobookshelfSidecar(path string) *MediaMetadata {
|
||||||
|
jsonPath := filepath.Join(filepath.Dir(path), "metadata.json")
|
||||||
|
if _, err := os.Stat(jsonPath); os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(jsonPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Warning: failed to read metadata.json sidecar for %s: %v\n", path, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var sidecar struct {
|
||||||
|
Title string `json:"title"`
|
||||||
|
Authors []string `json:"authors"`
|
||||||
|
Series []struct {
|
||||||
|
Series string `json:"series"`
|
||||||
|
Sequence string `json:"sequence"`
|
||||||
|
} `json:"series"`
|
||||||
|
Genres []string `json:"genres"`
|
||||||
|
Tags []string `json:"tags"`
|
||||||
|
PublishedYear *int `json:"publishedYear"`
|
||||||
|
PublishedDate *string `json:"publishedDate"`
|
||||||
|
Publisher *string `json:"publisher"`
|
||||||
|
Description *string `json:"description"`
|
||||||
|
ISBN *string `json:"isbn"`
|
||||||
|
ASIN *string `json:"asin"`
|
||||||
|
Language *string `json:"language"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &sidecar); err != nil {
|
||||||
|
fmt.Printf("Warning: failed to parse metadata.json sidecar for %s: %v\n", path, err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata := &MediaMetadata{
|
||||||
|
Title: strings.TrimSpace(sidecar.Title),
|
||||||
|
}
|
||||||
|
if len(sidecar.Authors) > 0 {
|
||||||
|
metadata.Author = strings.TrimSpace(sidecar.Authors[0])
|
||||||
|
}
|
||||||
|
if len(sidecar.Series) > 0 {
|
||||||
|
metadata.Series = strings.TrimSpace(sidecar.Series[0].Series)
|
||||||
|
if index, err := strconv.ParseFloat(strings.TrimSpace(sidecar.Series[0].Sequence), 32); err == nil {
|
||||||
|
metadata.SeriesNumber = int32(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tags := append(append([]string{}, sidecar.Genres...), sidecar.Tags...); len(tags) > 0 {
|
||||||
|
metadata.Tags = utils.NormalizeTags(tags)
|
||||||
|
}
|
||||||
|
if sidecar.PublishedDate != nil {
|
||||||
|
if date, err := time.Parse("2006-01-02", strings.TrimSpace(*sidecar.PublishedDate)); err == nil {
|
||||||
|
metadata.PublishDate = date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if metadata.PublishDate.IsZero() && sidecar.PublishedYear != nil && *sidecar.PublishedYear > 0 {
|
||||||
|
metadata.PublishDate = time.Date(*sidecar.PublishedYear, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
}
|
||||||
|
if sidecar.Publisher != nil {
|
||||||
|
metadata.Publisher = strings.TrimSpace(*sidecar.Publisher)
|
||||||
|
}
|
||||||
|
if sidecar.Description != nil {
|
||||||
|
metadata.Description = strings.TrimSpace(*sidecar.Description)
|
||||||
|
}
|
||||||
|
if sidecar.ISBN != nil {
|
||||||
|
metadata.ISBN = utils.NormalizeISBNSafe(strings.TrimSpace(*sidecar.ISBN))
|
||||||
|
}
|
||||||
|
if sidecar.ASIN != nil {
|
||||||
|
metadata.ASIN = strings.TrimSpace(*sidecar.ASIN)
|
||||||
|
}
|
||||||
|
if sidecar.Language != nil {
|
||||||
|
metadata.Language = strings.TrimSpace(*sidecar.Language)
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractAudiobookshelfSidecar-TMP-END
|
||||||
|
|
||||||
// mergeMetadata intelligently merges metadata from multiple sources
|
// mergeMetadata intelligently merges metadata from multiple sources
|
||||||
// Priority: metadata.opf (Calibre) → embedded metadata → folder structure → filename
|
// Priority: metadata.opf (Calibre) → embedded metadata → folder structure → filename
|
||||||
// For comics: metadata.opf → ComicInfo.xml → folder structure → filename
|
// For comics: metadata.opf → ComicInfo.xml → folder structure → filename
|
||||||
@@ -1265,6 +1394,8 @@ func extractGenreTagsFromComicInfo(comicInfo *ComicInfo) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *MediaScanner) extractMetadata(path string) (*MediaMetadata, error) {
|
func (s *MediaScanner) extractMetadata(path string) (*MediaMetadata, error) {
|
||||||
|
// Metadata sidecar priority: Calibre metadata.opf, then Audiobookshelf
|
||||||
|
// metadata.json, then the media file's own embedded metadata.
|
||||||
// Try Calibre sidecar first
|
// Try Calibre sidecar first
|
||||||
calibreMetadata := s.extractCalibreSidecar(path)
|
calibreMetadata := s.extractCalibreSidecar(path)
|
||||||
if calibreMetadata != nil {
|
if calibreMetadata != nil {
|
||||||
@@ -1279,7 +1410,21 @@ func (s *MediaScanner) extractMetadata(path string) (*MediaMetadata, error) {
|
|||||||
return s.mergeMetadata(path, calibreMetadata)
|
return s.mergeMetadata(path, calibreMetadata)
|
||||||
}
|
}
|
||||||
|
|
||||||
// EXISTING: Fallback to embedded metadata
|
// Audiobookshelf-style metadata.json sidecar (fields without a DB column
|
||||||
|
// - narrators, subtitle, explicit, abridged, chapters - are skipped)
|
||||||
|
abMetadata := extractAudiobookshelfSidecar(path)
|
||||||
|
if abMetadata != nil {
|
||||||
|
fmt.Printf("Using metadata.json sidecar for %s\n", path)
|
||||||
|
|
||||||
|
coverPath := findSidecarCover(path)
|
||||||
|
if coverPath != "" {
|
||||||
|
abMetadata.CoverPath = s.getRelativePath(coverPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.mergeMetadata(path, abMetadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to embedded metadata
|
||||||
ext := strings.ToLower(filepath.Ext(path))
|
ext := strings.ToLower(filepath.Ext(path))
|
||||||
|
|
||||||
switch ext {
|
switch ext {
|
||||||
@@ -1347,89 +1492,38 @@ func (s *MediaScanner) extractMetadata(path string) (*MediaMetadata, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractEPUBMetadata extracts metadata from an EPUB by parsing its embedded
|
||||||
|
// OPF document directly (container.xml → OPF → Dublin Core elements).
|
||||||
|
//
|
||||||
|
// It deliberately does NOT use a full-book parser: the previous go-epub
|
||||||
|
// implementation parsed every spine chapter and failed the whole call when any
|
||||||
|
// single chapter (or the TOC) was malformed, discarding perfectly good OPF
|
||||||
|
// metadata and leaving rescans writing blanks. The OPF holds all the metadata
|
||||||
|
// we need; chapter damage can no longer affect it.
|
||||||
func (s *MediaScanner) extractEPUBMetadata(path string) (*MediaMetadata, error) {
|
func (s *MediaScanner) extractEPUBMetadata(path string) (*MediaMetadata, error) {
|
||||||
book, err := epub.ReadBook(path)
|
r, err := zip.OpenReader(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to open EPUB: %v", err)
|
return nil, fmt.Errorf("failed to open EPUB: %v", err)
|
||||||
}
|
}
|
||||||
|
defer func() {
|
||||||
metadata := &MediaMetadata{}
|
if err := r.Close(); err != nil {
|
||||||
|
fmt.Printf("Warning: failed to close EPUB zip reader for %s: %v\n", path, err)
|
||||||
// Title
|
|
||||||
if title, err := book.Title(); err == nil && title != "" {
|
|
||||||
metadata.Title = title
|
|
||||||
}
|
|
||||||
|
|
||||||
// Author
|
|
||||||
if authors, err := book.MetadataByKey("creator"); err == nil && len(authors) > 0 {
|
|
||||||
metadata.Author = authors[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Description
|
|
||||||
if descriptions, err := book.MetadataByKey("description"); err == nil && len(descriptions) > 0 {
|
|
||||||
metadata.Description = descriptions[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Publisher
|
|
||||||
if publishers, err := book.MetadataByKey("publisher"); err == nil && len(publishers) > 0 {
|
|
||||||
metadata.Publisher = publishers[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Series and series number (Calibre specific metadata)
|
|
||||||
if series, err := book.MetadataByKey("calibre:series"); err == nil && len(series) > 0 {
|
|
||||||
metadata.Series = series[0]
|
|
||||||
}
|
|
||||||
if seriesIndex, err := book.MetadataByKey("calibre:series_index"); err == nil && len(seriesIndex) > 0 {
|
|
||||||
if index, err := strconv.ParseFloat(seriesIndex[0], 32); err == nil {
|
|
||||||
metadata.SeriesNumber = int32(index)
|
|
||||||
}
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
opfPath := findOPFPathInZip(r.File)
|
||||||
|
if opfPath == "" {
|
||||||
|
return nil, fmt.Errorf("no OPF document found in EPUB %s", path)
|
||||||
|
}
|
||||||
|
opfContent, err := readFileFromZip(r.File, opfPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read OPF %s from EPUB: %v", opfPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Publish date
|
metadata, err := parseOPFContent(opfContent)
|
||||||
if dates, err := book.MetadataByKey("date"); err == nil && len(dates) > 0 {
|
if err != nil {
|
||||||
if date, err := time.Parse("2006-01-02", dates[0]); err == nil {
|
return nil, fmt.Errorf("failed to parse OPF in EPUB %s: %v", path, err)
|
||||||
metadata.PublishDate = date
|
|
||||||
} else {
|
|
||||||
// Try alternative date formats
|
|
||||||
if date, err := time.Parse("2006", dates[0]); err == nil {
|
|
||||||
metadata.PublishDate = date
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Contributors
|
|
||||||
if contributors, err := book.MetadataByKey("contributor"); err == nil && len(contributors) > 0 {
|
|
||||||
// Normalize contributors for display
|
|
||||||
metadata.Contributors = utils.NormalizeContributors(contributors)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ISBN
|
|
||||||
if isbns, err := book.MetadataByKey("identifier"); err == nil && len(isbns) > 0 {
|
|
||||||
for _, isbn := range isbns {
|
|
||||||
if strings.Contains(strings.ToLower(isbn), "isbn") {
|
|
||||||
// Extract ISBN number from identifier like "isbn:978-3-16-148410-0"
|
|
||||||
isbnParts := strings.SplitN(isbn, ":", 2)
|
|
||||||
if len(isbnParts) == 2 {
|
|
||||||
metadata.ISBN = isbnParts[1]
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if strings.Contains(strings.ToLower(isbn), "asin") {
|
|
||||||
// Extract ASIN from identifier like "asin:B08XXXXX"
|
|
||||||
asinParts := strings.SplitN(isbn, ":", 2)
|
|
||||||
if len(asinParts) == 2 {
|
|
||||||
metadata.ASIN = asinParts[1]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tags
|
|
||||||
if tags, err := book.MetadataByKey("subject"); err == nil && len(tags) > 0 {
|
|
||||||
// Normalize tags for display
|
|
||||||
metadata.Tags = utils.NormalizeTags(tags)
|
|
||||||
}
|
|
||||||
|
|
||||||
return metadata, nil
|
return metadata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1604,7 +1698,7 @@ func (s *MediaScanner) LogProcessingIssue(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseCalibreMetadataOPF parses a Calibre metadata.opf file and extracts metadata
|
// parseCalibreMetadataOPF parses a Calibre metadata.opf sidecar file.
|
||||||
func (s *MediaScanner) parseCalibreMetadataOPF(opfPath string) (*MediaMetadata, error) {
|
func (s *MediaScanner) parseCalibreMetadataOPF(opfPath string) (*MediaMetadata, error) {
|
||||||
// Open file
|
// Open file
|
||||||
file, err := os.Open(opfPath)
|
file, err := os.Open(opfPath)
|
||||||
@@ -1616,6 +1710,18 @@ func (s *MediaScanner) parseCalibreMetadataOPF(opfPath string) (*MediaMetadata,
|
|||||||
fmt.Printf("Warning: failed to close metadata.opf: %v\n", err)
|
fmt.Printf("Warning: failed to close metadata.opf: %v\n", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
content, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read metadata.opf: %v", err)
|
||||||
|
}
|
||||||
|
return parseOPFContent(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseOPFContent parses an OPF document (Dublin Core metadata) into
|
||||||
|
// MediaMetadata. Used for both Calibre metadata.opf sidecars and the OPF
|
||||||
|
// embedded inside an EPUB - the dc:* vocabulary is identical. Namespace-aware
|
||||||
|
// parsing means it tolerates wherever the xmlns:dc declaration lives.
|
||||||
|
func parseOPFContent(content []byte) (*MediaMetadata, error) {
|
||||||
// Define XML structure for parsing with full Dublin Core namespace URLs
|
// Define XML structure for parsing with full Dublin Core namespace URLs
|
||||||
var opf struct {
|
var opf struct {
|
||||||
XMLName xml.Name `xml:"package"`
|
XMLName xml.Name `xml:"package"`
|
||||||
@@ -1641,8 +1747,8 @@ func (s *MediaScanner) parseCalibreMetadataOPF(opfPath string) (*MediaMetadata,
|
|||||||
} `xml:"metadata"`
|
} `xml:"metadata"`
|
||||||
}
|
}
|
||||||
// Parse XML
|
// Parse XML
|
||||||
if err := xml.NewDecoder(file).Decode(&opf); err != nil {
|
if err := xml.NewDecoder(bytes.NewReader(content)).Decode(&opf); err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse metadata.opf XML: %v", err)
|
return nil, fmt.Errorf("failed to parse OPF XML: %v", err)
|
||||||
}
|
}
|
||||||
// Map to MediaMetadata struct
|
// Map to MediaMetadata struct
|
||||||
metadata := &MediaMetadata{}
|
metadata := &MediaMetadata{}
|
||||||
@@ -1666,6 +1772,10 @@ func (s *MediaScanner) parseCalibreMetadataOPF(opfPath string) (*MediaMetadata,
|
|||||||
if len(opf.Metadata.Publisher) > 0 {
|
if len(opf.Metadata.Publisher) > 0 {
|
||||||
metadata.Publisher = opf.Metadata.Publisher[0]
|
metadata.Publisher = opf.Metadata.Publisher[0]
|
||||||
}
|
}
|
||||||
|
// Language
|
||||||
|
if len(opf.Metadata.Language) > 0 && opf.Metadata.Language[0] != "" {
|
||||||
|
metadata.Language = opf.Metadata.Language[0]
|
||||||
|
}
|
||||||
// Publish date
|
// Publish date
|
||||||
if len(opf.Metadata.Dates) > 0 {
|
if len(opf.Metadata.Dates) > 0 {
|
||||||
if date, err := time.Parse("2006-01-02T15:04:05Z07:00", opf.Metadata.Dates[0]); err == nil {
|
if date, err := time.Parse("2006-01-02T15:04:05Z07:00", opf.Metadata.Dates[0]); err == nil {
|
||||||
@@ -1681,14 +1791,23 @@ func (s *MediaScanner) parseCalibreMetadataOPF(opfPath string) (*MediaMetadata,
|
|||||||
}
|
}
|
||||||
// Identifiers (ISBN, ASIN)
|
// Identifiers (ISBN, ASIN)
|
||||||
for _, id := range opf.Metadata.Identifiers {
|
for _, id := range opf.Metadata.Identifiers {
|
||||||
|
value := strings.TrimSpace(id.Value)
|
||||||
switch strings.ToUpper(id.Scheme) {
|
switch strings.ToUpper(id.Scheme) {
|
||||||
case "ISBN":
|
case "ISBN":
|
||||||
metadata.ISBN = utils.NormalizeISBNSafe(id.Value)
|
metadata.ISBN = utils.NormalizeISBNSafe(value)
|
||||||
case "ASIN":
|
case "ASIN":
|
||||||
metadata.ASIN = id.Value
|
metadata.ASIN = value
|
||||||
case "UUID", "CALIBRE":
|
case "UUID", "CALIBRE":
|
||||||
// Store UUID in hash info, not metadata
|
// Store UUID in hash info, not metadata
|
||||||
// Will be extracted by extractHashInfo()
|
// Will be extracted by extractHashInfo()
|
||||||
|
default:
|
||||||
|
// EPUB3 identifiers often carry no opf:scheme attribute;
|
||||||
|
// accept a bare value that normalizes to a valid ISBN.
|
||||||
|
if metadata.ISBN == "" && id.Scheme == "" {
|
||||||
|
if normalized := utils.NormalizeISBNSafe(value); normalized != "" {
|
||||||
|
metadata.ISBN = normalized
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Contributors
|
// Contributors
|
||||||
@@ -1715,6 +1834,45 @@ func (s *MediaScanner) parseCalibreMetadataOPF(opfPath string) (*MediaMetadata,
|
|||||||
return metadata, nil
|
return metadata, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// findOPFPathInZip locates the OPF document inside an EPUB by reading
|
||||||
|
// META-INF/container.xml (string-scraped; we only need the rootfile
|
||||||
|
// full-path attribute). Returns "" when absent.
|
||||||
|
func findOPFPathInZip(files []*zip.File) string {
|
||||||
|
for _, f := range files {
|
||||||
|
if f.Name != "META-INF/container.xml" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rc, err := f.Open()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
content, readErr := io.ReadAll(rc)
|
||||||
|
if closeErr := rc.Close(); closeErr != nil {
|
||||||
|
fmt.Printf("Warning: failed to close META-INF/container.xml reader: %v\n", closeErr)
|
||||||
|
}
|
||||||
|
if readErr != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
opfStart := bytes.Index(content, []byte("<rootfile "))
|
||||||
|
if opfStart == -1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
opfStartAttr := bytes.Index(content[opfStart:], []byte("full-path="))
|
||||||
|
if opfStartAttr == -1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
opfStartAttr += len("full-path=")
|
||||||
|
quote := content[opfStart+opfStartAttr]
|
||||||
|
opfStartQuote := opfStart + opfStartAttr + 1
|
||||||
|
opfEndQuote := bytes.Index(content[opfStartQuote:], []byte{quote})
|
||||||
|
if opfEndQuote == -1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return string(content[opfStartQuote : opfStartQuote+opfEndQuote])
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// extractEPUBCover extracts the cover image from an EPUB file.
|
// extractEPUBCover extracts the cover image from an EPUB file.
|
||||||
// It looks for:
|
// It looks for:
|
||||||
// 1. An item with properties="cover-image" in the manifest
|
// 1. An item with properties="cover-image" in the manifest
|
||||||
@@ -1736,43 +1894,7 @@ func (s *MediaScanner) extractEPUBCover(epubPath string) (string, error) {
|
|||||||
// Try to find cover image from OPF metadata
|
// Try to find cover image from OPF metadata
|
||||||
coverImageName := ""
|
coverImageName := ""
|
||||||
|
|
||||||
// Attempt to read the OPF file to find cover reference
|
opfPath := findOPFPathInZip(r.File)
|
||||||
// First, find container.xml to locate the OPF
|
|
||||||
var opfPath string
|
|
||||||
for _, f := range r.File {
|
|
||||||
if f.Name == "META-INF/container.xml" {
|
|
||||||
rc, err := f.Open()
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
content, readErr := io.ReadAll(rc)
|
|
||||||
if closeErr := rc.Close(); closeErr != nil {
|
|
||||||
fmt.Printf("Warning: failed to close META-INF/container.xml reader in %s: %v\n", epubPath, closeErr)
|
|
||||||
}
|
|
||||||
if readErr != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Parse container.xml to find OPF path
|
|
||||||
// Simple string search since we just need the path
|
|
||||||
opfStart := bytes.Index(content, []byte("<rootfile "))
|
|
||||||
if opfStart == -1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
opfStartAttr := bytes.Index(content[opfStart:], []byte("full-path="))
|
|
||||||
if opfStartAttr == -1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
opfStartAttr += len("full-path=")
|
|
||||||
quote := content[opfStart+opfStartAttr]
|
|
||||||
opfStartQuote := opfStart + opfStartAttr + 1
|
|
||||||
opfEndQuote := bytes.Index(content[opfStartQuote:], []byte{quote})
|
|
||||||
if opfEndQuote == -1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
opfPath = string(content[opfStartQuote : opfStartQuote+opfEndQuote])
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if opfPath == "" {
|
if opfPath == "" {
|
||||||
// No OPF found, try common cover image paths
|
// No OPF found, try common cover image paths
|
||||||
|
|||||||
@@ -0,0 +1,232 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"bytes"
|
||||||
|
"image"
|
||||||
|
"image/jpeg"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// createTestJPEGBytes returns the bytes of a minimal valid JPEG.
|
||||||
|
func createTestJPEGBytes() string {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
img := image.NewRGBA(image.Rect(0, 0, 1, 1))
|
||||||
|
if err := jpeg.Encode(&buf, img, nil); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// createPragmaticStyleEPUB builds an EPUB modeled on Pragmatic Bookshelf
|
||||||
|
// output: the dc namespace declared on the <metadata> element (not on
|
||||||
|
// <package>), a scheme-less ISBN identifier, an OPF-declared cover, and a
|
||||||
|
// deliberately malformed chapter body. The malformed chapter is the
|
||||||
|
// regression trigger: the previous go-epub-based extractor failed the whole
|
||||||
|
// book when any chapter was unparseable and wrote blank metadata.
|
||||||
|
func createPragmaticStyleEPUB(epubPath string) error {
|
||||||
|
file, err := os.Create(epubPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
zipWriter := zip.NewWriter(file)
|
||||||
|
defer zipWriter.Close()
|
||||||
|
|
||||||
|
mimetypeW, err := zipWriter.CreateHeader(&zip.FileHeader{
|
||||||
|
Name: "mimetype",
|
||||||
|
Method: zip.Store,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mimetypeW.Write([]byte("application/epub+zip"))
|
||||||
|
|
||||||
|
files := map[string]string{
|
||||||
|
"META-INF/container.xml": `<?xml version="1.0"?><container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container"><rootfiles><rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/></rootfiles></container>`,
|
||||||
|
// dc namespace declared on <metadata>; identifiers carry no scheme attr
|
||||||
|
"OEBPS/content.opf": `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<package xmlns="http://www.idpf.org/2007/opf" version="3.0" unique-identifier="PubID">
|
||||||
|
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||||
|
<dc:language>en</dc:language>
|
||||||
|
<dc:title>A Common-Sense Guide</dc:title>
|
||||||
|
<dc:creator>Jay Wengrow</dc:creator>
|
||||||
|
<dc:publisher>The Pragmatic Bookshelf, LLC</dc:publisher>
|
||||||
|
<dc:description>Content that makes you a better programmer.</dc:description>
|
||||||
|
<dc:subject>Programming</dc:subject>
|
||||||
|
<dc:identifier id="PubID">978-1-68050-722-8</dc:identifier>
|
||||||
|
<meta name="cover" content="cover-image"/>
|
||||||
|
</metadata>
|
||||||
|
<manifest>
|
||||||
|
<item id="cover-image" href="images/cover.jpg" media-type="image/jpeg"/>
|
||||||
|
<item id="ch1" href="ch1.xhtml" media-type="application/xhtml+xml"/>
|
||||||
|
</manifest>
|
||||||
|
<spine><itemref idref="ch1"/></spine>
|
||||||
|
</package>`,
|
||||||
|
// Malformed on purpose: unclosed tags
|
||||||
|
"OEBPS/ch1.xhtml": `<html><body><p>unclosed paragraph`,
|
||||||
|
"OEBPS/images/cover.jpg": createTestJPEGBytes(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, content := range files {
|
||||||
|
w, err := zipWriter.Create(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := w.Write([]byte(content)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return zipWriter.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestExtractEPUBMetadataBrokenChapter guards the regression where one
|
||||||
|
// unparseable chapter made the extractor return nothing at all: metadata must
|
||||||
|
// come from the OPF regardless of chapter-body damage.
|
||||||
|
func TestExtractEPUBMetadataBrokenChapter(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
epubPath := filepath.Join(tmpDir, "book.epub")
|
||||||
|
if err := createPragmaticStyleEPUB(epubPath); err != nil {
|
||||||
|
t.Fatalf("failed to create test EPUB: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := NewMediaScanner(nil)
|
||||||
|
metadata, err := s.extractEPUBMetadata(epubPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("extractEPUBMetadata() error: %v", err)
|
||||||
|
}
|
||||||
|
if metadata.Title != "A Common-Sense Guide" {
|
||||||
|
t.Errorf("Title = %q, want %q", metadata.Title, "A Common-Sense Guide")
|
||||||
|
}
|
||||||
|
if metadata.Author != "Jay Wengrow" {
|
||||||
|
t.Errorf("Author = %q, want %q", metadata.Author, "Jay Wengrow")
|
||||||
|
}
|
||||||
|
if metadata.Publisher != "The Pragmatic Bookshelf, LLC" {
|
||||||
|
t.Errorf("Publisher = %q, want %q", metadata.Publisher, "The Pragmatic Bookshelf, LLC")
|
||||||
|
}
|
||||||
|
if metadata.Description == "" {
|
||||||
|
t.Error("Description missing")
|
||||||
|
}
|
||||||
|
if metadata.Language != "en" {
|
||||||
|
t.Errorf("Language = %q, want %q", metadata.Language, "en")
|
||||||
|
}
|
||||||
|
// Scheme-less identifier that normalizes to a valid ISBN must be picked up
|
||||||
|
if metadata.ISBN == "" {
|
||||||
|
t.Error("ISBN missing (scheme-less dc:identifier fallback failed)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseOPFContentCalibreSeries(t *testing.T) {
|
||||||
|
opf := `<?xml version="1.0"?>
|
||||||
|
<package xmlns="http://www.idpf.org/2007/opf" version="2.0" unique-identifier="id">
|
||||||
|
<metadata xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:opf="http://www.idpf.org/2007/opf">
|
||||||
|
<dc:title>Test Book</dc:title>
|
||||||
|
<dc:creator>Some Author</dc:creator>
|
||||||
|
<dc:date>2020-03-15</dc:date>
|
||||||
|
<dc:subject>Fiction</dc:subject>
|
||||||
|
<dc:subject>Classic</dc:subject>
|
||||||
|
<dc:identifier opf:scheme="ISBN">978-3-16-148410-0</dc:identifier>
|
||||||
|
<meta name="calibre:series" content="Great Series"/>
|
||||||
|
<meta name="calibre:series_index" content="2.5"/>
|
||||||
|
</metadata>
|
||||||
|
</package>`
|
||||||
|
metadata, err := parseOPFContent([]byte(opf))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("parseOPFContent() error: %v", err)
|
||||||
|
}
|
||||||
|
if metadata.Series != "Great Series" || metadata.SeriesNumber != 2 {
|
||||||
|
t.Errorf("Series = %q/%d, want Great Series/2", metadata.Series, metadata.SeriesNumber)
|
||||||
|
}
|
||||||
|
if metadata.ISBN == "" {
|
||||||
|
t.Error("schemed ISBN not extracted")
|
||||||
|
}
|
||||||
|
wantDate := time.Date(2020, 3, 15, 0, 0, 0, 0, time.UTC)
|
||||||
|
if !metadata.PublishDate.Equal(wantDate) {
|
||||||
|
t.Errorf("PublishDate = %v, want %v", metadata.PublishDate, wantDate)
|
||||||
|
}
|
||||||
|
if len(metadata.Tags) != 2 {
|
||||||
|
t.Errorf("Tags = %v, want 2 subjects", metadata.Tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractAudiobookshelfSidecar(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
json string
|
||||||
|
validate func(t *testing.T, m *MediaMetadata)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "full sidecar",
|
||||||
|
json: `{
|
||||||
|
"title": "An Book",
|
||||||
|
"authors": ["Author One", "Author Two"],
|
||||||
|
"series": [{"series": "The Series", "sequence": "4.5"}],
|
||||||
|
"genres": ["Fantasy"],
|
||||||
|
"tags": ["tag1"],
|
||||||
|
"publishedYear": 2019,
|
||||||
|
"publisher": "ACME Books",
|
||||||
|
"description": "A very good book.",
|
||||||
|
"isbn": "978-3-16-148410-0",
|
||||||
|
"asin": "B08XYZ",
|
||||||
|
"language": "en"
|
||||||
|
}`,
|
||||||
|
validate: func(t *testing.T, m *MediaMetadata) {
|
||||||
|
if m.Title != "An Book" || m.Author != "Author One" {
|
||||||
|
t.Errorf("Title/Author = %q/%q", m.Title, m.Author)
|
||||||
|
}
|
||||||
|
if m.Series != "The Series" || m.SeriesNumber != 4 {
|
||||||
|
t.Errorf("Series = %q/%d, want The Series/4", m.Series, m.SeriesNumber)
|
||||||
|
}
|
||||||
|
if len(m.Tags) != 2 {
|
||||||
|
t.Errorf("Tags = %v, want genres+tags merged", m.Tags)
|
||||||
|
}
|
||||||
|
if m.PublishDate.Year() != 2019 {
|
||||||
|
t.Errorf("PublishDate year = %d, want 2019", m.PublishDate.Year())
|
||||||
|
}
|
||||||
|
if m.Publisher != "ACME Books" || m.Description != "A very good book." {
|
||||||
|
t.Errorf("Publisher/Description = %q/%q", m.Publisher, m.Description)
|
||||||
|
}
|
||||||
|
if m.ISBN == "" || m.ASIN != "B08XYZ" || m.Language != "en" {
|
||||||
|
t.Errorf("ISBN/ASIN/Language = %q/%q/%q", m.ISBN, m.ASIN, m.Language)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sparse sidecar (real-world Audiobookshelf export)",
|
||||||
|
json: `{"title": "Sparse (1234)", "authors": ["X"], "tags": [], "description": null}`,
|
||||||
|
validate: func(t *testing.T, m *MediaMetadata) {
|
||||||
|
if m.Title != "Sparse (1234)" || m.Author != "X" {
|
||||||
|
t.Errorf("Title/Author = %q/%q", m.Title, m.Author)
|
||||||
|
}
|
||||||
|
if m.Description != "" || m.Tags != nil {
|
||||||
|
t.Error("null/empty sidecar fields must stay unset")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, "metadata.json"), []byte(tt.json), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
m := extractAudiobookshelfSidecar(filepath.Join(dir, "book.epub"))
|
||||||
|
if m == nil {
|
||||||
|
t.Fatal("extractAudiobookshelfSidecar() = nil, want metadata")
|
||||||
|
}
|
||||||
|
tt.validate(t, m)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("no sidecar returns nil", func(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
if m := extractAudiobookshelfSidecar(filepath.Join(dir, "book.epub")); m != nil {
|
||||||
|
t.Errorf("extractAudiobookshelfSidecar() = %v, want nil", m)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
package templates
|
package templates
|
||||||
|
|
||||||
templ AdminLibrary(user User, libraries []LibraryData, users []User) {
|
templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCount int64) {
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
@@ -34,6 +34,23 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
if archivedCount > 0 {
|
||||||
|
<div class="card p-4 mb-6 flex items-center justify-between gap-4 flex-wrap">
|
||||||
|
<div>
|
||||||
|
<p class="font-semibold" style="color: var(--text-primary)">
|
||||||
|
Archived items: { archivedCount }
|
||||||
|
</p>
|
||||||
|
<p class="text-sm" style="color: var(--text-secondary)">
|
||||||
|
Files missing from disk for two consecutive scans. Reading history is kept
|
||||||
|
unless purged; items return automatically if their files come back.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" onclick="purgeArchivedItems()" class="btn btn-secondary">
|
||||||
|
@Icon("trash", "h-4 w-4")
|
||||||
|
Purge Archived Now
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<div id="libraries-container">
|
<div id="libraries-container">
|
||||||
@LibraryList(user, libraries, users)
|
@LibraryList(user, libraries, users)
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+306
-275
File diff suppressed because it is too large
Load Diff
@@ -249,6 +249,46 @@ function stopScanStatusPolling(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Purge all archived items (files missing from disk for 2+ scans). Exposed on
|
||||||
|
// window for the library admin page's inline button; reading history is
|
||||||
|
// deleted with the rows, so a confirm dialog guards it.
|
||||||
|
async function purgeArchivedItems(): Promise<void> {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (!token) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!window.confirm(
|
||||||
|
"Permanently delete all archived items? Their reading progress, notes, and highlights will be lost.",
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resp = await fetch("/api/media-items/purge-archived", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { Authorization: `Bearer ${token}` },
|
||||||
|
});
|
||||||
|
if (!resp.ok) {
|
||||||
|
const err = await resp.json().catch(() => ({}));
|
||||||
|
throw new Error(err.error || "Failed to purge archived items");
|
||||||
|
}
|
||||||
|
const data = await resp.json();
|
||||||
|
showToast(
|
||||||
|
`Purged ${data.purged} archived item${data.purged === 1 ? "" : "s"}`,
|
||||||
|
"success",
|
||||||
|
);
|
||||||
|
setTimeout(() => window.location.reload(), 700);
|
||||||
|
} catch (e) {
|
||||||
|
showToast(
|
||||||
|
e instanceof Error ? e.message : "Failed to purge archived items",
|
||||||
|
"error",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(window as any).purgeArchivedItems = purgeArchivedItems;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
hideScanProgress,
|
hideScanProgress,
|
||||||
loadWatchStatus,
|
loadWatchStatus,
|
||||||
|
|||||||
Reference in New Issue
Block a user