diff --git a/client/components/app/Appbar.vue b/client/components/app/Appbar.vue
index 428ae6ebf..84fccd68c 100644
--- a/client/components/app/Appbar.vue
+++ b/client/components/app/Appbar.vue
@@ -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)
},
diff --git a/client/components/modals/BatchReorganizeModal.vue b/client/components/modals/BatchReorganizeModal.vue
new file mode 100644
index 000000000..2faabed10
--- /dev/null
+++ b/client/components/modals/BatchReorganizeModal.vue
@@ -0,0 +1,156 @@
+
+
+
+
+
+
+
+
+
+
{{ $strings.LabelToolsReorganizeFilesDescription }}
+
+
+
{{ $strings.LabelPreview }}
+
+
+
{{ preview.title }}
+
{{ preview.newPath }}
+
+
+
+
+
+
+
{{ $strings.ButtonCancel }}
+
+
+ {{ $strings.ButtonReorganizeFiles }}
+
+
+
+
+
+
+
+
+
+
diff --git a/client/components/modals/item/tabs/Tools.vue b/client/components/modals/item/tabs/Tools.vue
index 8e40ca3c0..531f515f1 100644
--- a/client/components/modals/item/tabs/Tools.vue
+++ b/client/components/modals/item/tabs/Tools.vue
@@ -49,6 +49,20 @@
{{ $strings.MessageNoAudioTracks }}
+
+
+
+
+
+
{{ $strings.LabelToolsReorganizeFiles }}
+
{{ $strings.LabelToolsReorganizeFilesDescription }}
+
+
+
+ {{ $strings.ButtonReorganizeFiles }}
+
+
+
@@ -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
+ }
}
}
}
diff --git a/client/layouts/default.vue b/client/layouts/default.vue
index 75753b214..fbca5a0ed 100644
--- a/client/layouts/default.vue
+++ b/client/layouts/default.vue
@@ -18,6 +18,7 @@
+
diff --git a/client/store/globals.js b/client/store/globals.js
index 7b416196a..e64d72450 100644
--- a/client/store/globals.js
+++ b/client/store/globals.js
@@ -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 = []
},
diff --git a/client/strings/ar.json b/client/strings/ar.json
index a176b9d8b..bc529be05 100644
--- a/client/strings/ar.json
+++ b/client/strings/ar.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/be.json b/client/strings/be.json
index d598ef7bf..17c1cbb07 100644
--- a/client/strings/be.json
+++ b/client/strings/be.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/bg.json b/client/strings/bg.json
index dacd62087..2122a5123 100644
--- a/client/strings/bg.json
+++ b/client/strings/bg.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/bn.json b/client/strings/bn.json
index 75cf98ee5..fb25d4e34 100644
--- a/client/strings/bn.json
+++ b/client/strings/bn.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/ca.json b/client/strings/ca.json
index 4180b2518..95a2eef5b 100644
--- a/client/strings/ca.json
+++ b/client/strings/ca.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/cs.json b/client/strings/cs.json
index 49b0d2bd7..20f15538f 100644
--- a/client/strings/cs.json
+++ b/client/strings/cs.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/da.json b/client/strings/da.json
index 6cf517fa3..f58f37120 100644
--- a/client/strings/da.json
+++ b/client/strings/da.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/de.json b/client/strings/de.json
index ed0cae52c..2432a0056 100644
--- a/client/strings/de.json
+++ b/client/strings/de.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/el.json b/client/strings/el.json
index 2160b6e14..9cfe97114 100644
--- a/client/strings/el.json
+++ b/client/strings/el.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/en-us.json b/client/strings/en-us.json
index fb2bcb281..ccd492a38 100644
--- a/client/strings/en-us.json
+++ b/client/strings/en-us.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/es.json b/client/strings/es.json
index eae32bd9f..7b3e69971 100644
--- a/client/strings/es.json
+++ b/client/strings/es.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/et.json b/client/strings/et.json
index d3ab4e760..e18a097d4 100644
--- a/client/strings/et.json
+++ b/client/strings/et.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/eu.json b/client/strings/eu.json
index 0967ef424..62c665253 100644
--- a/client/strings/eu.json
+++ b/client/strings/eu.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/fa.json b/client/strings/fa.json
index b0d4ab141..7e8210e6c 100644
--- a/client/strings/fa.json
+++ b/client/strings/fa.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/fi.json b/client/strings/fi.json
index dde7009ea..50d523beb 100644
--- a/client/strings/fi.json
+++ b/client/strings/fi.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/fr.json b/client/strings/fr.json
index 507574033..99e9940f7 100644
--- a/client/strings/fr.json
+++ b/client/strings/fr.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/gu.json b/client/strings/gu.json
index 301d673b9..72cd7d566 100644
--- a/client/strings/gu.json
+++ b/client/strings/gu.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/he.json b/client/strings/he.json
index 786683a5d..7c28b033a 100644
--- a/client/strings/he.json
+++ b/client/strings/he.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/hi.json b/client/strings/hi.json
index e08222dba..84314804a 100644
--- a/client/strings/hi.json
+++ b/client/strings/hi.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/hr.json b/client/strings/hr.json
index 65150409f..04a836796 100644
--- a/client/strings/hr.json
+++ b/client/strings/hr.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/hu.json b/client/strings/hu.json
index 90f58343a..39f3e7c15 100644
--- a/client/strings/hu.json
+++ b/client/strings/hu.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/is.json b/client/strings/is.json
index 0967ef424..62c665253 100644
--- a/client/strings/is.json
+++ b/client/strings/is.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/it.json b/client/strings/it.json
index d2178cbf6..a350e3e8c 100644
--- a/client/strings/it.json
+++ b/client/strings/it.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/ja.json b/client/strings/ja.json
index 0621614f4..5ddac4d3a 100644
--- a/client/strings/ja.json
+++ b/client/strings/ja.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/ko.json b/client/strings/ko.json
index a2a4dc92a..3771bcbf9 100644
--- a/client/strings/ko.json
+++ b/client/strings/ko.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/lt.json b/client/strings/lt.json
index 2480cdf35..98849f775 100644
--- a/client/strings/lt.json
+++ b/client/strings/lt.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/nl.json b/client/strings/nl.json
index 708ea7a97..8c81ab96b 100644
--- a/client/strings/nl.json
+++ b/client/strings/nl.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/no.json b/client/strings/no.json
index 8e1c9cfbb..854db1acb 100644
--- a/client/strings/no.json
+++ b/client/strings/no.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/pl.json b/client/strings/pl.json
index d1bc6c062..d270a6c4d 100644
--- a/client/strings/pl.json
+++ b/client/strings/pl.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json
index d2189698c..8d121fd2d 100644
--- a/client/strings/pt-br.json
+++ b/client/strings/pt-br.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/ro.json b/client/strings/ro.json
index 441ccc400..d9a7aa7de 100644
--- a/client/strings/ro.json
+++ b/client/strings/ro.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/ru.json b/client/strings/ru.json
index c84fe9dcf..640980900 100644
--- a/client/strings/ru.json
+++ b/client/strings/ru.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/sk.json b/client/strings/sk.json
index 6101eba7d..3a01ab543 100644
--- a/client/strings/sk.json
+++ b/client/strings/sk.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/sl.json b/client/strings/sl.json
index c88be4ba1..0e1e9538f 100644
--- a/client/strings/sl.json
+++ b/client/strings/sl.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/sv.json b/client/strings/sv.json
index fa0267e82..e738437ec 100644
--- a/client/strings/sv.json
+++ b/client/strings/sv.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/tr.json b/client/strings/tr.json
index 770521cb8..79c4b7cf5 100644
--- a/client/strings/tr.json
+++ b/client/strings/tr.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/uk.json b/client/strings/uk.json
index 1db23322f..739970422 100644
--- a/client/strings/uk.json
+++ b/client/strings/uk.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/vi-vn.json b/client/strings/vi-vn.json
index b8a257209..e02ffc361 100644
--- a/client/strings/vi-vn.json
+++ b/client/strings/vi-vn.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/zh-cn.json b/client/strings/zh-cn.json
index 14c70cb10..d8c132646 100644
--- a/client/strings/zh-cn.json
+++ b/client/strings/zh-cn.json
@@ -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"
+}
\ No newline at end of file
diff --git a/client/strings/zh-tw.json b/client/strings/zh-tw.json
index 92da7cf6b..15a33ca5d 100644
--- a/client/strings/zh-tw.json
+++ b/client/strings/zh-tw.json
@@ -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"
+}
\ No newline at end of file
diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js
index 5247dbb06..a7465c788 100644
--- a/server/controllers/LibraryItemController.js
+++ b/server/controllers/LibraryItemController.js
@@ -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)
diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js
index db04bf5ec..87b113966 100644
--- a/server/routers/ApiRouter.js
+++ b/server/routers/ApiRouter.js
@@ -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))