From dd1e56d2f7ef178763ae22d85a42ecadf670f109 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Thu, 19 Feb 2026 20:55:59 -0500 Subject: [PATCH] chore(dashboard): regenerate database code from Phase 3 queries Run sqlc generate to create Go code for dashboard queries: - GetDashboardPreferences / UpsertDashboardPreferences / UpdateDashboardPreferences - GetSystemCollectionsForDashboard / GetUserCollectionsForDashboard - DeleteUserSystemCollection - GetContinueReadingItems / GetRecentlyAddedItems / GetRecentlyReadItems / GetNotStartedItems - GetCollectionItemsForDashboard / GetLibraryItems Auto-generated from queries.sql changes. --- internal/database/querier.go | 18 + internal/database/queries.sql.go | 739 +++++++++++++++++++++++++++++++ 2 files changed, 757 insertions(+) diff --git a/internal/database/querier.go b/internal/database/querier.go index 3fb6773..1affd73 100644 --- a/internal/database/querier.go +++ b/internal/database/querier.go @@ -110,6 +110,7 @@ type Querier interface { // Delete unlinked book DeleteUnlinkedBook(ctx context.Context, id pgtype.UUID) error DeleteUser(ctx context.Context, id pgtype.UUID) error + DeleteUserSystemCollection(ctx context.Context, arg DeleteUserSystemCollectionParams) error GenerateKoboEntitlementId(ctx context.Context) (interface{}, error) // Get all system config GetAllSystemConfig(ctx context.Context) ([]SystemConfig, error) @@ -119,12 +120,20 @@ type Querier interface { GetCollection(ctx context.Context, id pgtype.UUID) (Collections, error) // Get collection items GetCollectionItems(ctx context.Context, collectionID pgtype.UUID) ([]GetCollectionItemsRow, error) + GetCollectionItemsForDashboard(ctx context.Context, arg GetCollectionItemsForDashboardParams) ([]GetCollectionItemsForDashboardRow, error) // Get collection with book count GetCollectionWithBookCount(ctx context.Context, id pgtype.UUID) (GetCollectionWithBookCountRow, error) // Get collections by user GetCollectionsByUser(ctx context.Context, userID pgtype.UUID) ([]Collections, error) // Get collections for book GetCollectionsForBook(ctx context.Context, mediaItemID pgtype.UUID) ([]Collections, error) + // Smart section queries (for system collections) + GetContinueReadingItems(ctx context.Context, arg GetContinueReadingItemsParams) ([]MediaItems, error) + // ============================================ + // CAROUSEL-STYLE DASHBOARD + // ============================================ + // Dashboard preferences queries + GetDashboardPreferences(ctx context.Context, arg GetDashboardPreferencesParams) (UserDashboardPreferences, error) GetDevice(ctx context.Context, id pgtype.UUID) (Devices, error) GetDeviceByAuthToken(ctx context.Context, authToken string) (Devices, error) GetDeviceByIdentifier(ctx context.Context, deviceIdentifier string) (Devices, error) @@ -156,6 +165,7 @@ type Querier interface { GetLibrary(ctx context.Context, id pgtype.UUID) (GetLibraryRow, error) GetLibraryByFolder(ctx context.Context, folderPath string) (GetLibraryByFolderRow, error) GetLibraryFolders(ctx context.Context, libraryID pgtype.UUID) ([]LibraryFolders, error) + GetLibraryItems(ctx context.Context, libraryID pgtype.UUID) ([]MediaItems, error) GetLibraryType(ctx context.Context, id pgtype.UUID) (LibraryTypes, error) GetLibraryTypeByName(ctx context.Context, name string) (LibraryTypes, error) // Library Types queries @@ -187,6 +197,7 @@ type Querier interface { GetMediaRating(ctx context.Context, arg GetMediaRatingParams) (MediaRatings, error) GetMediaRatings(ctx context.Context, mediaItemID pgtype.UUID) ([]GetMediaRatingsRow, error) GetNextRetryTime(ctx context.Context) (interface{}, error) + GetNotStartedItems(ctx context.Context, arg GetNotStartedItemsParams) ([]MediaItems, error) // Get OPDS token GetOpdsToken(ctx context.Context, token string) (GetOpdsTokenRow, error) // Get OPDS tokens by device @@ -196,11 +207,15 @@ type Querier interface { // Get reading history for a user and book GetReadingHistory(ctx context.Context, arg GetReadingHistoryParams) ([]ReadingHistory, error) GetReadingProgress(ctx context.Context, arg GetReadingProgressParams) (ReadingProgress, error) + GetRecentlyAddedItems(ctx context.Context, arg GetRecentlyAddedItemsParams) ([]MediaItems, error) + GetRecentlyReadItems(ctx context.Context, arg GetRecentlyReadItemsParams) ([]MediaItems, error) GetRefreshToken(ctx context.Context, token pgtype.UUID) (GetRefreshTokenRow, 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) GetSyncQueueStats(ctx context.Context, deviceID pgtype.UUID) (GetSyncQueueStatsRow, error) + // Dashboard collections queries + GetSystemCollectionsForDashboard(ctx context.Context) ([]Collections, error) // SYSTEM CONFIG QUERIES // Get system config GetSystemConfig(ctx context.Context, key string) (SystemConfig, error) @@ -218,6 +233,7 @@ type Querier interface { GetUserByEmail(ctx context.Context, email string) (GetUserByEmailRow, error) GetUserByEmailOrUsername(ctx context.Context, email string) (GetUserByEmailOrUsernameRow, error) GetUserByUsername(ctx context.Context, username string) (GetUserByUsernameRow, error) + GetUserCollectionsForDashboard(ctx context.Context, userID pgtype.UUID) ([]Collections, error) // Get user device usage statistics GetUserDeviceUsage(ctx context.Context, userID pgtype.UUID) ([]GetUserDeviceUsageRow, error) GetUserForLogin(ctx context.Context, email string) (GetUserForLoginRow, error) @@ -270,6 +286,7 @@ type Querier interface { SetSystemConfig(ctx context.Context, arg SetSystemConfigParams) (SystemConfig, error) // Update collection UpdateCollection(ctx context.Context, arg UpdateCollectionParams) (Collections, error) + UpdateDashboardPreferences(ctx context.Context, arg UpdateDashboardPreferencesParams) (UserDashboardPreferences, error) UpdateDevice(ctx context.Context, arg UpdateDeviceParams) (Devices, error) UpdateDeviceAuthToken(ctx context.Context, arg UpdateDeviceAuthTokenParams) (Devices, error) // Update device catalog availability @@ -316,6 +333,7 @@ type Querier interface { UpdateUserProfile(ctx context.Context, arg UpdateUserProfileParams) error UpdateUserTheme(ctx context.Context, arg UpdateUserThemeParams) error UpdateUsername(ctx context.Context, arg UpdateUsernameParams) error + UpsertDashboardPreferences(ctx context.Context, arg UpsertDashboardPreferencesParams) (UserDashboardPreferences, error) } var _ Querier = (*Queries)(nil) diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index e353cba..59774c2 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -1387,6 +1387,23 @@ func (q *Queries) DeleteUser(ctx context.Context, id pgtype.UUID) error { return err } +const DeleteUserSystemCollection = `-- name: DeleteUserSystemCollection :exec +DELETE FROM collections +WHERE user_id = $1 + AND name = $2 + AND is_system_collection = true +` + +type DeleteUserSystemCollectionParams struct { + UserID pgtype.UUID `db:"user_id" json:"user_id"` + Name string `db:"name" json:"name"` +} + +func (q *Queries) DeleteUserSystemCollection(ctx context.Context, arg DeleteUserSystemCollectionParams) error { + _, err := q.db.Exec(ctx, DeleteUserSystemCollection, arg.UserID, arg.Name) + return err +} + const GenerateKoboEntitlementId = `-- name: GenerateKoboEntitlementId :one SELECT 'kobo_' || uuid_generate_v4()::TEXT as entitlement_id ` @@ -1623,6 +1640,135 @@ func (q *Queries) GetCollectionItems(ctx context.Context, collectionID pgtype.UU return items, nil } +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.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.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, ci.excluded FROM media_items mi +INNER JOIN collection_items ci ON ci.media_item_id = mi.id +WHERE ci.collection_id = $1 + AND mi.library_id = $2 +ORDER BY ci.added_at DESC +LIMIT $3 +` + +type GetCollectionItemsForDashboardParams struct { + CollectionID pgtype.UUID `db:"collection_id" json:"collection_id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + Limit int32 `db:"limit" json:"limit"` +} + +type GetCollectionItemsForDashboardRow 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"` + 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"` + Excluded pgtype.Bool `db:"excluded" json:"excluded"` +} + +func (q *Queries) GetCollectionItemsForDashboard(ctx context.Context, arg GetCollectionItemsForDashboardParams) ([]GetCollectionItemsForDashboardRow, error) { + rows, err := q.db.Query(ctx, GetCollectionItemsForDashboard, arg.CollectionID, arg.LibraryID, arg.Limit) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetCollectionItemsForDashboardRow{} + for rows.Next() { + var i GetCollectionItemsForDashboardRow + 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.TagsSearch, + &i.ContributorsSearch, + &i.FileSha256, + &i.OpfIdentifier, + &i.OpfUuid, + &i.HashConfidence, + &i.Excluded, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const GetCollectionWithBookCount = `-- name: GetCollectionWithBookCount :one SELECT c.id, c.user_id, c.name, c.description, c.color, c.icon, c.auto_assign_rules, c.view_settings, c.show_on_dashboard, c.query_type, c.priority, c.is_system_collection, c.created_at, @@ -1754,6 +1900,120 @@ func (q *Queries) GetCollectionsForBook(ctx context.Context, mediaItemID pgtype. return items, nil } +const GetContinueReadingItems = `-- name: GetContinueReadingItems :many +SELECT DISTINCT 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.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +INNER JOIN reading_progress rp ON rp.media_item_id = mi.id +WHERE mi.library_id = $1 + AND rp.user_id = $2 + AND rp.percentage > 0 + AND rp.percentage < 1 +ORDER BY rp.last_read_at DESC +LIMIT $3 +` + +type GetContinueReadingItemsParams 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"` +} + +// Smart section queries (for system collections) +func (q *Queries) GetContinueReadingItems(ctx context.Context, arg GetContinueReadingItemsParams) ([]MediaItems, error) { + rows, err := q.db.Query(ctx, GetContinueReadingItems, arg.LibraryID, arg.UserID, arg.Limit) + 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.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 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 +WHERE user_id = $1 AND library_id = $2 +` + +type GetDashboardPreferencesParams struct { + UserID pgtype.UUID `db:"user_id" json:"user_id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` +} + +// ============================================ +// CAROUSEL-STYLE DASHBOARD +// ============================================ +// Dashboard preferences queries +func (q *Queries) GetDashboardPreferences(ctx context.Context, arg GetDashboardPreferencesParams) (UserDashboardPreferences, error) { + row := q.db.QueryRow(ctx, GetDashboardPreferences, arg.UserID, arg.LibraryID) + var i UserDashboardPreferences + err := row.Scan( + &i.ID, + &i.UserID, + &i.LibraryID, + &i.HiddenCollections, + &i.CollectionOrder, + &i.ItemsPerSection, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + const GetDevice = `-- name: GetDevice :one SELECT id, user_id, device_name, device_type, device_identifier, auth_token, last_sync, last_seen, sync_enabled, auto_sync, sync_frequency_minutes, device_metadata, created_at, updated_at FROM devices WHERE id = $1 ` @@ -2639,6 +2899,77 @@ func (q *Queries) GetLibraryFolders(ctx context.Context, libraryID pgtype.UUID) return items, nil } +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.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.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +WHERE mi.library_id = $1 +ORDER BY mi.created_at DESC +` + +func (q *Queries) GetLibraryItems(ctx context.Context, libraryID pgtype.UUID) ([]MediaItems, error) { + rows, err := q.db.Query(ctx, GetLibraryItems, libraryID) + 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.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 GetLibraryType = `-- name: GetLibraryType :one SELECT id, name, description, allowed_extensions, created_at FROM library_types WHERE id = $1 ` @@ -3452,6 +3783,90 @@ func (q *Queries) GetNextRetryTime(ctx context.Context) (interface{}, error) { return next_retry_time, err } +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.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.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +WHERE mi.library_id = $1 + AND NOT EXISTS ( + SELECT 1 FROM reading_progress rp + WHERE rp.media_item_id = mi.id + AND rp.user_id = $2 + AND rp.percentage > 0 + ) +ORDER BY mi.created_at DESC +LIMIT $3 +` + +type GetNotStartedItemsParams 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"` +} + +func (q *Queries) GetNotStartedItems(ctx context.Context, arg GetNotStartedItemsParams) ([]MediaItems, error) { + rows, err := q.db.Query(ctx, GetNotStartedItems, arg.LibraryID, arg.UserID, arg.Limit) + 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.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 GetOpdsToken = `-- name: GetOpdsToken :one SELECT ot.id, ot.device_id, ot.token, ot.token_type, ot.expires_at, ot.created_at, d.device_name, d.device_type FROM opds_tokens ot @@ -3664,6 +4079,164 @@ func (q *Queries) GetReadingProgress(ctx context.Context, arg GetReadingProgress return i, err } +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.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.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +WHERE mi.library_id = $1 +ORDER BY mi.created_at DESC +LIMIT $2 +` + +type GetRecentlyAddedItemsParams struct { + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + Limit int32 `db:"limit" json:"limit"` +} + +func (q *Queries) GetRecentlyAddedItems(ctx context.Context, arg GetRecentlyAddedItemsParams) ([]MediaItems, error) { + rows, err := q.db.Query(ctx, GetRecentlyAddedItems, arg.LibraryID, arg.Limit) + 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.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 GetRecentlyReadItems = `-- name: GetRecentlyReadItems :many +SELECT DISTINCT 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.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi +INNER JOIN reading_progress rp ON rp.media_item_id = mi.id +WHERE mi.library_id = $1 + AND rp.user_id = $2 + AND rp.percentage >= 1 +ORDER BY rp.last_read_at DESC +LIMIT $3 +` + +type GetRecentlyReadItemsParams 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"` +} + +func (q *Queries) GetRecentlyReadItems(ctx context.Context, arg GetRecentlyReadItemsParams) ([]MediaItems, error) { + rows, err := q.db.Query(ctx, GetRecentlyReadItems, arg.LibraryID, arg.UserID, arg.Limit) + 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.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 GetRefreshToken = `-- name: GetRefreshToken :one SELECT rt.id, rt.user_id, rt.token, rt.expires_at, rt.created_at, rt.revoked_at, u.email, u.username, u.role FROM refresh_tokens rt @@ -3817,6 +4390,48 @@ func (q *Queries) GetSyncQueueStats(ctx context.Context, deviceID pgtype.UUID) ( return i, err } +const GetSystemCollectionsForDashboard = `-- name: GetSystemCollectionsForDashboard :many +SELECT id, user_id, name, description, color, icon, auto_assign_rules, view_settings, show_on_dashboard, query_type, priority, is_system_collection, created_at FROM collections +WHERE user_id IS NULL + AND show_on_dashboard = true +ORDER BY priority ASC +` + +// Dashboard collections queries +func (q *Queries) GetSystemCollectionsForDashboard(ctx context.Context) ([]Collections, error) { + rows, err := q.db.Query(ctx, GetSystemCollectionsForDashboard) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Collections{} + for rows.Next() { + var i Collections + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.Name, + &i.Description, + &i.Color, + &i.Icon, + &i.AutoAssignRules, + &i.ViewSettings, + &i.ShowOnDashboard, + &i.QueryType, + &i.Priority, + &i.IsSystemCollection, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const GetSystemConfig = `-- name: GetSystemConfig :one SELECT key, value, updated_at, updated_by FROM system_config WHERE key = $1 @@ -4228,6 +4843,48 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (GetUs return i, err } +const GetUserCollectionsForDashboard = `-- name: GetUserCollectionsForDashboard :many +SELECT c.id, c.user_id, c.name, c.description, c.color, c.icon, c.auto_assign_rules, c.view_settings, c.show_on_dashboard, c.query_type, c.priority, c.is_system_collection, c.created_at FROM collections c +WHERE c.user_id = $1 + AND c.show_on_dashboard = true + AND c.is_system_collection = false +ORDER BY priority ASC +` + +func (q *Queries) GetUserCollectionsForDashboard(ctx context.Context, userID pgtype.UUID) ([]Collections, error) { + rows, err := q.db.Query(ctx, GetUserCollectionsForDashboard, userID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []Collections{} + for rows.Next() { + var i Collections + if err := rows.Scan( + &i.ID, + &i.UserID, + &i.Name, + &i.Description, + &i.Color, + &i.Icon, + &i.AutoAssignRules, + &i.ViewSettings, + &i.ShowOnDashboard, + &i.QueryType, + &i.Priority, + &i.IsSystemCollection, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const GetUserDeviceUsage = `-- name: GetUserDeviceUsage :many SELECT d.id, @@ -6553,6 +7210,46 @@ func (q *Queries) UpdateCollection(ctx context.Context, arg UpdateCollectionPara return i, err } +const UpdateDashboardPreferences = `-- name: UpdateDashboardPreferences :one +UPDATE user_dashboard_preferences +SET hidden_collections = $2, + collection_order = $3, + items_per_section = $4, + updated_at = NOW() +WHERE user_id = $1 AND library_id = $5 +RETURNING id, user_id, library_id, hidden_collections, collection_order, items_per_section, created_at, updated_at +` + +type UpdateDashboardPreferencesParams struct { + UserID pgtype.UUID `db:"user_id" json:"user_id"` + HiddenCollections []string `db:"hidden_collections" json:"hidden_collections"` + CollectionOrder []string `db:"collection_order" json:"collection_order"` + ItemsPerSection pgtype.Int4 `db:"items_per_section" json:"items_per_section"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` +} + +func (q *Queries) UpdateDashboardPreferences(ctx context.Context, arg UpdateDashboardPreferencesParams) (UserDashboardPreferences, error) { + row := q.db.QueryRow(ctx, UpdateDashboardPreferences, + arg.UserID, + arg.HiddenCollections, + arg.CollectionOrder, + arg.ItemsPerSection, + arg.LibraryID, + ) + var i UserDashboardPreferences + err := row.Scan( + &i.ID, + &i.UserID, + &i.LibraryID, + &i.HiddenCollections, + &i.CollectionOrder, + &i.ItemsPerSection, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + const UpdateDevice = `-- name: UpdateDevice :one UPDATE devices SET @@ -7760,3 +8457,45 @@ func (q *Queries) UpdateUsername(ctx context.Context, arg UpdateUsernameParams) _, err := q.db.Exec(ctx, UpdateUsername, arg.ID, arg.Username) return err } + +const UpsertDashboardPreferences = `-- name: UpsertDashboardPreferences :one +INSERT INTO user_dashboard_preferences (user_id, library_id, hidden_collections, collection_order, items_per_section) +VALUES ($1, $2, $3, $4, $5) +ON CONFLICT (user_id, library_id) +DO UPDATE SET + hidden_collections = EXCLUDED.hidden_collections, + collection_order = EXCLUDED.collection_order, + items_per_section = EXCLUDED.items_per_section, + updated_at = NOW() +RETURNING id, user_id, library_id, hidden_collections, collection_order, items_per_section, created_at, updated_at +` + +type UpsertDashboardPreferencesParams struct { + UserID pgtype.UUID `db:"user_id" json:"user_id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + HiddenCollections []string `db:"hidden_collections" json:"hidden_collections"` + CollectionOrder []string `db:"collection_order" json:"collection_order"` + ItemsPerSection pgtype.Int4 `db:"items_per_section" json:"items_per_section"` +} + +func (q *Queries) UpsertDashboardPreferences(ctx context.Context, arg UpsertDashboardPreferencesParams) (UserDashboardPreferences, error) { + row := q.db.QueryRow(ctx, UpsertDashboardPreferences, + arg.UserID, + arg.LibraryID, + arg.HiddenCollections, + arg.CollectionOrder, + arg.ItemsPerSection, + ) + var i UserDashboardPreferences + err := row.Scan( + &i.ID, + &i.UserID, + &i.LibraryID, + &i.HiddenCollections, + &i.CollectionOrder, + &i.ItemsPerSection, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +}