feat(podcasts): scanner exclusion — podcasts libraries are feed-managed (task 1.2)
WHY: podcast download roots hold fetcher-written episode files; the FS
walker must never discover them (invariant 4) — a walk would double-
ingest episodes as scanner items and fight the fetcher's sole-writer
rule (D9). Every scan/watch path funnels through MediaScanner.SetFolders
(StartScanner, ScanLibrary jobs incl. scoped single-folder scans,
watch mode), so the filter lives there once: folders resolving to a
podcasts-type library are dropped before registration, with a log line.
Belt and braces: ScanLibrary's per-folder scope refuses purpose='podcast'
folders explicitly (400) — the panel-level path that bypasses type
resolution. AddLibraryFolder now derives purpose from the library type
('podcast' for podcasts libraries, enforced single download root;
'scan' default elsewhere), so folder purpose and library type cannot
diverge through the API.
This commit is contained in:
1 parent
0b7ad0f376
commit
c0e6993273
6 files changed
+77
-3
No files matched your search
@@ -512,6 +512,33 @@ func (s *MediaScanner) GetStats() (int, int, int) {
|
||||
// watcher (and thus no fd/inotify watches) is allocated — the watcher is never
|
||||
// read by scan jobs and previously leaked one watcher per job.
|
||||
func (s *MediaScanner) SetFolders(folders []string, watch bool) error {
|
||||
// Podcasts libraries are feed-managed (plan: podcast-integration.md
|
||||
// task 1.2, invariant 4): their download roots are written by the
|
||||
// download worker, never discovered by the walker — including
|
||||
// scoped single-folder scans (0f1d0abb), which all funnel through
|
||||
// here. Filter them out before anything else touches the list.
|
||||
scannable := folders[:0:0]
|
||||
for _, folder := range folders {
|
||||
lib, err := s.db.GetLibraryByFolder(context.Background(), folder)
|
||||
if err != nil {
|
||||
// Unresolvable folders keep the old behavior (scan walk
|
||||
// skips them per-file; watch adds them harmlessly).
|
||||
scannable = append(scannable, folder)
|
||||
continue
|
||||
}
|
||||
libType, err := s.db.GetLibraryType(context.Background(), lib.LibraryTypeID)
|
||||
if err != nil {
|
||||
scannable = append(scannable, folder)
|
||||
continue
|
||||
}
|
||||
if libType.Name == LibraryTypePodcasts {
|
||||
fmt.Printf("Scanner: skipping %s (podcasts library %q — feed-managed, not scanned)\n", folder, lib.Name)
|
||||
continue
|
||||
}
|
||||
scannable = append(scannable, folder)
|
||||
}
|
||||
folders = scannable
|
||||
|
||||
s.folders = folders
|
||||
|
||||
// Always close any previously-owned watcher so reconfiguration doesn't leak.
|
||||
@@ -524,6 +551,12 @@ func (s *MediaScanner) SetFolders(folders []string, watch bool) error {
|
||||
|
||||
// Create + populate a fresh watcher only when the caller intends to read events.
|
||||
if watch {
|
||||
if len(folders) == 0 {
|
||||
// All requested folders filtered (e.g. a podcasts library:
|
||||
// nothing scannable, nothing to watch). Fail loudly at the
|
||||
// API boundary instead of panicking on folders[0] below.
|
||||
return fmt.Errorf("no scannable folders (podcasts libraries are feed-managed and cannot be scanned)")
|
||||
}
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
// Return an error instead of panicking so a failed watcher can't
|
||||
|
||||
Reference in new issue
Block a user