From a639ac073e5885e2df44dc41044e02198f33dd91 Mon Sep 17 00:00:00 2001 From: Viktor Varland Date: Thu, 2 Oct 2025 18:03:23 +0200 Subject: [PATCH] fix: attempt to fix cross device move --- internal/fsops/fsops.go | 45 +++++++++++++++++++++++- internal/metadata/metadata.go | 42 ++++++++--------------- internal/metadata/metadata_test.go | 55 ++++++++++++++++++++++++++++++ main.go | 1 + 4 files changed, 114 insertions(+), 29 deletions(-) diff --git a/internal/fsops/fsops.go b/internal/fsops/fsops.go index 02366d0..27fa0a5 100644 --- a/internal/fsops/fsops.go +++ b/internal/fsops/fsops.go @@ -3,15 +3,19 @@ package fsops import ( "errors" "fmt" + "io" "log" "os" "path/filepath" + "syscall" ) type Manager struct { DryRun bool } +var renameFn = os.Rename + func (m Manager) EnsureDir(path string) error { if path == "" { return fmt.Errorf("ensure dir: empty path") @@ -43,7 +47,16 @@ func (m Manager) Move(src, dst string) error { return nil } - if err := os.Rename(src, dst); err != nil { + if err := renameFn(src, dst); err != nil { + if errors.Is(err, syscall.EXDEV) { + if copyErr := copyFile(src, dst); copyErr != nil { + return fmt.Errorf("copy %s -> %s: %w", src, dst, copyErr) + } + if rmErr := os.Remove(src); rmErr != nil { + return fmt.Errorf("remove %s after copy: %w", src, rmErr) + } + return nil + } return fmt.Errorf("move %s -> %s: %w", src, dst, err) } return nil @@ -131,3 +144,33 @@ func (m Manager) RemoveEmptyDirs(root string) error { return nil }) } + +func copyFile(src, dst string) error { + info, err := os.Stat(src) + if err != nil { + return err + } + if info.IsDir() { + return fmt.Errorf("copy directory not supported: %s", src) + } + + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode()) + if err != nil { + return err + } + defer func() { + _ = out.Close() + }() + + if _, err := io.Copy(out, in); err != nil { + return err + } + + return out.Close() +} diff --git a/internal/metadata/metadata.go b/internal/metadata/metadata.go index 0e30c70..bcba239 100644 --- a/internal/metadata/metadata.go +++ b/internal/metadata/metadata.go @@ -3,7 +3,6 @@ package metadata import ( "errors" "fmt" - "log" "os" "path/filepath" "strings" @@ -76,29 +75,6 @@ func globSidecars(base string) ([]string, error) { return sidecars, nil } -func directoryImages(dir string) ([]string, error) { - entries, err := os.ReadDir(dir) - if err != nil { - if errors.Is(err, os.ErrNotExist) { - return nil, nil - } - return nil, fmt.Errorf("readdir %s: %w", dir, err) - } - - var images []string - for _, entry := range entries { - if entry.IsDir() { - continue - } - name := entry.Name() - switch strings.ToLower(filepath.Ext(name)) { - case ".jpg", ".jpeg", ".png", ".webp": - images = append(images, filepath.Join(dir, name)) - } - } - return images, nil -} - func resolveMediaPath(base string, episode model.Episode, sidecars []string) (string, []string) { if episode.Ext != "" { candidate := base + "." + episode.Ext @@ -140,17 +116,27 @@ func ScanShows(root string) ([]ShowAsset, error) { for _, infoPath := range infoFiles { show := model.LoadShow(infoPath) dir := filepath.Dir(infoPath) + base := trimInfoSuffix(infoPath) - imgs, imgErr := directoryImages(dir) - if imgErr != nil { - log.Printf("scan shows: %v\n", imgErr) + 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: imgs, + Images: images, }) } diff --git a/internal/metadata/metadata_test.go b/internal/metadata/metadata_test.go index aaae497..d682b8a 100644 --- a/internal/metadata/metadata_test.go +++ b/internal/metadata/metadata_test.go @@ -65,6 +65,10 @@ func TestScanShows(t *testing.T) { t.Fatalf("write poster: %v", err) } + if err := os.WriteFile(filepath.Join(showDir, "other-channel.jpg"), []byte("other"), 0o644); err != nil { + t.Fatalf("write extra image: %v", err) + } + assets, err := ScanShows(root) if err != nil { t.Fatalf("ScanShows error: %v", err) @@ -82,6 +86,57 @@ func TestScanShows(t *testing.T) { } } +func TestScanShowsFiltersImagesPerChannel(t *testing.T) { + root := t.TempDir() + showDir := filepath.Join(root, "sNA") + if err := os.MkdirAll(showDir, 0o755); err != nil { + t.Fatalf("mkdir: %v", err) + } + + primaryBase := filepath.Join(showDir, "channel-one") + secondaryBase := filepath.Join(showDir, "channel-two") + + primaryInfo := primaryBase + ".info.json" + secondaryInfo := secondaryBase + ".info.json" + + if err := os.WriteFile(primaryInfo, []byte(`{"channel":"One","channel_id":"one","description":"","thumbnails":[]}`), 0o644); err != nil { + t.Fatalf("write primary info: %v", err) + } + if err := os.WriteFile(secondaryInfo, []byte(`{"channel":"Two","channel_id":"two","description":"","thumbnails":[]}`), 0o644); err != nil { + t.Fatalf("write secondary info: %v", err) + } + + if err := os.WriteFile(primaryBase+".jpg", []byte("one"), 0o644); err != nil { + t.Fatalf("write primary image: %v", err) + } + if err := os.WriteFile(secondaryBase+".jpg", []byte("two"), 0o644); err != nil { + t.Fatalf("write secondary image: %v", err) + } + + assets, err := ScanShows(root) + if err != nil { + t.Fatalf("scan shows: %v", err) + } + if len(assets) != 2 { + t.Fatalf("expected 2 show assets, got %d", len(assets)) + } + + for _, asset := range assets { + switch asset.Show.Title { + case "One": + if len(asset.Images) != 1 || asset.Images[0] != primaryBase+".jpg" { + t.Fatalf("primary images mismatch: %#v", asset.Images) + } + case "Two": + if len(asset.Images) != 1 || asset.Images[0] != secondaryBase+".jpg" { + t.Fatalf("secondary images mismatch: %#v", asset.Images) + } + default: + t.Fatalf("unexpected show asset: %s", asset.Show.Title) + } + } +} + func TestNormalizeEpisodeThumbnail(t *testing.T) { got := NormalizeEpisodeThumbnail("file.jpg") expected := "file-thumb.jpg" diff --git a/main.go b/main.go index 6c017a3..d28002c 100644 --- a/main.go +++ b/main.go @@ -57,6 +57,7 @@ func run(cfg config.Config) { continue } + // download the channel dl.Youtube(dl.Download{ Url: feed.Author.Uri, OutDir: paths.ChannelsDir,