Skip to content

Commit b5cf323

Browse files
committed
Import library: Create root folders on the fly, import without impact on files and set monitoring
1 parent a98d337 commit b5cf323

45 files changed

Lines changed: 844 additions & 985 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

fe/src/__tests__/libraryImport.store.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ describe('library import store', () => {
8484
expect(startManualImport).toHaveBeenCalledWith({
8585
path: 'C:\\incoming',
8686
mode: 'interactive',
87-
inputMode: 'move',
87+
action: 'move',
8888
includeCompanionFiles: true,
8989
cleanupEmptySourceFolders: true,
9090
items: [

fe/src/components/domain/audiobook/LibraryImportFooter.vue

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,33 @@
22
<div class="import-footer">
33
<div class="footer-left">
44
<label class="footer-label">
5-
<select v-model="store.inputMode" class="mode-select" :disabled="isImporting">
5+
Monitor:
6+
<select v-model="store.monitor" class="mode-select" :disabled="isImporting">
7+
<option value="all">All</option>
8+
<option value="none">None</option>
9+
</select>
10+
</label>
11+
<label class="footer-label">
12+
On import:
13+
<select v-model="store.action" class="mode-select" :disabled="isImporting">
14+
<option value="none">Do nothing</option>
615
<option value="move">Move</option>
716
<option value="hardlink/copy">Hardlink / Copy</option>
817
</select>
9-
<span class="footer-to">to:</span>
10-
<select
11-
v-model="destinationFolderId"
12-
class="mode-select destination-select"
13-
:disabled="isImporting"
14-
>
15-
<option v-for="f in props.folders" :key="f.id" :value="f.id">
16-
{{ f.path }}
17-
</option>
18-
</select>
18+
<div v-if="store.action != 'none'">
19+
<label class="footer-label">to
20+
<select
21+
v-model="destinationFolderId"
22+
class="mode-select destination-select"
23+
:disabled="isImporting"
24+
>
25+
<option v-for="f in props.folders" :key="f.id" :value="f.id">
26+
{{ f.path }}
27+
</option>
28+
</select>
29+
</label>
30+
</div>
31+
<span v-else>Imported files will be left where they are</span>
1932
</label>
2033

2134
<div v-if="store.metadataFetchCount > 100" class="rate-limit-warning">
@@ -81,7 +94,10 @@ const isImporting = ref(false)
8194
const importingCount = ref(0)
8295
8396
const destinationPath = computed(
84-
() => props.folders.find((f) => f.id === destinationFolderId.value)?.path ?? '',
97+
() => {
98+
if (store.action == 'none') return '';
99+
return props.folders.find((f) => f.id === destinationFolderId.value)?.path ?? ''
100+
}
85101
)
86102
87103
const displayImportCount = computed(() =>

fe/src/stores/libraryImport.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ export const useLibraryImportStore = defineStore('libraryImport', () => {
119119
const scanStatus = ref<'idle' | 'scanning' | 'done' | 'error'>('idle')
120120
const scanError = ref<string | null>(null)
121121
const lastScannedAt = ref<string | null>(null)
122-
const inputMode = ref<'move' | 'hardlink/copy'>('move')
122+
const action = ref<'none' | 'move' | 'hardlink/copy'>('none')
123+
const monitor = ref<'none' | 'all'>('all')
123124
const metadataFetchCount = ref(0)
124125
const importErrors = ref<string[]>([])
125126

@@ -432,6 +433,7 @@ export const useLibraryImportStore = defineStore('libraryImport', () => {
432433
: match.series,
433434
}
434435
const { audiobook } = await apiService.addToLibrary(metadata, {
436+
monitored: monitor.value != 'none',
435437
destinationPath: rootFolderPath,
436438
searchResult: sanitizedMatch,
437439
})
@@ -461,9 +463,9 @@ export const useLibraryImportStore = defineStore('libraryImport', () => {
461463
await apiService.startManualImport({
462464
path: item.folderPath,
463465
mode: 'interactive',
464-
inputMode: inputMode.value,
466+
action: action.value,
465467
includeCompanionFiles: true,
466-
cleanupEmptySourceFolders: inputMode.value === 'move',
468+
cleanupEmptySourceFolders: action.value === 'move',
467469
items: item.sourceFiles.map((fullPath) => ({
468470
fullPath,
469471
matchedAudiobookId: audiobookId,
@@ -494,7 +496,8 @@ export const useLibraryImportStore = defineStore('libraryImport', () => {
494496
scanStatus,
495497
scanError,
496498
lastScannedAt,
497-
inputMode,
499+
action,
500+
monitor,
498501
metadataFetchCount,
499502
importErrors,
500503
// Computed

fe/src/stores/rootFolders.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,9 @@ export const useRootFoldersStore = defineStore('rootFolders', () => {
4949
return r
5050
}
5151

52-
return { folders, loading, defaultFolder, load, create, update, remove }
52+
function getLast() {
53+
return folders.value.at(-1);
54+
}
55+
56+
return { folders, loading, defaultFolder, load, create, update, remove, getLast }
5357
})

fe/src/types/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ export interface ApplicationSettings {
306306
allowedFileExtensions: string[]
307307
importBlacklistExtensions?: string[]
308308
// Action to perform for completed downloads.
309-
completedFileAction?: 'Move' | 'Copy' | 'Hardlink/Copy'
309+
completedFileAction?: 'None' | 'Move' | 'Copy' | 'Hardlink/Copy'
310310
// Show completed external downloads (torrents/NZBs) in the Activity view
311311
showCompletedExternalDownloads?: boolean
312312
// Failed download handling
@@ -893,7 +893,7 @@ export interface ManualImportRequestItem {
893893
export interface ManualImportRequest {
894894
path: string
895895
mode?: 'automatic' | 'interactive'
896-
inputMode?: 'move' | 'copy' | 'hardlink/copy'
896+
action?: 'none' | 'move' | 'copy' | 'hardlink/copy'
897897
includeCompanionFiles?: boolean
898898
cleanupEmptySourceFolders?: boolean
899899
items?: ManualImportRequestItem[]

fe/src/views/library/LibraryImportView.vue

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
<option v-for="f in rootFoldersStore.folders" :key="f.id" :value="f.id">
1515
{{ f.name || f.path }}
1616
</option>
17+
<option :value="null">
18+
Choose another folder...
19+
</option>
1720
</select>
1821
</div>
1922

@@ -175,6 +178,12 @@
175178
:folders="rootFoldersStore.folders"
176179
/>
177180
</div>
181+
182+
<RootFolderFormModal
183+
v-if="addRootFolder"
184+
@saved="refreshRootFolders"
185+
@close="addRootFolder = false"
186+
/>
178187
</template>
179188

180189
<script setup lang="ts">
@@ -202,6 +211,7 @@ import {
202211
type LibraryImportSortDirection,
203212
type LibraryImportSortKey,
204213
} from '@/utils/libraryImportTable'
214+
import RootFolderFormModal from '@/components/settings/RootFolderFormModal.vue'
205215
206216
const COLUMN_WIDTH_STORAGE_KEY = 'listenarr.libraryImport.columnWidths.v1'
207217
const MAX_COLUMN_WIDTH = 960
@@ -216,6 +226,7 @@ const sortKey = ref<LibraryImportSortKey>('folder')
216226
const sortDirection = ref<LibraryImportSortDirection>('asc')
217227
const columnWidths = ref<LibraryImportColumnWidths>({ ...DEFAULT_LIBRARY_IMPORT_COLUMN_WIDTHS })
218228
const resizingColumn = ref<LibraryImportResizableColumnKey | null>(null)
229+
const addRootFolder = ref<boolean>(false)
219230
220231
const sortOptions: Array<{ value: LibraryImportSortKey; label: string }> = [
221232
{ value: 'folder', label: 'Book' },
@@ -271,18 +282,23 @@ onMounted(async () => {
271282
await store.initFromRootFolder(defaultFolder.id)
272283
}
273284
274-
const action = configStore.applicationSettings?.completedFileAction
275-
store.inputMode = action === 'Move' || !action ? 'move' : 'hardlink/copy'
285+
store.action = 'none'
276286
})
277287
278288
onBeforeUnmount(() => {
279289
stopResize()
280290
})
281291
282292
async function onFolderChange() {
283-
if (!selectedFolderId.value) return
284-
store.stopProcessing()
285-
await store.initFromRootFolder(selectedFolderId.value)
293+
// Selected a valid root folder
294+
if (selectedFolderId.value) {
295+
store.stopProcessing()
296+
await store.initFromRootFolder(selectedFolderId.value)
297+
}
298+
// Tries to create a new root folder
299+
else {
300+
addRootFolder.value = true;
301+
}
286302
}
287303
288304
async function startScan() {
@@ -404,6 +420,18 @@ function persistColumnWidths() {
404420
// Non-fatal: resizing still works for the current session.
405421
}
406422
}
423+
424+
async function refreshRootFolders() {
425+
addRootFolder.value = false
426+
427+
await rootFoldersStore.load()
428+
429+
const newFolder = rootFoldersStore.getLast();
430+
if (newFolder) {
431+
selectedFolderId.value = newFolder.id
432+
await store.initFromRootFolder(newFolder.id)
433+
}
434+
}
407435
</script>
408436

409437
<style scoped>

listenarr.api/Controllers/LibraryController.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
using System.Text.RegularExpressions;
3737
using System.Security.Cryptography;
3838
using System.Text;
39+
using Listenarr.Domain.Utils;
3940

4041
namespace Listenarr.Api.Controllers
4142
{
@@ -2327,7 +2328,7 @@ private static bool PathsEqual(string? left, string? right)
23272328

23282329
private static bool IsSamePathOrWithin(string path, string rootPath)
23292330
{
2330-
return PathsEqual(path, rootPath) || FileUtils.IsPathWithinRoot(path, rootPath);
2331+
return PathsEqual(path, rootPath) || FileUtils.IsPathInsideOf(path, rootPath);
23312332
}
23322333

23332334
private static bool IsFilesystemRoot(string? path)
@@ -2776,7 +2777,7 @@ public async Task<IActionResult> ScanAudiobookFiles(int id, [FromBody] ScanReque
27762777
{
27772778
try
27782779
{
2779-
var jobId = await _scanQueueService.EnqueueScanAsync(id, request?.Path);
2780+
var jobId = await _scanQueueService.EnqueueScanAsync(audiobook, request?.Path);
27802781
_logger.LogInformation("Enqueued scan job {JobId} for audiobook {AudiobookId}", jobId, id);
27812782

27822783
// Broadcast initial job status via SignalR so clients can show queued state

0 commit comments

Comments
 (0)