subsyt/internal/metadata/metadata.go
Viktor Varland a639ac073e
Some checks are pending
build / build (push) Waiting to run
fix: attempt to fix cross device move
2025-10-02 18:03:23 +02:00

175 lines
3.5 KiB
Go

package metadata
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"git.meatbag.se/varl/subsyt/internal/model"
)
type ShowAsset struct {
Show model.Show
InfoPath string
Dir string
Images []string
}
type EpisodeAsset struct {
Episode model.Episode
InfoPath string
Dir string
MediaPath string
Sidecars []string
}
func findFiles(root, ext string) ([]string, error) {
if root == "" {
return nil, fmt.Errorf("find files: empty root")
}
if _, err := os.Stat(root); errors.Is(err, os.ErrNotExist) {
return nil, nil
} else if err != nil {
return nil, fmt.Errorf("find files: %w", err)
}
var result []string
walkErr := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
if strings.HasSuffix(path, ext) {
result = append(result, path)
}
return nil
})
if walkErr != nil {
return nil, fmt.Errorf("walk %s: %w", root, walkErr)
}
return result, nil
}
func trimInfoSuffix(path string) string {
return strings.TrimSuffix(path, ".info.json")
}
func globSidecars(base string) ([]string, error) {
matches, err := filepath.Glob(base + ".*")
if err != nil {
return nil, fmt.Errorf("glob %s.*: %w", base, err)
}
var sidecars []string
for _, match := range matches {
if strings.HasSuffix(match, ".info.json") {
continue
}
sidecars = append(sidecars, match)
}
return sidecars, nil
}
func resolveMediaPath(base string, episode model.Episode, sidecars []string) (string, []string) {
if episode.Ext != "" {
candidate := base + "." + episode.Ext
if _, err := os.Stat(candidate); err == nil {
var rest []string
for _, path := range sidecars {
if path != candidate {
rest = append(rest, path)
}
}
return candidate, rest
}
}
for _, path := range sidecars {
ext := strings.ToLower(filepath.Ext(path))
switch ext {
case ".mp4", ".mkv", ".webm", ".m4v", ".mov", ".avi":
var rest []string
for _, other := range sidecars {
if other != path {
rest = append(rest, other)
}
}
return path, rest
}
}
return "", sidecars
}
func ScanShows(root string) ([]ShowAsset, error) {
infoFiles, err := findFiles(root, ".info.json")
if err != nil {
return nil, err
}
var assets []ShowAsset
for _, infoPath := range infoFiles {
show := model.LoadShow(infoPath)
dir := filepath.Dir(infoPath)
base := trimInfoSuffix(infoPath)
sidecars, sidecarErr := globSidecars(base)
if sidecarErr != nil {
return nil, sidecarErr
}
var images []string
for _, path := range sidecars {
ext := strings.ToLower(filepath.Ext(path))
switch ext {
case ".jpg", ".jpeg", ".png", ".webp":
images = append(images, path)
}
}
assets = append(assets, ShowAsset{
Show: show,
InfoPath: infoPath,
Dir: dir,
Images: images,
})
}
return assets, nil
}
func ScanEpisodes(root string) ([]EpisodeAsset, error) {
infoFiles, err := findFiles(root, ".info.json")
if err != nil {
return nil, err
}
var assets []EpisodeAsset
for _, infoPath := range infoFiles {
episode := model.LoadEpisode(infoPath)
base := trimInfoSuffix(infoPath)
dir := filepath.Dir(infoPath)
sidecars, sidecarErr := globSidecars(base)
if sidecarErr != nil {
return nil, sidecarErr
}
mediaPath, remaining := resolveMediaPath(base, episode, sidecars)
assets = append(assets, EpisodeAsset{
Episode: episode,
InfoPath: infoPath,
Dir: dir,
MediaPath: mediaPath,
Sidecars: remaining,
})
}
return assets, nil
}