Skip to content

test#40

Closed
tsolo4ever wants to merge 27 commits intocanaryfrom
feature/library-import-page
Closed

test#40
tsolo4ever wants to merge 27 commits intocanaryfrom
feature/library-import-page

Conversation

@tsolo4ever
Copy link
Owner

test

tsolo4ever and others added 27 commits March 6, 2026 06:16
- New route /library-import with full-page import UX
- Pinia store (libraryImport.ts) with FIFO lookup queue, scan, per-row search, and batch import
- LibraryImportRow.vue: checkbox, folder path, detected metadata, inline match search with debounce
- LibraryImportFooter.vue: sticky toolbar with Start Processing / Cancel / Import buttons and rate-limit warning (>100 lookups)
- LibraryImportView.vue: root folder selector, scan button, last-scanned timestamp, results table
- Uncomment nav link in App.vue; add PhFolderOpen import
- Queue processes one item at a time (naturally rate-limit safe, matching Sonarr's approach)
- UnmatchedFilesModal.vue unchanged — additive change

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SearchResult has no 'author' string field — use authors[] only
- Narrow lookupQueue[0] with non-null assertion to satisfy strict index checks
- Use object spread assignment instead of direct index mutation for items record

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Folder names often contain year prefixes and series brackets (e.g.
"2021 - Cleave's Edge [Morcster Chef 1]") which produce poor search
results. When file tags provide a clean detectedTitle, use that instead
for both auto-processing queue searches and the inline search pre-fill.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ASIN from file tags gives an exact match via advancedSearch({ asin }).
Falls back to detectedTitle, then folderName for title search.
Inline search panel pre-fill follows the same priority.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Scans a root folder for audio files not already in the library,
parses metadata from the folder path structure and sidecar files
(desc.txt, reader.txt), and surfaces results for manual review.
Users can add each unmatched book to the library via the existing
AddLibraryModal and optionally link the file via manual-import.

- PathMetadataParser: parses {Author}/{Series}/{Year} - {Title} [{Series} #]/
  folder structure plus desc.txt and reader.txt sidecar files
- UnmatchedScanQueueService: channel-based background job queue
- UnmatchedScanBackgroundService: walks root folder, compares against
  tracked AudiobookFiles in DB, groups by book folder, pushes
  UnmatchedScanComplete via SettingsHub SignalR on completion
- RootFoldersController: POST /{id}/scan-unmatched and
  GET /unmatched-results/{jobId} endpoints
- UnmatchedFilesModal: two-phase modal (scan progress → results table
  with Add/Ignore per row) launched from root folder card
- RootFoldersSettings: magnifying glass button on each folder card

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds ffprobe-based tag extraction to the unmatched scan pipeline.
For each book group, reads album_artist (author), composer (narrator),
SERIES, PART, date, description, and ASIN from embedded iTunes/Nero
atoms. Embedded tags take priority over path-parsed values. ffprobe
calls run in parallel (max 4 concurrent) to avoid overwhelming the NAS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rvice

Add FakeUnmatchedQueue stub and update all controller instantiations to
include the new required constructor parameter.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds UnmatchedScanConcurrency setting (default 2, range 1-8) to
ApplicationSettings. The unmatched scan reads this at job start via
IConfigurationService and passes it to Parallel.ForEachAsync, so users
can tune NAS I/O pressure vs. scan speed from General Settings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Track last completed scan job per root folder path in UnmatchedScanQueueService
- Add CompletedAt timestamp to UnmatchedScanJob
- Add TryGetLastJobForPath() to IUnmatchedScanQueueService interface
- New GET /rootfolders/{id}/unmatched endpoint returns cached results for a folder
- Modal now loads saved results on open (no auto-scan)
- Added explicit Scan button — only rescans when clicked
- Shows 'Last scanned X ago' timestamp on cached results
- Shows empty state with instructions when no scan has run yet

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Controller constructor now requires ListenArrDbContext (added for
GetSavedUnmatched filtering). Tests use an in-memory DB via CreateDb().

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ManualImport_MultiFileCollisionTests: pass IRootFolderService mock to updated
  ManualImportController constructor
- SearchControllerTests + SearchControllerUnifiedTests: advanced search now returns
  { results: [...], totalResults: N } instead of a bare array; update assertions
  to unwrap the results property before checking array length and elements

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ok.FilePath

Books imported via manual import only have Audiobook.FilePath set — the
AudiobookFiles table is empty for them. The scanner was only checking
AudiobookFiles, causing already-imported books to appear as unmatched
and triggering a 409 on add. Now checks both sources.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
UnmatchedFilesModal addAllWithAsin now passes destinationPath so BasePath
is set correctly on the created audiobook (fixing silent import failure).
GetSavedUnmatched filters cached results against current library state so
re-opening the modal no longer shows already-added items.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… wording

- UnmatchedScanBackgroundService: files directly in the root folder (flat layout)
  now each produce their own result entry. Previously all flat files shared the
  same parent-folder group key → one entry with one representative file's metadata
  (and fileCount = N). Now each flat file is its own group; bookFolder falls back
  to the actual parent directory for the import path.
- UnmatchedFilesModal: eagerly load applicationSettings + rootFolders on open so
  the 'Move to:' / 'Copy to:' label and destination dropdown always reflect the
  user's configured file action and available root folders.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…youts

Replace flat-root special case with a general two-level grouping strategy:
1. Group files by parent directory (folder = one audiobook, as before)
2. Within each directory that has multiple files, sub-group by a normalized
   title stem derived from the filename (strip track numbers, Part/CD/Disc
   suffixes, year/series brackets, normalize whitespace)
   - Same stem or all-empty (numbered parts: 1.m4b, 2.m4b) → one audiobook entry
   - Different stems (author folder with multiple books, flat root with
     distinct titles) → separate audiobook entries per title

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- matchToMetadata: extract series name/position/asin from AudimetaSeries[]
  (advancedSearch returns series as array; AudibleBookMetadata expects string)
  This was causing import failures when a matched book had series data
- searchItem: detect 10-char ASIN pattern and pass as { asin } not { title }
  so manual search works when user enters or pre-fill contains an ASIN
- LibraryImportRow: display series name correctly in results dropdown
  (was showing raw JSON array object)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…oot folder

ManualImportController.GenerateManualImportPathAsync was treating any BasePath
that differed from settings.OutputPath as a 'custom' destination, applying only
the file naming pattern and placing files flat into that folder with no subfolder
hierarchy. This broke batch adds from UnmatchedFilesModal when the selected root
folder wasn't the same path as OutputPath.

Fix: inject IRootFolderService and check whether BasePath matches any configured
root folder. If it does, isCustomBasePath = false → full folder+file naming
pattern is applied → files are correctly organized into Author/Title/File.m4b.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…or UnmatchedScanConcurrency

Register IUnmatchedScanQueueService singleton outside the DisableHostedServices guard so
RootFoldersController can be resolved in tests. Add UnmatchedScanBackgroundService to
HostedServiceRegistrationExtensions. Add missing EF migration for the UnmatchedScanConcurrency
property on ApplicationSettings, which was causing PendingModelChangesWarning to abort migrations
at startup in the test host, leaving all tables uncreated and causing 10 test failures.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace static 'Move to:' / 'Copy to:' label with dropdown-first layout
showing the actual root folder path: '[Move ▼] to: /path/to/folder'.
Path truncates with ellipsis and full path on hover.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…hared folders

Files like "The Land (1).m4b", "The Land (2).m4b" in one folder were all
reducing to the same stem "the land" and being grouped as one multi-part
audiobook. They are actually separate books in a series.

Changes to ExtractTitleStem:
- Keep plain numeric parens (1), (2) etc. — these distinguish series books
- Only strip 4-digit year parens (2020), (2021)
- Only strip square bracket content [Series Name], [Chaos Seeds 1]
- Add 'pt' to the trailing part-number strip so "pt00" is removed
  (pt00 = whole book, never increments — safe to strip)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the static path label with a <select> showing all root folders,
letting users choose where to import files independently of the scan folder.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…earch

- importSelected: when addToLibrary returns 409 (book already in library),
  extract the existing audiobook from the conflict response body and continue
  with startManualImport — fixes import failure for Missing books
- buildSearchTitle: extract trailing number from filename (e.g. "The Land (3).m4b")
  and append to the search query ("The Land 3") so series book numbers resolve better

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When addToLibrary returns 409 (book already in library), the existing
audiobook may have BasePath=null or pointing to the wrong location.
Update BasePath to the selected destination folder before calling
startManualImport so the naming service generates the correct path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ismatches

- pickBestMatch: when detectedAuthor is available, prefer the search result
  whose author matches over just taking results[0]
- LibraryImportRow: show matched author in orange when it doesn't match
  detectedAuthor, with tooltip showing the detected author

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…thors/narrators

Search results return authors without names; fetch full Audimeta data at import
time (same pattern as UnmatchedFilesModal). Also fix v-if guards in detail view
to hide author/narrator rows when arrays are empty rather than showing blank labels.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@tsolo4ever
Copy link
Owner Author

test PR, closing

@tsolo4ever tsolo4ever closed this Mar 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant