@@ -253,7 +253,6 @@ export default {
hasSearched: false,
selectedMatch: null,
selectedMatchOrig: null,
- waitingForProviders: false,
selectedMatchUsage: {
title: true,
subtitle: true,
@@ -286,19 +285,9 @@ export default {
handler(newVal) {
if (newVal) this.init()
}
- },
- providersLoaded(isLoaded) {
- // Complete initialization once providers are loaded
- if (isLoaded && this.waitingForProviders) {
- this.waitingForProviders = false
- this.initProviderAndSearch()
- }
}
},
computed: {
- providersLoaded() {
- return this.$store.getters['scanners/areProvidersLoaded']
- },
isProcessing: {
get() {
return this.processing
@@ -330,7 +319,7 @@ export default {
},
providers() {
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
- return this.$store.state.scanners.bookProviders
+ return this.$store.state.scanners.providers
},
searchTitleLabel() {
if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN
@@ -489,24 +478,6 @@ export default {
this.checkboxToggled()
},
- initProviderAndSearch() {
- // Set provider based on media type
- if (this.isPodcast) {
- this.provider = 'itunes'
- } else {
- this.provider = this.getDefaultBookProvider()
- }
-
- // Prefer using ASIN if set and using audible provider
- if (this.provider.startsWith('audible') && this.libraryItem.media.metadata.asin) {
- this.searchTitle = this.libraryItem.media.metadata.asin
- this.searchAuthor = ''
- }
-
- if (this.searchTitle) {
- this.submitSearch()
- }
- },
init() {
this.clearSelectedMatch()
this.initSelectedMatchUsage()
@@ -524,13 +495,19 @@ export default {
}
this.searchTitle = this.libraryItem.media.metadata.title
this.searchAuthor = this.libraryItem.media.metadata.authorName || ''
+ if (this.isPodcast) this.provider = 'itunes'
+ else {
+ this.provider = this.getDefaultBookProvider()
+ }
- // Wait for providers to be loaded before setting provider and searching
- if (this.providersLoaded || this.isPodcast) {
- this.waitingForProviders = false
- this.initProviderAndSearch()
- } else {
- this.waitingForProviders = true
+ // Prefer using ASIN if set and using audible provider
+ if (this.provider.startsWith('audible') && this.libraryItem.media.metadata.asin) {
+ this.searchTitle = this.libraryItem.media.metadata.asin
+ this.searchAuthor = ''
+ }
+
+ if (this.searchTitle) {
+ this.submitSearch()
}
},
selectMatch(match) {
@@ -660,10 +637,6 @@ export default {
this.selectedMatch = null
this.selectedMatchOrig = null
}
- },
- mounted() {
- // Fetch providers if not already loaded
- this.$store.dispatch('scanners/fetchProviders')
}
}
diff --git a/client/components/modals/libraries/EditLibrary.vue b/client/components/modals/libraries/EditLibrary.vue
index c805f79b..083fc576 100644
--- a/client/components/modals/libraries/EditLibrary.vue
+++ b/client/components/modals/libraries/EditLibrary.vue
@@ -74,7 +74,7 @@ export default {
},
providers() {
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
- return this.$store.state.scanners.bookProviders
+ return this.$store.state.scanners.providers
}
},
methods: {
@@ -156,8 +156,6 @@ export default {
},
mounted() {
this.init()
- // Fetch providers if not already loaded
- this.$store.dispatch('scanners/fetchProviders')
}
}
diff --git a/client/components/modals/libraries/LibrarySettings.vue b/client/components/modals/libraries/LibrarySettings.vue
index 7cfc2201..d3b40de9 100644
--- a/client/components/modals/libraries/LibrarySettings.vue
+++ b/client/components/modals/libraries/LibrarySettings.vue
@@ -104,6 +104,7 @@ export default {
},
data() {
return {
+ provider: null,
useSquareBookCovers: false,
enableWatcher: false,
skipMatchingMediaWithAsin: false,
@@ -133,6 +134,10 @@ export default {
isPodcastLibrary() {
return this.mediaType === 'podcast'
},
+ providers() {
+ if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
+ return this.$store.state.scanners.providers
+ },
maskAsFinishedWhenItems() {
return [
{
diff --git a/client/layouts/default.vue b/client/layouts/default.vue
index 75753b21..4b972924 100644
--- a/client/layouts/default.vue
+++ b/client/layouts/default.vue
@@ -371,13 +371,11 @@ export default {
},
customMetadataProviderAdded(provider) {
if (!provider?.id) return
- // Refresh providers cache
- this.$store.dispatch('scanners/refreshProviders')
+ this.$store.commit('scanners/addCustomMetadataProvider', provider)
},
customMetadataProviderRemoved(provider) {
if (!provider?.id) return
- // Refresh providers cache
- this.$store.dispatch('scanners/refreshProviders')
+ this.$store.commit('scanners/removeCustomMetadataProvider', provider)
},
initializeSocket() {
if (this.$root.socket) {
diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue
index b8cf3cff..3d030bb3 100644
--- a/client/pages/config/index.vue
+++ b/client/pages/config/index.vue
@@ -247,8 +247,7 @@ export default {
return this.$store.state.serverSettings
},
providers() {
- // Use book cover providers for the cover provider dropdown
- return this.$store.state.scanners.bookCoverProviders || []
+ return this.$store.state.scanners.providers
},
dateFormats() {
return this.$store.state.globals.dateFormats
@@ -417,8 +416,6 @@ export default {
},
mounted() {
this.initServerSettings()
- // Fetch providers if not already loaded (for cover provider dropdown)
- this.$store.dispatch('scanners/fetchProviders')
}
}
diff --git a/client/pages/upload/index.vue b/client/pages/upload/index.vue
index 73ebef9c..eef05b60 100644
--- a/client/pages/upload/index.vue
+++ b/client/pages/upload/index.vue
@@ -155,7 +155,7 @@ export default {
},
providers() {
if (this.selectedLibraryIsPodcast) return this.$store.state.scanners.podcastProviders
- return this.$store.state.scanners.bookProviders
+ return this.$store.state.scanners.providers
},
canFetchMetadata() {
return !this.selectedLibraryIsPodcast && this.fetchMetadata.enabled
@@ -394,8 +394,6 @@ export default {
this.setMetadataProvider()
this.setDefaultFolder()
- // Fetch providers if not already loaded
- this.$store.dispatch('scanners/fetchProviders')
window.addEventListener('dragenter', this.dragenter)
window.addEventListener('dragleave', this.dragleave)
window.addEventListener('dragover', this.dragover)
diff --git a/client/store/libraries.js b/client/store/libraries.js
index a6cf1dd3..115fb53b 100644
--- a/client/store/libraries.js
+++ b/client/store/libraries.js
@@ -117,6 +117,7 @@ export const actions = {
const library = data.library
const filterData = data.filterdata
const issues = data.issues || 0
+ const customMetadataProviders = data.customMetadataProviders || []
const numUserPlaylists = data.numUserPlaylists
dispatch('user/checkUpdateLibrarySortFilter', library.mediaType, { root: true })
@@ -130,6 +131,8 @@ export const actions = {
commit('setLibraryIssues', issues)
commit('setLibraryFilterData', filterData)
commit('setNumUserPlaylists', numUserPlaylists)
+ commit('scanners/setCustomMetadataProviders', customMetadataProviders, { root: true })
+
commit('setCurrentLibrary', { id: libraryId })
return data
})
diff --git a/client/store/scanners.js b/client/store/scanners.js
index ccf7d924..2d3d465c 100644
--- a/client/store/scanners.js
+++ b/client/store/scanners.js
@@ -1,60 +1,126 @@
export const state = () => ({
- bookProviders: [],
- podcastProviders: [],
- bookCoverProviders: [],
- podcastCoverProviders: [],
- providersLoaded: false
+ providers: [
+ {
+ text: 'Google Books',
+ value: 'google'
+ },
+ {
+ text: 'Open Library',
+ value: 'openlibrary'
+ },
+ {
+ text: 'iTunes',
+ value: 'itunes'
+ },
+ {
+ text: 'Audible.com',
+ value: 'audible'
+ },
+ {
+ text: 'Audible.ca',
+ value: 'audible.ca'
+ },
+ {
+ text: 'Audible.co.uk',
+ value: 'audible.uk'
+ },
+ {
+ text: 'Audible.com.au',
+ value: 'audible.au'
+ },
+ {
+ text: 'Audible.fr',
+ value: 'audible.fr'
+ },
+ {
+ text: 'Audible.de',
+ value: 'audible.de'
+ },
+ {
+ text: 'Audible.co.jp',
+ value: 'audible.jp'
+ },
+ {
+ text: 'Audible.it',
+ value: 'audible.it'
+ },
+ {
+ text: 'Audible.co.in',
+ value: 'audible.in'
+ },
+ {
+ text: 'Audible.es',
+ value: 'audible.es'
+ },
+ {
+ text: 'FantLab.ru',
+ value: 'fantlab'
+ }
+ ],
+ podcastProviders: [
+ {
+ text: 'iTunes',
+ value: 'itunes'
+ }
+ ],
+ coverOnlyProviders: [
+ {
+ text: 'AudiobookCovers.com',
+ value: 'audiobookcovers'
+ }
+ ]
})
export const getters = {
- checkBookProviderExists: (state) => (providerValue) => {
- return state.bookProviders.some((p) => p.value === providerValue)
+ checkBookProviderExists: state => (providerValue) => {
+ return state.providers.some(p => p.value === providerValue)
},
- checkPodcastProviderExists: (state) => (providerValue) => {
- return state.podcastProviders.some((p) => p.value === providerValue)
- },
- areProvidersLoaded: (state) => state.providersLoaded
-}
-
-export const actions = {
- async fetchProviders({ commit, state }) {
- // Only fetch if not already loaded
- if (state.providersLoaded) {
- return
- }
-
- try {
- const response = await this.$axios.$get('/api/search/providers')
- if (response?.providers) {
- commit('setAllProviders', response.providers)
- }
- } catch (error) {
- console.error('Failed to fetch providers', error)
- }
- },
- async refreshProviders({ commit, state }) {
- // if providers are not loaded, do nothing - they will be fetched when required (
- if (!state.providersLoaded) {
- return
- }
-
- try {
- const response = await this.$axios.$get('/api/search/providers')
- if (response?.providers) {
- commit('setAllProviders', response.providers)
- }
- } catch (error) {
- console.error('Failed to refresh providers', error)
- }
+ checkPodcastProviderExists: state => (providerValue) => {
+ return state.podcastProviders.some(p => p.value === providerValue)
}
}
+export const actions = {}
+
export const mutations = {
- setAllProviders(state, providers) {
- state.bookProviders = providers.books || []
- state.podcastProviders = providers.podcasts || []
- state.bookCoverProviders = providers.booksCovers || []
- state.podcastCoverProviders = providers.podcasts || [] // Use same as bookCovers since podcasts use iTunes only
- state.providersLoaded = true
+ addCustomMetadataProvider(state, provider) {
+ if (provider.mediaType === 'book') {
+ if (state.providers.some(p => p.value === provider.slug)) return
+ state.providers.push({
+ text: provider.name,
+ value: provider.slug
+ })
+ } else {
+ if (state.podcastProviders.some(p => p.value === provider.slug)) return
+ state.podcastProviders.push({
+ text: provider.name,
+ value: provider.slug
+ })
+ }
+ },
+ removeCustomMetadataProvider(state, provider) {
+ if (provider.mediaType === 'book') {
+ state.providers = state.providers.filter(p => p.value !== provider.slug)
+ } else {
+ state.podcastProviders = state.podcastProviders.filter(p => p.value !== provider.slug)
+ }
+ },
+ setCustomMetadataProviders(state, providers) {
+ if (!providers?.length) return
+
+ const mediaType = providers[0].mediaType
+ if (mediaType === 'book') {
+ // clear previous values, and add new values to the end
+ state.providers = state.providers.filter((p) => !p.value.startsWith('custom-'))
+ state.providers = [
+ ...state.providers,
+ ...providers.map((p) => ({
+ text: p.name,
+ value: p.slug
+ }))
+ ]
+ } else {
+ // Podcast providers not supported yet
+ }
}
-}
+}
\ No newline at end of file
diff --git a/client/strings/ar.json b/client/strings/ar.json
index e3e13a85..fb13ad52 100644
--- a/client/strings/ar.json
+++ b/client/strings/ar.json
@@ -21,8 +21,7 @@
"ButtonChooseAFolder": "اختر المجلد",
"ButtonChooseFiles": "اختر الملفات",
"ButtonClearFilter": "تصفية الفرز",
- "ButtonClose": "إغلاق",
- "ButtonCloseFeed": "إغلاق الموجز",
+ "ButtonCloseFeed": "إغلاق",
"ButtonCloseSession": "إغلاق الجلسة المفتوحة",
"ButtonCollections": "المجموعات",
"ButtonConfigureScanner": "إعدادات الماسح الضوئي",
@@ -121,13 +120,11 @@
"HeaderAccount": "الحساب",
"HeaderAddCustomMetadataProvider": "إضافة موفر بيانات تعريفية مخصص",
"HeaderAdvanced": "متقدم",
- "HeaderApiKeys": "مفاتيح API",
"HeaderAppriseNotificationSettings": "إعدادات الإشعارات",
"HeaderAudioTracks": "المقاطع الصوتية",
"HeaderAudiobookTools": "أدوات إدارة ملفات الكتب الصوتية",
"HeaderAuthentication": "المصادقة",
"HeaderBackups": "النسخ الاحتياطية",
- "HeaderBulkChapterModal": "أضف فصولاً متعددة",
"HeaderChangePassword": "تغيير كلمة المرور",
"HeaderChapters": "الفصول",
"HeaderChooseAFolder": "اختيار المجلد",
@@ -166,7 +163,6 @@
"HeaderMetadataOrderOfPrecedence": "ترتيب أولوية البيانات الوصفية",
"HeaderMetadataToEmbed": "البيانات الوصفية المراد تضمينها",
"HeaderNewAccount": "حساب جديد",
- "HeaderNewApiKey": "مفتاح API جديد",
"HeaderNewLibrary": "مكتبة جديدة",
"HeaderNotificationCreate": "إنشاء إشعار",
"HeaderNotificationUpdate": "تحديث إشعار",
@@ -200,7 +196,6 @@
"HeaderSettingsExperimental": "ميزات تجريبية",
"HeaderSettingsGeneral": "عام",
"HeaderSettingsScanner": "إعدادات المسح",
- "HeaderSettingsSecurity": "الأمان",
"HeaderSettingsWebClient": "عميل الويب",
"HeaderSleepTimer": "مؤقت النوم",
"HeaderStatsLargestItems": "أكبر العناصر حجماً",
@@ -212,7 +207,6 @@
"HeaderTableOfContents": "جدول المحتويات",
"HeaderTools": "أدوات",
"HeaderUpdateAccount": "تحديث الحساب",
- "HeaderUpdateApiKey": "تحديث مفتاح API",
"HeaderUpdateAuthor": "تحديث المؤلف",
"HeaderUpdateDetails": "تحديث التفاصيل",
"HeaderUpdateLibrary": "تحديث المكتبة",
@@ -242,8 +236,6 @@
"LabelAllUsersExcludingGuests": "جميع المستخدمين باستثناء الضيوف",
"LabelAllUsersIncludingGuests": "جميع المستخدمين بما في ذلك الضيوف",
"LabelAlreadyInYourLibrary": "موجود بالفعل في مكتبتك",
- "LabelApiKeyCreated": "تم إنشاء مفتاح API \"{0}\" بنجاح.",
- "LabelApiKeyCreatedDescription": "تأكد من نسخ مفتاح API الآن، لن تتمكن من رؤيته مرة أخرى.",
"LabelApiToken": "رمز API",
"LabelAppend": "إلحاق",
"LabelAudioBitrate": "معدل بت الصوت (على سبيل المثال 128 كيلو بايت)",
diff --git a/client/strings/da.json b/client/strings/da.json
index ab144497..cb135145 100644
--- a/client/strings/da.json
+++ b/client/strings/da.json
@@ -1001,14 +1001,13 @@
"ToastCollectionItemsAddFailed": "Genstand(e) tilføjet til kollektion fejlet",
"ToastCollectionRemoveSuccess": "Samling fjernet",
"ToastCollectionUpdateSuccess": "Samling opdateret",
- "ToastConnectionNotAvailable": "Forbindelse mislykkedes. Prøv igen senere",
"ToastCoverUpdateFailed": "Cover opdatering fejlede",
- "ToastDateTimeInvalidOrIncomplete": "Dato og tid er ugyldig eller ufærdig",
- "ToastDeleteFileFailed": "Sletning af fil fejlede",
+ "ToastDateTimeInvalidOrIncomplete": "Dato og tid er forkert eller ufærdig",
+ "ToastDeleteFileFailed": "Slet fil fejlede",
"ToastDeleteFileSuccess": "Fil slettet",
- "ToastDeviceAddFailed": "Tilføjelse af enhed Fejlede",
- "ToastDeviceNameAlreadyExists": "E-læser enhed med det navn eksistere allerede",
- "ToastDeviceTestEmailFailed": "Afsendelse af test mail fejlede",
+ "ToastDeviceAddFailed": "Fejlede at tilføje enhed",
+ "ToastDeviceNameAlreadyExists": "Elæser enhed med det navn eksistere allerede",
+ "ToastDeviceTestEmailFailed": "Fejlede at sende test mail",
"ToastDeviceTestEmailSuccess": "Test mail sendt",
"ToastEmailSettingsUpdateSuccess": "Mail indstillinger opdateret",
"ToastEncodeCancelFailed": "Fejlede at afbryde indkodning",
@@ -1018,23 +1017,21 @@
"ToastEpisodeUpdateSuccess": "{0} afsnit opdateret",
"ToastErrorCannotShare": "Kan ikke dele på denne enhed",
"ToastFailedToCreate": "Oprettelsen mislykkedes",
- "ToastFailedToDelete": "Sletning fejlede",
- "ToastFailedToLoadData": "Indlæsning af data fejlede",
+ "ToastFailedToLoadData": "Fejlede at indlæse data",
"ToastFailedToMatch": "Fejlet match",
- "ToastFailedToShare": "Deling fejlede",
+ "ToastFailedToShare": "Fejlet deling",
"ToastFailedToUpdate": "Fejlet opdatering",
- "ToastInvalidImageUrl": "Ugyldig billede URL",
- "ToastInvalidMaxEpisodesToDownload": "Ugyldigt maks afsnit at hente",
- "ToastInvalidUrl": "Ugyldig URL",
- "ToastInvalidUrls": "En eller flere URLer er ugyldige",
- "ToastItemCoverUpdateSuccess": "Omslag opdateret",
- "ToastItemDeletedFailed": "Sletning af genstand fejlede",
+ "ToastInvalidImageUrl": "Forkert billede URL",
+ "ToastInvalidMaxEpisodesToDownload": "Forkert maks afsnit at hente",
+ "ToastInvalidUrl": "Forkert URL",
+ "ToastItemCoverUpdateSuccess": "Varens omslag opdateret",
+ "ToastItemDeletedFailed": "Fejlede at slette genstand",
"ToastItemDeletedSuccess": "Genstand slettet",
- "ToastItemDetailsUpdateSuccess": "Detaljer opdateret",
- "ToastItemMarkedAsFinishedFailed": "Markering som afsluttet mislykkedes",
- "ToastItemMarkedAsFinishedSuccess": "Element markeret som afsluttet",
- "ToastItemMarkedAsNotFinishedFailed": "Markering som ikke afsluttet mislykkedes",
- "ToastItemMarkedAsNotFinishedSuccess": "Element markeret som ikke afsluttet",
+ "ToastItemDetailsUpdateSuccess": "Varedetaljer opdateret",
+ "ToastItemMarkedAsFinishedFailed": "Mislykkedes markering som afsluttet",
+ "ToastItemMarkedAsFinishedSuccess": "Vare markeret som afsluttet",
+ "ToastItemMarkedAsNotFinishedFailed": "Mislykkedes markering som ikke afsluttet",
+ "ToastItemMarkedAsNotFinishedSuccess": "Vare markeret som ikke afsluttet",
"ToastItemUpdateSuccess": "Genstand opdateret",
"ToastLibraryCreateFailed": "Oprettelse af bibliotek mislykkedes",
"ToastLibraryCreateSuccess": "Bibliotek \"{0}\" oprettet",
diff --git a/client/strings/de.json b/client/strings/de.json
index 5acae6d9..2eaebbcc 100644
--- a/client/strings/de.json
+++ b/client/strings/de.json
@@ -1026,8 +1026,6 @@
"ToastCollectionItemsAddFailed": "Das Hinzufügen von Element(en) zur Sammlung ist fehlgeschlagen",
"ToastCollectionRemoveSuccess": "Sammlung entfernt",
"ToastCollectionUpdateSuccess": "Sammlung aktualisiert",
- "ToastConnectionNotAvailable": "Verbindung nicht möglich. Bitte später erneut versuchen",
- "ToastCoverSearchFailed": "Cover-Suche fehlgeschlagen",
"ToastCoverUpdateFailed": "Cover-Update fehlgeschlagen",
"ToastDateTimeInvalidOrIncomplete": "Datum und Zeit sind ungültig oder unvollständig",
"ToastDeleteFileFailed": "Die Datei konnte nicht gelöscht werden",
diff --git a/client/strings/fi.json b/client/strings/fi.json
index 8d224d81..d29ce7f9 100644
--- a/client/strings/fi.json
+++ b/client/strings/fi.json
@@ -355,7 +355,7 @@
"LabelExample": "Esimerkki",
"LabelExpandSeries": "Laajenna sarja",
"LabelExpandSubSeries": "Laajenna alisarja",
- "LabelExplicit": "Sopimaton",
+ "LabelExplicit": "Yksiselitteinen",
"LabelExplicitChecked": "Yksiselitteinen (valittu)",
"LabelExplicitUnchecked": "Ei yksiselitteinen (ei valittu)",
"LabelExportOPML": "Vie OPML",
diff --git a/client/strings/it.json b/client/strings/it.json
index 2354bc0a..a032a3c9 100644
--- a/client/strings/it.json
+++ b/client/strings/it.json
@@ -309,7 +309,6 @@
"LabelDeleteFromFileSystemCheckbox": "Elimina dal file system (togli la spunta per eliminarla solo dal DB)",
"LabelDescription": "Descrizione",
"LabelDeselectAll": "Deseleziona Tutto",
- "LabelDetectedPattern": "Trovato pattern:",
"LabelDevice": "Dispositivo",
"LabelDeviceInfo": "Info dispositivo",
"LabelDeviceIsAvailableTo": "Il dispositivo e disponibile su…",
@@ -378,7 +377,6 @@
"LabelFilterByUser": "Filtro per Utente",
"LabelFindEpisodes": "Trova Episodi",
"LabelFinished": "Finita",
- "LabelFinishedDate": "Finito {0}",
"LabelFolder": "Cartella",
"LabelFolders": "Cartelle",
"LabelFontBold": "Grassetto",
@@ -436,9 +434,8 @@
"LabelLibraryFilterSublistEmpty": "Nessuno {0}",
"LabelLibraryItem": "Elementi della biblioteca",
"LabelLibraryName": "Nome della biblioteca",
- "LabelLibrarySortByProgress": "Progressi: Ultimi aggiornamenti",
- "LabelLibrarySortByProgressFinished": "Progressi: Completati",
- "LabelLibrarySortByProgressStarted": "Progressi: Iniziati",
+ "LabelLibrarySortByProgress": "Aggiornamento dei progressi",
+ "LabelLibrarySortByProgressStarted": "Data di inizio",
"LabelLimit": "Limiti",
"LabelLineSpacing": "Interlinea",
"LabelListenAgain": "Ascolta ancora",
@@ -638,7 +635,6 @@
"LabelStartTime": "Tempo di inizio",
"LabelStarted": "Iniziato",
"LabelStartedAt": "Iniziato al",
- "LabelStartedDate": "Iniziati {0}",
"LabelStatsAudioTracks": "Tracce Audio",
"LabelStatsAuthors": "Autori",
"LabelStatsBestDay": "Giorno migliore",
@@ -1026,8 +1022,6 @@
"ToastCollectionItemsAddFailed": "l'aggiunta dell'elemento(i) alla raccolta non è riuscito",
"ToastCollectionRemoveSuccess": "Collezione rimossa",
"ToastCollectionUpdateSuccess": "Raccolta aggiornata",
- "ToastConnectionNotAvailable": "Connessione non disponibile. Provare più tardi",
- "ToastCoverSearchFailed": "Ricerca Cover fallita",
"ToastCoverUpdateFailed": "Aggiornamento cover fallito",
"ToastDateTimeInvalidOrIncomplete": "Data e ora non sono valide o incomplete",
"ToastDeleteFileFailed": "Impossibile eliminare il file",
diff --git a/client/strings/pl.json b/client/strings/pl.json
index 0b92dc89..98335a21 100644
--- a/client/strings/pl.json
+++ b/client/strings/pl.json
@@ -400,7 +400,7 @@
"LabelHours": "Godziny",
"LabelIcon": "Ikona",
"LabelImageURLFromTheWeb": "Link do obrazu w sieci",
- "LabelInProgress": "W toku",
+ "LabelInProgress": "W trakcie",
"LabelIncludeInTracklist": "Dołącz do listy odtwarzania",
"LabelIncomplete": "Nieukończone",
"LabelInterval": "Interwał",
diff --git a/client/strings/pt-br.json b/client/strings/pt-br.json
index 53b9199e..9604daae 100644
--- a/client/strings/pt-br.json
+++ b/client/strings/pt-br.json
@@ -1,6 +1,6 @@
{
"ButtonAdd": "Adicionar",
- "ButtonAddApiKey": "Adicionar chave de API",
+ "ButtonAddApiKey": "Adicionar Chave API",
"ButtonAddChapters": "Adicionar Capítulos",
"ButtonAddDevice": "Adicionar Dispositivo",
"ButtonAddLibrary": "Adicionar Biblioteca",
@@ -21,7 +21,6 @@
"ButtonChooseAFolder": "Escolha uma pasta",
"ButtonChooseFiles": "Escolha arquivos",
"ButtonClearFilter": "Limpar Filtro",
- "ButtonClose": "Fechar",
"ButtonCloseFeed": "Fechar Feed",
"ButtonCloseSession": "Fechar Sessão Aberta",
"ButtonCollections": "Coleções",
@@ -54,7 +53,7 @@
"ButtonNevermind": "Cancelar",
"ButtonNext": "Próximo",
"ButtonNextChapter": "Próximo Capítulo",
- "ButtonNextItemInQueue": "Próximo Item na Fila",
+ "ButtonNextItemInQueue": "Próximo Item da Fila",
"ButtonOk": "Ok",
"ButtonOpenFeed": "Abrir Feed",
"ButtonOpenManager": "Abrir Gerenciador",
@@ -121,13 +120,10 @@
"HeaderAccount": "Conta",
"HeaderAddCustomMetadataProvider": "Adicionar Provedor de Metadados Personalizado",
"HeaderAdvanced": "Avançado",
- "HeaderApiKeys": "Chaves de API",
"HeaderAppriseNotificationSettings": "Configuração de notificações Apprise",
"HeaderAudioTracks": "Trilhas de áudio",
"HeaderAudiobookTools": "Ferramentas de Gerenciamento de Arquivos de Audiobooks",
"HeaderAuthentication": "Autenticação",
- "HeaderBackups": "Backups",
- "HeaderBulkChapterModal": "Adicionar vários capítulos",
"HeaderChangePassword": "Trocar Senha",
"HeaderChapters": "Capítulos",
"HeaderChooseAFolder": "Escolha uma Pasta",
@@ -140,7 +136,6 @@
"HeaderDetails": "Detalhes",
"HeaderDownloadQueue": "Fila de Download",
"HeaderEbookFiles": "Arquivos Ebook",
- "HeaderEmail": "Email",
"HeaderEmailSettings": "Configurações de Email",
"HeaderEpisodes": "Episódios",
"HeaderEreaderDevices": "Dispositivos Ereader",
@@ -157,8 +152,6 @@
"HeaderLibraryStats": "Estatísticas da Biblioteca",
"HeaderListeningSessions": "Sessões",
"HeaderListeningStats": "Estatísticas",
- "HeaderLogin": "Login",
- "HeaderLogs": "Logs",
"HeaderManageGenres": "Gerenciar Gêneros",
"HeaderManageTags": "Gerenciar Etiquetas",
"HeaderMapDetails": "Designar Detalhes",
@@ -166,23 +159,17 @@
"HeaderMetadataOrderOfPrecedence": "Ordem de Prioridade dos Metadados",
"HeaderMetadataToEmbed": "Metadados a Serem Incluídos",
"HeaderNewAccount": "Nova Conta",
- "HeaderNewApiKey": "Nova chave de API",
"HeaderNewLibrary": "Nova Biblioteca",
- "HeaderNotificationCreate": "Criar Notificação",
- "HeaderNotificationUpdate": "Atualizar Notificação",
"HeaderNotifications": "Notificações",
"HeaderOpenIDConnectAuthentication": "Autenticação via OpenID Connect",
- "HeaderOpenListeningSessions": "Abrir Sessões de Escuta",
"HeaderOpenRSSFeed": "Abrir Feed RSS",
"HeaderOtherFiles": "Outros Arquivos",
"HeaderPasswordAuthentication": "Autenticação por Senha",
"HeaderPermissions": "Permissões",
"HeaderPlayerQueue": "Fila do reprodutor",
- "HeaderPlayerSettings": "Configurações do Reprodutor",
"HeaderPlaylist": "Lista de Reprodução",
"HeaderPlaylistItems": "Itens da lista de reprodução",
"HeaderPodcastsToAdd": "Podcasts para Adicionar",
- "HeaderPresets": "Valores predefinidos",
"HeaderPreviewCover": "Visualização da Capa",
"HeaderRSSFeedGeneral": "Detalhes RSS",
"HeaderRSSFeedIsOpen": "Feed RSS está Aberto",
@@ -191,7 +178,6 @@
"HeaderRemoveEpisodes": "Remover {0} Episódios",
"HeaderSavedMediaProgress": "Progresso da gravação das mídias",
"HeaderSchedule": "Programação",
- "HeaderScheduleEpisodeDownloads": "Programar Download Automático de Episódios",
"HeaderScheduleLibraryScans": "Programar Verificação Automática da Biblioteca",
"HeaderSession": "Sessão",
"HeaderSetBackupSchedule": "Definir Programação de Backup",
@@ -200,8 +186,6 @@
"HeaderSettingsExperimental": "Funcionalidades experimentais",
"HeaderSettingsGeneral": "Geral",
"HeaderSettingsScanner": "Verificador",
- "HeaderSettingsSecurity": "Segurança",
- "HeaderSettingsWebClient": "Cliente Web",
"HeaderSleepTimer": "Timer",
"HeaderStatsLargestItems": "Maiores Itens",
"HeaderStatsLongestItems": "Itens mais longos (hrs)",
@@ -212,7 +196,6 @@
"HeaderTableOfContents": "Sumário",
"HeaderTools": "Ferramentas",
"HeaderUpdateAccount": "Atualizar Conta",
- "HeaderUpdateApiKey": "Atualizar Chave de API",
"HeaderUpdateAuthor": "Atualizar Autor",
"HeaderUpdateDetails": "Atualizar Detalhes",
"HeaderUpdateLibrary": "Atualizar Biblioteca",
@@ -227,7 +210,6 @@
"LabelAccountTypeAdmin": "Administrador",
"LabelAccountTypeGuest": "Convidado",
"LabelAccountTypeUser": "Usuário",
- "LabelActivities": "Atividades",
"LabelActivity": "Atividade",
"LabelAddToCollection": "Adicionar à Coleção",
"LabelAddToCollectionBatch": "Adicionar {0} Livros à Coleção",
@@ -237,20 +219,11 @@
"LabelAddedDate": "Adicionado {0}",
"LabelAdminUsersOnly": "Apenas usuários administradores",
"LabelAll": "Todos",
- "LabelAllEpisodesDownloaded": "Todos os episódios baixados",
"LabelAllUsers": "Todos Usuários",
"LabelAllUsersExcludingGuests": "Todos usuários exceto convidados",
"LabelAllUsersIncludingGuests": "Todos usuários incluindo convidados",
"LabelAlreadyInYourLibrary": "Já na sua biblioteca",
- "LabelApiKeyCreated": "Chave de API \"{0}\" criada com sucesso.",
- "LabelApiKeyCreatedDescription": "Certifique-se de copiar a chave de API agora pois não será possível vê-la novamente.",
- "LabelApiKeyUser": "Agir em nome do usuário",
- "LabelApiKeyUserDescription": "Esta chave de API terá as mesmas permissões que o usuário em nome de quem ela está agindo. Isso aparecerá nos logs como se o usuário estivesse fazendo a solicitação.",
- "LabelApiToken": "Token de API",
"LabelAppend": "Acrescentar",
- "LabelAudioBitrate": "Bitrate de áudio (por exemplo, 128k)",
- "LabelAudioChannels": "Canais de áudio (1 ou 2)",
- "LabelAudioCodec": "Codec de áudio",
"LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (Nome Sobrenome)",
"LabelAuthorLastFirst": "Autor (Sobrenome, Nome)",
@@ -263,31 +236,24 @@
"LabelAutoRegister": "Registrar Automaticamente",
"LabelAutoRegisterDescription": "Registra automaticamente novos usuários após login",
"LabelBackToUser": "Voltar para Usuário",
- "LabelBackupAudioFiles": "Backup dos Arquivos de Áudio",
"LabelBackupLocation": "Localização do Backup",
- "LabelBackupsEnableAutomaticBackups": "Backups automáticos",
+ "LabelBackupsEnableAutomaticBackups": "Ativar backups automáticos",
"LabelBackupsEnableAutomaticBackupsHelp": "Backups salvos em /metadata/backups",
- "LabelBackupsMaxBackupSize": "Tamanho máximo do backup (em GB) (0 para ilimitado)",
+ "LabelBackupsMaxBackupSize": "Tamanho máximo do backup (em GB)",
"LabelBackupsMaxBackupSizeHelp": "Como proteção contra uma configuração incorreta, backups darão erro se excederem o tamanho configurado.",
"LabelBackupsNumberToKeep": "Número de backups para guardar",
"LabelBackupsNumberToKeepHelp": "Apenas 1 backup será removido por vez, então, se já existem mais backups, você deve apagá-los manualmente.",
- "LabelBitrate": "Bitrate",
- "LabelBonus": "Bônus",
"LabelBooks": "Livros",
"LabelButtonText": "Texto do botão",
"LabelByAuthor": "por {0}",
"LabelChangePassword": "Trocar Senha",
"LabelChannels": "Canais",
- "LabelChapterCount": "{0} Capítulos",
"LabelChapterTitle": "Título do Capítulo",
"LabelChapters": "Capítulos",
"LabelChaptersFound": "capítulos encontrados",
"LabelClickForMoreInfo": "Clique para mais informações",
- "LabelClickToUseCurrentValue": "Clique para usar o valor atual",
"LabelClosePlayer": "Fechar Reprodutor",
- "LabelCodec": "Codec",
"LabelCollapseSeries": "Fechar Série",
- "LabelCollapseSubSeries": "Fechar Sub Séries",
"LabelCollection": "Coleção",
"LabelCollections": "Coleções",
"LabelComplete": "Concluído",
@@ -295,21 +261,17 @@
"LabelContinueListening": "Continuar Escutando",
"LabelContinueReading": "Continuar Lendo",
"LabelContinueSeries": "Continuar Série",
- "LabelCorsAllowed": "Origens Permitidas para CORS",
"LabelCover": "Capa",
"LabelCoverImageURL": "URL da Imagem da Capa",
- "LabelCoverProvider": "Provedor de Capas",
"LabelCreatedAt": "Criado em",
"LabelCronExpression": "Expressão para o Cron",
"LabelCurrent": "Atual",
"LabelCurrently": "Atualmente:",
"LabelCustomCronExpression": "Expressão personalizada para o Cron:",
"LabelDatetime": "Data e Hora",
- "LabelDays": "Dias",
"LabelDeleteFromFileSystemCheckbox": "Apagar do sistema de arquivos (desmarcar para remover apenas da base de dados)",
"LabelDescription": "Descrição",
"LabelDeselectAll": "Desmarcar tudo",
- "LabelDetectedPattern": "Padrão detectado:",
"LabelDevice": "Dispositivo",
"LabelDeviceInfo": "Informação do Dispositivo",
"LabelDeviceIsAvailableTo": "Dispositivo está disponível para...",
@@ -319,7 +281,6 @@
"LabelDiscover": "Descobrir",
"LabelDownload": "Download",
"LabelDownloadNEpisodes": "Download de {0} Episódios",
- "LabelDownloadable": "Baixável",
"LabelDuration": "Duração",
"LabelDurationComparisonExactMatch": "(exato)",
"LabelDurationComparisonLonger": "({0} maior)",
@@ -328,7 +289,6 @@
"LabelEbook": "Ebook",
"LabelEbooks": "Ebooks",
"LabelEdit": "Editar",
- "LabelEmail": "Email",
"LabelEmailSettingsFromAddress": "Remetente",
"LabelEmailSettingsRejectUnauthorized": "Rejeitar certificados não autorizados",
"LabelEmailSettingsRejectUnauthorizedHelp": "Desativar a validação de certificados SSL pode expor sua conexão a riscos de segurança, como ataques \"man-in-the-middle\". Desative essa opção apenas se entender suas consequências e se puder confiar no servidor de email ao qual você está se conectando.",
@@ -337,24 +297,11 @@
"LabelEmailSettingsTestAddress": "Endereço de teste",
"LabelEmbeddedCover": "Capa Integrada",
"LabelEnable": "Habilitar",
- "LabelEncodingBackupLocation": "Um backup dos seus arquivos de áudio original será gravado em:",
- "LabelEncodingChaptersNotEmbedded": "Capítulos não são integrados em audiobooks com várias trilhas.",
- "LabelEncodingClearItemCache": "Certifique-se de, periodicamente, apagar os itens do cache.",
- "LabelEncodingFinishedM4B": "O arquivo M4B final será colocado na sua pasta de audiobooks em:",
- "LabelEncodingInfoEmbedded": "Os metadados serão integrados nas trilhas de áudio dentro da sua pasta de audiobooks.",
- "LabelEncodingStartedNavigation": "Assim que a tarefa for iniciada você pode sair dessa página.",
- "LabelEncodingTimeWarning": "A codificação pode durar até 30 minutos.",
- "LabelEncodingWarningAdvancedSettings": "Aviso: não atualize essas configurações se não estiver familiarizado com as opções de codificação do ffmpeg.",
- "LabelEncodingWatcherDisabled": "Se você desabilitou o monitoramento, será necessário fazer uma nova verificação deste audiobook depois.",
"LabelEnd": "Fim",
- "LabelEndOfChapter": "Fim do Capítulo",
"LabelEpisode": "Episódio",
"LabelEpisodeTitle": "Título do Episódio",
"LabelEpisodeType": "Tipo do Episódio",
- "LabelEpisodes": "Episódios",
"LabelExample": "Exemplo",
- "LabelExpired": "Expirado",
- "LabelExpiresNever": "Nunca",
"LabelExplicit": "Explícito",
"LabelExplicitChecked": "Explícito (verificado)",
"LabelExplicitUnchecked": "Não explícito (não verificado)",
@@ -381,11 +328,8 @@
"LabelHardDeleteFile": "Apagar definitivamente",
"LabelHasEbook": "Tem ebook",
"LabelHasSupplementaryEbook": "Tem ebook complementar",
- "LabelHideSubtitles": "Esconder Legendas",
"LabelHighestPriority": "Prioridade mais alta",
- "LabelHost": "Host",
"LabelHour": "Hora",
- "LabelHours": "Horas",
"LabelIcon": "Ícone",
"LabelImageURLFromTheWeb": "URL da imagem na internet",
"LabelInProgress": "Em Andamento",
@@ -400,9 +344,7 @@
"LabelIntervalEvery6Hours": "A cada 6 horas",
"LabelIntervalEveryDay": "Todo dia",
"LabelIntervalEveryHour": "Toda hora",
- "LabelIntervalEveryMinute": "A cada minuto",
"LabelInvert": "Inverter",
- "LabelItem": "Item",
"LabelLanguage": "Idioma",
"LabelLanguageDefaultServer": "Idioma Padrão do Servidor",
"LabelLanguages": "Idiomas",
@@ -411,22 +353,16 @@
"LabelLastSeen": "Visto pela Última Vez",
"LabelLastTime": "Progresso",
"LabelLastUpdate": "Última Atualização",
- "LabelLayout": "Layout",
"LabelLayoutSinglePage": "Uma página",
"LabelLayoutSplitPage": "Página dividida",
"LabelLess": "Menos",
"LabelLibrariesAccessibleToUser": "Bibliotecas Acessíveis ao Usuário",
"LabelLibrary": "Biblioteca",
- "LabelLibraryFilterSublistEmpty": "Sem {0}",
"LabelLibraryItem": "Item da Biblioteca",
"LabelLibraryName": "Nome da Biblioteca",
- "LabelLibrarySortByProgress": "Última Atualização",
- "LabelLibrarySortByProgressFinished": "Concluído",
"LabelLimit": "Limite",
"LabelLineSpacing": "Espaçamento entre linhas",
"LabelListenAgain": "Escutar novamente",
- "LabelLogLevelDebug": "Debug",
- "LabelLogLevelInfo": "Info",
"LabelLogLevelWarn": "Atenção",
"LabelLookForNewEpisodesAfterDate": "Procurar por novos Episódios após essa data",
"LabelLowestPriority": "Prioridade mais baixa",
@@ -439,7 +375,6 @@
"LabelMetadataOrderOfPrecedenceDescription": "Fontes de metadados de alta prioridade terão preferência sobre as fontes de metadados de prioridade baixa",
"LabelMetadataProvider": "Fonte de Metadados",
"LabelMinute": "Minuto",
- "LabelMinutes": "Minutos",
"LabelMissing": "Ausente",
"LabelMissingEbook": "Ebook não existe",
"LabelMissingSupplementaryEbook": "Ebook complementar não existe",
@@ -455,7 +390,6 @@
"LabelNewestAuthors": "Novos Autores",
"LabelNewestEpisodes": "Episódios mais recentes",
"LabelNextBackupDate": "Data do próximo backup",
- "LabelNextChapters": "Próximo capítulo será:",
"LabelNextScheduledRun": "Próxima execução programada",
"LabelNoCustomMetadataProviders": "Não existem fontes de metadados customizados",
"LabelNoEpisodesSelected": "Nenhum episódio selecionado",
@@ -478,10 +412,8 @@
"LabelOpenIDGroupClaimDescription": "Nome do claim OpenID contendo a lista de grupos do usuário, normalmente chamada de
groups.
Se configurada, a aplicação atribuirá automaticamente os perfis com base na participação do usuário nos grupos, contanto que os nomes desses grupos no claim, sem distinção entre maiúsculas e minúsculas, sejam 'admin', 'user' ou 'guest'. O claim deve conter uma lista e, se o usuário pertencer a múltiplos grupos, a aplicação atribuirá o perfil correspondendo ao maior nível de acesso. Se não houver correspondência a qualquer grupo, o acesso será negado.",
"LabelOpenRSSFeed": "Abrir Feed RSS",
"LabelOverwrite": "Sobrescrever",
- "LabelPaginationPageXOfY": "Página {0} de {1}",
"LabelPassword": "Senha",
"LabelPath": "Caminho",
- "LabelPermanent": "Permanente",
"LabelPermissionsAccessAllLibraries": "Pode Acessar Todas Bibliotecas",
"LabelPermissionsAccessAllTags": "Pode Acessar Todas as Etiquetas",
"LabelPermissionsAccessExplicitContent": "Pode Acessar Conteúdos Explícitos",
@@ -492,12 +424,9 @@
"LabelPersonalYearReview": "Sua Retrospectiva Anual ({0})",
"LabelPhotoPathURL": "Caminho/URL para Foto",
"LabelPlayMethod": "Método de Reprodução",
- "LabelPlayerChapterNumberMarker": "{0} de {1}",
"LabelPlaylists": "Listas de Reprodução",
- "LabelPodcast": "Podcast",
"LabelPodcastSearchRegion": "Região de busca do podcast",
"LabelPodcastType": "Tipo de Podcast",
- "LabelPodcasts": "Podcasts",
"LabelPort": "Porta",
"LabelPrefixesToIgnore": "Prefixos para Ignorar (sem distinção entre maiúsculas e minúsculas)",
"LabelPreventIndexing": "Evitar que o seu feed seja indexado pelos diretórios de podcast do iTunes e Google",
@@ -506,16 +435,14 @@
"LabelProvider": "Fonte",
"LabelPubDate": "Data de Publicação",
"LabelPublishYear": "Ano de Publicação",
- "LabelPublishedDate": "Publicado {0}",
"LabelPublisher": "Editora",
"LabelPublishers": "Editoras",
"LabelRSSFeedCustomOwnerEmail": "E-mail do dono personalizado",
"LabelRSSFeedCustomOwnerName": "Nome do dono personalizado",
- "LabelRSSFeedOpen": "Feed de RSS Aberto",
+ "LabelRSSFeedOpen": "Feed RSS Aberto",
"LabelRSSFeedPreventIndexing": "Impedir Indexação",
"LabelRSSFeedSlug": "Slug do Feed RSS",
"LabelRSSFeedURL": "URL do Feed RSS",
- "LabelRandomly": "Aleatoriamente",
"LabelRead": "Lido",
"LabelReadAgain": "Ler novamente",
"LabelReadEbookWithoutProgress": "Ler ebook sem armazenar progresso",
@@ -548,8 +475,6 @@
"LabelSettingsBookshelfViewHelp": "Aparência esqueomorfa com prateleiras de madeira",
"LabelSettingsChromecastSupport": "Suporte ao Chromecast",
"LabelSettingsDateFormat": "Formato de data",
- "LabelSettingsEnableWatcher": "Monitorar automaticamente alterações nas bibliotecas",
- "LabelSettingsEnableWatcherForLibrary": "Monitorar automaticamente alterações na biblioteca",
"LabelSettingsEnableWatcherHelp": "Ativa o acréscimo/atualização de itens quando forem detectadas mudanças no arquivo. *Requer reiniciar o servidor",
"LabelSettingsEpubsAllowScriptedContent": "Permitir scripts em epubs",
"LabelSettingsEpubsAllowScriptedContentHelp": "Permitir que arquivos epub executem scripts. É recomendado manter essa configuração desativada, a não ser que confie na fonte dos arquivos epub.",
@@ -578,14 +503,10 @@
"LabelSettingsStoreMetadataWithItem": "Armazenar metadados com o item",
"LabelSettingsStoreMetadataWithItemHelp": "Por padrão os arquivos de metadados são armazenados em /metadata/items. Ao ativar essa configuração os arquivos de metadados serão armazenadas nas pastas dos itens na sua biblioteca",
"LabelSettingsTimeFormat": "Formato da Tempo",
- "LabelShare": "Compartilhar",
- "LabelShareURL": "Compartilhar URL",
"LabelShowAll": "Exibir Todos",
"LabelShowSeconds": "Exibir segundos",
- "LabelShowSubtitles": "Mostrar Legendas",
"LabelSize": "Tamanho",
"LabelSleepTimer": "Timer",
- "LabelSlug": "Slug",
"LabelStart": "Iniciar",
"LabelStartTime": "Horário do Início",
"LabelStarted": "Iniciado",
@@ -613,17 +534,12 @@
"LabelTagsNotAccessibleToUser": "Etiquetas não Acessíveis Usuário",
"LabelTasks": "Tarefas em Execuçào",
"LabelTextEditorBulletedList": "Lista com marcadores",
- "LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "Lista numerada",
"LabelTextEditorUnlink": "Remover link",
"LabelTheme": "Tema",
"LabelThemeDark": "Escuro",
"LabelThemeLight": "Claro",
- "LabelThemeSepia": "Sépia",
"LabelTimeBase": "Base de tempo",
- "LabelTimeDurationXHours": "{0} horas",
- "LabelTimeDurationXMinutes": "{0} minutos",
- "LabelTimeDurationXSeconds": "{0} segundos",
"LabelTimeListened": "Tempo de escuta",
"LabelTimeListenedToday": "Tempo de escuta hoje",
"LabelTimeRemaining": "{0} restantes",
@@ -643,7 +559,6 @@
"LabelTracksMultiTrack": "Várias trilhas",
"LabelTracksNone": "Sem trilha",
"LabelTracksSingleTrack": "Trilha única",
- "LabelTrailer": "Trailer",
"LabelType": "Tipo",
"LabelUnabridged": "Não Abreviada",
"LabelUndo": "Desfazer",
@@ -665,18 +580,15 @@
"LabelViewBookmarks": "Ver marcadores",
"LabelViewChapters": "Ver capítulos",
"LabelViewQueue": "Ver fila do reprodutor",
- "LabelVolume": "Volume",
"LabelWeekdaysToRun": "Dias da semana para executar",
- "LabelXBooks": "{0} livros",
- "LabelXItems": "{0} itens",
- "LabelYearReviewHide": "Ocultar Retrospectiva",
- "LabelYearReviewShow": "Exibir Retrospectiva",
+ "LabelYearReviewHide": "Ocultar Retrospectiva Anual",
+ "LabelYearReviewShow": "Exibir Retrospectiva Anual",
"LabelYourAudiobookDuration": "Duração do seu audiobook",
"LabelYourBookmarks": "Seus Marcadores",
"LabelYourPlaylists": "Suas Listas de Reprodução",
"LabelYourProgress": "Seu Progresso",
"MessageAddToPlayerQueue": "Adicionar à lista do reprodutor",
- "MessageAppriseDescription": "Para utilizar essa funcionalidade é preciso ter uma instância da
API do Apprise em execução ou uma API que possa tratar esses mesmos chamados.
A URL da API do Apprise deve conter o caminho completo da URL para enviar as notificações. Ex: se a sua instância da API estiver em
http://192.168.1.1:8337 você deve utilizar
http://192.168.1.1:8337/notify.",
+ "MessageAppriseDescription": "Para utilizar essa funcionalidade é preciso ter uma instância da
API do Apprise em execução ou uma api que possa tratar esses mesmos chamados.
A URL da API do Apprise deve conter o caminho completo da URL para enviar as notificações. Ex: se a sua instância da API estiver em
http://192.168.1.1:8337 você deve utilizar
http://192.168.1.1:8337/notify.",
"MessageBackupsDescription": "Backups incluem usuários, progresso dos usuários, detalhes dos itens da biblioteca, configurações do servidor e imagens armazenadas em
/metadata/items &
/metadata/authors. Backups
não incluem quaisquer arquivos armazenados nas pastas da sua biblioteca.",
"MessageBatchQuickMatchDescription": "Consulta Rápida tentará adicionar capas e metadados ausentes para os itens selecionados. Ative as opções abaixo para permitir que a Consulta Rápida sobrescreva capas e/ou metadados existentes.",
"MessageBookshelfNoCollections": "Você ainda não criou coleções",
@@ -731,8 +643,8 @@
"MessageForceReScanDescription": "verificará todos os arquivos, como uma verificação nova. Etiquetas ID3 de arquivos de áudio, arquivos OPF e arquivos de texto serão tratados como novos.",
"MessageImportantNotice": "Aviso Importante!",
"MessageInsertChapterBelow": "Inserir capítulo abaixo",
- "MessageItemsSelected": "{0} itens selecionados",
- "MessageItemsUpdated": "{0} itens atualizados",
+ "MessageItemsSelected": "{0} Itens Selecionados",
+ "MessageItemsUpdated": "{0} Itens Atualizados",
"MessageJoinUsOn": "Junte-se a nós",
"MessageLoading": "Carregando...",
"MessageLoadingFolders": "Carregando pastas...",
@@ -780,7 +692,6 @@
"MessagePlayChapter": "Escutar o início do capítulo",
"MessagePlaylistCreateFromCollection": "Criar uma lista de reprodução a partir da coleção",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast não tem uma URL do feed RSS para ser usada na consulta",
- "MessagePodcastSearchField": "Digite um termo para a busca ou a URL de um feed RSS",
"MessageQuickMatchDescription": "Preenche detalhes vazios do item & capa com o primeiro resultado de '{0}'. Não sobrescreve detalhes a não ser que a configuração 'Preferir metadados consultados' do servidor esteja ativa.",
"MessageRemoveChapter": "Remover capítulo",
"MessageRemoveEpisodes": "Remover {0} episódio(s)",
@@ -795,7 +706,6 @@
"MessageServerCouldNotBeReached": "Não foi possível estabelecer conexão com o servidor",
"MessageSetChaptersFromTracksDescription": "Definir os capítulos usando cada arquivo de áudio como um capítulo e o nome do arquivo como o título do capítulo",
"MessageStartPlaybackAtTime": "Iniciar a reprodução de \"{0}\" em {1}?",
- "MessageTaskFailed": "Falhou",
"MessageThinking": "Pensando...",
"MessageUploaderItemFailed": "Falha no upload",
"MessageUploaderItemSuccess": "Upload realizado!",
@@ -813,19 +723,12 @@
"NoteUploaderFoldersWithMediaFiles": "Pastas com arquivos de mídia serão tratadas como itens de biblioteca distintos.",
"NoteUploaderOnlyAudioFiles": "Ao subir apenas arquivos de áudio, cada arquivo será tratado como um audiobook distinto.",
"NoteUploaderUnsupportedFiles": "Arquivos não suportados serão ignorados. Ao escolher ou arrastar uma pasta, outros arquivos que não estão em uma pasta dentro do item serão ignorados.",
- "PlaceholderBulkChapterInput": "Digite o título de um capítulo ou use uma numeração (por exemplo, 'Episódio 1', 'Capítulo 10', '1.')",
"PlaceholderNewCollection": "Novo nome da coleção",
"PlaceholderNewFolderPath": "Novo caminho para a pasta",
"PlaceholderNewPlaylist": "Novo nome da lista de reprodução",
"PlaceholderSearch": "Buscar..",
"PlaceholderSearchEpisode": "Buscar Episódio..",
- "StatsAuthorsAdded": "autores adicionados",
- "StatsBooksAdded": "livros adicionados",
- "StatsBooksFinished": "livros concluídos",
- "StatsTopAuthor": "TOP AUTOR",
- "StatsTopAuthors": "TOP AUTORES",
"ToastAccountUpdateSuccess": "Conta atualizada",
- "ToastAppriseUrlRequired": "É preciso digitar uma URL Apprise",
"ToastAuthorImageRemoveSuccess": "Imagem do autor removida",
"ToastAuthorUpdateMerged": "Autor combinado",
"ToastAuthorUpdateSuccess": "Autor atualizado",
@@ -842,7 +745,6 @@
"ToastBookmarkCreateFailed": "Falha ao criar marcador",
"ToastBookmarkCreateSuccess": "Marcador adicionado",
"ToastBookmarkRemoveSuccess": "Marcador removido",
- "ToastBulkChapterInvalidCount": "Digite um número entre 1 e 150",
"ToastCachePurgeFailed": "Falha ao apagar o cache",
"ToastCachePurgeSuccess": "Cache apagado com sucesso",
"ToastChaptersHaveErrors": "Capítulos com erro",
@@ -865,7 +767,6 @@
"ToastLibraryScanFailedToStart": "Falha ao iniciar verificação",
"ToastLibraryScanStarted": "Verificação da biblioteca iniciada",
"ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" atualizada",
- "ToastNewUserUsernameError": "Digite um nome de usuário",
"ToastPlaylistCreateFailed": "Falha ao criar lista de reprodução",
"ToastPlaylistCreateSuccess": "Lista de reprodução criada",
"ToastPlaylistRemoveSuccess": "Lista de reprodução removida",
@@ -889,6 +790,5 @@
"ToastSortingPrefixesEmptyError": "É preciso ter pelo menos um prefixo de ordenação",
"ToastSortingPrefixesUpdateSuccess": "Prefixos de ordenação atualizados ({0} item(ns))",
"ToastUserDeleteFailed": "Falha ao apagar usuário",
- "ToastUserDeleteSuccess": "Usuário apagado",
- "ToastUserRootRequireName": "É preciso entrar com um nome de usuário root"
+ "ToastUserDeleteSuccess": "Usuário apagado"
}
diff --git a/server/controllers/LibraryController.js b/server/controllers/LibraryController.js
index 55ef4569..e63441f0 100644
--- a/server/controllers/LibraryController.js
+++ b/server/controllers/LibraryController.js
@@ -221,11 +221,13 @@ class LibraryController {
const includeArray = (req.query.include || '').split(',')
if (includeArray.includes('filterdata')) {
const filterdata = await libraryFilters.getFilterData(req.library.mediaType, req.library.id)
+ const customMetadataProviders = await Database.customMetadataProviderModel.getForClientByMediaType(req.library.mediaType)
return res.json({
filterdata,
issues: filterdata.numIssues,
numUserPlaylists: await Database.playlistModel.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id),
+ customMetadataProviders,
library: req.library.toOldJSON()
})
}
diff --git a/server/controllers/SearchController.js b/server/controllers/SearchController.js
index f6f0ba47..bb3382f7 100644
--- a/server/controllers/SearchController.js
+++ b/server/controllers/SearchController.js
@@ -4,29 +4,7 @@ const BookFinder = require('../finders/BookFinder')
const PodcastFinder = require('../finders/PodcastFinder')
const AuthorFinder = require('../finders/AuthorFinder')
const Database = require('../Database')
-const { isValidASIN, getQueryParamAsString, ValidationError, NotFoundError } = require('../utils')
-
-// Provider name mappings for display purposes
-const providerMap = {
- all: 'All',
- best: 'Best',
- google: 'Google Books',
- itunes: 'iTunes',
- openlibrary: 'Open Library',
- fantlab: 'FantLab.ru',
- audiobookcovers: 'AudiobookCovers.com',
- audible: 'Audible.com',
- 'audible.ca': 'Audible.ca',
- 'audible.uk': 'Audible.co.uk',
- 'audible.au': 'Audible.com.au',
- 'audible.fr': 'Audible.fr',
- 'audible.de': 'Audible.de',
- 'audible.jp': 'Audible.co.jp',
- 'audible.it': 'Audible.it',
- 'audible.in': 'Audible.in',
- 'audible.es': 'Audible.es',
- audnexus: 'Audnexus'
-}
+const { isValidASIN } = require('../utils')
/**
* @typedef RequestUserObject
@@ -38,44 +16,6 @@ const providerMap = {
class SearchController {
constructor() {}
- /**
- * Fetches a library item by ID
- * @param {string} id - Library item ID
- * @param {string} methodName - Name of the calling method for logging
- * @returns {Promise
}
- */
- static async fetchLibraryItem(id) {
- const libraryItem = await Database.libraryItemModel.getExpandedById(id)
- if (!libraryItem) {
- throw new NotFoundError(`library item "${id}" not found`)
- }
- return libraryItem
- }
-
- /**
- * Maps custom metadata providers to standardized format
- * @param {Array} providers - Array of custom provider objects
- * @returns {Array<{value: string, text: string}>}
- */
- static mapCustomProviders(providers) {
- return providers.map((provider) => ({
- value: provider.getSlug(),
- text: provider.name
- }))
- }
-
- /**
- * Static helper method to format provider for client (for use in array methods)
- * @param {string} providerValue - Provider identifier
- * @returns {{value: string, text: string}}
- */
- static formatProvider(providerValue) {
- return {
- value: providerValue,
- text: providerMap[providerValue] || providerValue
- }
- }
-
/**
* GET: /api/search/books
*
@@ -83,25 +23,19 @@ class SearchController {
* @param {Response} res
*/
async findBooks(req, res) {
- try {
- const query = req.query
- const provider = getQueryParamAsString(query, 'provider', 'google')
- const title = getQueryParamAsString(query, 'title', '')
- const author = getQueryParamAsString(query, 'author', '')
- const id = getQueryParamAsString(query, 'id', '', true)
+ const id = req.query.id
+ const libraryItem = await Database.libraryItemModel.getExpandedById(id)
+ const provider = req.query.provider || 'google'
+ const title = req.query.title || ''
+ const author = req.query.author || ''
- // Fetch library item
- const libraryItem = await SearchController.fetchLibraryItem(id)
-
- const results = await BookFinder.search(libraryItem, provider, title, author)
- res.json(results)
- } catch (error) {
- Logger.error(`[SearchController] findBooks: ${error.message}`)
- if (error instanceof ValidationError || error instanceof NotFoundError) {
- return res.status(error.status).json({ error: error.message })
- }
- return res.status(500).json({ error: 'Internal server error' })
+ if (typeof provider !== 'string' || typeof title !== 'string' || typeof author !== 'string') {
+ Logger.error(`[SearchController] findBooks: Invalid request query params`)
+ return res.status(400).send('Invalid request query params')
}
+
+ const results = await BookFinder.search(libraryItem, provider, title, author)
+ res.json(results)
}
/**
@@ -111,24 +45,20 @@ class SearchController {
* @param {Response} res
*/
async findCovers(req, res) {
- try {
- const query = req.query
- const podcast = query.podcast === '1' || query.podcast === 1
- const title = getQueryParamAsString(query, 'title', '', true)
- const author = getQueryParamAsString(query, 'author', '')
- const provider = getQueryParamAsString(query, 'provider', 'google')
+ const query = req.query
+ const podcast = query.podcast == 1
- let results = null
- if (podcast) results = await PodcastFinder.findCovers(title)
- else results = await BookFinder.findCovers(provider, title, author)
- res.json({ results })
- } catch (error) {
- Logger.error(`[SearchController] findCovers: ${error.message}`)
- if (error instanceof ValidationError) {
- return res.status(error.status).json({ error: error.message })
- }
- return res.status(500).json({ error: 'Internal server error' })
+ if (!query.title || typeof query.title !== 'string') {
+ Logger.error(`[SearchController] findCovers: Invalid title sent in query`)
+ return res.sendStatus(400)
}
+
+ let results = null
+ if (podcast) results = await PodcastFinder.findCovers(query.title)
+ else results = await BookFinder.findCovers(query.provider || 'google', query.title, query.author || '')
+ res.json({
+ results
+ })
}
/**
@@ -139,42 +69,34 @@ class SearchController {
* @param {Response} res
*/
async findPodcasts(req, res) {
- try {
- const query = req.query
- const term = getQueryParamAsString(query, 'term', '', true)
- const country = getQueryParamAsString(query, 'country', 'us')
-
- const results = await PodcastFinder.search(term, { country })
- res.json(results)
- } catch (error) {
- Logger.error(`[SearchController] findPodcasts: ${error.message}`)
- if (error instanceof ValidationError) {
- return res.status(error.status).json({ error: error.message })
- }
- return res.status(500).json({ error: 'Internal server error' })
+ const term = req.query.term
+ const country = req.query.country || 'us'
+ if (!term) {
+ Logger.error('[SearchController] Invalid request query param "term" is required')
+ return res.status(400).send('Invalid request query param "term" is required')
}
+
+ const results = await PodcastFinder.search(term, {
+ country
+ })
+ res.json(results)
}
/**
* GET: /api/search/authors
- * Note: This endpoint is not currently used in the web client.
*
* @param {RequestWithUser} req
* @param {Response} res
*/
async findAuthor(req, res) {
- try {
- const query = getQueryParamAsString(req.query, 'q', '', true)
-
- const author = await AuthorFinder.findAuthorByName(query)
- res.json(author)
- } catch (error) {
- Logger.error(`[SearchController] findAuthor: ${error.message}`)
- if (error instanceof ValidationError) {
- return res.status(error.status).json({ error: error.message })
- }
- return res.status(500).json({ error: 'Internal server error' })
+ const query = req.query.q
+ if (!query || typeof query !== 'string') {
+ Logger.error(`[SearchController] findAuthor: Invalid query param`)
+ return res.status(400).send('Invalid query param')
}
+
+ const author = await AuthorFinder.findAuthorByName(query)
+ res.json(author)
}
/**
@@ -184,55 +106,16 @@ class SearchController {
* @param {Response} res
*/
async findChapters(req, res) {
- try {
- const query = req.query
- const asin = getQueryParamAsString(query, 'asin', '', true)
- const region = getQueryParamAsString(req.query.region, 'us').toLowerCase()
-
- if (!isValidASIN(asin.toUpperCase())) throw new ValidationError('asin', 'is invalid')
-
- const chapterData = await BookFinder.findChapters(asin, region)
- if (!chapterData) {
- return res.json({ error: 'Chapters not found', stringKey: 'MessageChaptersNotFound' })
- }
- res.json(chapterData)
- } catch (error) {
- Logger.error(`[SearchController] findChapters: ${error.message}`)
- if (error instanceof ValidationError) {
- if (error.paramName === 'asin') {
- return res.json({ error: 'Invalid ASIN', stringKey: 'MessageInvalidAsin' })
- }
- if (error.paramName === 'region') {
- return res.json({ error: 'Invalid region', stringKey: 'MessageInvalidRegion' })
- }
- }
- return res.status(500).json({ error: 'Internal server error' })
+ const asin = req.query.asin
+ if (!isValidASIN(asin.toUpperCase())) {
+ return res.json({ error: 'Invalid ASIN', stringKey: 'MessageInvalidAsin' })
}
- }
-
- /**
- * GET: /api/search/providers
- * Get all available metadata providers
- *
- * @param {RequestWithUser} req
- * @param {Response} res
- */
- async getAllProviders(req, res) {
- const customProviders = await Database.customMetadataProviderModel.findAll()
-
- const customBookProviders = customProviders.filter((p) => p.mediaType === 'book')
- const customPodcastProviders = customProviders.filter((p) => p.mediaType === 'podcast')
-
- const bookProviders = BookFinder.providers.filter((p) => p !== 'audiobookcovers')
-
- // Build minimized payload with custom providers merged in
- const providers = {
- books: [...bookProviders.map((p) => SearchController.formatProvider(p)), ...SearchController.mapCustomProviders(customBookProviders)],
- booksCovers: [SearchController.formatProvider('best'), ...BookFinder.providers.map((p) => SearchController.formatProvider(p)), ...SearchController.mapCustomProviders(customBookProviders), SearchController.formatProvider('all')],
- podcasts: [SearchController.formatProvider('itunes'), ...SearchController.mapCustomProviders(customPodcastProviders)]
+ const region = (req.query.region || 'us').toLowerCase()
+ const chapterData = await BookFinder.findChapters(asin, region)
+ if (!chapterData) {
+ return res.json({ error: 'Chapters not found', stringKey: 'MessageChaptersNotFound' })
}
-
- res.json({ providers })
+ res.json(chapterData)
}
}
module.exports = new SearchController()
diff --git a/server/finders/BookFinder.js b/server/finders/BookFinder.js
index fe1a6102..a6a6b07e 100644
--- a/server/finders/BookFinder.js
+++ b/server/finders/BookFinder.js
@@ -385,11 +385,6 @@ class BookFinder {
if (!title) return books
- // Truncate excessively long inputs to prevent ReDoS attacks
- const MAX_INPUT_LENGTH = 500
- title = title.substring(0, MAX_INPUT_LENGTH)
- author = author?.substring(0, MAX_INPUT_LENGTH) || author
-
const isTitleAsin = isValidASIN(title.toUpperCase())
let actualTitleQuery = title
@@ -407,8 +402,7 @@ class BookFinder {
let authorCandidates = new BookFinder.AuthorCandidates(cleanAuthor, this.audnexus)
// Remove underscores and parentheses with their contents, and replace with a separator
- // Use negated character classes to prevent ReDoS vulnerability (input length validated at entry point)
- const cleanTitle = title.replace(/\[[^\]]*\]|\([^)]*\)|{[^}]*}|_/g, ' - ')
+ const cleanTitle = title.replace(/\[.*?\]|\(.*?\)|{.*?}|_/g, ' - ')
// Split title into hypen-separated parts
const titleParts = cleanTitle.split(/ - | -|- /)
for (const titlePart of titleParts) authorCandidates.add(titlePart)
@@ -674,9 +668,7 @@ function cleanTitleForCompares(title, keepSubtitle = false) {
let stripped = keepSubtitle ? title : stripSubtitle(title)
// Remove text in paranthesis (i.e. "Ender's Game (Ender's Saga)" becomes "Ender's Game")
- // Use negated character class to prevent ReDoS vulnerability (input length validated at entry point)
- let cleaned = stripped.replace(/\([^)]*\)/g, '') // Remove parenthetical content
- cleaned = cleaned.replace(/\s+/g, ' ').trim() // Clean up any resulting multiple spaces
+ let cleaned = stripped.replace(/ *\([^)]*\) */g, '')
// Remove single quotes (i.e. "Ender's Game" becomes "Enders Game")
cleaned = cleaned.replace(/'/g, '')
diff --git a/server/finders/PodcastFinder.js b/server/finders/PodcastFinder.js
index 40d6a5a0..abaf02ac 100644
--- a/server/finders/PodcastFinder.js
+++ b/server/finders/PodcastFinder.js
@@ -7,9 +7,9 @@ class PodcastFinder {
}
/**
- *
- * @param {string} term
- * @param {{country:string}} options
+ *
+ * @param {string} term
+ * @param {{country:string}} options
* @returns {Promise}
*/
async search(term, options = {}) {
@@ -20,16 +20,12 @@ class PodcastFinder {
return results
}
- /**
- * @param {string} term
- * @returns {Promise}
- */
async findCovers(term) {
if (!term) return null
Logger.debug(`[iTunes] Searching for podcast covers with term "${term}"`)
- const results = await this.iTunesApi.searchPodcasts(term)
+ var results = await this.iTunesApi.searchPodcasts(term)
if (!results) return []
- return results.map((r) => r.cover).filter((r) => r)
+ return results.map(r => r.cover).filter(r => r)
}
}
-module.exports = new PodcastFinder()
+module.exports = new PodcastFinder()
\ No newline at end of file
diff --git a/server/managers/CoverSearchManager.js b/server/managers/CoverSearchManager.js
index 19317676..ddcaa23d 100644
--- a/server/managers/CoverSearchManager.js
+++ b/server/managers/CoverSearchManager.js
@@ -224,9 +224,6 @@ class CoverSearchManager {
if (!Array.isArray(results)) return covers
results.forEach((result) => {
- if (typeof result === 'string') {
- covers.push(result)
- }
if (result.covers && Array.isArray(result.covers)) {
covers.push(...result.covers)
}
diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js
index db04bf5e..6446ecc8 100644
--- a/server/routers/ApiRouter.js
+++ b/server/routers/ApiRouter.js
@@ -283,7 +283,6 @@ class ApiRouter {
this.router.get('/search/podcast', SearchController.findPodcasts.bind(this))
this.router.get('/search/authors', SearchController.findAuthor.bind(this))
this.router.get('/search/chapters', SearchController.findChapters.bind(this))
- this.router.get('/search/providers', SearchController.getAllProviders.bind(this))
//
// Cache Routes (Admin and up)
diff --git a/server/utils/index.js b/server/utils/index.js
index c7700a78..36962027 100644
--- a/server/utils/index.js
+++ b/server/utils/index.js
@@ -277,57 +277,3 @@ module.exports.timestampToSeconds = (timestamp) => {
}
return null
}
-
-class ValidationError extends Error {
- constructor(paramName, message, status = 400) {
- super(`Query parameter "${paramName}" ${message}`)
- this.name = 'ValidationError'
- this.paramName = paramName
- this.status = status
- }
-}
-module.exports.ValidationError = ValidationError
-
-class NotFoundError extends Error {
- constructor(message, status = 404) {
- super(message)
- this.name = 'NotFoundError'
- this.status = status
- }
-}
-module.exports.NotFoundError = NotFoundError
-
-/**
- * Safely extracts a query parameter as a string, rejecting arrays to prevent type confusion
- * Express query parameters can be arrays if the same parameter appears multiple times
- * @example ?author=Smith => "Smith"
- * @example ?author=Smith&author=Jones => throws error
- *
- * @param {Object} query - Query object
- * @param {string} paramName - Parameter name
- * @param {string} defaultValue - Default value if undefined/null
- * @param {boolean} required - Whether the parameter is required
- * @param {number} maxLength - Optional maximum length (defaults to 10000 to prevent ReDoS attacks)
- * @returns {string} String value
- * @throws {ValidationError} If value is an array
- * @throws {ValidationError} If value is too long
- * @throws {ValidationError} If value is required but not provided
- */
-module.exports.getQueryParamAsString = (query, paramName, defaultValue = '', required = false, maxLength = 1000) => {
- const value = query[paramName]
- if (value === undefined || value === null) {
- if (required) {
- throw new ValidationError(paramName, 'is required')
- }
- return defaultValue
- }
- // Explicitly reject arrays to prevent type confusion
- if (Array.isArray(value)) {
- throw new ValidationError(paramName, 'is an array')
- }
- // Reject excessively long strings to prevent ReDoS attacks
- if (typeof value === 'string' && value.length > maxLength) {
- throw new ValidationError(paramName, 'is too long')
- }
- return String(value)
-}