Sync from /srv/compose/unified-media-manager
This commit is contained in:
282
internal/service/quality.go
Normal file
282
internal/service/quality.go
Normal file
@@ -0,0 +1,282 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user