Sync from /srv/compose/unified-media-manager
This commit is contained in:
621
internal/service/media.go
Normal file
621
internal/service/media.go
Normal file
@@ -0,0 +1,621 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/TopherMayor/unified-media-manager/internal/db"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type Media struct {
|
||||
ID int64 `json:"id"`
|
||||
MediaType string `json:"media_type"`
|
||||
Title string `json:"title"`
|
||||
SortTitle string `json:"sort_title"`
|
||||
OriginalTitle *string `json:"original_title,omitempty"`
|
||||
Overview *string `json:"overview,omitempty"`
|
||||
Year *int `json:"year,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Monitored bool `json:"monitored"`
|
||||
ExternalIDs json.RawMessage `json:"external_ids"`
|
||||
Metadata json.RawMessage `json:"metadata"`
|
||||
Images json.RawMessage `json:"images"`
|
||||
QualityProfileID *int64 `json:"quality_profile_id,omitempty"`
|
||||
RootFolderID *int64 `json:"root_folder_id,omitempty"`
|
||||
ReleaseDate *time.Time `json:"release_date,omitempty"`
|
||||
CurrentQuality json.RawMessage `json:"current_quality,omitempty"`
|
||||
DesiredQuality json.RawMessage `json:"desired_quality,omitempty"`
|
||||
QualityUpgradeNeeded bool `json:"quality_upgrade_needed"`
|
||||
AddedAt time.Time `json:"added_at"`
|
||||
LastSearchAt *time.Time `json:"last_search_at,omitempty"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type MediaFile struct {
|
||||
ID int64 `json:"id"`
|
||||
MediaID int64 `json:"media_id"`
|
||||
Path string `json:"path"`
|
||||
OriginalPath *string `json:"original_path,omitempty"`
|
||||
FileName string `json:"file_name"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
Quality json.RawMessage `json:"quality"`
|
||||
Codec *string `json:"codec,omitempty"`
|
||||
Resolution *string `json:"resolution,omitempty"`
|
||||
Source *string `json:"source,omitempty"`
|
||||
TranscodeStatus string `json:"transcode_status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type MediaRelation struct {
|
||||
ID int64 `json:"id"`
|
||||
ParentID int64 `json:"parent_id"`
|
||||
ChildID int64 `json:"child_id"`
|
||||
Relation string `json:"relation"`
|
||||
Position *int `json:"position,omitempty"`
|
||||
Season *int `json:"season,omitempty"`
|
||||
}
|
||||
|
||||
type MediaDetail struct {
|
||||
Media Media `json:"media"`
|
||||
Files []MediaFile `json:"files"`
|
||||
Relations []MediaRelation `json:"relations"`
|
||||
}
|
||||
|
||||
type MediaFilters struct {
|
||||
MediaType string
|
||||
Status string
|
||||
Monitored string
|
||||
Query string
|
||||
Tag string
|
||||
Page int
|
||||
PageSize int
|
||||
}
|
||||
|
||||
type CreateMediaRequest struct {
|
||||
MediaType string `json:"media_type"`
|
||||
Title string `json:"title"`
|
||||
SortTitle string `json:"sort_title,omitempty"`
|
||||
OriginalTitle *string `json:"original_title,omitempty"`
|
||||
Overview *string `json:"overview,omitempty"`
|
||||
Year *int `json:"year,omitempty"`
|
||||
ReleaseDate *time.Time `json:"release_date,omitempty"`
|
||||
Status string `json:"status"`
|
||||
Monitored bool `json:"monitored"`
|
||||
ExternalIDs json.RawMessage `json:"external_ids,omitempty"`
|
||||
Metadata json.RawMessage `json:"metadata,omitempty"`
|
||||
Images json.RawMessage `json:"images,omitempty"`
|
||||
QualityProfileID *int64 `json:"quality_profile_id,omitempty"`
|
||||
RootFolderID *int64 `json:"root_folder_id,omitempty"`
|
||||
CurrentQuality json.RawMessage `json:"current_quality,omitempty"`
|
||||
DesiredQuality json.RawMessage `json:"desired_quality,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateMediaRequest struct {
|
||||
Title *string `json:"title,omitempty"`
|
||||
SortTitle *string `json:"sort_title,omitempty"`
|
||||
OriginalTitle *string `json:"original_title,omitempty"`
|
||||
Overview *string `json:"overview,omitempty"`
|
||||
Year *int `json:"year,omitempty"`
|
||||
Status *string `json:"status,omitempty"`
|
||||
Monitored *bool `json:"monitored,omitempty"`
|
||||
ExternalIDs json.RawMessage `json:"external_ids,omitempty"`
|
||||
Metadata json.RawMessage `json:"metadata,omitempty"`
|
||||
Images json.RawMessage `json:"images,omitempty"`
|
||||
QualityProfileID *int64 `json:"quality_profile_id,omitempty"`
|
||||
RootFolderID *int64 `json:"root_folder_id,omitempty"`
|
||||
CurrentQuality json.RawMessage `json:"current_quality,omitempty"`
|
||||
DesiredQuality json.RawMessage `json:"desired_quality,omitempty"`
|
||||
}
|
||||
|
||||
const mediaColumns = `id, media_type, title, sort_title, original_title, overview, year,
|
||||
release_date,
|
||||
status, monitored, external_ids, metadata, images, quality_profile_id, root_folder_id,
|
||||
current_quality, desired_quality,
|
||||
has_files,
|
||||
CASE
|
||||
WHEN has_files AND desired_quality IS NOT NULL
|
||||
AND current_quality IS NOT NULL
|
||||
AND current_quality::text != desired_quality::text
|
||||
THEN true
|
||||
ELSE false
|
||||
END AS quality_upgrade_needed,
|
||||
added_at, last_search_at, updated_at`
|
||||
|
||||
type MediaService struct {
|
||||
db *db.DB
|
||||
}
|
||||
|
||||
func NewMediaService(database *db.DB) *MediaService {
|
||||
return &MediaService{db: database}
|
||||
}
|
||||
|
||||
func scanMedia(scanner interface{ Scan(...interface{}) error }) (*Media, error) {
|
||||
var m Media
|
||||
var origTitle, overview sql.NullString
|
||||
var year sql.NullInt64
|
||||
var releaseDate sql.NullTime
|
||||
var qpID, rfID sql.NullInt64
|
||||
var lastSearchAt sql.NullTime
|
||||
var hasFiles bool
|
||||
|
||||
err := scanner.Scan(&m.ID, &m.MediaType, &m.Title, &m.SortTitle, &origTitle, &overview, &year,
|
||||
&releaseDate,
|
||||
&m.Status, &m.Monitored, &m.ExternalIDs, &m.Metadata, &m.Images, &qpID, &rfID,
|
||||
&m.CurrentQuality, &m.DesiredQuality, &hasFiles, &m.QualityUpgradeNeeded, &m.AddedAt, &lastSearchAt, &m.UpdatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if origTitle.Valid {
|
||||
m.OriginalTitle = &origTitle.String
|
||||
}
|
||||
if overview.Valid {
|
||||
m.Overview = &overview.String
|
||||
}
|
||||
if year.Valid {
|
||||
y := int(year.Int64)
|
||||
m.Year = &y
|
||||
}
|
||||
if releaseDate.Valid {
|
||||
m.ReleaseDate = &releaseDate.Time
|
||||
}
|
||||
if qpID.Valid {
|
||||
m.QualityProfileID = &qpID.Int64
|
||||
}
|
||||
if rfID.Valid {
|
||||
m.RootFolderID = &rfID.Int64
|
||||
}
|
||||
if lastSearchAt.Valid {
|
||||
m.LastSearchAt = &lastSearchAt.Time
|
||||
}
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
type mediaWithTotal struct {
|
||||
Media
|
||||
total int
|
||||
}
|
||||
|
||||
func scanMediaRowWithTotal(scanner interface{ Scan(...interface{}) error }) (*mediaWithTotal, error) {
|
||||
var m Media
|
||||
var origTitle, overview sql.NullString
|
||||
var year sql.NullInt64
|
||||
var releaseDate sql.NullTime
|
||||
var qpID, rfID sql.NullInt64
|
||||
var lastSearchAt sql.NullTime
|
||||
var hasFiles bool
|
||||
var total int
|
||||
|
||||
err := scanner.Scan(&m.ID, &m.MediaType, &m.Title, &m.SortTitle, &origTitle, &overview, &year,
|
||||
&releaseDate,
|
||||
&m.Status, &m.Monitored, &m.ExternalIDs, &m.Metadata, &m.Images, &qpID, &rfID,
|
||||
&m.CurrentQuality, &m.DesiredQuality, &hasFiles, &m.QualityUpgradeNeeded, &m.AddedAt, &lastSearchAt, &m.UpdatedAt,
|
||||
&total)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if origTitle.Valid {
|
||||
m.OriginalTitle = &origTitle.String
|
||||
}
|
||||
if overview.Valid {
|
||||
m.Overview = &overview.String
|
||||
}
|
||||
if year.Valid {
|
||||
y := int(year.Int64)
|
||||
m.Year = &y
|
||||
}
|
||||
if releaseDate.Valid {
|
||||
m.ReleaseDate = &releaseDate.Time
|
||||
}
|
||||
if qpID.Valid {
|
||||
m.QualityProfileID = &qpID.Int64
|
||||
}
|
||||
if rfID.Valid {
|
||||
m.RootFolderID = &rfID.Int64
|
||||
}
|
||||
if lastSearchAt.Valid {
|
||||
m.LastSearchAt = &lastSearchAt.Time
|
||||
}
|
||||
return &mediaWithTotal{Media: m, total: total}, nil
|
||||
}
|
||||
|
||||
func scanMediaRows(rows pgx.Rows) ([]Media, error) {
|
||||
var results []Media
|
||||
for rows.Next() {
|
||||
m, err := scanMedia(rows)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scan media row: %w", err)
|
||||
}
|
||||
results = append(results, *m)
|
||||
}
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func buildMediaFilters(filters *MediaFilters) *QueryBuilder {
|
||||
qb := NewQueryBuilder(1)
|
||||
qb.AddLiteral("deleted_at IS NULL")
|
||||
|
||||
if filters.MediaType != "" {
|
||||
qb.Add("media_type = $%d", filters.MediaType)
|
||||
}
|
||||
if filters.Status != "" {
|
||||
qb.Add("status = $%d", filters.Status)
|
||||
}
|
||||
if filters.Monitored != "" {
|
||||
qb.Add("monitored = $%d", filters.Monitored == "true")
|
||||
}
|
||||
return qb
|
||||
}
|
||||
|
||||
func (s *MediaService) List(ctx context.Context, filters MediaFilters) ([]Media, int, error) {
|
||||
qb := buildMediaFilters(&filters)
|
||||
|
||||
query := fmt.Sprintf("SELECT %s, COUNT(*) OVER() AS total FROM media%s ORDER BY sort_title LIMIT $%d OFFSET $%d",
|
||||
mediaColumns, qb.Where(), qb.Idx(), qb.Idx()+1)
|
||||
args := append(qb.Args(), filters.PageSize, (filters.Page-1)*filters.PageSize)
|
||||
|
||||
rows, err := s.db.Pool.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("list media: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []Media
|
||||
var total int
|
||||
for rows.Next() {
|
||||
m, err := scanMediaRowWithTotal(rows)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("scan media row: %w", err)
|
||||
}
|
||||
if total == 0 {
|
||||
total = m.total
|
||||
}
|
||||
items = append(items, m.Media)
|
||||
}
|
||||
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (s *MediaService) GetByID(ctx context.Context, id int64, mediaType string) (*MediaDetail, error) {
|
||||
qb := NewQueryBuilder(1)
|
||||
qb.AddLiteral("deleted_at IS NULL")
|
||||
qb.Add("id = $%d", id)
|
||||
if mediaType != "" {
|
||||
qb.Add("media_type = $%d", mediaType)
|
||||
}
|
||||
|
||||
row := s.db.Pool.QueryRow(ctx,
|
||||
"SELECT "+mediaColumns+" FROM media"+qb.Where(), qb.Args()...)
|
||||
|
||||
m, err := scanMedia(row)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get media: %w", err)
|
||||
}
|
||||
|
||||
detail := &MediaDetail{Media: *m}
|
||||
|
||||
fileRows, err := s.db.Pool.Query(ctx,
|
||||
`SELECT id, media_id, path, original_path, file_name, file_size, quality, codec, resolution, source, transcode_status, created_at
|
||||
FROM media_files WHERE media_id = $1 AND deleted_at IS NULL ORDER BY created_at DESC`, id)
|
||||
if err == nil {
|
||||
defer fileRows.Close()
|
||||
for fileRows.Next() {
|
||||
var f MediaFile
|
||||
var origPath, codec, resolution, source sql.NullString
|
||||
if err := fileRows.Scan(&f.ID, &f.MediaID, &f.Path, &origPath, &f.FileName, &f.FileSize,
|
||||
&f.Quality, &codec, &resolution, &source, &f.TranscodeStatus, &f.CreatedAt); err != nil {
|
||||
slog.Error("failed to scan media file", "error", err)
|
||||
continue
|
||||
}
|
||||
if origPath.Valid {
|
||||
f.OriginalPath = &origPath.String
|
||||
}
|
||||
if codec.Valid {
|
||||
f.Codec = &codec.String
|
||||
}
|
||||
if resolution.Valid {
|
||||
f.Resolution = &resolution.String
|
||||
}
|
||||
if source.Valid {
|
||||
f.Source = &source.String
|
||||
}
|
||||
detail.Files = append(detail.Files, f)
|
||||
}
|
||||
}
|
||||
|
||||
relRows, err := s.db.Pool.Query(ctx,
|
||||
`SELECT id, parent_id, child_id, relation, position, season
|
||||
FROM media_relations WHERE parent_id = $1 OR child_id = $1 ORDER BY relation, position`, id)
|
||||
if err == nil {
|
||||
defer relRows.Close()
|
||||
for relRows.Next() {
|
||||
var r MediaRelation
|
||||
if err := relRows.Scan(&r.ID, &r.ParentID, &r.ChildID, &r.Relation, &r.Position, &r.Season); err != nil {
|
||||
slog.Error("failed to scan media relation", "error", err)
|
||||
continue
|
||||
}
|
||||
detail.Relations = append(detail.Relations, r)
|
||||
}
|
||||
}
|
||||
|
||||
return detail, nil
|
||||
}
|
||||
|
||||
func (s *MediaService) Create(ctx context.Context, req CreateMediaRequest) (int64, error) {
|
||||
if req.SortTitle == "" {
|
||||
req.SortTitle = req.Title
|
||||
}
|
||||
if req.Status == "" {
|
||||
req.Status = "unavailable"
|
||||
}
|
||||
if req.ExternalIDs == nil {
|
||||
req.ExternalIDs = json.RawMessage("{}")
|
||||
}
|
||||
if req.Metadata == nil {
|
||||
req.Metadata = json.RawMessage("{}")
|
||||
}
|
||||
if req.Images == nil {
|
||||
req.Images = json.RawMessage("[]")
|
||||
}
|
||||
|
||||
var id int64
|
||||
err := s.db.Pool.QueryRow(ctx,
|
||||
`INSERT INTO media (media_type, title, sort_title, original_title, overview, year,
|
||||
release_date,
|
||||
status, monitored, external_ids, metadata, images, quality_profile_id, root_folder_id,
|
||||
current_quality, desired_quality)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING id`,
|
||||
req.MediaType, req.Title, req.SortTitle, req.OriginalTitle, req.Overview, req.Year,
|
||||
req.ReleaseDate,
|
||||
req.Status, req.Monitored, req.ExternalIDs, req.Metadata, req.Images,
|
||||
req.QualityProfileID, req.RootFolderID, req.CurrentQuality, req.DesiredQuality).Scan(&id)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("create media: %w", err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (s *MediaService) Update(ctx context.Context, id int64, mediaType string, req UpdateMediaRequest) error {
|
||||
var setClauses []string
|
||||
var args []interface{}
|
||||
idx := 1
|
||||
|
||||
addCol := func(col string, val interface{}) {
|
||||
setClauses = append(setClauses, fmt.Sprintf("%s = $%d", col, idx))
|
||||
args = append(args, val)
|
||||
idx++
|
||||
}
|
||||
|
||||
if req.Title != nil {
|
||||
addCol("title", *req.Title)
|
||||
}
|
||||
if req.SortTitle != nil {
|
||||
addCol("sort_title", *req.SortTitle)
|
||||
}
|
||||
if req.OriginalTitle != nil {
|
||||
addCol("original_title", *req.OriginalTitle)
|
||||
}
|
||||
if req.Overview != nil {
|
||||
addCol("overview", *req.Overview)
|
||||
}
|
||||
if req.Year != nil {
|
||||
addCol("year", *req.Year)
|
||||
}
|
||||
if req.Status != nil {
|
||||
addCol("status", *req.Status)
|
||||
}
|
||||
if req.Monitored != nil {
|
||||
addCol("monitored", *req.Monitored)
|
||||
}
|
||||
if req.ExternalIDs != nil {
|
||||
addCol("external_ids", req.ExternalIDs)
|
||||
}
|
||||
if req.Metadata != nil {
|
||||
addCol("metadata", req.Metadata)
|
||||
}
|
||||
if req.Images != nil {
|
||||
addCol("images", req.Images)
|
||||
}
|
||||
if req.QualityProfileID != nil {
|
||||
addCol("quality_profile_id", *req.QualityProfileID)
|
||||
}
|
||||
if req.RootFolderID != nil {
|
||||
addCol("root_folder_id", *req.RootFolderID)
|
||||
}
|
||||
if req.CurrentQuality != nil {
|
||||
addCol("current_quality", req.CurrentQuality)
|
||||
}
|
||||
if req.DesiredQuality != nil {
|
||||
addCol("desired_quality", req.DesiredQuality)
|
||||
}
|
||||
|
||||
if len(setClauses) == 0 {
|
||||
return fmt.Errorf("no fields to update")
|
||||
}
|
||||
|
||||
addCol("updated_at", time.Now())
|
||||
|
||||
query := fmt.Sprintf("UPDATE media SET %s WHERE id = $%d AND deleted_at IS NULL",
|
||||
strings.Join(setClauses, ", "), idx)
|
||||
args = append(args, id)
|
||||
|
||||
if mediaType != "" {
|
||||
query += fmt.Sprintf(" AND media_type = $%d", idx+1)
|
||||
args = append(args, mediaType)
|
||||
}
|
||||
|
||||
tag, err := s.db.Pool.Exec(ctx, query, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update media: %w", err)
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return fmt.Errorf("media not found")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MediaService) Delete(ctx context.Context, id int64, mediaType string) error {
|
||||
query := "UPDATE media SET deleted_at = NOW(), updated_at = NOW() WHERE id = $1 AND deleted_at IS NULL"
|
||||
args := []interface{}{id}
|
||||
|
||||
if mediaType != "" {
|
||||
query += " AND media_type = $2"
|
||||
args = append(args, mediaType)
|
||||
}
|
||||
|
||||
tag, err := s.db.Pool.Exec(ctx, query, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("delete media: %w", err)
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return fmt.Errorf("media not found")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MediaService) Search(ctx context.Context, filters MediaFilters) ([]Media, int, error) {
|
||||
qb := NewQueryBuilder(1)
|
||||
qb.AddLiteral("deleted_at IS NULL")
|
||||
|
||||
if filters.Query != "" {
|
||||
qb.Add("to_tsvector('english', coalesce(title, '')) @@ plainto_tsquery('english', $%d)", filters.Query)
|
||||
}
|
||||
if filters.MediaType != "" {
|
||||
qb.Add("media_type = $%d", filters.MediaType)
|
||||
}
|
||||
if filters.Status != "" {
|
||||
qb.Add("status = $%d", filters.Status)
|
||||
}
|
||||
if filters.Tag != "" {
|
||||
qb.Add("id IN (SELECT mt.media_id FROM media_tags mt JOIN tags t ON mt.tag_id = t.id WHERE t.name = $%d)", filters.Tag)
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("SELECT %s, COUNT(*) OVER() AS total FROM media%s ORDER BY sort_title LIMIT $%d OFFSET $%d",
|
||||
mediaColumns, qb.Where(), qb.Idx(), qb.Idx()+1)
|
||||
args := append(qb.Args(), filters.PageSize, (filters.Page-1)*filters.PageSize)
|
||||
|
||||
rows, err := s.db.Pool.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("search media: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []Media
|
||||
var total int
|
||||
for rows.Next() {
|
||||
m, err := scanMediaRowWithTotal(rows)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("scan search results: %w", err)
|
||||
}
|
||||
if total == 0 {
|
||||
total = m.total
|
||||
}
|
||||
items = append(items, m.Media)
|
||||
}
|
||||
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (s *MediaService) SearchMissing(ctx context.Context, filters MediaFilters) ([]Media, int, error) {
|
||||
qb := NewQueryBuilder(1)
|
||||
qb.AddLiteral("monitored = true")
|
||||
qb.AddLiteral("status = 'unavailable'")
|
||||
qb.AddLiteral("deleted_at IS NULL")
|
||||
|
||||
if filters.MediaType != "" {
|
||||
qb.Add("media_type = $%d", filters.MediaType)
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("SELECT %s, COUNT(*) OVER() AS total FROM media%s ORDER BY sort_title LIMIT $%d OFFSET $%d",
|
||||
mediaColumns, qb.Where(), qb.Idx(), qb.Idx()+1)
|
||||
args := append(qb.Args(), filters.PageSize, (filters.Page-1)*filters.PageSize)
|
||||
|
||||
rows, err := s.db.Pool.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("query missing media: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []Media
|
||||
var total int
|
||||
for rows.Next() {
|
||||
m, err := scanMediaRowWithTotal(rows)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("scan missing media: %w", err)
|
||||
}
|
||||
if total == 0 {
|
||||
total = m.total
|
||||
}
|
||||
items = append(items, m.Media)
|
||||
}
|
||||
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (s *MediaService) SearchUpgrades(ctx context.Context, filters MediaFilters) ([]Media, int, error) {
|
||||
qb := NewQueryBuilder(1)
|
||||
qb.AddLiteral("deleted_at IS NULL")
|
||||
qb.AddLiteral("monitored = true")
|
||||
qb.AddLiteral("has_files = true")
|
||||
qb.AddLiteral("current_quality IS NOT NULL")
|
||||
qb.AddLiteral("desired_quality IS NOT NULL")
|
||||
qb.AddLiteral("current_quality::text != desired_quality::text")
|
||||
|
||||
if filters.MediaType != "" {
|
||||
qb.Add("media_type = $%d", filters.MediaType)
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("SELECT %s, COUNT(*) OVER() AS total FROM media%s ORDER BY sort_title LIMIT $%d OFFSET $%d",
|
||||
mediaColumns, qb.Where(), qb.Idx(), qb.Idx()+1)
|
||||
args := append(qb.Args(), filters.PageSize, (filters.Page-1)*filters.PageSize)
|
||||
|
||||
rows, err := s.db.Pool.Query(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("query upgrades: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var items []Media
|
||||
var total int
|
||||
for rows.Next() {
|
||||
m, err := scanMediaRowWithTotal(rows)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("scan upgrades: %w", err)
|
||||
}
|
||||
if total == 0 {
|
||||
total = m.total
|
||||
}
|
||||
items = append(items, m.Media)
|
||||
}
|
||||
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func CalcTotalPages(total, pageSize int) int {
|
||||
totalPages := total / pageSize
|
||||
if total%pageSize > 0 {
|
||||
totalPages++
|
||||
}
|
||||
return totalPages
|
||||
}
|
||||
|
||||
func ParsePagination(pageStr, pageSizeStr string) (page, pageSize int) {
|
||||
page, _ = strconv.Atoi(pageStr)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
pageSize, _ = strconv.Atoi(pageSizeStr)
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 50
|
||||
}
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user