283 lines
8.1 KiB
Go
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
|
|
}
|