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
@@ -158,10 +158,39 @@ func (s *LibraryService) DeleteLibrary(ctx context.Context, libraryID pgtype.UUI
|
||||
}
|
||||
|
||||
// AddLibraryFolder adds a folder to a library
|
||||
// Folder purposes (library_folders.purpose): 'scan' roots feed the FS
|
||||
// walker; 'podcast' roots are download targets written only by the
|
||||
// podcast download worker.
|
||||
const (
|
||||
FolderPurposeScan = "scan"
|
||||
FolderPurposePodcast = "podcast"
|
||||
)
|
||||
|
||||
// AddLibraryFolder registers a folder against a library. The purpose is
|
||||
// derived from the library type: podcasts libraries register their
|
||||
// download root (purpose='podcast', exactly ONE folder — feeds lay out
|
||||
// <download-root>/<feed-slug>/...); every other type registers a
|
||||
// scanner root (purpose='scan').
|
||||
func (s *LibraryService) AddLibraryFolder(ctx context.Context, libraryID pgtype.UUID, folderPath string) (*database.LibraryFolders, error) {
|
||||
purpose := FolderPurposeScan
|
||||
|
||||
lib, err := s.db.GetLibrary(ctx, libraryID)
|
||||
if err == nil && lib.TypeName == LibraryTypePodcasts {
|
||||
// Podcasts libraries own exactly one folder: the download root.
|
||||
existing, err := s.db.GetLibraryFolders(ctx, libraryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(existing) > 0 {
|
||||
return nil, fmt.Errorf("podcasts libraries have a single download root; %s is already registered", existing[0].FolderPath)
|
||||
}
|
||||
purpose = FolderPurposePodcast
|
||||
}
|
||||
|
||||
folder, err := s.db.AddLibraryFolder(ctx, database.AddLibraryFolderParams{
|
||||
LibraryID: libraryID,
|
||||
FolderPath: folderPath,
|
||||
Purpose: purpose,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
Reference in new issue
Block a user