From 7920f610e5ed283d47614c24001edc05851ea386 Mon Sep 17 00:00:00 2001 From: tsolo4ever Date: Fri, 6 Mar 2026 05:52:52 -0600 Subject: [PATCH 01/49] feat: add Library Import page (Sonarr-style) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- fe/src/App.vue | 12 +- .../domain/audiobook/LibraryImportFooter.vue | 210 +++++++++ .../domain/audiobook/LibraryImportRow.vue | 428 ++++++++++++++++++ fe/src/stores/libraryImport.ts | 356 +++++++++++++++ fe/src/views/library/LibraryImportView.vue | 326 +++++++++++-- 5 files changed, 1302 insertions(+), 30 deletions(-) create mode 100644 fe/src/components/domain/audiobook/LibraryImportFooter.vue create mode 100644 fe/src/components/domain/audiobook/LibraryImportRow.vue create mode 100644 fe/src/stores/libraryImport.ts diff --git a/fe/src/App.vue b/fe/src/App.vue index a49ef69..4bccff6 100644 --- a/fe/src/App.vue +++ b/fe/src/App.vue @@ -291,10 +291,17 @@ Calendar - + @@ -92,7 +92,7 @@
{{ result.title }} - {{ result.authors?.[0]?.name ?? result.author }} + {{ result.authors?.[0]?.name }} · {{ result.series }} · {{ result.asin }} diff --git a/fe/src/stores/libraryImport.ts b/fe/src/stores/libraryImport.ts index 8edf68f..baee3dd 100644 --- a/fe/src/stores/libraryImport.ts +++ b/fe/src/stores/libraryImport.ts @@ -56,9 +56,7 @@ function matchToMetadata(result: SearchResult): AudibleBookMetadata { const authors: string[] = result.authors && result.authors.length > 0 ? result.authors.map((a) => a.name ?? '').filter(Boolean) - : result.author - ? [result.author] - : [] + : [] return { title: result.title ?? '', @@ -197,8 +195,9 @@ export const useLibraryImportStore = defineStore('libraryImport', () => { isProcessing.value = false // Clear any in-flight isSearching flags for (const id of Object.keys(items.value)) { - if (items.value[id].isSearching) { - items.value[id] = { ...items.value[id], isSearching: false } + const entry = items.value[id] + if (entry?.isSearching) { + items.value = { ...items.value, [id]: { ...entry, isSearching: false } } } } } @@ -209,7 +208,7 @@ export const useLibraryImportStore = defineStore('libraryImport', () => { return } - const id = lookupQueue.value[0] + const id: string = lookupQueue.value[0]! const item = items.value[id] if (!item) { @@ -218,21 +217,20 @@ export const useLibraryImportStore = defineStore('libraryImport', () => { return } - items.value[id] = { ...item, isSearching: true } + items.value = { ...items.value, [id]: { ...item, isSearching: true } } try { const results = await apiService.advancedSearch({ title: item.folderName, cap: 5 }) metadataFetchCount.value++ const first = results[0] ?? null - items.value[id] = { - ...items.value[id], - isSearching: false, - hasSearched: true, - selectedMatch: first, - selected: first !== null, + const current = items.value[id]! + items.value = { + ...items.value, + [id]: { ...current, isSearching: false, hasSearched: true, selectedMatch: first, selected: first !== null }, } } catch { - items.value[id] = { ...items.value[id], isSearching: false, hasSearched: true } + const current = items.value[id] + if (current) items.value = { ...items.value, [id]: { ...current, isSearching: false, hasSearched: true } } } lookupQueue.value = lookupQueue.value.slice(1) From ab426710733ea05a97f68c87e72b6efc7b4a8681 Mon Sep 17 00:00:00 2001 From: tsolo4ever Date: Fri, 6 Mar 2026 06:15:40 -0600 Subject: [PATCH 03/49] fix: prefer detectedTitle over folderName for auto-search queries 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 --- fe/src/components/domain/audiobook/LibraryImportRow.vue | 2 +- fe/src/stores/libraryImport.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/fe/src/components/domain/audiobook/LibraryImportRow.vue b/fe/src/components/domain/audiobook/LibraryImportRow.vue index dd82a6b..5aaeb80 100644 --- a/fe/src/components/domain/audiobook/LibraryImportRow.vue +++ b/fe/src/components/domain/audiobook/LibraryImportRow.vue @@ -120,7 +120,7 @@ const props = defineProps<{ item: LibraryImportItem }>() const store = useLibraryImportStore() const showSearch = ref(false) -const searchQuery = ref(props.item.folderName) +const searchQuery = ref(props.item.detectedTitle ?? props.item.folderName) const searchResults = ref([]) const isLocalSearching = ref(false) const hasSearched = ref(false) diff --git a/fe/src/stores/libraryImport.ts b/fe/src/stores/libraryImport.ts index baee3dd..fccdccc 100644 --- a/fe/src/stores/libraryImport.ts +++ b/fe/src/stores/libraryImport.ts @@ -220,7 +220,7 @@ export const useLibraryImportStore = defineStore('libraryImport', () => { items.value = { ...items.value, [id]: { ...item, isSearching: true } } try { - const results = await apiService.advancedSearch({ title: item.folderName, cap: 5 }) + const results = await apiService.advancedSearch({ title: item.detectedTitle ?? item.folderName, cap: 5 }) metadataFetchCount.value++ const first = results[0] ?? null const current = items.value[id]! From ac836d69d1bd04a51e1863cc9b86558ec6f1f75b Mon Sep 17 00:00:00 2001 From: tsolo4ever Date: Fri, 6 Mar 2026 06:20:11 -0600 Subject: [PATCH 04/49] fix: use detectedAsin for auto-search when available 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 --- fe/src/components/domain/audiobook/LibraryImportRow.vue | 2 +- fe/src/stores/libraryImport.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/fe/src/components/domain/audiobook/LibraryImportRow.vue b/fe/src/components/domain/audiobook/LibraryImportRow.vue index 5aaeb80..ff9aaf6 100644 --- a/fe/src/components/domain/audiobook/LibraryImportRow.vue +++ b/fe/src/components/domain/audiobook/LibraryImportRow.vue @@ -120,7 +120,7 @@ const props = defineProps<{ item: LibraryImportItem }>() const store = useLibraryImportStore() const showSearch = ref(false) -const searchQuery = ref(props.item.detectedTitle ?? props.item.folderName) +const searchQuery = ref(props.item.detectedAsin ?? props.item.detectedTitle ?? props.item.folderName) const searchResults = ref([]) const isLocalSearching = ref(false) const hasSearched = ref(false) diff --git a/fe/src/stores/libraryImport.ts b/fe/src/stores/libraryImport.ts index fccdccc..f92dd4a 100644 --- a/fe/src/stores/libraryImport.ts +++ b/fe/src/stores/libraryImport.ts @@ -220,7 +220,10 @@ export const useLibraryImportStore = defineStore('libraryImport', () => { items.value = { ...items.value, [id]: { ...item, isSearching: true } } try { - const results = await apiService.advancedSearch({ title: item.detectedTitle ?? item.folderName, cap: 5 }) + const searchParams = item.detectedAsin + ? { asin: item.detectedAsin, cap: 5 } + : { title: item.detectedTitle ?? item.folderName, cap: 5 } + const results = await apiService.advancedSearch(searchParams) metadataFetchCount.value++ const first = results[0] ?? null const current = items.value[id]! From 6b87601a9d10eb57985d1602769b78765952bddb Mon Sep 17 00:00:00 2001 From: tsolo4ever Date: Thu, 5 Mar 2026 03:40:30 -0600 Subject: [PATCH 05/49] feat: add root folder unmatched file scanner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../feedback/UnmatchedFilesModal.vue | 380 ++++++++++++++++++ .../settings/RootFoldersSettings.vue | 32 +- fe/src/services/api.ts | 11 + fe/src/services/signalr.ts | 20 + fe/src/types/index.ts | 28 ++ .../Controllers/RootFoldersController.cs | 36 +- .../HostedServiceRegistrationExtensions.cs | 5 + listenarr.api/Services/PathMetadataParser.cs | 122 ++++++ .../UnmatchedScanBackgroundService.cs | 174 ++++++++ .../Services/UnmatchedScanQueueService.cs | 86 ++++ 10 files changed, 889 insertions(+), 5 deletions(-) create mode 100644 fe/src/components/feedback/UnmatchedFilesModal.vue create mode 100644 listenarr.api/Services/PathMetadataParser.cs create mode 100644 listenarr.api/Services/UnmatchedScanBackgroundService.cs create mode 100644 listenarr.api/Services/UnmatchedScanQueueService.cs diff --git a/fe/src/components/feedback/UnmatchedFilesModal.vue b/fe/src/components/feedback/UnmatchedFilesModal.vue new file mode 100644 index 0000000..4113d45 --- /dev/null +++ b/fe/src/components/feedback/UnmatchedFilesModal.vue @@ -0,0 +1,380 @@ + + + + + diff --git a/fe/src/components/settings/RootFoldersSettings.vue b/fe/src/components/settings/RootFoldersSettings.vue index ffb5cf3..a7470fc 100644 --- a/fe/src/components/settings/RootFoldersSettings.vue +++ b/fe/src/components/settings/RootFoldersSettings.vue @@ -39,6 +39,14 @@
+