mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-07 09:51:37 +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'
|
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
|
// 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
|
// + 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) {
|
if (this.selectedMediaItems.length <= 40) {
|
||||||
|
|
@ -225,6 +232,8 @@ export default {
|
||||||
this.batchAutoMatchClick()
|
this.batchAutoMatchClick()
|
||||||
} else if (action === 'rescan') {
|
} else if (action === 'rescan') {
|
||||||
this.batchRescan()
|
this.batchRescan()
|
||||||
|
} else if (action === 'reorganize') {
|
||||||
|
this.batchReorganizeClick()
|
||||||
} else if (action === 'download') {
|
} else if (action === 'download') {
|
||||||
this.batchDownload()
|
this.batchDownload()
|
||||||
}
|
}
|
||||||
|
|
@ -368,6 +377,9 @@ export default {
|
||||||
batchEditClick() {
|
batchEditClick() {
|
||||||
this.$router.push('/batch')
|
this.$router.push('/batch')
|
||||||
},
|
},
|
||||||
|
batchReorganizeClick() {
|
||||||
|
this.$store.commit('globals/setShowBatchReorganizeModal', true)
|
||||||
|
},
|
||||||
batchAddToCollectionClick() {
|
batchAddToCollectionClick() {
|
||||||
this.$store.commit('globals/setShowBatchCollectionsModal', true)
|
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>
|
</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>
|
||||||
|
|
||||||
|
<!-- 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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -62,7 +76,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {
|
||||||
|
isReorganizing: false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
|
|
@ -123,6 +139,38 @@ export default {
|
||||||
type: 'yesNo'
|
type: 'yesNo'
|
||||||
}
|
}
|
||||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
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-podcast-view-episode />
|
||||||
<modals-authors-edit-modal />
|
<modals-authors-edit-modal />
|
||||||
<modals-batch-quick-match-model />
|
<modals-batch-quick-match-model />
|
||||||
|
<modals-batch-reorganize-modal />
|
||||||
<modals-rssfeed-open-close-modal />
|
<modals-rssfeed-open-close-modal />
|
||||||
<modals-raw-cover-preview-modal />
|
<modals-raw-cover-preview-modal />
|
||||||
<modals-share-modal />
|
<modals-share-modal />
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ export const state = () => ({
|
||||||
isCasting: false, // Actively casting
|
isCasting: false, // Actively casting
|
||||||
isChromecastInitialized: false, // Script loadeds
|
isChromecastInitialized: false, // Script loadeds
|
||||||
showBatchQuickMatchModal: false,
|
showBatchQuickMatchModal: false,
|
||||||
|
showBatchReorganizeModal: false,
|
||||||
dateFormats: [
|
dateFormats: [
|
||||||
{
|
{
|
||||||
text: 'MM/DD/YYYY',
|
text: 'MM/DD/YYYY',
|
||||||
|
|
@ -204,6 +205,9 @@ export const mutations = {
|
||||||
setShowBatchQuickMatchModal(state, val) {
|
setShowBatchQuickMatchModal(state, val) {
|
||||||
state.showBatchQuickMatchModal = val
|
state.showBatchQuickMatchModal = val
|
||||||
},
|
},
|
||||||
|
setShowBatchReorganizeModal(state, val) {
|
||||||
|
state.showBatchReorganizeModal = val
|
||||||
|
},
|
||||||
resetSelectedMediaItems(state) {
|
resetSelectedMediaItems(state) {
|
||||||
state.selectedMediaItems = []
|
state.selectedMediaItems = []
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1114,5 +1114,12 @@
|
||||||
"ToastUserPasswordChangeSuccess": "تم تغيير كلمة المرور بنجاح",
|
"ToastUserPasswordChangeSuccess": "تم تغيير كلمة المرور بنجاح",
|
||||||
"ToastUserPasswordMismatch": "كلمات المرور غير متطابقة",
|
"ToastUserPasswordMismatch": "كلمات المرور غير متطابقة",
|
||||||
"ToastUserPasswordMustChange": "يجب ألا تطابق كلمة المرور الجديدة كلمة المرور القديمة",
|
"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+націсканне для дыяпазону)",
|
"TooltipLockChapter": "Заблакіраваць раздзел (Shift+націсканне для дыяпазону)",
|
||||||
"TooltipSubtractOneSecond": "Адняць 1 секунду",
|
"TooltipSubtractOneSecond": "Адняць 1 секунду",
|
||||||
"TooltipUnlockAllChapters": "Разблакіраваць усе раздзелы",
|
"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 префикс за сортиране",
|
"ToastSortingPrefixesEmptyError": "Трябва да има поне 1 префикс за сортиране",
|
||||||
"ToastSortingPrefixesUpdateSuccess": "Префиксите за сортиране са актуализирани ({0} елемента)",
|
"ToastSortingPrefixesUpdateSuccess": "Префиксите за сортиране са актуализирани ({0} елемента)",
|
||||||
"ToastUserDeleteFailed": "Неуспешно изтриване на потребител",
|
"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": "পাসওয়ার্ড সফলভাবে পরিবর্তন করা হয়েছে",
|
"ToastUserPasswordChangeSuccess": "পাসওয়ার্ড সফলভাবে পরিবর্তন করা হয়েছে",
|
||||||
"ToastUserPasswordMismatch": "পাসওয়ার্ড মিলছে না",
|
"ToastUserPasswordMismatch": "পাসওয়ার্ড মিলছে না",
|
||||||
"ToastUserPasswordMustChange": "নতুন পাসওয়ার্ড পুরানো পাসওয়ার্ডের সাথে মিলতে পারবে না",
|
"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",
|
"ToastUserPasswordChangeSuccess": "Contrasenya canviada correctament",
|
||||||
"ToastUserPasswordMismatch": "Les contrasenyes no coincideixen",
|
"ToastUserPasswordMismatch": "Les contrasenyes no coincideixen",
|
||||||
"ToastUserPasswordMustChange": "La nova contrasenya no pot ser igual a l'anterior",
|
"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)",
|
"TooltipLockChapter": "Uzamknout kapitolu (Shift+klik pro rozsah)",
|
||||||
"TooltipSubtractOneSecond": "Odečíst 1 sekundu",
|
"TooltipSubtractOneSecond": "Odečíst 1 sekundu",
|
||||||
"TooltipUnlockAllChapters": "Odemknout všechny kapitoly",
|
"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)",
|
"TooltipLockChapter": "Lås kapitel (Shift+click for at markere flere)",
|
||||||
"TooltipSubtractOneSecond": "Fratag 1 sekund",
|
"TooltipSubtractOneSecond": "Fratag 1 sekund",
|
||||||
"TooltipUnlockAllChapters": "Lås alle kapitaler op",
|
"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)",
|
"TooltipLockChapter": "Kapitel sperren (Shift+Klick für mehrere)",
|
||||||
"TooltipSubtractOneSecond": "1 Sekunde abziehen",
|
"TooltipSubtractOneSecond": "1 Sekunde abziehen",
|
||||||
"TooltipUnlockAllChapters": "Alle Kapitel freigeben",
|
"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": "Φόρτωση...",
|
"MessageLoading": "Φόρτωση...",
|
||||||
"MessageMarkAsFinished": "Σήμανση ως Ολοκληρωμένο",
|
"MessageMarkAsFinished": "Σήμανση ως Ολοκληρωμένο",
|
||||||
"MessageNoItemsFound": "Δεν βρέθηκαν αντικείμενα",
|
"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)",
|
"TooltipLockChapter": "Lock chapter (Shift+click for range)",
|
||||||
"TooltipSubtractOneSecond": "Subtract 1 second",
|
"TooltipSubtractOneSecond": "Subtract 1 second",
|
||||||
"TooltipUnlockAllChapters": "Unlock all chapters",
|
"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)",
|
"TooltipLockChapter": "Bloquear capítulo (Mayús+clic para rango)",
|
||||||
"TooltipSubtractOneSecond": "Restar 1 segundo",
|
"TooltipSubtractOneSecond": "Restar 1 segundo",
|
||||||
"TooltipUnlockAllChapters": "Desbloquear todos los capítulos",
|
"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",
|
"ToastSocketDisconnected": "Pesa ühendus katkenud",
|
||||||
"ToastSocketFailedToConnect": "Pesa ühendamine ebaõnnestus",
|
"ToastSocketFailedToConnect": "Pesa ühendamine ebaõnnestus",
|
||||||
"ToastUserDeleteFailed": "Kasutaja kustutamine 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": "ذخیره",
|
"ButtonSave": "ذخیره",
|
||||||
"ButtonSearch": "جستجو",
|
"ButtonSearch": "جستجو",
|
||||||
"ButtonSeries": "مجموعه",
|
"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)",
|
"TooltipLockChapter": "Lukitse luku (Shift+napauta valitaksesi alueen)",
|
||||||
"TooltipSubtractOneSecond": "Vähennä 1 sekunti",
|
"TooltipSubtractOneSecond": "Vähennä 1 sekunti",
|
||||||
"TooltipUnlockAllChapters": "Avaa kaikki luvut",
|
"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)",
|
"TooltipLockChapter": "Verrouiller le chapitre (Maj+clic pour plage)",
|
||||||
"TooltipSubtractOneSecond": "Soustraire 1 seconde",
|
"TooltipSubtractOneSecond": "Soustraire 1 seconde",
|
||||||
"TooltipUnlockAllChapters": "Déverrouiller tous les chapitres",
|
"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": "પોડકાસ્ટ શોધ પ્રદેશ",
|
"LabelPodcastSearchRegion": "પોડકાસ્ટ શોધ પ્રદેશ",
|
||||||
"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.",
|
||||||
"MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"",
|
"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": "קצה תקשורת נותק",
|
"ToastSocketDisconnected": "קצה תקשורת נותק",
|
||||||
"ToastSocketFailedToConnect": "התחברות קצה התקשורת נכשלה",
|
"ToastSocketFailedToConnect": "התחברות קצה התקשורת נכשלה",
|
||||||
"ToastUserDeleteFailed": "מחיקת המשתמש נכשלה",
|
"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.",
|
"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}\"",
|
"MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"",
|
||||||
"NoteChangeRootPassword": "रूट user is the only user that can have an empty password",
|
"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)",
|
"TooltipLockChapter": "Zaključaj poglavlje (Shift + klik za raspon)",
|
||||||
"TooltipSubtractOneSecond": "Oduzmi 1 sekundu",
|
"TooltipSubtractOneSecond": "Oduzmi 1 sekundu",
|
||||||
"TooltipUnlockAllChapters": "Otključaj sva poglavlja",
|
"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)",
|
"TooltipLockChapter": "Fejezet zárolása (Shift+kattintás a tartományhoz)",
|
||||||
"TooltipSubtractOneSecond": "1 másodperc levonása",
|
"TooltipSubtractOneSecond": "1 másodperc levonása",
|
||||||
"TooltipUnlockAllChapters": "Az összes fejezet feloldá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)",
|
"TooltipLockChapter": "Bloccare capitolo (Shift+click per intervallo)",
|
||||||
"TooltipSubtractOneSecond": "Sottrarre 1 secondo",
|
"TooltipSubtractOneSecond": "Sottrarre 1 secondo",
|
||||||
"TooltipUnlockAllChapters": "Sbloccare tutti i capitoli",
|
"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": "表紙を項目と一緒に保存する",
|
"LabelSettingsStoreCoversWithItem": "表紙を項目と一緒に保存する",
|
||||||
"LabelSettingsStoreCoversWithItemHelp": "デフォルトでは表紙は /metadata/items に保存されますが、この設定をオンにするとライブラリーの項目のフォルダーに保存されます。\"cover\" という名前のファイル一つのみが保持されます",
|
"LabelSettingsStoreCoversWithItemHelp": "デフォルトでは表紙は /metadata/items に保存されますが、この設定をオンにするとライブラリーの項目のフォルダーに保存されます。\"cover\" という名前のファイル一つのみが保持されます",
|
||||||
"LabelSettingsStoreMetadataWithItem": "メタデータを項目と一緒に保存する",
|
"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+클릭)",
|
"TooltipLockChapter": "장 잠금(범위를 위해 Shift+클릭)",
|
||||||
"TooltipSubtractOneSecond": "1초 빼기",
|
"TooltipSubtractOneSecond": "1초 빼기",
|
||||||
"TooltipUnlockAllChapters": "모든 챕터 잠금 해제",
|
"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",
|
"ToastSocketDisconnected": "Severis atjungtas",
|
||||||
"ToastSocketFailedToConnect": "Nepavyko prisijungti prie serverio",
|
"ToastSocketFailedToConnect": "Nepavyko prisijungti prie serverio",
|
||||||
"ToastUserDeleteFailed": "Nepavyko ištrinti naudotojo",
|
"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)",
|
"TooltipLockChapter": "Hoofdstuk vergrendelen (Shift+klikken voor bereik)",
|
||||||
"TooltipSubtractOneSecond": "Trek 1 seconde af",
|
"TooltipSubtractOneSecond": "Trek 1 seconde af",
|
||||||
"TooltipUnlockAllChapters": "Alle hoofdstukken ontgrendelen",
|
"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)",
|
"TooltipLockChapter": "Lås kapittel (Shift+klikk for område)",
|
||||||
"TooltipSubtractOneSecond": "Trekk fra 1 sekund",
|
"TooltipSubtractOneSecond": "Trekk fra 1 sekund",
|
||||||
"TooltipUnlockAllChapters": "Lås opp alle kapitler",
|
"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)",
|
"TooltipLockChapter": "Zablokuj rozdział (przytrzymaj Shift i kliknij, aby zaznaczyć zakres)",
|
||||||
"TooltipSubtractOneSecond": "Odejmij sekundę",
|
"TooltipSubtractOneSecond": "Odejmij sekundę",
|
||||||
"TooltipUnlockAllChapters": "Odblokuj wszystkie rozdziały",
|
"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)",
|
"TooltipLockChapter": "Bloquear capítulo (Shift+clique para selecionar o intervalo)",
|
||||||
"TooltipSubtractOneSecond": "Subtrair 1 segundo",
|
"TooltipSubtractOneSecond": "Subtrair 1 segundo",
|
||||||
"TooltipUnlockAllChapters": "Desbloqueie todos os capítulos",
|
"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",
|
"ToastPodcastCreateFailed": "Nu s-a putut crea podcastul",
|
||||||
"ToastPodcastCreateSuccess": "Podcast creat cu succes",
|
"ToastPodcastCreateSuccess": "Podcast creat cu succes",
|
||||||
"ToastRSSFeedCloseFailed": "Nu s-a putut închide fluxul RSS",
|
"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+клик для диапазона)",
|
"TooltipLockChapter": "Заблокировать главу (Shift+клик для диапазона)",
|
||||||
"TooltipSubtractOneSecond": "Вычтите 1 секунду",
|
"TooltipSubtractOneSecond": "Вычтите 1 секунду",
|
||||||
"TooltipUnlockAllChapters": "Разблокируйте все главы",
|
"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)",
|
"TooltipLockChapter": "Zamknúť kapitolu (Shift+klik pre skupinu)",
|
||||||
"TooltipSubtractOneSecond": "Odobrať 1 sekundu",
|
"TooltipSubtractOneSecond": "Odobrať 1 sekundu",
|
||||||
"TooltipUnlockAllChapters": "Odomknúť všetky kapitoly",
|
"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)",
|
"TooltipLockChapter": "Zakleni poglavje (Shift+klik za obseg)",
|
||||||
"TooltipSubtractOneSecond": "Odštej 1 sekundo",
|
"TooltipSubtractOneSecond": "Odštej 1 sekundo",
|
||||||
"TooltipUnlockAllChapters": "Odkleni vsa poglavja",
|
"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)",
|
"TooltipLockChapter": "Lås kapitel (Tryck på Shift + Klick för att markera flera)",
|
||||||
"TooltipSubtractOneSecond": "Minska med 1 sekund",
|
"TooltipSubtractOneSecond": "Minska med 1 sekund",
|
||||||
"TooltipUnlockAllChapters": "Lås upp alla kapitel",
|
"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)",
|
"TooltipLockChapter": "Bölümü kilitle (aralık için Shift+tıkla)",
|
||||||
"TooltipSubtractOneSecond": "1 saniye çıkar",
|
"TooltipSubtractOneSecond": "1 saniye çıkar",
|
||||||
"TooltipUnlockAllChapters": "Tüm bölümlerin kilidini aç",
|
"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+клацання для діапазону)",
|
"TooltipLockChapter": "Заблокувати розділ (Shift+клацання для діапазону)",
|
||||||
"TooltipSubtractOneSecond": "Відніміть 1 секунду",
|
"TooltipSubtractOneSecond": "Відніміть 1 секунду",
|
||||||
"TooltipUnlockAllChapters": "Розблокувати всі розділи",
|
"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",
|
"ToastSocketDisconnected": "Ngắt kết nối socket",
|
||||||
"ToastSocketFailedToConnect": "Không thể 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",
|
"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再点击, 可进行范围选择)",
|
"TooltipLockChapter": "锁定章节 (按住 Shift再点击, 可进行范围选择)",
|
||||||
"TooltipSubtractOneSecond": "减 1 秒",
|
"TooltipSubtractOneSecond": "减 1 秒",
|
||||||
"TooltipUnlockAllChapters": "解锁所有章节",
|
"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": "網路已斷開",
|
"ToastSocketDisconnected": "網路已斷開",
|
||||||
"ToastSocketFailedToConnect": "網路連接失敗",
|
"ToastSocketFailedToConnect": "網路連接失敗",
|
||||||
"ToastUserDeleteFailed": "刪除使用者失敗",
|
"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 zipHelpers = require('../utils/zipHelpers')
|
||||||
const { reqSupportsWebp } = require('../utils/index')
|
const { reqSupportsWebp } = require('../utils/index')
|
||||||
const { ScanResult, AudioMimeType } = require('../utils/constants')
|
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 LibraryItemScanner = require('../scanner/LibraryItemScanner')
|
||||||
const AudioFileScanner = require('../scanner/AudioFileScanner')
|
const AudioFileScanner = require('../scanner/AudioFileScanner')
|
||||||
const Scanner = require('../scanner/Scanner')
|
const Scanner = require('../scanner/Scanner')
|
||||||
|
|
@ -489,6 +489,34 @@ class LibraryItemController {
|
||||||
res.json(req.libraryItem.toOldJSON())
|
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
|
* POST /api/items/:id/match
|
||||||
*
|
*
|
||||||
|
|
@ -1163,6 +1191,202 @@ class LibraryItemController {
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
* @param {NextFunction} next
|
* @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) {
|
async middleware(req, res, next) {
|
||||||
req.libraryItem = await Database.libraryItemModel.getExpandedById(req.params.id)
|
req.libraryItem = await Database.libraryItemModel.getExpandedById(req.params.id)
|
||||||
if (!req.libraryItem?.media) return res.sendStatus(404)
|
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/get', LibraryItemController.batchGet.bind(this))
|
||||||
this.router.post('/items/batch/quickmatch', LibraryItemController.batchQuickMatch.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/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.get('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.findOne.bind(this))
|
||||||
this.router.delete('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.delete.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', 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.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.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.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.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))
|
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