mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-07 01:41:35 +00:00
created a directory reorganizer
This commit is contained in:
parent
8b89b27654
commit
b4fded0d40
47 changed files with 809 additions and 80 deletions
|
|
@ -180,6 +180,13 @@ export default {
|
|||
action: 'rescan'
|
||||
})
|
||||
|
||||
if (this.userCanUpdate) {
|
||||
options.push({
|
||||
text: this.$strings.ButtonReorganizeFiles,
|
||||
action: 'reorganize'
|
||||
})
|
||||
}
|
||||
|
||||
// The limit of 50 is introduced because of the URL length. Each id has 36 chars, so 36 * 40 = 1440
|
||||
// + 40 , separators = 1480 chars + base path 280 chars = 1760 chars. This keeps the URL under 2000 chars even with longer domains
|
||||
if (this.selectedMediaItems.length <= 40) {
|
||||
|
|
@ -225,6 +232,8 @@ export default {
|
|||
this.batchAutoMatchClick()
|
||||
} else if (action === 'rescan') {
|
||||
this.batchRescan()
|
||||
} else if (action === 'reorganize') {
|
||||
this.batchReorganizeClick()
|
||||
} else if (action === 'download') {
|
||||
this.batchDownload()
|
||||
}
|
||||
|
|
@ -368,6 +377,9 @@ export default {
|
|||
batchEditClick() {
|
||||
this.$router.push('/batch')
|
||||
},
|
||||
batchReorganizeClick() {
|
||||
this.$store.commit('globals/setShowBatchReorganizeModal', true)
|
||||
},
|
||||
batchAddToCollectionClick() {
|
||||
this.$store.commit('globals/setShowBatchCollectionsModal', true)
|
||||
},
|
||||
|
|
|
|||
156
client/components/modals/BatchReorganizeModal.vue
Normal file
156
client/components/modals/BatchReorganizeModal.vue
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
<template>
|
||||
<modals-modal v-model="show" name="batchReorganize" :processing="processing" :width="500" :height="'unset'">
|
||||
<template #outer>
|
||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||
<div v-if="show" class="w-full h-full py-4">
|
||||
<div class="w-full overflow-y-auto overflow-x-hidden max-h-96">
|
||||
<p class="text-base px-8 py-4">{{ $strings.LabelToolsReorganizeFilesDescription }}</p>
|
||||
|
||||
<div v-if="organizationPreview.length" class="px-8 py-2">
|
||||
<p class="text-sm font-semibold mb-2">{{ $strings.LabelPreview }}</p>
|
||||
<div class="bg-bg-secondary rounded p-3 max-h-48 overflow-y-auto">
|
||||
<div v-for="(preview, index) in organizationPreview" :key="index" class="text-xs mb-2">
|
||||
<p class="text-gray-400">{{ preview.title }}</p>
|
||||
<p class="text-green-400">{{ preview.newPath }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-4 text-white/80 border-t border-white/5">
|
||||
<div class="flex items-center px-4">
|
||||
<ui-btn type="button" @click="show = false">{{ $strings.ButtonCancel }}</ui-btn>
|
||||
<div class="grow" />
|
||||
<ui-btn color="bg-success" :disabled="!selectedItemIds.length" @click="doBatchReorganize">
|
||||
{{ $strings.ButtonReorganizeFiles }}
|
||||
</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
processing: false,
|
||||
loadedItems: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
show: {
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.$store.state.globals.showBatchReorganizeModal
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit('globals/setShowBatchReorganizeModal', val)
|
||||
}
|
||||
},
|
||||
title() {
|
||||
return this.$getString('MessageItemsSelected', [this.selectedItemIds.length])
|
||||
},
|
||||
selectedItemIds() {
|
||||
return (this.$store.state.globals.selectedMediaItems || []).map((i) => i.id)
|
||||
},
|
||||
selectedItems() {
|
||||
return this.loadedItems.length > 0 ? this.loadedItems : (this.$store.state.globals.selectedMediaItems || [])
|
||||
},
|
||||
organizationPreview() {
|
||||
// Show a preview of where files will be moved
|
||||
return this.selectedItems.slice(0, 5).map((item) => {
|
||||
let newPath = ''
|
||||
if (item.mediaType === 'podcast') {
|
||||
newPath = item.media?.metadata?.title || 'Unknown Podcast'
|
||||
} else {
|
||||
const parts = []
|
||||
if (item.media?.metadata?.authorName) parts.push(item.media.metadata.authorName)
|
||||
if (item.media?.metadata?.seriesName) parts.push(item.media.metadata.seriesName)
|
||||
if (item.media?.metadata?.title) parts.push(item.media.metadata.title)
|
||||
newPath = parts.join(' / ') || 'Unknown Item'
|
||||
}
|
||||
return {
|
||||
title: item.media?.metadata?.title || item.title,
|
||||
newPath
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
// Listen for batch reorganize completion
|
||||
this.$socket.on('batch_reorganize_complete', this.handleBatchComplete)
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.$socket.off('batch_reorganize_complete', this.handleBatchComplete)
|
||||
},
|
||||
methods: {
|
||||
async init() {
|
||||
// Load full item data for preview
|
||||
this.loadedItems = []
|
||||
try {
|
||||
const libraryItems = await this.$axios.$post(`/api/items/batch/get`, {
|
||||
libraryItemIds: this.selectedItemIds
|
||||
})
|
||||
if (libraryItems && libraryItems.libraryItems) {
|
||||
this.loadedItems = libraryItems.libraryItems
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load items for preview', error)
|
||||
}
|
||||
},
|
||||
doBatchReorganize() {
|
||||
if (!this.selectedItemIds.length) return
|
||||
if (this.processing) return
|
||||
|
||||
this.processing = true
|
||||
this.$store.commit('setProcessingBatch', true)
|
||||
this.$axios
|
||||
.$post(`/api/items/batch/reorganize-files`, {
|
||||
libraryItemIds: this.selectedItemIds
|
||||
})
|
||||
.then(() => {
|
||||
this.$toast.info(this.$getString('MessageBatchReorganizeStarted', [this.selectedItemIds.length]))
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toast.error('Failed to start batch reorganize')
|
||||
console.error('Failed to batch reorganize', error)
|
||||
this.processing = false
|
||||
this.$store.commit('setProcessingBatch', false)
|
||||
})
|
||||
.finally(() => {
|
||||
this.show = false
|
||||
})
|
||||
},
|
||||
handleBatchComplete(result) {
|
||||
this.processing = false
|
||||
this.$store.commit('setProcessingBatch', false)
|
||||
|
||||
if (result.successCount > 0 && result.errorCount === 0) {
|
||||
this.$toast.success(this.$getString('MessageBatchReorganizeSuccess', [result.successCount]))
|
||||
} else if (result.successCount > 0 && result.errorCount > 0) {
|
||||
this.$toast.warning(this.$getString('MessageBatchReorganizePartial', [result.successCount, result.errorCount]))
|
||||
} else {
|
||||
this.$toast.error('Failed to reorganize files')
|
||||
}
|
||||
|
||||
// Clear selection
|
||||
this.$store.commit('globals/resetSelectedMediaItems')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -49,6 +49,20 @@
|
|||
</div>
|
||||
|
||||
<p v-if="!mediaTracks.length" class="text-lg text-center my-8">{{ $strings.MessageNoAudioTracks }}</p>
|
||||
|
||||
<!-- Reorganize Files -->
|
||||
<div class="w-full border border-black-200 p-4 my-8">
|
||||
<div class="flex items-center">
|
||||
<div>
|
||||
<p class="text-lg">{{ $strings.LabelToolsReorganizeFiles }}</p>
|
||||
<p class="max-w-sm text-sm pt-2 text-gray-300">{{ $strings.LabelToolsReorganizeFilesDescription }}</p>
|
||||
</div>
|
||||
<div class="grow" />
|
||||
<div>
|
||||
<ui-btn :loading="isReorganizing" color="bg-primary" @click.stop="reorganizeFiles">{{ $strings.ButtonReorganizeFiles }}</ui-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -62,7 +76,9 @@ export default {
|
|||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
return {
|
||||
isReorganizing: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
libraryItemId() {
|
||||
|
|
@ -123,6 +139,38 @@ export default {
|
|||
type: 'yesNo'
|
||||
}
|
||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||
},
|
||||
async reorganizeFiles() {
|
||||
const title = this.libraryItem?.media?.metadata?.title || this.libraryItem?.media?.title || this.libraryItem?.title || 'this item'
|
||||
const payload = {
|
||||
message: `Are you sure you want to reorganize the files for "${title}"? Files will be moved to a directory structure based on metadata.`,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.performReorganize()
|
||||
}
|
||||
},
|
||||
type: 'yesNo'
|
||||
}
|
||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||
},
|
||||
async performReorganize() {
|
||||
this.isReorganizing = true
|
||||
try {
|
||||
const response = await this.$axios.$post(`/api/items/${this.libraryItemId}/reorganize-files`)
|
||||
console.log('Reorganize response:', response)
|
||||
this.$toast.success('Files reorganized successfully')
|
||||
} catch (error) {
|
||||
console.error('Reorganize error:', error)
|
||||
let errorMsg = 'Failed to reorganize files'
|
||||
if (error.response?.data) {
|
||||
errorMsg = error.response.data
|
||||
} else if (error.message) {
|
||||
errorMsg = error.message
|
||||
}
|
||||
this.$toast.error(errorMsg)
|
||||
} finally {
|
||||
this.isReorganizing = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
<modals-podcast-view-episode />
|
||||
<modals-authors-edit-modal />
|
||||
<modals-batch-quick-match-model />
|
||||
<modals-batch-reorganize-modal />
|
||||
<modals-rssfeed-open-close-modal />
|
||||
<modals-raw-cover-preview-modal />
|
||||
<modals-share-modal />
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export const state = () => ({
|
|||
isCasting: false, // Actively casting
|
||||
isChromecastInitialized: false, // Script loadeds
|
||||
showBatchQuickMatchModal: false,
|
||||
showBatchReorganizeModal: false,
|
||||
dateFormats: [
|
||||
{
|
||||
text: 'MM/DD/YYYY',
|
||||
|
|
@ -204,6 +205,9 @@ export const mutations = {
|
|||
setShowBatchQuickMatchModal(state, val) {
|
||||
state.showBatchQuickMatchModal = val
|
||||
},
|
||||
setShowBatchReorganizeModal(state, val) {
|
||||
state.showBatchReorganizeModal = val
|
||||
},
|
||||
resetSelectedMediaItems(state) {
|
||||
state.selectedMediaItems = []
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1114,5 +1114,12 @@
|
|||
"ToastUserPasswordChangeSuccess": "تم تغيير كلمة المرور بنجاح",
|
||||
"ToastUserPasswordMismatch": "كلمات المرور غير متطابقة",
|
||||
"ToastUserPasswordMustChange": "يجب ألا تطابق كلمة المرور الجديدة كلمة المرور القديمة",
|
||||
"ToastUserRootRequireName": "يجب إدخال اسم مستخدم الجذر"
|
||||
}
|
||||
"ToastUserRootRequireName": "يجب إدخال اسم مستخدم الجذر",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1161,5 +1161,12 @@
|
|||
"TooltipLockChapter": "Заблакіраваць раздзел (Shift+націсканне для дыяпазону)",
|
||||
"TooltipSubtractOneSecond": "Адняць 1 секунду",
|
||||
"TooltipUnlockAllChapters": "Разблакіраваць усе раздзелы",
|
||||
"TooltipUnlockChapter": "Разблакіраваць раздзел (Shift+націсканне для выбару дыяпазону)"
|
||||
}
|
||||
"TooltipUnlockChapter": "Разблакіраваць раздзел (Shift+націсканне для выбару дыяпазону)",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1026,5 +1026,12 @@
|
|||
"ToastSortingPrefixesEmptyError": "Трябва да има поне 1 префикс за сортиране",
|
||||
"ToastSortingPrefixesUpdateSuccess": "Префиксите за сортиране са актуализирани ({0} елемента)",
|
||||
"ToastUserDeleteFailed": "Неуспешно изтриване на потребител",
|
||||
"ToastUserDeleteSuccess": "Потребителят е изтрит"
|
||||
}
|
||||
"ToastUserDeleteSuccess": "Потребителят е изтрит",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1080,5 +1080,12 @@
|
|||
"ToastUserPasswordChangeSuccess": "পাসওয়ার্ড সফলভাবে পরিবর্তন করা হয়েছে",
|
||||
"ToastUserPasswordMismatch": "পাসওয়ার্ড মিলছে না",
|
||||
"ToastUserPasswordMustChange": "নতুন পাসওয়ার্ড পুরানো পাসওয়ার্ডের সাথে মিলতে পারবে না",
|
||||
"ToastUserRootRequireName": "একটি রুট ব্যবহারকারীর নাম লিখতে হবে"
|
||||
}
|
||||
"ToastUserRootRequireName": "একটি রুট ব্যবহারকারীর নাম লিখতে হবে",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1073,5 +1073,12 @@
|
|||
"ToastUserPasswordChangeSuccess": "Contrasenya canviada correctament",
|
||||
"ToastUserPasswordMismatch": "Les contrasenyes no coincideixen",
|
||||
"ToastUserPasswordMustChange": "La nova contrasenya no pot ser igual a l'anterior",
|
||||
"ToastUserRootRequireName": "Cal introduir un nom d'usuari root"
|
||||
}
|
||||
"ToastUserRootRequireName": "Cal introduir un nom d'usuari root",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1161,5 +1161,12 @@
|
|||
"TooltipLockChapter": "Uzamknout kapitolu (Shift+klik pro rozsah)",
|
||||
"TooltipSubtractOneSecond": "Odečíst 1 sekundu",
|
||||
"TooltipUnlockAllChapters": "Odemknout všechny kapitoly",
|
||||
"TooltipUnlockChapter": "Odemknout kapitolu (Shift+klik pro rozsah)"
|
||||
}
|
||||
"TooltipUnlockChapter": "Odemknout kapitolu (Shift+klik pro rozsah)",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1161,5 +1161,12 @@
|
|||
"TooltipLockChapter": "Lås kapitel (Shift+click for at markere flere)",
|
||||
"TooltipSubtractOneSecond": "Fratag 1 sekund",
|
||||
"TooltipUnlockAllChapters": "Lås alle kapitaler op",
|
||||
"TooltipUnlockChapter": "Lås kapitel op (Shift+click for at markere flere)"
|
||||
}
|
||||
"TooltipUnlockChapter": "Lås kapitel op (Shift+click for at markere flere)",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1161,5 +1161,12 @@
|
|||
"TooltipLockChapter": "Kapitel sperren (Shift+Klick für mehrere)",
|
||||
"TooltipSubtractOneSecond": "1 Sekunde abziehen",
|
||||
"TooltipUnlockAllChapters": "Alle Kapitel freigeben",
|
||||
"TooltipUnlockChapter": "Kapitel freigeben (Shift+Klick für mehrere)"
|
||||
}
|
||||
"TooltipUnlockChapter": "Kapitel freigeben (Shift+Klick für mehrere)",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -320,5 +320,12 @@
|
|||
"MessageLoading": "Φόρτωση...",
|
||||
"MessageMarkAsFinished": "Σήμανση ως Ολοκληρωμένο",
|
||||
"MessageNoItemsFound": "Δεν βρέθηκαν αντικείμενα",
|
||||
"MessageNoUserPlaylists": "Δεν έχετε λίστες αναπαραγωγής"
|
||||
}
|
||||
"MessageNoUserPlaylists": "Δεν έχετε λίστες αναπαραγωγής",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1161,5 +1161,12 @@
|
|||
"TooltipLockChapter": "Lock chapter (Shift+click for range)",
|
||||
"TooltipSubtractOneSecond": "Subtract 1 second",
|
||||
"TooltipUnlockAllChapters": "Unlock all chapters",
|
||||
"TooltipUnlockChapter": "Unlock chapter (Shift+click for range)"
|
||||
}
|
||||
"TooltipUnlockChapter": "Unlock chapter (Shift+click for range)",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1161,5 +1161,12 @@
|
|||
"TooltipLockChapter": "Bloquear capítulo (Mayús+clic para rango)",
|
||||
"TooltipSubtractOneSecond": "Restar 1 segundo",
|
||||
"TooltipUnlockAllChapters": "Desbloquear todos los capítulos",
|
||||
"TooltipUnlockChapter": "Desbloquear capítulo (Mayús+clic para rango)"
|
||||
}
|
||||
"TooltipUnlockChapter": "Desbloquear capítulo (Mayús+clic para rango)",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -749,5 +749,12 @@
|
|||
"ToastSocketDisconnected": "Pesa ühendus katkenud",
|
||||
"ToastSocketFailedToConnect": "Pesa ühendamine ebaõnnestus",
|
||||
"ToastUserDeleteFailed": "Kasutaja kustutamine ebaõnnestus",
|
||||
"ToastUserDeleteSuccess": "Kasutaja kustutatud"
|
||||
}
|
||||
"ToastUserDeleteSuccess": "Kasutaja kustutatud",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1 +1,9 @@
|
|||
{}
|
||||
{
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -24,5 +24,12 @@
|
|||
"ButtonSave": "ذخیره",
|
||||
"ButtonSearch": "جستجو",
|
||||
"ButtonSeries": "مجموعه",
|
||||
"ButtonSubmit": "ثبت"
|
||||
}
|
||||
"ButtonSubmit": "ثبت",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1161,5 +1161,12 @@
|
|||
"TooltipLockChapter": "Lukitse luku (Shift+napauta valitaksesi alueen)",
|
||||
"TooltipSubtractOneSecond": "Vähennä 1 sekunti",
|
||||
"TooltipUnlockAllChapters": "Avaa kaikki luvut",
|
||||
"TooltipUnlockChapter": "Avaa luku (Shift+napauta valitaksesi alueen)"
|
||||
}
|
||||
"TooltipUnlockChapter": "Avaa luku (Shift+napauta valitaksesi alueen)",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1161,5 +1161,12 @@
|
|||
"TooltipLockChapter": "Verrouiller le chapitre (Maj+clic pour plage)",
|
||||
"TooltipSubtractOneSecond": "Soustraire 1 seconde",
|
||||
"TooltipUnlockAllChapters": "Déverrouiller tous les chapitres",
|
||||
"TooltipUnlockChapter": "Déverrouiller le chapitre (Maj+clic pour plage)"
|
||||
}
|
||||
"TooltipUnlockChapter": "Déverrouiller le chapitre (Maj+clic pour plage)",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -194,5 +194,12 @@
|
|||
"LabelPodcastSearchRegion": "પોડકાસ્ટ શોધ પ્રદેશ",
|
||||
"LabelSettingsPreferMatchedMetadataHelp": "Matched data will overide item details when using Quick Match. By default Quick Match will only fill in missing details.",
|
||||
"MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"",
|
||||
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device"
|
||||
}
|
||||
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -946,5 +946,12 @@
|
|||
"ToastSocketDisconnected": "קצה תקשורת נותק",
|
||||
"ToastSocketFailedToConnect": "התחברות קצה התקשורת נכשלה",
|
||||
"ToastUserDeleteFailed": "מחיקת המשתמש נכשלה",
|
||||
"ToastUserDeleteSuccess": "המשתמש נמחק בהצלחה"
|
||||
}
|
||||
"ToastUserDeleteSuccess": "המשתמש נמחק בהצלחה",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -99,5 +99,12 @@
|
|||
"LabelSettingsPreferMatchedMetadataHelp": "Matched data will overide item details when using Quick Match. By default Quick Match will only fill in missing details.",
|
||||
"MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"",
|
||||
"NoteChangeRootPassword": "रूट user is the only user that can have an empty password",
|
||||
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device"
|
||||
}
|
||||
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1161,5 +1161,12 @@
|
|||
"TooltipLockChapter": "Zaključaj poglavlje (Shift + klik za raspon)",
|
||||
"TooltipSubtractOneSecond": "Oduzmi 1 sekundu",
|
||||
"TooltipUnlockAllChapters": "Otključaj sva poglavlja",
|
||||
"TooltipUnlockChapter": "Otključaj poglavlje (Shift+klik za raspon)"
|
||||
}
|
||||
"TooltipUnlockChapter": "Otključaj poglavlje (Shift+klik za raspon)",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1161,5 +1161,12 @@
|
|||
"TooltipLockChapter": "Fejezet zárolása (Shift+kattintás a tartományhoz)",
|
||||
"TooltipSubtractOneSecond": "1 másodperc levonása",
|
||||
"TooltipUnlockAllChapters": "Az összes fejezet feloldása",
|
||||
"TooltipUnlockChapter": "Fejezet feloldása (Shift+kattintás a tartományhoz)"
|
||||
}
|
||||
"TooltipUnlockChapter": "Fejezet feloldása (Shift+kattintás a tartományhoz)",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1 +1,9 @@
|
|||
{}
|
||||
{
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1161,5 +1161,12 @@
|
|||
"TooltipLockChapter": "Bloccare capitolo (Shift+click per intervallo)",
|
||||
"TooltipSubtractOneSecond": "Sottrarre 1 secondo",
|
||||
"TooltipUnlockAllChapters": "Sbloccare tutti i capitoli",
|
||||
"TooltipUnlockChapter": "Sbloccare capitolo (Shift+click per intervallo)"
|
||||
}
|
||||
"TooltipUnlockChapter": "Sbloccare capitolo (Shift+click per intervallo)",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -267,5 +267,12 @@
|
|||
"LabelSettingsStoreCoversWithItem": "表紙を項目と一緒に保存する",
|
||||
"LabelSettingsStoreCoversWithItemHelp": "デフォルトでは表紙は /metadata/items に保存されますが、この設定をオンにするとライブラリーの項目のフォルダーに保存されます。\"cover\" という名前のファイル一つのみが保持されます",
|
||||
"LabelSettingsStoreMetadataWithItem": "メタデータを項目と一緒に保存する",
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "デフォルトではメタデータは/metadata/itemsに保存されますが、この設定をオンにするとライブラリーの項目のフォルダーに保存されます"
|
||||
}
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "デフォルトではメタデータは/metadata/itemsに保存されますが、この設定をオンにするとライブラリーの項目のフォルダーに保存されます",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1161,5 +1161,12 @@
|
|||
"TooltipLockChapter": "장 잠금(범위를 위해 Shift+클릭)",
|
||||
"TooltipSubtractOneSecond": "1초 빼기",
|
||||
"TooltipUnlockAllChapters": "모든 챕터 잠금 해제",
|
||||
"TooltipUnlockChapter": "챕터 잠금 해제(범위를 보려면 Shift+클릭)"
|
||||
}
|
||||
"TooltipUnlockChapter": "챕터 잠금 해제(범위를 보려면 Shift+클릭)",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -703,5 +703,12 @@
|
|||
"ToastSocketDisconnected": "Severis atjungtas",
|
||||
"ToastSocketFailedToConnect": "Nepavyko prisijungti prie serverio",
|
||||
"ToastUserDeleteFailed": "Nepavyko ištrinti naudotojo",
|
||||
"ToastUserDeleteSuccess": "Naudotojas ištrintas"
|
||||
}
|
||||
"ToastUserDeleteSuccess": "Naudotojas ištrintas",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1159,5 +1159,12 @@
|
|||
"TooltipLockChapter": "Hoofdstuk vergrendelen (Shift+klikken voor bereik)",
|
||||
"TooltipSubtractOneSecond": "Trek 1 seconde af",
|
||||
"TooltipUnlockAllChapters": "Alle hoofdstukken ontgrendelen",
|
||||
"TooltipUnlockChapter": "Hoofdstuk ontgrendelen (Shift+klikken voor bereik)"
|
||||
}
|
||||
"TooltipUnlockChapter": "Hoofdstuk ontgrendelen (Shift+klikken voor bereik)",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1161,5 +1161,12 @@
|
|||
"TooltipLockChapter": "Lås kapittel (Shift+klikk for område)",
|
||||
"TooltipSubtractOneSecond": "Trekk fra 1 sekund",
|
||||
"TooltipUnlockAllChapters": "Lås opp alle kapitler",
|
||||
"TooltipUnlockChapter": "Lås opp kapittel (Shift+klikk for område)"
|
||||
}
|
||||
"TooltipUnlockChapter": "Lås opp kapittel (Shift+klikk for område)",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1113,5 +1113,12 @@
|
|||
"TooltipLockChapter": "Zablokuj rozdział (przytrzymaj Shift i kliknij, aby zaznaczyć zakres)",
|
||||
"TooltipSubtractOneSecond": "Odejmij sekundę",
|
||||
"TooltipUnlockAllChapters": "Odblokuj wszystkie rozdziały",
|
||||
"TooltipUnlockChapter": "Odblokuj rozdział (przytrzymaj Shift i kliknij, aby zaznaczyć zakres)"
|
||||
}
|
||||
"TooltipUnlockChapter": "Odblokuj rozdział (przytrzymaj Shift i kliknij, aby zaznaczyć zakres)",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1161,5 +1161,12 @@
|
|||
"TooltipLockChapter": "Bloquear capítulo (Shift+clique para selecionar o intervalo)",
|
||||
"TooltipSubtractOneSecond": "Subtrair 1 segundo",
|
||||
"TooltipUnlockAllChapters": "Desbloqueie todos os capítulos",
|
||||
"TooltipUnlockChapter": "Desbloquear capítulo (Shift + clique para selecionar o intervalo)"
|
||||
}
|
||||
"TooltipUnlockChapter": "Desbloquear capítulo (Shift + clique para selecionar o intervalo)",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -641,5 +641,12 @@
|
|||
"ToastPodcastCreateFailed": "Nu s-a putut crea podcastul",
|
||||
"ToastPodcastCreateSuccess": "Podcast creat cu succes",
|
||||
"ToastRSSFeedCloseFailed": "Nu s-a putut închide fluxul RSS",
|
||||
"ToastRSSFeedCloseSuccess": "Flux RSS închis"
|
||||
}
|
||||
"ToastRSSFeedCloseSuccess": "Flux RSS închis",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1161,5 +1161,12 @@
|
|||
"TooltipLockChapter": "Заблокировать главу (Shift+клик для диапазона)",
|
||||
"TooltipSubtractOneSecond": "Вычтите 1 секунду",
|
||||
"TooltipUnlockAllChapters": "Разблокируйте все главы",
|
||||
"TooltipUnlockChapter": "Разблокируйте главу (Shift+клик для диапазона)"
|
||||
}
|
||||
"TooltipUnlockChapter": "Разблокируйте главу (Shift+клик для диапазона)",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1161,5 +1161,12 @@
|
|||
"TooltipLockChapter": "Zamknúť kapitolu (Shift+klik pre skupinu)",
|
||||
"TooltipSubtractOneSecond": "Odobrať 1 sekundu",
|
||||
"TooltipUnlockAllChapters": "Odomknúť všetky kapitoly",
|
||||
"TooltipUnlockChapter": "Odomknúť kapitolu (Shift+klik pre skupinu)"
|
||||
}
|
||||
"TooltipUnlockChapter": "Odomknúť kapitolu (Shift+klik pre skupinu)",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1161,5 +1161,12 @@
|
|||
"TooltipLockChapter": "Zakleni poglavje (Shift+klik za obseg)",
|
||||
"TooltipSubtractOneSecond": "Odštej 1 sekundo",
|
||||
"TooltipUnlockAllChapters": "Odkleni vsa poglavja",
|
||||
"TooltipUnlockChapter": "Odkleni poglavje (Shift+klik za obseg)"
|
||||
}
|
||||
"TooltipUnlockChapter": "Odkleni poglavje (Shift+klik za obseg)",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1161,5 +1161,12 @@
|
|||
"TooltipLockChapter": "Lås kapitel (Tryck på Shift + Klick för att markera flera)",
|
||||
"TooltipSubtractOneSecond": "Minska med 1 sekund",
|
||||
"TooltipUnlockAllChapters": "Lås upp alla kapitel",
|
||||
"TooltipUnlockChapter": "Lås upp kapitel (Tryck på Shift + Klick för att markera flera)"
|
||||
}
|
||||
"TooltipUnlockChapter": "Lås upp kapitel (Tryck på Shift + Klick för att markera flera)",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1161,5 +1161,12 @@
|
|||
"TooltipLockChapter": "Bölümü kilitle (aralık için Shift+tıkla)",
|
||||
"TooltipSubtractOneSecond": "1 saniye çıkar",
|
||||
"TooltipUnlockAllChapters": "Tüm bölümlerin kilidini aç",
|
||||
"TooltipUnlockChapter": "Bölüm kilidini aç (aralık için Shift+tıkla)"
|
||||
}
|
||||
"TooltipUnlockChapter": "Bölüm kilidini aç (aralık için Shift+tıkla)",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1161,5 +1161,12 @@
|
|||
"TooltipLockChapter": "Заблокувати розділ (Shift+клацання для діапазону)",
|
||||
"TooltipSubtractOneSecond": "Відніміть 1 секунду",
|
||||
"TooltipUnlockAllChapters": "Розблокувати всі розділи",
|
||||
"TooltipUnlockChapter": "Розблокувати розділ (Shift+клацання для діапазону)"
|
||||
}
|
||||
"TooltipUnlockChapter": "Розблокувати розділ (Shift+клацання для діапазону)",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -742,5 +742,12 @@
|
|||
"ToastSocketDisconnected": "Ngắt kết nối socket",
|
||||
"ToastSocketFailedToConnect": "Không thể kết nối socket",
|
||||
"ToastUserDeleteFailed": "Xóa người dùng thất bại",
|
||||
"ToastUserDeleteSuccess": "Người dùng đã được xóa"
|
||||
}
|
||||
"ToastUserDeleteSuccess": "Người dùng đã được xóa",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -1161,5 +1161,12 @@
|
|||
"TooltipLockChapter": "锁定章节 (按住 Shift再点击, 可进行范围选择)",
|
||||
"TooltipSubtractOneSecond": "减 1 秒",
|
||||
"TooltipUnlockAllChapters": "解锁所有章节",
|
||||
"TooltipUnlockChapter": "解锁章节 (按住 Shift再点击, 可进行范围选择)"
|
||||
}
|
||||
"TooltipUnlockChapter": "解锁章节 (按住 Shift再点击, 可进行范围选择)",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -755,5 +755,12 @@
|
|||
"ToastSocketDisconnected": "網路已斷開",
|
||||
"ToastSocketFailedToConnect": "網路連接失敗",
|
||||
"ToastUserDeleteFailed": "刪除使用者失敗",
|
||||
"ToastUserDeleteSuccess": "使用者已刪除"
|
||||
}
|
||||
"ToastUserDeleteSuccess": "使用者已刪除",
|
||||
"ButtonReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFiles": "Reorganize Files",
|
||||
"LabelToolsReorganizeFilesDescription": "Move all files to a directory structure based on item metadata (author/series/title for books, title for podcasts)",
|
||||
"MessageBatchReorganizeStarted": "Reorganizing files for {0} items in background...",
|
||||
"MessageBatchReorganizeSuccess": "{0} items reorganized successfully",
|
||||
"MessageBatchReorganizePartial": "{0} items reorganized, {1} failed",
|
||||
"LabelPreview": "Preview"
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ const Database = require('../Database')
|
|||
const zipHelpers = require('../utils/zipHelpers')
|
||||
const { reqSupportsWebp } = require('../utils/index')
|
||||
const { ScanResult, AudioMimeType } = require('../utils/constants')
|
||||
const { getAudioMimeTypeFromExtname, encodeUriPath } = require('../utils/fileUtils')
|
||||
const { getAudioMimeTypeFromExtname, encodeUriPath, sanitizeFilename } = require('../utils/fileUtils')
|
||||
const LibraryItemScanner = require('../scanner/LibraryItemScanner')
|
||||
const AudioFileScanner = require('../scanner/AudioFileScanner')
|
||||
const Scanner = require('../scanner/Scanner')
|
||||
|
|
@ -489,6 +489,34 @@ class LibraryItemController {
|
|||
res.json(req.libraryItem.toOldJSON())
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/items/:id/reorganize-files
|
||||
* Reorganize library item files into metadata-based directory structure
|
||||
*
|
||||
* @param {LibraryItemControllerRequest} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async reorganizeFiles(req, res) {
|
||||
if (!req.user.canUpdate) {
|
||||
Logger.warn(`[LibraryItemController] User "${req.user.username}" attempted to reorganize files without permission`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await this.reorganizeFilesForItem(req.libraryItem)
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(400).send(`Error: ${result.error}`)
|
||||
}
|
||||
|
||||
Logger.info(`[LibraryItemController] reorganizeFiles completed successfully`)
|
||||
res.json({ success: true })
|
||||
} catch (error) {
|
||||
Logger.error(`[LibraryItemController] reorganizeFiles error: ${error.message}`)
|
||||
res.status(400).send(`Error: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/items/:id/match
|
||||
*
|
||||
|
|
@ -1163,6 +1191,202 @@ class LibraryItemController {
|
|||
* @param {Response} res
|
||||
* @param {NextFunction} next
|
||||
*/
|
||||
|
||||
/**
|
||||
* Helper method to reorganize a single item's files
|
||||
* @param {import('../models/LibraryItem')} libraryItem
|
||||
* @returns {Promise<{success: boolean, error?: string}>}
|
||||
*/
|
||||
async reorganizeFilesForItem(libraryItem) {
|
||||
try {
|
||||
const itemId = libraryItem.id
|
||||
const currentPath = libraryItem.path
|
||||
const libraryId = libraryItem.libraryId
|
||||
const isBook = libraryItem.isBook
|
||||
const isPodcast = libraryItem.isPodcast
|
||||
|
||||
Logger.info(`[LibraryItemController] Reorganizing item ${itemId}`)
|
||||
|
||||
// Reload media with authors and series for books
|
||||
let media = libraryItem.media
|
||||
if (isBook) {
|
||||
media = await libraryItem.getMedia({
|
||||
include: [
|
||||
{
|
||||
model: Database.authorModel,
|
||||
through: {
|
||||
attributes: ['id', 'createdAt']
|
||||
}
|
||||
},
|
||||
{
|
||||
model: Database.seriesModel,
|
||||
through: {
|
||||
attributes: ['id', 'sequence', 'createdAt']
|
||||
}
|
||||
}
|
||||
],
|
||||
order: [
|
||||
[Database.authorModel, Database.bookAuthorModel, 'createdAt', 'ASC'],
|
||||
[Database.seriesModel, 'bookSeries', 'createdAt', 'ASC']
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
if (!media) {
|
||||
return { success: false, error: 'Media not found' }
|
||||
}
|
||||
|
||||
// Get library folder
|
||||
const library = await Database.libraryModel.findByIdWithFolders(libraryId)
|
||||
if (!library || !library.libraryFolders || !library.libraryFolders.length) {
|
||||
return { success: false, error: 'Library not found' }
|
||||
}
|
||||
|
||||
const libraryFolder = library.libraryFolders.find((f) => currentPath.startsWith(f.path))
|
||||
if (!libraryFolder) {
|
||||
return { success: false, error: 'Library folder not found' }
|
||||
}
|
||||
|
||||
// Build new path
|
||||
const parts = []
|
||||
if (isPodcast) {
|
||||
parts.push(media.title)
|
||||
} else if (isBook) {
|
||||
if (media.authorName) {
|
||||
parts.push(media.authorName)
|
||||
}
|
||||
if (media.seriesName) {
|
||||
parts.push(media.seriesName)
|
||||
}
|
||||
parts.push(media.title)
|
||||
}
|
||||
|
||||
const sanitizedParts = parts.filter(Boolean).map((p) => sanitizeFilename(p))
|
||||
const newPath = Path.join(libraryFolder.path, ...sanitizedParts)
|
||||
|
||||
Logger.info(`[LibraryItemController] Item path: ${currentPath} -> ${newPath}`)
|
||||
|
||||
// Move files if path is different
|
||||
if (newPath !== currentPath) {
|
||||
// Check current directory exists
|
||||
const currentExists = await fs.pathExists(currentPath)
|
||||
if (!currentExists) {
|
||||
return { success: false, error: 'Current directory does not exist' }
|
||||
}
|
||||
|
||||
// Check if target already exists with different item
|
||||
const targetExists = await fs.pathExists(newPath)
|
||||
if (targetExists) {
|
||||
const existingItem = await Database.libraryItemModel.findOne({ where: { path: newPath } })
|
||||
if (existingItem && existingItem.id !== itemId) {
|
||||
return { success: false, error: 'Target directory already contains a different item' }
|
||||
}
|
||||
}
|
||||
|
||||
// Create target directory
|
||||
await fs.ensureDir(newPath)
|
||||
|
||||
// Read and move files
|
||||
const files = await fs.readdir(currentPath)
|
||||
Logger.info(`[LibraryItemController] Moving ${files.length} files for item ${itemId}`)
|
||||
|
||||
for (const file of files) {
|
||||
const src = Path.join(currentPath, file)
|
||||
const dst = Path.join(newPath, file)
|
||||
await fs.move(src, dst, { overwrite: true })
|
||||
}
|
||||
|
||||
// Update database
|
||||
await Database.libraryItemModel.update({ path: newPath }, { where: { id: itemId } })
|
||||
|
||||
// Clean up old directory
|
||||
try {
|
||||
const remaining = await fs.readdir(currentPath)
|
||||
if (remaining.length === 0) {
|
||||
await fs.remove(currentPath)
|
||||
Logger.info(`[LibraryItemController] Removed empty old directory`)
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.warn(`[LibraryItemController] Could not remove old directory: ${err.message}`)
|
||||
}
|
||||
} else {
|
||||
Logger.info(`[LibraryItemController] Paths are the same - no move needed for item ${itemId}`)
|
||||
}
|
||||
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
Logger.error(`[LibraryItemController] Error reorganizing item: ${error.message}`)
|
||||
return { success: false, error: error.message }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST: /api/items/batch/reorganize-files
|
||||
* Reorganize multiple library items' files into metadata-based directory structure
|
||||
* Responds immediately, processes in background
|
||||
*
|
||||
* @this {import('../routers/ApiRouter')}
|
||||
* @param {RequestWithUser} req - Request with libraryItemIds in body
|
||||
* @param {Response} res
|
||||
*/
|
||||
async batchReorganizeFiles(req, res) {
|
||||
if (!req.user.canUpdate) {
|
||||
Logger.warn(`[LibraryItemController] User "${req.user.username}" attempted to batch reorganize without permission`)
|
||||
return res.sendStatus(403)
|
||||
}
|
||||
|
||||
const { libraryItemIds } = req.body
|
||||
if (!libraryItemIds?.length || !Array.isArray(libraryItemIds)) {
|
||||
return res.status(400).send('Invalid request body')
|
||||
}
|
||||
|
||||
// Fetch items
|
||||
const itemsToReorganize = await Database.libraryItemModel.findAllExpandedWhere({
|
||||
id: libraryItemIds
|
||||
})
|
||||
|
||||
if (!itemsToReorganize.length) {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
// Respond immediately - this is a long-running operation
|
||||
res.sendStatus(200)
|
||||
|
||||
// Get the controller instance for calling helper methods
|
||||
const controller = require('./LibraryItemController')
|
||||
|
||||
// Process in background
|
||||
Logger.info(`[LibraryItemController] Starting batch reorganize for ${itemsToReorganize.length} items`)
|
||||
let successCount = 0
|
||||
let errorCount = 0
|
||||
const errors = []
|
||||
|
||||
for (const libraryItem of itemsToReorganize) {
|
||||
// Get the controller instance to call helper methods
|
||||
const result = await controller.reorganizeFilesForItem(libraryItem)
|
||||
if (result.success) {
|
||||
successCount++
|
||||
Logger.info(`[LibraryItemController] Successfully reorganized: ${libraryItem.media.title}`)
|
||||
} else {
|
||||
errorCount++
|
||||
errors.push({ id: libraryItem.id, title: libraryItem.media.title, error: result.error })
|
||||
Logger.error(`[LibraryItemController] Failed to reorganize ${libraryItem.media.title}: ${result.error}`)
|
||||
}
|
||||
}
|
||||
|
||||
Logger.info(`[LibraryItemController] Batch reorganize complete: ${successCount} succeeded, ${errorCount} failed`)
|
||||
|
||||
// Notify user of completion
|
||||
SocketAuthority.clientEmitter(req.user.id, 'batch_reorganize_complete', {
|
||||
successCount,
|
||||
errorCount,
|
||||
errors,
|
||||
total: itemsToReorganize.length
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
|
||||
async middleware(req, res, next) {
|
||||
req.libraryItem = await Database.libraryItemModel.getExpandedById(req.params.id)
|
||||
if (!req.libraryItem?.media) return res.sendStatus(404)
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ class ApiRouter {
|
|||
this.router.post('/items/batch/get', LibraryItemController.batchGet.bind(this))
|
||||
this.router.post('/items/batch/quickmatch', LibraryItemController.batchQuickMatch.bind(this))
|
||||
this.router.post('/items/batch/scan', LibraryItemController.batchScan.bind(this))
|
||||
this.router.post('/items/batch/reorganize-files', LibraryItemController.batchReorganizeFiles.bind(this))
|
||||
|
||||
this.router.get('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.findOne.bind(this))
|
||||
this.router.delete('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.delete.bind(this))
|
||||
|
|
@ -117,6 +118,7 @@ class ApiRouter {
|
|||
this.router.post('/items/:id/play', LibraryItemController.middleware.bind(this), LibraryItemController.startPlaybackSession.bind(this))
|
||||
this.router.post('/items/:id/play/:episodeId', LibraryItemController.middleware.bind(this), LibraryItemController.startEpisodePlaybackSession.bind(this))
|
||||
this.router.patch('/items/:id/tracks', LibraryItemController.middleware.bind(this), LibraryItemController.updateTracks.bind(this))
|
||||
this.router.post('/items/:id/reorganize-files', LibraryItemController.middleware.bind(this), LibraryItemController.reorganizeFiles.bind(this))
|
||||
this.router.post('/items/:id/scan', LibraryItemController.middleware.bind(this), LibraryItemController.scan.bind(this))
|
||||
this.router.get('/items/:id/metadata-object', LibraryItemController.middleware.bind(this), LibraryItemController.getMetadataObject.bind(this))
|
||||
this.router.post('/items/:id/chapters', LibraryItemController.middleware.bind(this), LibraryItemController.updateMediaChapters.bind(this))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue