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 }