Sync from /srv/compose/unified-media-manager

This commit is contained in:
Christopher Mayor
2026-04-24 10:45:19 -07:00
commit 7dbd00e537
132 changed files with 25394 additions and 0 deletions

118
internal/service/naming.go Normal file
View File

@@ -0,0 +1,118 @@
package service
import (
"bytes"
"context"
"fmt"
"log/slog"
"path/filepath"
"strings"
"text/template"
"time"
"github.com/TopherMayor/unified-media-manager/internal/db"
)
type NamingData struct {
Title string
SortTitle string
Year int
Season int
Episode int
Quality string
Ext string
ReleaseGroup string
Resolution string
Source string
Codec string
Artist string
Album string
Track int
Author string
Chapter int
Date string
OriginalName string
}
type NamingService struct {
db *db.DB
}
func NewNamingService(database *db.DB) *NamingService {
return &NamingService{db: database}
}
var DefaultTemplates = map[string]string{
"movie": "{{sanitize .Title}} ({{.Year}})/{{sanitize .Title}} ({{.Year}}) - {{.Quality}}.{{.Ext}}",
"series": "{{sanitize .Title}}/Season {{printf \"%02d\" .Season}}/{{sanitize .Title}} - S{{printf \"%02d\" .Season}}E{{printf \"%02d\" .Episode}} - {{.Quality}}.{{.Ext}}",
"music": "{{sanitize .Artist}}/{{sanitize .Album}}/{{printf \"%02d\" .Track}} - {{sanitize .Title}}.{{.Ext}}",
"audiobook": "{{sanitize .Author}}/{{sanitize .Title}}/{{sanitize .Title}} - Ch{{printf \"%02d\" .Chapter}}.{{.Ext}}",
"podcast": "{{sanitize .Title}}/{{sanitize .Title}} - {{.Date}}.{{.Ext}}",
"book": "{{sanitize .Author}}/{{sanitize .Title}} ({{.Year}})/{{sanitize .Title}} ({{.Year}}).{{.Ext}}",
}
func sanitize(s string) string {
s = strings.Map(func(r rune) rune {
switch r {
case '\\', '/', ':', '*', '?', '"', '<', '>', '|':
return -1
}
return r
}, s)
for strings.Contains(s, " ") {
s = strings.ReplaceAll(s, " ", " ")
}
return strings.TrimSpace(s)
}
func namingFuncMap() template.FuncMap {
return template.FuncMap{
"sanitize": sanitize,
"lower": strings.ToLower,
}
}
func (s *NamingService) GetTemplate(ctx context.Context, mediaType string) (string, error) {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
var tmpl string
err := s.db.Pool.QueryRow(ctx,
"SELECT template FROM naming_templates WHERE media_type = $1", mediaType).Scan(&tmpl)
if err != nil {
if fallback, ok := DefaultTemplates[mediaType]; ok {
return fallback, nil
}
return "", fmt.Errorf("get naming template: %w", err)
}
return tmpl, nil
}
func (s *NamingService) Render(ctx context.Context, mediaType string, data NamingData) (string, error) {
tmplStr, err := s.GetTemplate(ctx, mediaType)
if err != nil {
return "", fmt.Errorf("get template for render: %w", err)
}
tmpl, err := template.New("naming").Funcs(namingFuncMap()).Parse(tmplStr)
if err != nil {
slog.Error("failed to parse naming template", "error", err, "media_type", mediaType)
return "", fmt.Errorf("parse naming template: %w", err)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, data); err != nil {
slog.Error("failed to execute naming template", "error", err, "media_type", mediaType)
return "", fmt.Errorf("execute naming template: %w", err)
}
return buf.String(), nil
}
func ExtractExt(filename string) string {
ext := filepath.Ext(filename)
if ext == "" {
return ""
}
return strings.TrimPrefix(ext, ".")
}