feat(data): duplicate-content queries — archive filters, cross-library moves, archived-twin matching
Three related query groups in queries.sql (sqlc regenerated; no generated
signatures changed, so no caller edits were needed):
1. Dashboard/collection archive filters. The smart-section and collection
queries returned archived and missing items (visible only as broken
covers once their files vanished). Added the codebase-standard
"AND archived_at IS NULL AND missing_scan_count = 0" predicate to:
GetContinueReadingItems, GetRecentlyAddedItems, GetRecentlyReadItems,
GetNotStartedItems, GetCollectionItemsForDashboard, GetLibraryItems.
Matches the existing convention in ListMediaItems, SearchMediaItems,
the library counts, and GetContinueSeriesItems.
2. Cross-library SHA move detection support.
- GetMediaItemsBySHA256AndSameLibraryType: finds identical content
(same file_sha256) registered in a DIFFERENT library of the SAME
library type, ordered archived-first, most-missing, oldest. Type
scoping keeps ebooks from merging into manga/comics libraries.
- MoveMediaItemToLibrary: repoints an existing row to the new library
(library_id + recomputed file_path + file_size) and clears
archive/missing state. The row keeps its ID, so reading progress,
annotations, collections, and kobo shelves follow the book
automatically; the new set_library_type_name_on_library_change
trigger refreshes the denormalized type column.
3. Archived-twin matching. ListHiddenMediaItems (admin archived-items
page) now LEFT JOIN LATERALs the best active same-SHA twin per hidden
row — preferring same library type, then same library, then most
reading progress, then oldest — exposing match_id / match_title /
match_library_name so the UI can offer merging the archived row's
reading data into its active duplicate. COALESCE on match_title keeps
the no-match case NULL-safe.
This commit is contained in:
@@ -259,6 +259,12 @@ type Querier interface {
|
|||||||
GetMediaItemFormats(ctx context.Context, mediaItemID pgtype.UUID) ([]MediaItemFormats, error)
|
GetMediaItemFormats(ctx context.Context, mediaItemID pgtype.UUID) ([]MediaItemFormats, error)
|
||||||
// Per-item user-data counts, used when choosing which duplicate copy to keep
|
// Per-item user-data counts, used when choosing which duplicate copy to keep
|
||||||
GetMediaItemUsageCounts(ctx context.Context, mediaItemID pgtype.UUID) (GetMediaItemUsageCountsRow, error)
|
GetMediaItemUsageCounts(ctx context.Context, mediaItemID pgtype.UUID) (GetMediaItemUsageCountsRow, error)
|
||||||
|
// Cross-library move detection: identical content registered in a different
|
||||||
|
// library of the same type (ebooks stay with ebooks, comics with comics).
|
||||||
|
// Candidates may be active copies (deliberate duplicates - leave alone) or
|
||||||
|
// hidden/archived rows whose file vanished (move candidates - the scanner
|
||||||
|
// verifies disk state before repointing).
|
||||||
|
GetMediaItemsBySHA256AndSameLibraryType(ctx context.Context, arg GetMediaItemsBySHA256AndSameLibraryTypeParams) ([]GetMediaItemsBySHA256AndSameLibraryTypeRow, error)
|
||||||
GetMediaNote(ctx context.Context, id pgtype.UUID) (MediaNotes, error)
|
GetMediaNote(ctx context.Context, id pgtype.UUID) (MediaNotes, error)
|
||||||
// ============================================
|
// ============================================
|
||||||
// ANNOTATION SYNC QUERIES (notes)
|
// ANNOTATION SYNC QUERIES (notes)
|
||||||
@@ -349,7 +355,10 @@ type Querier interface {
|
|||||||
ListDevicesByUser(ctx context.Context, userID pgtype.UUID) ([]Devices, error)
|
ListDevicesByUser(ctx context.Context, userID pgtype.UUID) ([]Devices, error)
|
||||||
// Items hidden from libraries: either missing from disk (waiting on the
|
// Items hidden from libraries: either missing from disk (waiting on the
|
||||||
// second scan that archives them) or already archived and pending purge.
|
// second scan that archives them) or already archived and pending purge.
|
||||||
// Feeds the admin archived-items page.
|
// Feeds the admin archived-items page. match_* columns identify an active
|
||||||
|
// item with identical content (same SHA-256) so the UI can offer merging the
|
||||||
|
// archived row's reading data into its active twin instead of waiting for a
|
||||||
|
// file that will never return.
|
||||||
ListHiddenMediaItems(ctx context.Context) ([]ListHiddenMediaItemsRow, error)
|
ListHiddenMediaItems(ctx context.Context) ([]ListHiddenMediaItemsRow, error)
|
||||||
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)
|
||||||
@@ -374,6 +383,11 @@ type Querier interface {
|
|||||||
// a move. Repoints the row (and refreshes file-level fields) so reading
|
// a move. Repoints the row (and refreshes file-level fields) so reading
|
||||||
// history follows the book; archive/missing state is cleared separately.
|
// history follows the book; archive/missing state is cleared separately.
|
||||||
MoveMediaItemFilePath(ctx context.Context, arg MoveMediaItemFilePathParams) error
|
MoveMediaItemFilePath(ctx context.Context, arg MoveMediaItemFilePathParams) error
|
||||||
|
// Cross-library move: identical content reappeared in a different library of
|
||||||
|
// the same type while the old file vanished. Repoints the row so reading
|
||||||
|
// history, annotations and collections follow the book; archive/missing
|
||||||
|
// state is cleared. library_type_name is refreshed by trigger.
|
||||||
|
MoveMediaItemToLibrary(ctx context.Context, arg MoveMediaItemToLibraryParams) error
|
||||||
// Manual bulk purge from the library admin page.
|
// Manual bulk purge from the library admin page.
|
||||||
PurgeAllArchivedMediaItems(ctx context.Context) ([]PurgeAllArchivedMediaItemsRow, error)
|
PurgeAllArchivedMediaItems(ctx context.Context) ([]PurgeAllArchivedMediaItemsRow, error)
|
||||||
// Retention sweep at library-scan time: hard-delete archived items older
|
// Retention sweep at library-scan time: hard-delete archived items older
|
||||||
|
|||||||
@@ -2623,6 +2623,8 @@ SELECT ci.id, ci.collection_id, ci.media_item_id, ci.added_at, ci.added_by_user_
|
|||||||
FROM collection_items ci
|
FROM collection_items ci
|
||||||
JOIN media_items mi ON ci.media_item_id = mi.id
|
JOIN media_items mi ON ci.media_item_id = mi.id
|
||||||
WHERE ci.collection_id = $1
|
WHERE ci.collection_id = $1
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
ORDER BY ci.added_at DESC
|
ORDER BY ci.added_at DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -2676,6 +2678,8 @@ SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.fi
|
|||||||
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)
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
ORDER BY ci.added_at DESC
|
ORDER BY ci.added_at DESC
|
||||||
LIMIT $3
|
LIMIT $3
|
||||||
`
|
`
|
||||||
@@ -2982,6 +2986,8 @@ INNER JOIN (
|
|||||||
ORDER BY media_item_id, last_read_at DESC
|
ORDER BY media_item_id, last_read_at DESC
|
||||||
) rp ON rp.media_item_id = mi.id
|
) rp ON rp.media_item_id = mi.id
|
||||||
WHERE ($2::uuid IS NULL OR mi.library_id = $2::uuid)
|
WHERE ($2::uuid IS NULL OR mi.library_id = $2::uuid)
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
ORDER BY rp.last_read_at DESC
|
ORDER BY rp.last_read_at DESC
|
||||||
LIMIT $3
|
LIMIT $3
|
||||||
`
|
`
|
||||||
@@ -4378,6 +4384,8 @@ 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.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi
|
SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.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 mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
ORDER BY mi.created_at DESC
|
ORDER BY mi.created_at DESC
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -5694,6 +5702,184 @@ func (q *Queries) GetMediaItemUsageCounts(ctx context.Context, mediaItemID pgtyp
|
|||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const GetMediaItemsBySHA256AndSameLibraryType = `-- name: GetMediaItemsBySHA256AndSameLibraryType :many
|
||||||
|
SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, lt.name AS library_type_name
|
||||||
|
FROM media_items mi
|
||||||
|
JOIN libraries l ON mi.library_id = l.id
|
||||||
|
JOIN library_types lt ON l.library_type_id = lt.id
|
||||||
|
JOIN libraries target_lib ON target_lib.id = $1
|
||||||
|
WHERE mi.file_sha256 = $2
|
||||||
|
AND mi.library_id <> $1
|
||||||
|
AND lt.id = target_lib.library_type_id
|
||||||
|
ORDER BY (mi.archived_at IS NOT NULL) DESC,
|
||||||
|
mi.missing_scan_count DESC,
|
||||||
|
mi.created_at ASC
|
||||||
|
`
|
||||||
|
|
||||||
|
type GetMediaItemsBySHA256AndSameLibraryTypeParams struct {
|
||||||
|
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
|
||||||
|
FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetMediaItemsBySHA256AndSameLibraryTypeRow struct {
|
||||||
|
ID pgtype.UUID `db:"id" json:"id"`
|
||||||
|
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
|
||||||
|
Title string `db:"title" json:"title"`
|
||||||
|
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"`
|
||||||
|
LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-library move detection: identical content registered in a different
|
||||||
|
// library of the same type (ebooks stay with ebooks, comics with comics).
|
||||||
|
// Candidates may be active copies (deliberate duplicates - leave alone) or
|
||||||
|
// hidden/archived rows whose file vanished (move candidates - the scanner
|
||||||
|
// verifies disk state before repointing).
|
||||||
|
func (q *Queries) GetMediaItemsBySHA256AndSameLibraryType(ctx context.Context, arg GetMediaItemsBySHA256AndSameLibraryTypeParams) ([]GetMediaItemsBySHA256AndSameLibraryTypeRow, error) {
|
||||||
|
rows, err := q.db.Query(ctx, GetMediaItemsBySHA256AndSameLibraryType, arg.LibraryID, arg.FileSha256)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
items := []GetMediaItemsBySHA256AndSameLibraryTypeRow{}
|
||||||
|
for rows.Next() {
|
||||||
|
var i GetMediaItemsBySHA256AndSameLibraryTypeRow
|
||||||
|
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.LibraryTypeName,
|
||||||
|
&i.TagsSearch,
|
||||||
|
&i.ContributorsSearch,
|
||||||
|
&i.FileSha256,
|
||||||
|
&i.OpfIdentifier,
|
||||||
|
&i.OpfUuid,
|
||||||
|
&i.HashConfidence,
|
||||||
|
&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 GetMediaNote = `-- name: GetMediaNote :one
|
const GetMediaNote = `-- name: GetMediaNote :one
|
||||||
SELECT id, media_item_id, user_id, content, position, created_at, updated_at, percentage_location, character_start, character_end, epubcfi_location, chapter_reference, paragraph_reference, device_sync_data, dedup_key, last_modified_at, last_modified_source, deleted, deleted_at FROM media_notes WHERE id = $1
|
SELECT id, media_item_id, user_id, content, position, created_at, updated_at, percentage_location, character_start, character_end, epubcfi_location, chapter_reference, paragraph_reference, device_sync_data, dedup_key, last_modified_at, last_modified_source, deleted, deleted_at FROM media_notes WHERE id = $1
|
||||||
`
|
`
|
||||||
@@ -5909,6 +6095,8 @@ 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.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi
|
SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.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 mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM reading_progress rp
|
SELECT 1 FROM reading_progress rp
|
||||||
WHERE rp.media_item_id = mi.id
|
WHERE rp.media_item_id = mi.id
|
||||||
@@ -6312,6 +6500,8 @@ 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.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi
|
SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.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 mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
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
|
||||||
`
|
`
|
||||||
@@ -6416,6 +6606,8 @@ INNER JOIN (
|
|||||||
ORDER BY media_item_id, last_read_at DESC
|
ORDER BY media_item_id, last_read_at DESC
|
||||||
) rp ON rp.media_item_id = mi.id
|
) rp ON rp.media_item_id = mi.id
|
||||||
WHERE ($2::uuid IS NULL OR mi.library_id = $2::uuid)
|
WHERE ($2::uuid IS NULL OR mi.library_id = $2::uuid)
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
ORDER BY rp.last_read_at DESC
|
ORDER BY rp.last_read_at DESC
|
||||||
LIMIT $3
|
LIMIT $3
|
||||||
`
|
`
|
||||||
@@ -8419,10 +8611,28 @@ func (q *Queries) ListDevicesByUser(ctx context.Context, userID pgtype.UUID) ([]
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ListHiddenMediaItems = `-- name: ListHiddenMediaItems :many
|
const ListHiddenMediaItems = `-- name: ListHiddenMediaItems :many
|
||||||
SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name
|
SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name,
|
||||||
|
twin.id AS match_id, COALESCE(twin.title, '') AS match_title,
|
||||||
|
twin_library.name AS match_library_name
|
||||||
FROM media_items mi
|
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 LATERAL (
|
||||||
|
SELECT t.id, t.title, t.library_id
|
||||||
|
FROM media_items t
|
||||||
|
JOIN libraries tl ON t.library_id = tl.id
|
||||||
|
JOIN library_types tlt ON tl.library_type_id = tlt.id
|
||||||
|
WHERE t.file_sha256 = mi.file_sha256
|
||||||
|
AND t.id <> mi.id
|
||||||
|
AND t.archived_at IS NULL
|
||||||
|
AND t.missing_scan_count = 0
|
||||||
|
ORDER BY (tlt.id = lt.id) DESC,
|
||||||
|
(tl.id = l.id) DESC,
|
||||||
|
(SELECT COUNT(*) FROM reading_progress rp WHERE rp.media_item_id = t.id) DESC,
|
||||||
|
t.created_at ASC
|
||||||
|
LIMIT 1
|
||||||
|
) twin ON true
|
||||||
|
LEFT JOIN libraries twin_library ON twin_library.id = twin.library_id
|
||||||
WHERE mi.archived_at IS NOT NULL OR mi.missing_scan_count > 0
|
WHERE mi.archived_at IS NOT NULL OR mi.missing_scan_count > 0
|
||||||
ORDER BY mi.archived_at NULLS LAST, mi.updated_at DESC
|
ORDER BY mi.archived_at NULLS LAST, mi.updated_at DESC
|
||||||
`
|
`
|
||||||
@@ -8494,11 +8704,17 @@ type ListHiddenMediaItemsRow struct {
|
|||||||
HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"`
|
HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"`
|
||||||
LibraryName string `db:"library_name" json:"library_name"`
|
LibraryName string `db:"library_name" json:"library_name"`
|
||||||
LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"`
|
LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"`
|
||||||
|
MatchID pgtype.UUID `db:"match_id" json:"match_id"`
|
||||||
|
MatchTitle string `db:"match_title" json:"match_title"`
|
||||||
|
MatchLibraryName pgtype.Text `db:"match_library_name" json:"match_library_name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Items hidden from libraries: either missing from disk (waiting on the
|
// Items hidden from libraries: either missing from disk (waiting on the
|
||||||
// second scan that archives them) or already archived and pending purge.
|
// second scan that archives them) or already archived and pending purge.
|
||||||
// Feeds the admin archived-items page.
|
// Feeds the admin archived-items page. match_* columns identify an active
|
||||||
|
// item with identical content (same SHA-256) so the UI can offer merging the
|
||||||
|
// archived row's reading data into its active twin instead of waiting for a
|
||||||
|
// file that will never return.
|
||||||
func (q *Queries) ListHiddenMediaItems(ctx context.Context) ([]ListHiddenMediaItemsRow, error) {
|
func (q *Queries) ListHiddenMediaItems(ctx context.Context) ([]ListHiddenMediaItemsRow, error) {
|
||||||
rows, err := q.db.Query(ctx, ListHiddenMediaItems)
|
rows, err := q.db.Query(ctx, ListHiddenMediaItems)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -8575,6 +8791,9 @@ func (q *Queries) ListHiddenMediaItems(ctx context.Context) ([]ListHiddenMediaIt
|
|||||||
&i.HashConfidence,
|
&i.HashConfidence,
|
||||||
&i.LibraryName,
|
&i.LibraryName,
|
||||||
&i.LibraryTypeName_2,
|
&i.LibraryTypeName_2,
|
||||||
|
&i.MatchID,
|
||||||
|
&i.MatchTitle,
|
||||||
|
&i.MatchLibraryName,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -9999,6 +10218,38 @@ func (q *Queries) MoveMediaItemFilePath(ctx context.Context, arg MoveMediaItemFi
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MoveMediaItemToLibrary = `-- name: MoveMediaItemToLibrary :exec
|
||||||
|
UPDATE media_items
|
||||||
|
SET library_id = $2,
|
||||||
|
file_path = $3,
|
||||||
|
file_size = $4,
|
||||||
|
archived_at = NULL,
|
||||||
|
missing_scan_count = 0,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = $1
|
||||||
|
`
|
||||||
|
|
||||||
|
type MoveMediaItemToLibraryParams struct {
|
||||||
|
ID pgtype.UUID `db:"id" json:"id"`
|
||||||
|
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
|
||||||
|
FilePath string `db:"file_path" json:"file_path"`
|
||||||
|
FileSize pgtype.Int8 `db:"file_size" json:"file_size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cross-library move: identical content reappeared in a different library of
|
||||||
|
// the same type while the old file vanished. Repoints the row so reading
|
||||||
|
// history, annotations and collections follow the book; archive/missing
|
||||||
|
// state is cleared. library_type_name is refreshed by trigger.
|
||||||
|
func (q *Queries) MoveMediaItemToLibrary(ctx context.Context, arg MoveMediaItemToLibraryParams) error {
|
||||||
|
_, err := q.db.Exec(ctx, MoveMediaItemToLibrary,
|
||||||
|
arg.ID,
|
||||||
|
arg.LibraryID,
|
||||||
|
arg.FilePath,
|
||||||
|
arg.FileSize,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
const PurgeAllArchivedMediaItems = `-- name: PurgeAllArchivedMediaItems :many
|
const PurgeAllArchivedMediaItems = `-- name: PurgeAllArchivedMediaItems :many
|
||||||
DELETE FROM media_items
|
DELETE FROM media_items
|
||||||
WHERE archived_at IS NOT NULL
|
WHERE archived_at IS NOT NULL
|
||||||
|
|||||||
@@ -328,6 +328,20 @@ WHERE id = $1;
|
|||||||
UPDATE media_items SET archived_at = NOW(), updated_at = NOW()
|
UPDATE media_items SET archived_at = NOW(), updated_at = NOW()
|
||||||
WHERE id = $1;
|
WHERE id = $1;
|
||||||
|
|
||||||
|
-- name: MoveMediaItemToLibrary :exec
|
||||||
|
-- Cross-library move: identical content reappeared in a different library of
|
||||||
|
-- the same type while the old file vanished. Repoints the row so reading
|
||||||
|
-- history, annotations and collections follow the book; archive/missing
|
||||||
|
-- state is cleared. library_type_name is refreshed by trigger.
|
||||||
|
UPDATE media_items
|
||||||
|
SET library_id = $2,
|
||||||
|
file_path = $3,
|
||||||
|
file_size = $4,
|
||||||
|
archived_at = NULL,
|
||||||
|
missing_scan_count = 0,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = $1;
|
||||||
|
|
||||||
-- name: ClearMediaItemArchive :exec
|
-- name: ClearMediaItemArchive :exec
|
||||||
-- File is back on disk (by path or content hash): restore visibility and
|
-- 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.
|
-- reset the missing-scan counter. No-op for items that were never archived.
|
||||||
@@ -353,11 +367,32 @@ SELECT COUNT(*) FROM media_items WHERE archived_at IS NOT NULL;
|
|||||||
-- name: ListHiddenMediaItems :many
|
-- name: ListHiddenMediaItems :many
|
||||||
-- Items hidden from libraries: either missing from disk (waiting on the
|
-- Items hidden from libraries: either missing from disk (waiting on the
|
||||||
-- second scan that archives them) or already archived and pending purge.
|
-- second scan that archives them) or already archived and pending purge.
|
||||||
-- Feeds the admin archived-items page.
|
-- Feeds the admin archived-items page. match_* columns identify an active
|
||||||
SELECT mi.*, l.name as library_name, lt.name as library_type_name
|
-- item with identical content (same SHA-256) so the UI can offer merging the
|
||||||
|
-- archived row's reading data into its active twin instead of waiting for a
|
||||||
|
-- file that will never return.
|
||||||
|
SELECT mi.*, l.name as library_name, lt.name as library_type_name,
|
||||||
|
twin.id AS match_id, COALESCE(twin.title, '') AS match_title,
|
||||||
|
twin_library.name AS match_library_name
|
||||||
FROM media_items mi
|
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 LATERAL (
|
||||||
|
SELECT t.id, t.title, t.library_id
|
||||||
|
FROM media_items t
|
||||||
|
JOIN libraries tl ON t.library_id = tl.id
|
||||||
|
JOIN library_types tlt ON tl.library_type_id = tlt.id
|
||||||
|
WHERE t.file_sha256 = mi.file_sha256
|
||||||
|
AND t.id <> mi.id
|
||||||
|
AND t.archived_at IS NULL
|
||||||
|
AND t.missing_scan_count = 0
|
||||||
|
ORDER BY (tlt.id = lt.id) DESC,
|
||||||
|
(tl.id = l.id) DESC,
|
||||||
|
(SELECT COUNT(*) FROM reading_progress rp WHERE rp.media_item_id = t.id) DESC,
|
||||||
|
t.created_at ASC
|
||||||
|
LIMIT 1
|
||||||
|
) twin ON true
|
||||||
|
LEFT JOIN libraries twin_library ON twin_library.id = twin.library_id
|
||||||
WHERE mi.archived_at IS NOT NULL OR mi.missing_scan_count > 0
|
WHERE mi.archived_at IS NOT NULL OR mi.missing_scan_count > 0
|
||||||
ORDER BY mi.archived_at NULLS LAST, mi.updated_at DESC;
|
ORDER BY mi.archived_at NULLS LAST, mi.updated_at DESC;
|
||||||
|
|
||||||
@@ -1898,6 +1933,24 @@ SELECT * FROM media_items WHERE file_sha256 = $1;
|
|||||||
-- name: GetMediaItemBySHA256AndLibrary :one
|
-- name: GetMediaItemBySHA256AndLibrary :one
|
||||||
SELECT * FROM media_items WHERE file_sha256 = $1 AND library_id = $2;
|
SELECT * FROM media_items WHERE file_sha256 = $1 AND library_id = $2;
|
||||||
|
|
||||||
|
-- name: GetMediaItemsBySHA256AndSameLibraryType :many
|
||||||
|
-- Cross-library move detection: identical content registered in a different
|
||||||
|
-- library of the same type (ebooks stay with ebooks, comics with comics).
|
||||||
|
-- Candidates may be active copies (deliberate duplicates - leave alone) or
|
||||||
|
-- hidden/archived rows whose file vanished (move candidates - the scanner
|
||||||
|
-- verifies disk state before repointing).
|
||||||
|
SELECT mi.*, 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
|
||||||
|
JOIN libraries target_lib ON target_lib.id = sqlc.arg('library_id')
|
||||||
|
WHERE mi.file_sha256 = sqlc.arg('file_sha256')
|
||||||
|
AND mi.library_id <> sqlc.arg('library_id')
|
||||||
|
AND lt.id = target_lib.library_type_id
|
||||||
|
ORDER BY (mi.archived_at IS NOT NULL) DESC,
|
||||||
|
mi.missing_scan_count DESC,
|
||||||
|
mi.created_at ASC;
|
||||||
|
|
||||||
-- 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)
|
||||||
-- name: ListMediaItemsBySHA256AndLibrary :many
|
-- name: ListMediaItemsBySHA256AndLibrary :many
|
||||||
SELECT * FROM media_items WHERE file_sha256 = $1 AND library_id = $2 ORDER BY file_path;
|
SELECT * FROM media_items WHERE file_sha256 = $1 AND library_id = $2 ORDER BY file_path;
|
||||||
@@ -2143,6 +2196,8 @@ SELECT ci.*, mi.title, mi.author, mi.cover_image_path, mi.library_id
|
|||||||
FROM collection_items ci
|
FROM collection_items ci
|
||||||
JOIN media_items mi ON ci.media_item_id = mi.id
|
JOIN media_items mi ON ci.media_item_id = mi.id
|
||||||
WHERE ci.collection_id = $1
|
WHERE ci.collection_id = $1
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
ORDER BY ci.added_at DESC;
|
ORDER BY ci.added_at DESC;
|
||||||
|
|
||||||
-- Get collections for book
|
-- Get collections for book
|
||||||
@@ -2449,12 +2504,16 @@ INNER JOIN (
|
|||||||
ORDER BY media_item_id, last_read_at DESC
|
ORDER BY media_item_id, last_read_at DESC
|
||||||
) rp ON rp.media_item_id = mi.id
|
) rp ON rp.media_item_id = mi.id
|
||||||
WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
ORDER BY rp.last_read_at DESC
|
ORDER BY rp.last_read_at DESC
|
||||||
LIMIT sqlc.narg('limit');
|
LIMIT sqlc.narg('limit');
|
||||||
|
|
||||||
-- name: GetRecentlyAddedItems :many
|
-- name: GetRecentlyAddedItems :many
|
||||||
SELECT mi.* FROM media_items mi
|
SELECT mi.* FROM media_items mi
|
||||||
WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
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 sqlc.narg('limit');
|
LIMIT sqlc.narg('limit');
|
||||||
|
|
||||||
@@ -2468,12 +2527,16 @@ INNER JOIN (
|
|||||||
ORDER BY media_item_id, last_read_at DESC
|
ORDER BY media_item_id, last_read_at DESC
|
||||||
) rp ON rp.media_item_id = mi.id
|
) rp ON rp.media_item_id = mi.id
|
||||||
WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
ORDER BY rp.last_read_at DESC
|
ORDER BY rp.last_read_at DESC
|
||||||
LIMIT sqlc.narg('limit');
|
LIMIT sqlc.narg('limit');
|
||||||
|
|
||||||
-- name: GetNotStartedItems :many
|
-- name: GetNotStartedItems :many
|
||||||
SELECT mi.* FROM media_items mi
|
SELECT mi.* FROM media_items mi
|
||||||
WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
AND NOT EXISTS (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM reading_progress rp
|
SELECT 1 FROM reading_progress rp
|
||||||
WHERE rp.media_item_id = mi.id
|
WHERE rp.media_item_id = mi.id
|
||||||
@@ -2488,12 +2551,16 @@ SELECT mi.*, 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 = sqlc.narg('collection_id')
|
WHERE ci.collection_id = sqlc.narg('collection_id')
|
||||||
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 mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
ORDER BY ci.added_at DESC
|
ORDER BY ci.added_at DESC
|
||||||
LIMIT sqlc.narg('limit');
|
LIMIT sqlc.narg('limit');
|
||||||
|
|
||||||
-- name: GetLibraryItems :many
|
-- name: GetLibraryItems :many
|
||||||
SELECT mi.* FROM media_items mi
|
SELECT mi.* FROM media_items mi
|
||||||
WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
||||||
|
AND mi.archived_at IS NULL
|
||||||
|
AND mi.missing_scan_count = 0
|
||||||
ORDER BY mi.created_at DESC;
|
ORDER BY mi.created_at DESC;
|
||||||
|
|
||||||
-- name: GetDistinctSeries :many
|
-- name: GetDistinctSeries :many
|
||||||
|
|||||||
Reference in New Issue
Block a user