diff --git a/internal/scanner/ebook_scan.go b/internal/scanner/ebook_scan.go index 16976034..5dc32773 100644 --- a/internal/scanner/ebook_scan.go +++ b/internal/scanner/ebook_scan.go @@ -53,6 +53,7 @@ func (s *Scanner) ScanEbookFolder(ctx context.Context, folder *models.MediaFolde type ebookRootScan struct { root string files []string + fileOnly bool // explicit single-file scan; index it but never reconcile a subtree rootErr error // root stat failed, or the root is not a directory walkFailures int // entries within the subtree the walk could not read or resolve } @@ -88,7 +89,11 @@ func collectEbookRootScans(ctx context.Context, folderID int, roots []string) ([ // Unmounted/missing/permission-broken root: failed, not empty. scan.rootErr = fmt.Errorf("stat root: %w", statErr) case !info.IsDir(): - scan.rootErr = fmt.Errorf("root is not a directory after symlink resolution") + if info.Mode().IsRegular() && SupportsEbookFile(cleanRoot) { + scan.fileOnly = true + } else { + scan.rootErr = fmt.Errorf("root is not a directory after symlink resolution") + } } if statErr == nil { if err := walkLogicalTree(ctx, cleanRoot, cleanRoot, walkModeEbook, visitedPhysicalDirs, &scan.files, &scan.walkFailures); err != nil { @@ -115,7 +120,7 @@ func splitEbookReconcileRoots(scans []ebookRootScan) (reconcileRoots []string, s reconcileRoots = make([]string, 0, len(scans)) for i := range scans { scan := &scans[i] - if scan.failed() { + if scan.failed() || scan.fileOnly { continue } if len(scan.files) > 0 { @@ -370,7 +375,14 @@ func (s *Scanner) reconcileEbookScan(ctx context.Context, folder *models.MediaFo } reconcileRoots, _ := splitEbookReconcileRoots(scans) if len(reconcileRoots) == 0 { - if len(scans) > 0 { + allFailed := len(scans) > 0 + for i := range scans { + if !scans[i].failed() { + allFailed = false + break + } + } + if allFailed { slog.WarnContext(ctx, "ebook scan: every root walk failed; skipping missing-file reconciliation", "component", "scanner", "folder_id", folder.ID, ) diff --git a/internal/scanner/ebook_test.go b/internal/scanner/ebook_test.go index f9e17c26..2bc3c845 100644 --- a/internal/scanner/ebook_test.go +++ b/internal/scanner/ebook_test.go @@ -1455,7 +1455,7 @@ func TestCollectEbookRootScansExcludesUnmountedRootFromReconciliation(t *testing } } -func TestCollectEbookRootScansTreatsNonDirectoryRootAsFailed(t *testing.T) { +func TestCollectEbookRootScansTreatsSingleFileRootAsNonReconciling(t *testing.T) { dir := t.TempDir() fileRoot := filepath.Join(dir, "book.epub") if err := os.WriteFile(fileRoot, []byte("x"), 0o644); err != nil { @@ -1466,8 +1466,8 @@ func TestCollectEbookRootScansTreatsNonDirectoryRootAsFailed(t *testing.T) { if err != nil { t.Fatalf("collectEbookRootScans: %v", err) } - if len(scans) != 1 || !scans[0].failed() { - t.Fatalf("scans = %+v, want one failed scan for a non-directory root", scans) + if len(scans) != 1 || scans[0].failed() || !scans[0].fileOnly { + t.Fatalf("scans = %+v, want one healthy file-only scan", scans) } // The file itself is still indexed; only reconciliation is withheld. if len(scans[0].files) != 1 {