diff --git a/docs/admin/scanning.md b/docs/admin/scanning.md new file mode 100644 index 00000000..c4dd4c0f --- /dev/null +++ b/docs/admin/scanning.md @@ -0,0 +1,89 @@ + + +# Scanning + +Scanning is how Bookhoard's server reads your library folders and keeps the +catalog in step with the files on disk. This page covers every scan an +admin can trigger, what each one actually does, and what it costs — so you +can pick the right one before clicking. + +## The one distinction: shallow vs. force + +Every scan is one of two kinds: + +- **Shallow** (the default). The scanner walks the folder and skips any + file whose size and modification time are unchanged since it was last + seen. New and changed files get processed; untouched files cost nothing. + Fast, and the right choice for day-to-day keeping-up. +- **Force**. Nothing is skipped. Every file is re-read and re-extracted + with the scanner's current parsing, and each item's hash identifiers + (the SHA-256 fingerprints used for duplicate detection and device book + matching) are recomputed. This is the backfill tool: run it after + scanner improvements you want applied to already-scanned books, after + mass edits in Calibre or on disk, or when metadata looks stale anywhere + in a library. It is deliberately slow on big libraries. + +## Every scan control, and what it does + +| Control | Where | Kind | Scope | +| --- | --- | --- | --- | +| **Force Rescan All Libraries** | Admin dashboard → Quick Actions | Force | Every library | +| **Scan** (library row) | Admin → Libraries | Force | That library | +| **Scan this folder** (folder-row icon) | Admin → Libraries → a library's panel | Shallow | That folder's subtree | +| **Auto-Scan** + **Scan Interval (seconds)** | Admin → Settings → Scanning | Shallow | Every library | +| **Retry** / **Retry all** | Admin → Libraries → Processing Issues | Per-item | Failed items only | +| **Rescan** / **Reset to Scanned** | A book's detail page | Per-item | That one item | + +The details behind the table: + +- **Force Rescan All Libraries** loops over every library and starts a + force scan job for each. Libraries that already have a scan running are + left alone — the button reports which libraries started and which were + skipped because a scan was already in flight. +- **Scan** on a library row is the same force rescan, scoped to that one + library. It shares the dashboard button's semantics exactly. +- **Scan this folder** is the only folder-sized scan, and the only shallow + one an admin triggers by hand: unchanged files are skipped, and only + that folder's subtree is walked. Because a partial scan can't vouch for + what it didn't look at, it never marks items from the library's *other* + folders as missing. Podcast download folders refuse this button — they + are managed by the feed system, not the scanner. +- **Auto-Scan** arms the two automatic triggers, both shallow: a + filesystem watcher that fires as files change, and a periodic poll that + runs every **Scan Interval (seconds)** (default 60, allowed 1–3600). + The same shallow pass also runs once at startup when auto-scan is on. +- **Retry** and **Retry all** on a library's Processing Issues page send + failed items back through the scanner one at a time — deliberately not + a library-wide scan, so fixing a few bad files never re-reads the whole + library. +- **Rescan** on a book's page re-extracts that item from its file and + recomputes its hashes. **Reset to Scanned** clears your manual metadata + edits first, so what lands is purely what the scanner derives from the + file. + +## Rules worth knowing + +- **One scan per library at a time.** Starting a second scan for a library + that already has one pending or running is refused — the dashboard + tells you a scan is active rather than stacking a duplicate. The + automatic triggers skip silently instead. +- **Scans run in the background.** The dashboard's Scanning panel shows + per-library progress while jobs run; **Cancel all scans** stops the ones + still queued or running, and you can keep working meanwhile. +- **Missing files archive after two consecutive scans.** When a + library-wide scan (shallow or force) can't find a file twice in a row, + the item is archived — reading history is kept in case the file comes + back. The Archived Items page in this section manages the results. +- **Metadata comes from the files themselves.** The scanner reads sidecar + files (Calibre's `metadata.opf` in particular), embedded tags, and + filenames — [Calibre Integration](../user/calibre-integration.md) covers + the sidecar story. What a scan extracts is exactly what a force rescan + re-extracts. diff --git a/docs/developer/ui-controls.md b/docs/developer/ui-controls.md index ebe9c5e2..cc2ff846 100644 --- a/docs/developer/ui-controls.md +++ b/docs/developer/ui-controls.md @@ -218,6 +218,14 @@ does: Force-rescans every library — re-reads each file, re-extracts metadata w also: Scan All Libraries note: renamed 2026-11-02 — the old label under-promised a heavy operation +label: Scan +where: Admin → Libraries → a library's card row +does: Force-rescans that one library (same semantics as Force Rescan All Libraries, scoped to a single library). + +label: Scan this folder +where: Admin → Libraries → a library's panel → a folder row +does: Shallow-scans only that folder's subtree — unchanged files are skipped, and items in the library's other folders are never marked missing by a partial scan. + label: Resume rewind where: audio player → player settings; profile settings does: Rewinds playback a configured few seconds when you resume. diff --git a/internal/docs/navigation_test.go b/internal/docs/navigation_test.go index 5a582eac..69a781a9 100644 --- a/internal/docs/navigation_test.go +++ b/internal/docs/navigation_test.go @@ -242,11 +242,28 @@ func TestBuildNavigationUsabilityOrder(t *testing.T) { t.Errorf("Devices order = %v, want %v", got, wantDevices) } - // Administration grows as its pages are written; pin what exists today: - // the guide leads (pinned landing), Libraries follows. - wantAdmin := []string{"Admin Guide", "Libraries"} - if got := titles(find("Administration")); !reflect.DeepEqual(got, wantAdmin) { - t.Errorf("Administration order = %v, want %v", got, wantAdmin) + // Administration grows as its pages are written; pin its shape, not + // its size: the guide leads, Libraries follows, and every other item + // must appear in usabilityOrder's declared relative order. + admin := find("Administration") + if len(admin.Items) < 2 || admin.Items[0].Title != "Admin Guide" || admin.Items[1].Title != "Libraries" { + t.Fatalf("Administration must lead with Admin Guide then Libraries, got %+v", admin.Items) + } + pos := map[string]int{} + for i, p := range usabilityOrder["Administration"] { + pos[docURL(p)] = i + } + last := -1 + for _, item := range admin.Items[2:] { + r, ok := pos[item.URL] + if !ok { + t.Errorf("Administration item %q (%s) is not in usabilityOrder", item.Title, item.URL) + continue + } + if r <= last { + t.Errorf("Administration item %q breaks usabilityOrder sequence", item.Title) + } + last = r } }