diff --git a/internal/database/querier.go b/internal/database/querier.go index 2385cce..86faaed 100644 --- a/internal/database/querier.go +++ b/internal/database/querier.go @@ -89,6 +89,9 @@ type Querier interface { ListUsers(ctx context.Context) ([]ListUsersRow, error) RevokeAllUserRefreshTokens(ctx context.Context, userID pgtype.UUID) error RevokeRefreshToken(ctx context.Context, token string) error + // Search Media Items queries + SearchMediaItems(ctx context.Context, arg SearchMediaItemsParams) ([]SearchMediaItemsRow, error) + SearchMediaItemsFuzzy(ctx context.Context, arg SearchMediaItemsFuzzyParams) ([]SearchMediaItemsFuzzyRow, error) // Library Visibility queries SetLibraryVisibility(ctx context.Context, arg SetLibraryVisibilityParams) (LibraryVisibility, error) UpdateEbook(ctx context.Context, arg UpdateEbookParams) (MediaItems, error) diff --git a/internal/database/queries.sql.go b/internal/database/queries.sql.go index bdb701f..9879ab9 100644 --- a/internal/database/queries.sql.go +++ b/internal/database/queries.sql.go @@ -1954,6 +1954,219 @@ func (q *Queries) RevokeRefreshToken(ctx context.Context, token string) error { return err } +const SearchMediaItems = `-- name: SearchMediaItems :many +SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.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 +LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1 +WHERE COALESCE(lv.is_visible, true) = true + AND ( + mi.title ILIKE $2 OR + mi.author ILIKE $2 OR + mi.series ILIKE $2 OR + mi.tags ILIKE $2 OR + mi.contributors ILIKE $2 + ) +ORDER BY + CASE + WHEN mi.title ILIKE $2 THEN 1 + WHEN mi.author ILIKE $2 THEN 2 + WHEN mi.series ILIKE $2 THEN 3 + WHEN mi.tags ILIKE $2 THEN 4 + ELSE 5 + END, + mi.title ASC +LIMIT $4 OFFSET $3 +` + +type SearchMediaItemsParams struct { + UserID pgtype.UUID `db:"user_id" json:"user_id"` + SearchPattern pgtype.Text `db:"search_pattern" json:"search_pattern"` + Offset pgtype.Int4 `db:"offset" json:"offset"` + Limit pgtype.Int4 `db:"limit" json:"limit"` +} + +type SearchMediaItemsRow 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"` +} + +// Search Media Items queries +func (q *Queries) SearchMediaItems(ctx context.Context, arg SearchMediaItemsParams) ([]SearchMediaItemsRow, error) { + rows, err := q.db.Query(ctx, SearchMediaItems, + arg.UserID, + arg.SearchPattern, + arg.Offset, + arg.Limit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + items := []SearchMediaItemsRow{} + for rows.Next() { + var i SearchMediaItemsRow + 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 SearchMediaItemsFuzzy = `-- name: SearchMediaItemsFuzzy :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 +LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = $1 +WHERE COALESCE(lv.is_visible, true) = true + AND ( + word_similarity($2, mi.title) > 0.3 OR + word_similarity($2, COALESCE(mi.author, '')) > 0.3 OR + word_similarity($2, COALESCE(mi.series, '')) > 0.3 OR + word_similarity($2, COALESCE(mi.tags, '')) > 0.3 OR + word_similarity($2, COALESCE(mi.contributors, '')) > 0.3 + ) +ORDER BY + GREATEST( + word_similarity($2, mi.title), + word_similarity($2, COALESCE(mi.author, '')), + word_similarity($2, COALESCE(mi.series, '')), + word_similarity($2, COALESCE(mi.tags, '')), + word_similarity($2, COALESCE(mi.contributors, '')) + ) DESC, + mi.title ASC +LIMIT $4 OFFSET $3 +` + +type SearchMediaItemsFuzzyParams struct { + UserID pgtype.UUID `db:"user_id" json:"user_id"` + SearchQuery pgtype.Text `db:"search_query" json:"search_query"` + Offset pgtype.Int4 `db:"offset" json:"offset"` + Limit pgtype.Int4 `db:"limit" json:"limit"` +} + +type SearchMediaItemsFuzzyRow 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) SearchMediaItemsFuzzy(ctx context.Context, arg SearchMediaItemsFuzzyParams) ([]SearchMediaItemsFuzzyRow, error) { + rows, err := q.db.Query(ctx, SearchMediaItemsFuzzy, + arg.UserID, + arg.SearchQuery, + arg.Offset, + arg.Limit, + ) + if err != nil { + return nil, err + } + defer rows.Close() + items := []SearchMediaItemsFuzzyRow{} + for rows.Next() { + var i SearchMediaItemsFuzzyRow + 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 SetLibraryVisibility = `-- name: SetLibraryVisibility :one INSERT INTO library_visibility (user_id, library_id, is_visible) VALUES ($1, $2, $3) diff --git a/internal/database/queries/queries.sql b/internal/database/queries/queries.sql index 47592cd..4d281fa 100644 --- a/internal/database/queries/queries.sql +++ b/internal/database/queries/queries.sql @@ -284,6 +284,57 @@ DELETE FROM media_ratings WHERE media_item_id = $1 AND user_id = $2; -- name: GetEbookByFilePath :one SELECT * FROM ebooks WHERE file_path = $1; +-- Search Media Items queries +-- name: SearchMediaItems :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 +LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id') +WHERE COALESCE(lv.is_visible, true) = true + AND ( + mi.title ILIKE sqlc.narg('search_pattern') OR + mi.author ILIKE sqlc.narg('search_pattern') OR + mi.series ILIKE sqlc.narg('search_pattern') OR + mi.tags ILIKE sqlc.narg('search_pattern') OR + mi.contributors ILIKE sqlc.narg('search_pattern') + ) +ORDER BY + CASE + WHEN mi.title ILIKE sqlc.narg('search_pattern') THEN 1 + WHEN mi.author ILIKE sqlc.narg('search_pattern') THEN 2 + WHEN mi.series ILIKE sqlc.narg('search_pattern') THEN 3 + WHEN mi.tags ILIKE sqlc.narg('search_pattern') THEN 4 + ELSE 5 + END, + mi.title ASC +LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); + +-- name: SearchMediaItemsFuzzy :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 +LEFT JOIN library_visibility lv ON l.id = lv.library_id AND lv.user_id = sqlc.narg('user_id') +WHERE COALESCE(lv.is_visible, true) = true + AND ( + word_similarity(sqlc.narg('search_query'), mi.title) > 0.3 OR + word_similarity(sqlc.narg('search_query'), COALESCE(mi.author, '')) > 0.3 OR + word_similarity(sqlc.narg('search_query'), COALESCE(mi.series, '')) > 0.3 OR + word_similarity(sqlc.narg('search_query'), COALESCE(mi.tags, '')) > 0.3 OR + word_similarity(sqlc.narg('search_query'), COALESCE(mi.contributors, '')) > 0.3 + ) +ORDER BY + GREATEST( + word_similarity(sqlc.narg('search_query'), mi.title), + word_similarity(sqlc.narg('search_query'), COALESCE(mi.author, '')), + word_similarity(sqlc.narg('search_query'), COALESCE(mi.series, '')), + word_similarity(sqlc.narg('search_query'), COALESCE(mi.tags, '')), + word_similarity(sqlc.narg('search_query'), COALESCE(mi.contributors, '')) + ) DESC, + mi.title ASC +LIMIT sqlc.narg('limit') OFFSET sqlc.narg('offset'); + -- Media Notes queries -- name: CreateMediaNote :one INSERT INTO media_notes (media_item_id, user_id, content, position)