mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-16 14:21:41 +00:00
Added functionality to rename book folders.
This commit is contained in:
parent
f9e6655359
commit
8ccaec3d9f
12 changed files with 652 additions and 148 deletions
|
|
@ -7,6 +7,7 @@
|
||||||
"fish": "latest"
|
"fish": "latest"
|
||||||
},
|
},
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"eamodio.gitlens"
|
"eamodio.gitlens",
|
||||||
|
"octref.vetur"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -50,6 +50,24 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Rename Folders -->
|
||||||
|
<div v-if="userIsAdminOrUp" class="w-full border border-black-200 p-4 my-8">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div>
|
||||||
|
<p class="text-lg">{{ $strings.LabelToolsRenamer }}</p>
|
||||||
|
<p class="max-w-sm text-sm pt-2 text-gray-300">{{ $strings.LabelToolsRenamerDescription }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<div>
|
||||||
|
<ui-btn :to="`/audiobook/${libraryItemId}/manage?tool=renamer`" class="flex items-center"
|
||||||
|
>{{ $strings.ButtonOpenManager }}
|
||||||
|
<span class="material-icons text-lg ml-2">launch</span>
|
||||||
|
</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p v-if="!mediaTracks.length" class="text-lg text-center my-8">{{ $strings.MessageNoAudioTracks }}</p>
|
<p v-if="!mediaTracks.length" class="text-lg text-center my-8">{{ $strings.MessageNoAudioTracks }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -70,6 +88,9 @@ export default {
|
||||||
showExperimentalFeatures() {
|
showExperimentalFeatures() {
|
||||||
return this.$store.state.showExperimentalFeatures
|
return this.$store.state.showExperimentalFeatures
|
||||||
},
|
},
|
||||||
|
userIsAdminOrUp() {
|
||||||
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
|
},
|
||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
return this.libraryItem ? this.libraryItem.id : null
|
return this.libraryItem ? this.libraryItem.id : null
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isM4BorEmbedTool">
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="w-full max-w-2xl">
|
<div class="w-full max-w-2xl">
|
||||||
<p class="text-xl mb-1">{{ $strings.HeaderMetadataToEmbed }}</p>
|
<p class="text-xl mb-1">{{ $strings.HeaderMetadataToEmbed }}</p>
|
||||||
|
|
@ -164,6 +165,56 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isRenameTool">
|
||||||
|
<div class="flex justify-left">
|
||||||
|
<div class="w-full max-w-2xl">
|
||||||
|
<p class="text-xl mb-1">{{ $strings.HeaderRenameTool }}</p>
|
||||||
|
<div class="flex items-start mb-2">
|
||||||
|
<ui-text-input-with-label ref="userRenameFormat" v-model="renameFormat" :label="'Format for Rename'" class="m-2 max-w-100" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start mb-2">
|
||||||
|
<ui-btn color="primary" :loading="processing" @click.stop="previewRenameClick">{{$strings.ButtonPreviewRename}}</ui-btn>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start mb-2">
|
||||||
|
<span class="material-icons text-base text-warning pt-1">star</span>
|
||||||
|
{{ $strings.LabelCurrent }} {{ $strings.LabelPath }}: <span ref="currentRelPath" class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">...{{this.directorySeperatorChar}}{{ libraryItemRelPath }}{{this.directorySeperatorChar}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start mb-2">
|
||||||
|
<span class="material-icons text-base text-warning pt-1">star</span>
|
||||||
|
{{ $strings.LabelNew }} {{ $strings.LabelPath }}: <span ref="newFolderRelPath" class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">...{{this.directorySeperatorChar}}{{ libraryItemRelPath }}{{this.directorySeperatorChar}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start mb-2">
|
||||||
|
<ui-btn ref="executeRenameButton" :loading="processing" color="primary" @click.stop="renameClick">{{$strings.ButtonExecuteRename}}</ui-btn>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-start mb-2">
|
||||||
|
<span ref="renameStatus" class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full max-w-2xl">
|
||||||
|
<div class="flex py-2 px-4 bg-primary bg-opacity-25">
|
||||||
|
<div class="w-1/3 text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelKey }}</div>
|
||||||
|
<div class="w-2/3 text-xs font-semibold uppercase text-gray-200">{{ $strings.LabelValue }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex py-1 px-4 text-sm" >
|
||||||
|
<div class="w-1/3 font-semibold">/</div>
|
||||||
|
<div class="w-2/3">
|
||||||
|
{{$strings.LabelDirectorySeperator}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-for="(value, key, index) in sampleFormatObject">
|
||||||
|
<div :key="key" class="flex py-1 px-4 text-sm" :class="index % 2 === 0 ? 'bg-primary bg-opacity-25' : ''">
|
||||||
|
<div class="w-1/3 font-semibold">${{ key.replace('lbr','[').replace('rbr',']') }}</div>
|
||||||
|
<div class="w-2/3">
|
||||||
|
{{ value }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -210,7 +261,10 @@ export default {
|
||||||
bitrate: '64k',
|
bitrate: '64k',
|
||||||
channels: '2',
|
channels: '2',
|
||||||
codec: 'aac'
|
codec: 'aac'
|
||||||
}
|
},
|
||||||
|
renameFormat: '',
|
||||||
|
sampleFormatObject: null,
|
||||||
|
directorySeperatorChar: '/'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
@ -229,6 +283,12 @@ export default {
|
||||||
isM4BTool() {
|
isM4BTool() {
|
||||||
return this.selectedTool === 'm4b'
|
return this.selectedTool === 'm4b'
|
||||||
},
|
},
|
||||||
|
isM4BorEmbedTool() {
|
||||||
|
return this.isM4BTool || this.isEmbedTool
|
||||||
|
},
|
||||||
|
isRenameTool() {
|
||||||
|
return this.selectedTool === 'renamer'
|
||||||
|
},
|
||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
return this.libraryItem.id
|
return this.libraryItem.id
|
||||||
},
|
},
|
||||||
|
|
@ -250,18 +310,22 @@ export default {
|
||||||
streamLibraryItem() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamLibraryItem
|
return this.$store.state.streamLibraryItem
|
||||||
},
|
},
|
||||||
|
defaultRenameString() {
|
||||||
|
return this.$store.state.defaultRenameString
|
||||||
|
},
|
||||||
metadataChapters() {
|
metadataChapters() {
|
||||||
return this.media.chapters || []
|
return this.media.chapters || []
|
||||||
},
|
},
|
||||||
availableTools() {
|
availableTools() {
|
||||||
if (this.isSingleM4b) {
|
var tools = [
|
||||||
return [{ value: 'embed', text: 'Embed Metadata' }]
|
|
||||||
} else {
|
|
||||||
return [
|
|
||||||
{ value: 'embed', text: 'Embed Metadata' },
|
{ value: 'embed', text: 'Embed Metadata' },
|
||||||
|
{ value: 'renamer', text: 'Rename Folder' },
|
||||||
{ value: 'm4b', text: 'M4B Encoder' }
|
{ value: 'm4b', text: 'M4B Encoder' }
|
||||||
]
|
]
|
||||||
|
if (this.isSingleM4b) {
|
||||||
|
tools.pop()
|
||||||
}
|
}
|
||||||
|
return tools
|
||||||
},
|
},
|
||||||
taskFailed() {
|
taskFailed() {
|
||||||
return this.isTaskFinished && this.task.isFailed
|
return this.isTaskFinished && this.task.isFailed
|
||||||
|
|
@ -337,6 +401,41 @@ export default {
|
||||||
}
|
}
|
||||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
},
|
},
|
||||||
|
async previewRenameClick(){
|
||||||
|
this.processing = true
|
||||||
|
const renameObject = await this.$axios
|
||||||
|
.$get(`/api/tools/item/${this.libraryItemId}/renameFolder?strFormat=${this.renameFormat}${this.sampleFormatObject==null ? '&includeSampleTemplate=1' : ''}`)
|
||||||
|
.then((renameObject) => {
|
||||||
|
return renameObject
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to Rename', error)
|
||||||
|
})
|
||||||
|
this.directorySeperatorChar = renameObject.directorySeperatorChar
|
||||||
|
this.$refs.newFolderRelPath.setHTML('...' + this.directorySeperatorChar + renameObject.newRelativeDirectory + this.directorySeperatorChar)
|
||||||
|
if(renameObject.templateSample){
|
||||||
|
this.sampleFormatObject = renameObject.templateSample
|
||||||
|
}
|
||||||
|
this.processing = false
|
||||||
|
},
|
||||||
|
async renameClick(){
|
||||||
|
const renameObject = await this.$axios
|
||||||
|
.$post(`/api/tools/item/${this.libraryItemId}/renameFolder?strFormat=${this.renameFormat}`)
|
||||||
|
.then((renameObject) => {
|
||||||
|
return renameObject
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to Rename', error)
|
||||||
|
})
|
||||||
|
if(renameObject.success){
|
||||||
|
this.$refs.renameStatus.setHTML('Folder for Book updated to ...' + this.directorySeperatorChar + renameObject.details.newRelativeDirectory + this.directorySeperatorChar + ' from ' + renameObject.details.oldRelativeDirectory)
|
||||||
|
this.$refs.currentRelPath.setHTML('...' + this.directorySeperatorChar + renameObject.details.newRelativeDirectory + this.directorySeperatorChar)
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
this.$refs.renameStatus.setHTML('Failed to Rename: ' + JSON.stringify(renameObject.details))
|
||||||
|
}
|
||||||
|
this.processing = false
|
||||||
|
},
|
||||||
updateAudioFileMetadata() {
|
updateAudioFileMetadata() {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
|
|
@ -379,6 +478,9 @@ export default {
|
||||||
selectedToolUpdated() {
|
selectedToolUpdated() {
|
||||||
let newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + `?tool=${this.selectedTool}`
|
let newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + `?tool=${this.selectedTool}`
|
||||||
window.history.replaceState({ path: newurl }, '', newurl)
|
window.history.replaceState({ path: newurl }, '', newurl)
|
||||||
|
if(this.selectedTool == 'renamer'){
|
||||||
|
this.previewRenameClick()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
this.fetchToneObject()
|
this.fetchToneObject()
|
||||||
|
|
@ -389,11 +491,22 @@ export default {
|
||||||
this.selectedToolUpdated()
|
this.selectedToolUpdated()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else if (this.$route.query.tool === 'renamer') {
|
||||||
|
if (this.availableTools.some((t) => t.value === 'renamer')) {
|
||||||
|
this.previewRenameClick()
|
||||||
|
this.selectedTool = 'renamer'
|
||||||
|
} else {
|
||||||
|
this.selectedToolUpdated()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.task) this.taskUpdated(this.task)
|
if (this.task) this.taskUpdated(this.task)
|
||||||
|
|
||||||
const shouldBackupAudioFiles = localStorage.getItem('embedMetadataShouldBackup')
|
const shouldBackupAudioFiles = localStorage.getItem('embedMetadataShouldBackup')
|
||||||
this.shouldBackupAudioFiles = shouldBackupAudioFiles != 0
|
this.shouldBackupAudioFiles = shouldBackupAudioFiles != 0
|
||||||
|
const defaultRenameString = localStorage.getItem('defaultRenameString')
|
||||||
|
this.renameFormat = defaultRenameString ? defaultRenameString : '$authorName/$bookName'
|
||||||
|
debugger
|
||||||
},
|
},
|
||||||
fetchToneObject() {
|
fetchToneObject() {
|
||||||
this.$axios
|
this.$axios
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,16 @@
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center py-2">
|
||||||
|
<ui-text-input-with-label ref="defaultRenameString" v-model="newServerSettings.defaultRenameString" :disabled="updatingServerSettings" :label="$strings.LabelSettingsRenameString" @blur="() => updateDefaultRenameString()" class="m-2 max-w-100" />
|
||||||
|
<ui-tooltip :text="$strings.LabelSettingsRenameStringHelp">
|
||||||
|
<p class="pl-4">
|
||||||
|
<span id="settings-sorting-ignore-prefixes"></span>
|
||||||
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- <div class="flex items-center py-2">
|
<!-- <div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerUseTone" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerUseTone', val)" />
|
<ui-toggle-switch v-model="newServerSettings.scannerUseTone" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerUseTone', val)" />
|
||||||
<ui-tooltip text="Tone library for metadata">
|
<ui-tooltip text="Tone library for metadata">
|
||||||
|
|
@ -291,6 +301,14 @@ export default {
|
||||||
this.$store.commit('setExperimentalFeatures', val)
|
this.$store.commit('setExperimentalFeatures', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
defaultRenameString: {
|
||||||
|
get() {
|
||||||
|
return this.$store.state.defaultRenameString
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$store.commit('defaultRenameString', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
dateFormats() {
|
dateFormats() {
|
||||||
return this.$store.state.globals.dateFormats
|
return this.$store.state.globals.dateFormats
|
||||||
}
|
}
|
||||||
|
|
@ -324,6 +342,10 @@ export default {
|
||||||
updateServerLanguage(val) {
|
updateServerLanguage(val) {
|
||||||
this.updateSettingsKey('language', val)
|
this.updateSettingsKey('language', val)
|
||||||
},
|
},
|
||||||
|
updateDefaultRenameString(){
|
||||||
|
this.updateSettingsKey('defaultRenameString', this.newServerSettings.defaultRenameString)
|
||||||
|
this.$store.commit('setDefaultRenameString', this.newServerSettings.defaultRenameString)
|
||||||
|
},
|
||||||
updateSettingsKey(key, val) {
|
updateSettingsKey(key, val) {
|
||||||
this.updateServerSettings({
|
this.updateServerSettings({
|
||||||
[key]: val
|
[key]: val
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ export const state = () => ({
|
||||||
processingBatch: false,
|
processingBatch: false,
|
||||||
previousPath: '/',
|
previousPath: '/',
|
||||||
showExperimentalFeatures: false,
|
showExperimentalFeatures: false,
|
||||||
|
defaultRenameString: '',
|
||||||
bookshelfBookIds: [],
|
bookshelfBookIds: [],
|
||||||
openModal: null,
|
openModal: null,
|
||||||
innerModalOpen: false,
|
innerModalOpen: false,
|
||||||
|
|
@ -215,6 +216,10 @@ export const mutations = {
|
||||||
setProcessingBatch(state, val) {
|
setProcessingBatch(state, val) {
|
||||||
state.processingBatch = val
|
state.processingBatch = val
|
||||||
},
|
},
|
||||||
|
setDefaultRenameString(state, val) {
|
||||||
|
state.defaultRenameString = val
|
||||||
|
localStorage.setItem('defaultRenameString', val)
|
||||||
|
},
|
||||||
setExperimentalFeatures(state, val) {
|
setExperimentalFeatures(state, val) {
|
||||||
state.showExperimentalFeatures = val
|
state.showExperimentalFeatures = val
|
||||||
localStorage.setItem('experimental', val ? 1 : 0)
|
localStorage.setItem('experimental', val ? 1 : 0)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
"ButtonEdit": "Edit",
|
"ButtonEdit": "Edit",
|
||||||
"ButtonEditChapters": "Edit Chapters",
|
"ButtonEditChapters": "Edit Chapters",
|
||||||
"ButtonEditPodcast": "Edit Podcast",
|
"ButtonEditPodcast": "Edit Podcast",
|
||||||
|
"ButtonExecuteRename": "Execute Rename",
|
||||||
"ButtonForceReScan": "Force Re-Scan",
|
"ButtonForceReScan": "Force Re-Scan",
|
||||||
"ButtonFullPath": "Full Path",
|
"ButtonFullPath": "Full Path",
|
||||||
"ButtonHide": "Hide",
|
"ButtonHide": "Hide",
|
||||||
|
|
@ -43,6 +44,7 @@
|
||||||
"ButtonPlay": "Play",
|
"ButtonPlay": "Play",
|
||||||
"ButtonPlaying": "Playing",
|
"ButtonPlaying": "Playing",
|
||||||
"ButtonPlaylists": "Playlists",
|
"ButtonPlaylists": "Playlists",
|
||||||
|
"ButtonPreviewRename": "Preview Rename",
|
||||||
"ButtonPurgeAllCache": "Purge All Cache",
|
"ButtonPurgeAllCache": "Purge All Cache",
|
||||||
"ButtonPurgeItemsCache": "Purge Items Cache",
|
"ButtonPurgeItemsCache": "Purge Items Cache",
|
||||||
"ButtonPurgeMediaProgress": "Purge Media Progress",
|
"ButtonPurgeMediaProgress": "Purge Media Progress",
|
||||||
|
|
@ -126,6 +128,7 @@
|
||||||
"HeaderPreviewCover": "Preview Cover",
|
"HeaderPreviewCover": "Preview Cover",
|
||||||
"HeaderRemoveEpisode": "Remove Episode",
|
"HeaderRemoveEpisode": "Remove Episode",
|
||||||
"HeaderRemoveEpisodes": "Remove {0} Episodes",
|
"HeaderRemoveEpisodes": "Remove {0} Episodes",
|
||||||
|
"HeaderRenameTool": "Folder Renaming Tool",
|
||||||
"HeaderRSSFeedIsOpen": "RSS Feed is Open",
|
"HeaderRSSFeedIsOpen": "RSS Feed is Open",
|
||||||
"HeaderSavedMediaProgress": "Saved Media Progress",
|
"HeaderSavedMediaProgress": "Saved Media Progress",
|
||||||
"HeaderSchedule": "Schedule",
|
"HeaderSchedule": "Schedule",
|
||||||
|
|
@ -198,6 +201,7 @@
|
||||||
"LabelDevice": "Device",
|
"LabelDevice": "Device",
|
||||||
"LabelDeviceInfo": "Device Info",
|
"LabelDeviceInfo": "Device Info",
|
||||||
"LabelDirectory": "Directory",
|
"LabelDirectory": "Directory",
|
||||||
|
"LabelDirectorySeperator": "Directory Seperator",
|
||||||
"LabelDiscFromFilename": "Disc from Filename",
|
"LabelDiscFromFilename": "Disc from Filename",
|
||||||
"LabelDiscFromMetadata": "Disc from Metadata",
|
"LabelDiscFromMetadata": "Disc from Metadata",
|
||||||
"LabelDownload": "Download",
|
"LabelDownload": "Download",
|
||||||
|
|
@ -239,6 +243,7 @@
|
||||||
"LabelIntervalEveryHour": "Every hour",
|
"LabelIntervalEveryHour": "Every hour",
|
||||||
"LabelInvalidParts": "Invalid Parts",
|
"LabelInvalidParts": "Invalid Parts",
|
||||||
"LabelItem": "Item",
|
"LabelItem": "Item",
|
||||||
|
"LabelKey": "Key",
|
||||||
"LabelLanguage": "Language",
|
"LabelLanguage": "Language",
|
||||||
"LabelLanguageDefaultServer": "Default Server Language",
|
"LabelLanguageDefaultServer": "Default Server Language",
|
||||||
"LabelLastSeen": "Last Seen",
|
"LabelLastSeen": "Last Seen",
|
||||||
|
|
@ -347,6 +352,8 @@
|
||||||
"LabelSettingsPreferMatchedMetadataHelp": "Matched data will overide item details when using Quick Match. By default Quick Match will only fill in missing details.",
|
"LabelSettingsPreferMatchedMetadataHelp": "Matched data will overide item details when using Quick Match. By default Quick Match will only fill in missing details.",
|
||||||
"LabelSettingsPreferOPFMetadata": "Prefer OPF metadata",
|
"LabelSettingsPreferOPFMetadata": "Prefer OPF metadata",
|
||||||
"LabelSettingsPreferOPFMetadataHelp": "OPF file metadata will be used for book details over folder names",
|
"LabelSettingsPreferOPFMetadataHelp": "OPF file metadata will be used for book details over folder names",
|
||||||
|
"LabelSettingsRenameString": "Default Rename Format",
|
||||||
|
"LabelSettingsRenameStringHelp": "Default format applied when manually renaming book folders",
|
||||||
"LabelSettingsSkipMatchingBooksWithASIN": "Skip matching books that already have an ASIN",
|
"LabelSettingsSkipMatchingBooksWithASIN": "Skip matching books that already have an ASIN",
|
||||||
"LabelSettingsSkipMatchingBooksWithISBN": "Skip matching books that already have an ISBN",
|
"LabelSettingsSkipMatchingBooksWithISBN": "Skip matching books that already have an ISBN",
|
||||||
"LabelSettingsSortingIgnorePrefixes": "Ignore prefixes when sorting",
|
"LabelSettingsSortingIgnorePrefixes": "Ignore prefixes when sorting",
|
||||||
|
|
@ -393,6 +400,8 @@
|
||||||
"LabelToolsEmbedMetadataDescription": "Embed metadata into audio files including cover image and chapters.",
|
"LabelToolsEmbedMetadataDescription": "Embed metadata into audio files including cover image and chapters.",
|
||||||
"LabelToolsMakeM4b": "Make M4B Audiobook File",
|
"LabelToolsMakeM4b": "Make M4B Audiobook File",
|
||||||
"LabelToolsMakeM4bDescription": "Generate a .M4B audiobook file with embedded metadata, cover image, and chapters.",
|
"LabelToolsMakeM4bDescription": "Generate a .M4B audiobook file with embedded metadata, cover image, and chapters.",
|
||||||
|
"LabelToolsRenamer": "Rename Item Folder",
|
||||||
|
"LabelToolsRenamerDescription": "Rename / move item folder based on a customizable pattern and the item's metadata",
|
||||||
"LabelToolsSplitM4b": "Split M4B to MP3's",
|
"LabelToolsSplitM4b": "Split M4B to MP3's",
|
||||||
"LabelToolsSplitM4bDescription": "Create MP3's from an M4B split by chapters with embedded metadata, cover image, and chapters.",
|
"LabelToolsSplitM4bDescription": "Create MP3's from an M4B split by chapters with embedded metadata, cover image, and chapters.",
|
||||||
"LabelTotalDuration": "Total Duration",
|
"LabelTotalDuration": "Total Duration",
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ const BackupManager = require('./managers/BackupManager')
|
||||||
const PlaybackSessionManager = require('./managers/PlaybackSessionManager')
|
const PlaybackSessionManager = require('./managers/PlaybackSessionManager')
|
||||||
const PodcastManager = require('./managers/PodcastManager')
|
const PodcastManager = require('./managers/PodcastManager')
|
||||||
const AudioMetadataMangaer = require('./managers/AudioMetadataManager')
|
const AudioMetadataMangaer = require('./managers/AudioMetadataManager')
|
||||||
|
const LibraryItemFolderManager = require('./managers/LibraryItemFolderManager')
|
||||||
const RssFeedManager = require('./managers/RssFeedManager')
|
const RssFeedManager = require('./managers/RssFeedManager')
|
||||||
const CronManager = require('./managers/CronManager')
|
const CronManager = require('./managers/CronManager')
|
||||||
const TaskManager = require('./managers/TaskManager')
|
const TaskManager = require('./managers/TaskManager')
|
||||||
|
|
@ -74,6 +75,7 @@ class Server {
|
||||||
this.coverManager = new CoverManager(this.db, this.cacheManager)
|
this.coverManager = new CoverManager(this.db, this.cacheManager)
|
||||||
this.podcastManager = new PodcastManager(this.db, this.watcher, this.notificationManager)
|
this.podcastManager = new PodcastManager(this.db, this.watcher, this.notificationManager)
|
||||||
this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.taskManager)
|
this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.taskManager)
|
||||||
|
this.folderRenameManager = new LibraryItemFolderManager(this.db, this.watcher)
|
||||||
this.rssFeedManager = new RssFeedManager(this.db)
|
this.rssFeedManager = new RssFeedManager(this.db)
|
||||||
this.eBookManager = new EBookManager(this.db)
|
this.eBookManager = new EBookManager(this.db)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -79,5 +79,41 @@ class ToolsController {
|
||||||
req.libraryItem = item
|
req.libraryItem = item
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
// POST: api/tools/item/:id/renameFolder
|
||||||
|
async renameBookFolder(req, res) {
|
||||||
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.error('[MiscController] renameBookFolder: Non-admin user attempting to make m4b', req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.libraryItem.isMissing || req.libraryItem.isInvalid) {
|
||||||
|
Logger.error(`[MiscController] renameBookFolder: library item not found or invalid ${req.params.id}`)
|
||||||
|
return res.status(404).send('Audiobook not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.libraryItem.mediaType !== 'book') {
|
||||||
|
Logger.error(`[MiscController] renameBookFolder: Invalid library item ${req.params.id}: not a book`)
|
||||||
|
return res.status(500).send('Invalid library item: not a book')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const options = req.query || {}
|
||||||
|
var renameStatus = await this.folderRenameManager.startRenameBookFolder(req.libraryItem, req.query.strFormat || "")
|
||||||
|
|
||||||
|
//res.sendStatus(200)
|
||||||
|
res.json(renameStatus)
|
||||||
|
}
|
||||||
|
testRenameBookFolder(req, res) {
|
||||||
|
if (req.libraryItem.isMissing || req.libraryItem.isInvalid) {
|
||||||
|
Logger.error(`[MiscController] renameBookFolder: library item not found or invalid ${req.params.id}`)
|
||||||
|
return res.status(404).send('Audiobook not found')
|
||||||
|
}
|
||||||
|
if (req.libraryItem.mediaType !== 'book') {
|
||||||
|
Logger.error(`[MiscController] renameBookFolder: Invalid library item ${req.params.id}: not a book`)
|
||||||
|
return res.status(500).send('Invalid library item: not a book')
|
||||||
|
}
|
||||||
|
var result = this.folderRenameManager.parseRenameFormat(req.libraryItem, req.query.strFormat || "", req.query.includeSampleTemplate || "")
|
||||||
|
res.json(result)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = new ToolsController()
|
module.exports = new ToolsController()
|
||||||
266
server/managers/LibraryItemFolderManager.js
Normal file
266
server/managers/LibraryItemFolderManager.js
Normal file
|
|
@ -0,0 +1,266 @@
|
||||||
|
|
||||||
|
|
||||||
|
const fs = require('../libs/fsExtra')
|
||||||
|
|
||||||
|
const Logger = require('../Logger')
|
||||||
|
const LibrarySettings = require('../objects/settings/LibrarySettings')
|
||||||
|
const fileUtils = require('../utils/fileUtils')
|
||||||
|
|
||||||
|
|
||||||
|
const { getLibraryItemFileData } = require('../utils/scandir')
|
||||||
|
|
||||||
|
const LoggerTag = `[LibraryItemFolderManager]`
|
||||||
|
|
||||||
|
|
||||||
|
class LibraryItemFolderManager {
|
||||||
|
constructor(db, watcher) {
|
||||||
|
this.db = db
|
||||||
|
this.watcher = watcher
|
||||||
|
this.directorySeperatorChar = '/'
|
||||||
|
}
|
||||||
|
parseRenameFormat(libraryItem, format, includeTemplateSample) {
|
||||||
|
const library = this.db.libraries.find(lib => lib.id === libraryItem.libraryId)
|
||||||
|
const folder = library.folders.find(f => f.id === libraryItem.folderId)
|
||||||
|
const basePath = fileUtils.filePathToPOSIX(folder.fullPath + this.directorySeperatorChar)
|
||||||
|
if (format == "") {
|
||||||
|
format = global.ServerSettings.defaultRenameString
|
||||||
|
}
|
||||||
|
const newRelativeDirectoryRaw = fileUtils.filePathToPOSIX(this.parseTemplateString(format, libraryItem))
|
||||||
|
const directorySplit = newRelativeDirectoryRaw.split(this.directorySeperatorChar)
|
||||||
|
var newRelativeDirectory = ""
|
||||||
|
for (const index in directorySplit) {
|
||||||
|
if (directorySplit[index]) { //make sure they dont put a bunch of //// in a row
|
||||||
|
const newProposedName = fileUtils.sanitizeFilename(directorySplit[index])
|
||||||
|
if (newProposedName) { //make sure there are at least some valid characters
|
||||||
|
if (newRelativeDirectory) newRelativeDirectory += this.directorySeperatorChar //add a slash if it is not the first element
|
||||||
|
newRelativeDirectory += newProposedName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var santizedLogString = ""
|
||||||
|
if (newRelativeDirectory != newRelativeDirectoryRaw) {
|
||||||
|
santizedLogString = ` and then sanitized to ${newRelativeDirectory}`
|
||||||
|
}
|
||||||
|
Logger.info(`${LoggerTag} ${format} processed to ${newRelativeDirectoryRaw}${santizedLogString}`)
|
||||||
|
|
||||||
|
const newDirectory = basePath + newRelativeDirectory
|
||||||
|
const oldRelativeDirectory = libraryItem.relPath
|
||||||
|
const oldDirectory = libraryItem.path
|
||||||
|
|
||||||
|
|
||||||
|
var templateSample = null
|
||||||
|
if (includeTemplateSample) {
|
||||||
|
templateSample = {
|
||||||
|
bookAuthor: fileUtils.sanitizeFilename(this.parseTemplateString("\$bookAuthor", libraryItem)),
|
||||||
|
bookTitle: fileUtils.sanitizeFilename(this.parseTemplateString("\$bookTitle", libraryItem)),
|
||||||
|
explicit: fileUtils.sanitizeFilename(this.parseTemplateString("\$explicit", libraryItem)),
|
||||||
|
subtitle: fileUtils.sanitizeFilename(this.parseTemplateString("\$subtitle", libraryItem)),
|
||||||
|
narrator: fileUtils.sanitizeFilename(this.parseTemplateString("\$narrator", libraryItem)),
|
||||||
|
genres: fileUtils.sanitizeFilename(this.parseTemplateString("\$genres", libraryItem)),
|
||||||
|
publishedYear: fileUtils.sanitizeFilename(this.parseTemplateString("\$publishedYear", libraryItem)),
|
||||||
|
publishedDate: fileUtils.sanitizeFilename(this.parseTemplateString("\$publishedDate", libraryItem)),
|
||||||
|
publisher: fileUtils.sanitizeFilename(this.parseTemplateString("\$publisher", libraryItem)),
|
||||||
|
isbn: fileUtils.sanitizeFilename(this.parseTemplateString("\$isbn", libraryItem)),
|
||||||
|
asin: fileUtils.sanitizeFilename(this.parseTemplateString("\$asin", libraryItem)),
|
||||||
|
language: fileUtils.sanitizeFilename(this.parseTemplateString("\$language", libraryItem)),
|
||||||
|
seriesSummary: fileUtils.sanitizeFilename(this.parseTemplateString("\$seriesSummary", libraryItem)),
|
||||||
|
seriesName: fileUtils.sanitizeFilename(this.parseTemplateString("\$seriesName", libraryItem)),
|
||||||
|
seriesNamelbr0rbr: fileUtils.sanitizeFilename(this.parseTemplateString("\$seriesName[0]", libraryItem)),
|
||||||
|
seriesSequence: fileUtils.sanitizeFilename(this.parseTemplateString("\$seriesSequence", libraryItem)),
|
||||||
|
seriesSequencelbr0rbr: fileUtils.sanitizeFilename(this.parseTemplateString("\$seriesSequence[0]", libraryItem))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
basePath: basePath,
|
||||||
|
newRelativeDirectory: newRelativeDirectory,
|
||||||
|
newDirectory: newDirectory,
|
||||||
|
oldRelativeDirectory: oldRelativeDirectory,
|
||||||
|
oldDirectory: oldDirectory,
|
||||||
|
formatUsed: format,
|
||||||
|
directorySeperatorChar: this.directorySeperatorChar,
|
||||||
|
templateSample: templateSample
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async startRenameBookFolder(libraryItem, format) {
|
||||||
|
Logger.info(`${LoggerTag} Start rename for ${libraryItem.id}`)
|
||||||
|
const newPaths = this.parseRenameFormat(libraryItem, format)
|
||||||
|
const basePath = newPaths.basePath
|
||||||
|
const newRelativeDirectory = newPaths.newRelativeDirectory
|
||||||
|
const newDirectory = newPaths.newDirectory
|
||||||
|
const oldRelativeDirectory = newPaths.oldRelativeDirectory
|
||||||
|
const oldDirectory = newPaths.oldDirectory
|
||||||
|
|
||||||
|
if (newDirectory === oldDirectory) {
|
||||||
|
Logger.info(`${LoggerTag} New folder (${newDirectory}) equal to current folder(${oldDirectory}), aborting rename`)
|
||||||
|
return {
|
||||||
|
msg: "Folder not Renamed",
|
||||||
|
success: false,
|
||||||
|
details: `New folder (${newDirectory}) equal to current folder(${oldDirectory}), aborting rename`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO - Check if target directory is a sub-directory of another book, fail if so.
|
||||||
|
|
||||||
|
Logger.info(`${LoggerTag} Renaming ${oldDirectory} to ${newDirectory}`)
|
||||||
|
this.watcher.addIgnoreDir(oldDirectory)
|
||||||
|
this.watcher.addIgnoreDir(newDirectory)
|
||||||
|
if (!await fs.pathExists(newDirectory)) {
|
||||||
|
|
||||||
|
const li = this.db.libraryItems.find(li => newDirectory.startsWith(li.path))
|
||||||
|
if (li) {
|
||||||
|
return {
|
||||||
|
msg: "Folder is sub path of another book",
|
||||||
|
success: false,
|
||||||
|
details: `The path you are trying to move to ${newDirectory} is a sub-directory of another book - ${li.media.metadata.title}(${li.id}) stored in ${li.path}`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var moveStatus = await fs.move(oldDirectory, newDirectory, { overwrite: false }).catch((err) => {
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
if (moveStatus != null) {
|
||||||
|
Logger.error(`${LoggerTag} Failed folder rename ${moveStatus}`)
|
||||||
|
return {
|
||||||
|
msg: "Failed to move folder",
|
||||||
|
success: false,
|
||||||
|
details: `${moveStatus}`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//move Success, check subfolders if empty and then remove.
|
||||||
|
var currentCheckingRelDirectory = oldRelativeDirectory
|
||||||
|
if (currentCheckingRelDirectory.endsWith(this.directorySeperatorChar)) currentCheckingRelDirectory = currentCheckingRelDirectory.substring(0, currentCheckingRelDirectory.length - 1)
|
||||||
|
const dirParts = currentCheckingRelDirectory.split(this.directorySeperatorChar)
|
||||||
|
for (var i = dirParts.length - 1; i >= 0; i--) {
|
||||||
|
currentCheckingRelDirectory = currentCheckingRelDirectory.substring(0, currentCheckingRelDirectory.length - dirParts[i].length - 1)
|
||||||
|
var currentCheckingDirectory = basePath + currentCheckingRelDirectory
|
||||||
|
if (currentCheckingRelDirectory) {
|
||||||
|
var fileItems = await fileUtils.recurseFiles(currentCheckingDirectory) //this will find files nested in subdirectories as well.
|
||||||
|
if (fileItems && fileItems.length == 0) {
|
||||||
|
Logger.debug(`${LoggerTag} no files in sub folders, safe to delete '${currentCheckingRelDirectory}'`)
|
||||||
|
fileUtils.removeFile(currentCheckingDirectory)
|
||||||
|
} else {
|
||||||
|
break; //if we do find a file, then no need to check parent dirs they will all have files in subfolders.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Logger.error(`${LoggerTag} target folder ${newDirectory} already exists. Cancelling move`)
|
||||||
|
return { msg: "failed to move folder", success: false, details: `target folder ${newDirectory} already exists. Cancelling move` }
|
||||||
|
}
|
||||||
|
Logger.debug(`${LoggerTag} dynamically updating all path variables in Library Item from ${oldRelativeDirectory} to ${newRelativeDirectory}`)
|
||||||
|
this.recursiveUpdatePath(libraryItem, libraryItem.toJSON(), oldRelativeDirectory, newRelativeDirectory, basePath)
|
||||||
|
await this.db.updateLibraryItem(libraryItem)
|
||||||
|
const libItem = this.db.getLibraryItem(libraryItem.id)
|
||||||
|
|
||||||
|
|
||||||
|
//you have to put the removal of the ignore on a delay for long enough that the Watcher process
|
||||||
|
//will have checkd on the directory, recognized it should be ignored, before removing the ignore process
|
||||||
|
setTimeout(() => {
|
||||||
|
this.watcher.removeIgnoreDir(oldDirectory)
|
||||||
|
this.watcher.removeIgnoreDir(newDirectory)
|
||||||
|
}, this.watcher.pendingDelay)
|
||||||
|
|
||||||
|
|
||||||
|
Logger.info(`${LoggerTag} rename successful.`)
|
||||||
|
return {
|
||||||
|
msg: "Rename Success",
|
||||||
|
success: true,
|
||||||
|
details: newPaths
|
||||||
|
//, testObj: testObj
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
recursiveUpdatePath(item, json, oldRelPath, newRelPath, basePath) {
|
||||||
|
for (const key in json) {
|
||||||
|
if (key.toLowerCase().includes("path")) {
|
||||||
|
if (item[key] == basePath + oldRelPath) {
|
||||||
|
item[key] = basePath + newRelPath
|
||||||
|
}
|
||||||
|
else if (item[key] == oldRelPath) {
|
||||||
|
item[key] = newRelPath
|
||||||
|
}
|
||||||
|
else if (item[key].startsWith(oldRelPath + this.directorySeperatorChar)) {
|
||||||
|
item[key] = item[key].replace(oldRelPath + this.directorySeperatorChar, newRelPath + this.directorySeperatorChar)
|
||||||
|
}
|
||||||
|
else if (item[key].startsWith(basePath + oldRelPath + this.directorySeperatorChar)) {
|
||||||
|
item[key] = item[key].replace(basePath + oldRelPath + this.directorySeperatorChar, basePath + newRelPath + this.directorySeperatorChar)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (typeof json[key] === "object") {
|
||||||
|
this.recursiveUpdatePath(item[key], json[key], oldRelPath, newRelPath, basePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitizeText(str) {
|
||||||
|
if (str) {
|
||||||
|
return fileUtils.sanitizeFilename(str)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parseTemplateString(userTemplate, libraryItem) {
|
||||||
|
var template = userTemplate ? userTemplate : global.ServerSettings.defaultRenameString
|
||||||
|
|
||||||
|
var bookTitle = this.sanitizeText(libraryItem.media.metadata.title)
|
||||||
|
var bookAuthor = this.sanitizeText(libraryItem.media.metadata.authorName)
|
||||||
|
var subtitle = this.sanitizeText(libraryItem.media.metadata.subtitle)
|
||||||
|
//series
|
||||||
|
var narrator = this.sanitizeText(libraryItem.media.metadata.narratorName)
|
||||||
|
var genres = this.sanitizeText(libraryItem.media.metadata.genresDisplay)
|
||||||
|
var publishedYear = this.sanitizeText(libraryItem.media.metadata.publishedYear)
|
||||||
|
var publishedDate = this.sanitizeText(libraryItem.media.metadata.publishedDate)
|
||||||
|
var publisher = this.sanitizeText(libraryItem.media.metadata.publisher)
|
||||||
|
var isbn = this.sanitizeText(libraryItem.media.metadata.isbn)
|
||||||
|
var asin = this.sanitizeText(libraryItem.media.metadata.asin)
|
||||||
|
var language = this.sanitizeText(libraryItem.media.metadata.language)
|
||||||
|
var explicit = this.sanitizeText(libraryItem.media.metadata.explicitDisplay)
|
||||||
|
var seriesSummary = this.sanitizeText(libraryItem.media.metadata.seriesName)
|
||||||
|
var output = template
|
||||||
|
while (output.includes("\$bookTitle")) output = output.replace("\$bookTitle", bookTitle)
|
||||||
|
while (output.includes("\$bookAuthor")) output = output.replace("\$bookAuthor", bookAuthor)
|
||||||
|
while (output.includes("\$subtitle")) output = output.replace("\$subtitle", subtitle)
|
||||||
|
while (output.includes("\$narrator")) output = output.replace("\$narrator", narrator)
|
||||||
|
while (output.includes("\$genres")) output = output.replace("\$genres", genres)
|
||||||
|
while (output.includes("\$publishedYear")) output = output.replace("\$publishedYear", publishedYear)
|
||||||
|
while (output.includes("\$publishedDate")) output = output.replace("\$publishedDate", publishedDate)
|
||||||
|
while (output.includes("\$publisher")) output = output.replace("\$publisher", publisher)
|
||||||
|
while (output.includes("\$isbn")) output = output.replace("\$isbn", isbn)
|
||||||
|
while (output.includes("\$asin")) output = output.replace("\$asin", asin)
|
||||||
|
while (output.includes("\$language")) output = output.replace("\$language", language)
|
||||||
|
while (output.includes("\$explicit")) output = output.replace("\$explicit", explicit)
|
||||||
|
while (output.includes("\$seriesSummary")) output = output.replace("\$seriesSummary", seriesSummary)
|
||||||
|
|
||||||
|
var seriesNameRegex = /\$seriesName\[\d+\]/
|
||||||
|
while (output.match(seriesNameRegex) != null) {
|
||||||
|
var subString = output.match(seriesNameRegex)[0]
|
||||||
|
if (subString != null) {
|
||||||
|
var digitStr = subString.replace("\$seriesName[", "").replace("]", "")
|
||||||
|
var digit = parseInt(digitStr)
|
||||||
|
var replacementText = this.sanitizeText(libraryItem.media.metadata.getSeriesNameDisplay(digit))
|
||||||
|
output = output.replace(subString, replacementText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (output.includes("\$seriesName")) output = output.replace("\$seriesName", this.sanitizeText(libraryItem.media.metadata.getSeriesNameDisplay(0)))
|
||||||
|
|
||||||
|
var seriesSeqRegex = /\$seriesSequence\[\d+\]/
|
||||||
|
while (output.match(seriesSeqRegex) != null) {
|
||||||
|
var subString = output.match(seriesSeqRegex)[0]
|
||||||
|
if (subString != null) {
|
||||||
|
var digitStr = subString.replace("\$seriesSequence[", "").replace("]", "")
|
||||||
|
var digit = parseInt(digitStr)
|
||||||
|
var replacementText = this.sanitizeText(libraryItem.media.metadata.getSeriesSequenceDisplay(digit))
|
||||||
|
output = output.replace(subString, replacementText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (output.includes("\$seriesSequence")) output = output.replace("\$seriesSequence", this.sanitizeText(libraryItem.media.metadata.getSeriesSequenceDisplay(0)))
|
||||||
|
|
||||||
|
return output
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
module.exports = LibraryItemFolderManager
|
||||||
|
|
@ -158,7 +158,13 @@ class BookMetadata {
|
||||||
if (!this.authorName) return this.title
|
if (!this.authorName) return this.title
|
||||||
return this.title + '&' + this.authorName
|
return this.title + '&' + this.authorName
|
||||||
}
|
}
|
||||||
|
get genresDisplay() {
|
||||||
|
if (!this.genres.length) return ''
|
||||||
|
return this.genres.join(', ')
|
||||||
|
}
|
||||||
|
get explicitDisplay() {
|
||||||
|
return this.explicit ? 'Explicit' : 'Not Explicit'
|
||||||
|
}
|
||||||
hasAuthor(id) {
|
hasAuthor(id) {
|
||||||
return !!this.authors.find(au => au.id == id)
|
return !!this.authors.find(au => au.id == id)
|
||||||
}
|
}
|
||||||
|
|
@ -184,7 +190,24 @@ class BookMetadata {
|
||||||
if (!series.sequence) return series.name
|
if (!series.sequence) return series.name
|
||||||
return `${series.name} #${series.sequence}`
|
return `${series.name} #${series.sequence}`
|
||||||
}
|
}
|
||||||
|
getSeriesNameDisplay(index) {
|
||||||
|
if (this.validateSeries(index)) {
|
||||||
|
return this.series[index].name
|
||||||
|
} else { return null }
|
||||||
|
}
|
||||||
|
getSeriesSequenceDisplay(index) {
|
||||||
|
if (this.validateSeries(index)) {
|
||||||
|
return this.series[index].sequence
|
||||||
|
} else { return null }
|
||||||
|
}
|
||||||
|
validateSeries(index) {
|
||||||
|
if (this.series.length && index < this.series.length && index >= 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
update(payload) {
|
update(payload) {
|
||||||
var json = this.toJSON()
|
var json = this.toJSON()
|
||||||
var hasUpdates = false
|
var hasUpdates = false
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ class ServerSettings {
|
||||||
// Metadata - choose to store inside users library item folder
|
// Metadata - choose to store inside users library item folder
|
||||||
this.storeCoverWithItem = false
|
this.storeCoverWithItem = false
|
||||||
this.storeMetadataWithItem = false
|
this.storeMetadataWithItem = false
|
||||||
|
this.defaultRenameString = "$bookAuthor/$bookTitle"
|
||||||
|
|
||||||
// Security/Rate limits
|
// Security/Rate limits
|
||||||
this.rateLimitLoginRequests = 10
|
this.rateLimitLoginRequests = 10
|
||||||
|
|
@ -76,6 +77,7 @@ class ServerSettings {
|
||||||
|
|
||||||
this.storeCoverWithItem = !!settings.storeCoverWithItem
|
this.storeCoverWithItem = !!settings.storeCoverWithItem
|
||||||
this.storeMetadataWithItem = !!settings.storeMetadataWithItem
|
this.storeMetadataWithItem = !!settings.storeMetadataWithItem
|
||||||
|
this.defaultRenameString = settings.defaultRenameString || "$bookAuthor/$bookTitle"
|
||||||
|
|
||||||
this.rateLimitLoginRequests = !isNaN(settings.rateLimitLoginRequests) ? Number(settings.rateLimitLoginRequests) : 10
|
this.rateLimitLoginRequests = !isNaN(settings.rateLimitLoginRequests) ? Number(settings.rateLimitLoginRequests) : 10
|
||||||
this.rateLimitLoginWindow = !isNaN(settings.rateLimitLoginWindow) ? Number(settings.rateLimitLoginWindow) : 10 * 60 * 1000 // 10 Minutes
|
this.rateLimitLoginWindow = !isNaN(settings.rateLimitLoginWindow) ? Number(settings.rateLimitLoginWindow) : 10 * 60 * 1000 // 10 Minutes
|
||||||
|
|
@ -131,6 +133,7 @@ class ServerSettings {
|
||||||
scannerUseTone: this.scannerUseTone,
|
scannerUseTone: this.scannerUseTone,
|
||||||
storeCoverWithItem: this.storeCoverWithItem,
|
storeCoverWithItem: this.storeCoverWithItem,
|
||||||
storeMetadataWithItem: this.storeMetadataWithItem,
|
storeMetadataWithItem: this.storeMetadataWithItem,
|
||||||
|
defaultRenameString: this.defaultRenameString,
|
||||||
rateLimitLoginRequests: this.rateLimitLoginRequests,
|
rateLimitLoginRequests: this.rateLimitLoginRequests,
|
||||||
rateLimitLoginWindow: this.rateLimitLoginWindow,
|
rateLimitLoginWindow: this.rateLimitLoginWindow,
|
||||||
backupSchedule: this.backupSchedule,
|
backupSchedule: this.backupSchedule,
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ class ApiRouter {
|
||||||
this.cacheManager = Server.cacheManager
|
this.cacheManager = Server.cacheManager
|
||||||
this.podcastManager = Server.podcastManager
|
this.podcastManager = Server.podcastManager
|
||||||
this.audioMetadataManager = Server.audioMetadataManager
|
this.audioMetadataManager = Server.audioMetadataManager
|
||||||
|
this.folderRenameManager = Server.folderRenameManager
|
||||||
this.rssFeedManager = Server.rssFeedManager
|
this.rssFeedManager = Server.rssFeedManager
|
||||||
this.cronManager = Server.cronManager
|
this.cronManager = Server.cronManager
|
||||||
this.notificationManager = Server.notificationManager
|
this.notificationManager = Server.notificationManager
|
||||||
|
|
@ -272,6 +273,8 @@ class ApiRouter {
|
||||||
this.router.post('/tools/item/:id/encode-m4b', ToolsController.itemMiddleware.bind(this), ToolsController.encodeM4b.bind(this))
|
this.router.post('/tools/item/:id/encode-m4b', ToolsController.itemMiddleware.bind(this), ToolsController.encodeM4b.bind(this))
|
||||||
this.router.delete('/tools/item/:id/encode-m4b', ToolsController.itemMiddleware.bind(this), ToolsController.cancelM4bEncode.bind(this))
|
this.router.delete('/tools/item/:id/encode-m4b', ToolsController.itemMiddleware.bind(this), ToolsController.cancelM4bEncode.bind(this))
|
||||||
this.router.post('/tools/item/:id/embed-metadata', ToolsController.itemMiddleware.bind(this), ToolsController.embedAudioFileMetadata.bind(this))
|
this.router.post('/tools/item/:id/embed-metadata', ToolsController.itemMiddleware.bind(this), ToolsController.embedAudioFileMetadata.bind(this))
|
||||||
|
this.router.post('/tools/item/:id/renameFolder', ToolsController.itemMiddleware.bind(this), ToolsController.renameBookFolder.bind(this))
|
||||||
|
this.router.get('/tools/item/:id/renameFolder', ToolsController.itemMiddleware.bind(this), ToolsController.testRenameBookFolder.bind(this))
|
||||||
|
|
||||||
//
|
//
|
||||||
// RSS Feed Routes (Admin and up)
|
// RSS Feed Routes (Admin and up)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue