119 lines
3.1 KiB
Go
119 lines
3.1 KiB
Go
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, ".")
|
|
}
|