diff --git a/database/schema/schema.sql b/database/schema/schema.sql index d4b0cd6..4f67324 100644 --- a/database/schema/schema.sql +++ b/database/schema/schema.sql @@ -1,5 +1,51 @@ -- Consolidated Bookmann Database Schema --- This file contains the complete current schema for the Bookmann ebook management system +-- This file contains the complete current schema for the Bookmann media library management system + +-- Create library types table +CREATE TABLE library_types ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(50) UNIQUE NOT NULL, + description TEXT, + allowed_extensions TEXT[] NOT NULL, -- Array of allowed file extensions for this type + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Insert default library types +INSERT INTO library_types (name, description, allowed_extensions) VALUES +('ebooks', 'Ebook files including EPUB, PDF, MOBI, etc.', ARRAY['.epub', '.pdf', '.mobi', '.azw', '.azw3', '.txt', '.rtf', '.doc', '.docx', '.lit', '.fb2', '.pdb']), +('comics', 'Comic book archives and image formats', ARRAY['.cbz', '.cbr', '.cb7', '.cbt', '.pdf']), +('manga', 'Manga files including archives and image folders', ARRAY['.cbz', '.cbr', '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp']); + +-- Create libraries table +CREATE TABLE libraries ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + description TEXT, + library_type_id UUID NOT NULL REFERENCES library_types(id) ON DELETE RESTRICT, + created_by_admin_id UUID REFERENCES users(id) ON DELETE SET NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +-- Create library_folders table for multiple folders per library +CREATE TABLE library_folders ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + library_id UUID NOT NULL REFERENCES libraries(id) ON DELETE CASCADE, + folder_path VARCHAR(500) NOT NULL, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + UNIQUE(library_id, folder_path) +); + +-- Create library_visibility table for user-specific library visibility +CREATE TABLE library_visibility ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + library_id UUID NOT NULL REFERENCES libraries(id) ON DELETE CASCADE, + is_visible BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + UNIQUE(user_id, library_id) +); -- Create users table CREATE TABLE users ( @@ -17,12 +63,13 @@ CREATE TABLE users ( updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); --- Create ebooks table -CREATE TABLE ebooks ( +-- Create media_items table (replaces ebooks table for broader media support) +CREATE TABLE media_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + library_id UUID NOT NULL REFERENCES libraries(id) ON DELETE CASCADE, title VARCHAR(255) NOT NULL, author VARCHAR(255), - isbn VARCHAR(13), + isbn VARCHAR(13), -- Still relevant for ebooks description TEXT, file_path VARCHAR(500) NOT NULL, file_size BIGINT, @@ -31,7 +78,7 @@ CREATE TABLE ebooks ( series VARCHAR(255), series_number INTEGER, tags TEXT, - asin VARCHAR(20), + asin VARCHAR(20), -- Still relevant for ebooks date_published DATE, publisher VARCHAR(255), contributors TEXT, @@ -40,57 +87,96 @@ CREATE TABLE ebooks ( updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() ); +-- Create ebooks view for backward compatibility +CREATE VIEW ebooks AS +SELECT mi.* +FROM media_items mi +JOIN libraries l ON mi.library_id = l.id +JOIN library_types lt ON l.library_type_id = lt.id +WHERE lt.name = 'ebooks'; + -- Create reading_progress table CREATE TABLE reading_progress ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - ebook_id UUID NOT NULL REFERENCES ebooks(id) ON DELETE CASCADE, + media_item_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, current_page INTEGER DEFAULT 0, total_pages INTEGER, last_read_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - UNIQUE(ebook_id, user_id) + UNIQUE(media_item_id, user_id) ); --- Create ebook_ratings table -CREATE TABLE ebook_ratings ( +-- Create reading_progress view for backward compatibility +CREATE VIEW ebook_reading_progress AS +SELECT rp.*, + mi.id as ebook_id -- Map media_item_id to ebook_id for compatibility +FROM reading_progress rp +JOIN media_items mi ON rp.media_item_id = mi.id +JOIN libraries l ON mi.library_id = l.id +JOIN library_types lt ON l.library_type_id = lt.id +WHERE lt.name = 'ebooks'; + +-- Create media_ratings table (replaces ebook_ratings) +CREATE TABLE media_ratings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - ebook_id UUID NOT NULL REFERENCES ebooks(id) ON DELETE CASCADE, + media_item_id UUID NOT NULL REFERENCES media_items(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, rating INTEGER NOT NULL CHECK (rating >= 1 AND rating <= 10), -- 10-point scale for half-star precision created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - UNIQUE(ebook_id, user_id) + UNIQUE(media_item_id, user_id) ); --- Create user_ebook_folders table for multiple folders per user -CREATE TABLE user_ebook_folders ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, - folder_path VARCHAR(500) NOT NULL, - created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), - UNIQUE(user_id, folder_path) - -- Note: Admin-only access is enforced at application level in handlers/auth.go -); +-- Create ebook_ratings view for backward compatibility +CREATE VIEW ebook_ratings AS +SELECT mr.*, + mi.id as ebook_id -- Map media_item_id to ebook_id for compatibility +FROM media_ratings mr +JOIN media_items mi ON mr.media_item_id = mi.id +JOIN libraries l ON mi.library_id = l.id +JOIN library_types lt ON l.library_type_id = lt.id +WHERE lt.name = 'ebooks'; + +-- Note: user_ebook_folders table is replaced by library_folders table +-- Libraries now handle folder management instead of individual users -- Create indexes for better query performance CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_users_username ON users(username); -CREATE INDEX idx_ebooks_title ON ebooks(title); -CREATE INDEX idx_ebooks_author ON ebooks(author); -CREATE INDEX idx_ebooks_added_by_admin_id ON ebooks(added_by_admin_id); -CREATE INDEX idx_reading_progress_ebook_id ON reading_progress(ebook_id); +CREATE INDEX idx_library_types_name ON library_types(name); + +-- Library indexes +CREATE INDEX idx_libraries_name ON libraries(name); +CREATE INDEX idx_libraries_library_type_id ON libraries(library_type_id); +CREATE INDEX idx_libraries_created_by_admin_id ON libraries(created_by_admin_id); +CREATE INDEX idx_library_folders_library_id ON library_folders(library_id); +CREATE INDEX idx_library_visibility_user_id ON library_visibility(user_id); +CREATE INDEX idx_library_visibility_library_id ON library_visibility(library_id); + +-- Media items indexes +CREATE INDEX idx_media_items_title ON media_items(title); +CREATE INDEX idx_media_items_author ON media_items(author); +CREATE INDEX idx_media_items_library_id ON media_items(library_id); +CREATE INDEX idx_media_items_added_by_admin_id ON media_items(added_by_admin_id); + +-- Progress and ratings indexes +CREATE INDEX idx_reading_progress_media_item_id ON reading_progress(media_item_id); CREATE INDEX idx_reading_progress_user_id ON reading_progress(user_id); -CREATE INDEX idx_ebook_ratings_ebook_id ON ebook_ratings(ebook_id); -CREATE INDEX idx_ebook_ratings_user_id ON ebook_ratings(user_id); -CREATE INDEX idx_user_ebook_folders_user_id ON user_ebook_folders(user_id); +CREATE INDEX idx_media_ratings_media_item_id ON media_ratings(media_item_id); +CREATE INDEX idx_media_ratings_user_id ON media_ratings(user_id); -- Add comment explaining the rating system -COMMENT ON COLUMN ebook_ratings.rating IS 'Rating scale 1-10 (odd numbers = half-stars: 1,3,5,7,9 = 0.5,1.5,2.5,3.5,4.5 stars)'; +COMMENT ON COLUMN media_ratings.rating IS 'Rating scale 1-10 (odd numbers = half-stars: 1,3,5,7,9 = 0.5,1.5,2.5,3.5,4.5 stars)'; + +-- Library Type File Extensions Notes: +-- - Ebooks: .epub, .pdf, .mobi, .azw, .azw3, .txt, .rtf, .doc, .docx, .lit, .fb2, .pdb +-- - Comics: .cbz, .cbr, .cb7, .cbt, .pdf +-- - Manga: .cbz, .cbr, .png, .jpg, .jpeg, .gif, .bmp, .webp (note: manga includes image folders) -- Role System Notes: -- - All users default to 'user' role --- - Admin users can: add/edit/delete folders, scan ebooks, modify ebook metadata, delete ebooks --- - Regular users can: view all ebooks, rate ebooks, track reading progress, manage their profile +-- - Admin users can: create/manage libraries and folders, scan media, modify media metadata, delete media, control library visibility +-- - Regular users can: view visible libraries, rate media, track reading progress, manage their profile -- - To create first admin: UPDATE users SET role = 'admin' WHERE email = 'your-admin-email'; --- - Only admins can manage folders and ebook metadata (enforced at application level) --- - user_ebook_folders table admin-only access is enforced by AdminMiddleware in handlers \ No newline at end of file +-- - Library visibility is controlled through library_visibility table - admins can hide/show libraries per user +-- - Backward compatibility views (ebooks, ebook_ratings, ebook_reading_progress) maintain existing API contracts \ No newline at end of file diff --git a/internal/database/models.go b/internal/database/models.go index 9d180dd..668a7a6 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -9,17 +9,28 @@ import ( ) type EbookRatings struct { - ID pgtype.UUID `db:"id" json:"id"` - EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"` - UserID pgtype.UUID `db:"user_id" json:"user_id"` - // Rating scale 1-10 (odd numbers = half-stars: 1,3,5,7,9 = 0.5,1.5,2.5,3.5,4.5 stars) - Rating int32 `db:"rating" json:"rating"` - CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` - UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + ID pgtype.UUID `db:"id" json:"id"` + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + Rating int32 `db:"rating" json:"rating"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"` +} + +type EbookReadingProgress struct { + ID pgtype.UUID `db:"id" json:"id"` + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + CurrentPage pgtype.Int4 `db:"current_page" json:"current_page"` + TotalPages pgtype.Int4 `db:"total_pages" json:"total_pages"` + LastReadAt pgtype.Timestamptz `db:"last_read_at" json:"last_read_at"` + EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"` } type Ebooks 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"` @@ -40,22 +51,82 @@ type Ebooks struct { UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` } +type Libraries struct { + ID pgtype.UUID `db:"id" json:"id"` + Name string `db:"name" json:"name"` + Description pgtype.Text `db:"description" json:"description"` + LibraryTypeID pgtype.UUID `db:"library_type_id" json:"library_type_id"` + CreatedByAdminID pgtype.UUID `db:"created_by_admin_id" json:"created_by_admin_id"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` +} + +type LibraryFolders struct { + ID pgtype.UUID `db:"id" json:"id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + FolderPath string `db:"folder_path" json:"folder_path"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` +} + +type LibraryTypes struct { + ID pgtype.UUID `db:"id" json:"id"` + Name string `db:"name" json:"name"` + Description pgtype.Text `db:"description" json:"description"` + AllowedExtensions []string `db:"allowed_extensions" json:"allowed_extensions"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` +} + +type LibraryVisibility struct { + ID pgtype.UUID `db:"id" json:"id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + IsVisible bool `db:"is_visible" json:"is_visible"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` +} + +type MediaItems 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 pgtype.Text `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 pgtype.Text `db:"contributors" json:"contributors"` + 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"` +} + +type MediaRatings struct { + ID pgtype.UUID `db:"id" json:"id"` + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + // Rating scale 1-10 (odd numbers = half-stars: 1,3,5,7,9 = 0.5,1.5,2.5,3.5,4.5 stars) + Rating int32 `db:"rating" json:"rating"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` +} + type ReadingProgress struct { ID pgtype.UUID `db:"id" json:"id"` - EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"` + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` UserID pgtype.UUID `db:"user_id" json:"user_id"` CurrentPage pgtype.Int4 `db:"current_page" json:"current_page"` TotalPages pgtype.Int4 `db:"total_pages" json:"total_pages"` LastReadAt pgtype.Timestamptz `db:"last_read_at" json:"last_read_at"` } -type UserEbookFolders struct { - ID pgtype.UUID `db:"id" json:"id"` - UserID pgtype.UUID `db:"user_id" json:"user_id"` - FolderPath string `db:"folder_path" json:"folder_path"` - CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` -} - type Users struct { ID pgtype.UUID `db:"id" json:"id"` Email string `db:"email" json:"email"` diff --git a/internal/database/querier.go b/internal/database/querier.go index cf15217..0937fa3 100644 --- a/internal/database/querier.go +++ b/internal/database/querier.go @@ -11,33 +11,65 @@ import ( ) type Querier interface { - AddUserEbookFolder(ctx context.Context, arg AddUserEbookFolderParams) (UserEbookFolders, error) - CreateEbook(ctx context.Context, arg CreateEbookParams) (Ebooks, error) - CreateEbookRating(ctx context.Context, arg CreateEbookRatingParams) (EbookRatings, error) + // Library Folders queries + AddLibraryFolder(ctx context.Context, arg AddLibraryFolderParams) (LibraryFolders, error) + CreateEbook(ctx context.Context, arg CreateEbookParams) (MediaItems, error) + // Backward compatibility - Ebooks ratings (using views) + CreateEbookRating(ctx context.Context, arg CreateEbookRatingParams) (MediaRatings, error) + // Libraries queries + CreateLibrary(ctx context.Context, arg CreateLibraryParams) (Libraries, error) + // Media Items queries + CreateMediaItem(ctx context.Context, arg CreateMediaItemParams) (MediaItems, error) + CreateMediaRating(ctx context.Context, arg CreateMediaRatingParams) (MediaRatings, error) CreateUser(ctx context.Context, arg CreateUserParams) (CreateUserRow, error) DeleteEbook(ctx context.Context, id pgtype.UUID) error DeleteEbookRating(ctx context.Context, arg DeleteEbookRatingParams) error + DeleteLibrary(ctx context.Context, id pgtype.UUID) error + DeleteLibraryFolder(ctx context.Context, arg DeleteLibraryFolderParams) (LibraryFolders, error) + DeleteMediaItem(ctx context.Context, id pgtype.UUID) error + DeleteMediaRating(ctx context.Context, arg DeleteMediaRatingParams) error DeleteReadingProgress(ctx context.Context, arg DeleteReadingProgressParams) error DeleteUser(ctx context.Context, id pgtype.UUID) error - DeleteUserEbookFolder(ctx context.Context, arg DeleteUserEbookFolderParams) (UserEbookFolders, error) + // Backward compatibility - Ebooks queries (using views) GetEbook(ctx context.Context, id pgtype.UUID) (Ebooks, error) + // Note: User ebook folders replaced by library folders system + // Legacy folder management is now handled through libraries GetEbookByFilePath(ctx context.Context, filePath string) (Ebooks, error) GetEbookRating(ctx context.Context, arg GetEbookRatingParams) (EbookRatings, error) GetEbookRatings(ctx context.Context, ebookID pgtype.UUID) ([]GetEbookRatingsRow, error) + GetLibrary(ctx context.Context, id pgtype.UUID) (GetLibraryRow, error) + GetLibraryFolders(ctx context.Context, libraryID pgtype.UUID) ([]LibraryFolders, error) + GetLibraryType(ctx context.Context, id pgtype.UUID) (LibraryTypes, error) + GetLibraryTypeByName(ctx context.Context, name string) (LibraryTypes, error) + // Library Types queries + GetLibraryTypes(ctx context.Context) ([]LibraryTypes, error) + GetLibraryVisibility(ctx context.Context, arg GetLibraryVisibilityParams) (LibraryVisibility, error) + GetMediaItem(ctx context.Context, id pgtype.UUID) (MediaItems, error) + GetMediaItemByFilePath(ctx context.Context, filePath string) (MediaItems, error) + GetMediaRating(ctx context.Context, arg GetMediaRatingParams) (MediaRatings, error) + GetMediaRatings(ctx context.Context, mediaItemID pgtype.UUID) ([]GetMediaRatingsRow, error) GetReadingProgress(ctx context.Context, arg GetReadingProgressParams) (ReadingProgress, error) GetScanSettings(ctx context.Context, id pgtype.UUID) (GetScanSettingsRow, error) GetUser(ctx context.Context, id pgtype.UUID) (GetUserRow, error) GetUserByEmail(ctx context.Context, email string) (GetUserByEmailRow, error) GetUserByEmailOrUsername(ctx context.Context, email string) (GetUserByEmailOrUsernameRow, error) GetUserByUsername(ctx context.Context, username string) (GetUserByUsernameRow, error) - GetUserEbookFolders(ctx context.Context, userID pgtype.UUID) ([]UserEbookFolders, error) GetUserForLogin(ctx context.Context, email string) (GetUserForLoginRow, error) GetUserPasswordHash(ctx context.Context, id pgtype.UUID) (string, error) + GetUserVisibleLibraries(ctx context.Context, userID pgtype.UUID) ([]GetUserVisibleLibrariesRow, error) ListEbooks(ctx context.Context, arg ListEbooksParams) ([]Ebooks, error) + ListLibraries(ctx context.Context) ([]ListLibrariesRow, error) + ListMediaItems(ctx context.Context, arg ListMediaItemsParams) ([]ListMediaItemsRow, error) + ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype.UUID) ([]ListMediaItemsByLibraryRow, error) ListUsers(ctx context.Context) ([]ListUsersRow, error) - UpdateEbook(ctx context.Context, arg UpdateEbookParams) (Ebooks, error) - UpdateEbookRating(ctx context.Context, arg UpdateEbookRatingParams) (EbookRatings, error) + // Library Visibility queries + SetLibraryVisibility(ctx context.Context, arg SetLibraryVisibilityParams) (LibraryVisibility, error) + UpdateEbook(ctx context.Context, arg UpdateEbookParams) (MediaItems, error) + UpdateEbookRating(ctx context.Context, arg UpdateEbookRatingParams) (MediaRatings, error) UpdateEmail(ctx context.Context, arg UpdateEmailParams) error + UpdateLibrary(ctx context.Context, arg UpdateLibraryParams) (Libraries, error) + UpdateMediaItem(ctx context.Context, arg UpdateMediaItemParams) (MediaItems, error) + UpdateMediaRating(ctx context.Context, arg UpdateMediaRatingParams) (MediaRatings, error) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) error UpdateReadingProgress(ctx context.Context, arg UpdateReadingProgressParams) (ReadingProgress, error) UpdateScanSettings(ctx context.Context, arg UpdateScanSettingsParams) error diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index f2d43bc..2ea9f07 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -11,21 +11,22 @@ import ( "github.com/jackc/pgx/v5/pgtype" ) -const AddUserEbookFolder = `-- name: AddUserEbookFolder :one -INSERT INTO user_ebook_folders (user_id, folder_path) VALUES ($1, $2) RETURNING id, user_id, folder_path, created_at +const AddLibraryFolder = `-- name: AddLibraryFolder :one +INSERT INTO library_folders (library_id, folder_path) VALUES ($1, $2) RETURNING id, library_id, folder_path, created_at ` -type AddUserEbookFolderParams struct { - UserID pgtype.UUID `db:"user_id" json:"user_id"` +type AddLibraryFolderParams struct { + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` FolderPath string `db:"folder_path" json:"folder_path"` } -func (q *Queries) AddUserEbookFolder(ctx context.Context, arg AddUserEbookFolderParams) (UserEbookFolders, error) { - row := q.db.QueryRow(ctx, AddUserEbookFolder, arg.UserID, arg.FolderPath) - var i UserEbookFolders +// Library Folders queries +func (q *Queries) AddLibraryFolder(ctx context.Context, arg AddLibraryFolderParams) (LibraryFolders, error) { + row := q.db.QueryRow(ctx, AddLibraryFolder, arg.LibraryID, arg.FolderPath) + var i LibraryFolders err := row.Scan( &i.ID, - &i.UserID, + &i.LibraryID, &i.FolderPath, &i.CreatedAt, ) @@ -33,9 +34,9 @@ func (q *Queries) AddUserEbookFolder(ctx context.Context, arg AddUserEbookFolder } const CreateEbook = `-- name: CreateEbook :one -INSERT INTO ebooks (title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) -RETURNING id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id, created_at, updated_at +INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id) +VALUES ((SELECT id FROM libraries WHERE library_type_id = (SELECT id FROM library_types WHERE name = 'ebooks') LIMIT 1), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) +RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id, created_at, updated_at ` type CreateEbookParams struct { @@ -57,7 +58,7 @@ type CreateEbookParams struct { AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"` } -func (q *Queries) CreateEbook(ctx context.Context, arg CreateEbookParams) (Ebooks, error) { +func (q *Queries) CreateEbook(ctx context.Context, arg CreateEbookParams) (MediaItems, error) { row := q.db.QueryRow(ctx, CreateEbook, arg.Title, arg.Author, @@ -76,9 +77,10 @@ func (q *Queries) CreateEbook(ctx context.Context, arg CreateEbookParams) (Ebook arg.Contributors, arg.AddedByAdminID, ) - var i Ebooks + var i MediaItems err := row.Scan( &i.ID, + &i.LibraryID, &i.Title, &i.Author, &i.Isbn, @@ -102,27 +104,166 @@ func (q *Queries) CreateEbook(ctx context.Context, arg CreateEbookParams) (Ebook } const CreateEbookRating = `-- name: CreateEbookRating :one -INSERT INTO ebook_ratings (ebook_id, user_id, rating) -VALUES ($1, $2, $3) -ON CONFLICT (ebook_id, user_id) +INSERT INTO media_ratings (media_item_id, user_id, rating) +SELECT $1, $2, $3 +WHERE EXISTS (SELECT 1 FROM media_items WHERE id = $1) +ON CONFLICT (media_item_id, user_id) DO UPDATE SET rating = EXCLUDED.rating, updated_at = NOW() -RETURNING id, ebook_id, user_id, rating, created_at, updated_at +RETURNING id, media_item_id, user_id, rating, created_at, updated_at ` type CreateEbookRatingParams struct { - EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"` - UserID pgtype.UUID `db:"user_id" json:"user_id"` - Rating int32 `db:"rating" json:"rating"` + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + Rating int32 `db:"rating" json:"rating"` } -func (q *Queries) CreateEbookRating(ctx context.Context, arg CreateEbookRatingParams) (EbookRatings, error) { - row := q.db.QueryRow(ctx, CreateEbookRating, arg.EbookID, arg.UserID, arg.Rating) - var i EbookRatings +// Backward compatibility - Ebooks ratings (using views) +func (q *Queries) CreateEbookRating(ctx context.Context, arg CreateEbookRatingParams) (MediaRatings, error) { + row := q.db.QueryRow(ctx, CreateEbookRating, arg.MediaItemID, arg.UserID, arg.Rating) + var i MediaRatings err := row.Scan( &i.ID, - &i.EbookID, + &i.MediaItemID, + &i.UserID, + &i.Rating, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const CreateLibrary = `-- name: CreateLibrary :one +INSERT INTO libraries (name, description, library_type_id, created_by_admin_id) +VALUES ($1, $2, $3, $4) +RETURNING id, name, description, library_type_id, created_by_admin_id, created_at, updated_at +` + +type CreateLibraryParams struct { + Name string `db:"name" json:"name"` + Description pgtype.Text `db:"description" json:"description"` + LibraryTypeID pgtype.UUID `db:"library_type_id" json:"library_type_id"` + CreatedByAdminID pgtype.UUID `db:"created_by_admin_id" json:"created_by_admin_id"` +} + +// Libraries queries +func (q *Queries) CreateLibrary(ctx context.Context, arg CreateLibraryParams) (Libraries, error) { + row := q.db.QueryRow(ctx, CreateLibrary, + arg.Name, + arg.Description, + arg.LibraryTypeID, + arg.CreatedByAdminID, + ) + var i Libraries + err := row.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.LibraryTypeID, + &i.CreatedByAdminID, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const CreateMediaItem = `-- name: CreateMediaItem :one +INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) +RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id, created_at, updated_at +` + +type CreateMediaItemParams struct { + 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 pgtype.Text `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 pgtype.Text `db:"contributors" json:"contributors"` + AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"` +} + +// Media Items queries +func (q *Queries) CreateMediaItem(ctx context.Context, arg CreateMediaItemParams) (MediaItems, error) { + row := q.db.QueryRow(ctx, CreateMediaItem, + arg.LibraryID, + arg.Title, + arg.Author, + arg.Isbn, + arg.Description, + arg.FilePath, + arg.FileSize, + arg.MimeType, + arg.CoverImagePath, + arg.Series, + arg.SeriesNumber, + arg.Tags, + arg.Asin, + arg.DatePublished, + arg.Publisher, + arg.Contributors, + arg.AddedByAdminID, + ) + var i MediaItems + err := row.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.AddedByAdminID, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const CreateMediaRating = `-- name: CreateMediaRating :one +INSERT INTO media_ratings (media_item_id, user_id, rating) +VALUES ($1, $2, $3) +ON CONFLICT (media_item_id, user_id) +DO UPDATE SET + rating = EXCLUDED.rating, + updated_at = NOW() +RETURNING id, media_item_id, user_id, rating, created_at, updated_at +` + +type CreateMediaRatingParams struct { + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + Rating int32 `db:"rating" json:"rating"` +} + +func (q *Queries) CreateMediaRating(ctx context.Context, arg CreateMediaRatingParams) (MediaRatings, error) { + row := q.db.QueryRow(ctx, CreateMediaRating, arg.MediaItemID, arg.UserID, arg.Rating) + var i MediaRatings + err := row.Scan( + &i.ID, + &i.MediaItemID, &i.UserID, &i.Rating, &i.CreatedAt, @@ -185,7 +326,7 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (CreateU } const DeleteEbook = `-- name: DeleteEbook :exec -DELETE FROM ebooks WHERE id = $1 +DELETE FROM media_items WHERE id = $1 ` func (q *Queries) DeleteEbook(ctx context.Context, id pgtype.UUID) error { @@ -194,30 +335,83 @@ func (q *Queries) DeleteEbook(ctx context.Context, id pgtype.UUID) error { } const DeleteEbookRating = `-- name: DeleteEbookRating :exec -DELETE FROM ebook_ratings WHERE ebook_id = $1 AND user_id = $2 +DELETE FROM media_ratings WHERE media_item_id = $1 AND user_id = $2 ` type DeleteEbookRatingParams struct { - EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"` - UserID pgtype.UUID `db:"user_id" json:"user_id"` + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` } func (q *Queries) DeleteEbookRating(ctx context.Context, arg DeleteEbookRatingParams) error { - _, err := q.db.Exec(ctx, DeleteEbookRating, arg.EbookID, arg.UserID) + _, err := q.db.Exec(ctx, DeleteEbookRating, arg.MediaItemID, arg.UserID) + return err +} + +const DeleteLibrary = `-- name: DeleteLibrary :exec +DELETE FROM libraries WHERE id = $1 +` + +func (q *Queries) DeleteLibrary(ctx context.Context, id pgtype.UUID) error { + _, err := q.db.Exec(ctx, DeleteLibrary, id) + return err +} + +const DeleteLibraryFolder = `-- name: DeleteLibraryFolder :one +DELETE FROM library_folders WHERE library_id = $1 AND folder_path = $2 RETURNING id, library_id, folder_path, created_at +` + +type DeleteLibraryFolderParams struct { + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + FolderPath string `db:"folder_path" json:"folder_path"` +} + +func (q *Queries) DeleteLibraryFolder(ctx context.Context, arg DeleteLibraryFolderParams) (LibraryFolders, error) { + row := q.db.QueryRow(ctx, DeleteLibraryFolder, arg.LibraryID, arg.FolderPath) + var i LibraryFolders + err := row.Scan( + &i.ID, + &i.LibraryID, + &i.FolderPath, + &i.CreatedAt, + ) + return i, err +} + +const DeleteMediaItem = `-- name: DeleteMediaItem :exec +DELETE FROM media_items WHERE id = $1 +` + +func (q *Queries) DeleteMediaItem(ctx context.Context, id pgtype.UUID) error { + _, err := q.db.Exec(ctx, DeleteMediaItem, id) + return err +} + +const DeleteMediaRating = `-- name: DeleteMediaRating :exec +DELETE FROM media_ratings WHERE media_item_id = $1 AND user_id = $2 +` + +type DeleteMediaRatingParams struct { + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` +} + +func (q *Queries) DeleteMediaRating(ctx context.Context, arg DeleteMediaRatingParams) error { + _, err := q.db.Exec(ctx, DeleteMediaRating, arg.MediaItemID, arg.UserID) return err } const DeleteReadingProgress = `-- name: DeleteReadingProgress :exec -DELETE FROM reading_progress WHERE ebook_id = $1 AND user_id = $2 +DELETE FROM reading_progress WHERE media_item_id = $1 AND user_id = $2 ` type DeleteReadingProgressParams struct { - EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"` - UserID pgtype.UUID `db:"user_id" json:"user_id"` + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` } func (q *Queries) DeleteReadingProgress(ctx context.Context, arg DeleteReadingProgressParams) error { - _, err := q.db.Exec(ctx, DeleteReadingProgress, arg.EbookID, arg.UserID) + _, err := q.db.Exec(ctx, DeleteReadingProgress, arg.MediaItemID, arg.UserID) return err } @@ -230,36 +424,17 @@ func (q *Queries) DeleteUser(ctx context.Context, id pgtype.UUID) error { return err } -const DeleteUserEbookFolder = `-- name: DeleteUserEbookFolder :one -DELETE FROM user_ebook_folders WHERE user_id = $1 AND folder_path = $2 RETURNING id, user_id, folder_path, created_at -` - -type DeleteUserEbookFolderParams struct { - UserID pgtype.UUID `db:"user_id" json:"user_id"` - FolderPath string `db:"folder_path" json:"folder_path"` -} - -func (q *Queries) DeleteUserEbookFolder(ctx context.Context, arg DeleteUserEbookFolderParams) (UserEbookFolders, error) { - row := q.db.QueryRow(ctx, DeleteUserEbookFolder, arg.UserID, arg.FolderPath) - var i UserEbookFolders - err := row.Scan( - &i.ID, - &i.UserID, - &i.FolderPath, - &i.CreatedAt, - ) - return i, err -} - const GetEbook = `-- name: GetEbook :one -SELECT id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id, created_at, updated_at FROM ebooks WHERE id = $1 +SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id, created_at, updated_at FROM ebooks WHERE id = $1 ` +// Backward compatibility - Ebooks queries (using views) func (q *Queries) GetEbook(ctx context.Context, id pgtype.UUID) (Ebooks, error) { row := q.db.QueryRow(ctx, GetEbook, id) var i Ebooks err := row.Scan( &i.ID, + &i.LibraryID, &i.Title, &i.Author, &i.Isbn, @@ -283,14 +458,18 @@ func (q *Queries) GetEbook(ctx context.Context, id pgtype.UUID) (Ebooks, error) } const GetEbookByFilePath = `-- name: GetEbookByFilePath :one -SELECT id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id, created_at, updated_at FROM ebooks WHERE file_path = $1 + +SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id, created_at, updated_at FROM ebooks WHERE file_path = $1 ` +// Note: User ebook folders replaced by library folders system +// Legacy folder management is now handled through libraries func (q *Queries) GetEbookByFilePath(ctx context.Context, filePath string) (Ebooks, error) { row := q.db.QueryRow(ctx, GetEbookByFilePath, filePath) var i Ebooks err := row.Scan( &i.ID, + &i.LibraryID, &i.Title, &i.Author, &i.Isbn, @@ -314,7 +493,7 @@ func (q *Queries) GetEbookByFilePath(ctx context.Context, filePath string) (Eboo } const GetEbookRating = `-- name: GetEbookRating :one -SELECT id, ebook_id, user_id, rating, created_at, updated_at FROM ebook_ratings WHERE ebook_id = $1 AND user_id = $2 +SELECT id, media_item_id, user_id, rating, created_at, updated_at, ebook_id FROM ebook_ratings WHERE ebook_id = $1 AND user_id = $2 ` type GetEbookRatingParams struct { @@ -327,17 +506,18 @@ func (q *Queries) GetEbookRating(ctx context.Context, arg GetEbookRatingParams) var i EbookRatings err := row.Scan( &i.ID, - &i.EbookID, + &i.MediaItemID, &i.UserID, &i.Rating, &i.CreatedAt, &i.UpdatedAt, + &i.EbookID, ) return i, err } const GetEbookRatings = `-- name: GetEbookRatings :many -SELECT er.id, er.ebook_id, er.user_id, er.rating, er.created_at, er.updated_at, u.username +SELECT er.id, er.media_item_id, er.user_id, er.rating, er.created_at, er.updated_at, er.ebook_id, u.username FROM ebook_ratings er JOIN users u ON er.user_id = u.id WHERE er.ebook_id = $1 @@ -345,13 +525,14 @@ ORDER BY er.created_at DESC ` type GetEbookRatingsRow struct { - ID pgtype.UUID `db:"id" json:"id"` - EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"` - UserID pgtype.UUID `db:"user_id" json:"user_id"` - Rating int32 `db:"rating" json:"rating"` - CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` - UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` - Username string `db:"username" json:"username"` + ID pgtype.UUID `db:"id" json:"id"` + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + Rating int32 `db:"rating" json:"rating"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"` + Username string `db:"username" json:"username"` } func (q *Queries) GetEbookRatings(ctx context.Context, ebookID pgtype.UUID) ([]GetEbookRatingsRow, error) { @@ -365,7 +546,294 @@ func (q *Queries) GetEbookRatings(ctx context.Context, ebookID pgtype.UUID) ([]G var i GetEbookRatingsRow if err := rows.Scan( &i.ID, + &i.MediaItemID, + &i.UserID, + &i.Rating, + &i.CreatedAt, + &i.UpdatedAt, &i.EbookID, + &i.Username, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetLibrary = `-- name: GetLibrary :one +SELECT l.id, l.name, l.description, l.library_type_id, l.created_by_admin_id, l.created_at, l.updated_at, lt.name as type_name, lt.description as type_description +FROM libraries l +JOIN library_types lt ON l.library_type_id = lt.id +WHERE l.id = $1 +` + +type GetLibraryRow struct { + ID pgtype.UUID `db:"id" json:"id"` + Name string `db:"name" json:"name"` + Description pgtype.Text `db:"description" json:"description"` + LibraryTypeID pgtype.UUID `db:"library_type_id" json:"library_type_id"` + CreatedByAdminID pgtype.UUID `db:"created_by_admin_id" json:"created_by_admin_id"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + TypeName string `db:"type_name" json:"type_name"` + TypeDescription pgtype.Text `db:"type_description" json:"type_description"` +} + +func (q *Queries) GetLibrary(ctx context.Context, id pgtype.UUID) (GetLibraryRow, error) { + row := q.db.QueryRow(ctx, GetLibrary, id) + var i GetLibraryRow + err := row.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.LibraryTypeID, + &i.CreatedByAdminID, + &i.CreatedAt, + &i.UpdatedAt, + &i.TypeName, + &i.TypeDescription, + ) + return i, err +} + +const GetLibraryFolders = `-- name: GetLibraryFolders :many +SELECT id, library_id, folder_path, created_at FROM library_folders WHERE library_id = $1 ORDER BY created_at +` + +func (q *Queries) GetLibraryFolders(ctx context.Context, libraryID pgtype.UUID) ([]LibraryFolders, error) { + rows, err := q.db.Query(ctx, GetLibraryFolders, libraryID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []LibraryFolders{} + for rows.Next() { + var i LibraryFolders + if err := rows.Scan( + &i.ID, + &i.LibraryID, + &i.FolderPath, + &i.CreatedAt, + ); 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 +` + +func (q *Queries) GetLibraryType(ctx context.Context, id pgtype.UUID) (LibraryTypes, error) { + row := q.db.QueryRow(ctx, GetLibraryType, id) + var i LibraryTypes + err := row.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.AllowedExtensions, + &i.CreatedAt, + ) + return i, err +} + +const GetLibraryTypeByName = `-- name: GetLibraryTypeByName :one +SELECT id, name, description, allowed_extensions, created_at FROM library_types WHERE name = $1 +` + +func (q *Queries) GetLibraryTypeByName(ctx context.Context, name string) (LibraryTypes, error) { + row := q.db.QueryRow(ctx, GetLibraryTypeByName, name) + var i LibraryTypes + err := row.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.AllowedExtensions, + &i.CreatedAt, + ) + return i, err +} + +const GetLibraryTypes = `-- name: GetLibraryTypes :many +SELECT id, name, description, allowed_extensions, created_at FROM library_types ORDER BY name +` + +// Library Types queries +func (q *Queries) GetLibraryTypes(ctx context.Context) ([]LibraryTypes, error) { + rows, err := q.db.Query(ctx, GetLibraryTypes) + if err != nil { + return nil, err + } + defer rows.Close() + items := []LibraryTypes{} + for rows.Next() { + var i LibraryTypes + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.AllowedExtensions, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const GetLibraryVisibility = `-- name: GetLibraryVisibility :one +SELECT id, user_id, library_id, is_visible, created_at, updated_at FROM library_visibility WHERE user_id = $1 AND library_id = $2 +` + +type GetLibraryVisibilityParams struct { + UserID pgtype.UUID `db:"user_id" json:"user_id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` +} + +func (q *Queries) GetLibraryVisibility(ctx context.Context, arg GetLibraryVisibilityParams) (LibraryVisibility, error) { + row := q.db.QueryRow(ctx, GetLibraryVisibility, arg.UserID, arg.LibraryID) + var i LibraryVisibility + err := row.Scan( + &i.ID, + &i.UserID, + &i.LibraryID, + &i.IsVisible, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const GetMediaItem = `-- name: GetMediaItem :one +SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id, created_at, updated_at FROM media_items WHERE id = $1 +` + +func (q *Queries) GetMediaItem(ctx context.Context, id pgtype.UUID) (MediaItems, error) { + row := q.db.QueryRow(ctx, GetMediaItem, id) + var i MediaItems + err := row.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.AddedByAdminID, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const GetMediaItemByFilePath = `-- name: GetMediaItemByFilePath :one +SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id, created_at, updated_at FROM media_items WHERE file_path = $1 +` + +func (q *Queries) GetMediaItemByFilePath(ctx context.Context, filePath string) (MediaItems, error) { + row := q.db.QueryRow(ctx, GetMediaItemByFilePath, filePath) + var i MediaItems + err := row.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.AddedByAdminID, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const GetMediaRating = `-- name: GetMediaRating :one +SELECT id, media_item_id, user_id, rating, created_at, updated_at FROM media_ratings WHERE media_item_id = $1 AND user_id = $2 +` + +type GetMediaRatingParams struct { + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` +} + +func (q *Queries) GetMediaRating(ctx context.Context, arg GetMediaRatingParams) (MediaRatings, error) { + row := q.db.QueryRow(ctx, GetMediaRating, arg.MediaItemID, arg.UserID) + var i MediaRatings + err := row.Scan( + &i.ID, + &i.MediaItemID, + &i.UserID, + &i.Rating, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const GetMediaRatings = `-- name: GetMediaRatings :many +SELECT mr.id, mr.media_item_id, mr.user_id, mr.rating, mr.created_at, mr.updated_at, u.username +FROM media_ratings mr +JOIN users u ON mr.user_id = u.id +WHERE mr.media_item_id = $1 +ORDER BY mr.created_at DESC +` + +type GetMediaRatingsRow struct { + ID pgtype.UUID `db:"id" json:"id"` + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + Rating int32 `db:"rating" json:"rating"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + Username string `db:"username" json:"username"` +} + +func (q *Queries) GetMediaRatings(ctx context.Context, mediaItemID pgtype.UUID) ([]GetMediaRatingsRow, error) { + rows, err := q.db.Query(ctx, GetMediaRatings, mediaItemID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetMediaRatingsRow{} + for rows.Next() { + var i GetMediaRatingsRow + if err := rows.Scan( + &i.ID, + &i.MediaItemID, &i.UserID, &i.Rating, &i.CreatedAt, @@ -383,20 +851,20 @@ func (q *Queries) GetEbookRatings(ctx context.Context, ebookID pgtype.UUID) ([]G } const GetReadingProgress = `-- name: GetReadingProgress :one -SELECT id, ebook_id, user_id, current_page, total_pages, last_read_at FROM reading_progress WHERE ebook_id = $1 AND user_id = $2 +SELECT id, media_item_id, user_id, current_page, total_pages, last_read_at FROM reading_progress WHERE media_item_id = $1 AND user_id = $2 ` type GetReadingProgressParams struct { - EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"` - UserID pgtype.UUID `db:"user_id" json:"user_id"` + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` } func (q *Queries) GetReadingProgress(ctx context.Context, arg GetReadingProgressParams) (ReadingProgress, error) { - row := q.db.QueryRow(ctx, GetReadingProgress, arg.EbookID, arg.UserID) + row := q.db.QueryRow(ctx, GetReadingProgress, arg.MediaItemID, arg.UserID) var i ReadingProgress err := row.Scan( &i.ID, - &i.EbookID, + &i.MediaItemID, &i.UserID, &i.CurrentPage, &i.TotalPages, @@ -553,35 +1021,6 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (GetUs return i, err } -const GetUserEbookFolders = `-- name: GetUserEbookFolders :many -SELECT id, user_id, folder_path, created_at FROM user_ebook_folders WHERE user_id = $1 ORDER BY created_at -` - -func (q *Queries) GetUserEbookFolders(ctx context.Context, userID pgtype.UUID) ([]UserEbookFolders, error) { - rows, err := q.db.Query(ctx, GetUserEbookFolders, userID) - if err != nil { - return nil, err - } - defer rows.Close() - items := []UserEbookFolders{} - for rows.Next() { - var i UserEbookFolders - if err := rows.Scan( - &i.ID, - &i.UserID, - &i.FolderPath, - &i.CreatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - const GetUserForLogin = `-- name: GetUserForLogin :one SELECT id, email, username, password_hash, theme, first_name, last_name, role, created_at, updated_at FROM users WHERE email = $1 OR username = $1 ` @@ -628,8 +1067,62 @@ func (q *Queries) GetUserPasswordHash(ctx context.Context, id pgtype.UUID) (stri return password_hash, err } +const GetUserVisibleLibraries = `-- name: GetUserVisibleLibraries :many +SELECT l.id, l.name, l.description, l.library_type_id, l.created_by_admin_id, l.created_at, l.updated_at, lt.name as type_name, lt.description as type_description, + COALESCE(lv.is_visible, true) as is_visible +FROM libraries l +JOIN library_types lt ON l.library_type_id = lt.id +LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1 +WHERE COALESCE(lv.is_visible, true) = true +ORDER BY l.created_at DESC +` + +type GetUserVisibleLibrariesRow struct { + ID pgtype.UUID `db:"id" json:"id"` + Name string `db:"name" json:"name"` + Description pgtype.Text `db:"description" json:"description"` + LibraryTypeID pgtype.UUID `db:"library_type_id" json:"library_type_id"` + CreatedByAdminID pgtype.UUID `db:"created_by_admin_id" json:"created_by_admin_id"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + TypeName string `db:"type_name" json:"type_name"` + TypeDescription pgtype.Text `db:"type_description" json:"type_description"` + IsVisible bool `db:"is_visible" json:"is_visible"` +} + +func (q *Queries) GetUserVisibleLibraries(ctx context.Context, userID pgtype.UUID) ([]GetUserVisibleLibrariesRow, error) { + rows, err := q.db.Query(ctx, GetUserVisibleLibraries, userID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []GetUserVisibleLibrariesRow{} + for rows.Next() { + var i GetUserVisibleLibrariesRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.LibraryTypeID, + &i.CreatedByAdminID, + &i.CreatedAt, + &i.UpdatedAt, + &i.TypeName, + &i.TypeDescription, + &i.IsVisible, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const ListEbooks = `-- name: ListEbooks :many -SELECT id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id, created_at, updated_at FROM ebooks ORDER BY created_at DESC LIMIT $1 OFFSET $2 +SELECT id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id, created_at, updated_at FROM ebooks ORDER BY created_at DESC LIMIT $1 OFFSET $2 ` type ListEbooksParams struct { @@ -648,6 +1141,7 @@ func (q *Queries) ListEbooks(ctx context.Context, arg ListEbooksParams) ([]Ebook var i Ebooks if err := rows.Scan( &i.ID, + &i.LibraryID, &i.Title, &i.Author, &i.Isbn, @@ -677,6 +1171,213 @@ func (q *Queries) ListEbooks(ctx context.Context, arg ListEbooksParams) ([]Ebook return items, nil } +const ListLibraries = `-- name: ListLibraries :many +SELECT l.id, l.name, l.description, l.library_type_id, l.created_by_admin_id, l.created_at, l.updated_at, lt.name as type_name, lt.description as type_description +FROM libraries l +JOIN library_types lt ON l.library_type_id = lt.id +ORDER BY l.created_at DESC +` + +type ListLibrariesRow struct { + ID pgtype.UUID `db:"id" json:"id"` + Name string `db:"name" json:"name"` + Description pgtype.Text `db:"description" json:"description"` + LibraryTypeID pgtype.UUID `db:"library_type_id" json:"library_type_id"` + CreatedByAdminID pgtype.UUID `db:"created_by_admin_id" json:"created_by_admin_id"` + CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"` + UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"` + TypeName string `db:"type_name" json:"type_name"` + TypeDescription pgtype.Text `db:"type_description" json:"type_description"` +} + +func (q *Queries) ListLibraries(ctx context.Context) ([]ListLibrariesRow, error) { + rows, err := q.db.Query(ctx, ListLibraries) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListLibrariesRow{} + for rows.Next() { + var i ListLibrariesRow + if err := rows.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.LibraryTypeID, + &i.CreatedByAdminID, + &i.CreatedAt, + &i.UpdatedAt, + &i.TypeName, + &i.TypeDescription, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const ListMediaItems = `-- name: ListMediaItems :many +SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.added_by_admin_id, mi.created_at, mi.updated_at, l.name as library_name, lt.name as library_type_name +FROM media_items mi +JOIN libraries l ON mi.library_id = l.id +JOIN library_types lt ON l.library_type_id = lt.id +ORDER BY mi.created_at DESC LIMIT $1 OFFSET $2 +` + +type ListMediaItemsParams struct { + Limit int32 `db:"limit" json:"limit"` + Offset int32 `db:"offset" json:"offset"` +} + +type ListMediaItemsRow 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 pgtype.Text `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 pgtype.Text `db:"contributors" json:"contributors"` + 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"` + LibraryName string `db:"library_name" json:"library_name"` + LibraryTypeName string `db:"library_type_name" json:"library_type_name"` +} + +func (q *Queries) ListMediaItems(ctx context.Context, arg ListMediaItemsParams) ([]ListMediaItemsRow, error) { + rows, err := q.db.Query(ctx, ListMediaItems, arg.Limit, arg.Offset) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListMediaItemsRow{} + for rows.Next() { + var i ListMediaItemsRow + 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.AddedByAdminID, + &i.CreatedAt, + &i.UpdatedAt, + &i.LibraryName, + &i.LibraryTypeName, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const ListMediaItemsByLibrary = `-- name: ListMediaItemsByLibrary :many +SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.added_by_admin_id, mi.created_at, mi.updated_at, l.name as library_name, lt.name as library_type_name +FROM media_items mi +JOIN libraries l ON mi.library_id = l.id +JOIN library_types lt ON l.library_type_id = lt.id +WHERE mi.library_id = $1 +ORDER BY mi.created_at DESC +` + +type ListMediaItemsByLibraryRow 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 pgtype.Text `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 pgtype.Text `db:"contributors" json:"contributors"` + 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"` + LibraryName string `db:"library_name" json:"library_name"` + LibraryTypeName string `db:"library_type_name" json:"library_type_name"` +} + +func (q *Queries) ListMediaItemsByLibrary(ctx context.Context, libraryID pgtype.UUID) ([]ListMediaItemsByLibraryRow, error) { + rows, err := q.db.Query(ctx, ListMediaItemsByLibrary, libraryID) + if err != nil { + return nil, err + } + defer rows.Close() + items := []ListMediaItemsByLibraryRow{} + for rows.Next() { + var i ListMediaItemsByLibraryRow + 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.AddedByAdminID, + &i.CreatedAt, + &i.UpdatedAt, + &i.LibraryName, + &i.LibraryTypeName, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const ListUsers = `-- name: ListUsers :many SELECT id, email, username, theme, first_name, last_name, role, created_at, updated_at FROM users ORDER BY created_at DESC ` @@ -723,8 +1424,39 @@ func (q *Queries) ListUsers(ctx context.Context) ([]ListUsersRow, error) { return items, nil } +const SetLibraryVisibility = `-- name: SetLibraryVisibility :one +INSERT INTO library_visibility (user_id, library_id, is_visible) +VALUES ($1, $2, $3) +ON CONFLICT (user_id, library_id) +DO UPDATE SET + is_visible = EXCLUDED.is_visible, + updated_at = NOW() +RETURNING id, user_id, library_id, is_visible, created_at, updated_at +` + +type SetLibraryVisibilityParams struct { + UserID pgtype.UUID `db:"user_id" json:"user_id"` + LibraryID pgtype.UUID `db:"library_id" json:"library_id"` + IsVisible bool `db:"is_visible" json:"is_visible"` +} + +// Library Visibility queries +func (q *Queries) SetLibraryVisibility(ctx context.Context, arg SetLibraryVisibilityParams) (LibraryVisibility, error) { + row := q.db.QueryRow(ctx, SetLibraryVisibility, arg.UserID, arg.LibraryID, arg.IsVisible) + var i LibraryVisibility + err := row.Scan( + &i.ID, + &i.UserID, + &i.LibraryID, + &i.IsVisible, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + const UpdateEbook = `-- name: UpdateEbook :one -UPDATE ebooks SET +UPDATE media_items SET title = $2, author = $3, isbn = $4, @@ -739,7 +1471,7 @@ UPDATE ebooks SET contributors = $13, updated_at = NOW() WHERE id = $1 -RETURNING id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id, created_at, updated_at +RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id, created_at, updated_at ` type UpdateEbookParams struct { @@ -758,7 +1490,7 @@ type UpdateEbookParams struct { Contributors pgtype.Text `db:"contributors" json:"contributors"` } -func (q *Queries) UpdateEbook(ctx context.Context, arg UpdateEbookParams) (Ebooks, error) { +func (q *Queries) UpdateEbook(ctx context.Context, arg UpdateEbookParams) (MediaItems, error) { row := q.db.QueryRow(ctx, UpdateEbook, arg.ID, arg.Title, @@ -774,9 +1506,10 @@ func (q *Queries) UpdateEbook(ctx context.Context, arg UpdateEbookParams) (Ebook arg.Publisher, arg.Contributors, ) - var i Ebooks + var i MediaItems err := row.Scan( &i.ID, + &i.LibraryID, &i.Title, &i.Author, &i.Isbn, @@ -800,25 +1533,25 @@ func (q *Queries) UpdateEbook(ctx context.Context, arg UpdateEbookParams) (Ebook } const UpdateEbookRating = `-- name: UpdateEbookRating :one -UPDATE ebook_ratings SET +UPDATE media_ratings SET rating = $3, updated_at = NOW() -WHERE ebook_id = $1 AND user_id = $2 -RETURNING id, ebook_id, user_id, rating, created_at, updated_at +WHERE media_item_id = $1 AND user_id = $2 +RETURNING id, media_item_id, user_id, rating, created_at, updated_at ` type UpdateEbookRatingParams struct { - EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"` - UserID pgtype.UUID `db:"user_id" json:"user_id"` - Rating int32 `db:"rating" json:"rating"` + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + Rating int32 `db:"rating" json:"rating"` } -func (q *Queries) UpdateEbookRating(ctx context.Context, arg UpdateEbookRatingParams) (EbookRatings, error) { - row := q.db.QueryRow(ctx, UpdateEbookRating, arg.EbookID, arg.UserID, arg.Rating) - var i EbookRatings +func (q *Queries) UpdateEbookRating(ctx context.Context, arg UpdateEbookRatingParams) (MediaRatings, error) { + row := q.db.QueryRow(ctx, UpdateEbookRating, arg.MediaItemID, arg.UserID, arg.Rating) + var i MediaRatings err := row.Scan( &i.ID, - &i.EbookID, + &i.MediaItemID, &i.UserID, &i.Rating, &i.CreatedAt, @@ -841,6 +1574,141 @@ func (q *Queries) UpdateEmail(ctx context.Context, arg UpdateEmailParams) error return err } +const UpdateLibrary = `-- name: UpdateLibrary :one +UPDATE libraries SET + name = $2, + description = $3, + updated_at = NOW() +WHERE id = $1 +RETURNING id, name, description, library_type_id, created_by_admin_id, created_at, updated_at +` + +type UpdateLibraryParams struct { + ID pgtype.UUID `db:"id" json:"id"` + Name string `db:"name" json:"name"` + Description pgtype.Text `db:"description" json:"description"` +} + +func (q *Queries) UpdateLibrary(ctx context.Context, arg UpdateLibraryParams) (Libraries, error) { + row := q.db.QueryRow(ctx, UpdateLibrary, arg.ID, arg.Name, arg.Description) + var i Libraries + err := row.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.LibraryTypeID, + &i.CreatedByAdminID, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const UpdateMediaItem = `-- name: UpdateMediaItem :one +UPDATE media_items SET + title = $2, + author = $3, + isbn = $4, + description = $5, + cover_image_path = $6, + series = $7, + series_number = $8, + tags = $9, + asin = $10, + date_published = $11, + publisher = $12, + contributors = $13, + updated_at = NOW() +WHERE id = $1 +RETURNING id, library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id, created_at, updated_at +` + +type UpdateMediaItemParams struct { + ID pgtype.UUID `db:"id" json:"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"` + 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 pgtype.Text `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 pgtype.Text `db:"contributors" json:"contributors"` +} + +func (q *Queries) UpdateMediaItem(ctx context.Context, arg UpdateMediaItemParams) (MediaItems, error) { + row := q.db.QueryRow(ctx, UpdateMediaItem, + arg.ID, + arg.Title, + arg.Author, + arg.Isbn, + arg.Description, + arg.CoverImagePath, + arg.Series, + arg.SeriesNumber, + arg.Tags, + arg.Asin, + arg.DatePublished, + arg.Publisher, + arg.Contributors, + ) + var i MediaItems + err := row.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.AddedByAdminID, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const UpdateMediaRating = `-- name: UpdateMediaRating :one +UPDATE media_ratings SET + rating = $3, + updated_at = NOW() +WHERE media_item_id = $1 AND user_id = $2 +RETURNING id, media_item_id, user_id, rating, created_at, updated_at +` + +type UpdateMediaRatingParams struct { + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` + UserID pgtype.UUID `db:"user_id" json:"user_id"` + Rating int32 `db:"rating" json:"rating"` +} + +func (q *Queries) UpdateMediaRating(ctx context.Context, arg UpdateMediaRatingParams) (MediaRatings, error) { + row := q.db.QueryRow(ctx, UpdateMediaRating, arg.MediaItemID, arg.UserID, arg.Rating) + var i MediaRatings + err := row.Scan( + &i.ID, + &i.MediaItemID, + &i.UserID, + &i.Rating, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + const UpdatePassword = `-- name: UpdatePassword :exec UPDATE users SET password_hash = $2, updated_at = NOW() WHERE id = $1 ` @@ -856,18 +1724,18 @@ func (q *Queries) UpdatePassword(ctx context.Context, arg UpdatePasswordParams) } const UpdateReadingProgress = `-- name: UpdateReadingProgress :one -INSERT INTO reading_progress (ebook_id, user_id, current_page, total_pages, last_read_at) +INSERT INTO reading_progress (media_item_id, user_id, current_page, total_pages, last_read_at) VALUES ($1, $2, $3, $4, NOW()) -ON CONFLICT (ebook_id, user_id) +ON CONFLICT (media_item_id, user_id) DO UPDATE SET current_page = EXCLUDED.current_page, total_pages = EXCLUDED.total_pages, last_read_at = NOW() -RETURNING id, ebook_id, user_id, current_page, total_pages, last_read_at +RETURNING id, media_item_id, user_id, current_page, total_pages, last_read_at ` type UpdateReadingProgressParams struct { - EbookID pgtype.UUID `db:"ebook_id" json:"ebook_id"` + MediaItemID pgtype.UUID `db:"media_item_id" json:"media_item_id"` UserID pgtype.UUID `db:"user_id" json:"user_id"` CurrentPage pgtype.Int4 `db:"current_page" json:"current_page"` TotalPages pgtype.Int4 `db:"total_pages" json:"total_pages"` @@ -875,7 +1743,7 @@ type UpdateReadingProgressParams struct { func (q *Queries) UpdateReadingProgress(ctx context.Context, arg UpdateReadingProgressParams) (ReadingProgress, error) { row := q.db.QueryRow(ctx, UpdateReadingProgress, - arg.EbookID, + arg.MediaItemID, arg.UserID, arg.CurrentPage, arg.TotalPages, @@ -883,7 +1751,7 @@ func (q *Queries) UpdateReadingProgress(ctx context.Context, arg UpdateReadingPr var i ReadingProgress err := row.Scan( &i.ID, - &i.EbookID, + &i.MediaItemID, &i.UserID, &i.CurrentPage, &i.TotalPages, diff --git a/internal/database/queries/queries.sql b/internal/database/queries/queries.sql index 5e90bf6..2519148 100644 --- a/internal/database/queries/queries.sql +++ b/internal/database/queries/queries.sql @@ -24,6 +24,126 @@ SELECT password_hash FROM users WHERE id = $1; -- name: ListUsers :many SELECT id, email, username, theme, first_name, last_name, role, created_at, updated_at FROM users ORDER BY created_at DESC; +-- Library Types queries +-- name: GetLibraryTypes :many +SELECT * FROM library_types ORDER BY name; + +-- name: GetLibraryType :one +SELECT * FROM library_types WHERE id = $1; + +-- name: GetLibraryTypeByName :one +SELECT * FROM library_types WHERE name = $1; + +-- Libraries queries +-- name: CreateLibrary :one +INSERT INTO libraries (name, description, library_type_id, created_by_admin_id) +VALUES ($1, $2, $3, $4) +RETURNING *; + +-- name: GetLibrary :one +SELECT l.*, lt.name as type_name, lt.description as type_description +FROM libraries l +JOIN library_types lt ON l.library_type_id = lt.id +WHERE l.id = $1; + +-- name: ListLibraries :many +SELECT l.*, lt.name as type_name, lt.description as type_description +FROM libraries l +JOIN library_types lt ON l.library_type_id = lt.id +ORDER BY l.created_at DESC; + +-- name: UpdateLibrary :one +UPDATE libraries SET + name = $2, + description = $3, + updated_at = NOW() +WHERE id = $1 +RETURNING *; + +-- name: DeleteLibrary :exec +DELETE FROM libraries WHERE id = $1; + +-- Library Folders queries +-- name: AddLibraryFolder :one +INSERT INTO library_folders (library_id, folder_path) VALUES ($1, $2) RETURNING *; + +-- name: GetLibraryFolders :many +SELECT * FROM library_folders WHERE library_id = $1 ORDER BY created_at; + +-- name: DeleteLibraryFolder :one +DELETE FROM library_folders WHERE library_id = $1 AND folder_path = $2 RETURNING *; + +-- Library Visibility queries +-- name: SetLibraryVisibility :one +INSERT INTO library_visibility (user_id, library_id, is_visible) +VALUES ($1, $2, $3) +ON CONFLICT (user_id, library_id) +DO UPDATE SET + is_visible = EXCLUDED.is_visible, + updated_at = NOW() +RETURNING *; + +-- name: GetLibraryVisibility :one +SELECT * FROM library_visibility WHERE user_id = $1 AND library_id = $2; + +-- name: GetUserVisibleLibraries :many +SELECT l.*, lt.name as type_name, lt.description as type_description, + COALESCE(lv.is_visible, true) as is_visible +FROM libraries l +JOIN library_types lt ON l.library_type_id = lt.id +LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1 +WHERE COALESCE(lv.is_visible, true) = true +ORDER BY l.created_at DESC; + +-- Media Items queries +-- name: CreateMediaItem :one +INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id) +VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) +RETURNING *; + +-- name: GetMediaItem :one +SELECT * FROM media_items WHERE id = $1; + +-- name: ListMediaItems :many +SELECT mi.*, l.name as library_name, lt.name as library_type_name +FROM media_items mi +JOIN libraries l ON mi.library_id = l.id +JOIN library_types lt ON l.library_type_id = lt.id +ORDER BY mi.created_at DESC LIMIT $1 OFFSET $2; + +-- name: ListMediaItemsByLibrary :many +SELECT mi.*, l.name as library_name, lt.name as library_type_name +FROM media_items mi +JOIN libraries l ON mi.library_id = l.id +JOIN library_types lt ON l.library_type_id = lt.id +WHERE mi.library_id = $1 +ORDER BY mi.created_at DESC; + +-- name: UpdateMediaItem :one +UPDATE media_items SET + title = $2, + author = $3, + isbn = $4, + description = $5, + cover_image_path = $6, + series = $7, + series_number = $8, + tags = $9, + asin = $10, + date_published = $11, + publisher = $12, + contributors = $13, + updated_at = NOW() +WHERE id = $1 +RETURNING *; + +-- name: DeleteMediaItem :exec +DELETE FROM media_items WHERE id = $1; + +-- name: GetMediaItemByFilePath :one +SELECT * FROM media_items WHERE file_path = $1; + +-- Backward compatibility - Ebooks queries (using views) -- name: GetEbook :one SELECT * FROM ebooks WHERE id = $1; @@ -31,12 +151,12 @@ SELECT * FROM ebooks WHERE id = $1; SELECT * FROM ebooks ORDER BY created_at DESC LIMIT $1 OFFSET $2; -- name: CreateEbook :one -INSERT INTO ebooks (title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id) -VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) +INSERT INTO media_items (library_id, title, author, isbn, description, file_path, file_size, mime_type, cover_image_path, series, series_number, tags, asin, date_published, publisher, contributors, added_by_admin_id) +VALUES ((SELECT id FROM libraries WHERE library_type_id = (SELECT id FROM library_types WHERE name = 'ebooks') LIMIT 1), $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING *; -- name: UpdateEbook :one -UPDATE ebooks SET +UPDATE media_items SET title = $2, author = $3, isbn = $4, @@ -54,15 +174,15 @@ WHERE id = $1 RETURNING *; -- name: DeleteEbook :exec -DELETE FROM ebooks WHERE id = $1; +DELETE FROM media_items WHERE id = $1; -- name: GetReadingProgress :one -SELECT * FROM reading_progress WHERE ebook_id = $1 AND user_id = $2; +SELECT * FROM reading_progress WHERE media_item_id = $1 AND user_id = $2; -- name: UpdateReadingProgress :one -INSERT INTO reading_progress (ebook_id, user_id, current_page, total_pages, last_read_at) +INSERT INTO reading_progress (media_item_id, user_id, current_page, total_pages, last_read_at) VALUES ($1, $2, $3, $4, NOW()) -ON CONFLICT (ebook_id, user_id) +ON CONFLICT (media_item_id, user_id) DO UPDATE SET current_page = EXCLUDED.current_page, total_pages = EXCLUDED.total_pages, @@ -70,7 +190,7 @@ DO UPDATE SET RETURNING *; -- name: DeleteReadingProgress :exec -DELETE FROM reading_progress WHERE ebook_id = $1 AND user_id = $2; +DELETE FROM reading_progress WHERE media_item_id = $1 AND user_id = $2; -- name: UpdateUserTheme :exec UPDATE users SET theme = $2, updated_at = NOW() WHERE id = $1; @@ -96,10 +216,41 @@ UPDATE users SET scan_frequency_minutes = $2, auto_scan_enabled = $3, updated_at -- name: GetScanSettings :one SELECT scan_frequency_minutes, auto_scan_enabled FROM users WHERE id = $1; --- name: CreateEbookRating :one -INSERT INTO ebook_ratings (ebook_id, user_id, rating) +-- name: CreateMediaRating :one +INSERT INTO media_ratings (media_item_id, user_id, rating) VALUES ($1, $2, $3) -ON CONFLICT (ebook_id, user_id) +ON CONFLICT (media_item_id, user_id) +DO UPDATE SET + rating = EXCLUDED.rating, + updated_at = NOW() +RETURNING *; + +-- name: GetMediaRating :one +SELECT * FROM media_ratings WHERE media_item_id = $1 AND user_id = $2; + +-- name: GetMediaRatings :many +SELECT mr.*, u.username +FROM media_ratings mr +JOIN users u ON mr.user_id = u.id +WHERE mr.media_item_id = $1 +ORDER BY mr.created_at DESC; + +-- name: UpdateMediaRating :one +UPDATE media_ratings SET + rating = $3, + updated_at = NOW() +WHERE media_item_id = $1 AND user_id = $2 +RETURNING *; + +-- name: DeleteMediaRating :exec +DELETE FROM media_ratings WHERE media_item_id = $1 AND user_id = $2; + +-- Backward compatibility - Ebooks ratings (using views) +-- name: CreateEbookRating :one +INSERT INTO media_ratings (media_item_id, user_id, rating) +SELECT $1, $2, $3 +WHERE EXISTS (SELECT 1 FROM media_items WHERE id = $1) +ON CONFLICT (media_item_id, user_id) DO UPDATE SET rating = EXCLUDED.rating, updated_at = NOW() @@ -116,23 +267,17 @@ WHERE er.ebook_id = $1 ORDER BY er.created_at DESC; -- name: UpdateEbookRating :one -UPDATE ebook_ratings SET +UPDATE media_ratings SET rating = $3, updated_at = NOW() -WHERE ebook_id = $1 AND user_id = $2 +WHERE media_item_id = $1 AND user_id = $2 RETURNING *; -- name: DeleteEbookRating :exec -DELETE FROM ebook_ratings WHERE ebook_id = $1 AND user_id = $2; +DELETE FROM media_ratings WHERE media_item_id = $1 AND user_id = $2; --- name: AddUserEbookFolder :one -INSERT INTO user_ebook_folders (user_id, folder_path) VALUES ($1, $2) RETURNING *; - --- name: GetUserEbookFolders :many -SELECT * FROM user_ebook_folders WHERE user_id = $1 ORDER BY created_at; - --- name: DeleteUserEbookFolder :one -DELETE FROM user_ebook_folders WHERE user_id = $1 AND folder_path = $2 RETURNING *; +-- Note: User ebook folders replaced by library folders system +-- Legacy folder management is now handled through libraries -- name: GetEbookByFilePath :one SELECT * FROM ebooks WHERE file_path = $1; \ No newline at end of file diff --git a/internal/services/library_service.go b/internal/services/library_service.go new file mode 100644 index 0000000..f84b0cc --- /dev/null +++ b/internal/services/library_service.go @@ -0,0 +1,200 @@ +package services + +import ( + "bookmann/internal/database" + "context" + "fmt" + "path/filepath" + "strings" + + "github.com/jackc/pgx/v5/pgtype" +) + +type LibraryService struct { + db *database.Queries +} + +func NewLibraryService(db *database.Queries) *LibraryService { + return &LibraryService{ + db: db, + } +} + +// Library type definitions and file extensions +const ( + LibraryTypeEbooks = "ebooks" + LibraryTypeComics = "comics" + LibraryTypeManga = "manga" +) + +var AllowedExtensions = map[string][]string{ + LibraryTypeEbooks: {".epub", ".pdf", ".mobi", ".azw", ".azw3", ".txt", ".rtf", ".doc", ".docx", ".lit", ".fb2", ".pdb"}, + LibraryTypeComics: {".cbz", ".cbr", ".cb7", ".cbt", ".pdf"}, + LibraryTypeManga: {".cbz", ".cbr", ".png", ".jpg", ".jpeg", ".gif", ".bmp", ".webp"}, +} + +// GetLibraryTypes retrieves all available library types +func (s *LibraryService) GetLibraryTypes(ctx context.Context) ([]database.LibraryTypes, error) { + return s.db.GetLibraryTypes(ctx) +} + +// CreateLibrary creates a new library with the given parameters +func (s *LibraryService) CreateLibrary(ctx context.Context, name, description, libraryType string, adminID pgtype.UUID) (*database.Libraries, error) { + // Get library type ID + libType, err := s.db.GetLibraryTypeByName(ctx, libraryType) + if err != nil { + return nil, fmt.Errorf("invalid library type: %w", err) + } + + // Create library + library, err := s.db.CreateLibrary(ctx, database.CreateLibraryParams{ + Name: name, + Description: pgtype.Text{String: description, Valid: true}, + LibraryTypeID: libType.ID, + CreatedByAdminID: adminID, + }) + if err != nil { + return nil, fmt.Errorf("failed to create library: %w", err) + } + + return &library, nil +} + +// GetLibrary retrieves a library by ID with type information +func (s *LibraryService) GetLibrary(ctx context.Context, libraryID pgtype.UUID) (*database.GetLibraryRow, error) { + library, err := s.db.GetLibrary(ctx, libraryID) + if err != nil { + return nil, err + } + return &library, nil +} + +// ListLibraries retrieves all libraries +func (s *LibraryService) ListLibraries(ctx context.Context) ([]database.ListLibrariesRow, error) { + return s.db.ListLibraries(ctx) +} + +// UpdateLibrary updates an existing library +func (s *LibraryService) UpdateLibrary(ctx context.Context, libraryID pgtype.UUID, name, description string) (*database.Libraries, error) { + library, err := s.db.UpdateLibrary(ctx, database.UpdateLibraryParams{ + ID: libraryID, + Name: name, + Description: pgtype.Text{String: description, Valid: true}, + }) + if err != nil { + return nil, err + } + return &library, nil +} + +// DeleteLibrary deletes a library and all its associated data +func (s *LibraryService) DeleteLibrary(ctx context.Context, libraryID pgtype.UUID) error { + return s.db.DeleteLibrary(ctx, libraryID) +} + +// AddLibraryFolder adds a folder to a library +func (s *LibraryService) AddLibraryFolder(ctx context.Context, libraryID pgtype.UUID, folderPath string) (*database.LibraryFolders, error) { + folder, err := s.db.AddLibraryFolder(ctx, database.AddLibraryFolderParams{ + LibraryID: libraryID, + FolderPath: folderPath, + }) + if err != nil { + return nil, err + } + return &folder, nil +} + +// GetLibraryFolders retrieves all folders for a library +func (s *LibraryService) GetLibraryFolders(ctx context.Context, libraryID pgtype.UUID) ([]database.LibraryFolders, error) { + return s.db.GetLibraryFolders(ctx, libraryID) +} + +// DeleteLibraryFolder removes a folder from a library +func (s *LibraryService) DeleteLibraryFolder(ctx context.Context, libraryID pgtype.UUID, folderPath string) error { + _, err := s.db.DeleteLibraryFolder(ctx, database.DeleteLibraryFolderParams{ + LibraryID: libraryID, + FolderPath: folderPath, + }) + return err +} + +// SetLibraryVisibility sets library visibility for a user +func (s *LibraryService) SetLibraryVisibility(ctx context.Context, userID, libraryID pgtype.UUID, isVisible bool) (*database.LibraryVisibility, error) { + visibility, err := s.db.SetLibraryVisibility(ctx, database.SetLibraryVisibilityParams{ + UserID: userID, + LibraryID: libraryID, + IsVisible: isVisible, + }) + if err != nil { + return nil, err + } + return &visibility, nil +} + +// GetUserVisibleLibraries retrieves all libraries visible to a user +func (s *LibraryService) GetUserVisibleLibraries(ctx context.Context, userID pgtype.UUID) ([]database.GetUserVisibleLibrariesRow, error) { + return s.db.GetUserVisibleLibraries(ctx, userID) +} + +// IsFileExtensionAllowed checks if a file extension is allowed for a library type +func (s *LibraryService) IsFileExtensionAllowed(libraryType, extension string) bool { + extensions, exists := AllowedExtensions[libraryType] + if !exists { + return false + } + + for _, ext := range extensions { + if strings.EqualFold(ext, extension) { + return true + } + } + return false +} + +// GetLibraryFileExtensions returns all allowed file extensions for a library type +func (s *LibraryService) GetLibraryFileExtensions(libraryType string) []string { + extensions, exists := AllowedExtensions[libraryType] + if !exists { + return []string{} + } + return extensions +} + +// GetLibraryTypeFromFileExtension determines the library type based on file extension +func (s *LibraryService) GetLibraryTypeFromFileExtension(filename string) string { + ext := strings.ToLower(filepath.Ext(filename)) + + for libType, extensions := range AllowedExtensions { + for _, allowedExt := range extensions { + if ext == allowedExt { + return libType + } + } + } + + return "" +} + +// ValidateLibraryPath checks if a path is valid for the given library type +func (s *LibraryService) ValidateLibraryPath(libraryType, folderPath string) error { + // You could add more validation here like: + // - Check if path exists + // - Check if path is readable + // - Validate path format for specific library types + // - Check for appropriate file structures (e.g., for manga with image folders) + + return nil +} + +// GetLibraryStats returns statistics for a library (media count, etc.) +func (s *LibraryService) GetLibraryStats(ctx context.Context, libraryID pgtype.UUID) (map[string]interface{}, error) { + // For now, return basic info. This can be expanded with more detailed stats + mediaItems, err := s.db.ListMediaItemsByLibrary(ctx, libraryID) + if err != nil { + return nil, err + } + + return map[string]interface{}{ + "media_count": len(mediaItems), + }, nil +}