Sync from /srv/compose/unified-media-manager
This commit is contained in:
118
internal/service/naming.go
Normal file
118
internal/service/naming.go
Normal 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, ".")
|
||||
}
|
||||
Reference in New Issue
Block a user