Files
2026-04-24 10:45:19 -07:00

283 lines
8.1 KiB
Go

package service
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/TopherMayor/unified-media-manager/internal/db"
)
type QualityTier struct {
Name string `json:"name"`
Rank int `json:"rank"`
Resolution string `json:"resolution"`
Source string `json:"source"`
Codec string `json:"codec"`
MinLinesize int `json:"min_linesize"`
}
var QualityTiers = []QualityTier{
{Name: "SDTV", Rank: 1, Resolution: "", Source: "television"},
{Name: "SDDVD", Rank: 2, Resolution: "480p", Source: "dvd"},
{Name: "WEBDL-480p", Rank: 3, Resolution: "480p", Source: "web"},
{Name: "HDTV-720p", Rank: 4, Resolution: "720p", Source: "television"},
{Name: "WEBDL-720p", Rank: 5, Resolution: "720p", Source: "web"},
{Name: "Bluray-720p", Rank: 6, Resolution: "720p", Source: "bluray"},
{Name: "HDTV-1080p", Rank: 7, Resolution: "1080p", Source: "television"},
{Name: "WEBDL-1080p", Rank: 8, Resolution: "1080p", Source: "web"},
{Name: "Bluray-1080p", Rank: 9, Resolution: "1080p", Source: "bluray"},
{Name: "Remux-1080p", Rank: 10, Resolution: "1080p", Source: "remux"},
{Name: "HDTV-2160p", Rank: 11, Resolution: "2160p", Source: "television"},
{Name: "WEBDL-2160p", Rank: 12, Resolution: "2160p", Source: "web"},
{Name: "Bluray-2160p", Rank: 13, Resolution: "2160p", Source: "bluray"},
{Name: "Remux-2160p", Rank: 14, Resolution: "2160p", Source: "remux"},
}
var sourceMatchMap = map[string][]string{
"television": {"HDTV", "PDTV", "SDTV"},
"web": {"WEB-DL", "WEBDL", "WEBRip", "WEB"},
"bluray": {"BluRay", "BDRip", "BRRip"},
"remux": {"REMUX", "Remux"},
"dvd": {"DVDRip", "DVD"},
}
func SourceMatch(tierSource, releaseSource string) bool {
matches, ok := sourceMatchMap[tierSource]
if !ok {
return strings.EqualFold(tierSource, releaseSource)
}
for _, m := range matches {
if strings.EqualFold(m, releaseSource) {
return true
}
}
return false
}
func GetTierByName(name string) *QualityTier {
for i := range QualityTiers {
if QualityTiers[i].Name == name {
return &QualityTiers[i]
}
}
return nil
}
func GetTiers() []QualityTier {
result := make([]QualityTier, len(QualityTiers))
copy(result, QualityTiers)
return result
}
func GetTiersByMediaType(mediaType string) []QualityTier {
return GetTiers()
}
type QualityProfile struct {
ID int64 `json:"id"`
Name string `json:"name"`
MediaTypes []string `json:"media_types"`
CutoffQuality string `json:"cutoff_quality"`
AllowedQualities []string `json:"allowed_qualities"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
type QualityService struct {
db *db.DB
}
func NewQualityService(database *db.DB) *QualityService {
return &QualityService{db: database}
}
const qualityProfileColumns = `id, name, media_types, cutoff_quality, allowed_qualities, created_at, updated_at`
func scanQualityProfile(scanner interface{ Scan(...interface{}) error }) (*QualityProfile, error) {
var p QualityProfile
var mediaTypes []string
var cutoffQuality []byte
var allowedQualities []byte
var createdAt, updatedAt time.Time
err := scanner.Scan(&p.ID, &p.Name, &mediaTypes, &cutoffQuality, &allowedQualities, &createdAt, &updatedAt)
if err != nil {
return nil, err
}
p.MediaTypes = mediaTypes
if err := json.Unmarshal(cutoffQuality, &p.CutoffQuality); err != nil {
p.CutoffQuality = ""
}
if err := json.Unmarshal(allowedQualities, &p.AllowedQualities); err != nil {
p.AllowedQualities = []string{}
}
p.CreatedAt = createdAt.Format(time.RFC3339)
p.UpdatedAt = updatedAt.Format(time.RFC3339)
return &p, nil
}
func (s *QualityService) List(ctx context.Context) ([]QualityProfile, error) {
rows, err := s.db.Pool.Query(ctx,
fmt.Sprintf("SELECT %s FROM quality_profiles ORDER BY name", qualityProfileColumns))
if err != nil {
return nil, fmt.Errorf("list quality profiles: %w", err)
}
defer rows.Close()
var items []QualityProfile
for rows.Next() {
p, err := scanQualityProfile(rows)
if err != nil {
continue
}
items = append(items, *p)
}
return items, nil
}
func (s *QualityService) Create(ctx context.Context, name string, mediaTypes []string, cutoffQuality string, allowedQualities []string) (int64, error) {
if GetTierByName(cutoffQuality) == nil {
return 0, fmt.Errorf("invalid cutoff quality tier: %s", cutoffQuality)
}
for _, q := range allowedQualities {
if GetTierByName(q) == nil {
return 0, fmt.Errorf("invalid allowed quality tier: %s", q)
}
}
cutoffJSON, _ := json.Marshal(cutoffQuality)
allowedJSON, _ := json.Marshal(allowedQualities)
var id int64
err := s.db.Pool.QueryRow(ctx,
`INSERT INTO quality_profiles (name, media_types, cutoff_quality, allowed_qualities)
VALUES ($1, $2::media_type[], $3, $4) RETURNING id`,
name, mediaTypes, cutoffJSON, allowedJSON).Scan(&id)
if err != nil {
return 0, fmt.Errorf("create quality profile: %w", err)
}
return id, nil
}
type UpdateQualityProfileRequest struct {
Name *string `json:"name,omitempty"`
MediaTypes []string `json:"media_types,omitempty"`
CutoffQuality *string `json:"cutoff_quality,omitempty"`
AllowedQualities []string `json:"allowed_qualities,omitempty"`
}
func (s *QualityService) Update(ctx context.Context, id int64, req UpdateQualityProfileRequest) 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.Name != nil {
addCol("name", *req.Name)
}
if req.MediaTypes != nil {
setClauses = append(setClauses, fmt.Sprintf("media_types = $%d::media_type[]", idx))
args = append(args, req.MediaTypes)
idx++
}
if req.CutoffQuality != nil {
if GetTierByName(*req.CutoffQuality) == nil {
return fmt.Errorf("invalid cutoff quality tier: %s", *req.CutoffQuality)
}
cutoffJSON, _ := json.Marshal(*req.CutoffQuality)
addCol("cutoff_quality", cutoffJSON)
}
if req.AllowedQualities != nil {
for _, q := range req.AllowedQualities {
if GetTierByName(q) == nil {
return fmt.Errorf("invalid allowed quality tier: %s", q)
}
}
allowedJSON, _ := json.Marshal(req.AllowedQualities)
addCol("allowed_qualities", allowedJSON)
}
if len(setClauses) == 0 {
return fmt.Errorf("no fields to update")
}
addCol("updated_at", time.Now())
query := fmt.Sprintf("UPDATE quality_profiles SET %s WHERE id = $%d",
strings.Join(setClauses, ", "), idx)
args = append(args, id)
tag, err := s.db.Pool.Exec(ctx, query, args...)
if err != nil {
return fmt.Errorf("update quality profile: %w", err)
}
if tag.RowsAffected() == 0 {
return fmt.Errorf("quality profile not found")
}
return nil
}
func (s *QualityService) Delete(ctx context.Context, id int64) error {
tag, err := s.db.Pool.Exec(ctx, "DELETE FROM quality_profiles WHERE id = $1", id)
if err != nil {
return fmt.Errorf("delete quality profile: %w", err)
}
if tag.RowsAffected() == 0 {
return fmt.Errorf("quality profile not found")
}
return nil
}
func (s *QualityService) GetByID(ctx context.Context, id int64) (*QualityProfile, error) {
row := s.db.Pool.QueryRow(ctx,
fmt.Sprintf("SELECT %s FROM quality_profiles WHERE id = $1", qualityProfileColumns), id)
p, err := scanQualityProfile(row)
if err != nil {
return nil, fmt.Errorf("quality profile not found")
}
return p, nil
}
func (s *QualityService) NeedsUpgrade(currentQuality string, cutoffQuality string) bool {
current := GetTierByName(currentQuality)
cutoff := GetTierByName(cutoffQuality)
if current == nil || cutoff == nil {
return false
}
return current.Rank < cutoff.Rank
}
func (s *QualityService) IsCutoffMet(currentQuality string, cutoffQuality string) bool {
current := GetTierByName(currentQuality)
cutoff := GetTierByName(cutoffQuality)
if current == nil || cutoff == nil {
return false
}
return current.Rank >= cutoff.Rank
}
func (s *QualityService) GetAllowedTierNames(allowedQualitiesJSON json.RawMessage) []string {
var names []string
if err := json.Unmarshal(allowedQualitiesJSON, &names); err != nil {
return []string{}
}
return names
}