subsyt/internal/organize/organize.go
Viktor Varland 09134c46c4
Some checks are pending
build / build (push) Waiting to run
refactor: split logic into distinct parts
2025-10-01 21:21:59 +02:00

167 lines
4.1 KiB
Go

package organize
import (
"fmt"
"log"
"os"
"path/filepath"
"strings"
"git.meatbag.se/varl/subsyt/internal/config"
"git.meatbag.se/varl/subsyt/internal/fsops"
"git.meatbag.se/varl/subsyt/internal/metadata"
"git.meatbag.se/varl/subsyt/internal/nfo"
)
type Organizer struct {
Paths config.Paths
FS fsops.Manager
}
func (o Organizer) Prepare() error {
if err := o.FS.EnsureDir(o.Paths.DownloadRoot); err != nil {
return err
}
if err := o.FS.EnsureDir(o.Paths.ChannelsDir); err != nil {
return err
}
if err := o.FS.EnsureDir(o.Paths.EpisodesDir); err != nil {
return err
}
if err := o.FS.EnsureDir(o.Paths.MediaDir); err != nil {
return err
}
return nil
}
func (o Organizer) ProcessShows(shows []metadata.ShowAsset) error {
for _, show := range shows {
showDir := filepath.Join(o.Paths.MediaDir, safeName(show.Show.Title))
if err := o.FS.EnsureDir(showDir); err != nil {
return err
}
if o.FS.DryRun {
log.Printf("[dry-run] write tvshow.nfo for %s\n", show.Show.Title)
} else {
nfo.WriteShowInfo(show.Show, filepath.Join(showDir, "tvshow.nfo"))
}
for _, img := range show.Images {
name := categorizeShowImage(img)
if name == "" {
continue
}
dest := filepath.Join(showDir, name)
if _, err := os.Stat(dest); err == nil {
log.Printf("skip image move, destination exists: %s\n", dest)
if err := o.FS.Remove(img); err != nil {
return err
}
continue
}
if err := o.FS.Move(img, dest); err != nil {
return err
}
}
metadata.EnsureBanner(show.Show, showDir, o.FS.DryRun)
metadata.EnsureFanart(show.Show, showDir, o.FS.DryRun)
if err := o.FS.Remove(show.InfoPath); err != nil {
return err
}
}
if err := o.FS.RemoveEmptyDirs(o.Paths.ChannelsDir); err != nil {
return err
}
return nil
}
func (o Organizer) ProcessEpisodes(episodes []metadata.EpisodeAsset) error {
for _, ep := range episodes {
showDir := filepath.Join(o.Paths.MediaDir, safeName(ep.Episode.ShowTitle))
seasonDir := filepath.Join(showDir, ep.Episode.Season)
if err := o.FS.EnsureDir(seasonDir); err != nil {
return err
}
var destMediaPath string
if ep.MediaPath != "" {
filename := filepath.Base(ep.MediaPath)
destMediaPath = filepath.Join(seasonDir, filename)
if err := o.FS.Move(ep.MediaPath, destMediaPath); err != nil {
return err
}
} else {
log.Printf("no media file for %s (%s)\n", ep.Episode.Title, ep.InfoPath)
}
for _, sidecar := range ep.Sidecars {
filename := filepath.Base(sidecar)
ext := strings.ToLower(filepath.Ext(filename))
if ext == ".jpg" || ext == ".jpeg" {
filename = metadata.NormalizeEpisodeThumbnail(filename)
}
dest := filepath.Join(seasonDir, filename)
if _, err := os.Stat(dest); err == nil {
log.Printf("skip sidecar move, destination exists: %s\n", dest)
if err := o.FS.Remove(sidecar); err != nil {
return err
}
continue
}
if err := o.FS.Move(sidecar, dest); err != nil {
return err
}
}
if destMediaPath != "" {
base := strings.TrimSuffix(filepath.Base(destMediaPath), filepath.Ext(destMediaPath))
nfoPath := filepath.Join(seasonDir, fmt.Sprintf("%s.nfo", base))
if o.FS.DryRun {
log.Printf("[dry-run] write episode nfo %s\n", nfoPath)
} else {
nfo.WriteEpisodeNFO(ep.Episode, nfoPath)
}
}
if err := o.FS.Remove(ep.InfoPath); err != nil {
return err
}
}
if err := o.FS.RemoveEmptyDirs(o.Paths.EpisodesDir); err != nil {
return err
}
return nil
}
func safeName(name string) string {
replacer := strings.NewReplacer("/", "-", "\\", "-", ":", "-", "?", "", "*", "", "\"", "", "<", "", ">", "", "|", "")
sanitized := replacer.Replace(strings.TrimSpace(name))
if sanitized == "" {
return "unknown"
}
return sanitized
}
func categorizeShowImage(path string) string {
lower := strings.ToLower(filepath.Base(path))
switch {
case strings.Contains(lower, "banner"):
return "banner.jpg"
case strings.Contains(lower, "fanart"):
return "fanart.jpg"
case strings.Contains(lower, "poster"):
return "poster.jpg"
default:
if strings.HasSuffix(lower, ".jpg") || strings.HasSuffix(lower, ".jpeg") {
return "poster.jpg"
}
}
return ""
}