From b4fded0d405ababf5594baed858510a8e379f60a Mon Sep 17 00:00:00 2001 From: Thomas Date: Fri, 20 Mar 2026 11:21:56 -0600 Subject: [PATCH] created a directory reorganizer --- client/components/app/Appbar.vue | 12 + .../modals/BatchReorganizeModal.vue | 156 ++++++++++++ client/components/modals/item/tabs/Tools.vue | 50 +++- client/layouts/default.vue | 1 + client/store/globals.js | 4 + client/strings/ar.json | 11 +- client/strings/be.json | 11 +- client/strings/bg.json | 11 +- client/strings/bn.json | 11 +- client/strings/ca.json | 11 +- client/strings/cs.json | 11 +- client/strings/da.json | 11 +- client/strings/de.json | 11 +- client/strings/el.json | 11 +- client/strings/en-us.json | 11 +- client/strings/es.json | 11 +- client/strings/et.json | 11 +- client/strings/eu.json | 10 +- client/strings/fa.json | 11 +- client/strings/fi.json | 11 +- client/strings/fr.json | 11 +- client/strings/gu.json | 11 +- client/strings/he.json | 11 +- client/strings/hi.json | 11 +- client/strings/hr.json | 11 +- client/strings/hu.json | 11 +- client/strings/is.json | 10 +- client/strings/it.json | 11 +- client/strings/ja.json | 11 +- client/strings/ko.json | 11 +- client/strings/lt.json | 11 +- client/strings/nl.json | 11 +- client/strings/no.json | 11 +- client/strings/pl.json | 11 +- client/strings/pt-br.json | 11 +- client/strings/ro.json | 11 +- client/strings/ru.json | 11 +- client/strings/sk.json | 11 +- client/strings/sl.json | 11 +- client/strings/sv.json | 11 +- client/strings/tr.json | 11 +- client/strings/uk.json | 11 +- client/strings/vi-vn.json | 11 +- client/strings/zh-cn.json | 11 +- client/strings/zh-tw.json | 11 +- server/controllers/LibraryItemController.js | 226 +++++++++++++++++- server/routers/ApiRouter.js | 2 + 47 files changed, 809 insertions(+), 80 deletions(-) create mode 100644 client/components/modals/BatchReorganizeModal.vue 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 @@ + + + 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))