mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-11-24 20:05:41 +01:00
Merge branch 'advplyr:master' into #4584-sort-options-for-narrators
This commit is contained in:
commit
0a963b4abc
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@ -47,7 +47,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@ -60,7 +60,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v2
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
@ -73,6 +73,6 @@ jobs:
|
|||||||
# ./location_of_script_within_repo/buildscript.sh
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2
|
uses: github/codeql-action/analyze@v3
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
category: '/language:${{matrix.language}}'
|
||||||
|
|||||||
@ -88,7 +88,7 @@ export default {
|
|||||||
},
|
},
|
||||||
providers() {
|
providers() {
|
||||||
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
|
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
|
||||||
return this.$store.state.scanners.providers
|
return this.$store.state.scanners.bookProviders
|
||||||
},
|
},
|
||||||
libraryProvider() {
|
libraryProvider() {
|
||||||
return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google'
|
return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google'
|
||||||
@ -96,6 +96,9 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
init() {
|
init() {
|
||||||
|
// Fetch providers when modal is shown
|
||||||
|
this.$store.dispatch('scanners/fetchProviders')
|
||||||
|
|
||||||
// If we don't have a set provider (first open of dialog) or we've switched library, set
|
// If we don't have a set provider (first open of dialog) or we've switched library, set
|
||||||
// the selected provider to the current library default provider
|
// the selected provider to the current library default provider
|
||||||
if (!this.options.provider || this.lastUsedLibrary != this.currentLibraryId) {
|
if (!this.options.provider || this.lastUsedLibrary != this.currentLibraryId) {
|
||||||
@ -127,8 +130,7 @@ export default {
|
|||||||
this.show = false
|
this.show = false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
mounted() {}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -133,8 +133,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
providers() {
|
providers() {
|
||||||
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
|
if (this.isPodcast) return this.$store.state.scanners.podcastCoverProviders
|
||||||
return [{ text: 'Best', value: 'best' }, ...this.$store.state.scanners.providers, ...this.$store.state.scanners.coverOnlyProviders, { text: 'All', value: 'all' }]
|
return this.$store.state.scanners.bookCoverProviders
|
||||||
},
|
},
|
||||||
searchTitleLabel() {
|
searchTitleLabel() {
|
||||||
if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN
|
if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN
|
||||||
@ -438,6 +438,8 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
// Setup socket listeners when component is mounted
|
// Setup socket listeners when component is mounted
|
||||||
this.addSocketListeners()
|
this.addSocketListeners()
|
||||||
|
// Fetch providers if not already loaded
|
||||||
|
this.$store.dispatch('scanners/fetchProviders')
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
// Cancel any ongoing search when component is destroyed
|
// Cancel any ongoing search when component is destroyed
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<div id="match-wrapper" class="w-full h-full overflow-hidden px-2 md:px-4 py-4 md:py-6 relative">
|
<div id="match-wrapper" class="w-full h-full overflow-hidden px-2 md:px-4 py-4 md:py-6 relative">
|
||||||
<form @submit.prevent="submitSearch">
|
<form @submit.prevent="submitSearch">
|
||||||
<div class="flex flex-wrap md:flex-nowrap items-center justify-start -mx-1">
|
<div class="flex flex-wrap md:flex-nowrap items-center justify-start -mx-1">
|
||||||
<div class="w-36 px-1">
|
<div v-if="providersLoaded" class="w-36 px-1">
|
||||||
<ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelProvider" small />
|
<ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelProvider" small />
|
||||||
</div>
|
</div>
|
||||||
<div class="grow md:w-72 px-1">
|
<div class="grow md:w-72 px-1">
|
||||||
@ -253,6 +253,7 @@ export default {
|
|||||||
hasSearched: false,
|
hasSearched: false,
|
||||||
selectedMatch: null,
|
selectedMatch: null,
|
||||||
selectedMatchOrig: null,
|
selectedMatchOrig: null,
|
||||||
|
waitingForProviders: false,
|
||||||
selectedMatchUsage: {
|
selectedMatchUsage: {
|
||||||
title: true,
|
title: true,
|
||||||
subtitle: true,
|
subtitle: true,
|
||||||
@ -285,9 +286,19 @@ export default {
|
|||||||
handler(newVal) {
|
handler(newVal) {
|
||||||
if (newVal) this.init()
|
if (newVal) this.init()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
providersLoaded(isLoaded) {
|
||||||
|
// Complete initialization once providers are loaded
|
||||||
|
if (isLoaded && this.waitingForProviders) {
|
||||||
|
this.waitingForProviders = false
|
||||||
|
this.initProviderAndSearch()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
providersLoaded() {
|
||||||
|
return this.$store.getters['scanners/areProvidersLoaded']
|
||||||
|
},
|
||||||
isProcessing: {
|
isProcessing: {
|
||||||
get() {
|
get() {
|
||||||
return this.processing
|
return this.processing
|
||||||
@ -319,7 +330,7 @@ export default {
|
|||||||
},
|
},
|
||||||
providers() {
|
providers() {
|
||||||
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
|
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
|
||||||
return this.$store.state.scanners.providers
|
return this.$store.state.scanners.bookProviders
|
||||||
},
|
},
|
||||||
searchTitleLabel() {
|
searchTitleLabel() {
|
||||||
if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN
|
if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN
|
||||||
@ -478,6 +489,24 @@ export default {
|
|||||||
|
|
||||||
this.checkboxToggled()
|
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() {
|
init() {
|
||||||
this.clearSelectedMatch()
|
this.clearSelectedMatch()
|
||||||
this.initSelectedMatchUsage()
|
this.initSelectedMatchUsage()
|
||||||
@ -495,19 +524,13 @@ export default {
|
|||||||
}
|
}
|
||||||
this.searchTitle = this.libraryItem.media.metadata.title
|
this.searchTitle = this.libraryItem.media.metadata.title
|
||||||
this.searchAuthor = this.libraryItem.media.metadata.authorName || ''
|
this.searchAuthor = this.libraryItem.media.metadata.authorName || ''
|
||||||
if (this.isPodcast) this.provider = 'itunes'
|
|
||||||
else {
|
|
||||||
this.provider = this.getDefaultBookProvider()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prefer using ASIN if set and using audible provider
|
// Wait for providers to be loaded before setting provider and searching
|
||||||
if (this.provider.startsWith('audible') && this.libraryItem.media.metadata.asin) {
|
if (this.providersLoaded || this.isPodcast) {
|
||||||
this.searchTitle = this.libraryItem.media.metadata.asin
|
this.waitingForProviders = false
|
||||||
this.searchAuthor = ''
|
this.initProviderAndSearch()
|
||||||
}
|
} else {
|
||||||
|
this.waitingForProviders = true
|
||||||
if (this.searchTitle) {
|
|
||||||
this.submitSearch()
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
selectMatch(match) {
|
selectMatch(match) {
|
||||||
@ -637,6 +660,10 @@ export default {
|
|||||||
this.selectedMatch = null
|
this.selectedMatch = null
|
||||||
this.selectedMatchOrig = null
|
this.selectedMatchOrig = null
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// Fetch providers if not already loaded
|
||||||
|
this.$store.dispatch('scanners/fetchProviders')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -74,7 +74,7 @@ export default {
|
|||||||
},
|
},
|
||||||
providers() {
|
providers() {
|
||||||
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
|
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
|
||||||
return this.$store.state.scanners.providers
|
return this.$store.state.scanners.bookProviders
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -156,6 +156,8 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.init()
|
this.init()
|
||||||
|
// Fetch providers if not already loaded
|
||||||
|
this.$store.dispatch('scanners/fetchProviders')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -104,7 +104,6 @@ export default {
|
|||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
provider: null,
|
|
||||||
useSquareBookCovers: false,
|
useSquareBookCovers: false,
|
||||||
enableWatcher: false,
|
enableWatcher: false,
|
||||||
skipMatchingMediaWithAsin: false,
|
skipMatchingMediaWithAsin: false,
|
||||||
@ -134,10 +133,6 @@ export default {
|
|||||||
isPodcastLibrary() {
|
isPodcastLibrary() {
|
||||||
return this.mediaType === 'podcast'
|
return this.mediaType === 'podcast'
|
||||||
},
|
},
|
||||||
providers() {
|
|
||||||
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
|
|
||||||
return this.$store.state.scanners.providers
|
|
||||||
},
|
|
||||||
maskAsFinishedWhenItems() {
|
maskAsFinishedWhenItems() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -371,11 +371,13 @@ export default {
|
|||||||
},
|
},
|
||||||
customMetadataProviderAdded(provider) {
|
customMetadataProviderAdded(provider) {
|
||||||
if (!provider?.id) return
|
if (!provider?.id) return
|
||||||
this.$store.commit('scanners/addCustomMetadataProvider', provider)
|
// Refresh providers cache
|
||||||
|
this.$store.dispatch('scanners/refreshProviders')
|
||||||
},
|
},
|
||||||
customMetadataProviderRemoved(provider) {
|
customMetadataProviderRemoved(provider) {
|
||||||
if (!provider?.id) return
|
if (!provider?.id) return
|
||||||
this.$store.commit('scanners/removeCustomMetadataProvider', provider)
|
// Refresh providers cache
|
||||||
|
this.$store.dispatch('scanners/refreshProviders')
|
||||||
},
|
},
|
||||||
initializeSocket() {
|
initializeSocket() {
|
||||||
if (this.$root.socket) {
|
if (this.$root.socket) {
|
||||||
|
|||||||
@ -247,7 +247,8 @@ export default {
|
|||||||
return this.$store.state.serverSettings
|
return this.$store.state.serverSettings
|
||||||
},
|
},
|
||||||
providers() {
|
providers() {
|
||||||
return this.$store.state.scanners.providers
|
// Use book cover providers for the cover provider dropdown
|
||||||
|
return this.$store.state.scanners.bookCoverProviders || []
|
||||||
},
|
},
|
||||||
dateFormats() {
|
dateFormats() {
|
||||||
return this.$store.state.globals.dateFormats
|
return this.$store.state.globals.dateFormats
|
||||||
@ -416,6 +417,8 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.initServerSettings()
|
this.initServerSettings()
|
||||||
|
// Fetch providers if not already loaded (for cover provider dropdown)
|
||||||
|
this.$store.dispatch('scanners/fetchProviders')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -155,7 +155,7 @@ export default {
|
|||||||
},
|
},
|
||||||
providers() {
|
providers() {
|
||||||
if (this.selectedLibraryIsPodcast) return this.$store.state.scanners.podcastProviders
|
if (this.selectedLibraryIsPodcast) return this.$store.state.scanners.podcastProviders
|
||||||
return this.$store.state.scanners.providers
|
return this.$store.state.scanners.bookProviders
|
||||||
},
|
},
|
||||||
canFetchMetadata() {
|
canFetchMetadata() {
|
||||||
return !this.selectedLibraryIsPodcast && this.fetchMetadata.enabled
|
return !this.selectedLibraryIsPodcast && this.fetchMetadata.enabled
|
||||||
@ -394,6 +394,8 @@ export default {
|
|||||||
this.setMetadataProvider()
|
this.setMetadataProvider()
|
||||||
|
|
||||||
this.setDefaultFolder()
|
this.setDefaultFolder()
|
||||||
|
// Fetch providers if not already loaded
|
||||||
|
this.$store.dispatch('scanners/fetchProviders')
|
||||||
window.addEventListener('dragenter', this.dragenter)
|
window.addEventListener('dragenter', this.dragenter)
|
||||||
window.addEventListener('dragleave', this.dragleave)
|
window.addEventListener('dragleave', this.dragleave)
|
||||||
window.addEventListener('dragover', this.dragover)
|
window.addEventListener('dragover', this.dragover)
|
||||||
|
|||||||
@ -117,7 +117,6 @@ export const actions = {
|
|||||||
const library = data.library
|
const library = data.library
|
||||||
const filterData = data.filterdata
|
const filterData = data.filterdata
|
||||||
const issues = data.issues || 0
|
const issues = data.issues || 0
|
||||||
const customMetadataProviders = data.customMetadataProviders || []
|
|
||||||
const numUserPlaylists = data.numUserPlaylists
|
const numUserPlaylists = data.numUserPlaylists
|
||||||
|
|
||||||
dispatch('user/checkUpdateLibrarySortFilter', library.mediaType, { root: true })
|
dispatch('user/checkUpdateLibrarySortFilter', library.mediaType, { root: true })
|
||||||
@ -131,8 +130,6 @@ export const actions = {
|
|||||||
commit('setLibraryIssues', issues)
|
commit('setLibraryIssues', issues)
|
||||||
commit('setLibraryFilterData', filterData)
|
commit('setLibraryFilterData', filterData)
|
||||||
commit('setNumUserPlaylists', numUserPlaylists)
|
commit('setNumUserPlaylists', numUserPlaylists)
|
||||||
commit('scanners/setCustomMetadataProviders', customMetadataProviders, { root: true })
|
|
||||||
|
|
||||||
commit('setCurrentLibrary', { id: libraryId })
|
commit('setCurrentLibrary', { id: libraryId })
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,126 +1,60 @@
|
|||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
providers: [
|
bookProviders: [],
|
||||||
{
|
podcastProviders: [],
|
||||||
text: 'Google Books',
|
bookCoverProviders: [],
|
||||||
value: 'google'
|
podcastCoverProviders: [],
|
||||||
},
|
providersLoaded: false
|
||||||
{
|
|
||||||
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 = {
|
export const getters = {
|
||||||
checkBookProviderExists: state => (providerValue) => {
|
checkBookProviderExists: (state) => (providerValue) => {
|
||||||
return state.providers.some(p => p.value === providerValue)
|
return state.bookProviders.some((p) => p.value === providerValue)
|
||||||
},
|
},
|
||||||
checkPodcastProviderExists: state => (providerValue) => {
|
checkPodcastProviderExists: (state) => (providerValue) => {
|
||||||
return state.podcastProviders.some(p => p.value === 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const actions = {}
|
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
addCustomMetadataProvider(state, provider) {
|
setAllProviders(state, providers) {
|
||||||
if (provider.mediaType === 'book') {
|
state.bookProviders = providers.books || []
|
||||||
if (state.providers.some(p => p.value === provider.slug)) return
|
state.podcastProviders = providers.podcasts || []
|
||||||
state.providers.push({
|
state.bookCoverProviders = providers.booksCovers || []
|
||||||
text: provider.name,
|
state.podcastCoverProviders = providers.podcasts || [] // Use same as bookCovers since podcasts use iTunes only
|
||||||
value: provider.slug
|
state.providersLoaded = true
|
||||||
})
|
|
||||||
} 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,8 @@
|
|||||||
"ButtonChooseAFolder": "اختر المجلد",
|
"ButtonChooseAFolder": "اختر المجلد",
|
||||||
"ButtonChooseFiles": "اختر الملفات",
|
"ButtonChooseFiles": "اختر الملفات",
|
||||||
"ButtonClearFilter": "تصفية الفرز",
|
"ButtonClearFilter": "تصفية الفرز",
|
||||||
"ButtonCloseFeed": "إغلاق",
|
"ButtonClose": "إغلاق",
|
||||||
|
"ButtonCloseFeed": "إغلاق الموجز",
|
||||||
"ButtonCloseSession": "إغلاق الجلسة المفتوحة",
|
"ButtonCloseSession": "إغلاق الجلسة المفتوحة",
|
||||||
"ButtonCollections": "المجموعات",
|
"ButtonCollections": "المجموعات",
|
||||||
"ButtonConfigureScanner": "إعدادات الماسح الضوئي",
|
"ButtonConfigureScanner": "إعدادات الماسح الضوئي",
|
||||||
@ -120,11 +121,13 @@
|
|||||||
"HeaderAccount": "الحساب",
|
"HeaderAccount": "الحساب",
|
||||||
"HeaderAddCustomMetadataProvider": "إضافة موفر بيانات تعريفية مخصص",
|
"HeaderAddCustomMetadataProvider": "إضافة موفر بيانات تعريفية مخصص",
|
||||||
"HeaderAdvanced": "متقدم",
|
"HeaderAdvanced": "متقدم",
|
||||||
|
"HeaderApiKeys": "مفاتيح API",
|
||||||
"HeaderAppriseNotificationSettings": "إعدادات الإشعارات",
|
"HeaderAppriseNotificationSettings": "إعدادات الإشعارات",
|
||||||
"HeaderAudioTracks": "المقاطع الصوتية",
|
"HeaderAudioTracks": "المقاطع الصوتية",
|
||||||
"HeaderAudiobookTools": "أدوات إدارة ملفات الكتب الصوتية",
|
"HeaderAudiobookTools": "أدوات إدارة ملفات الكتب الصوتية",
|
||||||
"HeaderAuthentication": "المصادقة",
|
"HeaderAuthentication": "المصادقة",
|
||||||
"HeaderBackups": "النسخ الاحتياطية",
|
"HeaderBackups": "النسخ الاحتياطية",
|
||||||
|
"HeaderBulkChapterModal": "أضف فصولاً متعددة",
|
||||||
"HeaderChangePassword": "تغيير كلمة المرور",
|
"HeaderChangePassword": "تغيير كلمة المرور",
|
||||||
"HeaderChapters": "الفصول",
|
"HeaderChapters": "الفصول",
|
||||||
"HeaderChooseAFolder": "اختيار المجلد",
|
"HeaderChooseAFolder": "اختيار المجلد",
|
||||||
@ -163,6 +166,7 @@
|
|||||||
"HeaderMetadataOrderOfPrecedence": "ترتيب أولوية البيانات الوصفية",
|
"HeaderMetadataOrderOfPrecedence": "ترتيب أولوية البيانات الوصفية",
|
||||||
"HeaderMetadataToEmbed": "البيانات الوصفية المراد تضمينها",
|
"HeaderMetadataToEmbed": "البيانات الوصفية المراد تضمينها",
|
||||||
"HeaderNewAccount": "حساب جديد",
|
"HeaderNewAccount": "حساب جديد",
|
||||||
|
"HeaderNewApiKey": "مفتاح API جديد",
|
||||||
"HeaderNewLibrary": "مكتبة جديدة",
|
"HeaderNewLibrary": "مكتبة جديدة",
|
||||||
"HeaderNotificationCreate": "إنشاء إشعار",
|
"HeaderNotificationCreate": "إنشاء إشعار",
|
||||||
"HeaderNotificationUpdate": "تحديث إشعار",
|
"HeaderNotificationUpdate": "تحديث إشعار",
|
||||||
@ -196,6 +200,7 @@
|
|||||||
"HeaderSettingsExperimental": "ميزات تجريبية",
|
"HeaderSettingsExperimental": "ميزات تجريبية",
|
||||||
"HeaderSettingsGeneral": "عام",
|
"HeaderSettingsGeneral": "عام",
|
||||||
"HeaderSettingsScanner": "إعدادات المسح",
|
"HeaderSettingsScanner": "إعدادات المسح",
|
||||||
|
"HeaderSettingsSecurity": "الأمان",
|
||||||
"HeaderSettingsWebClient": "عميل الويب",
|
"HeaderSettingsWebClient": "عميل الويب",
|
||||||
"HeaderSleepTimer": "مؤقت النوم",
|
"HeaderSleepTimer": "مؤقت النوم",
|
||||||
"HeaderStatsLargestItems": "أكبر العناصر حجماً",
|
"HeaderStatsLargestItems": "أكبر العناصر حجماً",
|
||||||
@ -207,6 +212,7 @@
|
|||||||
"HeaderTableOfContents": "جدول المحتويات",
|
"HeaderTableOfContents": "جدول المحتويات",
|
||||||
"HeaderTools": "أدوات",
|
"HeaderTools": "أدوات",
|
||||||
"HeaderUpdateAccount": "تحديث الحساب",
|
"HeaderUpdateAccount": "تحديث الحساب",
|
||||||
|
"HeaderUpdateApiKey": "تحديث مفتاح API",
|
||||||
"HeaderUpdateAuthor": "تحديث المؤلف",
|
"HeaderUpdateAuthor": "تحديث المؤلف",
|
||||||
"HeaderUpdateDetails": "تحديث التفاصيل",
|
"HeaderUpdateDetails": "تحديث التفاصيل",
|
||||||
"HeaderUpdateLibrary": "تحديث المكتبة",
|
"HeaderUpdateLibrary": "تحديث المكتبة",
|
||||||
@ -236,6 +242,8 @@
|
|||||||
"LabelAllUsersExcludingGuests": "جميع المستخدمين باستثناء الضيوف",
|
"LabelAllUsersExcludingGuests": "جميع المستخدمين باستثناء الضيوف",
|
||||||
"LabelAllUsersIncludingGuests": "جميع المستخدمين بما في ذلك الضيوف",
|
"LabelAllUsersIncludingGuests": "جميع المستخدمين بما في ذلك الضيوف",
|
||||||
"LabelAlreadyInYourLibrary": "موجود بالفعل في مكتبتك",
|
"LabelAlreadyInYourLibrary": "موجود بالفعل في مكتبتك",
|
||||||
|
"LabelApiKeyCreated": "تم إنشاء مفتاح API \"{0}\" بنجاح.",
|
||||||
|
"LabelApiKeyCreatedDescription": "تأكد من نسخ مفتاح API الآن، لن تتمكن من رؤيته مرة أخرى.",
|
||||||
"LabelApiToken": "رمز API",
|
"LabelApiToken": "رمز API",
|
||||||
"LabelAppend": "إلحاق",
|
"LabelAppend": "إلحاق",
|
||||||
"LabelAudioBitrate": "معدل بت الصوت (على سبيل المثال 128 كيلو بايت)",
|
"LabelAudioBitrate": "معدل بت الصوت (على سبيل المثال 128 كيلو بايت)",
|
||||||
|
|||||||
@ -1001,13 +1001,14 @@
|
|||||||
"ToastCollectionItemsAddFailed": "Genstand(e) tilføjet til kollektion fejlet",
|
"ToastCollectionItemsAddFailed": "Genstand(e) tilføjet til kollektion fejlet",
|
||||||
"ToastCollectionRemoveSuccess": "Samling fjernet",
|
"ToastCollectionRemoveSuccess": "Samling fjernet",
|
||||||
"ToastCollectionUpdateSuccess": "Samling opdateret",
|
"ToastCollectionUpdateSuccess": "Samling opdateret",
|
||||||
|
"ToastConnectionNotAvailable": "Forbindelse mislykkedes. Prøv igen senere",
|
||||||
"ToastCoverUpdateFailed": "Cover opdatering fejlede",
|
"ToastCoverUpdateFailed": "Cover opdatering fejlede",
|
||||||
"ToastDateTimeInvalidOrIncomplete": "Dato og tid er forkert eller ufærdig",
|
"ToastDateTimeInvalidOrIncomplete": "Dato og tid er ugyldig eller ufærdig",
|
||||||
"ToastDeleteFileFailed": "Slet fil fejlede",
|
"ToastDeleteFileFailed": "Sletning af fil fejlede",
|
||||||
"ToastDeleteFileSuccess": "Fil slettet",
|
"ToastDeleteFileSuccess": "Fil slettet",
|
||||||
"ToastDeviceAddFailed": "Fejlede at tilføje enhed",
|
"ToastDeviceAddFailed": "Tilføjelse af enhed Fejlede",
|
||||||
"ToastDeviceNameAlreadyExists": "Elæser enhed med det navn eksistere allerede",
|
"ToastDeviceNameAlreadyExists": "E-læser enhed med det navn eksistere allerede",
|
||||||
"ToastDeviceTestEmailFailed": "Fejlede at sende test mail",
|
"ToastDeviceTestEmailFailed": "Afsendelse af test mail fejlede",
|
||||||
"ToastDeviceTestEmailSuccess": "Test mail sendt",
|
"ToastDeviceTestEmailSuccess": "Test mail sendt",
|
||||||
"ToastEmailSettingsUpdateSuccess": "Mail indstillinger opdateret",
|
"ToastEmailSettingsUpdateSuccess": "Mail indstillinger opdateret",
|
||||||
"ToastEncodeCancelFailed": "Fejlede at afbryde indkodning",
|
"ToastEncodeCancelFailed": "Fejlede at afbryde indkodning",
|
||||||
@ -1017,21 +1018,23 @@
|
|||||||
"ToastEpisodeUpdateSuccess": "{0} afsnit opdateret",
|
"ToastEpisodeUpdateSuccess": "{0} afsnit opdateret",
|
||||||
"ToastErrorCannotShare": "Kan ikke dele på denne enhed",
|
"ToastErrorCannotShare": "Kan ikke dele på denne enhed",
|
||||||
"ToastFailedToCreate": "Oprettelsen mislykkedes",
|
"ToastFailedToCreate": "Oprettelsen mislykkedes",
|
||||||
"ToastFailedToLoadData": "Fejlede at indlæse data",
|
"ToastFailedToDelete": "Sletning fejlede",
|
||||||
|
"ToastFailedToLoadData": "Indlæsning af data fejlede",
|
||||||
"ToastFailedToMatch": "Fejlet match",
|
"ToastFailedToMatch": "Fejlet match",
|
||||||
"ToastFailedToShare": "Fejlet deling",
|
"ToastFailedToShare": "Deling fejlede",
|
||||||
"ToastFailedToUpdate": "Fejlet opdatering",
|
"ToastFailedToUpdate": "Fejlet opdatering",
|
||||||
"ToastInvalidImageUrl": "Forkert billede URL",
|
"ToastInvalidImageUrl": "Ugyldig billede URL",
|
||||||
"ToastInvalidMaxEpisodesToDownload": "Forkert maks afsnit at hente",
|
"ToastInvalidMaxEpisodesToDownload": "Ugyldigt maks afsnit at hente",
|
||||||
"ToastInvalidUrl": "Forkert URL",
|
"ToastInvalidUrl": "Ugyldig URL",
|
||||||
"ToastItemCoverUpdateSuccess": "Varens omslag opdateret",
|
"ToastInvalidUrls": "En eller flere URLer er ugyldige",
|
||||||
"ToastItemDeletedFailed": "Fejlede at slette genstand",
|
"ToastItemCoverUpdateSuccess": "Omslag opdateret",
|
||||||
|
"ToastItemDeletedFailed": "Sletning af genstand fejlede",
|
||||||
"ToastItemDeletedSuccess": "Genstand slettet",
|
"ToastItemDeletedSuccess": "Genstand slettet",
|
||||||
"ToastItemDetailsUpdateSuccess": "Varedetaljer opdateret",
|
"ToastItemDetailsUpdateSuccess": "Detaljer opdateret",
|
||||||
"ToastItemMarkedAsFinishedFailed": "Mislykkedes markering som afsluttet",
|
"ToastItemMarkedAsFinishedFailed": "Markering som afsluttet mislykkedes",
|
||||||
"ToastItemMarkedAsFinishedSuccess": "Vare markeret som afsluttet",
|
"ToastItemMarkedAsFinishedSuccess": "Element markeret som afsluttet",
|
||||||
"ToastItemMarkedAsNotFinishedFailed": "Mislykkedes markering som ikke afsluttet",
|
"ToastItemMarkedAsNotFinishedFailed": "Markering som ikke afsluttet mislykkedes",
|
||||||
"ToastItemMarkedAsNotFinishedSuccess": "Vare markeret som ikke afsluttet",
|
"ToastItemMarkedAsNotFinishedSuccess": "Element markeret som ikke afsluttet",
|
||||||
"ToastItemUpdateSuccess": "Genstand opdateret",
|
"ToastItemUpdateSuccess": "Genstand opdateret",
|
||||||
"ToastLibraryCreateFailed": "Oprettelse af bibliotek mislykkedes",
|
"ToastLibraryCreateFailed": "Oprettelse af bibliotek mislykkedes",
|
||||||
"ToastLibraryCreateSuccess": "Bibliotek \"{0}\" oprettet",
|
"ToastLibraryCreateSuccess": "Bibliotek \"{0}\" oprettet",
|
||||||
|
|||||||
@ -1026,6 +1026,8 @@
|
|||||||
"ToastCollectionItemsAddFailed": "Das Hinzufügen von Element(en) zur Sammlung ist fehlgeschlagen",
|
"ToastCollectionItemsAddFailed": "Das Hinzufügen von Element(en) zur Sammlung ist fehlgeschlagen",
|
||||||
"ToastCollectionRemoveSuccess": "Sammlung entfernt",
|
"ToastCollectionRemoveSuccess": "Sammlung entfernt",
|
||||||
"ToastCollectionUpdateSuccess": "Sammlung aktualisiert",
|
"ToastCollectionUpdateSuccess": "Sammlung aktualisiert",
|
||||||
|
"ToastConnectionNotAvailable": "Verbindung nicht möglich. Bitte später erneut versuchen",
|
||||||
|
"ToastCoverSearchFailed": "Cover-Suche fehlgeschlagen",
|
||||||
"ToastCoverUpdateFailed": "Cover-Update fehlgeschlagen",
|
"ToastCoverUpdateFailed": "Cover-Update fehlgeschlagen",
|
||||||
"ToastDateTimeInvalidOrIncomplete": "Datum und Zeit sind ungültig oder unvollständig",
|
"ToastDateTimeInvalidOrIncomplete": "Datum und Zeit sind ungültig oder unvollständig",
|
||||||
"ToastDeleteFileFailed": "Die Datei konnte nicht gelöscht werden",
|
"ToastDeleteFileFailed": "Die Datei konnte nicht gelöscht werden",
|
||||||
|
|||||||
@ -588,8 +588,8 @@
|
|||||||
"LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves",
|
"LabelSettingsBookshelfViewHelp": "Skeumorphic design with wooden shelves",
|
||||||
"LabelSettingsChromecastSupport": "Chromecast support",
|
"LabelSettingsChromecastSupport": "Chromecast support",
|
||||||
"LabelSettingsDateFormat": "Date Format",
|
"LabelSettingsDateFormat": "Date Format",
|
||||||
"LabelSettingsEnableWatcher": "Automatically scan libraries for changes",
|
"LabelSettingsEnableWatcher": "Automatically watch libraries for changes",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Automatically scan library for changes",
|
"LabelSettingsEnableWatcherForLibrary": "Automatically watch library for changes",
|
||||||
"LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart",
|
"LabelSettingsEnableWatcherHelp": "Enables the automatic adding/updating of items when file changes are detected. *Requires server restart",
|
||||||
"LabelSettingsEpubsAllowScriptedContent": "Allow scripted content in epubs",
|
"LabelSettingsEpubsAllowScriptedContent": "Allow scripted content in epubs",
|
||||||
"LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
|
"LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
|
||||||
@ -888,7 +888,7 @@
|
|||||||
"MessageResetChaptersConfirm": "Are you sure you want to reset chapters and undo the changes you made?",
|
"MessageResetChaptersConfirm": "Are you sure you want to reset chapters and undo the changes you made?",
|
||||||
"MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on",
|
"MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on",
|
||||||
"MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.",
|
"MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.",
|
||||||
"MessageScheduleLibraryScanNote": "For most users, it is recommended to leave this feature disabled and keep the folder watcher setting enabled. The folder watcher will automatically detect changes in your library folders. The folder watcher doesn't work for every file system (like NFS) so scheduled library scans can be used instead.",
|
"MessageScheduleLibraryScanNote": "For most users, it is recommended to leave this feature disabled and keep the \"Automatically watch library for changes\" setting enabled - it will automatically detect changes in your library folders. Enable this feature if \"Automatically watch library for changes\" does not work for your file system (like NFS).",
|
||||||
"MessageScheduleRunEveryWeekdayAtTime": "Run every {0} at {1}",
|
"MessageScheduleRunEveryWeekdayAtTime": "Run every {0} at {1}",
|
||||||
"MessageSearchResultsFor": "Search results for",
|
"MessageSearchResultsFor": "Search results for",
|
||||||
"MessageSelected": "{0} selected",
|
"MessageSelected": "{0} selected",
|
||||||
|
|||||||
@ -355,7 +355,7 @@
|
|||||||
"LabelExample": "Esimerkki",
|
"LabelExample": "Esimerkki",
|
||||||
"LabelExpandSeries": "Laajenna sarja",
|
"LabelExpandSeries": "Laajenna sarja",
|
||||||
"LabelExpandSubSeries": "Laajenna alisarja",
|
"LabelExpandSubSeries": "Laajenna alisarja",
|
||||||
"LabelExplicit": "Yksiselitteinen",
|
"LabelExplicit": "Sopimaton",
|
||||||
"LabelExplicitChecked": "Yksiselitteinen (valittu)",
|
"LabelExplicitChecked": "Yksiselitteinen (valittu)",
|
||||||
"LabelExplicitUnchecked": "Ei yksiselitteinen (ei valittu)",
|
"LabelExplicitUnchecked": "Ei yksiselitteinen (ei valittu)",
|
||||||
"LabelExportOPML": "Vie OPML",
|
"LabelExportOPML": "Vie OPML",
|
||||||
|
|||||||
@ -309,6 +309,7 @@
|
|||||||
"LabelDeleteFromFileSystemCheckbox": "Elimina dal file system (togli la spunta per eliminarla solo dal DB)",
|
"LabelDeleteFromFileSystemCheckbox": "Elimina dal file system (togli la spunta per eliminarla solo dal DB)",
|
||||||
"LabelDescription": "Descrizione",
|
"LabelDescription": "Descrizione",
|
||||||
"LabelDeselectAll": "Deseleziona Tutto",
|
"LabelDeselectAll": "Deseleziona Tutto",
|
||||||
|
"LabelDetectedPattern": "Trovato pattern:",
|
||||||
"LabelDevice": "Dispositivo",
|
"LabelDevice": "Dispositivo",
|
||||||
"LabelDeviceInfo": "Info dispositivo",
|
"LabelDeviceInfo": "Info dispositivo",
|
||||||
"LabelDeviceIsAvailableTo": "Il dispositivo e disponibile su…",
|
"LabelDeviceIsAvailableTo": "Il dispositivo e disponibile su…",
|
||||||
@ -377,6 +378,7 @@
|
|||||||
"LabelFilterByUser": "Filtro per Utente",
|
"LabelFilterByUser": "Filtro per Utente",
|
||||||
"LabelFindEpisodes": "Trova Episodi",
|
"LabelFindEpisodes": "Trova Episodi",
|
||||||
"LabelFinished": "Finita",
|
"LabelFinished": "Finita",
|
||||||
|
"LabelFinishedDate": "Finito {0}",
|
||||||
"LabelFolder": "Cartella",
|
"LabelFolder": "Cartella",
|
||||||
"LabelFolders": "Cartelle",
|
"LabelFolders": "Cartelle",
|
||||||
"LabelFontBold": "Grassetto",
|
"LabelFontBold": "Grassetto",
|
||||||
@ -434,8 +436,9 @@
|
|||||||
"LabelLibraryFilterSublistEmpty": "Nessuno {0}",
|
"LabelLibraryFilterSublistEmpty": "Nessuno {0}",
|
||||||
"LabelLibraryItem": "Elementi della biblioteca",
|
"LabelLibraryItem": "Elementi della biblioteca",
|
||||||
"LabelLibraryName": "Nome della biblioteca",
|
"LabelLibraryName": "Nome della biblioteca",
|
||||||
"LabelLibrarySortByProgress": "Aggiornamento dei progressi",
|
"LabelLibrarySortByProgress": "Progressi: Ultimi aggiornamenti",
|
||||||
"LabelLibrarySortByProgressStarted": "Data di inizio",
|
"LabelLibrarySortByProgressFinished": "Progressi: Completati",
|
||||||
|
"LabelLibrarySortByProgressStarted": "Progressi: Iniziati",
|
||||||
"LabelLimit": "Limiti",
|
"LabelLimit": "Limiti",
|
||||||
"LabelLineSpacing": "Interlinea",
|
"LabelLineSpacing": "Interlinea",
|
||||||
"LabelListenAgain": "Ascolta ancora",
|
"LabelListenAgain": "Ascolta ancora",
|
||||||
@ -635,6 +638,7 @@
|
|||||||
"LabelStartTime": "Tempo di inizio",
|
"LabelStartTime": "Tempo di inizio",
|
||||||
"LabelStarted": "Iniziato",
|
"LabelStarted": "Iniziato",
|
||||||
"LabelStartedAt": "Iniziato al",
|
"LabelStartedAt": "Iniziato al",
|
||||||
|
"LabelStartedDate": "Iniziati {0}",
|
||||||
"LabelStatsAudioTracks": "Tracce Audio",
|
"LabelStatsAudioTracks": "Tracce Audio",
|
||||||
"LabelStatsAuthors": "Autori",
|
"LabelStatsAuthors": "Autori",
|
||||||
"LabelStatsBestDay": "Giorno migliore",
|
"LabelStatsBestDay": "Giorno migliore",
|
||||||
@ -1022,6 +1026,8 @@
|
|||||||
"ToastCollectionItemsAddFailed": "l'aggiunta dell'elemento(i) alla raccolta non è riuscito",
|
"ToastCollectionItemsAddFailed": "l'aggiunta dell'elemento(i) alla raccolta non è riuscito",
|
||||||
"ToastCollectionRemoveSuccess": "Collezione rimossa",
|
"ToastCollectionRemoveSuccess": "Collezione rimossa",
|
||||||
"ToastCollectionUpdateSuccess": "Raccolta aggiornata",
|
"ToastCollectionUpdateSuccess": "Raccolta aggiornata",
|
||||||
|
"ToastConnectionNotAvailable": "Connessione non disponibile. Provare più tardi",
|
||||||
|
"ToastCoverSearchFailed": "Ricerca Cover fallita",
|
||||||
"ToastCoverUpdateFailed": "Aggiornamento cover fallito",
|
"ToastCoverUpdateFailed": "Aggiornamento cover fallito",
|
||||||
"ToastDateTimeInvalidOrIncomplete": "Data e ora non sono valide o incomplete",
|
"ToastDateTimeInvalidOrIncomplete": "Data e ora non sono valide o incomplete",
|
||||||
"ToastDeleteFileFailed": "Impossibile eliminare il file",
|
"ToastDeleteFileFailed": "Impossibile eliminare il file",
|
||||||
|
|||||||
@ -400,7 +400,7 @@
|
|||||||
"LabelHours": "Godziny",
|
"LabelHours": "Godziny",
|
||||||
"LabelIcon": "Ikona",
|
"LabelIcon": "Ikona",
|
||||||
"LabelImageURLFromTheWeb": "Link do obrazu w sieci",
|
"LabelImageURLFromTheWeb": "Link do obrazu w sieci",
|
||||||
"LabelInProgress": "W trakcie",
|
"LabelInProgress": "W toku",
|
||||||
"LabelIncludeInTracklist": "Dołącz do listy odtwarzania",
|
"LabelIncludeInTracklist": "Dołącz do listy odtwarzania",
|
||||||
"LabelIncomplete": "Nieukończone",
|
"LabelIncomplete": "Nieukończone",
|
||||||
"LabelInterval": "Interwał",
|
"LabelInterval": "Interwał",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"ButtonAdd": "Adicionar",
|
"ButtonAdd": "Adicionar",
|
||||||
"ButtonAddApiKey": "Adicionar Chave API",
|
"ButtonAddApiKey": "Adicionar chave de API",
|
||||||
"ButtonAddChapters": "Adicionar Capítulos",
|
"ButtonAddChapters": "Adicionar Capítulos",
|
||||||
"ButtonAddDevice": "Adicionar Dispositivo",
|
"ButtonAddDevice": "Adicionar Dispositivo",
|
||||||
"ButtonAddLibrary": "Adicionar Biblioteca",
|
"ButtonAddLibrary": "Adicionar Biblioteca",
|
||||||
@ -21,6 +21,7 @@
|
|||||||
"ButtonChooseAFolder": "Escolha uma pasta",
|
"ButtonChooseAFolder": "Escolha uma pasta",
|
||||||
"ButtonChooseFiles": "Escolha arquivos",
|
"ButtonChooseFiles": "Escolha arquivos",
|
||||||
"ButtonClearFilter": "Limpar Filtro",
|
"ButtonClearFilter": "Limpar Filtro",
|
||||||
|
"ButtonClose": "Fechar",
|
||||||
"ButtonCloseFeed": "Fechar Feed",
|
"ButtonCloseFeed": "Fechar Feed",
|
||||||
"ButtonCloseSession": "Fechar Sessão Aberta",
|
"ButtonCloseSession": "Fechar Sessão Aberta",
|
||||||
"ButtonCollections": "Coleções",
|
"ButtonCollections": "Coleções",
|
||||||
@ -53,7 +54,7 @@
|
|||||||
"ButtonNevermind": "Cancelar",
|
"ButtonNevermind": "Cancelar",
|
||||||
"ButtonNext": "Próximo",
|
"ButtonNext": "Próximo",
|
||||||
"ButtonNextChapter": "Próximo Capítulo",
|
"ButtonNextChapter": "Próximo Capítulo",
|
||||||
"ButtonNextItemInQueue": "Próximo Item da Fila",
|
"ButtonNextItemInQueue": "Próximo Item na Fila",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Abrir Feed",
|
"ButtonOpenFeed": "Abrir Feed",
|
||||||
"ButtonOpenManager": "Abrir Gerenciador",
|
"ButtonOpenManager": "Abrir Gerenciador",
|
||||||
@ -120,10 +121,13 @@
|
|||||||
"HeaderAccount": "Conta",
|
"HeaderAccount": "Conta",
|
||||||
"HeaderAddCustomMetadataProvider": "Adicionar Provedor de Metadados Personalizado",
|
"HeaderAddCustomMetadataProvider": "Adicionar Provedor de Metadados Personalizado",
|
||||||
"HeaderAdvanced": "Avançado",
|
"HeaderAdvanced": "Avançado",
|
||||||
|
"HeaderApiKeys": "Chaves de API",
|
||||||
"HeaderAppriseNotificationSettings": "Configuração de notificações Apprise",
|
"HeaderAppriseNotificationSettings": "Configuração de notificações Apprise",
|
||||||
"HeaderAudioTracks": "Trilhas de áudio",
|
"HeaderAudioTracks": "Trilhas de áudio",
|
||||||
"HeaderAudiobookTools": "Ferramentas de Gerenciamento de Arquivos de Audiobooks",
|
"HeaderAudiobookTools": "Ferramentas de Gerenciamento de Arquivos de Audiobooks",
|
||||||
"HeaderAuthentication": "Autenticação",
|
"HeaderAuthentication": "Autenticação",
|
||||||
|
"HeaderBackups": "Backups",
|
||||||
|
"HeaderBulkChapterModal": "Adicionar vários capítulos",
|
||||||
"HeaderChangePassword": "Trocar Senha",
|
"HeaderChangePassword": "Trocar Senha",
|
||||||
"HeaderChapters": "Capítulos",
|
"HeaderChapters": "Capítulos",
|
||||||
"HeaderChooseAFolder": "Escolha uma Pasta",
|
"HeaderChooseAFolder": "Escolha uma Pasta",
|
||||||
@ -136,6 +140,7 @@
|
|||||||
"HeaderDetails": "Detalhes",
|
"HeaderDetails": "Detalhes",
|
||||||
"HeaderDownloadQueue": "Fila de Download",
|
"HeaderDownloadQueue": "Fila de Download",
|
||||||
"HeaderEbookFiles": "Arquivos Ebook",
|
"HeaderEbookFiles": "Arquivos Ebook",
|
||||||
|
"HeaderEmail": "Email",
|
||||||
"HeaderEmailSettings": "Configurações de Email",
|
"HeaderEmailSettings": "Configurações de Email",
|
||||||
"HeaderEpisodes": "Episódios",
|
"HeaderEpisodes": "Episódios",
|
||||||
"HeaderEreaderDevices": "Dispositivos Ereader",
|
"HeaderEreaderDevices": "Dispositivos Ereader",
|
||||||
@ -152,6 +157,8 @@
|
|||||||
"HeaderLibraryStats": "Estatísticas da Biblioteca",
|
"HeaderLibraryStats": "Estatísticas da Biblioteca",
|
||||||
"HeaderListeningSessions": "Sessões",
|
"HeaderListeningSessions": "Sessões",
|
||||||
"HeaderListeningStats": "Estatísticas",
|
"HeaderListeningStats": "Estatísticas",
|
||||||
|
"HeaderLogin": "Login",
|
||||||
|
"HeaderLogs": "Logs",
|
||||||
"HeaderManageGenres": "Gerenciar Gêneros",
|
"HeaderManageGenres": "Gerenciar Gêneros",
|
||||||
"HeaderManageTags": "Gerenciar Etiquetas",
|
"HeaderManageTags": "Gerenciar Etiquetas",
|
||||||
"HeaderMapDetails": "Designar Detalhes",
|
"HeaderMapDetails": "Designar Detalhes",
|
||||||
@ -159,17 +166,23 @@
|
|||||||
"HeaderMetadataOrderOfPrecedence": "Ordem de Prioridade dos Metadados",
|
"HeaderMetadataOrderOfPrecedence": "Ordem de Prioridade dos Metadados",
|
||||||
"HeaderMetadataToEmbed": "Metadados a Serem Incluídos",
|
"HeaderMetadataToEmbed": "Metadados a Serem Incluídos",
|
||||||
"HeaderNewAccount": "Nova Conta",
|
"HeaderNewAccount": "Nova Conta",
|
||||||
|
"HeaderNewApiKey": "Nova chave de API",
|
||||||
"HeaderNewLibrary": "Nova Biblioteca",
|
"HeaderNewLibrary": "Nova Biblioteca",
|
||||||
|
"HeaderNotificationCreate": "Criar Notificação",
|
||||||
|
"HeaderNotificationUpdate": "Atualizar Notificação",
|
||||||
"HeaderNotifications": "Notificações",
|
"HeaderNotifications": "Notificações",
|
||||||
"HeaderOpenIDConnectAuthentication": "Autenticação via OpenID Connect",
|
"HeaderOpenIDConnectAuthentication": "Autenticação via OpenID Connect",
|
||||||
|
"HeaderOpenListeningSessions": "Abrir Sessões de Escuta",
|
||||||
"HeaderOpenRSSFeed": "Abrir Feed RSS",
|
"HeaderOpenRSSFeed": "Abrir Feed RSS",
|
||||||
"HeaderOtherFiles": "Outros Arquivos",
|
"HeaderOtherFiles": "Outros Arquivos",
|
||||||
"HeaderPasswordAuthentication": "Autenticação por Senha",
|
"HeaderPasswordAuthentication": "Autenticação por Senha",
|
||||||
"HeaderPermissions": "Permissões",
|
"HeaderPermissions": "Permissões",
|
||||||
"HeaderPlayerQueue": "Fila do reprodutor",
|
"HeaderPlayerQueue": "Fila do reprodutor",
|
||||||
|
"HeaderPlayerSettings": "Configurações do Reprodutor",
|
||||||
"HeaderPlaylist": "Lista de Reprodução",
|
"HeaderPlaylist": "Lista de Reprodução",
|
||||||
"HeaderPlaylistItems": "Itens da lista de reprodução",
|
"HeaderPlaylistItems": "Itens da lista de reprodução",
|
||||||
"HeaderPodcastsToAdd": "Podcasts para Adicionar",
|
"HeaderPodcastsToAdd": "Podcasts para Adicionar",
|
||||||
|
"HeaderPresets": "Valores predefinidos",
|
||||||
"HeaderPreviewCover": "Visualização da Capa",
|
"HeaderPreviewCover": "Visualização da Capa",
|
||||||
"HeaderRSSFeedGeneral": "Detalhes RSS",
|
"HeaderRSSFeedGeneral": "Detalhes RSS",
|
||||||
"HeaderRSSFeedIsOpen": "Feed RSS está Aberto",
|
"HeaderRSSFeedIsOpen": "Feed RSS está Aberto",
|
||||||
@ -178,6 +191,7 @@
|
|||||||
"HeaderRemoveEpisodes": "Remover {0} Episódios",
|
"HeaderRemoveEpisodes": "Remover {0} Episódios",
|
||||||
"HeaderSavedMediaProgress": "Progresso da gravação das mídias",
|
"HeaderSavedMediaProgress": "Progresso da gravação das mídias",
|
||||||
"HeaderSchedule": "Programação",
|
"HeaderSchedule": "Programação",
|
||||||
|
"HeaderScheduleEpisodeDownloads": "Programar Download Automático de Episódios",
|
||||||
"HeaderScheduleLibraryScans": "Programar Verificação Automática da Biblioteca",
|
"HeaderScheduleLibraryScans": "Programar Verificação Automática da Biblioteca",
|
||||||
"HeaderSession": "Sessão",
|
"HeaderSession": "Sessão",
|
||||||
"HeaderSetBackupSchedule": "Definir Programação de Backup",
|
"HeaderSetBackupSchedule": "Definir Programação de Backup",
|
||||||
@ -186,6 +200,8 @@
|
|||||||
"HeaderSettingsExperimental": "Funcionalidades experimentais",
|
"HeaderSettingsExperimental": "Funcionalidades experimentais",
|
||||||
"HeaderSettingsGeneral": "Geral",
|
"HeaderSettingsGeneral": "Geral",
|
||||||
"HeaderSettingsScanner": "Verificador",
|
"HeaderSettingsScanner": "Verificador",
|
||||||
|
"HeaderSettingsSecurity": "Segurança",
|
||||||
|
"HeaderSettingsWebClient": "Cliente Web",
|
||||||
"HeaderSleepTimer": "Timer",
|
"HeaderSleepTimer": "Timer",
|
||||||
"HeaderStatsLargestItems": "Maiores Itens",
|
"HeaderStatsLargestItems": "Maiores Itens",
|
||||||
"HeaderStatsLongestItems": "Itens mais longos (hrs)",
|
"HeaderStatsLongestItems": "Itens mais longos (hrs)",
|
||||||
@ -196,6 +212,7 @@
|
|||||||
"HeaderTableOfContents": "Sumário",
|
"HeaderTableOfContents": "Sumário",
|
||||||
"HeaderTools": "Ferramentas",
|
"HeaderTools": "Ferramentas",
|
||||||
"HeaderUpdateAccount": "Atualizar Conta",
|
"HeaderUpdateAccount": "Atualizar Conta",
|
||||||
|
"HeaderUpdateApiKey": "Atualizar Chave de API",
|
||||||
"HeaderUpdateAuthor": "Atualizar Autor",
|
"HeaderUpdateAuthor": "Atualizar Autor",
|
||||||
"HeaderUpdateDetails": "Atualizar Detalhes",
|
"HeaderUpdateDetails": "Atualizar Detalhes",
|
||||||
"HeaderUpdateLibrary": "Atualizar Biblioteca",
|
"HeaderUpdateLibrary": "Atualizar Biblioteca",
|
||||||
@ -210,6 +227,7 @@
|
|||||||
"LabelAccountTypeAdmin": "Administrador",
|
"LabelAccountTypeAdmin": "Administrador",
|
||||||
"LabelAccountTypeGuest": "Convidado",
|
"LabelAccountTypeGuest": "Convidado",
|
||||||
"LabelAccountTypeUser": "Usuário",
|
"LabelAccountTypeUser": "Usuário",
|
||||||
|
"LabelActivities": "Atividades",
|
||||||
"LabelActivity": "Atividade",
|
"LabelActivity": "Atividade",
|
||||||
"LabelAddToCollection": "Adicionar à Coleção",
|
"LabelAddToCollection": "Adicionar à Coleção",
|
||||||
"LabelAddToCollectionBatch": "Adicionar {0} Livros à Coleção",
|
"LabelAddToCollectionBatch": "Adicionar {0} Livros à Coleção",
|
||||||
@ -219,11 +237,20 @@
|
|||||||
"LabelAddedDate": "Adicionado {0}",
|
"LabelAddedDate": "Adicionado {0}",
|
||||||
"LabelAdminUsersOnly": "Apenas usuários administradores",
|
"LabelAdminUsersOnly": "Apenas usuários administradores",
|
||||||
"LabelAll": "Todos",
|
"LabelAll": "Todos",
|
||||||
|
"LabelAllEpisodesDownloaded": "Todos os episódios baixados",
|
||||||
"LabelAllUsers": "Todos Usuários",
|
"LabelAllUsers": "Todos Usuários",
|
||||||
"LabelAllUsersExcludingGuests": "Todos usuários exceto convidados",
|
"LabelAllUsersExcludingGuests": "Todos usuários exceto convidados",
|
||||||
"LabelAllUsersIncludingGuests": "Todos usuários incluindo convidados",
|
"LabelAllUsersIncludingGuests": "Todos usuários incluindo convidados",
|
||||||
"LabelAlreadyInYourLibrary": "Já na sua biblioteca",
|
"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",
|
"LabelAppend": "Acrescentar",
|
||||||
|
"LabelAudioBitrate": "Bitrate de áudio (por exemplo, 128k)",
|
||||||
|
"LabelAudioChannels": "Canais de áudio (1 ou 2)",
|
||||||
|
"LabelAudioCodec": "Codec de áudio",
|
||||||
"LabelAuthor": "Autor",
|
"LabelAuthor": "Autor",
|
||||||
"LabelAuthorFirstLast": "Autor (Nome Sobrenome)",
|
"LabelAuthorFirstLast": "Autor (Nome Sobrenome)",
|
||||||
"LabelAuthorLastFirst": "Autor (Sobrenome, Nome)",
|
"LabelAuthorLastFirst": "Autor (Sobrenome, Nome)",
|
||||||
@ -236,24 +263,31 @@
|
|||||||
"LabelAutoRegister": "Registrar Automaticamente",
|
"LabelAutoRegister": "Registrar Automaticamente",
|
||||||
"LabelAutoRegisterDescription": "Registra automaticamente novos usuários após login",
|
"LabelAutoRegisterDescription": "Registra automaticamente novos usuários após login",
|
||||||
"LabelBackToUser": "Voltar para Usuário",
|
"LabelBackToUser": "Voltar para Usuário",
|
||||||
|
"LabelBackupAudioFiles": "Backup dos Arquivos de Áudio",
|
||||||
"LabelBackupLocation": "Localização do Backup",
|
"LabelBackupLocation": "Localização do Backup",
|
||||||
"LabelBackupsEnableAutomaticBackups": "Ativar backups automáticos",
|
"LabelBackupsEnableAutomaticBackups": "Backups automáticos",
|
||||||
"LabelBackupsEnableAutomaticBackupsHelp": "Backups salvos em /metadata/backups",
|
"LabelBackupsEnableAutomaticBackupsHelp": "Backups salvos em /metadata/backups",
|
||||||
"LabelBackupsMaxBackupSize": "Tamanho máximo do backup (em GB)",
|
"LabelBackupsMaxBackupSize": "Tamanho máximo do backup (em GB) (0 para ilimitado)",
|
||||||
"LabelBackupsMaxBackupSizeHelp": "Como proteção contra uma configuração incorreta, backups darão erro se excederem o tamanho configurado.",
|
"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",
|
"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.",
|
"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",
|
"LabelBooks": "Livros",
|
||||||
"LabelButtonText": "Texto do botão",
|
"LabelButtonText": "Texto do botão",
|
||||||
"LabelByAuthor": "por {0}",
|
"LabelByAuthor": "por {0}",
|
||||||
"LabelChangePassword": "Trocar Senha",
|
"LabelChangePassword": "Trocar Senha",
|
||||||
"LabelChannels": "Canais",
|
"LabelChannels": "Canais",
|
||||||
|
"LabelChapterCount": "{0} Capítulos",
|
||||||
"LabelChapterTitle": "Título do Capítulo",
|
"LabelChapterTitle": "Título do Capítulo",
|
||||||
"LabelChapters": "Capítulos",
|
"LabelChapters": "Capítulos",
|
||||||
"LabelChaptersFound": "capítulos encontrados",
|
"LabelChaptersFound": "capítulos encontrados",
|
||||||
"LabelClickForMoreInfo": "Clique para mais informações",
|
"LabelClickForMoreInfo": "Clique para mais informações",
|
||||||
|
"LabelClickToUseCurrentValue": "Clique para usar o valor atual",
|
||||||
"LabelClosePlayer": "Fechar Reprodutor",
|
"LabelClosePlayer": "Fechar Reprodutor",
|
||||||
|
"LabelCodec": "Codec",
|
||||||
"LabelCollapseSeries": "Fechar Série",
|
"LabelCollapseSeries": "Fechar Série",
|
||||||
|
"LabelCollapseSubSeries": "Fechar Sub Séries",
|
||||||
"LabelCollection": "Coleção",
|
"LabelCollection": "Coleção",
|
||||||
"LabelCollections": "Coleções",
|
"LabelCollections": "Coleções",
|
||||||
"LabelComplete": "Concluído",
|
"LabelComplete": "Concluído",
|
||||||
@ -261,17 +295,21 @@
|
|||||||
"LabelContinueListening": "Continuar Escutando",
|
"LabelContinueListening": "Continuar Escutando",
|
||||||
"LabelContinueReading": "Continuar Lendo",
|
"LabelContinueReading": "Continuar Lendo",
|
||||||
"LabelContinueSeries": "Continuar Série",
|
"LabelContinueSeries": "Continuar Série",
|
||||||
|
"LabelCorsAllowed": "Origens Permitidas para CORS",
|
||||||
"LabelCover": "Capa",
|
"LabelCover": "Capa",
|
||||||
"LabelCoverImageURL": "URL da Imagem da Capa",
|
"LabelCoverImageURL": "URL da Imagem da Capa",
|
||||||
|
"LabelCoverProvider": "Provedor de Capas",
|
||||||
"LabelCreatedAt": "Criado em",
|
"LabelCreatedAt": "Criado em",
|
||||||
"LabelCronExpression": "Expressão para o Cron",
|
"LabelCronExpression": "Expressão para o Cron",
|
||||||
"LabelCurrent": "Atual",
|
"LabelCurrent": "Atual",
|
||||||
"LabelCurrently": "Atualmente:",
|
"LabelCurrently": "Atualmente:",
|
||||||
"LabelCustomCronExpression": "Expressão personalizada para o Cron:",
|
"LabelCustomCronExpression": "Expressão personalizada para o Cron:",
|
||||||
"LabelDatetime": "Data e Hora",
|
"LabelDatetime": "Data e Hora",
|
||||||
|
"LabelDays": "Dias",
|
||||||
"LabelDeleteFromFileSystemCheckbox": "Apagar do sistema de arquivos (desmarcar para remover apenas da base de dados)",
|
"LabelDeleteFromFileSystemCheckbox": "Apagar do sistema de arquivos (desmarcar para remover apenas da base de dados)",
|
||||||
"LabelDescription": "Descrição",
|
"LabelDescription": "Descrição",
|
||||||
"LabelDeselectAll": "Desmarcar tudo",
|
"LabelDeselectAll": "Desmarcar tudo",
|
||||||
|
"LabelDetectedPattern": "Padrão detectado:",
|
||||||
"LabelDevice": "Dispositivo",
|
"LabelDevice": "Dispositivo",
|
||||||
"LabelDeviceInfo": "Informação do Dispositivo",
|
"LabelDeviceInfo": "Informação do Dispositivo",
|
||||||
"LabelDeviceIsAvailableTo": "Dispositivo está disponível para...",
|
"LabelDeviceIsAvailableTo": "Dispositivo está disponível para...",
|
||||||
@ -281,6 +319,7 @@
|
|||||||
"LabelDiscover": "Descobrir",
|
"LabelDiscover": "Descobrir",
|
||||||
"LabelDownload": "Download",
|
"LabelDownload": "Download",
|
||||||
"LabelDownloadNEpisodes": "Download de {0} Episódios",
|
"LabelDownloadNEpisodes": "Download de {0} Episódios",
|
||||||
|
"LabelDownloadable": "Baixável",
|
||||||
"LabelDuration": "Duração",
|
"LabelDuration": "Duração",
|
||||||
"LabelDurationComparisonExactMatch": "(exato)",
|
"LabelDurationComparisonExactMatch": "(exato)",
|
||||||
"LabelDurationComparisonLonger": "({0} maior)",
|
"LabelDurationComparisonLonger": "({0} maior)",
|
||||||
@ -289,6 +328,7 @@
|
|||||||
"LabelEbook": "Ebook",
|
"LabelEbook": "Ebook",
|
||||||
"LabelEbooks": "Ebooks",
|
"LabelEbooks": "Ebooks",
|
||||||
"LabelEdit": "Editar",
|
"LabelEdit": "Editar",
|
||||||
|
"LabelEmail": "Email",
|
||||||
"LabelEmailSettingsFromAddress": "Remetente",
|
"LabelEmailSettingsFromAddress": "Remetente",
|
||||||
"LabelEmailSettingsRejectUnauthorized": "Rejeitar certificados não autorizados",
|
"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.",
|
"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.",
|
||||||
@ -297,11 +337,24 @@
|
|||||||
"LabelEmailSettingsTestAddress": "Endereço de teste",
|
"LabelEmailSettingsTestAddress": "Endereço de teste",
|
||||||
"LabelEmbeddedCover": "Capa Integrada",
|
"LabelEmbeddedCover": "Capa Integrada",
|
||||||
"LabelEnable": "Habilitar",
|
"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",
|
"LabelEnd": "Fim",
|
||||||
|
"LabelEndOfChapter": "Fim do Capítulo",
|
||||||
"LabelEpisode": "Episódio",
|
"LabelEpisode": "Episódio",
|
||||||
"LabelEpisodeTitle": "Título do Episódio",
|
"LabelEpisodeTitle": "Título do Episódio",
|
||||||
"LabelEpisodeType": "Tipo do Episódio",
|
"LabelEpisodeType": "Tipo do Episódio",
|
||||||
|
"LabelEpisodes": "Episódios",
|
||||||
"LabelExample": "Exemplo",
|
"LabelExample": "Exemplo",
|
||||||
|
"LabelExpired": "Expirado",
|
||||||
|
"LabelExpiresNever": "Nunca",
|
||||||
"LabelExplicit": "Explícito",
|
"LabelExplicit": "Explícito",
|
||||||
"LabelExplicitChecked": "Explícito (verificado)",
|
"LabelExplicitChecked": "Explícito (verificado)",
|
||||||
"LabelExplicitUnchecked": "Não explícito (não verificado)",
|
"LabelExplicitUnchecked": "Não explícito (não verificado)",
|
||||||
@ -328,8 +381,11 @@
|
|||||||
"LabelHardDeleteFile": "Apagar definitivamente",
|
"LabelHardDeleteFile": "Apagar definitivamente",
|
||||||
"LabelHasEbook": "Tem ebook",
|
"LabelHasEbook": "Tem ebook",
|
||||||
"LabelHasSupplementaryEbook": "Tem ebook complementar",
|
"LabelHasSupplementaryEbook": "Tem ebook complementar",
|
||||||
|
"LabelHideSubtitles": "Esconder Legendas",
|
||||||
"LabelHighestPriority": "Prioridade mais alta",
|
"LabelHighestPriority": "Prioridade mais alta",
|
||||||
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Hora",
|
"LabelHour": "Hora",
|
||||||
|
"LabelHours": "Horas",
|
||||||
"LabelIcon": "Ícone",
|
"LabelIcon": "Ícone",
|
||||||
"LabelImageURLFromTheWeb": "URL da imagem na internet",
|
"LabelImageURLFromTheWeb": "URL da imagem na internet",
|
||||||
"LabelInProgress": "Em Andamento",
|
"LabelInProgress": "Em Andamento",
|
||||||
@ -344,7 +400,9 @@
|
|||||||
"LabelIntervalEvery6Hours": "A cada 6 horas",
|
"LabelIntervalEvery6Hours": "A cada 6 horas",
|
||||||
"LabelIntervalEveryDay": "Todo dia",
|
"LabelIntervalEveryDay": "Todo dia",
|
||||||
"LabelIntervalEveryHour": "Toda hora",
|
"LabelIntervalEveryHour": "Toda hora",
|
||||||
|
"LabelIntervalEveryMinute": "A cada minuto",
|
||||||
"LabelInvert": "Inverter",
|
"LabelInvert": "Inverter",
|
||||||
|
"LabelItem": "Item",
|
||||||
"LabelLanguage": "Idioma",
|
"LabelLanguage": "Idioma",
|
||||||
"LabelLanguageDefaultServer": "Idioma Padrão do Servidor",
|
"LabelLanguageDefaultServer": "Idioma Padrão do Servidor",
|
||||||
"LabelLanguages": "Idiomas",
|
"LabelLanguages": "Idiomas",
|
||||||
@ -353,16 +411,22 @@
|
|||||||
"LabelLastSeen": "Visto pela Última Vez",
|
"LabelLastSeen": "Visto pela Última Vez",
|
||||||
"LabelLastTime": "Progresso",
|
"LabelLastTime": "Progresso",
|
||||||
"LabelLastUpdate": "Última Atualização",
|
"LabelLastUpdate": "Última Atualização",
|
||||||
|
"LabelLayout": "Layout",
|
||||||
"LabelLayoutSinglePage": "Uma página",
|
"LabelLayoutSinglePage": "Uma página",
|
||||||
"LabelLayoutSplitPage": "Página dividida",
|
"LabelLayoutSplitPage": "Página dividida",
|
||||||
"LabelLess": "Menos",
|
"LabelLess": "Menos",
|
||||||
"LabelLibrariesAccessibleToUser": "Bibliotecas Acessíveis ao Usuário",
|
"LabelLibrariesAccessibleToUser": "Bibliotecas Acessíveis ao Usuário",
|
||||||
"LabelLibrary": "Biblioteca",
|
"LabelLibrary": "Biblioteca",
|
||||||
|
"LabelLibraryFilterSublistEmpty": "Sem {0}",
|
||||||
"LabelLibraryItem": "Item da Biblioteca",
|
"LabelLibraryItem": "Item da Biblioteca",
|
||||||
"LabelLibraryName": "Nome da Biblioteca",
|
"LabelLibraryName": "Nome da Biblioteca",
|
||||||
|
"LabelLibrarySortByProgress": "Última Atualização",
|
||||||
|
"LabelLibrarySortByProgressFinished": "Concluído",
|
||||||
"LabelLimit": "Limite",
|
"LabelLimit": "Limite",
|
||||||
"LabelLineSpacing": "Espaçamento entre linhas",
|
"LabelLineSpacing": "Espaçamento entre linhas",
|
||||||
"LabelListenAgain": "Escutar novamente",
|
"LabelListenAgain": "Escutar novamente",
|
||||||
|
"LabelLogLevelDebug": "Debug",
|
||||||
|
"LabelLogLevelInfo": "Info",
|
||||||
"LabelLogLevelWarn": "Atenção",
|
"LabelLogLevelWarn": "Atenção",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Procurar por novos Episódios após essa data",
|
"LabelLookForNewEpisodesAfterDate": "Procurar por novos Episódios após essa data",
|
||||||
"LabelLowestPriority": "Prioridade mais baixa",
|
"LabelLowestPriority": "Prioridade mais baixa",
|
||||||
@ -375,6 +439,7 @@
|
|||||||
"LabelMetadataOrderOfPrecedenceDescription": "Fontes de metadados de alta prioridade terão preferência sobre as fontes de metadados de prioridade baixa",
|
"LabelMetadataOrderOfPrecedenceDescription": "Fontes de metadados de alta prioridade terão preferência sobre as fontes de metadados de prioridade baixa",
|
||||||
"LabelMetadataProvider": "Fonte de Metadados",
|
"LabelMetadataProvider": "Fonte de Metadados",
|
||||||
"LabelMinute": "Minuto",
|
"LabelMinute": "Minuto",
|
||||||
|
"LabelMinutes": "Minutos",
|
||||||
"LabelMissing": "Ausente",
|
"LabelMissing": "Ausente",
|
||||||
"LabelMissingEbook": "Ebook não existe",
|
"LabelMissingEbook": "Ebook não existe",
|
||||||
"LabelMissingSupplementaryEbook": "Ebook complementar não existe",
|
"LabelMissingSupplementaryEbook": "Ebook complementar não existe",
|
||||||
@ -390,6 +455,7 @@
|
|||||||
"LabelNewestAuthors": "Novos Autores",
|
"LabelNewestAuthors": "Novos Autores",
|
||||||
"LabelNewestEpisodes": "Episódios mais recentes",
|
"LabelNewestEpisodes": "Episódios mais recentes",
|
||||||
"LabelNextBackupDate": "Data do próximo backup",
|
"LabelNextBackupDate": "Data do próximo backup",
|
||||||
|
"LabelNextChapters": "Próximo capítulo será:",
|
||||||
"LabelNextScheduledRun": "Próxima execução programada",
|
"LabelNextScheduledRun": "Próxima execução programada",
|
||||||
"LabelNoCustomMetadataProviders": "Não existem fontes de metadados customizados",
|
"LabelNoCustomMetadataProviders": "Não existem fontes de metadados customizados",
|
||||||
"LabelNoEpisodesSelected": "Nenhum episódio selecionado",
|
"LabelNoEpisodesSelected": "Nenhum episódio selecionado",
|
||||||
@ -412,8 +478,10 @@
|
|||||||
"LabelOpenIDGroupClaimDescription": "Nome do claim OpenID contendo a lista de grupos do usuário, normalmente chamada de <code>groups</code>. <b>Se configurada</b>, 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.",
|
"LabelOpenIDGroupClaimDescription": "Nome do claim OpenID contendo a lista de grupos do usuário, normalmente chamada de <code>groups</code>. <b>Se configurada</b>, 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",
|
"LabelOpenRSSFeed": "Abrir Feed RSS",
|
||||||
"LabelOverwrite": "Sobrescrever",
|
"LabelOverwrite": "Sobrescrever",
|
||||||
|
"LabelPaginationPageXOfY": "Página {0} de {1}",
|
||||||
"LabelPassword": "Senha",
|
"LabelPassword": "Senha",
|
||||||
"LabelPath": "Caminho",
|
"LabelPath": "Caminho",
|
||||||
|
"LabelPermanent": "Permanente",
|
||||||
"LabelPermissionsAccessAllLibraries": "Pode Acessar Todas Bibliotecas",
|
"LabelPermissionsAccessAllLibraries": "Pode Acessar Todas Bibliotecas",
|
||||||
"LabelPermissionsAccessAllTags": "Pode Acessar Todas as Etiquetas",
|
"LabelPermissionsAccessAllTags": "Pode Acessar Todas as Etiquetas",
|
||||||
"LabelPermissionsAccessExplicitContent": "Pode Acessar Conteúdos Explícitos",
|
"LabelPermissionsAccessExplicitContent": "Pode Acessar Conteúdos Explícitos",
|
||||||
@ -424,9 +492,12 @@
|
|||||||
"LabelPersonalYearReview": "Sua Retrospectiva Anual ({0})",
|
"LabelPersonalYearReview": "Sua Retrospectiva Anual ({0})",
|
||||||
"LabelPhotoPathURL": "Caminho/URL para Foto",
|
"LabelPhotoPathURL": "Caminho/URL para Foto",
|
||||||
"LabelPlayMethod": "Método de Reprodução",
|
"LabelPlayMethod": "Método de Reprodução",
|
||||||
|
"LabelPlayerChapterNumberMarker": "{0} de {1}",
|
||||||
"LabelPlaylists": "Listas de Reprodução",
|
"LabelPlaylists": "Listas de Reprodução",
|
||||||
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcastSearchRegion": "Região de busca do podcast",
|
"LabelPodcastSearchRegion": "Região de busca do podcast",
|
||||||
"LabelPodcastType": "Tipo de Podcast",
|
"LabelPodcastType": "Tipo de Podcast",
|
||||||
|
"LabelPodcasts": "Podcasts",
|
||||||
"LabelPort": "Porta",
|
"LabelPort": "Porta",
|
||||||
"LabelPrefixesToIgnore": "Prefixos para Ignorar (sem distinção entre maiúsculas e minúsculas)",
|
"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",
|
"LabelPreventIndexing": "Evitar que o seu feed seja indexado pelos diretórios de podcast do iTunes e Google",
|
||||||
@ -435,14 +506,16 @@
|
|||||||
"LabelProvider": "Fonte",
|
"LabelProvider": "Fonte",
|
||||||
"LabelPubDate": "Data de Publicação",
|
"LabelPubDate": "Data de Publicação",
|
||||||
"LabelPublishYear": "Ano de Publicação",
|
"LabelPublishYear": "Ano de Publicação",
|
||||||
|
"LabelPublishedDate": "Publicado {0}",
|
||||||
"LabelPublisher": "Editora",
|
"LabelPublisher": "Editora",
|
||||||
"LabelPublishers": "Editoras",
|
"LabelPublishers": "Editoras",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "E-mail do dono personalizado",
|
"LabelRSSFeedCustomOwnerEmail": "E-mail do dono personalizado",
|
||||||
"LabelRSSFeedCustomOwnerName": "Nome do dono personalizado",
|
"LabelRSSFeedCustomOwnerName": "Nome do dono personalizado",
|
||||||
"LabelRSSFeedOpen": "Feed RSS Aberto",
|
"LabelRSSFeedOpen": "Feed de RSS Aberto",
|
||||||
"LabelRSSFeedPreventIndexing": "Impedir Indexação",
|
"LabelRSSFeedPreventIndexing": "Impedir Indexação",
|
||||||
"LabelRSSFeedSlug": "Slug do Feed RSS",
|
"LabelRSSFeedSlug": "Slug do Feed RSS",
|
||||||
"LabelRSSFeedURL": "URL do Feed RSS",
|
"LabelRSSFeedURL": "URL do Feed RSS",
|
||||||
|
"LabelRandomly": "Aleatoriamente",
|
||||||
"LabelRead": "Lido",
|
"LabelRead": "Lido",
|
||||||
"LabelReadAgain": "Ler novamente",
|
"LabelReadAgain": "Ler novamente",
|
||||||
"LabelReadEbookWithoutProgress": "Ler ebook sem armazenar progresso",
|
"LabelReadEbookWithoutProgress": "Ler ebook sem armazenar progresso",
|
||||||
@ -475,6 +548,8 @@
|
|||||||
"LabelSettingsBookshelfViewHelp": "Aparência esqueomorfa com prateleiras de madeira",
|
"LabelSettingsBookshelfViewHelp": "Aparência esqueomorfa com prateleiras de madeira",
|
||||||
"LabelSettingsChromecastSupport": "Suporte ao Chromecast",
|
"LabelSettingsChromecastSupport": "Suporte ao Chromecast",
|
||||||
"LabelSettingsDateFormat": "Formato de data",
|
"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",
|
"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",
|
"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.",
|
"LabelSettingsEpubsAllowScriptedContentHelp": "Permitir que arquivos epub executem scripts. É recomendado manter essa configuração desativada, a não ser que confie na fonte dos arquivos epub.",
|
||||||
@ -503,10 +578,14 @@
|
|||||||
"LabelSettingsStoreMetadataWithItem": "Armazenar metadados com o item",
|
"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",
|
"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",
|
"LabelSettingsTimeFormat": "Formato da Tempo",
|
||||||
|
"LabelShare": "Compartilhar",
|
||||||
|
"LabelShareURL": "Compartilhar URL",
|
||||||
"LabelShowAll": "Exibir Todos",
|
"LabelShowAll": "Exibir Todos",
|
||||||
"LabelShowSeconds": "Exibir segundos",
|
"LabelShowSeconds": "Exibir segundos",
|
||||||
|
"LabelShowSubtitles": "Mostrar Legendas",
|
||||||
"LabelSize": "Tamanho",
|
"LabelSize": "Tamanho",
|
||||||
"LabelSleepTimer": "Timer",
|
"LabelSleepTimer": "Timer",
|
||||||
|
"LabelSlug": "Slug",
|
||||||
"LabelStart": "Iniciar",
|
"LabelStart": "Iniciar",
|
||||||
"LabelStartTime": "Horário do Início",
|
"LabelStartTime": "Horário do Início",
|
||||||
"LabelStarted": "Iniciado",
|
"LabelStarted": "Iniciado",
|
||||||
@ -534,12 +613,17 @@
|
|||||||
"LabelTagsNotAccessibleToUser": "Etiquetas não Acessíveis Usuário",
|
"LabelTagsNotAccessibleToUser": "Etiquetas não Acessíveis Usuário",
|
||||||
"LabelTasks": "Tarefas em Execuçào",
|
"LabelTasks": "Tarefas em Execuçào",
|
||||||
"LabelTextEditorBulletedList": "Lista com marcadores",
|
"LabelTextEditorBulletedList": "Lista com marcadores",
|
||||||
|
"LabelTextEditorLink": "Link",
|
||||||
"LabelTextEditorNumberedList": "Lista numerada",
|
"LabelTextEditorNumberedList": "Lista numerada",
|
||||||
"LabelTextEditorUnlink": "Remover link",
|
"LabelTextEditorUnlink": "Remover link",
|
||||||
"LabelTheme": "Tema",
|
"LabelTheme": "Tema",
|
||||||
"LabelThemeDark": "Escuro",
|
"LabelThemeDark": "Escuro",
|
||||||
"LabelThemeLight": "Claro",
|
"LabelThemeLight": "Claro",
|
||||||
|
"LabelThemeSepia": "Sépia",
|
||||||
"LabelTimeBase": "Base de tempo",
|
"LabelTimeBase": "Base de tempo",
|
||||||
|
"LabelTimeDurationXHours": "{0} horas",
|
||||||
|
"LabelTimeDurationXMinutes": "{0} minutos",
|
||||||
|
"LabelTimeDurationXSeconds": "{0} segundos",
|
||||||
"LabelTimeListened": "Tempo de escuta",
|
"LabelTimeListened": "Tempo de escuta",
|
||||||
"LabelTimeListenedToday": "Tempo de escuta hoje",
|
"LabelTimeListenedToday": "Tempo de escuta hoje",
|
||||||
"LabelTimeRemaining": "{0} restantes",
|
"LabelTimeRemaining": "{0} restantes",
|
||||||
@ -559,6 +643,7 @@
|
|||||||
"LabelTracksMultiTrack": "Várias trilhas",
|
"LabelTracksMultiTrack": "Várias trilhas",
|
||||||
"LabelTracksNone": "Sem trilha",
|
"LabelTracksNone": "Sem trilha",
|
||||||
"LabelTracksSingleTrack": "Trilha única",
|
"LabelTracksSingleTrack": "Trilha única",
|
||||||
|
"LabelTrailer": "Trailer",
|
||||||
"LabelType": "Tipo",
|
"LabelType": "Tipo",
|
||||||
"LabelUnabridged": "Não Abreviada",
|
"LabelUnabridged": "Não Abreviada",
|
||||||
"LabelUndo": "Desfazer",
|
"LabelUndo": "Desfazer",
|
||||||
@ -580,15 +665,18 @@
|
|||||||
"LabelViewBookmarks": "Ver marcadores",
|
"LabelViewBookmarks": "Ver marcadores",
|
||||||
"LabelViewChapters": "Ver capítulos",
|
"LabelViewChapters": "Ver capítulos",
|
||||||
"LabelViewQueue": "Ver fila do reprodutor",
|
"LabelViewQueue": "Ver fila do reprodutor",
|
||||||
|
"LabelVolume": "Volume",
|
||||||
"LabelWeekdaysToRun": "Dias da semana para executar",
|
"LabelWeekdaysToRun": "Dias da semana para executar",
|
||||||
"LabelYearReviewHide": "Ocultar Retrospectiva Anual",
|
"LabelXBooks": "{0} livros",
|
||||||
"LabelYearReviewShow": "Exibir Retrospectiva Anual",
|
"LabelXItems": "{0} itens",
|
||||||
|
"LabelYearReviewHide": "Ocultar Retrospectiva",
|
||||||
|
"LabelYearReviewShow": "Exibir Retrospectiva",
|
||||||
"LabelYourAudiobookDuration": "Duração do seu audiobook",
|
"LabelYourAudiobookDuration": "Duração do seu audiobook",
|
||||||
"LabelYourBookmarks": "Seus Marcadores",
|
"LabelYourBookmarks": "Seus Marcadores",
|
||||||
"LabelYourPlaylists": "Suas Listas de Reprodução",
|
"LabelYourPlaylists": "Suas Listas de Reprodução",
|
||||||
"LabelYourProgress": "Seu Progresso",
|
"LabelYourProgress": "Seu Progresso",
|
||||||
"MessageAddToPlayerQueue": "Adicionar à lista do reprodutor",
|
"MessageAddToPlayerQueue": "Adicionar à lista do reprodutor",
|
||||||
"MessageAppriseDescription": "Para utilizar essa funcionalidade é preciso ter uma instância da <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API do Apprise</a> em execução ou uma api que possa tratar esses mesmos chamados. <br />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 <code>http://192.168.1.1:8337</code> você deve utilizar <code>http://192.168.1.1:8337/notify</code>.",
|
"MessageAppriseDescription": "Para utilizar essa funcionalidade é preciso ter uma instância da <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API do Apprise</a> em execução ou uma API que possa tratar esses mesmos chamados. <br />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 <code>http://192.168.1.1:8337</code> você deve utilizar <code>http://192.168.1.1:8337/notify</code>.",
|
||||||
"MessageBackupsDescription": "Backups incluem usuários, progresso dos usuários, detalhes dos itens da biblioteca, configurações do servidor e imagens armazenadas em <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups <strong>não</strong> incluem quaisquer arquivos armazenados nas pastas da sua biblioteca.",
|
"MessageBackupsDescription": "Backups incluem usuários, progresso dos usuários, detalhes dos itens da biblioteca, configurações do servidor e imagens armazenadas em <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups <strong>não</strong> 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.",
|
"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",
|
"MessageBookshelfNoCollections": "Você ainda não criou coleções",
|
||||||
@ -643,8 +731,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.",
|
"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!",
|
"MessageImportantNotice": "Aviso Importante!",
|
||||||
"MessageInsertChapterBelow": "Inserir capítulo abaixo",
|
"MessageInsertChapterBelow": "Inserir capítulo abaixo",
|
||||||
"MessageItemsSelected": "{0} Itens Selecionados",
|
"MessageItemsSelected": "{0} itens selecionados",
|
||||||
"MessageItemsUpdated": "{0} Itens Atualizados",
|
"MessageItemsUpdated": "{0} itens atualizados",
|
||||||
"MessageJoinUsOn": "Junte-se a nós",
|
"MessageJoinUsOn": "Junte-se a nós",
|
||||||
"MessageLoading": "Carregando...",
|
"MessageLoading": "Carregando...",
|
||||||
"MessageLoadingFolders": "Carregando pastas...",
|
"MessageLoadingFolders": "Carregando pastas...",
|
||||||
@ -692,6 +780,7 @@
|
|||||||
"MessagePlayChapter": "Escutar o início do capítulo",
|
"MessagePlayChapter": "Escutar o início do capítulo",
|
||||||
"MessagePlaylistCreateFromCollection": "Criar uma lista de reprodução a partir da coleção",
|
"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",
|
"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.",
|
"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",
|
"MessageRemoveChapter": "Remover capítulo",
|
||||||
"MessageRemoveEpisodes": "Remover {0} episódio(s)",
|
"MessageRemoveEpisodes": "Remover {0} episódio(s)",
|
||||||
@ -706,6 +795,7 @@
|
|||||||
"MessageServerCouldNotBeReached": "Não foi possível estabelecer conexão com o servidor",
|
"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",
|
"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}?",
|
"MessageStartPlaybackAtTime": "Iniciar a reprodução de \"{0}\" em {1}?",
|
||||||
|
"MessageTaskFailed": "Falhou",
|
||||||
"MessageThinking": "Pensando...",
|
"MessageThinking": "Pensando...",
|
||||||
"MessageUploaderItemFailed": "Falha no upload",
|
"MessageUploaderItemFailed": "Falha no upload",
|
||||||
"MessageUploaderItemSuccess": "Upload realizado!",
|
"MessageUploaderItemSuccess": "Upload realizado!",
|
||||||
@ -723,12 +813,19 @@
|
|||||||
"NoteUploaderFoldersWithMediaFiles": "Pastas com arquivos de mídia serão tratadas como itens de biblioteca distintos.",
|
"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.",
|
"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.",
|
"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",
|
"PlaceholderNewCollection": "Novo nome da coleção",
|
||||||
"PlaceholderNewFolderPath": "Novo caminho para a pasta",
|
"PlaceholderNewFolderPath": "Novo caminho para a pasta",
|
||||||
"PlaceholderNewPlaylist": "Novo nome da lista de reprodução",
|
"PlaceholderNewPlaylist": "Novo nome da lista de reprodução",
|
||||||
"PlaceholderSearch": "Buscar..",
|
"PlaceholderSearch": "Buscar..",
|
||||||
"PlaceholderSearchEpisode": "Buscar Episódio..",
|
"PlaceholderSearchEpisode": "Buscar Episódio..",
|
||||||
|
"StatsAuthorsAdded": "autores adicionados",
|
||||||
|
"StatsBooksAdded": "livros adicionados",
|
||||||
|
"StatsBooksFinished": "livros concluídos",
|
||||||
|
"StatsTopAuthor": "TOP AUTOR",
|
||||||
|
"StatsTopAuthors": "TOP AUTORES",
|
||||||
"ToastAccountUpdateSuccess": "Conta atualizada",
|
"ToastAccountUpdateSuccess": "Conta atualizada",
|
||||||
|
"ToastAppriseUrlRequired": "É preciso digitar uma URL Apprise",
|
||||||
"ToastAuthorImageRemoveSuccess": "Imagem do autor removida",
|
"ToastAuthorImageRemoveSuccess": "Imagem do autor removida",
|
||||||
"ToastAuthorUpdateMerged": "Autor combinado",
|
"ToastAuthorUpdateMerged": "Autor combinado",
|
||||||
"ToastAuthorUpdateSuccess": "Autor atualizado",
|
"ToastAuthorUpdateSuccess": "Autor atualizado",
|
||||||
@ -745,6 +842,7 @@
|
|||||||
"ToastBookmarkCreateFailed": "Falha ao criar marcador",
|
"ToastBookmarkCreateFailed": "Falha ao criar marcador",
|
||||||
"ToastBookmarkCreateSuccess": "Marcador adicionado",
|
"ToastBookmarkCreateSuccess": "Marcador adicionado",
|
||||||
"ToastBookmarkRemoveSuccess": "Marcador removido",
|
"ToastBookmarkRemoveSuccess": "Marcador removido",
|
||||||
|
"ToastBulkChapterInvalidCount": "Digite um número entre 1 e 150",
|
||||||
"ToastCachePurgeFailed": "Falha ao apagar o cache",
|
"ToastCachePurgeFailed": "Falha ao apagar o cache",
|
||||||
"ToastCachePurgeSuccess": "Cache apagado com sucesso",
|
"ToastCachePurgeSuccess": "Cache apagado com sucesso",
|
||||||
"ToastChaptersHaveErrors": "Capítulos com erro",
|
"ToastChaptersHaveErrors": "Capítulos com erro",
|
||||||
@ -767,6 +865,7 @@
|
|||||||
"ToastLibraryScanFailedToStart": "Falha ao iniciar verificação",
|
"ToastLibraryScanFailedToStart": "Falha ao iniciar verificação",
|
||||||
"ToastLibraryScanStarted": "Verificação da biblioteca iniciada",
|
"ToastLibraryScanStarted": "Verificação da biblioteca iniciada",
|
||||||
"ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" atualizada",
|
"ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" atualizada",
|
||||||
|
"ToastNewUserUsernameError": "Digite um nome de usuário",
|
||||||
"ToastPlaylistCreateFailed": "Falha ao criar lista de reprodução",
|
"ToastPlaylistCreateFailed": "Falha ao criar lista de reprodução",
|
||||||
"ToastPlaylistCreateSuccess": "Lista de reprodução criada",
|
"ToastPlaylistCreateSuccess": "Lista de reprodução criada",
|
||||||
"ToastPlaylistRemoveSuccess": "Lista de reprodução removida",
|
"ToastPlaylistRemoveSuccess": "Lista de reprodução removida",
|
||||||
@ -790,5 +889,6 @@
|
|||||||
"ToastSortingPrefixesEmptyError": "É preciso ter pelo menos um prefixo de ordenação",
|
"ToastSortingPrefixesEmptyError": "É preciso ter pelo menos um prefixo de ordenação",
|
||||||
"ToastSortingPrefixesUpdateSuccess": "Prefixos de ordenação atualizados ({0} item(ns))",
|
"ToastSortingPrefixesUpdateSuccess": "Prefixos de ordenação atualizados ({0} item(ns))",
|
||||||
"ToastUserDeleteFailed": "Falha ao apagar usuário",
|
"ToastUserDeleteFailed": "Falha ao apagar usuário",
|
||||||
"ToastUserDeleteSuccess": "Usuário apagado"
|
"ToastUserDeleteSuccess": "Usuário apagado",
|
||||||
|
"ToastUserRootRequireName": "É preciso entrar com um nome de usuário root"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,9 +12,9 @@ class TokenManager {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
/** @type {number} Refresh token expiry in seconds */
|
/** @type {number} Refresh token expiry in seconds */
|
||||||
this.RefreshTokenExpiry = parseInt(process.env.REFRESH_TOKEN_EXPIRY) || 7 * 24 * 60 * 60 // 7 days
|
this.RefreshTokenExpiry = parseInt(process.env.REFRESH_TOKEN_EXPIRY) || 30 * 24 * 60 * 60 // 30 days
|
||||||
/** @type {number} Access token expiry in seconds */
|
/** @type {number} Access token expiry in seconds */
|
||||||
this.AccessTokenExpiry = parseInt(process.env.ACCESS_TOKEN_EXPIRY) || 12 * 60 * 60 // 12 hours
|
this.AccessTokenExpiry = parseInt(process.env.ACCESS_TOKEN_EXPIRY) || 1 * 60 * 60 // 1 hour
|
||||||
|
|
||||||
if (parseInt(process.env.REFRESH_TOKEN_EXPIRY) > 0) {
|
if (parseInt(process.env.REFRESH_TOKEN_EXPIRY) > 0) {
|
||||||
Logger.info(`[TokenManager] Refresh token expiry set from ENV variable to ${this.RefreshTokenExpiry} seconds`)
|
Logger.info(`[TokenManager] Refresh token expiry set from ENV variable to ${this.RefreshTokenExpiry} seconds`)
|
||||||
|
|||||||
@ -221,13 +221,11 @@ class LibraryController {
|
|||||||
const includeArray = (req.query.include || '').split(',')
|
const includeArray = (req.query.include || '').split(',')
|
||||||
if (includeArray.includes('filterdata')) {
|
if (includeArray.includes('filterdata')) {
|
||||||
const filterdata = await libraryFilters.getFilterData(req.library.mediaType, req.library.id)
|
const filterdata = await libraryFilters.getFilterData(req.library.mediaType, req.library.id)
|
||||||
const customMetadataProviders = await Database.customMetadataProviderModel.getForClientByMediaType(req.library.mediaType)
|
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
filterdata,
|
filterdata,
|
||||||
issues: filterdata.numIssues,
|
issues: filterdata.numIssues,
|
||||||
numUserPlaylists: await Database.playlistModel.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id),
|
numUserPlaylists: await Database.playlistModel.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id),
|
||||||
customMetadataProviders,
|
|
||||||
library: req.library.toOldJSON()
|
library: req.library.toOldJSON()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,29 @@ const BookFinder = require('../finders/BookFinder')
|
|||||||
const PodcastFinder = require('../finders/PodcastFinder')
|
const PodcastFinder = require('../finders/PodcastFinder')
|
||||||
const AuthorFinder = require('../finders/AuthorFinder')
|
const AuthorFinder = require('../finders/AuthorFinder')
|
||||||
const Database = require('../Database')
|
const Database = require('../Database')
|
||||||
const { isValidASIN } = require('../utils')
|
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'
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef RequestUserObject
|
* @typedef RequestUserObject
|
||||||
@ -16,6 +38,44 @@ const { isValidASIN } = require('../utils')
|
|||||||
class SearchController {
|
class SearchController {
|
||||||
constructor() {}
|
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<import('../models/LibraryItem').LibraryItemExpanded>}
|
||||||
|
*/
|
||||||
|
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
|
* GET: /api/search/books
|
||||||
*
|
*
|
||||||
@ -23,19 +83,25 @@ class SearchController {
|
|||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async findBooks(req, res) {
|
async findBooks(req, res) {
|
||||||
const id = req.query.id
|
try {
|
||||||
const libraryItem = await Database.libraryItemModel.getExpandedById(id)
|
const query = req.query
|
||||||
const provider = req.query.provider || 'google'
|
const provider = getQueryParamAsString(query, 'provider', 'google')
|
||||||
const title = req.query.title || ''
|
const title = getQueryParamAsString(query, 'title', '')
|
||||||
const author = req.query.author || ''
|
const author = getQueryParamAsString(query, 'author', '')
|
||||||
|
const id = getQueryParamAsString(query, 'id', '', true)
|
||||||
|
|
||||||
if (typeof provider !== 'string' || typeof title !== 'string' || typeof author !== 'string') {
|
// Fetch library item
|
||||||
Logger.error(`[SearchController] findBooks: Invalid request query params`)
|
const libraryItem = await SearchController.fetchLibraryItem(id)
|
||||||
return res.status(400).send('Invalid request query params')
|
|
||||||
|
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' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = await BookFinder.search(libraryItem, provider, title, author)
|
|
||||||
res.json(results)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -45,20 +111,24 @@ class SearchController {
|
|||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async findCovers(req, res) {
|
async findCovers(req, res) {
|
||||||
const query = req.query
|
try {
|
||||||
const podcast = query.podcast == 1
|
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')
|
||||||
|
|
||||||
if (!query.title || typeof query.title !== 'string') {
|
let results = null
|
||||||
Logger.error(`[SearchController] findCovers: Invalid title sent in query`)
|
if (podcast) results = await PodcastFinder.findCovers(title)
|
||||||
return res.sendStatus(400)
|
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' })
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -69,34 +139,42 @@ class SearchController {
|
|||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async findPodcasts(req, res) {
|
async findPodcasts(req, res) {
|
||||||
const term = req.query.term
|
try {
|
||||||
const country = req.query.country || 'us'
|
const query = req.query
|
||||||
if (!term) {
|
const term = getQueryParamAsString(query, 'term', '', true)
|
||||||
Logger.error('[SearchController] Invalid request query param "term" is required')
|
const country = getQueryParamAsString(query, 'country', 'us')
|
||||||
return res.status(400).send('Invalid request query param "term" is required')
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = await PodcastFinder.search(term, {
|
const results = await PodcastFinder.search(term, { country })
|
||||||
country
|
res.json(results)
|
||||||
})
|
} catch (error) {
|
||||||
res.json(results)
|
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' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET: /api/search/authors
|
* GET: /api/search/authors
|
||||||
|
* Note: This endpoint is not currently used in the web client.
|
||||||
*
|
*
|
||||||
* @param {RequestWithUser} req
|
* @param {RequestWithUser} req
|
||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async findAuthor(req, res) {
|
async findAuthor(req, res) {
|
||||||
const query = req.query.q
|
try {
|
||||||
if (!query || typeof query !== 'string') {
|
const query = getQueryParamAsString(req.query, 'q', '', true)
|
||||||
Logger.error(`[SearchController] findAuthor: Invalid query param`)
|
|
||||||
return res.status(400).send('Invalid query param')
|
|
||||||
}
|
|
||||||
|
|
||||||
const author = await AuthorFinder.findAuthorByName(query)
|
const author = await AuthorFinder.findAuthorByName(query)
|
||||||
res.json(author)
|
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' })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -106,16 +184,55 @@ class SearchController {
|
|||||||
* @param {Response} res
|
* @param {Response} res
|
||||||
*/
|
*/
|
||||||
async findChapters(req, res) {
|
async findChapters(req, res) {
|
||||||
const asin = req.query.asin
|
try {
|
||||||
if (!isValidASIN(asin.toUpperCase())) {
|
const query = req.query
|
||||||
return res.json({ error: 'Invalid ASIN', stringKey: 'MessageInvalidAsin' })
|
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 region = (req.query.region || 'us').toLowerCase()
|
}
|
||||||
const chapterData = await BookFinder.findChapters(asin, region)
|
|
||||||
if (!chapterData) {
|
/**
|
||||||
return res.json({ error: 'Chapters not found', stringKey: 'MessageChaptersNotFound' })
|
* 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)]
|
||||||
}
|
}
|
||||||
res.json(chapterData)
|
|
||||||
|
res.json({ providers })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = new SearchController()
|
module.exports = new SearchController()
|
||||||
|
|||||||
@ -385,6 +385,11 @@ class BookFinder {
|
|||||||
|
|
||||||
if (!title) return books
|
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())
|
const isTitleAsin = isValidASIN(title.toUpperCase())
|
||||||
|
|
||||||
let actualTitleQuery = title
|
let actualTitleQuery = title
|
||||||
@ -402,7 +407,8 @@ class BookFinder {
|
|||||||
let authorCandidates = new BookFinder.AuthorCandidates(cleanAuthor, this.audnexus)
|
let authorCandidates = new BookFinder.AuthorCandidates(cleanAuthor, this.audnexus)
|
||||||
|
|
||||||
// Remove underscores and parentheses with their contents, and replace with a separator
|
// Remove underscores and parentheses with their contents, and replace with a separator
|
||||||
const cleanTitle = title.replace(/\[.*?\]|\(.*?\)|{.*?}|_/g, ' - ')
|
// Use negated character classes to prevent ReDoS vulnerability (input length validated at entry point)
|
||||||
|
const cleanTitle = title.replace(/\[[^\]]*\]|\([^)]*\)|{[^}]*}|_/g, ' - ')
|
||||||
// Split title into hypen-separated parts
|
// Split title into hypen-separated parts
|
||||||
const titleParts = cleanTitle.split(/ - | -|- /)
|
const titleParts = cleanTitle.split(/ - | -|- /)
|
||||||
for (const titlePart of titleParts) authorCandidates.add(titlePart)
|
for (const titlePart of titleParts) authorCandidates.add(titlePart)
|
||||||
@ -668,7 +674,9 @@ function cleanTitleForCompares(title, keepSubtitle = false) {
|
|||||||
let stripped = keepSubtitle ? title : stripSubtitle(title)
|
let stripped = keepSubtitle ? title : stripSubtitle(title)
|
||||||
|
|
||||||
// Remove text in paranthesis (i.e. "Ender's Game (Ender's Saga)" becomes "Ender's Game")
|
// Remove text in paranthesis (i.e. "Ender's Game (Ender's Saga)" becomes "Ender's Game")
|
||||||
let cleaned = stripped.replace(/ *\([^)]*\) */g, '')
|
// 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
|
||||||
|
|
||||||
// Remove single quotes (i.e. "Ender's Game" becomes "Enders Game")
|
// Remove single quotes (i.e. "Ender's Game" becomes "Enders Game")
|
||||||
cleaned = cleaned.replace(/'/g, '')
|
cleaned = cleaned.replace(/'/g, '')
|
||||||
|
|||||||
@ -7,9 +7,9 @@ class PodcastFinder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string} term
|
* @param {string} term
|
||||||
* @param {{country:string}} options
|
* @param {{country:string}} options
|
||||||
* @returns {Promise<import('../providers/iTunes').iTunesPodcastSearchResult[]>}
|
* @returns {Promise<import('../providers/iTunes').iTunesPodcastSearchResult[]>}
|
||||||
*/
|
*/
|
||||||
async search(term, options = {}) {
|
async search(term, options = {}) {
|
||||||
@ -20,12 +20,16 @@ class PodcastFinder {
|
|||||||
return results
|
return results
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} term
|
||||||
|
* @returns {Promise<string[]>}
|
||||||
|
*/
|
||||||
async findCovers(term) {
|
async findCovers(term) {
|
||||||
if (!term) return null
|
if (!term) return null
|
||||||
Logger.debug(`[iTunes] Searching for podcast covers with term "${term}"`)
|
Logger.debug(`[iTunes] Searching for podcast covers with term "${term}"`)
|
||||||
var results = await this.iTunesApi.searchPodcasts(term)
|
const results = await this.iTunesApi.searchPodcasts(term)
|
||||||
if (!results) return []
|
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()
|
||||||
|
|||||||
@ -224,6 +224,9 @@ class CoverSearchManager {
|
|||||||
if (!Array.isArray(results)) return covers
|
if (!Array.isArray(results)) return covers
|
||||||
|
|
||||||
results.forEach((result) => {
|
results.forEach((result) => {
|
||||||
|
if (typeof result === 'string') {
|
||||||
|
covers.push(result)
|
||||||
|
}
|
||||||
if (result.covers && Array.isArray(result.covers)) {
|
if (result.covers && Array.isArray(result.covers)) {
|
||||||
covers.push(...result.covers)
|
covers.push(...result.covers)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -73,7 +73,7 @@ class Stream extends EventEmitter {
|
|||||||
return [AudioMimeType.FLAC, AudioMimeType.OPUS, AudioMimeType.WMA, AudioMimeType.AIFF, AudioMimeType.WEBM, AudioMimeType.WEBMA, AudioMimeType.AWB, AudioMimeType.CAF]
|
return [AudioMimeType.FLAC, AudioMimeType.OPUS, AudioMimeType.WMA, AudioMimeType.AIFF, AudioMimeType.WEBM, AudioMimeType.WEBMA, AudioMimeType.AWB, AudioMimeType.CAF]
|
||||||
}
|
}
|
||||||
get codecsToForceAAC() {
|
get codecsToForceAAC() {
|
||||||
return ['alac']
|
return ['alac', 'ac3', 'eac3']
|
||||||
}
|
}
|
||||||
get userToken() {
|
get userToken() {
|
||||||
return this.user.token
|
return this.user.token
|
||||||
@ -273,7 +273,16 @@ class Stream extends EventEmitter {
|
|||||||
audioCodec = 'aac'
|
audioCodec = 'aac'
|
||||||
}
|
}
|
||||||
|
|
||||||
this.ffmpeg.addOption([`-loglevel ${logLevel}`, '-map 0:a', `-c:a ${audioCodec}`])
|
const codecOptions = [`-loglevel ${logLevel}`, '-map 0:a']
|
||||||
|
|
||||||
|
if (['ac3', 'eac3'].includes(this.tracksCodec) && this.tracks.length > 0 && this.tracks[0].bitRate && this.tracks[0].channels) {
|
||||||
|
// In case for ac3/eac3 it needs to be passed the bitrate and channels to avoid ffmpeg errors
|
||||||
|
codecOptions.push(`-c:a ${audioCodec}`, `-b:a ${this.tracks[0].bitRate}`, `-ac ${this.tracks[0].channels}`)
|
||||||
|
} else {
|
||||||
|
codecOptions.push(`-c:a ${audioCodec}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ffmpeg.addOption(codecOptions)
|
||||||
const hlsOptions = ['-f hls', '-copyts', '-avoid_negative_ts make_non_negative', '-max_delay 5000000', '-max_muxing_queue_size 2048', `-hls_time 6`, `-hls_segment_type ${this.hlsSegmentType}`, `-start_number ${this.segmentStartNumber}`, '-hls_playlist_type vod', '-hls_list_size 0', '-hls_allow_cache 0']
|
const hlsOptions = ['-f hls', '-copyts', '-avoid_negative_ts make_non_negative', '-max_delay 5000000', '-max_muxing_queue_size 2048', `-hls_time 6`, `-hls_segment_type ${this.hlsSegmentType}`, `-start_number ${this.segmentStartNumber}`, '-hls_playlist_type vod', '-hls_list_size 0', '-hls_allow_cache 0']
|
||||||
this.ffmpeg.addOption(hlsOptions)
|
this.ffmpeg.addOption(hlsOptions)
|
||||||
if (this.hlsSegmentType === 'fmp4') {
|
if (this.hlsSegmentType === 'fmp4') {
|
||||||
|
|||||||
@ -283,6 +283,7 @@ class ApiRouter {
|
|||||||
this.router.get('/search/podcast', SearchController.findPodcasts.bind(this))
|
this.router.get('/search/podcast', SearchController.findPodcasts.bind(this))
|
||||||
this.router.get('/search/authors', SearchController.findAuthor.bind(this))
|
this.router.get('/search/authors', SearchController.findAuthor.bind(this))
|
||||||
this.router.get('/search/chapters', SearchController.findChapters.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)
|
// Cache Routes (Admin and up)
|
||||||
|
|||||||
@ -277,3 +277,57 @@ module.exports.timestampToSeconds = (timestamp) => {
|
|||||||
}
|
}
|
||||||
return null
|
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)
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user