mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-01-26 00:14:49 +01:00
Merge pull request #2333 from kieraneglin/ke/feature/upload-auto-fetch-data
Add ability to fetch book data on upload
This commit is contained in:
commit
fbc2c2b481
@ -15,24 +15,33 @@
|
|||||||
|
|
||||||
<div class="flex my-2 -mx-2">
|
<div class="flex my-2 -mx-2">
|
||||||
<div class="w-1/2 px-2">
|
<div class="w-1/2 px-2">
|
||||||
<ui-text-input-with-label v-model="itemData.title" :disabled="processing" :label="$strings.LabelTitle" @input="titleUpdated" />
|
<ui-text-input-with-label v-model.trim="itemData.title" :disabled="processing" :label="$strings.LabelTitle" @input="titleUpdated" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2 px-2">
|
<div class="w-1/2 px-2">
|
||||||
<ui-text-input-with-label v-if="!isPodcast" v-model="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" />
|
<div v-if="!isPodcast" class="flex items-end">
|
||||||
|
<ui-text-input-with-label v-model.trim="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" />
|
||||||
|
<ui-tooltip :text="$strings.LabelUploaderItemFetchMetadataHelp">
|
||||||
|
<div
|
||||||
|
class="ml-2 mb-1 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-primary cursor-pointer"
|
||||||
|
@click="fetchMetadata">
|
||||||
|
<span class="text-base text-white text-opacity-80 font-mono material-icons">sync</span>
|
||||||
|
</div>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
<div v-else class="w-full">
|
<div v-else class="w-full">
|
||||||
<p class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></p>
|
<p class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></p>
|
||||||
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs" style="height: 38px" />
|
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!isPodcast" class="flex my-2 -mx-2">
|
<div v-if="!isPodcast" class="flex my-2 -mx-2">
|
||||||
<div class="w-1/2 px-2">
|
<div class="w-1/2 px-2">
|
||||||
<ui-text-input-with-label v-model="itemData.series" :disabled="processing" :label="$strings.LabelSeries" note="(optional)" />
|
<ui-text-input-with-label v-model.trim="itemData.series" :disabled="processing" :label="$strings.LabelSeries" note="(optional)" inputClass="h-10" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2 px-2">
|
<div class="w-1/2 px-2">
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<p class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></p>
|
<label class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></label>
|
||||||
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs" style="height: 38px" />
|
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs h-10" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -48,8 +57,8 @@
|
|||||||
<p class="text-base">{{ $strings.MessageUploaderItemFailed }}</p>
|
<p class="text-base">{{ $strings.MessageUploaderItemFailed }}</p>
|
||||||
</widgets-alert>
|
</widgets-alert>
|
||||||
|
|
||||||
<div v-if="isUploading" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20">
|
<div v-if="isNonInteractable" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20">
|
||||||
<ui-loading-indicator :text="$strings.MessageUploading" />
|
<ui-loading-indicator :text="nonInteractionLabel" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -64,7 +73,8 @@ export default {
|
|||||||
default: () => { }
|
default: () => { }
|
||||||
},
|
},
|
||||||
mediaType: String,
|
mediaType: String,
|
||||||
processing: Boolean
|
processing: Boolean,
|
||||||
|
provider: String
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -76,7 +86,8 @@ export default {
|
|||||||
error: '',
|
error: '',
|
||||||
isUploading: false,
|
isUploading: false,
|
||||||
uploadFailed: false,
|
uploadFailed: false,
|
||||||
uploadSuccess: false
|
uploadSuccess: false,
|
||||||
|
isFetchingMetadata: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -87,12 +98,19 @@ export default {
|
|||||||
if (!this.itemData.title) return ''
|
if (!this.itemData.title) return ''
|
||||||
if (this.isPodcast) return this.itemData.title
|
if (this.isPodcast) return this.itemData.title
|
||||||
|
|
||||||
if (this.itemData.series && this.itemData.author) {
|
const outputPathParts = [this.itemData.author, this.itemData.series, this.itemData.title]
|
||||||
return Path.join(this.itemData.author, this.itemData.series, this.itemData.title)
|
const cleanedOutputPathParts = outputPathParts.filter(Boolean).map(part => this.$sanitizeFilename(part))
|
||||||
} else if (this.itemData.author) {
|
|
||||||
return Path.join(this.itemData.author, this.itemData.title)
|
return Path.join(...cleanedOutputPathParts)
|
||||||
} else {
|
},
|
||||||
return this.itemData.title
|
isNonInteractable() {
|
||||||
|
return this.isUploading || this.isFetchingMetadata
|
||||||
|
},
|
||||||
|
nonInteractionLabel() {
|
||||||
|
if (this.isUploading) {
|
||||||
|
return this.$strings.MessageUploading
|
||||||
|
} else if (this.isFetchingMetadata) {
|
||||||
|
return this.$strings.LabelFetchingMetadata
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -105,9 +123,42 @@ export default {
|
|||||||
titleUpdated() {
|
titleUpdated() {
|
||||||
this.error = ''
|
this.error = ''
|
||||||
},
|
},
|
||||||
|
async fetchMetadata() {
|
||||||
|
if (!this.itemData.title.trim().length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isFetchingMetadata = true
|
||||||
|
this.error = ''
|
||||||
|
|
||||||
|
try {
|
||||||
|
const searchQueryString = new URLSearchParams({
|
||||||
|
title: this.itemData.title,
|
||||||
|
author: this.itemData.author,
|
||||||
|
provider: this.provider
|
||||||
|
})
|
||||||
|
const [bestCandidate, ..._rest] = await this.$axios.$get(`/api/search/books?${searchQueryString}`)
|
||||||
|
|
||||||
|
if (bestCandidate) {
|
||||||
|
this.itemData = {
|
||||||
|
...this.itemData,
|
||||||
|
title: bestCandidate.title,
|
||||||
|
author: bestCandidate.author,
|
||||||
|
series: (bestCandidate.series || [])[0]?.series
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.error = this.$strings.ErrorUploadFetchMetadataNoResults
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed', e)
|
||||||
|
this.error = this.$strings.ErrorUploadFetchMetadataAPI
|
||||||
|
} finally {
|
||||||
|
this.isFetchingMetadata = false
|
||||||
|
}
|
||||||
|
},
|
||||||
getData() {
|
getData() {
|
||||||
if (!this.itemData.title) {
|
if (!this.itemData.title) {
|
||||||
this.error = 'Must have a title'
|
this.error = this.$strings.ErrorUploadLacksTitle
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
this.error = ''
|
this.error = ''
|
||||||
|
@ -14,6 +14,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!selectedLibraryIsPodcast" class="flex items-center mb-6">
|
||||||
|
<label class="flex cursor-pointer pt-4">
|
||||||
|
<ui-toggle-switch v-model="fetchMetadata.enabled" class="inline-flex" />
|
||||||
|
<span class="pl-2 text-base">{{ $strings.LabelAutoFetchMetadata }}</span>
|
||||||
|
</label>
|
||||||
|
<ui-tooltip :text="$strings.LabelAutoFetchMetadataHelp" class="inline-flex pt-4">
|
||||||
|
<span class="pl-1 material-icons icon-text text-sm cursor-pointer">info_outlined</span>
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
|
<div class="flex-grow ml-4">
|
||||||
|
<ui-dropdown v-model="fetchMetadata.provider" :items="providers" :label="$strings.LabelProvider" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<widgets-alert v-if="error" type="error">
|
<widgets-alert v-if="error" type="error">
|
||||||
<p class="text-lg">{{ error }}</p>
|
<p class="text-lg">{{ error }}</p>
|
||||||
</widgets-alert>
|
</widgets-alert>
|
||||||
@ -61,9 +75,7 @@
|
|||||||
</widgets-alert>
|
</widgets-alert>
|
||||||
|
|
||||||
<!-- Item Upload cards -->
|
<!-- Item Upload cards -->
|
||||||
<template v-for="item in items">
|
<cards-item-upload-card v-for="item in items" :key="item.index" :ref="`itemCard-${item.index}`" :media-type="selectedLibraryMediaType" :item="item" :provider="fetchMetadata.provider" :processing="processing" @remove="removeItem(item)" />
|
||||||
<cards-item-upload-card :ref="`itemCard-${item.index}`" :key="item.index" :media-type="selectedLibraryMediaType" :item="item" :processing="processing" @remove="removeItem(item)" />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Upload/Reset btns -->
|
<!-- Upload/Reset btns -->
|
||||||
<div v-show="items.length" class="flex justify-end pb-8 pt-4">
|
<div v-show="items.length" class="flex justify-end pb-8 pt-4">
|
||||||
@ -92,13 +104,18 @@ export default {
|
|||||||
selectedLibraryId: null,
|
selectedLibraryId: null,
|
||||||
selectedFolderId: null,
|
selectedFolderId: null,
|
||||||
processing: false,
|
processing: false,
|
||||||
uploadFinished: false
|
uploadFinished: false,
|
||||||
|
fetchMetadata: {
|
||||||
|
enabled: false,
|
||||||
|
provider: null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
selectedLibrary(newVal) {
|
selectedLibrary(newVal) {
|
||||||
if (newVal && !this.selectedFolderId) {
|
if (newVal && !this.selectedFolderId) {
|
||||||
this.setDefaultFolder()
|
this.setDefaultFolder()
|
||||||
|
this.setMetadataProvider()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -133,6 +150,13 @@ export default {
|
|||||||
selectedLibraryIsPodcast() {
|
selectedLibraryIsPodcast() {
|
||||||
return this.selectedLibraryMediaType === 'podcast'
|
return this.selectedLibraryMediaType === 'podcast'
|
||||||
},
|
},
|
||||||
|
providers() {
|
||||||
|
if (this.selectedLibraryIsPodcast) return this.$store.state.scanners.podcastProviders
|
||||||
|
return this.$store.state.scanners.providers
|
||||||
|
},
|
||||||
|
canFetchMetadata() {
|
||||||
|
return !this.selectedLibraryIsPodcast && this.fetchMetadata.enabled
|
||||||
|
},
|
||||||
selectedFolder() {
|
selectedFolder() {
|
||||||
if (!this.selectedLibrary) return null
|
if (!this.selectedLibrary) return null
|
||||||
return this.selectedLibrary.folders.find((fold) => fold.id === this.selectedFolderId)
|
return this.selectedLibrary.folders.find((fold) => fold.id === this.selectedFolderId)
|
||||||
@ -160,12 +184,16 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.setDefaultFolder()
|
this.setDefaultFolder()
|
||||||
|
this.setMetadataProvider()
|
||||||
},
|
},
|
||||||
setDefaultFolder() {
|
setDefaultFolder() {
|
||||||
if (!this.selectedFolderId && this.selectedLibrary && this.selectedLibrary.folders.length) {
|
if (!this.selectedFolderId && this.selectedLibrary && this.selectedLibrary.folders.length) {
|
||||||
this.selectedFolderId = this.selectedLibrary.folders[0].id
|
this.selectedFolderId = this.selectedLibrary.folders[0].id
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setMetadataProvider() {
|
||||||
|
this.fetchMetadata.provider ||= this.$store.getters['libraries/getLibraryProvider'](this.selectedLibraryId)
|
||||||
|
},
|
||||||
removeItem(item) {
|
removeItem(item) {
|
||||||
this.items = this.items.filter((b) => b.index !== item.index)
|
this.items = this.items.filter((b) => b.index !== item.index)
|
||||||
if (!this.items.length) {
|
if (!this.items.length) {
|
||||||
@ -213,27 +241,49 @@ export default {
|
|||||||
var items = e.dataTransfer.items || []
|
var items = e.dataTransfer.items || []
|
||||||
|
|
||||||
var itemResults = await this.uploadHelpers.getItemsFromDrop(items, this.selectedLibraryMediaType)
|
var itemResults = await this.uploadHelpers.getItemsFromDrop(items, this.selectedLibraryMediaType)
|
||||||
this.setResults(itemResults)
|
this.onItemsSelected(itemResults)
|
||||||
},
|
},
|
||||||
inputChanged(e) {
|
inputChanged(e) {
|
||||||
if (!e.target || !e.target.files) return
|
if (!e.target || !e.target.files) return
|
||||||
var _files = Array.from(e.target.files)
|
var _files = Array.from(e.target.files)
|
||||||
if (_files && _files.length) {
|
if (_files && _files.length) {
|
||||||
var itemResults = this.uploadHelpers.getItemsFromPicker(_files, this.selectedLibraryMediaType)
|
var itemResults = this.uploadHelpers.getItemsFromPicker(_files, this.selectedLibraryMediaType)
|
||||||
this.setResults(itemResults)
|
this.onItemsSelected(itemResults)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setResults(itemResults) {
|
onItemsSelected(itemResults) {
|
||||||
|
if (this.itemSelectionSuccessful(itemResults)) {
|
||||||
|
// setTimeout ensures the new item ref is attached before this method is called
|
||||||
|
setTimeout(this.attemptMetadataFetch, 0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemSelectionSuccessful(itemResults) {
|
||||||
|
console.log('Upload results', itemResults)
|
||||||
|
|
||||||
if (itemResults.error) {
|
if (itemResults.error) {
|
||||||
this.error = itemResults.error
|
this.error = itemResults.error
|
||||||
this.items = []
|
this.items = []
|
||||||
this.ignoredFiles = []
|
this.ignoredFiles = []
|
||||||
} else {
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
this.error = ''
|
this.error = ''
|
||||||
this.items = itemResults.items
|
this.items = itemResults.items
|
||||||
this.ignoredFiles = itemResults.ignoredFiles
|
this.ignoredFiles = itemResults.ignoredFiles
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
attemptMetadataFetch() {
|
||||||
|
if (!this.canFetchMetadata) {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
console.log('Upload results', itemResults)
|
|
||||||
|
this.items.forEach((item) => {
|
||||||
|
let itemRef = this.$refs[`itemCard-${item.index}`]
|
||||||
|
|
||||||
|
if (itemRef?.length) {
|
||||||
|
itemRef[0].fetchMetadata(this.fetchMetadata.provider)
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
updateItemCardStatus(index, status) {
|
updateItemCardStatus(index, status) {
|
||||||
var ref = this.$refs[`itemCard-${index}`]
|
var ref = this.$refs[`itemCard-${index}`]
|
||||||
@ -248,8 +298,8 @@ export default {
|
|||||||
var form = new FormData()
|
var form = new FormData()
|
||||||
form.set('title', item.title)
|
form.set('title', item.title)
|
||||||
if (!this.selectedLibraryIsPodcast) {
|
if (!this.selectedLibraryIsPodcast) {
|
||||||
form.set('author', item.author)
|
form.set('author', item.author || '')
|
||||||
form.set('series', item.series)
|
form.set('series', item.series || '')
|
||||||
}
|
}
|
||||||
form.set('library', this.selectedLibraryId)
|
form.set('library', this.selectedLibraryId)
|
||||||
form.set('folder', this.selectedFolderId)
|
form.set('folder', this.selectedFolderId)
|
||||||
@ -346,6 +396,8 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.selectedLibraryId = this.$store.state.libraries.currentLibraryId
|
this.selectedLibraryId = this.$store.state.libraries.currentLibraryId
|
||||||
|
this.setMetadataProvider()
|
||||||
|
|
||||||
this.setDefaultFolder()
|
this.setDefaultFolder()
|
||||||
window.addEventListener('dragenter', this.dragenter)
|
window.addEventListener('dragenter', this.dragenter)
|
||||||
window.addEventListener('dragleave', this.dragleave)
|
window.addEventListener('dragleave', this.dragleave)
|
||||||
|
@ -77,6 +77,7 @@ Vue.prototype.$sanitizeFilename = (filename, colonReplacement = ' - ') => {
|
|||||||
.replace(lineBreaks, replacement)
|
.replace(lineBreaks, replacement)
|
||||||
.replace(windowsReservedRe, replacement)
|
.replace(windowsReservedRe, replacement)
|
||||||
.replace(windowsTrailingRe, replacement)
|
.replace(windowsTrailingRe, replacement)
|
||||||
|
.replace(/\s+/g, ' ') // Replace consecutive spaces with a single space
|
||||||
|
|
||||||
// Check if basename is too many bytes
|
// Check if basename is too many bytes
|
||||||
const ext = Path.extname(sanitized) // separate out file extension
|
const ext = Path.extname(sanitized) // separate out file extension
|
||||||
|
@ -87,6 +87,9 @@
|
|||||||
"ButtonUserEdit": "Edit user {0}",
|
"ButtonUserEdit": "Edit user {0}",
|
||||||
"ButtonViewAll": "View All",
|
"ButtonViewAll": "View All",
|
||||||
"ButtonYes": "Yes",
|
"ButtonYes": "Yes",
|
||||||
|
"ErrorUploadFetchMetadataAPI": "Error fetching metadata",
|
||||||
|
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
|
||||||
|
"ErrorUploadLacksTitle": "Must have a title",
|
||||||
"HeaderAccount": "Account",
|
"HeaderAccount": "Account",
|
||||||
"HeaderAdvanced": "Advanced",
|
"HeaderAdvanced": "Advanced",
|
||||||
"HeaderAppriseNotificationSettings": "Apprise Notification Settings",
|
"HeaderAppriseNotificationSettings": "Apprise Notification Settings",
|
||||||
@ -196,6 +199,8 @@
|
|||||||
"LabelAuthorLastFirst": "Author (Last, First)",
|
"LabelAuthorLastFirst": "Author (Last, First)",
|
||||||
"LabelAuthors": "Authors",
|
"LabelAuthors": "Authors",
|
||||||
"LabelAutoDownloadEpisodes": "Auto Download Episodes",
|
"LabelAutoDownloadEpisodes": "Auto Download Episodes",
|
||||||
|
"LabelAutoFetchMetadata": "Auto Fetch Metadata",
|
||||||
|
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
|
||||||
"LabelAutoLaunch": "Auto Launch",
|
"LabelAutoLaunch": "Auto Launch",
|
||||||
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
|
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
|
||||||
"LabelAutoRegister": "Auto Register",
|
"LabelAutoRegister": "Auto Register",
|
||||||
@ -266,6 +271,7 @@
|
|||||||
"LabelExample": "Example",
|
"LabelExample": "Example",
|
||||||
"LabelExplicit": "Explicit",
|
"LabelExplicit": "Explicit",
|
||||||
"LabelFeedURL": "Feed URL",
|
"LabelFeedURL": "Feed URL",
|
||||||
|
"LabelFetchingMetadata": "Fetching Metadata",
|
||||||
"LabelFile": "File",
|
"LabelFile": "File",
|
||||||
"LabelFileBirthtime": "File Birthtime",
|
"LabelFileBirthtime": "File Birthtime",
|
||||||
"LabelFileModified": "File Modified",
|
"LabelFileModified": "File Modified",
|
||||||
@ -515,6 +521,7 @@
|
|||||||
"LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located",
|
"LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located",
|
||||||
"LabelUploaderDragAndDrop": "Drag & drop files or folders",
|
"LabelUploaderDragAndDrop": "Drag & drop files or folders",
|
||||||
"LabelUploaderDropFiles": "Drop files",
|
"LabelUploaderDropFiles": "Drop files",
|
||||||
|
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
|
||||||
"LabelUseChapterTrack": "Use chapter track",
|
"LabelUseChapterTrack": "Use chapter track",
|
||||||
"LabelUseFullTrack": "Use full track",
|
"LabelUseFullTrack": "Use full track",
|
||||||
"LabelUser": "User",
|
"LabelUser": "User",
|
||||||
|
@ -8,6 +8,7 @@ const Database = require('../Database')
|
|||||||
const libraryItemFilters = require('../utils/queries/libraryItemFilters')
|
const libraryItemFilters = require('../utils/queries/libraryItemFilters')
|
||||||
const patternValidation = require('../libs/nodeCron/pattern-validation')
|
const patternValidation = require('../libs/nodeCron/pattern-validation')
|
||||||
const { isObject, getTitleIgnorePrefix } = require('../utils/index')
|
const { isObject, getTitleIgnorePrefix } = require('../utils/index')
|
||||||
|
const { sanitizeFilename } = require('../utils/fileUtils')
|
||||||
|
|
||||||
const TaskManager = require('../managers/TaskManager')
|
const TaskManager = require('../managers/TaskManager')
|
||||||
|
|
||||||
@ -32,12 +33,9 @@ class MiscController {
|
|||||||
Logger.error('Invalid request, no files')
|
Logger.error('Invalid request, no files')
|
||||||
return res.sendStatus(400)
|
return res.sendStatus(400)
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = Object.values(req.files)
|
const files = Object.values(req.files)
|
||||||
const title = req.body.title
|
const { title, author, series, folder: folderId, library: libraryId } = req.body
|
||||||
const author = req.body.author
|
|
||||||
const series = req.body.series
|
|
||||||
const libraryId = req.body.library
|
|
||||||
const folderId = req.body.folder
|
|
||||||
|
|
||||||
const library = await Database.libraryModel.getOldById(libraryId)
|
const library = await Database.libraryModel.getOldById(libraryId)
|
||||||
if (!library) {
|
if (!library) {
|
||||||
@ -52,40 +50,26 @@ class MiscController {
|
|||||||
return res.status(500).send(`Invalid post data`)
|
return res.status(500).send(`Invalid post data`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// For setting permissions recursively
|
// Podcasts should only be one folder deep
|
||||||
let outputDirectory = ''
|
const outputDirectoryParts = library.isPodcast ? [title] : [author, series, title]
|
||||||
let firstDirPath = ''
|
// `.filter(Boolean)` to strip out all the potentially missing details (eg: `author`)
|
||||||
|
// before sanitizing all the directory parts to remove illegal chars and finally prepending
|
||||||
if (library.isPodcast) { // Podcasts only in 1 folder
|
// the base folder path
|
||||||
outputDirectory = Path.join(folder.fullPath, title)
|
const cleanedOutputDirectoryParts = outputDirectoryParts.filter(Boolean).map(part => sanitizeFilename(part))
|
||||||
firstDirPath = outputDirectory
|
const outputDirectory = Path.join(...[folder.fullPath, ...cleanedOutputDirectoryParts])
|
||||||
} else {
|
|
||||||
firstDirPath = Path.join(folder.fullPath, author)
|
|
||||||
if (series && author) {
|
|
||||||
outputDirectory = Path.join(folder.fullPath, author, series, title)
|
|
||||||
} else if (author) {
|
|
||||||
outputDirectory = Path.join(folder.fullPath, author, title)
|
|
||||||
} else {
|
|
||||||
outputDirectory = Path.join(folder.fullPath, title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await fs.pathExists(outputDirectory)) {
|
|
||||||
Logger.error(`[Server] Upload directory "${outputDirectory}" already exists`)
|
|
||||||
return res.status(500).send(`Directory "${outputDirectory}" already exists`)
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.ensureDir(outputDirectory)
|
await fs.ensureDir(outputDirectory)
|
||||||
|
|
||||||
Logger.info(`Uploading ${files.length} files to`, outputDirectory)
|
Logger.info(`Uploading ${files.length} files to`, outputDirectory)
|
||||||
|
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (const file of files) {
|
||||||
var file = files[i]
|
const path = Path.join(outputDirectory, sanitizeFilename(file.name))
|
||||||
|
|
||||||
var path = Path.join(outputDirectory, file.name)
|
await file.mv(path)
|
||||||
await file.mv(path).then(() => {
|
.then(() => {
|
||||||
return true
|
return true
|
||||||
}).catch((error) => {
|
})
|
||||||
|
.catch((error) => {
|
||||||
Logger.error('Failed to move file', path, error)
|
Logger.error('Failed to move file', path, error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
@ -308,6 +308,7 @@ module.exports.sanitizeFilename = (filename, colonReplacement = ' - ') => {
|
|||||||
.replace(lineBreaks, replacement)
|
.replace(lineBreaks, replacement)
|
||||||
.replace(windowsReservedRe, replacement)
|
.replace(windowsReservedRe, replacement)
|
||||||
.replace(windowsTrailingRe, replacement)
|
.replace(windowsTrailingRe, replacement)
|
||||||
|
.replace(/\s+/g, ' ') // Replace consecutive spaces with a single space
|
||||||
|
|
||||||
// Check if basename is too many bytes
|
// Check if basename is too many bytes
|
||||||
const ext = Path.extname(sanitized) // separate out file extension
|
const ext = Path.extname(sanitized) // separate out file extension
|
||||||
|
Loading…
Reference in New Issue
Block a user