fix(scanner): track podcast show audio paths during folder reconcile

Extract listPodcastShowAudioFiles and record each show's audio paths in
seenPaths during ScanPodcastFolder so reconciliation does not treat
still-present episodes as removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Quick
2026-06-26 08:34:18 -04:00
co-authored by Claude Opus 4.8
parent ed6c084c68
commit 6b45cfd855
3 changed files with 63 additions and 15 deletions
+23 -15
View File
@@ -32,23 +32,10 @@ type parsedPodcastEpisode struct {
// Returns an os.ErrNotExist-wrapped error if the folder contains zero
// audio files.
func parsePodcastShow(ctx context.Context, ffprobePath string, folderPath string) (*parsedPodcastShow, error) {
entries, err := os.ReadDir(folderPath)
audioFiles, err := listPodcastShowAudioFiles(folderPath)
if err != nil {
return nil, fmt.Errorf("read podcast folder %s: %w", folderPath, err)
return nil, err
}
var audioFiles []string
for _, entry := range entries {
if entry.IsDir() {
continue
}
if SupportsAudioFile(entry.Name()) {
audioFiles = append(audioFiles, filepath.Join(folderPath, entry.Name()))
}
}
if len(audioFiles) == 0 {
return nil, fmt.Errorf("podcast show %s: %w", folderPath, os.ErrNotExist)
}
sort.Strings(audioFiles)
show := &parsedPodcastShow{}
for idx, path := range audioFiles {
@@ -83,6 +70,27 @@ func parsePodcastShow(ctx context.Context, ffprobePath string, folderPath string
return show, nil
}
func listPodcastShowAudioFiles(folderPath string) ([]string, error) {
entries, err := os.ReadDir(folderPath)
if err != nil {
return nil, fmt.Errorf("read podcast folder %s: %w", folderPath, err)
}
var audioFiles []string
for _, entry := range entries {
if entry.IsDir() {
continue
}
if SupportsAudioFile(entry.Name()) {
audioFiles = append(audioFiles, filepath.Join(folderPath, entry.Name()))
}
}
if len(audioFiles) == 0 {
return nil, fmt.Errorf("podcast show %s: %w", folderPath, os.ErrNotExist)
}
sort.Strings(audioFiles)
return audioFiles, nil
}
// parseTrackNumber accepts ID3-style track values which may be "5" or
// "5/12". Returns 0 if no leading integer is present.
func parseTrackNumber(s string) int {
+5
View File
@@ -56,6 +56,11 @@ func (s *Scanner) ScanPodcastFolder(ctx context.Context, folder *models.MediaFol
return err
}
attempted++
if paths, err := listPodcastShowAudioFiles(subPath); err == nil {
for _, path := range paths {
seenPaths[path] = true
}
}
episodePaths, err := s.reconcilePodcastShow(ctx, folder, subPath)
if err != nil {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
+35
View File
@@ -98,6 +98,41 @@ func TestScanPodcastFolderReturnsCanceledContext(t *testing.T) {
}
}
func TestListPodcastShowAudioFilesReturnsSortedAudioPaths(t *testing.T) {
root := t.TempDir()
if err := os.WriteFile(filepath.Join(root, "02.mp3"), []byte("audio"), 0o644); err != nil {
t.Fatalf("write audio: %v", err)
}
if err := os.WriteFile(filepath.Join(root, "notes.txt"), []byte("notes"), 0o644); err != nil {
t.Fatalf("write notes: %v", err)
}
if err := os.WriteFile(filepath.Join(root, "01.flac"), []byte("audio"), 0o644); err != nil {
t.Fatalf("write audio: %v", err)
}
if err := os.Mkdir(filepath.Join(root, "nested"), 0o755); err != nil {
t.Fatalf("mkdir nested: %v", err)
}
got, err := listPodcastShowAudioFiles(root)
if err != nil {
t.Fatalf("listPodcastShowAudioFiles: %v", err)
}
want := []string{
filepath.Join(root, "01.flac"),
filepath.Join(root, "02.mp3"),
}
if strings.Join(got, "\n") != strings.Join(want, "\n") {
t.Fatalf("audio files = %#v, want %#v", got, want)
}
}
func TestListPodcastShowAudioFilesReturnsNotExistForEmptyShow(t *testing.T) {
_, err := listPodcastShowAudioFiles(t.TempDir())
if !errors.Is(err, os.ErrNotExist) {
t.Fatalf("empty podcast show error = %v, want os.ErrNotExist", err)
}
}
func TestResolvePodcastMediaItemReusesRootScopedContentID(t *testing.T) {
finder := &fakeRootContentFinder{contentID: "podcast-root-id"}
writer := &fakeFilesystemItemWriter{}