feat(series): add SQL queries for series browsing and continue-series
Add five new sqlc queries to support the series browse page and continue-series dashboard collection: - GetDistinctSeries: list unique series with book counts, sorted by most recent entry, with pagination - GetDistinctSeriesCount: total distinct series count for pagination - GetSeriesCovers: fetch up to N cover image paths for a series, ordered by series_number - GetSeriesBooks: fetch all books in a series ordered by series_number - GetContinueSeriesItems: CTE-based query using DISTINCT ON to find the next unread book per series for a given user/library, sorted by most recent last_read_at
This commit is contained in:
@@ -142,6 +142,7 @@ type Querier interface {
|
||||
GetCollectionsForBook(ctx context.Context, mediaItemID pgtype.UUID) ([]Collections, error)
|
||||
// Smart section queries (for system collections)
|
||||
GetContinueReadingItems(ctx context.Context, arg GetContinueReadingItemsParams) ([]MediaItems, error)
|
||||
GetContinueSeriesItems(ctx context.Context, arg GetContinueSeriesItemsParams) ([]GetContinueSeriesItemsRow, error)
|
||||
// ============================================
|
||||
// CAROUSEL-STYLE DASHBOARD
|
||||
// ============================================
|
||||
@@ -167,6 +168,8 @@ type Querier interface {
|
||||
// Get device shelf mappings
|
||||
GetDeviceShelfMappings(ctx context.Context, deviceID pgtype.UUID) ([]GetDeviceShelfMappingsRow, error)
|
||||
GetDictionaryEntry(ctx context.Context, word string) (DictionaryCache, error)
|
||||
GetDistinctSeries(ctx context.Context, arg GetDistinctSeriesParams) ([]GetDistinctSeriesRow, error)
|
||||
GetDistinctSeriesCount(ctx context.Context, libraryID pgtype.UUID) (int32, error)
|
||||
GetFailedSyncQueueItems(ctx context.Context, limit int32) ([]SyncQueue, error)
|
||||
GetKoboEntitlementByContentId(ctx context.Context, arg GetKoboEntitlementByContentIdParams) (GetKoboEntitlementByContentIdRow, error)
|
||||
GetKoboEntitlementByEntitlementId(ctx context.Context, arg GetKoboEntitlementByEntitlementIdParams) (GetKoboEntitlementByEntitlementIdRow, error)
|
||||
@@ -236,6 +239,8 @@ type Querier interface {
|
||||
GetRefreshToken(ctx context.Context, token pgtype.UUID) (GetRefreshTokenRow, error)
|
||||
GetSavedFilterByID(ctx context.Context, arg GetSavedFilterByIDParams) (SavedFilters, error)
|
||||
GetSavedFilters(ctx context.Context, arg GetSavedFiltersParams) ([]SavedFilters, error)
|
||||
GetSeriesBooks(ctx context.Context, arg GetSeriesBooksParams) ([]MediaItems, error)
|
||||
GetSeriesCovers(ctx context.Context, arg GetSeriesCoversParams) ([]GetSeriesCoversRow, error)
|
||||
GetStuckSyncQueueItems(ctx context.Context) ([]SyncQueue, error)
|
||||
GetSyncConflict(ctx context.Context, id pgtype.UUID) (SyncConflicts, error)
|
||||
GetSyncQueueItem(ctx context.Context, id pgtype.UUID) (SyncQueue, error)
|
||||
|
||||
@@ -2411,6 +2411,185 @@ func (q *Queries) GetContinueReadingItems(ctx context.Context, arg GetContinueRe
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetContinueSeriesItems = `-- name: GetContinueSeriesItems :many
|
||||
WITH user_series_progress AS (
|
||||
SELECT mi.series,
|
||||
MAX(mi.series_number) as max_read_number,
|
||||
MAX(rp.last_read_at) as last_read_at
|
||||
FROM reading_progress rp
|
||||
JOIN media_items mi ON mi.id = rp.media_item_id
|
||||
WHERE rp.user_id = $2
|
||||
AND rp.percentage > 0
|
||||
AND mi.series IS NOT NULL AND mi.series != ''
|
||||
AND mi.library_id = $1
|
||||
GROUP BY mi.series
|
||||
),
|
||||
next_books AS (
|
||||
SELECT DISTINCT ON (mi.series) mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.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.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
|
||||
FROM media_items mi
|
||||
JOIN user_series_progress usp ON mi.series = usp.series
|
||||
WHERE mi.library_id = $1
|
||||
AND (mi.series_number > usp.max_read_number OR usp.max_read_number IS NULL)
|
||||
ORDER BY mi.series, mi.series_number ASC NULLS LAST
|
||||
)
|
||||
SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, language, edition, page_count, genre, copyright_year, goodreads_id, openlibrary_id, google_books_id, added_by_admin_id, created_at, 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, 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
|
||||
LIMIT $3
|
||||
`
|
||||
|
||||
type GetContinueSeriesItemsParams struct {
|
||||
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
|
||||
UserID pgtype.UUID `db:"user_id" json:"user_id"`
|
||||
Limit int32 `db:"limit" json:"limit"`
|
||||
}
|
||||
|
||||
type GetContinueSeriesItemsRow struct {
|
||||
ID pgtype.UUID `db:"id" json:"id"`
|
||||
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
|
||||
Title string `db:"title" json:"title"`
|
||||
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"`
|
||||
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"`
|
||||
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"`
|
||||
LastReadAt interface{} `db:"last_read_at" json:"last_read_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetContinueSeriesItems(ctx context.Context, arg GetContinueSeriesItemsParams) ([]GetContinueSeriesItemsRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetContinueSeriesItems, arg.LibraryID, arg.UserID, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []GetContinueSeriesItemsRow{}
|
||||
for rows.Next() {
|
||||
var i GetContinueSeriesItemsRow
|
||||
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.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.ChapterMetadata,
|
||||
&i.LibraryTypeName,
|
||||
&i.TagsSearch,
|
||||
&i.ContributorsSearch,
|
||||
&i.FileSha256,
|
||||
&i.OpfIdentifier,
|
||||
&i.OpfUuid,
|
||||
&i.HashConfidence,
|
||||
&i.LastReadAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetDashboardPreferences = `-- name: GetDashboardPreferences :one
|
||||
|
||||
SELECT id, user_id, library_id, hidden_collections, collection_order, items_per_section, created_at, updated_at FROM user_dashboard_preferences
|
||||
@@ -2830,6 +3009,68 @@ func (q *Queries) GetDictionaryEntry(ctx context.Context, word string) (Dictiona
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetDistinctSeries = `-- name: GetDistinctSeries :many
|
||||
SELECT series, COUNT(*) as book_count,
|
||||
MAX(series_count) as total_in_series,
|
||||
MAX(created_at) as last_entry_at
|
||||
FROM media_items
|
||||
WHERE library_id = $1 AND series IS NOT NULL AND series != ''
|
||||
GROUP BY series
|
||||
ORDER BY MAX(created_at) DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
`
|
||||
|
||||
type GetDistinctSeriesParams struct {
|
||||
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
|
||||
Limit int32 `db:"limit" json:"limit"`
|
||||
Offset int32 `db:"offset" json:"offset"`
|
||||
}
|
||||
|
||||
type GetDistinctSeriesRow struct {
|
||||
Series pgtype.Text `db:"series" json:"series"`
|
||||
BookCount int64 `db:"book_count" json:"book_count"`
|
||||
TotalInSeries interface{} `db:"total_in_series" json:"total_in_series"`
|
||||
LastEntryAt interface{} `db:"last_entry_at" json:"last_entry_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetDistinctSeries(ctx context.Context, arg GetDistinctSeriesParams) ([]GetDistinctSeriesRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetDistinctSeries, arg.LibraryID, arg.Limit, arg.Offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []GetDistinctSeriesRow{}
|
||||
for rows.Next() {
|
||||
var i GetDistinctSeriesRow
|
||||
if err := rows.Scan(
|
||||
&i.Series,
|
||||
&i.BookCount,
|
||||
&i.TotalInSeries,
|
||||
&i.LastEntryAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetDistinctSeriesCount = `-- name: GetDistinctSeriesCount :one
|
||||
SELECT COUNT(DISTINCT series)::int
|
||||
FROM media_items
|
||||
WHERE library_id = $1 AND series IS NOT NULL AND series != ''
|
||||
`
|
||||
|
||||
func (q *Queries) GetDistinctSeriesCount(ctx context.Context, libraryID pgtype.UUID) (int32, error) {
|
||||
row := q.db.QueryRow(ctx, GetDistinctSeriesCount, libraryID)
|
||||
var column_1 int32
|
||||
err := row.Scan(&column_1)
|
||||
return column_1, err
|
||||
}
|
||||
|
||||
const GetFailedSyncQueueItems = `-- name: GetFailedSyncQueueItems :many
|
||||
SELECT id, device_id, media_item_id, sync_type, sync_data, priority, attempts, max_attempts, status, error_message, created_at, processed_at FROM sync_queue
|
||||
WHERE status = 'failed' AND attempts < max_attempts
|
||||
@@ -5217,6 +5458,137 @@ func (q *Queries) GetSavedFilters(ctx context.Context, arg GetSavedFiltersParams
|
||||
return items, nil
|
||||
}
|
||||
|
||||
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, 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, 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 series = $2
|
||||
ORDER BY series_number ASC NULLS LAST
|
||||
`
|
||||
|
||||
type GetSeriesBooksParams struct {
|
||||
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
|
||||
Series pgtype.Text `db:"series" json:"series"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSeriesBooks(ctx context.Context, arg GetSeriesBooksParams) ([]MediaItems, error) {
|
||||
rows, err := q.db.Query(ctx, GetSeriesBooks, arg.LibraryID, arg.Series)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []MediaItems{}
|
||||
for rows.Next() {
|
||||
var i MediaItems
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.LibraryID,
|
||||
&i.Title,
|
||||
&i.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.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.ChapterMetadata,
|
||||
&i.LibraryTypeName,
|
||||
&i.TagsSearch,
|
||||
&i.ContributorsSearch,
|
||||
&i.FileSha256,
|
||||
&i.OpfIdentifier,
|
||||
&i.OpfUuid,
|
||||
&i.HashConfidence,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetSeriesCovers = `-- name: GetSeriesCovers :many
|
||||
SELECT cover_image_path, library_id
|
||||
FROM media_items
|
||||
WHERE library_id = $1 AND series = $2 AND cover_image_path IS NOT NULL AND cover_image_path != ''
|
||||
ORDER BY series_number ASC NULLS LAST
|
||||
LIMIT $3
|
||||
`
|
||||
|
||||
type GetSeriesCoversParams struct {
|
||||
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
|
||||
Series pgtype.Text `db:"series" json:"series"`
|
||||
Limit int32 `db:"limit" json:"limit"`
|
||||
}
|
||||
|
||||
type GetSeriesCoversRow struct {
|
||||
CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"`
|
||||
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetSeriesCovers(ctx context.Context, arg GetSeriesCoversParams) ([]GetSeriesCoversRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetSeriesCovers, arg.LibraryID, arg.Series, arg.Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []GetSeriesCoversRow{}
|
||||
for rows.Next() {
|
||||
var i GetSeriesCoversRow
|
||||
if err := rows.Scan(&i.CoverImagePath, &i.LibraryID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetStuckSyncQueueItems = `-- name: GetStuckSyncQueueItems :many
|
||||
SELECT id, device_id, media_item_id, sync_type, sync_data, priority, attempts, max_attempts, status, error_message, created_at, processed_at FROM sync_queue
|
||||
WHERE status = 'processing' AND created_at < NOW() - INTERVAL '1 hour'
|
||||
|
||||
@@ -1905,6 +1905,59 @@ SELECT mi.* FROM media_items mi
|
||||
WHERE mi.library_id = $1
|
||||
ORDER BY mi.created_at DESC;
|
||||
|
||||
-- name: GetDistinctSeries :many
|
||||
SELECT series, COUNT(*) as book_count,
|
||||
MAX(series_count) as total_in_series,
|
||||
MAX(created_at) as last_entry_at
|
||||
FROM media_items
|
||||
WHERE library_id = $1 AND series IS NOT NULL AND series != ''
|
||||
GROUP BY series
|
||||
ORDER BY MAX(created_at) DESC
|
||||
LIMIT $2 OFFSET $3;
|
||||
|
||||
-- name: GetDistinctSeriesCount :one
|
||||
SELECT COUNT(DISTINCT series)::int
|
||||
FROM media_items
|
||||
WHERE library_id = $1 AND series IS NOT NULL AND series != '';
|
||||
|
||||
-- name: GetSeriesCovers :many
|
||||
SELECT cover_image_path, library_id
|
||||
FROM media_items
|
||||
WHERE library_id = $1 AND series = $2 AND cover_image_path IS NOT NULL AND cover_image_path != ''
|
||||
ORDER BY series_number ASC NULLS LAST
|
||||
LIMIT $3;
|
||||
|
||||
-- name: GetSeriesBooks :many
|
||||
SELECT * FROM media_items
|
||||
WHERE library_id = $1 AND series = $2
|
||||
ORDER BY series_number ASC NULLS LAST;
|
||||
|
||||
-- name: GetContinueSeriesItems :many
|
||||
WITH user_series_progress AS (
|
||||
SELECT mi.series,
|
||||
MAX(mi.series_number) as max_read_number,
|
||||
MAX(rp.last_read_at) as last_read_at
|
||||
FROM reading_progress rp
|
||||
JOIN media_items mi ON mi.id = rp.media_item_id
|
||||
WHERE rp.user_id = $2
|
||||
AND rp.percentage > 0
|
||||
AND mi.series IS NOT NULL AND mi.series != ''
|
||||
AND mi.library_id = $1
|
||||
GROUP BY mi.series
|
||||
),
|
||||
next_books AS (
|
||||
SELECT DISTINCT ON (mi.series) mi.*,
|
||||
usp.last_read_at
|
||||
FROM media_items mi
|
||||
JOIN user_series_progress usp ON mi.series = usp.series
|
||||
WHERE mi.library_id = $1
|
||||
AND (mi.series_number > usp.max_read_number OR usp.max_read_number IS NULL)
|
||||
ORDER BY mi.series, mi.series_number ASC NULLS LAST
|
||||
)
|
||||
SELECT * FROM next_books
|
||||
ORDER BY last_read_at DESC NULLS LAST
|
||||
LIMIT $3;
|
||||
|
||||
-- name: GetSavedFilters :many
|
||||
SELECT * FROM saved_filters
|
||||
WHERE user_id = @user_id AND resource_type = @resource_type
|
||||
|
||||
Reference in New Issue
Block a user