mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-07-10 03:11:34 +00:00
Merge branch 'refs/heads/master' into mf/rssInboundManager
This commit is contained in:
commit
31e50e44ed
62 changed files with 1248 additions and 793 deletions
|
|
@ -9,7 +9,7 @@
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="w-40 p-2">
|
<div class="w-40 p-2">
|
||||||
<div class="w-full h-45 relative">
|
<div class="w-full h-45 relative">
|
||||||
<covers-author-image :author="author" />
|
<covers-author-image :author="authorCopy" />
|
||||||
<div v-if="userCanDelete && !processing && author.imagePath" class="absolute top-0 left-0 w-full h-full opacity-0 hover:opacity-100">
|
<div v-if="userCanDelete && !processing && author.imagePath" class="absolute top-0 left-0 w-full h-full opacity-0 hover:opacity-100">
|
||||||
<span class="absolute top-2 right-2 material-icons text-error transform hover:scale-125 transition-transform cursor-pointer text-lg" @click="removeCover">delete</span>
|
<span class="absolute top-2 right-2 material-icons text-error transform hover:scale-125 transition-transform cursor-pointer text-lg" @click="removeCover">delete</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -30,9 +30,6 @@
|
||||||
<ui-text-input-with-label v-model="authorCopy.asin" :disabled="processing" label="ASIN" />
|
<ui-text-input-with-label v-model="authorCopy.asin" :disabled="processing" label="ASIN" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="p-2">
|
|
||||||
<ui-text-input-with-label v-model="authorCopy.imagePath" :disabled="processing" :label="$strings.LabelPhotoPathURL" />
|
|
||||||
</div> -->
|
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<ui-textarea-with-label v-model="authorCopy.description" :disabled="processing" :label="$strings.LabelDescription" :rows="8" />
|
<ui-textarea-with-label v-model="authorCopy.description" :disabled="processing" :label="$strings.LabelDescription" :rows="8" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -106,9 +103,9 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
init() {
|
init() {
|
||||||
this.imageUrl = ''
|
this.imageUrl = ''
|
||||||
this.authorCopy.name = this.author.name
|
this.authorCopy = {
|
||||||
this.authorCopy.asin = this.author.asin
|
...this.author
|
||||||
this.authorCopy.description = this.author.description
|
}
|
||||||
},
|
},
|
||||||
removeClick() {
|
removeClick() {
|
||||||
const payload = {
|
const payload = {
|
||||||
|
|
@ -171,7 +168,9 @@ export default {
|
||||||
.$delete(`/api/authors/${this.authorId}/image`)
|
.$delete(`/api/authors/${this.authorId}/image`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.$toast.success(this.$strings.ToastAuthorImageRemoveSuccess)
|
this.$toast.success(this.$strings.ToastAuthorImageRemoveSuccess)
|
||||||
this.$store.commit('globals/showEditAuthorModal', data.author)
|
|
||||||
|
this.authorCopy.updatedAt = data.author.updatedAt
|
||||||
|
this.authorCopy.imagePath = data.author.imagePath
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
|
|
@ -196,7 +195,9 @@ export default {
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
this.imageUrl = ''
|
this.imageUrl = ''
|
||||||
this.$toast.success('Author image updated')
|
this.$toast.success('Author image updated')
|
||||||
this.$store.commit('globals/showEditAuthorModal', data.author)
|
|
||||||
|
this.authorCopy.updatedAt = data.author.updatedAt
|
||||||
|
this.authorCopy.imagePath = data.author.imagePath
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
|
|
@ -231,8 +232,11 @@ export default {
|
||||||
} else if (response.updated) {
|
} else if (response.updated) {
|
||||||
if (response.author.imagePath) {
|
if (response.author.imagePath) {
|
||||||
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
||||||
this.$store.commit('globals/showEditAuthorModal', response.author)
|
|
||||||
} else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
|
} else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
|
||||||
|
|
||||||
|
this.authorCopy = {
|
||||||
|
...response.author
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.$toast.info('No updates were made for Author')
|
this.$toast.info('No updates were made for Author')
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,12 @@ export default {
|
||||||
ereaderDevice: {
|
ereaderDevice: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => null
|
default: () => null
|
||||||
}
|
},
|
||||||
|
users: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
loadUsers: Function
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
@ -56,8 +61,7 @@ export default {
|
||||||
email: '',
|
email: '',
|
||||||
availabilityOption: 'adminAndUp',
|
availabilityOption: 'adminAndUp',
|
||||||
users: []
|
users: []
|
||||||
},
|
}
|
||||||
users: []
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
@ -108,25 +112,13 @@ export default {
|
||||||
methods: {
|
methods: {
|
||||||
availabilityOptionChanged(option) {
|
availabilityOptionChanged(option) {
|
||||||
if (option === 'specificUsers' && !this.users.length) {
|
if (option === 'specificUsers' && !this.users.length) {
|
||||||
this.loadUsers()
|
this.callLoadUsers()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async loadUsers() {
|
async callLoadUsers() {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.users = await this.$axios
|
await this.loadUsers()
|
||||||
.$get('/api/users')
|
|
||||||
.then((res) => {
|
|
||||||
return res.users.sort((a, b) => {
|
|
||||||
return a.createdAt - b.createdAt
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('Failed', error)
|
|
||||||
return []
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
this.processing = false
|
this.processing = false
|
||||||
})
|
|
||||||
},
|
},
|
||||||
submitForm() {
|
submitForm() {
|
||||||
this.$refs.ereaderNameInput.blur()
|
this.$refs.ereaderNameInput.blur()
|
||||||
|
|
@ -226,10 +218,6 @@ export default {
|
||||||
this.newDevice.email = this.ereaderDevice.email
|
this.newDevice.email = this.ereaderDevice.email
|
||||||
this.newDevice.availabilityOption = this.ereaderDevice.availabilityOption || 'adminOrUp'
|
this.newDevice.availabilityOption = this.ereaderDevice.availabilityOption || 'adminOrUp'
|
||||||
this.newDevice.users = this.ereaderDevice.users || []
|
this.newDevice.users = this.ereaderDevice.users || []
|
||||||
|
|
||||||
if (this.newDevice.availabilityOption === 'specificUsers' && !this.users.length) {
|
|
||||||
this.loadUsers()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.newDevice.name = ''
|
this.newDevice.name = ''
|
||||||
this.newDevice.email = ''
|
this.newDevice.email = ''
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@
|
||||||
<div v-if="selectedMatchOrig.narrator" class="flex items-center py-2">
|
<div v-if="selectedMatchOrig.narrator" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.narrator" checkbox-bg="bg" @input="checkboxToggled" />
|
<ui-checkbox v-model="selectedMatchUsage.narrator" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.narrator" :disabled="!selectedMatchUsage.narrator" :label="$strings.LabelNarrators" />
|
<ui-multi-select v-model="selectedMatch.narrator" :items="narrators" :disabled="!selectedMatchUsage.narrator" :label="$strings.LabelNarrators" />
|
||||||
<p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.narratorName || '' }}</p>
|
<p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.narratorName || '' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -122,7 +122,7 @@
|
||||||
<div v-if="selectedMatchOrig.tags" class="flex items-center py-2">
|
<div v-if="selectedMatchOrig.tags" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.tags" checkbox-bg="bg" @input="checkboxToggled" />
|
<ui-checkbox v-model="selectedMatchUsage.tags" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-text-input-with-label v-model="selectedMatch.tags" :disabled="!selectedMatchUsage.tags" :label="$strings.LabelTags" />
|
<ui-multi-select v-model="selectedMatch.tags" :items="tags" :disabled="!selectedMatchUsage.tags" :label="$strings.LabelTags" />
|
||||||
<p v-if="media.tags" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ media.tags.join(', ') }}</p>
|
<p v-if="media.tags" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ media.tags.join(', ') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -280,6 +280,9 @@ export default {
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
|
filterData() {
|
||||||
|
return this.$store.state.libraries.filterData
|
||||||
|
},
|
||||||
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.providers
|
||||||
|
|
@ -305,11 +308,16 @@ export default {
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.mediaType == 'podcast'
|
return this.mediaType == 'podcast'
|
||||||
},
|
},
|
||||||
|
narrators() {
|
||||||
|
return this.filterData.narrators || []
|
||||||
|
},
|
||||||
genres() {
|
genres() {
|
||||||
const filterData = this.$store.state.libraries.filterData || {}
|
const currentGenres = this.filterData.genres || []
|
||||||
const currentGenres = filterData.genres || []
|
|
||||||
const selectedMatchGenres = this.selectedMatch.genres || []
|
const selectedMatchGenres = this.selectedMatch.genres || []
|
||||||
return [...new Set([...currentGenres, ...selectedMatchGenres])]
|
return [...new Set([...currentGenres, ...selectedMatchGenres])]
|
||||||
|
},
|
||||||
|
tags() {
|
||||||
|
return this.filterData.tags || []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
@ -479,6 +487,12 @@ export default {
|
||||||
// match.genres = match.genres.join(',')
|
// match.genres = match.genres.join(',')
|
||||||
match.genres = match.genres.split(',').map((g) => g.trim())
|
match.genres = match.genres.split(',').map((g) => g.trim())
|
||||||
}
|
}
|
||||||
|
if (match.tags && !Array.isArray(match.tags)) {
|
||||||
|
match.tags = match.tags.split(',').map((g) => g.trim())
|
||||||
|
}
|
||||||
|
if (match.narrator && !Array.isArray(match.narrator)) {
|
||||||
|
match.narrator = match.narrator.split(',').map((g) => g.trim())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Select Match', match)
|
console.log('Select Match', match)
|
||||||
|
|
@ -522,11 +536,11 @@ export default {
|
||||||
)
|
)
|
||||||
updatePayload.metadata.authors = authorPayload
|
updatePayload.metadata.authors = authorPayload
|
||||||
} else if (key === 'narrator') {
|
} else if (key === 'narrator') {
|
||||||
updatePayload.metadata.narrators = this.selectedMatch[key].split(',').map((v) => v.trim())
|
updatePayload.metadata.narrators = this.selectedMatch[key]
|
||||||
} else if (key === 'genres') {
|
} else if (key === 'genres') {
|
||||||
updatePayload.metadata.genres = [...this.selectedMatch[key]]
|
updatePayload.metadata.genres = [...this.selectedMatch[key]]
|
||||||
} else if (key === 'tags') {
|
} else if (key === 'tags') {
|
||||||
updatePayload.tags = this.selectedMatch[key].split(',').map((v) => v.trim())
|
updatePayload.tags = this.selectedMatch[key]
|
||||||
} else if (key === 'itunesId') {
|
} else if (key === 'itunesId') {
|
||||||
updatePayload.metadata.itunesId = Number(this.selectedMatch[key])
|
updatePayload.metadata.itunesId = Number(this.selectedMatch[key])
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,17 @@
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="isBookLibrary" class="py-3">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<ui-toggle-switch v-model="epubsAllowScriptedContent" @input="formUpdated" />
|
||||||
|
<ui-tooltip :text="$strings.LabelSettingsEpubsAllowScriptedContentHelp">
|
||||||
|
<p class="pl-4 text-base">
|
||||||
|
{{ $strings.LabelSettingsEpubsAllowScriptedContent }}
|
||||||
|
<span class="material-icons icon-text text-sm">info_outlined</span>
|
||||||
|
</p>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-if="isPodcastLibrary" class="py-3">
|
<div v-if="isPodcastLibrary" class="py-3">
|
||||||
<ui-dropdown :label="$strings.LabelPodcastSearchRegion" v-model="podcastSearchRegion" :items="$podcastSearchRegionOptions" small class="max-w-72" menu-max-height="200px" @input="formUpdated" />
|
<ui-dropdown :label="$strings.LabelPodcastSearchRegion" v-model="podcastSearchRegion" :items="$podcastSearchRegionOptions" small class="max-w-72" menu-max-height="200px" @input="formUpdated" />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -83,6 +94,7 @@ export default {
|
||||||
skipMatchingMediaWithAsin: false,
|
skipMatchingMediaWithAsin: false,
|
||||||
skipMatchingMediaWithIsbn: false,
|
skipMatchingMediaWithIsbn: false,
|
||||||
audiobooksOnly: false,
|
audiobooksOnly: false,
|
||||||
|
epubsAllowScriptedContent: false,
|
||||||
hideSingleBookSeries: false,
|
hideSingleBookSeries: false,
|
||||||
onlyShowLaterBooksInContinueSeries: false,
|
onlyShowLaterBooksInContinueSeries: false,
|
||||||
podcastSearchRegion: 'us'
|
podcastSearchRegion: 'us'
|
||||||
|
|
@ -118,6 +130,7 @@ export default {
|
||||||
skipMatchingMediaWithAsin: !!this.skipMatchingMediaWithAsin,
|
skipMatchingMediaWithAsin: !!this.skipMatchingMediaWithAsin,
|
||||||
skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn,
|
skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn,
|
||||||
audiobooksOnly: !!this.audiobooksOnly,
|
audiobooksOnly: !!this.audiobooksOnly,
|
||||||
|
epubsAllowScriptedContent: !!this.epubsAllowScriptedContent,
|
||||||
hideSingleBookSeries: !!this.hideSingleBookSeries,
|
hideSingleBookSeries: !!this.hideSingleBookSeries,
|
||||||
onlyShowLaterBooksInContinueSeries: !!this.onlyShowLaterBooksInContinueSeries,
|
onlyShowLaterBooksInContinueSeries: !!this.onlyShowLaterBooksInContinueSeries,
|
||||||
podcastSearchRegion: this.podcastSearchRegion
|
podcastSearchRegion: this.podcastSearchRegion
|
||||||
|
|
@ -133,6 +146,7 @@ export default {
|
||||||
this.skipMatchingMediaWithAsin = !!this.librarySettings.skipMatchingMediaWithAsin
|
this.skipMatchingMediaWithAsin = !!this.librarySettings.skipMatchingMediaWithAsin
|
||||||
this.skipMatchingMediaWithIsbn = !!this.librarySettings.skipMatchingMediaWithIsbn
|
this.skipMatchingMediaWithIsbn = !!this.librarySettings.skipMatchingMediaWithIsbn
|
||||||
this.audiobooksOnly = !!this.librarySettings.audiobooksOnly
|
this.audiobooksOnly = !!this.librarySettings.audiobooksOnly
|
||||||
|
this.epubsAllowScriptedContent = !!this.librarySettings.epubsAllowScriptedContent
|
||||||
this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries
|
this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries
|
||||||
this.onlyShowLaterBooksInContinueSeries = !!this.librarySettings.onlyShowLaterBooksInContinueSeries
|
this.onlyShowLaterBooksInContinueSeries = !!this.librarySettings.onlyShowLaterBooksInContinueSeries
|
||||||
this.podcastSearchRegion = this.librarySettings.podcastSearchRegion || 'us'
|
this.podcastSearchRegion = this.librarySettings.podcastSearchRegion || 'us'
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,9 @@ export default {
|
||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
return this.libraryItem?.id
|
return this.libraryItem?.id
|
||||||
},
|
},
|
||||||
|
allowScriptedContent() {
|
||||||
|
return this.$store.getters['libraries/getLibraryEpubsAllowScriptedContent']
|
||||||
|
},
|
||||||
hasPrev() {
|
hasPrev() {
|
||||||
return !this.rendition?.location?.atStart
|
return !this.rendition?.location?.atStart
|
||||||
},
|
},
|
||||||
|
|
@ -316,7 +319,7 @@ export default {
|
||||||
reader.rendition = reader.book.renderTo('viewer', {
|
reader.rendition = reader.book.renderTo('viewer', {
|
||||||
width: this.readerWidth,
|
width: this.readerWidth,
|
||||||
height: this.readerHeight * 0.8,
|
height: this.readerHeight * 0.8,
|
||||||
allowScriptedContent: true,
|
allowScriptedContent: this.allowScriptedContent,
|
||||||
spread: 'auto',
|
spread: 'auto',
|
||||||
snap: true,
|
snap: true,
|
||||||
manager: 'continuous',
|
manager: 'continuous',
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
<div v-if="userCanUpdate" class="mx-1" :class="isHovering ? '' : 'ml-6'">
|
<div v-if="userCanUpdate" class="mx-1" :class="isHovering ? '' : 'ml-6'">
|
||||||
<ui-icon-btn icon="edit" borderless @click="clickEdit" />
|
<ui-icon-btn icon="edit" borderless @click="clickEdit" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="userCanDelete" class="mx-1">
|
<div class="mx-1" :class="isHovering ? '' : 'ml-6'">
|
||||||
<ui-icon-btn icon="close" borderless @click="removeClick" />
|
<ui-icon-btn icon="close" borderless @click="removeClick" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -75,8 +75,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
translateDistance() {
|
translateDistance() {
|
||||||
if (!this.userCanUpdate && !this.userCanDelete) return 'translate-x-0'
|
if (!this.userCanUpdate) return '-translate-x-12'
|
||||||
else if (!this.userCanUpdate || !this.userCanDelete) return '-translate-x-12'
|
|
||||||
return '-translate-x-24'
|
return '-translate-x-24'
|
||||||
},
|
},
|
||||||
libraryItem() {
|
libraryItem() {
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,14 @@ export default {
|
||||||
this.recalcMenuPos()
|
this.recalcMenuPos()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
resetInput() {
|
||||||
|
this.textInput = null
|
||||||
|
this.currentSearch = null
|
||||||
|
this.selectedMenuItemIndex = null
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.blur()
|
||||||
|
})
|
||||||
|
},
|
||||||
insertNewItem(item) {
|
insertNewItem(item) {
|
||||||
this.selected.push(item)
|
this.selected.push(item)
|
||||||
this.$emit('input', this.selected)
|
this.$emit('input', this.selected)
|
||||||
|
|
@ -316,15 +324,18 @@ export default {
|
||||||
submitForm() {
|
submitForm() {
|
||||||
if (!this.textInput) return
|
if (!this.textInput) return
|
||||||
|
|
||||||
var cleaned = this.textInput.trim()
|
const cleaned = this.textInput.trim()
|
||||||
var matchesItem = this.items.find((i) => {
|
if (!cleaned) {
|
||||||
return i === cleaned
|
this.resetInput()
|
||||||
})
|
} else {
|
||||||
|
const matchesItem = this.items.find((i) => i === cleaned)
|
||||||
if (matchesItem) {
|
if (matchesItem) {
|
||||||
this.clickedOption(null, matchesItem)
|
this.clickedOption(null, matchesItem)
|
||||||
} else {
|
} else {
|
||||||
this.insertNewItem(this.textInput)
|
this.insertNewItem(cleaned)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.$refs.input) this.$refs.input.style.width = '24px'
|
if (this.$refs.input) this.$refs.input.style.width = '24px'
|
||||||
},
|
},
|
||||||
scroll() {
|
scroll() {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div tabindex="0" @focus="focusDigit('second0')" class="relative">
|
||||||
<div class="rounded text-gray-200 border w-full px-3 py-2" :class="focusedDigit ? 'bg-primary bg-opacity-50 border-gray-300' : 'bg-primary border-gray-600'" @click="clickInput" v-click-outside="clickOutsideObj">
|
<div class="rounded text-gray-200 border w-full px-3 py-2" :class="focusedDigit ? 'bg-primary bg-opacity-50 border-gray-300' : 'bg-primary border-gray-600'" @click="clickInput" v-click-outside="clickOutsideObj">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<template v-for="(digit, index) in digitDisplay">
|
<template v-for="(digit, index) in digitDisplay">
|
||||||
|
|
@ -174,7 +174,7 @@ export default {
|
||||||
return this.increaseFocused()
|
return this.increaseFocused()
|
||||||
} else if (evt.key === 'ArrowDown') {
|
} else if (evt.key === 'ArrowDown') {
|
||||||
return this.decreaseFocused()
|
return this.decreaseFocused()
|
||||||
} else if (evt.key === 'Enter' || evt.key === 'Escape') {
|
} else if (evt.key === 'Enter' || evt.key === 'Escape' || evt.key === 'Tab') {
|
||||||
return this.removeFocus()
|
return this.removeFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="absolute bg-bg rounded-md py-1 border border-black-200 shadow-lg z-50" v-click-outside="clickOutsideObj" :style="{ width: menuWidth + 'px' }" style="top: 0; left: 0">
|
<div ref="wrapper" class="absolute bg-bg rounded-md py-1 border border-black-200 shadow-lg z-50" v-click-outside="clickOutsideObj" :style="{ width: menuWidth + 'px' }">
|
||||||
<template v-for="(item, index) in items">
|
<template v-for="(item, index) in items">
|
||||||
<template v-if="item.subitems">
|
<template v-if="item.subitems">
|
||||||
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-default" :class="{ 'bg-white/5': mouseoverItemIndex == index }" @mouseover="mouseoverItem(index)" @mouseleave="mouseleaveItem(index)" @click.stop>
|
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white/5 text-white text-xs cursor-default" :class="{ 'bg-white/5': mouseoverItemIndex == index }" @mouseover="mouseoverItem(index)" @mouseleave="mouseleaveItem(index)" @click.stop>
|
||||||
|
|
|
||||||
4
client/package-lock.json
generated
4
client/package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.9.0",
|
"version": "2.10.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.9.0",
|
"version": "2.10.1",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.9.0",
|
"version": "2.10.1",
|
||||||
"buildNumber": 1,
|
"buildNumber": 1,
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@
|
||||||
</div>
|
</div>
|
||||||
</app-settings-content>
|
</app-settings-content>
|
||||||
|
|
||||||
<app-settings-content :header-text="$strings.HeaderEreaderDevices" :description="''">
|
<app-settings-content :header-text="$strings.HeaderEreaderDevices" :description="$strings.MessageEreaderDevices">
|
||||||
<template #header-items>
|
<template #header-items>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
|
|
@ -70,6 +70,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-left">{{ $strings.LabelName }}</th>
|
<th class="text-left">{{ $strings.LabelName }}</th>
|
||||||
<th class="text-left">{{ $strings.LabelEmail }}</th>
|
<th class="text-left">{{ $strings.LabelEmail }}</th>
|
||||||
|
<th class="text-left">{{ $strings.LabelAccessibleBy }}</th>
|
||||||
<th class="w-40"></th>
|
<th class="w-40"></th>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-for="device in existingEReaderDevices" :key="device.name">
|
<tr v-for="device in existingEReaderDevices" :key="device.name">
|
||||||
|
|
@ -79,6 +80,9 @@
|
||||||
<td class="text-left">
|
<td class="text-left">
|
||||||
<p class="text-sm md:text-base text-gray-100">{{ device.email }}</p>
|
<p class="text-sm md:text-base text-gray-100">{{ device.email }}</p>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="text-left">
|
||||||
|
<p class="text-sm md:text-base text-gray-100">{{ getAccessibleBy(device) }}</p>
|
||||||
|
</td>
|
||||||
<td class="w-40">
|
<td class="w-40">
|
||||||
<div class="flex justify-end items-center h-10">
|
<div class="flex justify-end items-center h-10">
|
||||||
<ui-icon-btn icon="edit" borderless :size="8" icon-font-size="1.1rem" :disabled="deletingDeviceName === device.name" class="mx-1" @click="editDeviceClick(device)" />
|
<ui-icon-btn icon="edit" borderless :size="8" icon-font-size="1.1rem" :disabled="deletingDeviceName === device.name" class="mx-1" @click="editDeviceClick(device)" />
|
||||||
|
|
@ -87,12 +91,12 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<div v-else class="text-center py-4">
|
<div v-else-if="!loading" class="text-center py-4">
|
||||||
<p class="text-lg text-gray-100">No Devices</p>
|
<p class="text-lg text-gray-100">No Devices</p>
|
||||||
</div>
|
</div>
|
||||||
</app-settings-content>
|
</app-settings-content>
|
||||||
|
|
||||||
<modals-emails-e-reader-device-modal v-model="showEReaderDeviceModal" :existing-devices="existingEReaderDevices" :ereader-device="selectedEReaderDevice" @update="ereaderDevicesUpdated" />
|
<modals-emails-e-reader-device-modal v-model="showEReaderDeviceModal" :users="users" :existing-devices="existingEReaderDevices" :ereader-device="selectedEReaderDevice" @update="ereaderDevicesUpdated" :loadUsers="loadUsers" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
@ -105,6 +109,7 @@ export default {
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
users: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
savingSettings: false,
|
savingSettings: false,
|
||||||
sendingTest: false,
|
sendingTest: false,
|
||||||
|
|
@ -146,6 +151,30 @@ export default {
|
||||||
...this.settings
|
...this.settings
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async loadUsers() {
|
||||||
|
if (this.users.length) return
|
||||||
|
this.users = await this.$axios
|
||||||
|
.$get('/api/users')
|
||||||
|
.then((res) => {
|
||||||
|
return res.users.sort((a, b) => {
|
||||||
|
return a.createdAt - b.createdAt
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
this.$toast.error(this.$strings.ToastFailedToLoadData)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getAccessibleBy(device) {
|
||||||
|
const user = device.availabilityOption
|
||||||
|
if (user === 'userOrUp') return 'Users (excluding Guests)'
|
||||||
|
if (user === 'guestOrUp') return 'Users (including Guests)'
|
||||||
|
if (user === 'specificUsers') {
|
||||||
|
return device.users.map((id) => this.users.find((u) => u.id === id)?.username).join(', ')
|
||||||
|
}
|
||||||
|
return 'Admins Only'
|
||||||
|
},
|
||||||
editDeviceClick(device) {
|
editDeviceClick(device) {
|
||||||
this.selectedEReaderDevice = device
|
this.selectedEReaderDevice = device
|
||||||
this.showEReaderDeviceModal = true
|
this.showEReaderDeviceModal = true
|
||||||
|
|
@ -184,6 +213,11 @@ export default {
|
||||||
ereaderDevicesUpdated(ereaderDevices) {
|
ereaderDevicesUpdated(ereaderDevices) {
|
||||||
this.settings.ereaderDevices = ereaderDevices
|
this.settings.ereaderDevices = ereaderDevices
|
||||||
this.newSettings.ereaderDevices = ereaderDevices.map((d) => ({ ...d }))
|
this.newSettings.ereaderDevices = ereaderDevices.map((d) => ({ ...d }))
|
||||||
|
|
||||||
|
// Load users if a device has availability set to specific users
|
||||||
|
if (ereaderDevices.some((device) => device.availabilityOption === 'specificUsers')) {
|
||||||
|
this.loadUsers()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
addNewDeviceClick() {
|
addNewDeviceClick() {
|
||||||
this.selectedEReaderDevice = null
|
this.selectedEReaderDevice = null
|
||||||
|
|
@ -251,7 +285,12 @@ export default {
|
||||||
|
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/emails/settings`)
|
.$get(`/api/emails/settings`)
|
||||||
.then((data) => {
|
.then(async (data) => {
|
||||||
|
// Load users if a device has availability set to specific users
|
||||||
|
if (data.settings.ereaderDevices.some((device) => device.availabilityOption === 'specificUsers')) {
|
||||||
|
await this.loadUsers()
|
||||||
|
}
|
||||||
|
|
||||||
this.settings = data.settings
|
this.settings = data.settings
|
||||||
this.newSettings = {
|
this.newSettings = {
|
||||||
...this.settings
|
...this.settings
|
||||||
|
|
|
||||||
|
|
@ -79,9 +79,6 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
authorUpdated(author) {
|
authorUpdated(author) {
|
||||||
if (this.selectedAuthor && this.selectedAuthor.id === author.id) {
|
|
||||||
this.$store.commit('globals/setSelectedAuthor', author)
|
|
||||||
}
|
|
||||||
this.authors = this.authors.map((au) => {
|
this.authors = this.authors.map((au) => {
|
||||||
if (au.id === author.id) {
|
if (au.id === author.id) {
|
||||||
return author
|
return author
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,9 @@
|
||||||
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
||||||
<ui-icon-btn v-if="userCanUpdate" icon="edit" class="mx-0.5" @click="editClick" />
|
<ui-icon-btn icon="edit" class="mx-0.5" @click="editClick" />
|
||||||
|
|
||||||
<ui-icon-btn v-if="userCanDelete" icon="delete" class="mx-0.5" @click="removeClick" />
|
<ui-icon-btn icon="delete" class="mx-0.5" @click="removeClick" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-8 max-w-2xl">
|
<div class="my-8 max-w-2xl">
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ export const state = () => ({
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
getCurrentLibrary: state => {
|
getCurrentLibrary: (state) => {
|
||||||
return state.libraries.find(lib => lib.id === state.currentLibraryId)
|
return state.libraries.find((lib) => lib.id === state.currentLibraryId)
|
||||||
},
|
},
|
||||||
getCurrentLibraryName: (state, getters) => {
|
getCurrentLibraryName: (state, getters) => {
|
||||||
var currentLibrary = getters.getCurrentLibrary
|
var currentLibrary = getters.getCurrentLibrary
|
||||||
|
|
@ -28,11 +28,11 @@ export const getters = {
|
||||||
if (!getters.getCurrentLibrary) return null
|
if (!getters.getCurrentLibrary) return null
|
||||||
return getters.getCurrentLibrary.mediaType
|
return getters.getCurrentLibrary.mediaType
|
||||||
},
|
},
|
||||||
getSortedLibraries: state => () => {
|
getSortedLibraries: (state) => () => {
|
||||||
return state.libraries.map(lib => ({ ...lib })).sort((a, b) => a.displayOrder - b.displayOrder)
|
return state.libraries.map((lib) => ({ ...lib })).sort((a, b) => a.displayOrder - b.displayOrder)
|
||||||
},
|
},
|
||||||
getLibraryProvider: state => libraryId => {
|
getLibraryProvider: (state) => (libraryId) => {
|
||||||
var library = state.libraries.find(l => l.id === libraryId)
|
var library = state.libraries.find((l) => l.id === libraryId)
|
||||||
if (!library) return null
|
if (!library) return null
|
||||||
return library.provider
|
return library.provider
|
||||||
},
|
},
|
||||||
|
|
@ -60,11 +60,14 @@ export const getters = {
|
||||||
getLibraryIsAudiobooksOnly: (state, getters) => {
|
getLibraryIsAudiobooksOnly: (state, getters) => {
|
||||||
return !!getters.getCurrentLibrarySettings?.audiobooksOnly
|
return !!getters.getCurrentLibrarySettings?.audiobooksOnly
|
||||||
},
|
},
|
||||||
getCollection: state => id => {
|
getLibraryEpubsAllowScriptedContent: (state, getters) => {
|
||||||
return state.collections.find(c => c.id === id)
|
return !!getters.getCurrentLibrarySettings?.epubsAllowScriptedContent
|
||||||
},
|
},
|
||||||
getPlaylist: state => id => {
|
getCollection: (state) => (id) => {
|
||||||
return state.userPlaylists.find(p => p.id === id)
|
return state.collections.find((c) => c.id === id)
|
||||||
|
},
|
||||||
|
getPlaylist: (state) => (id) => {
|
||||||
|
return state.userPlaylists.find((p) => p.id === id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,7 +78,8 @@ export const actions = {
|
||||||
loadFolders({ state, commit }) {
|
loadFolders({ state, commit }) {
|
||||||
if (state.folders.length) {
|
if (state.folders.length) {
|
||||||
const lastCheck = Date.now() - state.folderLastUpdate
|
const lastCheck = Date.now() - state.folderLastUpdate
|
||||||
if (lastCheck < 1000 * 5) { // 5 seconds
|
if (lastCheck < 1000 * 5) {
|
||||||
|
// 5 seconds
|
||||||
// Folders up to date
|
// Folders up to date
|
||||||
return state.folders
|
return state.folders
|
||||||
}
|
}
|
||||||
|
|
@ -204,7 +208,7 @@ export const mutations = {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
addUpdate(state, library) {
|
addUpdate(state, library) {
|
||||||
var index = state.libraries.findIndex(a => a.id === library.id)
|
var index = state.libraries.findIndex((a) => a.id === library.id)
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
state.libraries.splice(index, 1, library)
|
state.libraries.splice(index, 1, library)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -216,19 +220,19 @@ export const mutations = {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
remove(state, library) {
|
remove(state, library) {
|
||||||
state.libraries = state.libraries.filter(a => a.id !== library.id)
|
state.libraries = state.libraries.filter((a) => a.id !== library.id)
|
||||||
|
|
||||||
state.listeners.forEach((listener) => {
|
state.listeners.forEach((listener) => {
|
||||||
listener.meth()
|
listener.meth()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
addListener(state, listener) {
|
addListener(state, listener) {
|
||||||
var index = state.listeners.findIndex(l => l.id === listener.id)
|
var index = state.listeners.findIndex((l) => l.id === listener.id)
|
||||||
if (index >= 0) state.listeners.splice(index, 1, listener)
|
if (index >= 0) state.listeners.splice(index, 1, listener)
|
||||||
else state.listeners.push(listener)
|
else state.listeners.push(listener)
|
||||||
},
|
},
|
||||||
removeListener(state, listenerId) {
|
removeListener(state, listenerId) {
|
||||||
state.listeners = state.listeners.filter(l => l.id !== listenerId)
|
state.listeners = state.listeners.filter((l) => l.id !== listenerId)
|
||||||
},
|
},
|
||||||
setLibraryFilterData(state, filterData) {
|
setLibraryFilterData(state, filterData) {
|
||||||
state.filterData = filterData
|
state.filterData = filterData
|
||||||
|
|
@ -238,7 +242,7 @@ export const mutations = {
|
||||||
},
|
},
|
||||||
removeSeriesFromFilterData(state, seriesId) {
|
removeSeriesFromFilterData(state, seriesId) {
|
||||||
if (!seriesId || !state.filterData) return
|
if (!seriesId || !state.filterData) return
|
||||||
state.filterData.series = state.filterData.series.filter(se => se.id !== seriesId)
|
state.filterData.series = state.filterData.series.filter((se) => se.id !== seriesId)
|
||||||
},
|
},
|
||||||
updateFilterDataWithItem(state, libraryItem) {
|
updateFilterDataWithItem(state, libraryItem) {
|
||||||
if (!libraryItem || !state.filterData) return
|
if (!libraryItem || !state.filterData) return
|
||||||
|
|
@ -260,12 +264,12 @@ export const mutations = {
|
||||||
// Add/update book authors
|
// Add/update book authors
|
||||||
if (mediaMetadata.authors?.length) {
|
if (mediaMetadata.authors?.length) {
|
||||||
mediaMetadata.authors.forEach((author) => {
|
mediaMetadata.authors.forEach((author) => {
|
||||||
const indexOf = state.filterData.authors.findIndex(au => au.id === author.id)
|
const indexOf = state.filterData.authors.findIndex((au) => au.id === author.id)
|
||||||
if (indexOf >= 0) {
|
if (indexOf >= 0) {
|
||||||
state.filterData.authors.splice(indexOf, 1, author)
|
state.filterData.authors.splice(indexOf, 1, author)
|
||||||
} else {
|
} else {
|
||||||
state.filterData.authors.push(author)
|
state.filterData.authors.push(author)
|
||||||
state.filterData.authors.sort((a, b) => (a.name || '').localeCompare((b.name || '')))
|
state.filterData.authors.sort((a, b) => (a.name || '').localeCompare(b.name || ''))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -273,12 +277,12 @@ export const mutations = {
|
||||||
// Add/update series
|
// Add/update series
|
||||||
if (mediaMetadata.series?.length) {
|
if (mediaMetadata.series?.length) {
|
||||||
mediaMetadata.series.forEach((series) => {
|
mediaMetadata.series.forEach((series) => {
|
||||||
const indexOf = state.filterData.series.findIndex(se => se.id === series.id)
|
const indexOf = state.filterData.series.findIndex((se) => se.id === series.id)
|
||||||
if (indexOf >= 0) {
|
if (indexOf >= 0) {
|
||||||
state.filterData.series.splice(indexOf, 1, { id: series.id, name: series.name })
|
state.filterData.series.splice(indexOf, 1, { id: series.id, name: series.name })
|
||||||
} else {
|
} else {
|
||||||
state.filterData.series.push({ id: series.id, name: series.name })
|
state.filterData.series.push({ id: series.id, name: series.name })
|
||||||
state.filterData.series.sort((a, b) => (a.name || '').localeCompare((b.name || '')))
|
state.filterData.series.sort((a, b) => (a.name || '').localeCompare(b.name || ''))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -329,7 +333,7 @@ export const mutations = {
|
||||||
state.collections = collections
|
state.collections = collections
|
||||||
},
|
},
|
||||||
addUpdateCollection(state, collection) {
|
addUpdateCollection(state, collection) {
|
||||||
var index = state.collections.findIndex(c => c.id === collection.id)
|
var index = state.collections.findIndex((c) => c.id === collection.id)
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
state.collections.splice(index, 1, collection)
|
state.collections.splice(index, 1, collection)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -337,14 +341,14 @@ export const mutations = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeCollection(state, collection) {
|
removeCollection(state, collection) {
|
||||||
state.collections = state.collections.filter(c => c.id !== collection.id)
|
state.collections = state.collections.filter((c) => c.id !== collection.id)
|
||||||
},
|
},
|
||||||
setUserPlaylists(state, playlists) {
|
setUserPlaylists(state, playlists) {
|
||||||
state.userPlaylists = playlists
|
state.userPlaylists = playlists
|
||||||
state.numUserPlaylists = playlists.length
|
state.numUserPlaylists = playlists.length
|
||||||
},
|
},
|
||||||
addUpdateUserPlaylist(state, playlist) {
|
addUpdateUserPlaylist(state, playlist) {
|
||||||
const index = state.userPlaylists.findIndex(p => p.id === playlist.id)
|
const index = state.userPlaylists.findIndex((p) => p.id === playlist.id)
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
state.userPlaylists.splice(index, 1, playlist)
|
state.userPlaylists.splice(index, 1, playlist)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -353,7 +357,7 @@ export const mutations = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
removeUserPlaylist(state, playlist) {
|
removeUserPlaylist(state, playlist) {
|
||||||
state.userPlaylists = state.userPlaylists.filter(p => p.id !== playlist.id)
|
state.userPlaylists = state.userPlaylists.filter((p) => p.id !== playlist.id)
|
||||||
state.numUserPlaylists = state.userPlaylists.length
|
state.numUserPlaylists = state.userPlaylists.length
|
||||||
},
|
},
|
||||||
setEReaderDevices(state, ereaderDevices) {
|
setEReaderDevices(state, ereaderDevices) {
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "Съкратен",
|
"LabelAbridged": "Съкратен",
|
||||||
"LabelAbridgedChecked": "Abridged (checked)",
|
"LabelAbridgedChecked": "Abridged (checked)",
|
||||||
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "Тип на Профила",
|
"LabelAccountType": "Тип на Профила",
|
||||||
"LabelAccountTypeAdmin": "Администратор",
|
"LabelAccountTypeAdmin": "Администратор",
|
||||||
"LabelAccountTypeGuest": "Гост",
|
"LabelAccountTypeGuest": "Гост",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "Включи наблюдателя",
|
"LabelSettingsEnableWatcher": "Включи наблюдателя",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Включи наблюдателя за библиотека",
|
"LabelSettingsEnableWatcherForLibrary": "Включи наблюдателя за библиотека",
|
||||||
"LabelSettingsEnableWatcherHelp": "Включва автоматичното добавяне/обновяване на елементи, когато се открият промени във файловете. *Изисква рестарт на сървъра",
|
"LabelSettingsEnableWatcherHelp": "Включва автоматичното добавяне/обновяване на елементи, когато се открият промени във файловете. *Изисква рестарт на сървъра",
|
||||||
|
"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.",
|
||||||
"LabelSettingsExperimentalFeatures": "Експериментални Функции",
|
"LabelSettingsExperimentalFeatures": "Експериментални Функции",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Функции в разработка, които могат да изискват вашето мнение и помощ за тестване. Кликнете за да отворите дискусия в github.",
|
"LabelSettingsExperimentalFeaturesHelp": "Функции в разработка, които могат да изискват вашето мнение и помощ за тестване. Кликнете за да отворите дискусия в github.",
|
||||||
"LabelSettingsFindCovers": "Намери Корици",
|
"LabelSettingsFindCovers": "Намери Корици",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "Плъзнете файлове в правилния ред на каналите",
|
"MessageDragFilesIntoTrackOrder": "Плъзнете файлове в правилния ред на каналите",
|
||||||
"MessageEmbedFinished": "Вграждането завърши!",
|
"MessageEmbedFinished": "Вграждането завърши!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} епизод(и) в опашка за изтегляне",
|
"MessageEpisodesQueuedForDownload": "{0} епизод(и) в опашка за изтегляне",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "Feed URL-a ще бъде {0}",
|
"MessageFeedURLWillBe": "Feed URL-a ще бъде {0}",
|
||||||
"MessageFetching": "Взимане...",
|
"MessageFetching": "Взимане...",
|
||||||
"MessageForceReScanDescription": "ще сканира всички файлове отново като прясно сканиране. Аудио файлове ID3 тагове, OPF файлове и текстови файлове ще бъдат сканирани като нови.",
|
"MessageForceReScanDescription": "ще сканира всички файлове отново като прясно сканиране. Аудио файлове ID3 тагове, OPF файлове и текстови файлове ще бъдат сканирани като нови.",
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "সংক্ষিপ্ত",
|
"LabelAbridged": "সংক্ষিপ্ত",
|
||||||
"LabelAbridgedChecked": "Abridged (checked)",
|
"LabelAbridgedChecked": "Abridged (checked)",
|
||||||
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "অ্যাকাউন্টের প্রকার",
|
"LabelAccountType": "অ্যাকাউন্টের প্রকার",
|
||||||
"LabelAccountTypeAdmin": "প্রশাসন",
|
"LabelAccountTypeAdmin": "প্রশাসন",
|
||||||
"LabelAccountTypeGuest": "অতিথি",
|
"LabelAccountTypeGuest": "অতিথি",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "প্রহরী সক্ষম করুন",
|
"LabelSettingsEnableWatcher": "প্রহরী সক্ষম করুন",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "লাইব্রেরির জন্য ফোল্ডার প্রহরী সক্ষম করুন",
|
"LabelSettingsEnableWatcherForLibrary": "লাইব্রেরির জন্য ফোল্ডার প্রহরী সক্ষম করুন",
|
||||||
"LabelSettingsEnableWatcherHelp": "ফাইলের পরিবর্তন শনাক্ত হলে আইটেমগুলির স্বয়ংক্রিয় যোগ/আপডেট সক্ষম করবে। *সার্ভার পুনরায় চালু করতে হবে",
|
"LabelSettingsEnableWatcherHelp": "ফাইলের পরিবর্তন শনাক্ত হলে আইটেমগুলির স্বয়ংক্রিয় যোগ/আপডেট সক্ষম করবে। *সার্ভার পুনরায় চালু করতে হবে",
|
||||||
|
"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.",
|
||||||
"LabelSettingsExperimentalFeatures": "পরীক্ষামূলক বৈশিষ্ট্য",
|
"LabelSettingsExperimentalFeatures": "পরীক্ষামূলক বৈশিষ্ট্য",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "ফিচারের বৈশিষ্ট্য যা আপনার প্রতিক্রিয়া ব্যবহার করতে পারে এবং পরীক্ষায় সহায়তা করতে পারে। গিটহাব আলোচনা খুলতে ক্লিক করুন।",
|
"LabelSettingsExperimentalFeaturesHelp": "ফিচারের বৈশিষ্ট্য যা আপনার প্রতিক্রিয়া ব্যবহার করতে পারে এবং পরীক্ষায় সহায়তা করতে পারে। গিটহাব আলোচনা খুলতে ক্লিক করুন।",
|
||||||
"LabelSettingsFindCovers": "কভার খুঁজুন",
|
"LabelSettingsFindCovers": "কভার খুঁজুন",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "সঠিক ট্র্যাক অর্ডারে ফাইল টেনে আনুন",
|
"MessageDragFilesIntoTrackOrder": "সঠিক ট্র্যাক অর্ডারে ফাইল টেনে আনুন",
|
||||||
"MessageEmbedFinished": "এম্বেড করা শেষ!",
|
"MessageEmbedFinished": "এম্বেড করা শেষ!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} পর্ব(গুলি) ডাউনলোডের জন্য সারিবদ্ধ",
|
"MessageEpisodesQueuedForDownload": "{0} পর্ব(গুলি) ডাউনলোডের জন্য সারিবদ্ধ",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "ফিড URL হবে {0}",
|
"MessageFeedURLWillBe": "ফিড URL হবে {0}",
|
||||||
"MessageFetching": "আনয় হচ্ছে...",
|
"MessageFetching": "আনয় হচ্ছে...",
|
||||||
"MessageForceReScanDescription": "সকল ফাইল আবার নতুন স্ক্যানের মত স্ক্যান করবে। অডিও ফাইল ID3 ট্যাগ, OPF ফাইল, এবং টেক্সট ফাইলগুলি নতুন হিসাবে স্ক্যান করা হবে।",
|
"MessageForceReScanDescription": "সকল ফাইল আবার নতুন স্ক্যানের মত স্ক্যান করবে। অডিও ফাইল ID3 ট্যাগ, OPF ফাইল, এবং টেক্সট ফাইলগুলি নতুন হিসাবে স্ক্যান করা হবে।",
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "Zkráceno",
|
"LabelAbridged": "Zkráceno",
|
||||||
"LabelAbridgedChecked": "Abridged (checked)",
|
"LabelAbridgedChecked": "Abridged (checked)",
|
||||||
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "Typ účtu",
|
"LabelAccountType": "Typ účtu",
|
||||||
"LabelAccountTypeAdmin": "Správce",
|
"LabelAccountTypeAdmin": "Správce",
|
||||||
"LabelAccountTypeGuest": "Host",
|
"LabelAccountTypeGuest": "Host",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "Povolit sledování",
|
"LabelSettingsEnableWatcher": "Povolit sledování",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Povolit sledování složky pro knihovnu",
|
"LabelSettingsEnableWatcherForLibrary": "Povolit sledování složky pro knihovnu",
|
||||||
"LabelSettingsEnableWatcherHelp": "Povoluje automatické přidávání/aktualizaci položek, když jsou zjištěny změny souborů. *Vyžaduje restart serveru",
|
"LabelSettingsEnableWatcherHelp": "Povoluje automatické přidávání/aktualizaci položek, když jsou zjištěny změny souborů. *Vyžaduje restart serveru",
|
||||||
|
"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.",
|
||||||
"LabelSettingsExperimentalFeatures": "Experimentální funkce",
|
"LabelSettingsExperimentalFeatures": "Experimentální funkce",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Funkce ve vývoji, které by mohly využít vaši zpětnou vazbu a pomoc s testováním. Kliknutím otevřete diskuzi na githubu.",
|
"LabelSettingsExperimentalFeaturesHelp": "Funkce ve vývoji, které by mohly využít vaši zpětnou vazbu a pomoc s testováním. Kliknutím otevřete diskuzi na githubu.",
|
||||||
"LabelSettingsFindCovers": "Najít obálky",
|
"LabelSettingsFindCovers": "Najít obálky",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "Přetáhněte soubory do správného pořadí stop",
|
"MessageDragFilesIntoTrackOrder": "Přetáhněte soubory do správného pořadí stop",
|
||||||
"MessageEmbedFinished": "Vložení dokončeno!",
|
"MessageEmbedFinished": "Vložení dokončeno!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} epizody zařazené do fronty ke stažení",
|
"MessageEpisodesQueuedForDownload": "{0} epizody zařazené do fronty ke stažení",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "URL zdroje bude {0}",
|
"MessageFeedURLWillBe": "URL zdroje bude {0}",
|
||||||
"MessageFetching": "Stahování...",
|
"MessageFetching": "Stahování...",
|
||||||
"MessageForceReScanDescription": "znovu prohledá všechny soubory jako při novém skenování. ID3 tagy zvukových souborů OPF soubory a textové soubory budou skenovány jako nové.",
|
"MessageForceReScanDescription": "znovu prohledá všechny soubory jako při novém skenování. ID3 tagy zvukových souborů OPF soubory a textové soubory budou skenovány jako nové.",
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "Abridged",
|
"LabelAbridged": "Abridged",
|
||||||
"LabelAbridgedChecked": "Abridged (checked)",
|
"LabelAbridgedChecked": "Abridged (checked)",
|
||||||
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "Kontotype",
|
"LabelAccountType": "Kontotype",
|
||||||
"LabelAccountTypeAdmin": "Administrator",
|
"LabelAccountTypeAdmin": "Administrator",
|
||||||
"LabelAccountTypeGuest": "Gæst",
|
"LabelAccountTypeGuest": "Gæst",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "Aktiver overvågning",
|
"LabelSettingsEnableWatcher": "Aktiver overvågning",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Aktiver mappeovervågning for bibliotek",
|
"LabelSettingsEnableWatcherForLibrary": "Aktiver mappeovervågning for bibliotek",
|
||||||
"LabelSettingsEnableWatcherHelp": "Aktiverer automatisk tilføjelse/opdatering af elementer, når filændringer registreres. *Kræver servergenstart",
|
"LabelSettingsEnableWatcherHelp": "Aktiverer automatisk tilføjelse/opdatering af elementer, når filændringer registreres. *Kræver servergenstart",
|
||||||
|
"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.",
|
||||||
"LabelSettingsExperimentalFeatures": "Eksperimentelle funktioner",
|
"LabelSettingsExperimentalFeatures": "Eksperimentelle funktioner",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Funktioner under udvikling, der kunne bruge din feedback og hjælp til test. Klik for at åbne Github-diskussionen.",
|
"LabelSettingsExperimentalFeaturesHelp": "Funktioner under udvikling, der kunne bruge din feedback og hjælp til test. Klik for at åbne Github-diskussionen.",
|
||||||
"LabelSettingsFindCovers": "Find omslag",
|
"LabelSettingsFindCovers": "Find omslag",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "Træk filer ind i korrekt spororden",
|
"MessageDragFilesIntoTrackOrder": "Træk filer ind i korrekt spororden",
|
||||||
"MessageEmbedFinished": "Indlejring færdig!",
|
"MessageEmbedFinished": "Indlejring færdig!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} episoder er sat i kø til download",
|
"MessageEpisodesQueuedForDownload": "{0} episoder er sat i kø til download",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "Feed-URL vil være {0}",
|
"MessageFeedURLWillBe": "Feed-URL vil være {0}",
|
||||||
"MessageFetching": "Henter...",
|
"MessageFetching": "Henter...",
|
||||||
"MessageForceReScanDescription": "vil scanne alle filer igen som en frisk scanning. Lydfilens ID3-tags, OPF-filer og tekstfiler scannes som nye.",
|
"MessageForceReScanDescription": "vil scanne alle filer igen som en frisk scanning. Lydfilens ID3-tags, OPF-filer og tekstfiler scannes som nye.",
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "Gekürzt",
|
"LabelAbridged": "Gekürzt",
|
||||||
"LabelAbridgedChecked": "Gekürzt (angehakt)",
|
"LabelAbridgedChecked": "Gekürzt (angehakt)",
|
||||||
"LabelAbridgedUnchecked": "Ungekürzt (nicht angehakt)",
|
"LabelAbridgedUnchecked": "Ungekürzt (nicht angehakt)",
|
||||||
|
"LabelAccessibleBy": "Zugänglich für",
|
||||||
"LabelAccountType": "Kontoart",
|
"LabelAccountType": "Kontoart",
|
||||||
"LabelAccountTypeAdmin": "Admin",
|
"LabelAccountTypeAdmin": "Admin",
|
||||||
"LabelAccountTypeGuest": "Gast",
|
"LabelAccountTypeGuest": "Gast",
|
||||||
|
|
@ -270,7 +271,7 @@
|
||||||
"LabelDownloadNEpisodes": "Download {0} Episoden",
|
"LabelDownloadNEpisodes": "Download {0} Episoden",
|
||||||
"LabelDuration": "Laufzeit",
|
"LabelDuration": "Laufzeit",
|
||||||
"LabelDurationComparisonExactMatch": "(genauer Treffer)",
|
"LabelDurationComparisonExactMatch": "(genauer Treffer)",
|
||||||
"LabelDurationComparisonLonger": "({0} änger)",
|
"LabelDurationComparisonLonger": "({0} länger)",
|
||||||
"LabelDurationComparisonShorter": "({0} kürzer)",
|
"LabelDurationComparisonShorter": "({0} kürzer)",
|
||||||
"LabelDurationFound": "Gefundene Laufzeit:",
|
"LabelDurationFound": "Gefundene Laufzeit:",
|
||||||
"LabelEbook": "E-Book",
|
"LabelEbook": "E-Book",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "Überwachung aktivieren",
|
"LabelSettingsEnableWatcher": "Überwachung aktivieren",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Ordnerüberwachung für die Bibliothek aktivieren",
|
"LabelSettingsEnableWatcherForLibrary": "Ordnerüberwachung für die Bibliothek aktivieren",
|
||||||
"LabelSettingsEnableWatcherHelp": "Aktiviert das automatische Hinzufügen/Aktualisieren von Elementen, wenn Dateiänderungen erkannt werden. *Erfordert einen Server-Neustart",
|
"LabelSettingsEnableWatcherHelp": "Aktiviert das automatische Hinzufügen/Aktualisieren von Elementen, wenn Dateiänderungen erkannt werden. *Erfordert einen Server-Neustart",
|
||||||
|
"LabelSettingsEpubsAllowScriptedContent": "Skriptinhalte in Epubs zulassen",
|
||||||
|
"LabelSettingsEpubsAllowScriptedContentHelp": "Erlaube Epub-Dateien, Skripte auszuführen. Es wird empfohlen, diese Einstellung deaktiviert zu lassen, es sei denn, du vertraust der Quelle der Epub-Dateien.",
|
||||||
"LabelSettingsExperimentalFeatures": "Experimentelle Funktionen",
|
"LabelSettingsExperimentalFeatures": "Experimentelle Funktionen",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Funktionen welche sich in der Entwicklung befinden, benötigen dein Feedback und deine Hilfe beim Testen. Klicke hier, um die Github-Diskussion zu öffnen.",
|
"LabelSettingsExperimentalFeaturesHelp": "Funktionen welche sich in der Entwicklung befinden, benötigen dein Feedback und deine Hilfe beim Testen. Klicke hier, um die Github-Diskussion zu öffnen.",
|
||||||
"LabelSettingsFindCovers": "Suche Titelbilder",
|
"LabelSettingsFindCovers": "Suche Titelbilder",
|
||||||
|
|
@ -498,7 +501,7 @@
|
||||||
"LabelShowAll": "Alles anzeigen",
|
"LabelShowAll": "Alles anzeigen",
|
||||||
"LabelShowSeconds": "Zeige Sekunden",
|
"LabelShowSeconds": "Zeige Sekunden",
|
||||||
"LabelSize": "Größe",
|
"LabelSize": "Größe",
|
||||||
"LabelSleepTimer": "Sleep-Timer",
|
"LabelSleepTimer": "Schlummerfunktion",
|
||||||
"LabelSlug": "URL Teil",
|
"LabelSlug": "URL Teil",
|
||||||
"LabelStart": "Start",
|
"LabelStart": "Start",
|
||||||
"LabelStarted": "Gestartet",
|
"LabelStarted": "Gestartet",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "Verschiebe die Dateien in die richtige Reihenfolge",
|
"MessageDragFilesIntoTrackOrder": "Verschiebe die Dateien in die richtige Reihenfolge",
|
||||||
"MessageEmbedFinished": "Einbettung abgeschlossen!",
|
"MessageEmbedFinished": "Einbettung abgeschlossen!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} Episode(n) in der Warteschlange zum Herunterladen",
|
"MessageEpisodesQueuedForDownload": "{0} Episode(n) in der Warteschlange zum Herunterladen",
|
||||||
|
"MessageEreaderDevices": "Um die Zustellung von E-Books sicherzustellen, musst du eventuell die oben genannte E-Mail-Adresse als gültigen Absender für jedes unten aufgeführte Gerät hinzufügen.",
|
||||||
"MessageFeedURLWillBe": "Feed-URL wird {0} sein",
|
"MessageFeedURLWillBe": "Feed-URL wird {0} sein",
|
||||||
"MessageFetching": "Abrufen...",
|
"MessageFetching": "Abrufen...",
|
||||||
"MessageForceReScanDescription": "Durchsucht alle Dateien erneut, wie bei einem frischen Scan. ID3-Tags von Audiodateien, OPF-Dateien und Textdateien werden neu durchsucht.",
|
"MessageForceReScanDescription": "Durchsucht alle Dateien erneut, wie bei einem frischen Scan. ID3-Tags von Audiodateien, OPF-Dateien und Textdateien werden neu durchsucht.",
|
||||||
|
|
|
||||||
|
|
@ -195,6 +195,7 @@
|
||||||
"LabelAbridged": "Abridged",
|
"LabelAbridged": "Abridged",
|
||||||
"LabelAbridgedChecked": "Abridged (checked)",
|
"LabelAbridgedChecked": "Abridged (checked)",
|
||||||
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "Account Type",
|
"LabelAccountType": "Account Type",
|
||||||
"LabelAccountTypeAdmin": "Admin",
|
"LabelAccountTypeAdmin": "Admin",
|
||||||
"LabelAccountTypeGuest": "Guest",
|
"LabelAccountTypeGuest": "Guest",
|
||||||
|
|
@ -483,6 +484,8 @@
|
||||||
"LabelSettingsEnableWatcher": "Enable Watcher",
|
"LabelSettingsEnableWatcher": "Enable Watcher",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
|
"LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
|
||||||
"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",
|
||||||
|
"LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
|
||||||
"LabelSettingsExperimentalFeatures": "Experimental features",
|
"LabelSettingsExperimentalFeatures": "Experimental features",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Features in development that could use your feedback and help testing. Click to open github discussion.",
|
"LabelSettingsExperimentalFeaturesHelp": "Features in development that could use your feedback and help testing. Click to open github discussion.",
|
||||||
"LabelSettingsFindCovers": "Find covers",
|
"LabelSettingsFindCovers": "Find covers",
|
||||||
|
|
@ -643,6 +646,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
|
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
|
||||||
"MessageEmbedFinished": "Embed Finished!",
|
"MessageEmbedFinished": "Embed Finished!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} Episode(s) queued for download",
|
"MessageEpisodesQueuedForDownload": "{0} Episode(s) queued for download",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "Feed URL will be {0}",
|
"MessageFeedURLWillBe": "Feed URL will be {0}",
|
||||||
"MessageFetching": "Fetching...",
|
"MessageFetching": "Fetching...",
|
||||||
"MessageForceReScanDescription": "will scan all files again like a fresh scan. Audio file ID3 tags, OPF files, and text files will be scanned as new.",
|
"MessageForceReScanDescription": "will scan all files again like a fresh scan. Audio file ID3 tags, OPF files, and text files will be scanned as new.",
|
||||||
|
|
|
||||||
|
|
@ -61,9 +61,9 @@
|
||||||
"ButtonQueueRemoveItem": "Remover de la Fila",
|
"ButtonQueueRemoveItem": "Remover de la Fila",
|
||||||
"ButtonQuickMatch": "Encontrar Rápido",
|
"ButtonQuickMatch": "Encontrar Rápido",
|
||||||
"ButtonRead": "Leer",
|
"ButtonRead": "Leer",
|
||||||
"ButtonReadLess": "Read less",
|
"ButtonReadLess": "Lea menos",
|
||||||
"ButtonReadMore": "Read more",
|
"ButtonReadMore": "Lea mas",
|
||||||
"ButtonRefresh": "Refresh",
|
"ButtonRefresh": "Refrecar",
|
||||||
"ButtonRemove": "Remover",
|
"ButtonRemove": "Remover",
|
||||||
"ButtonRemoveAll": "Remover Todos",
|
"ButtonRemoveAll": "Remover Todos",
|
||||||
"ButtonRemoveAllLibraryItems": "Remover Todos los Elementos de la Biblioteca",
|
"ButtonRemoveAllLibraryItems": "Remover Todos los Elementos de la Biblioteca",
|
||||||
|
|
@ -72,7 +72,7 @@
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Remover Serie de Continuar Series",
|
"ButtonRemoveSeriesFromContinueSeries": "Remover Serie de Continuar Series",
|
||||||
"ButtonReScan": "Re-Escanear",
|
"ButtonReScan": "Re-Escanear",
|
||||||
"ButtonReset": "Reiniciar",
|
"ButtonReset": "Reiniciar",
|
||||||
"ButtonResetToDefault": "Reset to default",
|
"ButtonResetToDefault": "Restaurar valores por defecto",
|
||||||
"ButtonRestore": "Restaurar",
|
"ButtonRestore": "Restaurar",
|
||||||
"ButtonSave": "Guardar",
|
"ButtonSave": "Guardar",
|
||||||
"ButtonSaveAndClose": "Guardar y Cerrar",
|
"ButtonSaveAndClose": "Guardar y Cerrar",
|
||||||
|
|
@ -83,7 +83,7 @@
|
||||||
"ButtonSelectFolderPath": "Seleccionar Ruta de Carpeta",
|
"ButtonSelectFolderPath": "Seleccionar Ruta de Carpeta",
|
||||||
"ButtonSeries": "Series",
|
"ButtonSeries": "Series",
|
||||||
"ButtonSetChaptersFromTracks": "Seleccionar Capítulos Según las Pistas",
|
"ButtonSetChaptersFromTracks": "Seleccionar Capítulos Según las Pistas",
|
||||||
"ButtonShare": "Share",
|
"ButtonShare": "Compartir",
|
||||||
"ButtonShiftTimes": "Desplazar Tiempos",
|
"ButtonShiftTimes": "Desplazar Tiempos",
|
||||||
"ButtonShow": "Mostrar",
|
"ButtonShow": "Mostrar",
|
||||||
"ButtonStartM4BEncode": "Iniciar Codificación M4B",
|
"ButtonStartM4BEncode": "Iniciar Codificación M4B",
|
||||||
|
|
@ -161,11 +161,11 @@
|
||||||
"HeaderRemoveEpisodes": "Remover {0} Episodios",
|
"HeaderRemoveEpisodes": "Remover {0} Episodios",
|
||||||
"HeaderRSSFeedGeneral": "Detalles RSS",
|
"HeaderRSSFeedGeneral": "Detalles RSS",
|
||||||
"HeaderRSSFeedIsOpen": "Fuente RSS esta abierta",
|
"HeaderRSSFeedIsOpen": "Fuente RSS esta abierta",
|
||||||
"HeaderRSSFeeds": "RSS Feeds",
|
"HeaderRSSFeeds": "Fuentes RSS",
|
||||||
"HeaderSavedMediaProgress": "Guardar Progreso de Multimedia",
|
"HeaderSavedMediaProgress": "Guardar Progreso de Multimedia",
|
||||||
"HeaderSchedule": "Horario",
|
"HeaderSchedule": "Horario",
|
||||||
"HeaderScheduleLibraryScans": "Programar Escaneo Automático de Biblioteca",
|
"HeaderScheduleLibraryScans": "Programar Escaneo Automático de Biblioteca",
|
||||||
"HeaderSession": "Session",
|
"HeaderSession": "Sesión",
|
||||||
"HeaderSetBackupSchedule": "Programar Respaldo",
|
"HeaderSetBackupSchedule": "Programar Respaldo",
|
||||||
"HeaderSettings": "Configuraciones",
|
"HeaderSettings": "Configuraciones",
|
||||||
"HeaderSettingsDisplay": "Interfaz",
|
"HeaderSettingsDisplay": "Interfaz",
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "Abreviado",
|
"LabelAbridged": "Abreviado",
|
||||||
"LabelAbridgedChecked": "Abridged (checked)",
|
"LabelAbridgedChecked": "Abridged (checked)",
|
||||||
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "Tipo de Cuenta",
|
"LabelAccountType": "Tipo de Cuenta",
|
||||||
"LabelAccountTypeAdmin": "Administrador",
|
"LabelAccountTypeAdmin": "Administrador",
|
||||||
"LabelAccountTypeGuest": "Invitado",
|
"LabelAccountTypeGuest": "Invitado",
|
||||||
|
|
@ -207,7 +208,7 @@
|
||||||
"LabelAllUsers": "Todos los Usuarios",
|
"LabelAllUsers": "Todos los Usuarios",
|
||||||
"LabelAllUsersExcludingGuests": "Todos los usuarios excepto invitados",
|
"LabelAllUsersExcludingGuests": "Todos los usuarios excepto invitados",
|
||||||
"LabelAllUsersIncludingGuests": "Todos los usuarios e invitados",
|
"LabelAllUsersIncludingGuests": "Todos los usuarios e invitados",
|
||||||
"LabelAlreadyInYourLibrary": "Ya en la Biblioteca",
|
"LabelAlreadyInYourLibrary": "Ya existe en la Biblioteca",
|
||||||
"LabelAppend": "Adjuntar",
|
"LabelAppend": "Adjuntar",
|
||||||
"LabelAuthor": "Autor",
|
"LabelAuthor": "Autor",
|
||||||
"LabelAuthorFirstLast": "Autor (Nombre Apellido)",
|
"LabelAuthorFirstLast": "Autor (Nombre Apellido)",
|
||||||
|
|
@ -269,9 +270,9 @@
|
||||||
"LabelDownload": "Descargar",
|
"LabelDownload": "Descargar",
|
||||||
"LabelDownloadNEpisodes": "Descargar {0} episodios",
|
"LabelDownloadNEpisodes": "Descargar {0} episodios",
|
||||||
"LabelDuration": "Duración",
|
"LabelDuration": "Duración",
|
||||||
"LabelDurationComparisonExactMatch": "(exact match)",
|
"LabelDurationComparisonExactMatch": "(coincidencia exacta)",
|
||||||
"LabelDurationComparisonLonger": "({0} longer)",
|
"LabelDurationComparisonLonger": "({0} más largo)",
|
||||||
"LabelDurationComparisonShorter": "({0} shorter)",
|
"LabelDurationComparisonShorter": "({0} más corto)",
|
||||||
"LabelDurationFound": "Duración Comprobada:",
|
"LabelDurationFound": "Duración Comprobada:",
|
||||||
"LabelEbook": "Ebook",
|
"LabelEbook": "Ebook",
|
||||||
"LabelEbooks": "Ebooks",
|
"LabelEbooks": "Ebooks",
|
||||||
|
|
@ -289,8 +290,8 @@
|
||||||
"LabelEpisodeType": "Tipo de Episodio",
|
"LabelEpisodeType": "Tipo de Episodio",
|
||||||
"LabelExample": "Ejemplo",
|
"LabelExample": "Ejemplo",
|
||||||
"LabelExplicit": "Explicito",
|
"LabelExplicit": "Explicito",
|
||||||
"LabelExplicitChecked": "Explicit (checked)",
|
"LabelExplicitChecked": "Explícito (marcado)",
|
||||||
"LabelExplicitUnchecked": "Not Explicit (unchecked)",
|
"LabelExplicitUnchecked": "No Explícito (sin marcar)",
|
||||||
"LabelFeedURL": "Fuente de URL",
|
"LabelFeedURL": "Fuente de URL",
|
||||||
"LabelFetchingMetadata": "Obteniendo metadatos",
|
"LabelFetchingMetadata": "Obteniendo metadatos",
|
||||||
"LabelFile": "Archivo",
|
"LabelFile": "Archivo",
|
||||||
|
|
@ -365,8 +366,8 @@
|
||||||
"LabelMetaTags": "Metaetiquetas",
|
"LabelMetaTags": "Metaetiquetas",
|
||||||
"LabelMinute": "Minuto",
|
"LabelMinute": "Minuto",
|
||||||
"LabelMissing": "Ausente",
|
"LabelMissing": "Ausente",
|
||||||
"LabelMissingEbook": "Has no ebook",
|
"LabelMissingEbook": "No tiene ebook",
|
||||||
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
|
"LabelMissingSupplementaryEbook": "No tiene ebook suplementario",
|
||||||
"LabelMobileRedirectURIs": "URIs de redirección a móviles permitidos",
|
"LabelMobileRedirectURIs": "URIs de redirección a móviles permitidos",
|
||||||
"LabelMobileRedirectURIsDescription": "Esta es una lista de URIs válidos para redireccionamiento de apps móviles. La URI por defecto es <code>audiobookshelf://oauth</code>, la cual puedes remover or corroborar con URIs adicionales para la integración con apps de terceros. Utilizando un asterisco (<code>*</code>) como el único punto de entrada permite cualquier URI.",
|
"LabelMobileRedirectURIsDescription": "Esta es una lista de URIs válidos para redireccionamiento de apps móviles. La URI por defecto es <code>audiobookshelf://oauth</code>, la cual puedes remover or corroborar con URIs adicionales para la integración con apps de terceros. Utilizando un asterisco (<code>*</code>) como el único punto de entrada permite cualquier URI.",
|
||||||
"LabelMore": "Más",
|
"LabelMore": "Más",
|
||||||
|
|
@ -469,7 +470,9 @@
|
||||||
"LabelSettingsDisableWatcherHelp": "Deshabilitar la función de agregar/actualizar elementos automáticamente cuando se detectan cambios en los archivos. *Require Reiniciar el Servidor",
|
"LabelSettingsDisableWatcherHelp": "Deshabilitar la función de agregar/actualizar elementos automáticamente cuando se detectan cambios en los archivos. *Require Reiniciar el Servidor",
|
||||||
"LabelSettingsEnableWatcher": "Habilitar Watcher",
|
"LabelSettingsEnableWatcher": "Habilitar Watcher",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Habilitar Watcher para la carpeta de esta biblioteca",
|
"LabelSettingsEnableWatcherForLibrary": "Habilitar Watcher para la carpeta de esta biblioteca",
|
||||||
"LabelSettingsEnableWatcherHelp": "Permite agregar/actualizar elementos automáticamente cuando se detectan cambios en los archivos. *Requires server restart",
|
"LabelSettingsEnableWatcherHelp": "Permite agregar/actualizar elementos automáticamente cuando se detectan cambios en los archivos. *Requiere reiniciar el servidor",
|
||||||
|
"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.",
|
||||||
"LabelSettingsExperimentalFeatures": "Funciones Experimentales",
|
"LabelSettingsExperimentalFeatures": "Funciones Experimentales",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Funciones en desarrollo que se beneficiarían de sus comentarios y experiencias de prueba. Haga click aquí para abrir una conversación en Github.",
|
"LabelSettingsExperimentalFeaturesHelp": "Funciones en desarrollo que se beneficiarían de sus comentarios y experiencias de prueba. Haga click aquí para abrir una conversación en Github.",
|
||||||
"LabelSettingsFindCovers": "Buscar Portadas",
|
"LabelSettingsFindCovers": "Buscar Portadas",
|
||||||
|
|
@ -479,7 +482,7 @@
|
||||||
"LabelSettingsHomePageBookshelfView": "Usar la vista de librero en la página principal",
|
"LabelSettingsHomePageBookshelfView": "Usar la vista de librero en la página principal",
|
||||||
"LabelSettingsLibraryBookshelfView": "Usar la vista de librero en la biblioteca",
|
"LabelSettingsLibraryBookshelfView": "Usar la vista de librero en la biblioteca",
|
||||||
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
|
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
|
||||||
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
|
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "El estante de la página de inicio de Continuar Serie muestra el primer libro no iniciado de una serie que tenga por lo menos un libro finalizado y no tenga libros en progreso. Habilitar esta opción le permitirá continuar series desde el último libro que ha completado en vez del primer libro que no ha empezado.",
|
||||||
"LabelSettingsParseSubtitles": "Extraer Subtítulos",
|
"LabelSettingsParseSubtitles": "Extraer Subtítulos",
|
||||||
"LabelSettingsParseSubtitlesHelp": "Extraer subtítulos de los nombres de las carpetas de los audiolibros.<br>Los subtítulos deben estar separados por \" - \"<br>Por ejemplo: \"Ejemplo de Título - Subtítulo Aquí\" tiene el subtítulo \"Subtítulo Aquí\"",
|
"LabelSettingsParseSubtitlesHelp": "Extraer subtítulos de los nombres de las carpetas de los audiolibros.<br>Los subtítulos deben estar separados por \" - \"<br>Por ejemplo: \"Ejemplo de Título - Subtítulo Aquí\" tiene el subtítulo \"Subtítulo Aquí\"",
|
||||||
"LabelSettingsPreferMatchedMetadata": "Preferir metadatos encontrados",
|
"LabelSettingsPreferMatchedMetadata": "Preferir metadatos encontrados",
|
||||||
|
|
@ -496,7 +499,7 @@
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "Por defecto, los archivos de metadatos se almacenan en /metadata/items. Si habilita esta opción, los archivos de metadatos se guardarán en la carpeta de elementos de su biblioteca",
|
"LabelSettingsStoreMetadataWithItemHelp": "Por defecto, los archivos de metadatos se almacenan en /metadata/items. Si habilita esta opción, los archivos de metadatos se guardarán en la carpeta de elementos de su biblioteca",
|
||||||
"LabelSettingsTimeFormat": "Formato de Tiempo",
|
"LabelSettingsTimeFormat": "Formato de Tiempo",
|
||||||
"LabelShowAll": "Mostrar Todos",
|
"LabelShowAll": "Mostrar Todos",
|
||||||
"LabelShowSeconds": "Show seconds",
|
"LabelShowSeconds": "Mostrar segundos",
|
||||||
"LabelSize": "Tamaño",
|
"LabelSize": "Tamaño",
|
||||||
"LabelSleepTimer": "Temporizador para Dormir",
|
"LabelSleepTimer": "Temporizador para Dormir",
|
||||||
"LabelSlug": "Slug",
|
"LabelSlug": "Slug",
|
||||||
|
|
@ -576,8 +579,8 @@
|
||||||
"LabelViewQueue": "Ver Fila del Reproductor",
|
"LabelViewQueue": "Ver Fila del Reproductor",
|
||||||
"LabelVolume": "Volumen",
|
"LabelVolume": "Volumen",
|
||||||
"LabelWeekdaysToRun": "Correr en Días de la Semana",
|
"LabelWeekdaysToRun": "Correr en Días de la Semana",
|
||||||
"LabelYearReviewHide": "Hide Year in Review",
|
"LabelYearReviewHide": "Ocultar Year in Review",
|
||||||
"LabelYearReviewShow": "See Year in Review",
|
"LabelYearReviewShow": "Ver Year in Review",
|
||||||
"LabelYourAudiobookDuration": "Duración de tu Audiolibro",
|
"LabelYourAudiobookDuration": "Duración de tu Audiolibro",
|
||||||
"LabelYourBookmarks": "Tus Marcadores",
|
"LabelYourBookmarks": "Tus Marcadores",
|
||||||
"LabelYourPlaylists": "Tus Listas",
|
"LabelYourPlaylists": "Tus Listas",
|
||||||
|
|
@ -608,14 +611,14 @@
|
||||||
"MessageConfirmMarkAllEpisodesNotFinished": "¿Está seguro de que desea marcar todos los episodios como no terminados?",
|
"MessageConfirmMarkAllEpisodesNotFinished": "¿Está seguro de que desea marcar todos los episodios como no terminados?",
|
||||||
"MessageConfirmMarkSeriesFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como terminados?",
|
"MessageConfirmMarkSeriesFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como terminados?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como no terminados?",
|
"MessageConfirmMarkSeriesNotFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como no terminados?",
|
||||||
"MessageConfirmPurgeCache": "Purge cache will delete the entire directory at <code>/metadata/cache</code>. <br /><br />Are you sure you want to remove the cache directory?",
|
"MessageConfirmPurgeCache": "Purgar el caché eliminará el directorio completo ubicado en <code>/metadata/cache</code>. <br /><br />¿Está seguro que desea eliminar el directorio del caché?",
|
||||||
"MessageConfirmQuickEmbed": "¡Advertencia! La integración rápida no realiza copias de seguridad a ninguno de tus archivos de audio. Asegúrate de haber realizado una copia de los mismos previamente. <br><br>¿Deseas continuar?",
|
"MessageConfirmQuickEmbed": "¡Advertencia! La integración rápida no realiza copias de seguridad a ninguno de tus archivos de audio. Asegúrate de haber realizado una copia de los mismos previamente. <br><br>¿Deseas continuar?",
|
||||||
"MessageConfirmRemoveAllChapters": "¿Está seguro de que desea remover todos los capitulos?",
|
"MessageConfirmRemoveAllChapters": "¿Está seguro de que desea remover todos los capitulos?",
|
||||||
"MessageConfirmRemoveAuthor": "¿Está seguro de que desea remover el autor \"{0}\"?",
|
"MessageConfirmRemoveAuthor": "¿Está seguro de que desea remover el autor \"{0}\"?",
|
||||||
"MessageConfirmRemoveCollection": "¿Está seguro de que desea remover la colección \"{0}\"?",
|
"MessageConfirmRemoveCollection": "¿Está seguro de que desea remover la colección \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "¿Está seguro de que desea remover el episodio \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "¿Está seguro de que desea remover el episodio \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "¿Está seguro de que desea remover {0} episodios?",
|
"MessageConfirmRemoveEpisodes": "¿Está seguro de que desea remover {0} episodios?",
|
||||||
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
|
"MessageConfirmRemoveListeningSessions": "¿Está seguro que desea remover {0} sesiones de escuchar?",
|
||||||
"MessageConfirmRemoveNarrator": "¿Está seguro de que desea remover el narrador \"{0}\"?",
|
"MessageConfirmRemoveNarrator": "¿Está seguro de que desea remover el narrador \"{0}\"?",
|
||||||
"MessageConfirmRemovePlaylist": "¿Está seguro de que desea remover la lista de reproducción \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "¿Está seguro de que desea remover la lista de reproducción \"{0}\"?",
|
||||||
"MessageConfirmRenameGenre": "¿Está seguro de que desea renombrar el genero \"{0}\" a \"{1}\" de todos los elementos?",
|
"MessageConfirmRenameGenre": "¿Está seguro de que desea renombrar el genero \"{0}\" a \"{1}\" de todos los elementos?",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "Arrastra los archivos al orden correcto de las pistas.",
|
"MessageDragFilesIntoTrackOrder": "Arrastra los archivos al orden correcto de las pistas.",
|
||||||
"MessageEmbedFinished": "Incrustación Terminada!",
|
"MessageEmbedFinished": "Incrustación Terminada!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} Episodio(s) en cola para descargar",
|
"MessageEpisodesQueuedForDownload": "{0} Episodio(s) en cola para descargar",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "URL de la fuente será {0}",
|
"MessageFeedURLWillBe": "URL de la fuente será {0}",
|
||||||
"MessageFetching": "Buscando...",
|
"MessageFetching": "Buscando...",
|
||||||
"MessageForceReScanDescription": "Escaneará todos los archivos como un nuevo escaneo. Archivos de audio con etiquetas ID3, archivos OPF y archivos de texto serán escaneados como nuevos.",
|
"MessageForceReScanDescription": "Escaneará todos los archivos como un nuevo escaneo. Archivos de audio con etiquetas ID3, archivos OPF y archivos de texto serán escaneados como nuevos.",
|
||||||
|
|
@ -641,7 +645,7 @@
|
||||||
"MessageListeningSessionsInTheLastYear": "{0} sesiones de escucha en el último año",
|
"MessageListeningSessionsInTheLastYear": "{0} sesiones de escucha en el último año",
|
||||||
"MessageLoading": "Cargando...",
|
"MessageLoading": "Cargando...",
|
||||||
"MessageLoadingFolders": "Cargando archivos...",
|
"MessageLoadingFolders": "Cargando archivos...",
|
||||||
"MessageLogsDescription": "Logs are stored in <code>/metadata/logs</code> as JSON files. Crash logs are stored in <code>/metadata/logs/crash_logs.txt</code>.",
|
"MessageLogsDescription": "Logs son almacenados en <code>/metadata/logs</code> en archivos bajo formato JSON. Logs de fallos son almacenados en <code>/metadata/logs/crash_logs.txt</code>.",
|
||||||
"MessageM4BFailed": "¡Fallo de M4B!",
|
"MessageM4BFailed": "¡Fallo de M4B!",
|
||||||
"MessageM4BFinished": "¡M4B Terminado!",
|
"MessageM4BFinished": "¡M4B Terminado!",
|
||||||
"MessageMapChapterTitles": "Asignar los nombres de capítulos a los capítulos existentes en tu audiolibro sin ajustar sus tiempos",
|
"MessageMapChapterTitles": "Asignar los nombres de capítulos a los capítulos existentes en tu audiolibro sin ajustar sus tiempos",
|
||||||
|
|
@ -689,7 +693,7 @@
|
||||||
"MessageQuickMatchDescription": "Rellenar detalles de elementos vacíos y portada con los primeros resultados de '{0}'. No sobrescribe los detalles a menos que la opción \"Preferir Metadatos Encontrados\" del servidor esté habilitada.",
|
"MessageQuickMatchDescription": "Rellenar detalles de elementos vacíos y portada con los primeros resultados de '{0}'. No sobrescribe los detalles a menos que la opción \"Preferir Metadatos Encontrados\" del servidor esté habilitada.",
|
||||||
"MessageRemoveChapter": "Remover capítulos",
|
"MessageRemoveChapter": "Remover capítulos",
|
||||||
"MessageRemoveEpisodes": "Remover {0} episodio(s)",
|
"MessageRemoveEpisodes": "Remover {0} episodio(s)",
|
||||||
"MessageRemoveFromPlayerQueue": "Romover la cola de reproducción",
|
"MessageRemoveFromPlayerQueue": "Remover la cola de reproducción",
|
||||||
"MessageRemoveUserWarning": "¿Está seguro de que desea eliminar el usuario \"{0}\"?",
|
"MessageRemoveUserWarning": "¿Está seguro de que desea eliminar el usuario \"{0}\"?",
|
||||||
"MessageReportBugsAndContribute": "Reporte erres, solicite funciones y contribuya en",
|
"MessageReportBugsAndContribute": "Reporte erres, solicite funciones y contribuya en",
|
||||||
"MessageResetChaptersConfirm": "¿Está seguro de que desea deshacer los cambios y revertir los capítulos a su estado original?",
|
"MessageResetChaptersConfirm": "¿Está seguro de que desea deshacer los cambios y revertir los capítulos a su estado original?",
|
||||||
|
|
@ -704,9 +708,9 @@
|
||||||
"MessageUploaderItemFailed": "Error al Subir",
|
"MessageUploaderItemFailed": "Error al Subir",
|
||||||
"MessageUploaderItemSuccess": "¡Éxito al Subir!",
|
"MessageUploaderItemSuccess": "¡Éxito al Subir!",
|
||||||
"MessageUploading": "Subiendo...",
|
"MessageUploading": "Subiendo...",
|
||||||
"MessageValidCronExpression": "Expresión de Cron bálida",
|
"MessageValidCronExpression": "Expresión de Cron válida",
|
||||||
"MessageWatcherIsDisabledGlobally": "El watcher está desactivado globalmente en la configuración del servidor",
|
"MessageWatcherIsDisabledGlobally": "El watcher está desactivado globalmente en la configuración del servidor",
|
||||||
"MessageXLibraryIsEmpty": "La biblioteca {0} está vacía!",
|
"MessageXLibraryIsEmpty": "¡La biblioteca {0} está vacía!",
|
||||||
"MessageYourAudiobookDurationIsLonger": "La duración de su audiolibro es más larga que la duración encontrada",
|
"MessageYourAudiobookDurationIsLonger": "La duración de su audiolibro es más larga que la duración encontrada",
|
||||||
"MessageYourAudiobookDurationIsShorter": "La duración de su audiolibro es más corta que la duración encontrada",
|
"MessageYourAudiobookDurationIsShorter": "La duración de su audiolibro es más corta que la duración encontrada",
|
||||||
"NoteChangeRootPassword": "El usuario Root es el único usuario que puede no tener una contraseña",
|
"NoteChangeRootPassword": "El usuario Root es el único usuario que puede no tener una contraseña",
|
||||||
|
|
@ -745,8 +749,8 @@
|
||||||
"ToastBookmarkRemoveSuccess": "Marcador eliminado",
|
"ToastBookmarkRemoveSuccess": "Marcador eliminado",
|
||||||
"ToastBookmarkUpdateFailed": "Error al actualizar el marcador",
|
"ToastBookmarkUpdateFailed": "Error al actualizar el marcador",
|
||||||
"ToastBookmarkUpdateSuccess": "Marcador actualizado",
|
"ToastBookmarkUpdateSuccess": "Marcador actualizado",
|
||||||
"ToastCachePurgeFailed": "Failed to purge cache",
|
"ToastCachePurgeFailed": "Error al purgar el caché",
|
||||||
"ToastCachePurgeSuccess": "Cache purged successfully",
|
"ToastCachePurgeSuccess": "Caché purgado de manera exitosa",
|
||||||
"ToastChaptersHaveErrors": "Los capítulos tienen errores",
|
"ToastChaptersHaveErrors": "Los capítulos tienen errores",
|
||||||
"ToastChaptersMustHaveTitles": "Los capítulos tienen que tener un título",
|
"ToastChaptersMustHaveTitles": "Los capítulos tienen que tener un título",
|
||||||
"ToastCollectionItemsRemoveFailed": "Error al remover elemento(s) de la colección",
|
"ToastCollectionItemsRemoveFailed": "Error al remover elemento(s) de la colección",
|
||||||
|
|
@ -755,9 +759,9 @@
|
||||||
"ToastCollectionRemoveSuccess": "Colección removida",
|
"ToastCollectionRemoveSuccess": "Colección removida",
|
||||||
"ToastCollectionUpdateFailed": "Error al actualizar la colección",
|
"ToastCollectionUpdateFailed": "Error al actualizar la colección",
|
||||||
"ToastCollectionUpdateSuccess": "Colección actualizada",
|
"ToastCollectionUpdateSuccess": "Colección actualizada",
|
||||||
"ToastDeleteFileFailed": "Failed to delete file",
|
"ToastDeleteFileFailed": "Error el eliminar archivo",
|
||||||
"ToastDeleteFileSuccess": "File deleted",
|
"ToastDeleteFileSuccess": "Archivo eliminado",
|
||||||
"ToastFailedToLoadData": "Failed to load data",
|
"ToastFailedToLoadData": "Error al cargar data",
|
||||||
"ToastItemCoverUpdateFailed": "Error al actualizar la portada del elemento",
|
"ToastItemCoverUpdateFailed": "Error al actualizar la portada del elemento",
|
||||||
"ToastItemCoverUpdateSuccess": "Portada del elemento actualizada",
|
"ToastItemCoverUpdateSuccess": "Portada del elemento actualizada",
|
||||||
"ToastItemDetailsUpdateFailed": "Error al actualizar los detalles del elemento",
|
"ToastItemDetailsUpdateFailed": "Error al actualizar los detalles del elemento",
|
||||||
|
|
@ -791,16 +795,16 @@
|
||||||
"ToastSendEbookToDeviceSuccess": "Ebook enviado al dispositivo \"{0}\"",
|
"ToastSendEbookToDeviceSuccess": "Ebook enviado al dispositivo \"{0}\"",
|
||||||
"ToastSeriesUpdateFailed": "Error al actualizar la serie",
|
"ToastSeriesUpdateFailed": "Error al actualizar la serie",
|
||||||
"ToastSeriesUpdateSuccess": "Serie actualizada",
|
"ToastSeriesUpdateSuccess": "Serie actualizada",
|
||||||
"ToastServerSettingsUpdateFailed": "Failed to update server settings",
|
"ToastServerSettingsUpdateFailed": "Error al actualizar configuración del servidor",
|
||||||
"ToastServerSettingsUpdateSuccess": "Server settings updated",
|
"ToastServerSettingsUpdateSuccess": "Configuración del servidor actualizada",
|
||||||
"ToastSessionDeleteFailed": "Error al eliminar sesión",
|
"ToastSessionDeleteFailed": "Error al eliminar sesión",
|
||||||
"ToastSessionDeleteSuccess": "Sesión eliminada",
|
"ToastSessionDeleteSuccess": "Sesión eliminada",
|
||||||
"ToastSocketConnected": "Socket conectado",
|
"ToastSocketConnected": "Socket conectado",
|
||||||
"ToastSocketDisconnected": "Socket desconectado",
|
"ToastSocketDisconnected": "Socket desconectado",
|
||||||
"ToastSocketFailedToConnect": "Error al conectar al Socket",
|
"ToastSocketFailedToConnect": "Error al conectar al Socket",
|
||||||
"ToastSortingPrefixesEmptyError": "Must have at least 1 sorting prefix",
|
"ToastSortingPrefixesEmptyError": "Debe tener por lo menos 1 prefijo para ordenar",
|
||||||
"ToastSortingPrefixesUpdateFailed": "Failed to update sorting prefixes",
|
"ToastSortingPrefixesUpdateFailed": "Error al actualizar los prefijos de ordenar",
|
||||||
"ToastSortingPrefixesUpdateSuccess": "Sorting prefixes updated ({0} items)",
|
"ToastSortingPrefixesUpdateSuccess": "Prefijos de ordenar actualizaron ({0} items)",
|
||||||
"ToastUserDeleteFailed": "Error al eliminar el usuario",
|
"ToastUserDeleteFailed": "Error al eliminar el usuario",
|
||||||
"ToastUserDeleteSuccess": "Usuario eliminado"
|
"ToastUserDeleteSuccess": "Usuario eliminado"
|
||||||
}
|
}
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "Kärbitud",
|
"LabelAbridged": "Kärbitud",
|
||||||
"LabelAbridgedChecked": "Abridged (checked)",
|
"LabelAbridgedChecked": "Abridged (checked)",
|
||||||
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "Konto tüüp",
|
"LabelAccountType": "Konto tüüp",
|
||||||
"LabelAccountTypeAdmin": "Administraator",
|
"LabelAccountTypeAdmin": "Administraator",
|
||||||
"LabelAccountTypeGuest": "Külaline",
|
"LabelAccountTypeGuest": "Külaline",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "Luba vaatamine",
|
"LabelSettingsEnableWatcher": "Luba vaatamine",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Luba kaustavaatamine raamatukogu jaoks",
|
"LabelSettingsEnableWatcherForLibrary": "Luba kaustavaatamine raamatukogu jaoks",
|
||||||
"LabelSettingsEnableWatcherHelp": "Lubab automaatset lisamist/uuendamist, kui tuvastatakse failimuudatused. *Nõuab serveri taaskäivitamist",
|
"LabelSettingsEnableWatcherHelp": "Lubab automaatset lisamist/uuendamist, kui tuvastatakse failimuudatused. *Nõuab serveri taaskäivitamist",
|
||||||
|
"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.",
|
||||||
"LabelSettingsExperimentalFeatures": "Eksperimentaalsed funktsioonid",
|
"LabelSettingsExperimentalFeatures": "Eksperimentaalsed funktsioonid",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Arengus olevad funktsioonid, mis vajavad teie tagasisidet ja abi testimisel. Klõpsake GitHubi arutelu avamiseks.",
|
"LabelSettingsExperimentalFeaturesHelp": "Arengus olevad funktsioonid, mis vajavad teie tagasisidet ja abi testimisel. Klõpsake GitHubi arutelu avamiseks.",
|
||||||
"LabelSettingsFindCovers": "Leia ümbrised",
|
"LabelSettingsFindCovers": "Leia ümbrised",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "Lohistage failid õigesse järjekorda",
|
"MessageDragFilesIntoTrackOrder": "Lohistage failid õigesse järjekorda",
|
||||||
"MessageEmbedFinished": "Manustamine lõpetatud!",
|
"MessageEmbedFinished": "Manustamine lõpetatud!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} Episood(i) on allalaadimiseks järjekorras",
|
"MessageEpisodesQueuedForDownload": "{0} Episood(i) on allalaadimiseks järjekorras",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "Toite URL saab olema {0}",
|
"MessageFeedURLWillBe": "Toite URL saab olema {0}",
|
||||||
"MessageFetching": "Hangitakse...",
|
"MessageFetching": "Hangitakse...",
|
||||||
"MessageForceReScanDescription": "skaneerib kõik failid uuesti nagu värsket skannimist. Heli faili ID3 silte, OPF faile ja tekstifaile skaneeritakse uuesti.",
|
"MessageForceReScanDescription": "skaneerib kõik failid uuesti nagu värsket skannimist. Heli faili ID3 silte, OPF faile ja tekstifaile skaneeritakse uuesti.",
|
||||||
|
|
|
||||||
|
|
@ -61,8 +61,8 @@
|
||||||
"ButtonQueueRemoveItem": "Supprimer de la liste de lecture",
|
"ButtonQueueRemoveItem": "Supprimer de la liste de lecture",
|
||||||
"ButtonQuickMatch": "Recherche rapide",
|
"ButtonQuickMatch": "Recherche rapide",
|
||||||
"ButtonRead": "Lire",
|
"ButtonRead": "Lire",
|
||||||
"ButtonReadLess": "Read less",
|
"ButtonReadLess": "Lire moins",
|
||||||
"ButtonReadMore": "Read more",
|
"ButtonReadMore": "Lire la suite",
|
||||||
"ButtonRefresh": "Rafraîchir",
|
"ButtonRefresh": "Rafraîchir",
|
||||||
"ButtonRemove": "Supprimer",
|
"ButtonRemove": "Supprimer",
|
||||||
"ButtonRemoveAll": "Supprimer tout",
|
"ButtonRemoveAll": "Supprimer tout",
|
||||||
|
|
@ -186,11 +186,12 @@
|
||||||
"HeaderUpdateDetails": "Mettre à jour les détails",
|
"HeaderUpdateDetails": "Mettre à jour les détails",
|
||||||
"HeaderUpdateLibrary": "Mettre à jour la bibliothèque",
|
"HeaderUpdateLibrary": "Mettre à jour la bibliothèque",
|
||||||
"HeaderUsers": "Utilisateurs",
|
"HeaderUsers": "Utilisateurs",
|
||||||
"HeaderYearReview": "Year {0} in Review",
|
"HeaderYearReview": "Bilan de l’année {0}",
|
||||||
"HeaderYourStats": "Vos statistiques",
|
"HeaderYourStats": "Vos statistiques",
|
||||||
"LabelAbridged": "Version courte",
|
"LabelAbridged": "Version courte",
|
||||||
"LabelAbridgedChecked": "Abridged (checked)",
|
"LabelAbridgedChecked": "Abrégé (vérifié)",
|
||||||
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
"LabelAbridgedUnchecked": "Intégral (non vérifié)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "Type de compte",
|
"LabelAccountType": "Type de compte",
|
||||||
"LabelAccountTypeAdmin": "Admin",
|
"LabelAccountTypeAdmin": "Admin",
|
||||||
"LabelAccountTypeGuest": "Invité",
|
"LabelAccountTypeGuest": "Invité",
|
||||||
|
|
@ -217,7 +218,7 @@
|
||||||
"LabelAutoFetchMetadata": "Recherche automatique de métadonnées",
|
"LabelAutoFetchMetadata": "Recherche automatique de métadonnées",
|
||||||
"LabelAutoFetchMetadataHelp": "Récupère les métadonnées du titre, de l’auteur et de la série pour simplifier le téléchargement. Il se peut que des métadonnées supplémentaires doivent être ajoutées après le téléchargement.",
|
"LabelAutoFetchMetadataHelp": "Récupère les métadonnées du titre, de l’auteur et de la série pour simplifier le téléchargement. Il se peut que des métadonnées supplémentaires doivent être ajoutées après le téléchargement.",
|
||||||
"LabelAutoLaunch": "Lancement automatique",
|
"LabelAutoLaunch": "Lancement automatique",
|
||||||
"LabelAutoLaunchDescription": "Redirection automatique vers le fournisseur d'authentification lors de la navigation vers la page de connexion (chemin de remplacement manuel <code>/login?autoLaunch=0</code>)",
|
"LabelAutoLaunchDescription": "Redirection automatique vers le fournisseur d’authentification lors de la navigation vers la page de connexion (chemin de remplacement manuel <code>/login?autoLaunch=0</code>)",
|
||||||
"LabelAutoRegister": "Enregistrement automatique",
|
"LabelAutoRegister": "Enregistrement automatique",
|
||||||
"LabelAutoRegisterDescription": "Créer automatiquement de nouveaux utilisateurs après la connexion",
|
"LabelAutoRegisterDescription": "Créer automatiquement de nouveaux utilisateurs après la connexion",
|
||||||
"LabelBackToUser": "Retour à l’utilisateur",
|
"LabelBackToUser": "Retour à l’utilisateur",
|
||||||
|
|
@ -231,7 +232,7 @@
|
||||||
"LabelBitrate": "Bitrate",
|
"LabelBitrate": "Bitrate",
|
||||||
"LabelBooks": "Livres",
|
"LabelBooks": "Livres",
|
||||||
"LabelButtonText": "Texte du bouton",
|
"LabelButtonText": "Texte du bouton",
|
||||||
"LabelByAuthor": "by {0}",
|
"LabelByAuthor": "par {0}",
|
||||||
"LabelChangePassword": "Modifier le mot de passe",
|
"LabelChangePassword": "Modifier le mot de passe",
|
||||||
"LabelChannels": "Canaux",
|
"LabelChannels": "Canaux",
|
||||||
"LabelChapters": "Chapitres",
|
"LabelChapters": "Chapitres",
|
||||||
|
|
@ -269,9 +270,9 @@
|
||||||
"LabelDownload": "Téléchargement",
|
"LabelDownload": "Téléchargement",
|
||||||
"LabelDownloadNEpisodes": "Télécharger {0} épisode(s)",
|
"LabelDownloadNEpisodes": "Télécharger {0} épisode(s)",
|
||||||
"LabelDuration": "Durée",
|
"LabelDuration": "Durée",
|
||||||
"LabelDurationComparisonExactMatch": "(exact match)",
|
"LabelDurationComparisonExactMatch": "(correspondance exacte)",
|
||||||
"LabelDurationComparisonLonger": "({0} longer)",
|
"LabelDurationComparisonLonger": "({0} plus long)",
|
||||||
"LabelDurationComparisonShorter": "({0} shorter)",
|
"LabelDurationComparisonShorter": "({0} plus court)",
|
||||||
"LabelDurationFound": "Durée trouvée :",
|
"LabelDurationFound": "Durée trouvée :",
|
||||||
"LabelEbook": "Livre numérique",
|
"LabelEbook": "Livre numérique",
|
||||||
"LabelEbooks": "Livres numériques",
|
"LabelEbooks": "Livres numériques",
|
||||||
|
|
@ -289,8 +290,8 @@
|
||||||
"LabelEpisodeType": "Type de l’épisode",
|
"LabelEpisodeType": "Type de l’épisode",
|
||||||
"LabelExample": "Exemple",
|
"LabelExample": "Exemple",
|
||||||
"LabelExplicit": "Restriction",
|
"LabelExplicit": "Restriction",
|
||||||
"LabelExplicitChecked": "Explicit (checked)",
|
"LabelExplicitChecked": "Explicite (vérifié)",
|
||||||
"LabelExplicitUnchecked": "Not Explicit (unchecked)",
|
"LabelExplicitUnchecked": "Non explicite (non vérifié)",
|
||||||
"LabelFeedURL": "URL du flux",
|
"LabelFeedURL": "URL du flux",
|
||||||
"LabelFetchingMetadata": "Récupération des métadonnées",
|
"LabelFetchingMetadata": "Récupération des métadonnées",
|
||||||
"LabelFile": "Fichier",
|
"LabelFile": "Fichier",
|
||||||
|
|
@ -365,10 +366,10 @@
|
||||||
"LabelMetaTags": "Balises de métadonnée",
|
"LabelMetaTags": "Balises de métadonnée",
|
||||||
"LabelMinute": "Minute",
|
"LabelMinute": "Minute",
|
||||||
"LabelMissing": "Manquant",
|
"LabelMissing": "Manquant",
|
||||||
"LabelMissingEbook": "Has no ebook",
|
"LabelMissingEbook": "Ne possède pas de livre numérique",
|
||||||
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
|
"LabelMissingSupplementaryEbook": "Ne possède pas de livre numérique supplémentaire",
|
||||||
"LabelMobileRedirectURIs": "URI de redirection mobile autorisés",
|
"LabelMobileRedirectURIs": "URI de redirection mobile autorisés",
|
||||||
"LabelMobileRedirectURIsDescription": "Il s'agit d'une liste blanche d’URI de redirection valides pour les applications mobiles. Celui par défaut est <code>audiobookshelf://oauth</code>, que vous pouvez supprimer ou compléter avec des URIs supplémentaires pour l'intégration d'applications tierces. L’utilisation d’un astérisque (<code>*</code>) comme seule entrée autorise n’importe quel URI.",
|
"LabelMobileRedirectURIsDescription": "Il s’agit d’une liste blanche d’URI de redirection valides pour les applications mobiles. Celui par défaut est <code>audiobookshelf://oauth</code>, que vous pouvez supprimer ou compléter avec des URIs supplémentaires pour l’intégration d’applications tierces. L’utilisation d’un astérisque (<code>*</code>) comme seule entrée autorise n’importe quel URI.",
|
||||||
"LabelMore": "Plus",
|
"LabelMore": "Plus",
|
||||||
"LabelMoreInfo": "Plus d’informations",
|
"LabelMoreInfo": "Plus d’informations",
|
||||||
"LabelName": "Nom",
|
"LabelName": "Nom",
|
||||||
|
|
@ -395,9 +396,9 @@
|
||||||
"LabelNotStarted": "Pas commencé",
|
"LabelNotStarted": "Pas commencé",
|
||||||
"LabelNumberOfBooks": "Nombre de livres",
|
"LabelNumberOfBooks": "Nombre de livres",
|
||||||
"LabelNumberOfEpisodes": "Nombre d’épisodes",
|
"LabelNumberOfEpisodes": "Nombre d’épisodes",
|
||||||
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
|
"LabelOpenIDAdvancedPermsClaimDescription": "Nom de la demande OpenID qui contient des autorisations avancées pour les actions de l’utilisateur dans l’application, qui s’appliqueront à des rôles autres que celui d’administrateur (<b>s’il est configuré</b>). Si la demande est absente de la réponse, l’accès à ABS sera refusé. Si une seule option est manquante, elle sera considérée comme <code>false</code>. Assurez-vous que la demande du fournisseur d’identité correspond à la structure attendue :",
|
||||||
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
|
"LabelOpenIDClaims": "Laissez les options suivantes vides pour désactiver l’attribution avancée de groupes et d’autorisations, en attribuant alors automatiquement le groupe « Utilisateur ».",
|
||||||
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
|
"LabelOpenIDGroupClaimDescription": "Nom de la demande OpenID qui contient une liste des groupes de l’utilisateur. Communément appelé <code>groups</code>. <b>Si elle est configurée</b>, l’application attribuera automatiquement des rôles en fonction de l’appartenance de l’utilisateur à un groupe, à condition que ces groupes soient nommés -sensible à la casse- tel que « admin », « user » ou « guest » dans la demande. Elle doit contenir une liste, et si un utilisateur appartient à plusieurs groupes, l’application attribuera le rôle correspondant au niveau d’accès le plus élevé. Si aucun groupe ne correspond, l’accès sera refusé.",
|
||||||
"LabelOpenRSSFeed": "Ouvrir le flux RSS",
|
"LabelOpenRSSFeed": "Ouvrir le flux RSS",
|
||||||
"LabelOverwrite": "Écraser",
|
"LabelOverwrite": "Écraser",
|
||||||
"LabelPassword": "Mot de passe",
|
"LabelPassword": "Mot de passe",
|
||||||
|
|
@ -447,7 +448,7 @@
|
||||||
"LabelSearchTitle": "Titre de recherche",
|
"LabelSearchTitle": "Titre de recherche",
|
||||||
"LabelSearchTitleOrASIN": "Recherche du titre ou ASIN",
|
"LabelSearchTitleOrASIN": "Recherche du titre ou ASIN",
|
||||||
"LabelSeason": "Saison",
|
"LabelSeason": "Saison",
|
||||||
"LabelSelectAll": "Select all",
|
"LabelSelectAll": "Tout sélectionner",
|
||||||
"LabelSelectAllEpisodes": "Sélectionner tous les épisodes",
|
"LabelSelectAllEpisodes": "Sélectionner tous les épisodes",
|
||||||
"LabelSelectEpisodesShowing": "Sélectionner {0} episode(s) en cours",
|
"LabelSelectEpisodesShowing": "Sélectionner {0} episode(s) en cours",
|
||||||
"LabelSelectUsers": "Sélectionner les utilisateurs",
|
"LabelSelectUsers": "Sélectionner les utilisateurs",
|
||||||
|
|
@ -460,7 +461,7 @@
|
||||||
"LabelSetEbookAsPrimary": "Définir comme principale",
|
"LabelSetEbookAsPrimary": "Définir comme principale",
|
||||||
"LabelSetEbookAsSupplementary": "Définir comme supplémentaire",
|
"LabelSetEbookAsSupplementary": "Définir comme supplémentaire",
|
||||||
"LabelSettingsAudiobooksOnly": "Livres audios seulement",
|
"LabelSettingsAudiobooksOnly": "Livres audios seulement",
|
||||||
"LabelSettingsAudiobooksOnlyHelp": "L'activation de ce paramètre ignorera les fichiers de type « livre numériques », sauf s'ils se trouvent dans un dossier spécifique , auquel cas ils seront définis comme des livres numériques supplémentaires.",
|
"LabelSettingsAudiobooksOnlyHelp": "L’activation de ce paramètre ignorera les fichiers de type « livre numériques », sauf s’ils se trouvent dans un dossier spécifique , auquel cas ils seront définis comme des livres numériques supplémentaires.",
|
||||||
"LabelSettingsBookshelfViewHelp": "Interface skeuomorphique avec une étagère en bois",
|
"LabelSettingsBookshelfViewHelp": "Interface skeuomorphique avec une étagère en bois",
|
||||||
"LabelSettingsChromecastSupport": "Support du Chromecast",
|
"LabelSettingsChromecastSupport": "Support du Chromecast",
|
||||||
"LabelSettingsDateFormat": "Format de date",
|
"LabelSettingsDateFormat": "Format de date",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "Activer la veille",
|
"LabelSettingsEnableWatcher": "Activer la veille",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Activer la surveillance des dossiers pour la bibliothèque",
|
"LabelSettingsEnableWatcherForLibrary": "Activer la surveillance des dossiers pour la bibliothèque",
|
||||||
"LabelSettingsEnableWatcherHelp": "Active la mise à jour automatique automatique lorsque des modifications de fichiers sont détectées. * nécessite le redémarrage du serveur",
|
"LabelSettingsEnableWatcherHelp": "Active la mise à jour automatique automatique lorsque des modifications de fichiers sont détectées. * nécessite le redémarrage du serveur",
|
||||||
|
"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.",
|
||||||
"LabelSettingsExperimentalFeatures": "Fonctionnalités expérimentales",
|
"LabelSettingsExperimentalFeatures": "Fonctionnalités expérimentales",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Fonctionnalités en cours de développement sur lesquelles nous attendons votre retour et expérience. Cliquez pour ouvrir la discussion GitHub.",
|
"LabelSettingsExperimentalFeaturesHelp": "Fonctionnalités en cours de développement sur lesquelles nous attendons votre retour et expérience. Cliquez pour ouvrir la discussion GitHub.",
|
||||||
"LabelSettingsFindCovers": "Chercher des couvertures de livre",
|
"LabelSettingsFindCovers": "Chercher des couvertures de livre",
|
||||||
|
|
@ -478,10 +481,10 @@
|
||||||
"LabelSettingsHideSingleBookSeriesHelp": "Les séries qui ne comportent qu’un seul livre seront masquées sur la page de la série et sur les étagères de la page d’accueil.",
|
"LabelSettingsHideSingleBookSeriesHelp": "Les séries qui ne comportent qu’un seul livre seront masquées sur la page de la série et sur les étagères de la page d’accueil.",
|
||||||
"LabelSettingsHomePageBookshelfView": "La page d’accueil utilise la vue étagère",
|
"LabelSettingsHomePageBookshelfView": "La page d’accueil utilise la vue étagère",
|
||||||
"LabelSettingsLibraryBookshelfView": "La bibliothèque utilise la vue étagère",
|
"LabelSettingsLibraryBookshelfView": "La bibliothèque utilise la vue étagère",
|
||||||
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
|
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Sauter les livres précédents dans « Continuer la série »",
|
||||||
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
|
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "L’étagère de la page d’accueil « Continuer la série » affiche le premier livre non commencé dans les séries dont au moins un livre est terminé et aucun livre n’est en cours. L’activation de ce paramètre permet de poursuivre la série à partir du dernier livre terminé au lieu du premier livre non commencé.",
|
||||||
"LabelSettingsParseSubtitles": "Analyser les sous-titres",
|
"LabelSettingsParseSubtitles": "Analyser les sous-titres",
|
||||||
"LabelSettingsParseSubtitlesHelp": "Extrait les sous-titres depuis le dossier du livre audio.<br>Les sous-titres doivent être séparés par « - »<br>c’est-à-dire : « Titre du livre - Ceci est un sous-titre » aura le sous-titre « Ceci est un sous-titre »",
|
"LabelSettingsParseSubtitlesHelp": "Extrait les sous-titres depuis le dossier du livre audio.<br>Les sous-titres doivent être séparés par des « - »<br>c’est-à-dire : « Titre du livre - Ceci est un sous-titre » aura le sous-titre « Ceci est un sous-titre »",
|
||||||
"LabelSettingsPreferMatchedMetadata": "Préférer les métadonnées par correspondance",
|
"LabelSettingsPreferMatchedMetadata": "Préférer les métadonnées par correspondance",
|
||||||
"LabelSettingsPreferMatchedMetadataHelp": "Les métadonnées par correspondance écrase les détails de l’article lors d’une recherche par correspondance rapide. Par défaut, la recherche par correspondance rapide ne comblera que les éléments manquant.",
|
"LabelSettingsPreferMatchedMetadataHelp": "Les métadonnées par correspondance écrase les détails de l’article lors d’une recherche par correspondance rapide. Par défaut, la recherche par correspondance rapide ne comblera que les éléments manquant.",
|
||||||
"LabelSettingsSkipMatchingBooksWithASIN": "Ignorer la recherche par correspondance sur les livres ayant déjà un ASIN",
|
"LabelSettingsSkipMatchingBooksWithASIN": "Ignorer la recherche par correspondance sur les livres ayant déjà un ASIN",
|
||||||
|
|
@ -496,7 +499,7 @@
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les métadonnées sont enregistrées dans /metadata/items",
|
"LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les métadonnées sont enregistrées dans /metadata/items",
|
||||||
"LabelSettingsTimeFormat": "Format d’heure",
|
"LabelSettingsTimeFormat": "Format d’heure",
|
||||||
"LabelShowAll": "Tout afficher",
|
"LabelShowAll": "Tout afficher",
|
||||||
"LabelShowSeconds": "Show seconds",
|
"LabelShowSeconds": "Afficher le seondes",
|
||||||
"LabelSize": "Taille",
|
"LabelSize": "Taille",
|
||||||
"LabelSleepTimer": "Minuterie",
|
"LabelSleepTimer": "Minuterie",
|
||||||
"LabelSlug": "Balise",
|
"LabelSlug": "Balise",
|
||||||
|
|
@ -517,7 +520,7 @@
|
||||||
"LabelStatsMinutes": "minutes",
|
"LabelStatsMinutes": "minutes",
|
||||||
"LabelStatsMinutesListening": "Minutes d’écoute",
|
"LabelStatsMinutesListening": "Minutes d’écoute",
|
||||||
"LabelStatsOverallDays": "Nombre total de jours",
|
"LabelStatsOverallDays": "Nombre total de jours",
|
||||||
"LabelStatsOverallHours": "Nombre total d'heures",
|
"LabelStatsOverallHours": "Nombre total d’heures",
|
||||||
"LabelStatsWeekListening": "Écoute de la semaine",
|
"LabelStatsWeekListening": "Écoute de la semaine",
|
||||||
"LabelSubtitle": "Sous-titre",
|
"LabelSubtitle": "Sous-titre",
|
||||||
"LabelSupportedFileTypes": "Types de fichiers supportés",
|
"LabelSupportedFileTypes": "Types de fichiers supportés",
|
||||||
|
|
@ -604,12 +607,12 @@
|
||||||
"MessageConfirmDeleteLibraryItems": "Cette opération supprimera {0} éléments de la base de données et de votre système de fichiers. Êtes-vous sûr ?",
|
"MessageConfirmDeleteLibraryItems": "Cette opération supprimera {0} éléments de la base de données et de votre système de fichiers. Êtes-vous sûr ?",
|
||||||
"MessageConfirmDeleteSession": "Êtes-vous sûr de vouloir supprimer cette session ?",
|
"MessageConfirmDeleteSession": "Êtes-vous sûr de vouloir supprimer cette session ?",
|
||||||
"MessageConfirmForceReScan": "Êtes-vous sûr de vouloir lancer une analyse forcée ?",
|
"MessageConfirmForceReScan": "Êtes-vous sûr de vouloir lancer une analyse forcée ?",
|
||||||
"MessageConfirmMarkAllEpisodesFinished": "Êtes-vous sûr de marquer tous les épisodes comme terminés ?",
|
"MessageConfirmMarkAllEpisodesFinished": "Êtes-vous sûr de marquer tous les épisodes comme terminés ?",
|
||||||
"MessageConfirmMarkAllEpisodesNotFinished": "Êtes-vous sûr de vouloir marquer tous les épisodes comme non terminés ?",
|
"MessageConfirmMarkAllEpisodesNotFinished": "Êtes-vous sûr de vouloir marquer tous les épisodes comme non terminés ?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Êtes-vous sûr de vouloir marquer tous les livres de cette série comme terminées ?",
|
"MessageConfirmMarkSeriesFinished": "Êtes-vous sûr de vouloir marquer tous les livres de cette série comme terminées ?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Êtes-vous sûr de vouloir marquer tous les livres de cette série comme comme non terminés ?",
|
"MessageConfirmMarkSeriesNotFinished": "Êtes-vous sûr de vouloir marquer tous les livres de cette série comme comme non terminés ?",
|
||||||
"MessageConfirmPurgeCache": "Purge cache will delete the entire directory at <code>/metadata/cache</code>. <br /><br />Are you sure you want to remove the cache directory?",
|
"MessageConfirmPurgeCache": "La purge du cache supprimera l’intégralité du répertoire à <code>/metadata/cache</code>.<br><br>Êtes-vous sûr de vouloir supprimer le répertoire de cache ?",
|
||||||
"MessageConfirmQuickEmbed": "Attention ! L’intégration rapide ne sauvegardera pas vos fichiers audio. Assurez-vous d’avoir effectuer une sauvegarde de vos fichiers audio.<br><br>Souhaitez-vous continuer ?",
|
"MessageConfirmQuickEmbed": "Attention ! L’intégration rapide ne sauvegardera pas vos fichiers audio. Assurez-vous d’avoir effectuer une sauvegarde de vos fichiers audio.<br><br>Souhaitez-vous continuer ?",
|
||||||
"MessageConfirmRemoveAllChapters": "Êtes-vous sûr de vouloir supprimer tous les chapitres ?",
|
"MessageConfirmRemoveAllChapters": "Êtes-vous sûr de vouloir supprimer tous les chapitres ?",
|
||||||
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
|
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
|
||||||
"MessageConfirmRemoveCollection": "Êtes-vous sûr de vouloir supprimer la collection « {0} » ?",
|
"MessageConfirmRemoveCollection": "Êtes-vous sûr de vouloir supprimer la collection « {0} » ?",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "Faites glisser les fichiers dans l’ordre correct des pistes",
|
"MessageDragFilesIntoTrackOrder": "Faites glisser les fichiers dans l’ordre correct des pistes",
|
||||||
"MessageEmbedFinished": "Intégration terminée !",
|
"MessageEmbedFinished": "Intégration terminée !",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} épisode(s) mis en file pour téléchargement",
|
"MessageEpisodesQueuedForDownload": "{0} épisode(s) mis en file pour téléchargement",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "L’URL du flux sera {0}",
|
"MessageFeedURLWillBe": "L’URL du flux sera {0}",
|
||||||
"MessageFetching": "Récupération…",
|
"MessageFetching": "Récupération…",
|
||||||
"MessageForceReScanDescription": "analysera de nouveau tous les fichiers. Les étiquettes ID3 des fichiers audio, les fichiers OPF et les fichiers texte seront analysés comme s’ils étaient nouveaux.",
|
"MessageForceReScanDescription": "analysera de nouveau tous les fichiers. Les étiquettes ID3 des fichiers audio, les fichiers OPF et les fichiers texte seront analysés comme s’ils étaient nouveaux.",
|
||||||
|
|
@ -641,7 +645,7 @@
|
||||||
"MessageListeningSessionsInTheLastYear": "{0} sessions d’écoute l’an dernier",
|
"MessageListeningSessionsInTheLastYear": "{0} sessions d’écoute l’an dernier",
|
||||||
"MessageLoading": "Chargement…",
|
"MessageLoading": "Chargement…",
|
||||||
"MessageLoadingFolders": "Chargement des dossiers…",
|
"MessageLoadingFolders": "Chargement des dossiers…",
|
||||||
"MessageLogsDescription": "Logs are stored in <code>/metadata/logs</code> as JSON files. Crash logs are stored in <code>/metadata/logs/crash_logs.txt</code>.",
|
"MessageLogsDescription": "Les journaux sont stockés dans <code>/metadata/logs</code> sous forme de fichiers JSON. Les journaux d’incidents sont stockés dans <code>/metadata/logs/crash_logs.txt</code>.",
|
||||||
"MessageM4BFailed": "M4B échec",
|
"MessageM4BFailed": "M4B échec",
|
||||||
"MessageM4BFinished": "M4B terminé",
|
"MessageM4BFinished": "M4B terminé",
|
||||||
"MessageMapChapterTitles": "Faire correspondre les titres des chapitres aux chapitres existants de votre livre audio sans ajuster l’horodatage.",
|
"MessageMapChapterTitles": "Faire correspondre les titres des chapitres aux chapitres existants de votre livre audio sans ajuster l’horodatage.",
|
||||||
|
|
@ -745,8 +749,8 @@
|
||||||
"ToastBookmarkRemoveSuccess": "Signet supprimé",
|
"ToastBookmarkRemoveSuccess": "Signet supprimé",
|
||||||
"ToastBookmarkUpdateFailed": "Échec de la mise à jour de signet",
|
"ToastBookmarkUpdateFailed": "Échec de la mise à jour de signet",
|
||||||
"ToastBookmarkUpdateSuccess": "Signet mis à jour",
|
"ToastBookmarkUpdateSuccess": "Signet mis à jour",
|
||||||
"ToastCachePurgeFailed": "Failed to purge cache",
|
"ToastCachePurgeFailed": "Échec de la purge du cache",
|
||||||
"ToastCachePurgeSuccess": "Cache purged successfully",
|
"ToastCachePurgeSuccess": "Cache purgé avec succès",
|
||||||
"ToastChaptersHaveErrors": "Les chapitres contiennent des erreurs",
|
"ToastChaptersHaveErrors": "Les chapitres contiennent des erreurs",
|
||||||
"ToastChaptersMustHaveTitles": "Les chapitre doivent avoir un titre",
|
"ToastChaptersMustHaveTitles": "Les chapitre doivent avoir un titre",
|
||||||
"ToastCollectionItemsRemoveFailed": "Échec de la suppression de(s) article(s) de la collection",
|
"ToastCollectionItemsRemoveFailed": "Échec de la suppression de(s) article(s) de la collection",
|
||||||
|
|
@ -755,9 +759,9 @@
|
||||||
"ToastCollectionRemoveSuccess": "Collection supprimée",
|
"ToastCollectionRemoveSuccess": "Collection supprimée",
|
||||||
"ToastCollectionUpdateFailed": "Échec de la mise à jour de la collection",
|
"ToastCollectionUpdateFailed": "Échec de la mise à jour de la collection",
|
||||||
"ToastCollectionUpdateSuccess": "Collection mise à jour",
|
"ToastCollectionUpdateSuccess": "Collection mise à jour",
|
||||||
"ToastDeleteFileFailed": "Failed to delete file",
|
"ToastDeleteFileFailed": "Échec de la suppression du fichier",
|
||||||
"ToastDeleteFileSuccess": "File deleted",
|
"ToastDeleteFileSuccess": "Fichier supprimé",
|
||||||
"ToastFailedToLoadData": "Failed to load data",
|
"ToastFailedToLoadData": "Échec du chargement des données",
|
||||||
"ToastItemCoverUpdateFailed": "Échec de la mise à jour de la couverture de l’article",
|
"ToastItemCoverUpdateFailed": "Échec de la mise à jour de la couverture de l’article",
|
||||||
"ToastItemCoverUpdateSuccess": "Couverture de l’article mise à jour",
|
"ToastItemCoverUpdateSuccess": "Couverture de l’article mise à jour",
|
||||||
"ToastItemDetailsUpdateFailed": "Échec de la mise à jour des détails de l’article",
|
"ToastItemDetailsUpdateFailed": "Échec de la mise à jour des détails de l’article",
|
||||||
|
|
@ -791,16 +795,16 @@
|
||||||
"ToastSendEbookToDeviceSuccess": "Livre numérique envoyé à l’appareil : {0}",
|
"ToastSendEbookToDeviceSuccess": "Livre numérique envoyé à l’appareil : {0}",
|
||||||
"ToastSeriesUpdateFailed": "Échec de la mise à jour de la série",
|
"ToastSeriesUpdateFailed": "Échec de la mise à jour de la série",
|
||||||
"ToastSeriesUpdateSuccess": "Mise à jour de la série réussie",
|
"ToastSeriesUpdateSuccess": "Mise à jour de la série réussie",
|
||||||
"ToastServerSettingsUpdateFailed": "Failed to update server settings",
|
"ToastServerSettingsUpdateFailed": "Échec de la mise à jour des paramètres du serveur",
|
||||||
"ToastServerSettingsUpdateSuccess": "Server settings updated",
|
"ToastServerSettingsUpdateSuccess": "Mise à jour des paramètres du serveur",
|
||||||
"ToastSessionDeleteFailed": "Échec de la suppression de session",
|
"ToastSessionDeleteFailed": "Échec de la suppression de session",
|
||||||
"ToastSessionDeleteSuccess": "Session supprimée",
|
"ToastSessionDeleteSuccess": "Session supprimée",
|
||||||
"ToastSocketConnected": "WebSocket connecté",
|
"ToastSocketConnected": "WebSocket connecté",
|
||||||
"ToastSocketDisconnected": "WebSocket déconnecté",
|
"ToastSocketDisconnected": "WebSocket déconnecté",
|
||||||
"ToastSocketFailedToConnect": "Échec de la connexion WebSocket",
|
"ToastSocketFailedToConnect": "Échec de la connexion WebSocket",
|
||||||
"ToastSortingPrefixesEmptyError": "Must have at least 1 sorting prefix",
|
"ToastSortingPrefixesEmptyError": "Doit avoir au moins 1 préfixe de tri",
|
||||||
"ToastSortingPrefixesUpdateFailed": "Failed to update sorting prefixes",
|
"ToastSortingPrefixesUpdateFailed": "Échec de la mise à jour des préfixes de tri",
|
||||||
"ToastSortingPrefixesUpdateSuccess": "Sorting prefixes updated ({0} items)",
|
"ToastSortingPrefixesUpdateSuccess": "Mise à jour des préfixes de tri ({0} élément)",
|
||||||
"ToastUserDeleteFailed": "Échec de la suppression de l’utilisateur",
|
"ToastUserDeleteFailed": "Échec de la suppression de l’utilisateur",
|
||||||
"ToastUserDeleteSuccess": "Utilisateur supprimé"
|
"ToastUserDeleteSuccess": "Utilisateur supprimé"
|
||||||
}
|
}
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "Abridged",
|
"LabelAbridged": "Abridged",
|
||||||
"LabelAbridgedChecked": "Abridged (checked)",
|
"LabelAbridgedChecked": "Abridged (checked)",
|
||||||
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "Account Type",
|
"LabelAccountType": "Account Type",
|
||||||
"LabelAccountTypeAdmin": "Admin",
|
"LabelAccountTypeAdmin": "Admin",
|
||||||
"LabelAccountTypeGuest": "Guest",
|
"LabelAccountTypeGuest": "Guest",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "Enable Watcher",
|
"LabelSettingsEnableWatcher": "Enable Watcher",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
|
"LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
|
||||||
"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",
|
||||||
|
"LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
|
||||||
"LabelSettingsExperimentalFeatures": "Experimental features",
|
"LabelSettingsExperimentalFeatures": "Experimental features",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Features in development that could use your feedback and help testing. Click to open github discussion.",
|
"LabelSettingsExperimentalFeaturesHelp": "Features in development that could use your feedback and help testing. Click to open github discussion.",
|
||||||
"LabelSettingsFindCovers": "Find covers",
|
"LabelSettingsFindCovers": "Find covers",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
|
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
|
||||||
"MessageEmbedFinished": "Embed Finished!",
|
"MessageEmbedFinished": "Embed Finished!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} Episode(s) queued for download",
|
"MessageEpisodesQueuedForDownload": "{0} Episode(s) queued for download",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "Feed URL will be {0}",
|
"MessageFeedURLWillBe": "Feed URL will be {0}",
|
||||||
"MessageFetching": "Fetching...",
|
"MessageFetching": "Fetching...",
|
||||||
"MessageForceReScanDescription": "will scan all files again like a fresh scan. Audio file ID3 tags, OPF files, and text files will be scanned as new.",
|
"MessageForceReScanDescription": "will scan all files again like a fresh scan. Audio file ID3 tags, OPF files, and text files will be scanned as new.",
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "מקוצר",
|
"LabelAbridged": "מקוצר",
|
||||||
"LabelAbridgedChecked": "Abridged (checked)",
|
"LabelAbridgedChecked": "Abridged (checked)",
|
||||||
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "סוג חשבון",
|
"LabelAccountType": "סוג חשבון",
|
||||||
"LabelAccountTypeAdmin": "מנהל",
|
"LabelAccountTypeAdmin": "מנהל",
|
||||||
"LabelAccountTypeGuest": "אורח",
|
"LabelAccountTypeGuest": "אורח",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "הפעל עוקב",
|
"LabelSettingsEnableWatcher": "הפעל עוקב",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "הפעל עוקב תיקייה עבור ספרייה",
|
"LabelSettingsEnableWatcherForLibrary": "הפעל עוקב תיקייה עבור ספרייה",
|
||||||
"LabelSettingsEnableWatcherHelp": "מאפשר הוספת/עדכון אוטומטי של פריטים כאשר שינויי קבצים זוהים. *דורש איתחול שרת",
|
"LabelSettingsEnableWatcherHelp": "מאפשר הוספת/עדכון אוטומטי של פריטים כאשר שינויי קבצים זוהים. *דורש איתחול שרת",
|
||||||
|
"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.",
|
||||||
"LabelSettingsExperimentalFeatures": "תכונות ניסיוניות",
|
"LabelSettingsExperimentalFeatures": "תכונות ניסיוניות",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "תכונות בפיתוח שדורשות משובך ובדיקה. לחץ לפתיחת דיון ב-GitHub.",
|
"LabelSettingsExperimentalFeaturesHelp": "תכונות בפיתוח שדורשות משובך ובדיקה. לחץ לפתיחת דיון ב-GitHub.",
|
||||||
"LabelSettingsFindCovers": "מצא כריכות",
|
"LabelSettingsFindCovers": "מצא כריכות",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "גרור קבצים לסדר ההשמעה נכון",
|
"MessageDragFilesIntoTrackOrder": "גרור קבצים לסדר ההשמעה נכון",
|
||||||
"MessageEmbedFinished": "ההטמעה הושלמה!",
|
"MessageEmbedFinished": "ההטמעה הושלמה!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} פרקים בתור להורדה",
|
"MessageEpisodesQueuedForDownload": "{0} פרקים בתור להורדה",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "כתובת URL של העדכון תהיה {0}",
|
"MessageFeedURLWillBe": "כתובת URL של העדכון תהיה {0}",
|
||||||
"MessageFetching": "מושך...",
|
"MessageFetching": "מושך...",
|
||||||
"MessageForceReScanDescription": "תבוצע סריקה מחדש כמו סריקה חדש מאפס, תגי ID3 של קבצי קול, קבצי OPF, וקבצי טקסט ייסרקו כחדשים.",
|
"MessageForceReScanDescription": "תבוצע סריקה מחדש כמו סריקה חדש מאפס, תגי ID3 של קבצי קול, קבצי OPF, וקבצי טקסט ייסרקו כחדשים.",
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "Abridged",
|
"LabelAbridged": "Abridged",
|
||||||
"LabelAbridgedChecked": "Abridged (checked)",
|
"LabelAbridgedChecked": "Abridged (checked)",
|
||||||
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "Account Type",
|
"LabelAccountType": "Account Type",
|
||||||
"LabelAccountTypeAdmin": "Admin",
|
"LabelAccountTypeAdmin": "Admin",
|
||||||
"LabelAccountTypeGuest": "Guest",
|
"LabelAccountTypeGuest": "Guest",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "Enable Watcher",
|
"LabelSettingsEnableWatcher": "Enable Watcher",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
|
"LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
|
||||||
"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",
|
||||||
|
"LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
|
||||||
"LabelSettingsExperimentalFeatures": "Experimental features",
|
"LabelSettingsExperimentalFeatures": "Experimental features",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Features in development that could use your feedback and help testing. Click to open github discussion.",
|
"LabelSettingsExperimentalFeaturesHelp": "Features in development that could use your feedback and help testing. Click to open github discussion.",
|
||||||
"LabelSettingsFindCovers": "Find covers",
|
"LabelSettingsFindCovers": "Find covers",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
|
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
|
||||||
"MessageEmbedFinished": "Embed Finished!",
|
"MessageEmbedFinished": "Embed Finished!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} Episode(s) queued for download",
|
"MessageEpisodesQueuedForDownload": "{0} Episode(s) queued for download",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "Feed URL will be {0}",
|
"MessageFeedURLWillBe": "Feed URL will be {0}",
|
||||||
"MessageFetching": "Fetching...",
|
"MessageFetching": "Fetching...",
|
||||||
"MessageForceReScanDescription": "will scan all files again like a fresh scan. Audio file ID3 tags, OPF files, and text files will be scanned as new.",
|
"MessageForceReScanDescription": "will scan all files again like a fresh scan. Audio file ID3 tags, OPF files, and text files will be scanned as new.",
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "Abridged",
|
"LabelAbridged": "Abridged",
|
||||||
"LabelAbridgedChecked": "Abridged (checked)",
|
"LabelAbridgedChecked": "Abridged (checked)",
|
||||||
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "Vrsta korisničkog računa",
|
"LabelAccountType": "Vrsta korisničkog računa",
|
||||||
"LabelAccountTypeAdmin": "Administrator",
|
"LabelAccountTypeAdmin": "Administrator",
|
||||||
"LabelAccountTypeGuest": "Gost",
|
"LabelAccountTypeGuest": "Gost",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "Enable Watcher",
|
"LabelSettingsEnableWatcher": "Enable Watcher",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
|
"LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
|
||||||
"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",
|
||||||
|
"LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
|
||||||
"LabelSettingsExperimentalFeatures": "Eksperimentalni features",
|
"LabelSettingsExperimentalFeatures": "Eksperimentalni features",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Features u razvoju trebaju vaš feedback i pomoć pri testiranju. Klikni da odeš to Github discussionsa.",
|
"LabelSettingsExperimentalFeaturesHelp": "Features u razvoju trebaju vaš feedback i pomoć pri testiranju. Klikni da odeš to Github discussionsa.",
|
||||||
"LabelSettingsFindCovers": "Pronađi covers",
|
"LabelSettingsFindCovers": "Pronađi covers",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "Povuci datoteke u pravilan redoslijed tracka.",
|
"MessageDragFilesIntoTrackOrder": "Povuci datoteke u pravilan redoslijed tracka.",
|
||||||
"MessageEmbedFinished": "Embed završen!",
|
"MessageEmbedFinished": "Embed završen!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} Epizoda/-e u redu za preuzimanje",
|
"MessageEpisodesQueuedForDownload": "{0} Epizoda/-e u redu za preuzimanje",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "Feed URL će biti {0}",
|
"MessageFeedURLWillBe": "Feed URL će biti {0}",
|
||||||
"MessageFetching": "Dobavljam...",
|
"MessageFetching": "Dobavljam...",
|
||||||
"MessageForceReScanDescription": "će skenirati sve datoteke ponovno kao svježi sken. ID3 tagovi od audio datoteka, OPF datoteke i tekst datoteke će biti skenirane kao da su nove.",
|
"MessageForceReScanDescription": "će skenirati sve datoteke ponovno kao svježi sken. ID3 tagovi od audio datoteka, OPF datoteke i tekst datoteke će biti skenirane kao da su nove.",
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "Tömörített",
|
"LabelAbridged": "Tömörített",
|
||||||
"LabelAbridgedChecked": "Abridged (checked)",
|
"LabelAbridgedChecked": "Abridged (checked)",
|
||||||
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "Fióktípus",
|
"LabelAccountType": "Fióktípus",
|
||||||
"LabelAccountTypeAdmin": "Admin",
|
"LabelAccountTypeAdmin": "Admin",
|
||||||
"LabelAccountTypeGuest": "Vendég",
|
"LabelAccountTypeGuest": "Vendég",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "Figyelő engedélyezése",
|
"LabelSettingsEnableWatcher": "Figyelő engedélyezése",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Mappafigyelő engedélyezése a könyvtárban",
|
"LabelSettingsEnableWatcherForLibrary": "Mappafigyelő engedélyezése a könyvtárban",
|
||||||
"LabelSettingsEnableWatcherHelp": "Engedélyezi az automatikus elem hozzáadás/frissítés funkciót, amikor fájlváltozásokat észlel. *Szerver újraindítása szükséges",
|
"LabelSettingsEnableWatcherHelp": "Engedélyezi az automatikus elem hozzáadás/frissítés funkciót, amikor fájlváltozásokat észlel. *Szerver újraindítása szükséges",
|
||||||
|
"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.",
|
||||||
"LabelSettingsExperimentalFeatures": "Kísérleti funkciók",
|
"LabelSettingsExperimentalFeatures": "Kísérleti funkciók",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Fejlesztés alatt álló funkciók, amelyek visszajelzésre és tesztelésre szorulnak. Kattintson a github megbeszélés megnyitásához.",
|
"LabelSettingsExperimentalFeaturesHelp": "Fejlesztés alatt álló funkciók, amelyek visszajelzésre és tesztelésre szorulnak. Kattintson a github megbeszélés megnyitásához.",
|
||||||
"LabelSettingsFindCovers": "Borítók keresése",
|
"LabelSettingsFindCovers": "Borítók keresése",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "Húzza a fájlokat a helyes sávrendbe",
|
"MessageDragFilesIntoTrackOrder": "Húzza a fájlokat a helyes sávrendbe",
|
||||||
"MessageEmbedFinished": "Beágyazás befejeződött!",
|
"MessageEmbedFinished": "Beágyazás befejeződött!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} Epizód letöltésre várakozik",
|
"MessageEpisodesQueuedForDownload": "{0} Epizód letöltésre várakozik",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "A hírcsatorna URL-je {0} lesz",
|
"MessageFeedURLWillBe": "A hírcsatorna URL-je {0} lesz",
|
||||||
"MessageFetching": "Lekérés...",
|
"MessageFetching": "Lekérés...",
|
||||||
"MessageForceReScanDescription": "minden fájlt újra szkennel, mint egy friss szkennelés. Az audiofájlok ID3 címkéi, OPF fájlok és szövegfájlok újként lesznek szkennelve.",
|
"MessageForceReScanDescription": "minden fájlt újra szkennel, mint egy friss szkennelés. Az audiofájlok ID3 címkéi, OPF fájlok és szövegfájlok újként lesznek szkennelve.",
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "Abbreviato",
|
"LabelAbridged": "Abbreviato",
|
||||||
"LabelAbridgedChecked": "Abridged (checked)",
|
"LabelAbridgedChecked": "Abridged (checked)",
|
||||||
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "Tipo di Account",
|
"LabelAccountType": "Tipo di Account",
|
||||||
"LabelAccountTypeAdmin": "Admin",
|
"LabelAccountTypeAdmin": "Admin",
|
||||||
"LabelAccountTypeGuest": "Ospite",
|
"LabelAccountTypeGuest": "Ospite",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "Abilita Watcher",
|
"LabelSettingsEnableWatcher": "Abilita Watcher",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Abilita il controllo cartelle per la libreria",
|
"LabelSettingsEnableWatcherForLibrary": "Abilita il controllo cartelle per la libreria",
|
||||||
"LabelSettingsEnableWatcherHelp": "Abilita l'aggiunta/aggiornamento automatico degli elementi quando vengono rilevate modifiche ai file. *Richiede il riavvio del Server",
|
"LabelSettingsEnableWatcherHelp": "Abilita l'aggiunta/aggiornamento automatico degli elementi quando vengono rilevate modifiche ai file. *Richiede il riavvio del Server",
|
||||||
|
"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.",
|
||||||
"LabelSettingsExperimentalFeatures": "Opzioni Sperimentali",
|
"LabelSettingsExperimentalFeatures": "Opzioni Sperimentali",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Funzionalità in fase di sviluppo che potrebbero utilizzare i tuoi feedback e aiutare i test. Fare clic per aprire la discussione github.",
|
"LabelSettingsExperimentalFeaturesHelp": "Funzionalità in fase di sviluppo che potrebbero utilizzare i tuoi feedback e aiutare i test. Fare clic per aprire la discussione github.",
|
||||||
"LabelSettingsFindCovers": "Trova covers",
|
"LabelSettingsFindCovers": "Trova covers",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
|
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
|
||||||
"MessageEmbedFinished": "Incorporamento finito!",
|
"MessageEmbedFinished": "Incorporamento finito!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} Episodio(i) in coda per il Download",
|
"MessageEpisodesQueuedForDownload": "{0} Episodio(i) in coda per il Download",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "Feed URL Saranno {0}",
|
"MessageFeedURLWillBe": "Feed URL Saranno {0}",
|
||||||
"MessageFetching": "Recupero Info...",
|
"MessageFetching": "Recupero Info...",
|
||||||
"MessageForceReScanDescription": "eseguirà nuovamente la scansione di tutti i file come una nuova scansione. I tag ID3 dei file audio, i file OPF e i file di testo verranno scansionati come nuovi.",
|
"MessageForceReScanDescription": "eseguirà nuovamente la scansione di tutti i file come una nuova scansione. I tag ID3 dei file audio, i file OPF e i file di testo verranno scansionati come nuovi.",
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "Santrauka",
|
"LabelAbridged": "Santrauka",
|
||||||
"LabelAbridgedChecked": "Abridged (checked)",
|
"LabelAbridgedChecked": "Abridged (checked)",
|
||||||
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "Paskyros tipas",
|
"LabelAccountType": "Paskyros tipas",
|
||||||
"LabelAccountTypeAdmin": "Administratorius",
|
"LabelAccountTypeAdmin": "Administratorius",
|
||||||
"LabelAccountTypeGuest": "Svečias",
|
"LabelAccountTypeGuest": "Svečias",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "Enable Watcher",
|
"LabelSettingsEnableWatcher": "Enable Watcher",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
|
"LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
|
||||||
"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",
|
||||||
|
"LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
|
||||||
"LabelSettingsExperimentalFeatures": "Eksperimentiniai funkcionalumai",
|
"LabelSettingsExperimentalFeatures": "Eksperimentiniai funkcionalumai",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Funkcijos, kurios yra kuriamos ir laukiami jūsų komentarai. Spustelėkite, kad atidarytumėte „GitHub“ diskusiją.",
|
"LabelSettingsExperimentalFeaturesHelp": "Funkcijos, kurios yra kuriamos ir laukiami jūsų komentarai. Spustelėkite, kad atidarytumėte „GitHub“ diskusiją.",
|
||||||
"LabelSettingsFindCovers": "Rasti viršelius",
|
"LabelSettingsFindCovers": "Rasti viršelius",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "Surikiuokite takelius vilkdami failus",
|
"MessageDragFilesIntoTrackOrder": "Surikiuokite takelius vilkdami failus",
|
||||||
"MessageEmbedFinished": "Įterpimas baigtas!",
|
"MessageEmbedFinished": "Įterpimas baigtas!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} epizodai laukia atsisiuntimo",
|
"MessageEpisodesQueuedForDownload": "{0} epizodai laukia atsisiuntimo",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "Srauto URL bus {0}",
|
"MessageFeedURLWillBe": "Srauto URL bus {0}",
|
||||||
"MessageFetching": "Surenkama...",
|
"MessageFetching": "Surenkama...",
|
||||||
"MessageForceReScanDescription": "skenuos visus failus lyg iš naujo. Garsinių failų ID3 žymos, OPF failai ir tekstiniai failai bus nuskenuoti kaip nauji.",
|
"MessageForceReScanDescription": "skenuos visus failus lyg iš naujo. Garsinių failų ID3 žymos, OPF failai ir tekstiniai failai bus nuskenuoti kaip nauji.",
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "Verkort",
|
"LabelAbridged": "Verkort",
|
||||||
"LabelAbridgedChecked": "Abridged (checked)",
|
"LabelAbridgedChecked": "Abridged (checked)",
|
||||||
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "Accounttype",
|
"LabelAccountType": "Accounttype",
|
||||||
"LabelAccountTypeAdmin": "Beheerder",
|
"LabelAccountTypeAdmin": "Beheerder",
|
||||||
"LabelAccountTypeGuest": "Gast",
|
"LabelAccountTypeGuest": "Gast",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "Watcher inschakelen",
|
"LabelSettingsEnableWatcher": "Watcher inschakelen",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Map-watcher voor bibliotheek inschakelen",
|
"LabelSettingsEnableWatcherForLibrary": "Map-watcher voor bibliotheek inschakelen",
|
||||||
"LabelSettingsEnableWatcherHelp": "Zorgt voor het automatisch toevoegen/bijwerken van onderdelen als bestandswijzigingen worden gedetecteerd. *Vereist herstarten van server",
|
"LabelSettingsEnableWatcherHelp": "Zorgt voor het automatisch toevoegen/bijwerken van onderdelen als bestandswijzigingen worden gedetecteerd. *Vereist herstarten van server",
|
||||||
|
"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.",
|
||||||
"LabelSettingsExperimentalFeatures": "Experimentele functies",
|
"LabelSettingsExperimentalFeatures": "Experimentele functies",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Functies in ontwikkeling die je feedback en testing kunnen gebruiken. Klik om de Github-discussie te openen.",
|
"LabelSettingsExperimentalFeaturesHelp": "Functies in ontwikkeling die je feedback en testing kunnen gebruiken. Klik om de Github-discussie te openen.",
|
||||||
"LabelSettingsFindCovers": "Zoek covers",
|
"LabelSettingsFindCovers": "Zoek covers",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "Sleep bestanden in de juiste trackvolgorde",
|
"MessageDragFilesIntoTrackOrder": "Sleep bestanden in de juiste trackvolgorde",
|
||||||
"MessageEmbedFinished": "Insluiting voltooid!",
|
"MessageEmbedFinished": "Insluiting voltooid!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} aflevering(en) in de rij om te downloaden",
|
"MessageEpisodesQueuedForDownload": "{0} aflevering(en) in de rij om te downloaden",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "Feed URL zal {0} zijn",
|
"MessageFeedURLWillBe": "Feed URL zal {0} zijn",
|
||||||
"MessageFetching": "Aan het ophalen...",
|
"MessageFetching": "Aan het ophalen...",
|
||||||
"MessageForceReScanDescription": "zal alle bestanden opnieuw scannen als een verse scan. Audiobestanden ID3-tags, OPF-bestanden en textbestanden zullen als nieuw worden gescand.",
|
"MessageForceReScanDescription": "zal alle bestanden opnieuw scannen als een verse scan. Audiobestanden ID3-tags, OPF-bestanden en textbestanden zullen als nieuw worden gescand.",
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "Forkortet",
|
"LabelAbridged": "Forkortet",
|
||||||
"LabelAbridgedChecked": "Abridged (checked)",
|
"LabelAbridgedChecked": "Abridged (checked)",
|
||||||
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "Kontotype",
|
"LabelAccountType": "Kontotype",
|
||||||
"LabelAccountTypeAdmin": "Admin",
|
"LabelAccountTypeAdmin": "Admin",
|
||||||
"LabelAccountTypeGuest": "Gjest",
|
"LabelAccountTypeGuest": "Gjest",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "Aktiver overvåker",
|
"LabelSettingsEnableWatcher": "Aktiver overvåker",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Aktiver mappe overvåker for bibliotek",
|
"LabelSettingsEnableWatcherForLibrary": "Aktiver mappe overvåker for bibliotek",
|
||||||
"LabelSettingsEnableWatcherHelp": "Aktiverer automatisk opprettelse/oppdatering av enheter når filendringer er oppdaget. *Krever restart av server*",
|
"LabelSettingsEnableWatcherHelp": "Aktiverer automatisk opprettelse/oppdatering av enheter når filendringer er oppdaget. *Krever restart av server*",
|
||||||
|
"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.",
|
||||||
"LabelSettingsExperimentalFeatures": "Eksperimentelle funksjoner",
|
"LabelSettingsExperimentalFeatures": "Eksperimentelle funksjoner",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Funksjoner under utvikling som kan trenge din tilbakemelding og hjelp med testing. Klikk for å åpne GitHub diskusjon.",
|
"LabelSettingsExperimentalFeaturesHelp": "Funksjoner under utvikling som kan trenge din tilbakemelding og hjelp med testing. Klikk for å åpne GitHub diskusjon.",
|
||||||
"LabelSettingsFindCovers": "Finn omslag",
|
"LabelSettingsFindCovers": "Finn omslag",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "Dra filene i rett spor rekkefølge",
|
"MessageDragFilesIntoTrackOrder": "Dra filene i rett spor rekkefølge",
|
||||||
"MessageEmbedFinished": "Bak inn Fullført!",
|
"MessageEmbedFinished": "Bak inn Fullført!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} Episode(r) lagt til i kø for nedlasting",
|
"MessageEpisodesQueuedForDownload": "{0} Episode(r) lagt til i kø for nedlasting",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "Feed URL vil bli {0}",
|
"MessageFeedURLWillBe": "Feed URL vil bli {0}",
|
||||||
"MessageFetching": "Henter...",
|
"MessageFetching": "Henter...",
|
||||||
"MessageForceReScanDescription": "vil skanne alle filene igjen som en ny skann. Lyd fil ID3 tagger, OPF filer og tekstfiler vil bli skannet som nye.",
|
"MessageForceReScanDescription": "vil skanne alle filene igjen som en ny skann. Lyd fil ID3 tagger, OPF filer og tekstfiler vil bli skannet som nye.",
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "Abridged",
|
"LabelAbridged": "Abridged",
|
||||||
"LabelAbridgedChecked": "Abridged (checked)",
|
"LabelAbridgedChecked": "Abridged (checked)",
|
||||||
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "Typ konta",
|
"LabelAccountType": "Typ konta",
|
||||||
"LabelAccountTypeAdmin": "Administrator",
|
"LabelAccountTypeAdmin": "Administrator",
|
||||||
"LabelAccountTypeGuest": "Gość",
|
"LabelAccountTypeGuest": "Gość",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "Enable Watcher",
|
"LabelSettingsEnableWatcher": "Enable Watcher",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
|
"LabelSettingsEnableWatcherForLibrary": "Enable folder watcher for library",
|
||||||
"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",
|
||||||
|
"LabelSettingsEpubsAllowScriptedContentHelp": "Allow epub files to execute scripts. It is recommended to keep this setting disabled unless you trust the source of the epub files.",
|
||||||
"LabelSettingsExperimentalFeatures": "Funkcje eksperymentalne",
|
"LabelSettingsExperimentalFeatures": "Funkcje eksperymentalne",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Funkcje w trakcie rozwoju, które mogą zyskanć na Twojej opinii i pomocy w testowaniu. Kliknij, aby otworzyć dyskusję na githubie.",
|
"LabelSettingsExperimentalFeaturesHelp": "Funkcje w trakcie rozwoju, które mogą zyskanć na Twojej opinii i pomocy w testowaniu. Kliknij, aby otworzyć dyskusję na githubie.",
|
||||||
"LabelSettingsFindCovers": "Szukanie okładek",
|
"LabelSettingsFindCovers": "Szukanie okładek",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "przeciągnij pliki aby ustawić właściwą kolejność utworów",
|
"MessageDragFilesIntoTrackOrder": "przeciągnij pliki aby ustawić właściwą kolejność utworów",
|
||||||
"MessageEmbedFinished": "Osadzanie zakończone!",
|
"MessageEmbedFinished": "Osadzanie zakończone!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} odcinki w kolejce do pobrania",
|
"MessageEpisodesQueuedForDownload": "{0} odcinki w kolejce do pobrania",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "URL kanału: {0}",
|
"MessageFeedURLWillBe": "URL kanału: {0}",
|
||||||
"MessageFetching": "Pobieranie...",
|
"MessageFetching": "Pobieranie...",
|
||||||
"MessageForceReScanDescription": "przeskanuje wszystkie pliki ponownie, jak przy świeżym skanowaniu. Tagi ID3 plików audio, pliki OPF i pliki tekstowe będą skanowane jak nowe.",
|
"MessageForceReScanDescription": "przeskanuje wszystkie pliki ponownie, jak przy świeżym skanowaniu. Tagi ID3 plików audio, pliki OPF i pliki tekstowe będą skanowane jak nowe.",
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "Versão Abreviada",
|
"LabelAbridged": "Versão Abreviada",
|
||||||
"LabelAbridgedChecked": "Abreviada (verificada)",
|
"LabelAbridgedChecked": "Abreviada (verificada)",
|
||||||
"LabelAbridgedUnchecked": "Não Abreviada (não verificada)",
|
"LabelAbridgedUnchecked": "Não Abreviada (não verificada)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "Tipo de Conta",
|
"LabelAccountType": "Tipo de Conta",
|
||||||
"LabelAccountTypeAdmin": "Administrador",
|
"LabelAccountTypeAdmin": "Administrador",
|
||||||
"LabelAccountTypeGuest": "Convidado",
|
"LabelAccountTypeGuest": "Convidado",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "Ativar Monitoramento",
|
"LabelSettingsEnableWatcher": "Ativar Monitoramento",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Ativa o monitoramento de pastas para a biblioteca",
|
"LabelSettingsEnableWatcherForLibrary": "Ativa o monitoramento de pastas para a 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": "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.",
|
||||||
"LabelSettingsExperimentalFeatures": "Funcionalidade experimentais",
|
"LabelSettingsExperimentalFeatures": "Funcionalidade experimentais",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Funcionalidade em desenvolvimento que se beneficiairam dos seus comentários e da sua ajuda para testar. Clique para abrir a discussão no github.",
|
"LabelSettingsExperimentalFeaturesHelp": "Funcionalidade em desenvolvimento que se beneficiairam dos seus comentários e da sua ajuda para testar. Clique para abrir a discussão no github.",
|
||||||
"LabelSettingsFindCovers": "Localizar capas",
|
"LabelSettingsFindCovers": "Localizar capas",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "Arraste os arquivos para ordenar as trilhas corretamente",
|
"MessageDragFilesIntoTrackOrder": "Arraste os arquivos para ordenar as trilhas corretamente",
|
||||||
"MessageEmbedFinished": "Inclusão Concluída!",
|
"MessageEmbedFinished": "Inclusão Concluída!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} Episódio(s) na fila de download",
|
"MessageEpisodesQueuedForDownload": "{0} Episódio(s) na fila de download",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "URL do Feed será {0}",
|
"MessageFeedURLWillBe": "URL do Feed será {0}",
|
||||||
"MessageFetching": "Buscando...",
|
"MessageFetching": "Buscando...",
|
||||||
"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.",
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "Сокращенное издание",
|
"LabelAbridged": "Сокращенное издание",
|
||||||
"LabelAbridgedChecked": "Abridged (checked)",
|
"LabelAbridgedChecked": "Abridged (checked)",
|
||||||
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "Тип учетной записи",
|
"LabelAccountType": "Тип учетной записи",
|
||||||
"LabelAccountTypeAdmin": "Администратор",
|
"LabelAccountTypeAdmin": "Администратор",
|
||||||
"LabelAccountTypeGuest": "Гость",
|
"LabelAccountTypeGuest": "Гость",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "Включить отслеживание",
|
"LabelSettingsEnableWatcher": "Включить отслеживание",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Включить отслеживание за папками библиотеки",
|
"LabelSettingsEnableWatcherForLibrary": "Включить отслеживание за папками библиотеки",
|
||||||
"LabelSettingsEnableWatcherHelp": "Включает автоматическое добавление/обновление элементов при обнаружении изменений файлов. *Требуется перезапуск сервера",
|
"LabelSettingsEnableWatcherHelp": "Включает автоматическое добавление/обновление элементов при обнаружении изменений файлов. *Требуется перезапуск сервера",
|
||||||
|
"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.",
|
||||||
"LabelSettingsExperimentalFeatures": "Экспериментальные функции",
|
"LabelSettingsExperimentalFeatures": "Экспериментальные функции",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Функционал в разработке на который Вы могли бы дать отзыв или помочь в тестировании. Нажмите для открытия обсуждения на github.",
|
"LabelSettingsExperimentalFeaturesHelp": "Функционал в разработке на который Вы могли бы дать отзыв или помочь в тестировании. Нажмите для открытия обсуждения на github.",
|
||||||
"LabelSettingsFindCovers": "Найти обложки",
|
"LabelSettingsFindCovers": "Найти обложки",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "Перетащите файлы для исправления порядка треков",
|
"MessageDragFilesIntoTrackOrder": "Перетащите файлы для исправления порядка треков",
|
||||||
"MessageEmbedFinished": "Встраивание завершено!",
|
"MessageEmbedFinished": "Встраивание завершено!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} Эпизод(ов) запланировано для закачки",
|
"MessageEpisodesQueuedForDownload": "{0} Эпизод(ов) запланировано для закачки",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "URL канала будет {0}",
|
"MessageFeedURLWillBe": "URL канала будет {0}",
|
||||||
"MessageFetching": "Завершается...",
|
"MessageFetching": "Завершается...",
|
||||||
"MessageForceReScanDescription": "будет сканировать все файлы снова, как свежее сканирование. Теги ID3 аудиофайлов, OPF-файлы и текстовые файлы будут сканироваться как новые.",
|
"MessageForceReScanDescription": "будет сканировать все файлы снова, как свежее сканирование. Теги ID3 аудиофайлов, OPF-файлы и текстовые файлы будут сканироваться как новые.",
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "Förkortad",
|
"LabelAbridged": "Förkortad",
|
||||||
"LabelAbridgedChecked": "Abridged (checked)",
|
"LabelAbridgedChecked": "Abridged (checked)",
|
||||||
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "Kontotyp",
|
"LabelAccountType": "Kontotyp",
|
||||||
"LabelAccountTypeAdmin": "Admin",
|
"LabelAccountTypeAdmin": "Admin",
|
||||||
"LabelAccountTypeGuest": "Gäst",
|
"LabelAccountTypeGuest": "Gäst",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "Aktivera Watcher",
|
"LabelSettingsEnableWatcher": "Aktivera Watcher",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Aktivera mappbevakning för bibliotek",
|
"LabelSettingsEnableWatcherForLibrary": "Aktivera mappbevakning för bibliotek",
|
||||||
"LabelSettingsEnableWatcherHelp": "Aktiverar automatiskt lägga till/uppdatera objekt när filändringar upptäcks. *Kräver omstart av servern",
|
"LabelSettingsEnableWatcherHelp": "Aktiverar automatiskt lägga till/uppdatera objekt när filändringar upptäcks. *Kräver omstart av servern",
|
||||||
|
"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.",
|
||||||
"LabelSettingsExperimentalFeatures": "Experimentella funktioner",
|
"LabelSettingsExperimentalFeatures": "Experimentella funktioner",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Funktioner under utveckling som behöver din feedback och hjälp med testning. Klicka för att öppna diskussionen på GitHub.",
|
"LabelSettingsExperimentalFeaturesHelp": "Funktioner under utveckling som behöver din feedback och hjälp med testning. Klicka för att öppna diskussionen på GitHub.",
|
||||||
"LabelSettingsFindCovers": "Hitta omslag",
|
"LabelSettingsFindCovers": "Hitta omslag",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "Dra filer till rätt spårordning",
|
"MessageDragFilesIntoTrackOrder": "Dra filer till rätt spårordning",
|
||||||
"MessageEmbedFinished": "Inbäddning klar!",
|
"MessageEmbedFinished": "Inbäddning klar!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} avsnitt i kö för nedladdning",
|
"MessageEpisodesQueuedForDownload": "{0} avsnitt i kö för nedladdning",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "Flödes-URL kommer att vara {0}",
|
"MessageFeedURLWillBe": "Flödes-URL kommer att vara {0}",
|
||||||
"MessageFetching": "Hämtar...",
|
"MessageFetching": "Hämtar...",
|
||||||
"MessageForceReScanDescription": "kommer att göra en omgångssökning av alla filer som en färsk sökning. ID3-taggar för ljudfiler, OPF-filer och textfiler kommer att sökas som nya.",
|
"MessageForceReScanDescription": "kommer att göra en omgångssökning av alla filer som en färsk sökning. ID3-taggar för ljudfiler, OPF-filer och textfiler kommer att sökas som nya.",
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "Скорочена",
|
"LabelAbridged": "Скорочена",
|
||||||
"LabelAbridgedChecked": "Скорочена (з прапорцем)",
|
"LabelAbridgedChecked": "Скорочена (з прапорцем)",
|
||||||
"LabelAbridgedUnchecked": "Нескорочена (без прапорця)",
|
"LabelAbridgedUnchecked": "Нескорочена (без прапорця)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "Тип профілю",
|
"LabelAccountType": "Тип профілю",
|
||||||
"LabelAccountTypeAdmin": "Адміністратор",
|
"LabelAccountTypeAdmin": "Адміністратор",
|
||||||
"LabelAccountTypeGuest": "Гість",
|
"LabelAccountTypeGuest": "Гість",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "Увімкнути спостерігача",
|
"LabelSettingsEnableWatcher": "Увімкнути спостерігача",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Увімкнути спостерігання тек бібліотеки",
|
"LabelSettingsEnableWatcherForLibrary": "Увімкнути спостерігання тек бібліотеки",
|
||||||
"LabelSettingsEnableWatcherHelp": "Вмикає автоматичне додавання/оновлення елементів, коли спостерігаються зміни файлів. *Потребує перезавантаження сервера",
|
"LabelSettingsEnableWatcherHelp": "Вмикає автоматичне додавання/оновлення елементів, коли спостерігаються зміни файлів. *Потребує перезавантаження сервера",
|
||||||
|
"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.",
|
||||||
"LabelSettingsExperimentalFeatures": "Експериментальні функції",
|
"LabelSettingsExperimentalFeatures": "Експериментальні функції",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Функції в розробці, які потребують вашого відгуку та допомоги в тестуванні. Натисніть, щоб відкрити обговорення на Github.",
|
"LabelSettingsExperimentalFeaturesHelp": "Функції в розробці, які потребують вашого відгуку та допомоги в тестуванні. Натисніть, щоб відкрити обговорення на Github.",
|
||||||
"LabelSettingsFindCovers": "Пошук обкладинок",
|
"LabelSettingsFindCovers": "Пошук обкладинок",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "Перетягніть файли до правильного порядку",
|
"MessageDragFilesIntoTrackOrder": "Перетягніть файли до правильного порядку",
|
||||||
"MessageEmbedFinished": "Вбудовано!",
|
"MessageEmbedFinished": "Вбудовано!",
|
||||||
"MessageEpisodesQueuedForDownload": "Епізодів у черзі завантаження: {0}",
|
"MessageEpisodesQueuedForDownload": "Епізодів у черзі завантаження: {0}",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "URL-адреса каналу буде {0}",
|
"MessageFeedURLWillBe": "URL-адреса каналу буде {0}",
|
||||||
"MessageFetching": "Отримання...",
|
"MessageFetching": "Отримання...",
|
||||||
"MessageForceReScanDescription": "Просканує усі файли заново, неначе вперше. ID3-мітки, файли OPF та текстові файли будуть проскановані як нові.",
|
"MessageForceReScanDescription": "Просканує усі файли заново, неначе вперше. ID3-мітки, файли OPF та текстові файли будуть проскановані як нові.",
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "Rút Gọn",
|
"LabelAbridged": "Rút Gọn",
|
||||||
"LabelAbridgedChecked": "Abridged (checked)",
|
"LabelAbridgedChecked": "Abridged (checked)",
|
||||||
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "Loại Tài Khoản",
|
"LabelAccountType": "Loại Tài Khoản",
|
||||||
"LabelAccountTypeAdmin": "Quản Trị Viên",
|
"LabelAccountTypeAdmin": "Quản Trị Viên",
|
||||||
"LabelAccountTypeGuest": "Khách",
|
"LabelAccountTypeGuest": "Khách",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "Bật Watcher",
|
"LabelSettingsEnableWatcher": "Bật Watcher",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "Bật watcher thư mục cho thư viện",
|
"LabelSettingsEnableWatcherForLibrary": "Bật watcher thư mục cho thư viện",
|
||||||
"LabelSettingsEnableWatcherHelp": "Bật chức năng tự động thêm/cập nhật các mục khi phát hiện thay đổi tập tin. *Yêu cầu khởi động lại máy chủ",
|
"LabelSettingsEnableWatcherHelp": "Bật chức năng tự động thêm/cập nhật các mục khi phát hiện thay đổi tập tin. *Yêu cầu khởi động lại máy chủ",
|
||||||
|
"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.",
|
||||||
"LabelSettingsExperimentalFeatures": "Tính năng thử nghiệm",
|
"LabelSettingsExperimentalFeatures": "Tính năng thử nghiệm",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Các tính năng đang phát triển có thể cần phản hồi của bạn và sự giúp đỡ trong thử nghiệm. Nhấp để mở thảo luận trên github.",
|
"LabelSettingsExperimentalFeaturesHelp": "Các tính năng đang phát triển có thể cần phản hồi của bạn và sự giúp đỡ trong thử nghiệm. Nhấp để mở thảo luận trên github.",
|
||||||
"LabelSettingsFindCovers": "Tìm ảnh bìa",
|
"LabelSettingsFindCovers": "Tìm ảnh bìa",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "Kéo tệp vào thứ tự track đúng",
|
"MessageDragFilesIntoTrackOrder": "Kéo tệp vào thứ tự track đúng",
|
||||||
"MessageEmbedFinished": "Nhúng Hoàn thành!",
|
"MessageEmbedFinished": "Nhúng Hoàn thành!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} Tập(s) đã được thêm vào hàng đợi để tải xuống",
|
"MessageEpisodesQueuedForDownload": "{0} Tập(s) đã được thêm vào hàng đợi để tải xuống",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "URL nguồn cấp sẽ là {0}",
|
"MessageFeedURLWillBe": "URL nguồn cấp sẽ là {0}",
|
||||||
"MessageFetching": "Đang tìm...",
|
"MessageFetching": "Đang tìm...",
|
||||||
"MessageForceReScanDescription": "sẽ quét lại tất cả các tệp như một quét mới. Các thẻ ID3 của tệp âm thanh, tệp OPF và tệp văn bản sẽ được quét làm mới.",
|
"MessageForceReScanDescription": "sẽ quét lại tất cả các tệp như một quét mới. Các thẻ ID3 của tệp âm thanh, tệp OPF và tệp văn bản sẽ được quét làm mới.",
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "概要",
|
"LabelAbridged": "概要",
|
||||||
"LabelAbridgedChecked": "Abridged (checked)",
|
"LabelAbridgedChecked": "Abridged (checked)",
|
||||||
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "帐户类型",
|
"LabelAccountType": "帐户类型",
|
||||||
"LabelAccountTypeAdmin": "管理员",
|
"LabelAccountTypeAdmin": "管理员",
|
||||||
"LabelAccountTypeGuest": "来宾",
|
"LabelAccountTypeGuest": "来宾",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "启用监视程序",
|
"LabelSettingsEnableWatcher": "启用监视程序",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "为库启用文件夹监视程序",
|
"LabelSettingsEnableWatcherForLibrary": "为库启用文件夹监视程序",
|
||||||
"LabelSettingsEnableWatcherHelp": "当检测到文件更改时, 启用项目的自动添加/更新. *需要重新启动服务器",
|
"LabelSettingsEnableWatcherHelp": "当检测到文件更改时, 启用项目的自动添加/更新. *需要重新启动服务器",
|
||||||
|
"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.",
|
||||||
"LabelSettingsExperimentalFeatures": "实验功能",
|
"LabelSettingsExperimentalFeatures": "实验功能",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "开发中的功能需要你的反馈并帮助测试. 点击打开 github 讨论.",
|
"LabelSettingsExperimentalFeaturesHelp": "开发中的功能需要你的反馈并帮助测试. 点击打开 github 讨论.",
|
||||||
"LabelSettingsFindCovers": "查找封面",
|
"LabelSettingsFindCovers": "查找封面",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "将文件拖动到正确的音轨顺序",
|
"MessageDragFilesIntoTrackOrder": "将文件拖动到正确的音轨顺序",
|
||||||
"MessageEmbedFinished": "嵌入完成!",
|
"MessageEmbedFinished": "嵌入完成!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} 个剧集排队等待下载",
|
"MessageEpisodesQueuedForDownload": "{0} 个剧集排队等待下载",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "源 URL 将改为 {0}",
|
"MessageFeedURLWillBe": "源 URL 将改为 {0}",
|
||||||
"MessageFetching": "正在获取...",
|
"MessageFetching": "正在获取...",
|
||||||
"MessageForceReScanDescription": "将像重新扫描一样再次扫描所有文件. 音频文件 ID3 标签, OPF 文件和文本文件将被扫描为新文件.",
|
"MessageForceReScanDescription": "将像重新扫描一样再次扫描所有文件. 音频文件 ID3 标签, OPF 文件和文本文件将被扫描为新文件.",
|
||||||
|
|
|
||||||
|
|
@ -191,6 +191,7 @@
|
||||||
"LabelAbridged": "概要",
|
"LabelAbridged": "概要",
|
||||||
"LabelAbridgedChecked": "Abridged (checked)",
|
"LabelAbridgedChecked": "Abridged (checked)",
|
||||||
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
"LabelAbridgedUnchecked": "Unabridged (unchecked)",
|
||||||
|
"LabelAccessibleBy": "Accessible by",
|
||||||
"LabelAccountType": "帳號類型",
|
"LabelAccountType": "帳號類型",
|
||||||
"LabelAccountTypeAdmin": "管理員",
|
"LabelAccountTypeAdmin": "管理員",
|
||||||
"LabelAccountTypeGuest": "來賓",
|
"LabelAccountTypeGuest": "來賓",
|
||||||
|
|
@ -470,6 +471,8 @@
|
||||||
"LabelSettingsEnableWatcher": "啟用監視程序",
|
"LabelSettingsEnableWatcher": "啟用監視程序",
|
||||||
"LabelSettingsEnableWatcherForLibrary": "為庫啟用資料夾監視程序",
|
"LabelSettingsEnableWatcherForLibrary": "為庫啟用資料夾監視程序",
|
||||||
"LabelSettingsEnableWatcherHelp": "當檢測到檔案更改時, 啟用項目的自動新增/更新. *需要重新啟動伺服器",
|
"LabelSettingsEnableWatcherHelp": "當檢測到檔案更改時, 啟用項目的自動新增/更新. *需要重新啟動伺服器",
|
||||||
|
"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.",
|
||||||
"LabelSettingsExperimentalFeatures": "實驗功能",
|
"LabelSettingsExperimentalFeatures": "實驗功能",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "開發中的功能需要你的反饋並幫助測試. 點擊打開 github 討論.",
|
"LabelSettingsExperimentalFeaturesHelp": "開發中的功能需要你的反饋並幫助測試. 點擊打開 github 討論.",
|
||||||
"LabelSettingsFindCovers": "查找封面",
|
"LabelSettingsFindCovers": "查找封面",
|
||||||
|
|
@ -630,6 +633,7 @@
|
||||||
"MessageDragFilesIntoTrackOrder": "將檔案拖動到正確的音軌順序",
|
"MessageDragFilesIntoTrackOrder": "將檔案拖動到正確的音軌順序",
|
||||||
"MessageEmbedFinished": "嵌入完成!",
|
"MessageEmbedFinished": "嵌入完成!",
|
||||||
"MessageEpisodesQueuedForDownload": "{0} 個劇集排隊等待下載",
|
"MessageEpisodesQueuedForDownload": "{0} 個劇集排隊等待下載",
|
||||||
|
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
|
||||||
"MessageFeedURLWillBe": "源 URL 將改為 {0}",
|
"MessageFeedURLWillBe": "源 URL 將改為 {0}",
|
||||||
"MessageFetching": "正在獲取...",
|
"MessageFetching": "正在獲取...",
|
||||||
"MessageForceReScanDescription": "將像重新掃描一樣再次掃描所有檔案. 音頻檔 ID3 標籤, OPF 檔和文本檔將被掃描為新檔案.",
|
"MessageForceReScanDescription": "將像重新掃描一樣再次掃描所有檔案. 音頻檔 ID3 標籤, OPF 檔和文本檔將被掃描為新檔案.",
|
||||||
|
|
|
||||||
1
index.js
1
index.js
|
|
@ -4,7 +4,6 @@ global.appRoot = __dirname
|
||||||
const isDev = process.env.NODE_ENV !== 'production'
|
const isDev = process.env.NODE_ENV !== 'production'
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
const devEnv = require('./dev').config
|
const devEnv = require('./dev').config
|
||||||
process.env.NODE_ENV = 'development'
|
|
||||||
if (devEnv.Port) process.env.PORT = devEnv.Port
|
if (devEnv.Port) process.env.PORT = devEnv.Port
|
||||||
if (devEnv.ConfigPath) process.env.CONFIG_PATH = devEnv.ConfigPath
|
if (devEnv.ConfigPath) process.env.CONFIG_PATH = devEnv.ConfigPath
|
||||||
if (devEnv.MetadataPath) process.env.METADATA_PATH = devEnv.MetadataPath
|
if (devEnv.MetadataPath) process.env.METADATA_PATH = devEnv.MetadataPath
|
||||||
|
|
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.9.0",
|
"version": "2.10.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.9.0",
|
"version": "2.10.1",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.9.0",
|
"version": "2.10.1",
|
||||||
"buildNumber": 1,
|
"buildNumber": 1,
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ class Logger {
|
||||||
this.logManager = null
|
this.logManager = null
|
||||||
|
|
||||||
this.isDev = process.env.NODE_ENV !== 'production'
|
this.isDev = process.env.NODE_ENV !== 'production'
|
||||||
|
|
||||||
this.logLevel = !this.isDev ? LogLevel.INFO : LogLevel.TRACE
|
this.logLevel = !this.isDev ? LogLevel.INFO : LogLevel.TRACE
|
||||||
this.socketListeners = []
|
this.socketListeners = []
|
||||||
}
|
}
|
||||||
|
|
@ -49,7 +50,7 @@ class Logger {
|
||||||
}
|
}
|
||||||
|
|
||||||
addSocketListener(socket, level) {
|
addSocketListener(socket, level) {
|
||||||
var index = this.socketListeners.findIndex(s => s.id === socket.id)
|
var index = this.socketListeners.findIndex((s) => s.id === socket.id)
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
this.socketListeners.splice(index, 1, {
|
this.socketListeners.splice(index, 1, {
|
||||||
id: socket.id,
|
id: socket.id,
|
||||||
|
|
@ -66,7 +67,7 @@ class Logger {
|
||||||
}
|
}
|
||||||
|
|
||||||
removeSocketListener(socketId) {
|
removeSocketListener(socketId) {
|
||||||
this.socketListeners = this.socketListeners.filter(s => s.id !== socketId)
|
this.socketListeners = this.socketListeners.filter((s) => s.id !== socketId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ class Server {
|
||||||
global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH))
|
global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH))
|
||||||
global.RouterBasePath = ROUTER_BASE_PATH
|
global.RouterBasePath = ROUTER_BASE_PATH
|
||||||
global.XAccel = process.env.USE_X_ACCEL
|
global.XAccel = process.env.USE_X_ACCEL
|
||||||
|
global.AllowCors = process.env.ALLOW_CORS === '1'
|
||||||
|
|
||||||
if (!fs.pathExistsSync(global.ConfigPath)) {
|
if (!fs.pathExistsSync(global.ConfigPath)) {
|
||||||
fs.mkdirSync(global.ConfigPath)
|
fs.mkdirSync(global.ConfigPath)
|
||||||
|
|
@ -182,11 +183,12 @@ class Server {
|
||||||
* @see https://ionicframework.com/docs/troubleshooting/cors
|
* @see https://ionicframework.com/docs/troubleshooting/cors
|
||||||
*
|
*
|
||||||
* Running in development allows cors to allow testing the mobile apps in the browser
|
* Running in development allows cors to allow testing the mobile apps in the browser
|
||||||
|
* or env variable ALLOW_CORS = '1'
|
||||||
*/
|
*/
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
if (Logger.isDev || req.path.match(/\/api\/items\/([a-z0-9-]{36})\/(ebook|cover)(\/[0-9]+)?/)) {
|
if (Logger.isDev || req.path.match(/\/api\/items\/([a-z0-9-]{36})\/(ebook|cover)(\/[0-9]+)?/)) {
|
||||||
const allowedOrigins = ['capacitor://localhost', 'http://localhost']
|
const allowedOrigins = ['capacitor://localhost', 'http://localhost']
|
||||||
if (Logger.isDev || allowedOrigins.some((o) => o === req.get('origin'))) {
|
if (global.AllowCors || Logger.isDev || allowedOrigins.some((o) => o === req.get('origin'))) {
|
||||||
res.header('Access-Control-Allow-Origin', req.get('origin'))
|
res.header('Access-Control-Allow-Origin', req.get('origin'))
|
||||||
res.header('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
|
res.header('Access-Control-Allow-Methods', 'GET, POST, PATCH, PUT, DELETE, OPTIONS')
|
||||||
res.header('Access-Control-Allow-Headers', '*')
|
res.header('Access-Control-Allow-Headers', '*')
|
||||||
|
|
|
||||||
|
|
@ -512,8 +512,7 @@ class LibraryController {
|
||||||
* @param {*} res
|
* @param {*} res
|
||||||
*/
|
*/
|
||||||
async getUserPlaylistsForLibrary(req, res) {
|
async getUserPlaylistsForLibrary(req, res) {
|
||||||
let playlistsForUser = await Database.playlistModel.getPlaylistsForUserAndLibrary(req.user.id, req.library.id)
|
let playlistsForUser = await Database.playlistModel.getOldPlaylistsForUserAndLibrary(req.user.id, req.library.id)
|
||||||
playlistsForUser = await Promise.all(playlistsForUser.map(async (p) => p.getOldJsonExpanded()))
|
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
results: [],
|
results: [],
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ const { toNumber } = require('../utils/index')
|
||||||
const userStats = require('../utils/queries/userStats')
|
const userStats = require('../utils/queries/userStats')
|
||||||
|
|
||||||
class MeController {
|
class MeController {
|
||||||
constructor() { }
|
constructor() {}
|
||||||
|
|
||||||
getCurrentUser(req, res) {
|
getCurrentUser(req, res) {
|
||||||
res.json(req.user.toJSONForBrowser())
|
res.json(req.user.toJSONForBrowser())
|
||||||
|
|
@ -33,6 +33,43 @@ class MeController {
|
||||||
res.json(payload)
|
res.json(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET: /api/me/item/listening-sessions/:libraryItemId/:episodeId
|
||||||
|
*
|
||||||
|
* @this import('../routers/ApiRouter')
|
||||||
|
*
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
*/
|
||||||
|
async getItemListeningSessions(req, res) {
|
||||||
|
const libraryItem = await Database.libraryItemModel.findByPk(req.params.libraryItemId)
|
||||||
|
const episode = await Database.podcastEpisodeModel.findByPk(req.params.episodeId)
|
||||||
|
|
||||||
|
if (!libraryItem || (libraryItem.mediaType === 'podcast' && !episode)) {
|
||||||
|
Logger.error(`[MeController] Media item not found for library item id "${req.params.libraryItemId}"`)
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaItemId = episode?.id || libraryItem.mediaId
|
||||||
|
let listeningSessions = await this.getUserItemListeningSessionsHelper(req.user.id, mediaItemId)
|
||||||
|
|
||||||
|
const itemsPerPage = toNumber(req.query.itemsPerPage, 10) || 10
|
||||||
|
const page = toNumber(req.query.page, 0)
|
||||||
|
|
||||||
|
const start = page * itemsPerPage
|
||||||
|
const sessions = listeningSessions.slice(start, start + itemsPerPage)
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
total: listeningSessions.length,
|
||||||
|
numPages: Math.ceil(listeningSessions.length / itemsPerPage),
|
||||||
|
page,
|
||||||
|
itemsPerPage,
|
||||||
|
sessions
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(payload)
|
||||||
|
}
|
||||||
|
|
||||||
// GET: api/me/listening-stats
|
// GET: api/me/listening-stats
|
||||||
async getListeningStats(req, res) {
|
async getListeningStats(req, res) {
|
||||||
const listeningStats = await this.getUserListeningStatsHelpers(req.user.id)
|
const listeningStats = await this.getUserListeningStatsHelpers(req.user.id)
|
||||||
|
|
@ -80,7 +117,7 @@ class MeController {
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
return res.status(404).send('Item not found')
|
return res.status(404).send('Item not found')
|
||||||
}
|
}
|
||||||
if (!libraryItem.media.episodes.find(ep => ep.id === episodeId)) {
|
if (!libraryItem.media.episodes.find((ep) => ep.id === episodeId)) {
|
||||||
Logger.error(`[MeController] removeEpisode episode ${episodeId} not found for item ${libraryItem.id}`)
|
Logger.error(`[MeController] removeEpisode episode ${episodeId} not found for item ${libraryItem.id}`)
|
||||||
return res.status(404).send('Episode not found')
|
return res.status(404).send('Episode not found')
|
||||||
}
|
}
|
||||||
|
|
@ -123,7 +160,7 @@ class MeController {
|
||||||
|
|
||||||
// POST: api/me/item/:id/bookmark
|
// POST: api/me/item/:id/bookmark
|
||||||
async createBookmark(req, res) {
|
async createBookmark(req, res) {
|
||||||
if (!await Database.libraryItemModel.checkExistsById(req.params.id)) return res.sendStatus(404)
|
if (!(await Database.libraryItemModel.checkExistsById(req.params.id))) return res.sendStatus(404)
|
||||||
|
|
||||||
const { time, title } = req.body
|
const { time, title } = req.body
|
||||||
const bookmark = req.user.createBookmark(req.params.id, time, title)
|
const bookmark = req.user.createBookmark(req.params.id, time, title)
|
||||||
|
|
@ -134,7 +171,7 @@ class MeController {
|
||||||
|
|
||||||
// PATCH: api/me/item/:id/bookmark
|
// PATCH: api/me/item/:id/bookmark
|
||||||
async updateBookmark(req, res) {
|
async updateBookmark(req, res) {
|
||||||
if (!await Database.libraryItemModel.checkExistsById(req.params.id)) return res.sendStatus(404)
|
if (!(await Database.libraryItemModel.checkExistsById(req.params.id))) return res.sendStatus(404)
|
||||||
|
|
||||||
const { time, title } = req.body
|
const { time, title } = req.body
|
||||||
if (!req.user.findBookmark(req.params.id, time)) {
|
if (!req.user.findBookmark(req.params.id, time)) {
|
||||||
|
|
@ -152,7 +189,7 @@ class MeController {
|
||||||
|
|
||||||
// DELETE: api/me/item/:id/bookmark/:time
|
// DELETE: api/me/item/:id/bookmark/:time
|
||||||
async removeBookmark(req, res) {
|
async removeBookmark(req, res) {
|
||||||
if (!await Database.libraryItemModel.checkExistsById(req.params.id)) return res.sendStatus(404)
|
if (!(await Database.libraryItemModel.checkExistsById(req.params.id))) return res.sendStatus(404)
|
||||||
|
|
||||||
const time = Number(req.params.time)
|
const time = Number(req.params.time)
|
||||||
if (isNaN(time)) return res.sendStatus(500)
|
if (isNaN(time)) return res.sendStatus(500)
|
||||||
|
|
@ -254,11 +291,10 @@ class MeController {
|
||||||
// TODO: More efficient to do this in a single query
|
// TODO: More efficient to do this in a single query
|
||||||
for (const mediaProgress of req.user.mediaProgress) {
|
for (const mediaProgress of req.user.mediaProgress) {
|
||||||
if (!mediaProgress.isFinished && (mediaProgress.progress > 0 || mediaProgress.ebookProgress > 0)) {
|
if (!mediaProgress.isFinished && (mediaProgress.progress > 0 || mediaProgress.ebookProgress > 0)) {
|
||||||
|
|
||||||
const libraryItem = await Database.libraryItemModel.getOldById(mediaProgress.libraryItemId)
|
const libraryItem = await Database.libraryItemModel.getOldById(mediaProgress.libraryItemId)
|
||||||
if (libraryItem) {
|
if (libraryItem) {
|
||||||
if (mediaProgress.episodeId && libraryItem.mediaType === 'podcast') {
|
if (mediaProgress.episodeId && libraryItem.mediaType === 'podcast') {
|
||||||
const episode = libraryItem.media.episodes.find(ep => ep.id === mediaProgress.episodeId)
|
const episode = libraryItem.media.episodes.find((ep) => ep.id === mediaProgress.episodeId)
|
||||||
if (episode) {
|
if (episode) {
|
||||||
const libraryItemWithEpisode = {
|
const libraryItemWithEpisode = {
|
||||||
...libraryItem.toJSONMinified(),
|
...libraryItem.toJSONMinified(),
|
||||||
|
|
@ -277,7 +313,9 @@ class MeController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
itemsInProgress = sort(itemsInProgress).desc(li => li.progressLastUpdate).slice(0, limit)
|
itemsInProgress = sort(itemsInProgress)
|
||||||
|
.desc((li) => li.progressLastUpdate)
|
||||||
|
.slice(0, limit)
|
||||||
res.json({
|
res.json({
|
||||||
libraryItems: itemsInProgress
|
libraryItems: itemsInProgress
|
||||||
})
|
})
|
||||||
|
|
@ -317,19 +355,22 @@ class MeController {
|
||||||
|
|
||||||
// GET: api/me/progress/:id/remove-from-continue-listening
|
// GET: api/me/progress/:id/remove-from-continue-listening
|
||||||
async removeItemFromContinueListening(req, res) {
|
async removeItemFromContinueListening(req, res) {
|
||||||
const mediaProgress = req.user.mediaProgress.find(mp => mp.id === req.params.id)
|
const mediaProgress = req.user.mediaProgress.find((mp) => mp.id === req.params.id)
|
||||||
if (!mediaProgress) {
|
if (!mediaProgress) {
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
const hasUpdated = req.user.removeProgressFromContinueListening(req.params.id)
|
const hasUpdated = req.user.removeProgressFromContinueListening(req.params.id)
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
await Database.mediaProgressModel.update({
|
await Database.mediaProgressModel.update(
|
||||||
|
{
|
||||||
hideFromContinueListening: true
|
hideFromContinueListening: true
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
where: {
|
where: {
|
||||||
id: mediaProgress.id
|
id: mediaProgress.id
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
)
|
||||||
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
SocketAuthority.clientEmitter(req.user.id, 'user_updated', req.user.toJSONForBrowser())
|
||||||
}
|
}
|
||||||
res.json(req.user.toJSONForBrowser())
|
res.json(req.user.toJSONForBrowser())
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ const Logger = require('../Logger')
|
||||||
const { levenshteinDistance, escapeRegExp } = require('../utils/index')
|
const { levenshteinDistance, escapeRegExp } = require('../utils/index')
|
||||||
|
|
||||||
class BookFinder {
|
class BookFinder {
|
||||||
|
#providerResponseTimeout = 30000
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.openLibrary = new OpenLibrary()
|
this.openLibrary = new OpenLibrary()
|
||||||
this.googleBooks = new GoogleBooks()
|
this.googleBooks = new GoogleBooks()
|
||||||
|
|
@ -36,7 +38,8 @@ class BookFinder {
|
||||||
filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance) {
|
filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance) {
|
||||||
var searchTitle = cleanTitleForCompares(title)
|
var searchTitle = cleanTitleForCompares(title)
|
||||||
var searchAuthor = cleanAuthorForCompares(author)
|
var searchAuthor = cleanAuthorForCompares(author)
|
||||||
return books.map(b => {
|
return books
|
||||||
|
.map((b) => {
|
||||||
b.cleanedTitle = cleanTitleForCompares(b.title)
|
b.cleanedTitle = cleanTitleForCompares(b.title)
|
||||||
b.titleDistance = levenshteinDistance(b.cleanedTitle, title)
|
b.titleDistance = levenshteinDistance(b.cleanedTitle, title)
|
||||||
|
|
||||||
|
|
@ -68,8 +71,10 @@ class BookFinder {
|
||||||
else if (title.length > 4 && b.title.includes(title)) b.includesTitle = title
|
else if (title.length > 4 && b.title.includes(title)) b.includesTitle = title
|
||||||
|
|
||||||
return b
|
return b
|
||||||
}).filter(b => {
|
})
|
||||||
if (b.includesTitle) { // If search title was found in result title then skip over leven distance check
|
.filter((b) => {
|
||||||
|
if (b.includesTitle) {
|
||||||
|
// If search title was found in result title then skip over leven distance check
|
||||||
if (this.verbose) Logger.debug(`Exact title was included in "${b.title}", Search: "${b.includesTitle}"`)
|
if (this.verbose) Logger.debug(`Exact title was included in "${b.title}", Search: "${b.includesTitle}"`)
|
||||||
} else if (b.titleDistance > maxTitleDistance) {
|
} else if (b.titleDistance > maxTitleDistance) {
|
||||||
if (this.verbose) Logger.debug(`Filtering out search result title distance = ${b.titleDistance}: "${b.cleanedTitle}"/"${searchTitle}"`)
|
if (this.verbose) Logger.debug(`Filtering out search result title distance = ${b.titleDistance}: "${b.cleanedTitle}"/"${searchTitle}"`)
|
||||||
|
|
@ -77,7 +82,8 @@ class BookFinder {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (author) {
|
if (author) {
|
||||||
if (b.includesAuthor) { // If search author was found in result author then skip over leven distance check
|
if (b.includesAuthor) {
|
||||||
|
// If search author was found in result author then skip over leven distance check
|
||||||
if (this.verbose) Logger.debug(`Exact author was included in "${b.author}", Search: "${b.includesAuthor}"`)
|
if (this.verbose) Logger.debug(`Exact author was included in "${b.author}", Search: "${b.includesAuthor}"`)
|
||||||
} else if (b.authorDistance > maxAuthorDistance) {
|
} else if (b.authorDistance > maxAuthorDistance) {
|
||||||
if (this.verbose) Logger.debug(`Filtering out search result "${b.author}", author distance = ${b.authorDistance}: "${b.author}"/"${author}"`)
|
if (this.verbose) Logger.debug(`Filtering out search result "${b.author}", author distance = ${b.authorDistance}: "${b.author}"/"${author}"`)
|
||||||
|
|
@ -91,8 +97,16 @@ class BookFinder {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} title
|
||||||
|
* @param {string} author
|
||||||
|
* @param {number} maxTitleDistance
|
||||||
|
* @param {number} maxAuthorDistance
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
*/
|
||||||
async getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance) {
|
async getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance) {
|
||||||
var books = await this.openLibrary.searchTitle(title)
|
var books = await this.openLibrary.searchTitle(title, this.#providerResponseTimeout)
|
||||||
if (this.verbose) Logger.debug(`OpenLib Book Search Results: ${books.length || 0}`)
|
if (this.verbose) Logger.debug(`OpenLib Book Search Results: ${books.length || 0}`)
|
||||||
if (books.errorCode) {
|
if (books.errorCode) {
|
||||||
Logger.error(`OpenLib Search Error ${books.errorCode}`)
|
Logger.error(`OpenLib Search Error ${books.errorCode}`)
|
||||||
|
|
@ -109,8 +123,14 @@ class BookFinder {
|
||||||
return booksFiltered
|
return booksFiltered
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} title
|
||||||
|
* @param {string} author
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
*/
|
||||||
async getGoogleBooksResults(title, author) {
|
async getGoogleBooksResults(title, author) {
|
||||||
var books = await this.googleBooks.search(title, author)
|
var books = await this.googleBooks.search(title, author, this.#providerResponseTimeout)
|
||||||
if (this.verbose) Logger.debug(`GoogleBooks Book Search Results: ${books.length || 0}`)
|
if (this.verbose) Logger.debug(`GoogleBooks Book Search Results: ${books.length || 0}`)
|
||||||
if (books.errorCode) {
|
if (books.errorCode) {
|
||||||
Logger.error(`GoogleBooks Search Error ${books.errorCode}`)
|
Logger.error(`GoogleBooks Search Error ${books.errorCode}`)
|
||||||
|
|
@ -120,8 +140,14 @@ class BookFinder {
|
||||||
return books
|
return books
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} title
|
||||||
|
* @param {string} author
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
*/
|
||||||
async getFantLabResults(title, author) {
|
async getFantLabResults(title, author) {
|
||||||
var books = await this.fantLab.search(title, author)
|
var books = await this.fantLab.search(title, author, this.#providerResponseTimeout)
|
||||||
if (this.verbose) Logger.debug(`FantLab Book Search Results: ${books.length || 0}`)
|
if (this.verbose) Logger.debug(`FantLab Book Search Results: ${books.length || 0}`)
|
||||||
if (books.errorCode) {
|
if (books.errorCode) {
|
||||||
Logger.error(`FantLab Search Error ${books.errorCode}`)
|
Logger.error(`FantLab Search Error ${books.errorCode}`)
|
||||||
|
|
@ -131,19 +157,37 @@ class BookFinder {
|
||||||
return books
|
return books
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} search
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
*/
|
||||||
async getAudiobookCoversResults(search) {
|
async getAudiobookCoversResults(search) {
|
||||||
const covers = await this.audiobookCovers.search(search)
|
const covers = await this.audiobookCovers.search(search, this.#providerResponseTimeout)
|
||||||
if (this.verbose) Logger.debug(`AudiobookCovers Search Results: ${covers.length || 0}`)
|
if (this.verbose) Logger.debug(`AudiobookCovers Search Results: ${covers.length || 0}`)
|
||||||
return covers || []
|
return covers || []
|
||||||
}
|
}
|
||||||
|
|
||||||
async getiTunesAudiobooksResults(title, author) {
|
/**
|
||||||
return this.iTunesApi.searchAudiobooks(title)
|
*
|
||||||
|
* @param {string} title
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
*/
|
||||||
|
async getiTunesAudiobooksResults(title) {
|
||||||
|
return this.iTunesApi.searchAudiobooks(title, this.#providerResponseTimeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} title
|
||||||
|
* @param {string} author
|
||||||
|
* @param {string} asin
|
||||||
|
* @param {string} provider
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
*/
|
||||||
async getAudibleResults(title, author, asin, provider) {
|
async getAudibleResults(title, author, asin, provider) {
|
||||||
const region = provider.includes('.') ? provider.split('.').pop() : ''
|
const region = provider.includes('.') ? provider.split('.').pop() : ''
|
||||||
const books = await this.audible.search(title, author, asin, region)
|
const books = await this.audible.search(title, author, asin, region, this.#providerResponseTimeout)
|
||||||
if (this.verbose) Logger.debug(`Audible Book Search Results: ${books.length || 0}`)
|
if (this.verbose) Logger.debug(`Audible Book Search Results: ${books.length || 0}`)
|
||||||
if (!books) return []
|
if (!books) return []
|
||||||
return books
|
return books
|
||||||
|
|
@ -158,14 +202,13 @@ class BookFinder {
|
||||||
* @returns {Promise<Object[]>}
|
* @returns {Promise<Object[]>}
|
||||||
*/
|
*/
|
||||||
async getCustomProviderResults(title, author, isbn, providerSlug) {
|
async getCustomProviderResults(title, author, isbn, providerSlug) {
|
||||||
const books = await this.customProviderAdapter.search(title, author, isbn, providerSlug, 'book')
|
const books = await this.customProviderAdapter.search(title, author, isbn, providerSlug, 'book', this.#providerResponseTimeout)
|
||||||
if (this.verbose) Logger.debug(`Custom provider '${providerSlug}' Search Results: ${books.length || 0}`)
|
if (this.verbose) Logger.debug(`Custom provider '${providerSlug}' Search Results: ${books.length || 0}`)
|
||||||
|
|
||||||
return books
|
return books
|
||||||
}
|
}
|
||||||
|
|
||||||
static TitleCandidates = class {
|
static TitleCandidates = class {
|
||||||
|
|
||||||
constructor(cleanAuthor) {
|
constructor(cleanAuthor) {
|
||||||
this.candidates = new Set()
|
this.candidates = new Set()
|
||||||
this.cleanAuthor = cleanAuthor
|
this.cleanAuthor = cleanAuthor
|
||||||
|
|
@ -185,7 +228,7 @@ class BookFinder {
|
||||||
[/(^| |\.)(m4b|m4a|mp3)( |$)/g, ''], // Remove file-type
|
[/(^| |\.)(m4b|m4a|mp3)( |$)/g, ''], // Remove file-type
|
||||||
[/ a novel.*$/g, ''], // Remove "a novel"
|
[/ a novel.*$/g, ''], // Remove "a novel"
|
||||||
[/(^| )(un)?abridged( |$)/g, ' '], // Remove "unabridged/abridged"
|
[/(^| )(un)?abridged( |$)/g, ' '], // Remove "unabridged/abridged"
|
||||||
[/^\d+ | \d+$/g, ''], // Remove preceding/trailing numbers
|
[/^\d+ | \d+$/g, ''] // Remove preceding/trailing numbers
|
||||||
]
|
]
|
||||||
|
|
||||||
// Main variant
|
// Main variant
|
||||||
|
|
@ -197,8 +240,7 @@ class BookFinder {
|
||||||
|
|
||||||
let candidate = cleanTitle
|
let candidate = cleanTitle
|
||||||
|
|
||||||
for (const transformer of titleTransformers)
|
for (const transformer of titleTransformers) candidate = candidate.replace(transformer[0], transformer[1]).trim()
|
||||||
candidate = candidate.replace(transformer[0], transformer[1]).trim()
|
|
||||||
|
|
||||||
if (candidate != cleanTitle) {
|
if (candidate != cleanTitle) {
|
||||||
if (candidate) {
|
if (candidate) {
|
||||||
|
|
@ -240,7 +282,7 @@ class BookFinder {
|
||||||
|
|
||||||
#removeAuthorFromTitle(title) {
|
#removeAuthorFromTitle(title) {
|
||||||
if (!this.cleanAuthor) return title
|
if (!this.cleanAuthor) return title
|
||||||
const authorRe = new RegExp(`(^| | by |)${escapeRegExp(this.cleanAuthor)}(?= |$)`, "g")
|
const authorRe = new RegExp(`(^| | by |)${escapeRegExp(this.cleanAuthor)}(?= |$)`, 'g')
|
||||||
const authorCleanedTitle = cleanAuthorForCompares(title)
|
const authorCleanedTitle = cleanAuthorForCompares(title)
|
||||||
const authorCleanedTitleWithoutAuthor = authorCleanedTitle.replace(authorRe, '')
|
const authorCleanedTitleWithoutAuthor = authorCleanedTitle.replace(authorRe, '')
|
||||||
if (authorCleanedTitleWithoutAuthor !== authorCleanedTitle) {
|
if (authorCleanedTitleWithoutAuthor !== authorCleanedTitle) {
|
||||||
|
|
@ -297,7 +339,7 @@ class BookFinder {
|
||||||
promises.push(this.validateAuthor(candidate))
|
promises.push(this.validateAuthor(candidate))
|
||||||
}
|
}
|
||||||
const results = [...new Set(await Promise.all(promises))]
|
const results = [...new Set(await Promise.all(promises))]
|
||||||
filteredCandidates = results.filter(author => author)
|
filteredCandidates = results.filter((author) => author)
|
||||||
// If no valid candidates were found, add back an aggresively cleaned author version
|
// If no valid candidates were found, add back an aggresively cleaned author version
|
||||||
if (!filteredCandidates.length && this.cleanAuthor) filteredCandidates.push(this.agressivelyCleanAuthor)
|
if (!filteredCandidates.length && this.cleanAuthor) filteredCandidates.push(this.agressivelyCleanAuthor)
|
||||||
// Always add an empty author candidate
|
// Always add an empty author candidate
|
||||||
|
|
@ -312,7 +354,6 @@ class BookFinder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search for books including fuzzy searches
|
* Search for books including fuzzy searches
|
||||||
*
|
*
|
||||||
|
|
@ -337,8 +378,7 @@ class BookFinder {
|
||||||
return this.getCustomProviderResults(title, author, isbn, provider)
|
return this.getCustomProviderResults(title, author, isbn, provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!title)
|
if (!title) return books
|
||||||
return books
|
|
||||||
|
|
||||||
books = await this.runSearch(title, author, provider, asin, maxTitleDistance, maxAuthorDistance)
|
books = await this.runSearch(title, author, provider, asin, maxTitleDistance, maxAuthorDistance)
|
||||||
|
|
||||||
|
|
@ -353,17 +393,14 @@ 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, " - ")
|
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)
|
for (const titlePart of titleParts) authorCandidates.add(titlePart)
|
||||||
authorCandidates.add(titlePart)
|
|
||||||
authorCandidates = await authorCandidates.getCandidates()
|
authorCandidates = await authorCandidates.getCandidates()
|
||||||
loop_author:
|
loop_author: for (const authorCandidate of authorCandidates) {
|
||||||
for (const authorCandidate of authorCandidates) {
|
|
||||||
let titleCandidates = new BookFinder.TitleCandidates(authorCandidate)
|
let titleCandidates = new BookFinder.TitleCandidates(authorCandidate)
|
||||||
for (const titlePart of titleParts)
|
for (const titlePart of titleParts) titleCandidates.add(titlePart)
|
||||||
titleCandidates.add(titlePart)
|
|
||||||
titleCandidates = titleCandidates.getCandidates()
|
titleCandidates = titleCandidates.getCandidates()
|
||||||
for (const titleCandidate of titleCandidates) {
|
for (const titleCandidate of titleCandidates) {
|
||||||
if (titleCandidate == title && authorCandidate == author) continue // We already tried this
|
if (titleCandidate == title && authorCandidate == author) continue // We already tried this
|
||||||
|
|
@ -412,7 +449,7 @@ class BookFinder {
|
||||||
} else if (provider.startsWith('audible')) {
|
} else if (provider.startsWith('audible')) {
|
||||||
books = await this.getAudibleResults(title, author, asin, provider)
|
books = await this.getAudibleResults(title, author, asin, provider)
|
||||||
} else if (provider === 'itunes') {
|
} else if (provider === 'itunes') {
|
||||||
books = await this.getiTunesAudiobooksResults(title, author)
|
books = await this.getiTunesAudiobooksResults(title)
|
||||||
} else if (provider === 'openlibrary') {
|
} else if (provider === 'openlibrary') {
|
||||||
books = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance)
|
books = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance)
|
||||||
} else if (provider === 'fantlab') {
|
} else if (provider === 'fantlab') {
|
||||||
|
|
@ -448,7 +485,7 @@ class BookFinder {
|
||||||
covers.push(result.cover)
|
covers.push(result.cover)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return [...(new Set(covers))]
|
return [...new Set(covers)]
|
||||||
}
|
}
|
||||||
|
|
||||||
findChapters(asin, region) {
|
findChapters(asin, region) {
|
||||||
|
|
@ -468,7 +505,7 @@ function stripSubtitle(title) {
|
||||||
|
|
||||||
function replaceAccentedChars(str) {
|
function replaceAccentedChars(str) {
|
||||||
try {
|
try {
|
||||||
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, "")
|
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error('[BookFinder] str normalize error', error)
|
Logger.error('[BookFinder] str normalize error', error)
|
||||||
return str
|
return str
|
||||||
|
|
@ -483,7 +520,7 @@ function cleanTitleForCompares(title) {
|
||||||
let stripped = stripSubtitle(title)
|
let stripped = 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, "")
|
let cleaned = stripped.replace(/ *\([^)]*\) */g, '')
|
||||||
|
|
||||||
// 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, '')
|
||||||
|
|
|
||||||
|
|
@ -43,12 +43,14 @@ class Playlist extends Model {
|
||||||
},
|
},
|
||||||
order: [['playlistMediaItems', 'order', 'ASC']]
|
order: [['playlistMediaItems', 'order', 'ASC']]
|
||||||
})
|
})
|
||||||
return playlists.map(p => this.getOldPlaylist(p))
|
return playlists.map((p) => this.getOldPlaylist(p))
|
||||||
}
|
}
|
||||||
|
|
||||||
static getOldPlaylist(playlistExpanded) {
|
static getOldPlaylist(playlistExpanded) {
|
||||||
const items = playlistExpanded.playlistMediaItems.map(pmi => {
|
const items = playlistExpanded.playlistMediaItems
|
||||||
const libraryItemId = pmi.mediaItem?.podcast?.libraryItem?.id || pmi.mediaItem?.libraryItem?.id || null
|
.map((pmi) => {
|
||||||
|
const mediaItem = pmi.mediaItem || pmi.dataValues?.mediaItem
|
||||||
|
const libraryItemId = mediaItem?.podcast?.libraryItem?.id || mediaItem?.libraryItem?.id || null
|
||||||
if (!libraryItemId) {
|
if (!libraryItemId) {
|
||||||
Logger.error(`[Playlist] Invalid playlist media item - No library item id found`, JSON.stringify(pmi, null, 2))
|
Logger.error(`[Playlist] Invalid playlist media item - No library item id found`, JSON.stringify(pmi, null, 2))
|
||||||
return null
|
return null
|
||||||
|
|
@ -57,7 +59,8 @@ class Playlist extends Model {
|
||||||
episodeId: pmi.mediaItemType === 'podcastEpisode' ? pmi.mediaItemId : '',
|
episodeId: pmi.mediaItemType === 'podcastEpisode' ? pmi.mediaItemId : '',
|
||||||
libraryItemId
|
libraryItemId
|
||||||
}
|
}
|
||||||
}).filter(pmi => pmi)
|
})
|
||||||
|
.filter((pmi) => pmi)
|
||||||
|
|
||||||
return new oldPlaylist({
|
return new oldPlaylist({
|
||||||
id: playlistExpanded.id,
|
id: playlistExpanded.id,
|
||||||
|
|
@ -77,7 +80,8 @@ class Playlist extends Model {
|
||||||
* @returns {Promise<object>} oldPlaylist.toJSONExpanded
|
* @returns {Promise<object>} oldPlaylist.toJSONExpanded
|
||||||
*/
|
*/
|
||||||
async getOldJsonExpanded(include) {
|
async getOldJsonExpanded(include) {
|
||||||
this.playlistMediaItems = await this.getPlaylistMediaItems({
|
this.playlistMediaItems =
|
||||||
|
(await this.getPlaylistMediaItems({
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: this.sequelize.models.book,
|
model: this.sequelize.models.book,
|
||||||
|
|
@ -92,10 +96,10 @@ class Playlist extends Model {
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
order: [['order', 'ASC']]
|
order: [['order', 'ASC']]
|
||||||
}) || []
|
})) || []
|
||||||
|
|
||||||
const oldPlaylist = this.sequelize.models.playlist.getOldPlaylist(this)
|
const oldPlaylist = this.sequelize.models.playlist.getOldPlaylist(this)
|
||||||
const libraryItemIds = oldPlaylist.items.map(i => i.libraryItemId)
|
const libraryItemIds = oldPlaylist.items.map((i) => i.libraryItemId)
|
||||||
|
|
||||||
let libraryItems = await this.sequelize.models.libraryItem.getAllOldLibraryItems({
|
let libraryItems = await this.sequelize.models.libraryItem.getAllOldLibraryItems({
|
||||||
id: libraryItemIds
|
id: libraryItemIds
|
||||||
|
|
@ -167,12 +171,13 @@ class Playlist extends Model {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get playlists for user and optionally for library
|
* Get old playlists for user and optionally for library
|
||||||
|
*
|
||||||
* @param {string} userId
|
* @param {string} userId
|
||||||
* @param {[string]} libraryId optional
|
* @param {string} [libraryId]
|
||||||
* @returns {Promise<Playlist[]>}
|
* @returns {Promise<oldPlaylist[]>}
|
||||||
*/
|
*/
|
||||||
static async getPlaylistsForUserAndLibrary(userId, libraryId = null) {
|
static async getOldPlaylistsForUserAndLibrary(userId, libraryId = null) {
|
||||||
if (!userId && !libraryId) return []
|
if (!userId && !libraryId) return []
|
||||||
const whereQuery = {}
|
const whereQuery = {}
|
||||||
if (userId) {
|
if (userId) {
|
||||||
|
|
@ -181,7 +186,7 @@ class Playlist extends Model {
|
||||||
if (libraryId) {
|
if (libraryId) {
|
||||||
whereQuery.libraryId = libraryId
|
whereQuery.libraryId = libraryId
|
||||||
}
|
}
|
||||||
const playlists = await this.findAll({
|
const playlistsExpanded = await this.findAll({
|
||||||
where: whereQuery,
|
where: whereQuery,
|
||||||
include: {
|
include: {
|
||||||
model: this.sequelize.models.playlistMediaItem,
|
model: this.sequelize.models.playlistMediaItem,
|
||||||
|
|
@ -204,7 +209,37 @@ class Playlist extends Model {
|
||||||
['playlistMediaItems', 'order', 'ASC']
|
['playlistMediaItems', 'order', 'ASC']
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
return playlists
|
|
||||||
|
const oldPlaylists = []
|
||||||
|
for (const playlistExpanded of playlistsExpanded) {
|
||||||
|
const oldPlaylist = this.getOldPlaylist(playlistExpanded)
|
||||||
|
const libraryItems = []
|
||||||
|
for (const pmi of playlistExpanded.playlistMediaItems) {
|
||||||
|
let mediaItem = pmi.mediaItem || pmi.dataValues.mediaItem
|
||||||
|
|
||||||
|
if (!mediaItem) {
|
||||||
|
Logger.error(`[Playlist] Invalid playlist media item - No media item found`, JSON.stringify(mediaItem, null, 2))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let libraryItem = mediaItem.libraryItem || mediaItem.podcast?.libraryItem
|
||||||
|
|
||||||
|
if (mediaItem.podcast) {
|
||||||
|
libraryItem.media = mediaItem.podcast
|
||||||
|
libraryItem.media.podcastEpisodes = [mediaItem]
|
||||||
|
delete mediaItem.podcast.libraryItem
|
||||||
|
} else {
|
||||||
|
libraryItem.media = mediaItem
|
||||||
|
delete mediaItem.libraryItem
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldLibraryItem = this.sequelize.models.libraryItem.getOldLibraryItem(libraryItem)
|
||||||
|
libraryItems.push(oldLibraryItem)
|
||||||
|
}
|
||||||
|
const oldPlaylistJson = oldPlaylist.toJSONExpanded(libraryItems)
|
||||||
|
oldPlaylists.push(oldPlaylistJson)
|
||||||
|
}
|
||||||
|
|
||||||
|
return oldPlaylists
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -263,9 +298,9 @@ class Playlist extends Model {
|
||||||
const playlists = []
|
const playlists = []
|
||||||
for (const playlistMediaItem of playlistMediaItemsExpanded) {
|
for (const playlistMediaItem of playlistMediaItemsExpanded) {
|
||||||
const playlist = playlistMediaItem.playlist
|
const playlist = playlistMediaItem.playlist
|
||||||
if (playlists.some(p => p.id === playlist.id)) continue
|
if (playlists.some((p) => p.id === playlist.id)) continue
|
||||||
|
|
||||||
playlist.playlistMediaItems = playlist.playlistMediaItems.map(pmi => {
|
playlist.playlistMediaItems = playlist.playlistMediaItems.map((pmi) => {
|
||||||
if (pmi.mediaItemType === 'book' && pmi.book !== undefined) {
|
if (pmi.mediaItemType === 'book' && pmi.book !== undefined) {
|
||||||
pmi.mediaItem = pmi.book
|
pmi.mediaItem = pmi.book
|
||||||
pmi.dataValues.mediaItem = pmi.dataValues.book
|
pmi.dataValues.mediaItem = pmi.dataValues.book
|
||||||
|
|
@ -289,7 +324,8 @@ class Playlist extends Model {
|
||||||
* @param {import('../Database').sequelize} sequelize
|
* @param {import('../Database').sequelize} sequelize
|
||||||
*/
|
*/
|
||||||
static init(sequelize) {
|
static init(sequelize) {
|
||||||
super.init({
|
super.init(
|
||||||
|
{
|
||||||
id: {
|
id: {
|
||||||
type: DataTypes.UUID,
|
type: DataTypes.UUID,
|
||||||
defaultValue: DataTypes.UUIDV4,
|
defaultValue: DataTypes.UUIDV4,
|
||||||
|
|
@ -297,10 +333,12 @@ class Playlist extends Model {
|
||||||
},
|
},
|
||||||
name: DataTypes.STRING,
|
name: DataTypes.STRING,
|
||||||
description: DataTypes.TEXT
|
description: DataTypes.TEXT
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
sequelize,
|
sequelize,
|
||||||
modelName: 'playlist'
|
modelName: 'playlist'
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const { library, user } = sequelize.models
|
const { library, user } = sequelize.models
|
||||||
library.hasMany(Playlist)
|
library.hasMany(Playlist)
|
||||||
|
|
@ -311,14 +349,14 @@ class Playlist extends Model {
|
||||||
})
|
})
|
||||||
Playlist.belongsTo(user)
|
Playlist.belongsTo(user)
|
||||||
|
|
||||||
Playlist.addHook('afterFind', findResult => {
|
Playlist.addHook('afterFind', (findResult) => {
|
||||||
if (!findResult) return
|
if (!findResult) return
|
||||||
|
|
||||||
if (!Array.isArray(findResult)) findResult = [findResult]
|
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||||
|
|
||||||
for (const instance of findResult) {
|
for (const instance of findResult) {
|
||||||
if (instance.playlistMediaItems?.length) {
|
if (instance.playlistMediaItems?.length) {
|
||||||
instance.playlistMediaItems = instance.playlistMediaItems.map(pmi => {
|
instance.playlistMediaItems = instance.playlistMediaItems.map((pmi) => {
|
||||||
if (pmi.mediaItemType === 'book' && pmi.book !== undefined) {
|
if (pmi.mediaItemType === 'book' && pmi.book !== undefined) {
|
||||||
pmi.mediaItem = pmi.book
|
pmi.mediaItem = pmi.book
|
||||||
pmi.dataValues.mediaItem = pmi.dataValues.book
|
pmi.dataValues.mediaItem = pmi.dataValues.book
|
||||||
|
|
@ -334,7 +372,6 @@ class Playlist extends Model {
|
||||||
return pmi
|
return pmi
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ class LibrarySettings {
|
||||||
this.skipMatchingMediaWithIsbn = false
|
this.skipMatchingMediaWithIsbn = false
|
||||||
this.autoScanCronExpression = null
|
this.autoScanCronExpression = null
|
||||||
this.audiobooksOnly = false
|
this.audiobooksOnly = false
|
||||||
|
this.epubsAllowScriptedContent = false
|
||||||
this.hideSingleBookSeries = false // Do not show series that only have 1 book
|
this.hideSingleBookSeries = false // Do not show series that only have 1 book
|
||||||
this.onlyShowLaterBooksInContinueSeries = false // Skip showing books that are earlier than the max sequence read
|
this.onlyShowLaterBooksInContinueSeries = false // Skip showing books that are earlier than the max sequence read
|
||||||
this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
|
this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
|
||||||
|
|
@ -25,6 +26,7 @@ class LibrarySettings {
|
||||||
this.skipMatchingMediaWithIsbn = !!settings.skipMatchingMediaWithIsbn
|
this.skipMatchingMediaWithIsbn = !!settings.skipMatchingMediaWithIsbn
|
||||||
this.autoScanCronExpression = settings.autoScanCronExpression || null
|
this.autoScanCronExpression = settings.autoScanCronExpression || null
|
||||||
this.audiobooksOnly = !!settings.audiobooksOnly
|
this.audiobooksOnly = !!settings.audiobooksOnly
|
||||||
|
this.epubsAllowScriptedContent = !!settings.epubsAllowScriptedContent
|
||||||
this.hideSingleBookSeries = !!settings.hideSingleBookSeries
|
this.hideSingleBookSeries = !!settings.hideSingleBookSeries
|
||||||
this.onlyShowLaterBooksInContinueSeries = !!settings.onlyShowLaterBooksInContinueSeries
|
this.onlyShowLaterBooksInContinueSeries = !!settings.onlyShowLaterBooksInContinueSeries
|
||||||
if (settings.metadataPrecedence) {
|
if (settings.metadataPrecedence) {
|
||||||
|
|
@ -44,6 +46,7 @@ class LibrarySettings {
|
||||||
skipMatchingMediaWithIsbn: this.skipMatchingMediaWithIsbn,
|
skipMatchingMediaWithIsbn: this.skipMatchingMediaWithIsbn,
|
||||||
autoScanCronExpression: this.autoScanCronExpression,
|
autoScanCronExpression: this.autoScanCronExpression,
|
||||||
audiobooksOnly: this.audiobooksOnly,
|
audiobooksOnly: this.audiobooksOnly,
|
||||||
|
epubsAllowScriptedContent: this.epubsAllowScriptedContent,
|
||||||
hideSingleBookSeries: this.hideSingleBookSeries,
|
hideSingleBookSeries: this.hideSingleBookSeries,
|
||||||
onlyShowLaterBooksInContinueSeries: this.onlyShowLaterBooksInContinueSeries,
|
onlyShowLaterBooksInContinueSeries: this.onlyShowLaterBooksInContinueSeries,
|
||||||
metadataPrecedence: [...this.metadataPrecedence],
|
metadataPrecedence: [...this.metadataPrecedence],
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,22 @@
|
||||||
const axios = require('axios')
|
const axios = require('axios').default
|
||||||
const htmlSanitizer = require('../utils/htmlSanitizer')
|
const htmlSanitizer = require('../utils/htmlSanitizer')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
class Audible {
|
class Audible {
|
||||||
|
#responseTimeout = 30000
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.regionMap = {
|
this.regionMap = {
|
||||||
'us': '.com',
|
us: '.com',
|
||||||
'ca': '.ca',
|
ca: '.ca',
|
||||||
'uk': '.co.uk',
|
uk: '.co.uk',
|
||||||
'au': '.com.au',
|
au: '.com.au',
|
||||||
'fr': '.fr',
|
fr: '.fr',
|
||||||
'de': '.de',
|
de: '.de',
|
||||||
'jp': '.co.jp',
|
jp: '.co.jp',
|
||||||
'it': '.it',
|
it: '.it',
|
||||||
'in': '.in',
|
in: '.in',
|
||||||
'es': '.es'
|
es: '.es'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,8 +57,8 @@ class Audible {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const genresFiltered = genres ? genres.filter(g => g.type == "genre").map(g => g.name) : []
|
const genresFiltered = genres ? genres.filter((g) => g.type == 'genre').map((g) => g.name) : []
|
||||||
const tagsFiltered = genres ? genres.filter(g => g.type == "tag").map(g => g.name) : []
|
const tagsFiltered = genres ? genres.filter((g) => g.type == 'tag').map((g) => g.name) : []
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
|
|
@ -89,34 +91,58 @@ class Audible {
|
||||||
return /^[0-9A-Za-z]{10}$/.test(title)
|
return /^[0-9A-Za-z]{10}$/.test(title)
|
||||||
}
|
}
|
||||||
|
|
||||||
asinSearch(asin, region) {
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} asin
|
||||||
|
* @param {string} region
|
||||||
|
* @param {number} [timeout] response timeout in ms
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
*/
|
||||||
|
asinSearch(asin, region, timeout = this.#responseTimeout) {
|
||||||
if (!asin) return []
|
if (!asin) return []
|
||||||
|
if (!timeout || isNaN(timeout)) timeout = this.#responseTimeout
|
||||||
|
|
||||||
asin = encodeURIComponent(asin.toUpperCase())
|
asin = encodeURIComponent(asin.toUpperCase())
|
||||||
var regionQuery = region ? `?region=${region}` : ''
|
var regionQuery = region ? `?region=${region}` : ''
|
||||||
var url = `https://api.audnex.us/books/${asin}${regionQuery}`
|
var url = `https://api.audnex.us/books/${asin}${regionQuery}`
|
||||||
Logger.debug(`[Audible] ASIN url: ${url}`)
|
Logger.debug(`[Audible] ASIN url: ${url}`)
|
||||||
return axios.get(url).then((res) => {
|
return axios
|
||||||
|
.get(url, {
|
||||||
|
timeout
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
if (!res || !res.data || !res.data.asin) return null
|
if (!res || !res.data || !res.data.asin) return null
|
||||||
return res.data
|
return res.data
|
||||||
}).catch(error => {
|
})
|
||||||
|
.catch((error) => {
|
||||||
Logger.error('[Audible] ASIN search error', error)
|
Logger.error('[Audible] ASIN search error', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(title, author, asin, region) {
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} title
|
||||||
|
* @param {string} author
|
||||||
|
* @param {string} asin
|
||||||
|
* @param {string} region
|
||||||
|
* @param {number} [timeout] response timeout in ms
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
*/
|
||||||
|
async search(title, author, asin, region, timeout = this.#responseTimeout) {
|
||||||
if (region && !this.regionMap[region]) {
|
if (region && !this.regionMap[region]) {
|
||||||
Logger.error(`[Audible] search: Invalid region ${region}`)
|
Logger.error(`[Audible] search: Invalid region ${region}`)
|
||||||
region = ''
|
region = ''
|
||||||
}
|
}
|
||||||
|
if (!timeout || isNaN(timeout)) timeout = this.#responseTimeout
|
||||||
|
|
||||||
let items
|
let items
|
||||||
if (asin) {
|
if (asin) {
|
||||||
items = [await this.asinSearch(asin, region)]
|
items = [await this.asinSearch(asin, region, timeout)]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!items && this.isProbablyAsin(title)) {
|
if (!items && this.isProbablyAsin(title)) {
|
||||||
items = [await this.asinSearch(title, region)]
|
items = [await this.asinSearch(title, region, timeout)]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!items) {
|
if (!items) {
|
||||||
|
|
@ -126,19 +152,24 @@ class Audible {
|
||||||
title: title
|
title: title
|
||||||
}
|
}
|
||||||
if (author) queryObj.author = author
|
if (author) queryObj.author = author
|
||||||
const queryString = (new URLSearchParams(queryObj)).toString()
|
const queryString = new URLSearchParams(queryObj).toString()
|
||||||
const tld = region ? this.regionMap[region] : '.com'
|
const tld = region ? this.regionMap[region] : '.com'
|
||||||
const url = `https://api.audible${tld}/1.0/catalog/products?${queryString}`
|
const url = `https://api.audible${tld}/1.0/catalog/products?${queryString}`
|
||||||
Logger.debug(`[Audible] Search url: ${url}`)
|
Logger.debug(`[Audible] Search url: ${url}`)
|
||||||
items = await axios.get(url).then((res) => {
|
items = await axios
|
||||||
|
.get(url, {
|
||||||
|
timeout
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
if (!res?.data?.products) return null
|
if (!res?.data?.products) return null
|
||||||
return Promise.all(res.data.products.map(result => this.asinSearch(result.asin, region)))
|
return Promise.all(res.data.products.map((result) => this.asinSearch(result.asin, region, timeout)))
|
||||||
}).catch(error => {
|
})
|
||||||
|
.catch((error) => {
|
||||||
Logger.error('[Audible] query search error', error)
|
Logger.error('[Audible] query search error', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return items ? items.map(item => this.cleanResult(item)) : []
|
return items?.map((item) => this.cleanResult(item)) || []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,32 @@ const axios = require('axios')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
class AudiobookCovers {
|
class AudiobookCovers {
|
||||||
constructor() { }
|
#responseTimeout = 30000
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} search
|
||||||
|
* @param {number} [timeout]
|
||||||
|
* @returns {Promise<{cover: string}[]>}
|
||||||
|
*/
|
||||||
|
async search(search, timeout = this.#responseTimeout) {
|
||||||
|
if (!timeout || isNaN(timeout)) timeout = this.#responseTimeout
|
||||||
|
|
||||||
async search(search) {
|
|
||||||
const url = `https://api.audiobookcovers.com/cover/bytext/`
|
const url = `https://api.audiobookcovers.com/cover/bytext/`
|
||||||
const params = new URLSearchParams([['q', search]])
|
const params = new URLSearchParams([['q', search]])
|
||||||
const items = await axios.get(url, { params }).then((res) => {
|
const items = await axios
|
||||||
if (!res || !res.data) return []
|
.get(url, {
|
||||||
return res.data
|
params,
|
||||||
}).catch(error => {
|
timeout
|
||||||
|
})
|
||||||
|
.then((res) => res?.data || [])
|
||||||
|
.catch((error) => {
|
||||||
Logger.error('[AudiobookCovers] Cover search error', error)
|
Logger.error('[AudiobookCovers] Cover search error', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
return items.map(item => ({ cover: item.versions.png.original }))
|
return items.map((item) => ({ cover: item.versions.png.original }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = AudiobookCovers
|
module.exports = AudiobookCovers
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const axios = require('axios')
|
const axios = require('axios').default
|
||||||
const { levenshteinDistance } = require('../utils/index')
|
const { levenshteinDistance } = require('../utils/index')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
|
|
@ -27,9 +27,12 @@ class Audnexus {
|
||||||
if (region) searchParams.set('region', region)
|
if (region) searchParams.set('region', region)
|
||||||
const authorRequestUrl = `${this.baseUrl}/authors?${searchParams.toString()}`
|
const authorRequestUrl = `${this.baseUrl}/authors?${searchParams.toString()}`
|
||||||
Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`)
|
Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`)
|
||||||
return axios.get(authorRequestUrl).then((res) => {
|
return axios
|
||||||
|
.get(authorRequestUrl)
|
||||||
|
.then((res) => {
|
||||||
return res.data || []
|
return res.data || []
|
||||||
}).catch((error) => {
|
})
|
||||||
|
.catch((error) => {
|
||||||
Logger.error(`[Audnexus] Author ASIN request failed for ${name}`, error)
|
Logger.error(`[Audnexus] Author ASIN request failed for ${name}`, error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
|
|
@ -46,9 +49,12 @@ class Audnexus {
|
||||||
const regionQuery = region ? `?region=${region}` : ''
|
const regionQuery = region ? `?region=${region}` : ''
|
||||||
const authorRequestUrl = `${this.baseUrl}/authors/${asin}${regionQuery}`
|
const authorRequestUrl = `${this.baseUrl}/authors/${asin}${regionQuery}`
|
||||||
Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`)
|
Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`)
|
||||||
return axios.get(authorRequestUrl).then((res) => {
|
return axios
|
||||||
|
.get(authorRequestUrl)
|
||||||
|
.then((res) => {
|
||||||
return res.data
|
return res.data
|
||||||
}).catch((error) => {
|
})
|
||||||
|
.catch((error) => {
|
||||||
Logger.error(`[Audnexus] Author request failed for ${asin}`, error)
|
Logger.error(`[Audnexus] Author request failed for ${asin}`, error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
@ -108,9 +114,12 @@ class Audnexus {
|
||||||
|
|
||||||
getChaptersByASIN(asin, region) {
|
getChaptersByASIN(asin, region) {
|
||||||
Logger.debug(`[Audnexus] Get chapters for ASIN ${asin}/${region}`)
|
Logger.debug(`[Audnexus] Get chapters for ASIN ${asin}/${region}`)
|
||||||
return axios.get(`${this.baseUrl}/books/${asin}/chapters?region=${region}`).then((res) => {
|
return axios
|
||||||
|
.get(`${this.baseUrl}/books/${asin}/chapters?region=${region}`)
|
||||||
|
.then((res) => {
|
||||||
return res.data
|
return res.data
|
||||||
}).catch((error) => {
|
})
|
||||||
|
.catch((error) => {
|
||||||
Logger.error(`[Audnexus] Chapter ASIN request failed for ${asin}/${region}`, error)
|
Logger.error(`[Audnexus] Chapter ASIN request failed for ${asin}/${region}`, error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
|
const axios = require('axios').default
|
||||||
const Database = require('../Database')
|
const Database = require('../Database')
|
||||||
const axios = require('axios')
|
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
class CustomProviderAdapter {
|
class CustomProviderAdapter {
|
||||||
constructor() { }
|
#responseTimeout = 30000
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
|
@ -12,14 +14,17 @@ class CustomProviderAdapter {
|
||||||
* @param {string} isbn
|
* @param {string} isbn
|
||||||
* @param {string} providerSlug
|
* @param {string} providerSlug
|
||||||
* @param {string} mediaType
|
* @param {string} mediaType
|
||||||
|
* @param {number} [timeout] response timeout in ms
|
||||||
* @returns {Promise<Object[]>}
|
* @returns {Promise<Object[]>}
|
||||||
*/
|
*/
|
||||||
async search(title, author, isbn, providerSlug, mediaType) {
|
async search(title, author, isbn, providerSlug, mediaType, timeout = this.#responseTimeout) {
|
||||||
|
if (!timeout || isNaN(timeout)) timeout = this.#responseTimeout
|
||||||
|
|
||||||
const providerId = providerSlug.split('custom-')[1]
|
const providerId = providerSlug.split('custom-')[1]
|
||||||
const provider = await Database.customMetadataProviderModel.findByPk(providerId)
|
const provider = await Database.customMetadataProviderModel.findByPk(providerId)
|
||||||
|
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
throw new Error("Custom provider not found for the given id")
|
throw new Error('Custom provider not found for the given id')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup query params
|
// Setup query params
|
||||||
|
|
@ -33,46 +38,35 @@ class CustomProviderAdapter {
|
||||||
if (isbn) {
|
if (isbn) {
|
||||||
queryObj.isbn = isbn
|
queryObj.isbn = isbn
|
||||||
}
|
}
|
||||||
const queryString = (new URLSearchParams(queryObj)).toString()
|
const queryString = new URLSearchParams(queryObj).toString()
|
||||||
|
|
||||||
// Setup headers
|
// Setup headers
|
||||||
const axiosOptions = {}
|
const axiosOptions = {
|
||||||
|
timeout
|
||||||
|
}
|
||||||
if (provider.authHeaderValue) {
|
if (provider.authHeaderValue) {
|
||||||
axiosOptions.headers = {
|
axiosOptions.headers = {
|
||||||
'Authorization': provider.authHeaderValue
|
Authorization: provider.authHeaderValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const matches = await axios.get(`${provider.url}/search?${queryString}`, axiosOptions).then((res) => {
|
const matches = await axios
|
||||||
|
.get(`${provider.url}/search?${queryString}`, axiosOptions)
|
||||||
|
.then((res) => {
|
||||||
if (!res?.data || !Array.isArray(res.data.matches)) return null
|
if (!res?.data || !Array.isArray(res.data.matches)) return null
|
||||||
return res.data.matches
|
return res.data.matches
|
||||||
}).catch(error => {
|
})
|
||||||
|
.catch((error) => {
|
||||||
Logger.error('[CustomMetadataProvider] Search error', error)
|
Logger.error('[CustomMetadataProvider] Search error', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!matches) {
|
if (!matches) {
|
||||||
throw new Error("Custom provider returned malformed response")
|
throw new Error('Custom provider returned malformed response')
|
||||||
}
|
}
|
||||||
|
|
||||||
// re-map keys to throw out
|
// re-map keys to throw out
|
||||||
return matches.map(({
|
return matches.map(({ title, subtitle, author, narrator, publisher, publishedYear, description, cover, isbn, asin, genres, tags, series, language, duration }) => {
|
||||||
title,
|
|
||||||
subtitle,
|
|
||||||
author,
|
|
||||||
narrator,
|
|
||||||
publisher,
|
|
||||||
publishedYear,
|
|
||||||
description,
|
|
||||||
cover,
|
|
||||||
isbn,
|
|
||||||
asin,
|
|
||||||
genres,
|
|
||||||
tags,
|
|
||||||
series,
|
|
||||||
language,
|
|
||||||
duration
|
|
||||||
}) => {
|
|
||||||
return {
|
return {
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ const axios = require('axios')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
class FantLab {
|
class FantLab {
|
||||||
|
#responseTimeout = 30000
|
||||||
// 7 - other
|
// 7 - other
|
||||||
// 11 - essay
|
// 11 - essay
|
||||||
// 12 - article
|
// 12 - article
|
||||||
|
|
@ -22,28 +23,47 @@ class FantLab {
|
||||||
_filterWorkType = [7, 11, 12, 22, 23, 24, 25, 26, 46, 47, 49, 51, 52, 55, 56, 57]
|
_filterWorkType = [7, 11, 12, 22, 23, 24, 25, 26, 46, 47, 49, 51, 52, 55, 56, 57]
|
||||||
_baseUrl = 'https://api.fantlab.ru'
|
_baseUrl = 'https://api.fantlab.ru'
|
||||||
|
|
||||||
constructor() { }
|
constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} title
|
||||||
|
* @param {string} author'
|
||||||
|
* @param {number} [timeout] response timeout in ms
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
**/
|
||||||
|
async search(title, author, timeout = this.#responseTimeout) {
|
||||||
|
if (!timeout || isNaN(timeout)) timeout = this.#responseTimeout
|
||||||
|
|
||||||
async search(title, author) {
|
|
||||||
let searchString = encodeURIComponent(title)
|
let searchString = encodeURIComponent(title)
|
||||||
if (author) {
|
if (author) {
|
||||||
searchString += encodeURIComponent(' ' + author)
|
searchString += encodeURIComponent(' ' + author)
|
||||||
}
|
}
|
||||||
const url = `${this._baseUrl}/search-works?q=${searchString}&page=1&onlymatches=1`
|
const url = `${this._baseUrl}/search-works?q=${searchString}&page=1&onlymatches=1`
|
||||||
Logger.debug(`[FantLab] Search url: ${url}`)
|
Logger.debug(`[FantLab] Search url: ${url}`)
|
||||||
const items = await axios.get(url).then((res) => {
|
const items = await axios
|
||||||
|
.get(url, {
|
||||||
|
timeout
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
return res.data || []
|
return res.data || []
|
||||||
}).catch(error => {
|
})
|
||||||
|
.catch((error) => {
|
||||||
Logger.error('[FantLab] search error', error)
|
Logger.error('[FantLab] search error', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
|
|
||||||
return Promise.all(items.map(async item => await this.getWork(item))).then(resArray => {
|
return Promise.all(items.map(async (item) => await this.getWork(item, timeout))).then((resArray) => {
|
||||||
return resArray.filter(res => res)
|
return resArray.filter((res) => res)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getWork(item) {
|
/**
|
||||||
|
* @param {Object} item
|
||||||
|
* @param {number} [timeout] response timeout in ms
|
||||||
|
* @returns {Promise<Object>}
|
||||||
|
**/
|
||||||
|
async getWork(item, timeout = this.#responseTimeout) {
|
||||||
|
if (!timeout || isNaN(timeout)) timeout = this.#responseTimeout
|
||||||
const { work_id, work_type_id } = item
|
const { work_id, work_type_id } = item
|
||||||
|
|
||||||
if (this._filterWorkType.includes(work_type_id)) {
|
if (this._filterWorkType.includes(work_type_id)) {
|
||||||
|
|
@ -51,23 +71,34 @@ class FantLab {
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `${this._baseUrl}/work/${work_id}/extended`
|
const url = `${this._baseUrl}/work/${work_id}/extended`
|
||||||
const bookData = await axios.get(url).then((resp) => {
|
const bookData = await axios
|
||||||
|
.get(url, {
|
||||||
|
timeout
|
||||||
|
})
|
||||||
|
.then((resp) => {
|
||||||
return resp.data || null
|
return resp.data || null
|
||||||
}).catch((error) => {
|
})
|
||||||
|
.catch((error) => {
|
||||||
Logger.error(`[FantLab] work info request for url "${url}" error`, error)
|
Logger.error(`[FantLab] work info request for url "${url}" error`, error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
||||||
return this.cleanBookData(bookData)
|
return this.cleanBookData(bookData, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanBookData(bookData) {
|
/**
|
||||||
|
*
|
||||||
|
* @param {Object} bookData
|
||||||
|
* @param {number} [timeout]
|
||||||
|
* @returns {Promise<Object>}
|
||||||
|
*/
|
||||||
|
async cleanBookData(bookData, timeout = this.#responseTimeout) {
|
||||||
let { authors, work_name_alts, work_id, work_name, work_year, work_description, image, classificatory, editions_blocks } = bookData
|
let { authors, work_name_alts, work_id, work_name, work_year, work_description, image, classificatory, editions_blocks } = bookData
|
||||||
|
|
||||||
const subtitle = Array.isArray(work_name_alts) ? work_name_alts[0] : null
|
const subtitle = Array.isArray(work_name_alts) ? work_name_alts[0] : null
|
||||||
const authorNames = authors.map(au => (au.name || '').trim()).filter(au => au)
|
const authorNames = authors.map((au) => (au.name || '').trim()).filter((au) => au)
|
||||||
|
|
||||||
const imageAndIsbn = await this.tryGetCoverFromEditions(editions_blocks)
|
const imageAndIsbn = await this.tryGetCoverFromEditions(editions_blocks, timeout)
|
||||||
|
|
||||||
const imageToUse = imageAndIsbn?.imageUrl || image
|
const imageToUse = imageAndIsbn?.imageUrl || image
|
||||||
|
|
||||||
|
|
@ -88,7 +119,7 @@ class FantLab {
|
||||||
tryGetGenres(classificatory) {
|
tryGetGenres(classificatory) {
|
||||||
if (!classificatory || !classificatory.genre_group) return []
|
if (!classificatory || !classificatory.genre_group) return []
|
||||||
|
|
||||||
const genresGroup = classificatory.genre_group.find(group => group.genre_group_id == 1) // genres and subgenres
|
const genresGroup = classificatory.genre_group.find((group) => group.genre_group_id == 1) // genres and subgenres
|
||||||
|
|
||||||
// genre_group_id=2 - General Characteristics
|
// genre_group_id=2 - General Characteristics
|
||||||
// genre_group_id=3 - Arena
|
// genre_group_id=3 - Arena
|
||||||
|
|
@ -108,10 +139,16 @@ class FantLab {
|
||||||
|
|
||||||
tryGetSubGenres(rootGenre) {
|
tryGetSubGenres(rootGenre) {
|
||||||
if (!rootGenre.genre || !rootGenre.genre.length) return []
|
if (!rootGenre.genre || !rootGenre.genre.length) return []
|
||||||
return rootGenre.genre.map(g => g.label).filter(g => g)
|
return rootGenre.genre.map((g) => g.label).filter((g) => g)
|
||||||
}
|
}
|
||||||
|
|
||||||
async tryGetCoverFromEditions(editions) {
|
/**
|
||||||
|
*
|
||||||
|
* @param {Object} editions
|
||||||
|
* @param {number} [timeout]
|
||||||
|
* @returns {Promise<{imageUrl: string, isbn: string}>
|
||||||
|
*/
|
||||||
|
async tryGetCoverFromEditions(editions, timeout = this.#responseTimeout) {
|
||||||
if (!editions) {
|
if (!editions) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
@ -129,18 +166,31 @@ class FantLab {
|
||||||
const isbn = lastEdition['isbn'] || null // get only from paper edition
|
const isbn = lastEdition['isbn'] || null // get only from paper edition
|
||||||
|
|
||||||
return {
|
return {
|
||||||
imageUrl: await this.getCoverFromEdition(editionId),
|
imageUrl: await this.getCoverFromEdition(editionId, timeout),
|
||||||
isbn
|
isbn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCoverFromEdition(editionId) {
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} editionId
|
||||||
|
* @param {number} [timeout]
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
async getCoverFromEdition(editionId, timeout = this.#responseTimeout) {
|
||||||
if (!editionId) return null
|
if (!editionId) return null
|
||||||
|
if (!timeout || isNaN(timeout)) timeout = this.#responseTimeout
|
||||||
|
|
||||||
const url = `${this._baseUrl}/edition/${editionId}`
|
const url = `${this._baseUrl}/edition/${editionId}`
|
||||||
|
|
||||||
const editionInfo = await axios.get(url).then((resp) => {
|
const editionInfo = await axios
|
||||||
|
.get(url, {
|
||||||
|
timeout
|
||||||
|
})
|
||||||
|
.then((resp) => {
|
||||||
return resp.data || null
|
return resp.data || null
|
||||||
}).catch(error => {
|
})
|
||||||
|
.catch((error) => {
|
||||||
Logger.error(`[FantLab] search cover from edition with url "${url}" error`, error)
|
Logger.error(`[FantLab] search cover from edition with url "${url}" error`, error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,14 @@ const axios = require('axios')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
class GoogleBooks {
|
class GoogleBooks {
|
||||||
constructor() { }
|
#responseTimeout = 30000
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
extractIsbn(industryIdentifiers) {
|
extractIsbn(industryIdentifiers) {
|
||||||
if (!industryIdentifiers || !industryIdentifiers.length) return null
|
if (!industryIdentifiers || !industryIdentifiers.length) return null
|
||||||
|
|
||||||
var isbnObj = industryIdentifiers.find(i => i.type === 'ISBN_13') || industryIdentifiers.find(i => i.type === 'ISBN_10')
|
var isbnObj = industryIdentifiers.find((i) => i.type === 'ISBN_13') || industryIdentifiers.find((i) => i.type === 'ISBN_10')
|
||||||
if (isbnObj && isbnObj.identifier) return isbnObj.identifier
|
if (isbnObj && isbnObj.identifier) return isbnObj.identifier
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
@ -38,23 +40,37 @@ class GoogleBooks {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(title, author) {
|
/**
|
||||||
|
* Search for a book by title and author
|
||||||
|
* @param {string} title
|
||||||
|
* @param {string} author
|
||||||
|
* @param {number} [timeout] response timeout in ms
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
**/
|
||||||
|
async search(title, author, timeout = this.#responseTimeout) {
|
||||||
|
if (!timeout || isNaN(timeout)) timeout = this.#responseTimeout
|
||||||
|
|
||||||
title = encodeURIComponent(title)
|
title = encodeURIComponent(title)
|
||||||
var queryString = `q=intitle:${title}`
|
let queryString = `q=intitle:${title}`
|
||||||
if (author) {
|
if (author) {
|
||||||
author = encodeURIComponent(author)
|
author = encodeURIComponent(author)
|
||||||
queryString += `+inauthor:${author}`
|
queryString += `+inauthor:${author}`
|
||||||
}
|
}
|
||||||
var url = `https://www.googleapis.com/books/v1/volumes?${queryString}`
|
const url = `https://www.googleapis.com/books/v1/volumes?${queryString}`
|
||||||
Logger.debug(`[GoogleBooks] Search url: ${url}`)
|
Logger.debug(`[GoogleBooks] Search url: ${url}`)
|
||||||
var items = await axios.get(url).then((res) => {
|
const items = await axios
|
||||||
|
.get(url, {
|
||||||
|
timeout
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
if (!res || !res.data || !res.data.items) return []
|
if (!res || !res.data || !res.data.items) return []
|
||||||
return res.data.items
|
return res.data.items
|
||||||
}).catch(error => {
|
})
|
||||||
|
.catch((error) => {
|
||||||
Logger.error('[GoogleBooks] Volume search error', error)
|
Logger.error('[GoogleBooks] Volume search error', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
return items.map(item => this.cleanResult(item))
|
return items.map((item) => this.cleanResult(item))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,30 @@
|
||||||
var axios = require('axios')
|
const axios = require('axios').default
|
||||||
|
|
||||||
class OpenLibrary {
|
class OpenLibrary {
|
||||||
|
#responseTimeout = 30000
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.baseUrl = 'https://openlibrary.org'
|
this.baseUrl = 'https://openlibrary.org'
|
||||||
}
|
}
|
||||||
|
|
||||||
get(uri) {
|
/**
|
||||||
return axios.get(`${this.baseUrl}/${uri}`).then((res) => {
|
*
|
||||||
|
* @param {string} uri
|
||||||
|
* @param {number} timeout
|
||||||
|
* @returns {Promise<Object>}
|
||||||
|
*/
|
||||||
|
get(uri, timeout = this.#responseTimeout) {
|
||||||
|
if (!timeout || isNaN(timeout)) timeout = this.#responseTimeout
|
||||||
|
return axios
|
||||||
|
.get(`${this.baseUrl}/${uri}`, {
|
||||||
|
timeout
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
return res.data
|
return res.data
|
||||||
}).catch((error) => {
|
})
|
||||||
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return false
|
return null
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,7 +47,7 @@ class OpenLibrary {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!worksData.covers) worksData.covers = []
|
if (!worksData.covers) worksData.covers = []
|
||||||
var coverImages = worksData.covers.filter(c => c > 0).map(c => `https://covers.openlibrary.org/b/id/${c}-L.jpg`)
|
var coverImages = worksData.covers.filter((c) => c > 0).map((c) => `https://covers.openlibrary.org/b/id/${c}-L.jpg`)
|
||||||
var description = null
|
var description = null
|
||||||
if (worksData.description) {
|
if (worksData.description) {
|
||||||
if (typeof worksData.description === 'string') {
|
if (typeof worksData.description === 'string') {
|
||||||
|
|
@ -73,26 +87,34 @@ class OpenLibrary {
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(query) {
|
async search(query) {
|
||||||
var queryString = Object.keys(query).map(key => key + '=' + query[key]).join('&')
|
var queryString = Object.keys(query)
|
||||||
|
.map((key) => key + '=' + query[key])
|
||||||
|
.join('&')
|
||||||
var lookupData = await this.get(`/search.json?${queryString}`)
|
var lookupData = await this.get(`/search.json?${queryString}`)
|
||||||
if (!lookupData) {
|
if (!lookupData) {
|
||||||
return {
|
return {
|
||||||
errorCode: 404
|
errorCode: 404
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var searchDocs = await Promise.all(lookupData.docs.map(d => this.cleanSearchDoc(d)))
|
var searchDocs = await Promise.all(lookupData.docs.map((d) => this.cleanSearchDoc(d)))
|
||||||
return searchDocs
|
return searchDocs
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchTitle(title) {
|
/**
|
||||||
title = encodeURIComponent(title);
|
*
|
||||||
var lookupData = await this.get(`/search.json?title=${title}`)
|
* @param {string} title
|
||||||
|
* @param {number} timeout
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
*/
|
||||||
|
async searchTitle(title, timeout = this.#responseTimeout) {
|
||||||
|
title = encodeURIComponent(title)
|
||||||
|
var lookupData = await this.get(`/search.json?title=${title}`, timeout)
|
||||||
if (!lookupData) {
|
if (!lookupData) {
|
||||||
return {
|
return {
|
||||||
errorCode: 404
|
errorCode: 404
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var searchDocs = await Promise.all(lookupData.docs.map(d => this.cleanSearchDoc(d)))
|
var searchDocs = await Promise.all(lookupData.docs.map((d) => this.cleanSearchDoc(d)))
|
||||||
return searchDocs
|
return searchDocs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,19 +28,24 @@ const htmlSanitizer = require('../utils/htmlSanitizer')
|
||||||
*/
|
*/
|
||||||
|
|
||||||
class iTunes {
|
class iTunes {
|
||||||
constructor() { }
|
#responseTimeout = 30000
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/iTuneSearchAPI/Searching.html
|
* @see https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/iTuneSearchAPI/Searching.html
|
||||||
*
|
*
|
||||||
* @param {iTunesSearchParams} options
|
* @param {iTunesSearchParams} options
|
||||||
|
* @param {number} [timeout] response timeout in ms
|
||||||
* @returns {Promise<Object[]>}
|
* @returns {Promise<Object[]>}
|
||||||
*/
|
*/
|
||||||
search(options) {
|
search(options, timeout = this.#responseTimeout) {
|
||||||
if (!options.term) {
|
if (!options.term) {
|
||||||
Logger.error('[iTunes] Invalid search options - no term')
|
Logger.error('[iTunes] Invalid search options - no term')
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
if (!timeout || isNaN(timeout)) timeout = this.#responseTimeout
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
term: options.term,
|
term: options.term,
|
||||||
media: options.media,
|
media: options.media,
|
||||||
|
|
@ -49,9 +54,15 @@ class iTunes {
|
||||||
limit: options.limit,
|
limit: options.limit,
|
||||||
country: options.country
|
country: options.country
|
||||||
}
|
}
|
||||||
return axios.get('https://itunes.apple.com/search', { params: query }).then((response) => {
|
return axios
|
||||||
|
.get('https://itunes.apple.com/search', {
|
||||||
|
params: query,
|
||||||
|
timeout
|
||||||
|
})
|
||||||
|
.then((response) => {
|
||||||
return response.data.results || []
|
return response.data.results || []
|
||||||
}).catch((error) => {
|
})
|
||||||
|
.catch((error) => {
|
||||||
Logger.error(`[iTunes] search request error`, error)
|
Logger.error(`[iTunes] search request error`, error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
|
|
@ -65,7 +76,9 @@ class iTunes {
|
||||||
return data.artworkUrl600
|
return data.artworkUrl600
|
||||||
}
|
}
|
||||||
// Should already be sorted from small to large
|
// Should already be sorted from small to large
|
||||||
var artworkSizes = Object.keys(data).filter(key => key.startsWith('artworkUrl')).map(key => {
|
var artworkSizes = Object.keys(data)
|
||||||
|
.filter((key) => key.startsWith('artworkUrl'))
|
||||||
|
.map((key) => {
|
||||||
return {
|
return {
|
||||||
url: data[key],
|
url: data[key],
|
||||||
size: Number(key.replace('artworkUrl', ''))
|
size: Number(key.replace('artworkUrl', ''))
|
||||||
|
|
@ -74,11 +87,11 @@ class iTunes {
|
||||||
if (!artworkSizes.length) return null
|
if (!artworkSizes.length) return null
|
||||||
|
|
||||||
// Return next biggest size > 600
|
// Return next biggest size > 600
|
||||||
var nextBestSize = artworkSizes.find(size => size.size > 600)
|
var nextBestSize = artworkSizes.find((size) => size.size > 600)
|
||||||
if (nextBestSize) return nextBestSize.url
|
if (nextBestSize) return nextBestSize.url
|
||||||
|
|
||||||
// Find square artwork
|
// Find square artwork
|
||||||
var squareArtwork = artworkSizes.find(size => size.url.includes(`${size.size}x${size.size}bb`))
|
var squareArtwork = artworkSizes.find((size) => size.url.includes(`${size.size}x${size.size}bb`))
|
||||||
|
|
||||||
// Square cover replace with 600x600bb
|
// Square cover replace with 600x600bb
|
||||||
if (squareArtwork) {
|
if (squareArtwork) {
|
||||||
|
|
@ -106,8 +119,14 @@ class iTunes {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
searchAudiobooks(term) {
|
/**
|
||||||
return this.search({ term, entity: 'audiobook', media: 'audiobook' }).then((results) => {
|
*
|
||||||
|
* @param {string} term
|
||||||
|
* @param {number} [timeout] response timeout in ms
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
*/
|
||||||
|
searchAudiobooks(term, timeout = this.#responseTimeout) {
|
||||||
|
return this.search({ term, entity: 'audiobook', media: 'audiobook' }, timeout).then((results) => {
|
||||||
return results.map(this.cleanAudiobook.bind(this))
|
return results.map(this.cleanAudiobook.bind(this))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -139,10 +158,11 @@ class iTunes {
|
||||||
*
|
*
|
||||||
* @param {string} term
|
* @param {string} term
|
||||||
* @param {{country:string}} options
|
* @param {{country:string}} options
|
||||||
|
* @param {number} [timeout] response timeout in ms
|
||||||
* @returns {Promise<iTunesPodcastSearchResult[]>}
|
* @returns {Promise<iTunesPodcastSearchResult[]>}
|
||||||
*/
|
*/
|
||||||
searchPodcasts(term, options = {}) {
|
searchPodcasts(term, options = {}, timeout = this.#responseTimeout) {
|
||||||
return this.search({ term, entity: 'podcast', media: 'podcast', ...options }).then((results) => {
|
return this.search({ term, entity: 'podcast', media: 'podcast', ...options }, timeout).then((results) => {
|
||||||
return results.map(this.cleanPodcast.bind(this))
|
return results.map(this.cleanPodcast.bind(this))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,7 @@ class ApiRouter {
|
||||||
//
|
//
|
||||||
this.router.get('/me', MeController.getCurrentUser.bind(this))
|
this.router.get('/me', MeController.getCurrentUser.bind(this))
|
||||||
this.router.get('/me/listening-sessions', MeController.getListeningSessions.bind(this))
|
this.router.get('/me/listening-sessions', MeController.getListeningSessions.bind(this))
|
||||||
|
this.router.get('/me/item/listening-sessions/:libraryItemId/:episodeId?', MeController.getItemListeningSessions.bind(this))
|
||||||
this.router.get('/me/listening-stats', MeController.getListeningStats.bind(this))
|
this.router.get('/me/listening-stats', MeController.getListeningStats.bind(this))
|
||||||
this.router.get('/me/progress/:id/remove-from-continue-listening', MeController.removeItemFromContinueListening.bind(this))
|
this.router.get('/me/progress/:id/remove-from-continue-listening', MeController.removeItemFromContinueListening.bind(this))
|
||||||
this.router.get('/me/progress/:id/:episodeId?', MeController.getMediaProgress.bind(this))
|
this.router.get('/me/progress/:id/:episodeId?', MeController.getMediaProgress.bind(this))
|
||||||
|
|
@ -477,6 +478,11 @@ class ApiRouter {
|
||||||
return userSessions.sort((a, b) => b.updatedAt - a.updatedAt)
|
return userSessions.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getUserItemListeningSessionsHelper(userId, mediaItemId) {
|
||||||
|
const userSessions = await Database.getPlaybackSessions({ userId, mediaItemId })
|
||||||
|
return userSessions.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||||
|
}
|
||||||
|
|
||||||
async getUserListeningStatsHelpers(userId) {
|
async getUserListeningStatsHelpers(userId) {
|
||||||
const today = date.format(new Date(), 'YYYY-MM-DD')
|
const today = date.format(new Date(), 'YYYY-MM-DD')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ const LibraryItem = require('../models/LibraryItem')
|
||||||
const AudioFile = require('../objects/files/AudioFile')
|
const AudioFile = require('../objects/files/AudioFile')
|
||||||
|
|
||||||
class AudioFileScanner {
|
class AudioFileScanner {
|
||||||
constructor() { }
|
constructor() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Is array of numbers sequential, i.e. 1, 2, 3, 4
|
* Is array of numbers sequential, i.e. 1, 2, 3, 4
|
||||||
|
|
@ -193,7 +193,7 @@ class AudioFileScanner {
|
||||||
for (let i = batch; i < Math.min(batch + batchSize, audioLibraryFiles.length); i++) {
|
for (let i = batch; i < Math.min(batch + batchSize, audioLibraryFiles.length); i++) {
|
||||||
proms.push(this.scan(mediaType, audioLibraryFiles[i], libraryItemScanData.mediaMetadata))
|
proms.push(this.scan(mediaType, audioLibraryFiles[i], libraryItemScanData.mediaMetadata))
|
||||||
}
|
}
|
||||||
results.push(...await Promise.all(proms).then((scanResults) => scanResults.filter(sr => sr)))
|
results.push(...(await Promise.all(proms).then((scanResults) => scanResults.filter((sr) => sr))))
|
||||||
}
|
}
|
||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
@ -243,7 +243,7 @@ class AudioFileScanner {
|
||||||
{
|
{
|
||||||
tag: 'tagAlbum',
|
tag: 'tagAlbum',
|
||||||
altTag: 'tagTitle',
|
altTag: 'tagTitle',
|
||||||
key: 'title',
|
key: 'title'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'tagArtist',
|
tag: 'tagArtist',
|
||||||
|
|
@ -343,7 +343,7 @@ class AudioFileScanner {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'tagPodcastType',
|
tag: 'tagPodcastType',
|
||||||
key: 'podcastType',
|
key: 'podcastType'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -391,7 +391,7 @@ class AudioFileScanner {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'tagDisc',
|
tag: 'tagDisc',
|
||||||
key: 'season',
|
key: 'season'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'tagTrack',
|
tag: 'tagTrack',
|
||||||
|
|
@ -464,12 +464,7 @@ class AudioFileScanner {
|
||||||
// If first audio file has embedded chapters then use embedded chapters
|
// If first audio file has embedded chapters then use embedded chapters
|
||||||
if (audioFiles[0].chapters?.length) {
|
if (audioFiles[0].chapters?.length) {
|
||||||
// If all files chapters are the same, then only make chapters for the first file
|
// If all files chapters are the same, then only make chapters for the first file
|
||||||
if (
|
if (audioFiles.length === 1 || (audioFiles.length > 1 && audioFiles[0].chapters.length === audioFiles[1].chapters?.length && audioFiles[0].chapters.every((c, i) => c.title === audioFiles[1].chapters[i].title && c.start === audioFiles[1].chapters[i].start))) {
|
||||||
audioFiles.length === 1 ||
|
|
||||||
audioFiles.length > 1 &&
|
|
||||||
audioFiles[0].chapters.length === audioFiles[1].chapters?.length &&
|
|
||||||
audioFiles[0].chapters.every((c, i) => c.title === audioFiles[1].chapters[i].title && c.start === audioFiles[1].chapters[i].start)
|
|
||||||
) {
|
|
||||||
libraryScan.addLog(LogLevel.DEBUG, `setChapters: Using embedded chapters in first audio file ${audioFiles[0].metadata?.path}`)
|
libraryScan.addLog(LogLevel.DEBUG, `setChapters: Using embedded chapters in first audio file ${audioFiles[0].metadata?.path}`)
|
||||||
chapters = audioFiles[0].chapters.map((c) => ({ ...c }))
|
chapters = audioFiles[0].chapters.map((c) => ({ ...c }))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -479,11 +474,12 @@ class AudioFileScanner {
|
||||||
|
|
||||||
audioFiles.forEach((file) => {
|
audioFiles.forEach((file) => {
|
||||||
if (file.duration) {
|
if (file.duration) {
|
||||||
const afChapters = file.chapters?.map((c) => ({
|
const afChapters =
|
||||||
|
file.chapters?.map((c) => ({
|
||||||
...c,
|
...c,
|
||||||
id: c.id + currChapterId,
|
id: c.id + currChapterId,
|
||||||
start: c.start + currStartTime,
|
start: c.start + currStartTime,
|
||||||
end: c.end + currStartTime,
|
end: c.end + currStartTime
|
||||||
})) ?? []
|
})) ?? []
|
||||||
chapters = chapters.concat(afChapters)
|
chapters = chapters.concat(afChapters)
|
||||||
|
|
||||||
|
|
@ -494,12 +490,11 @@ class AudioFileScanner {
|
||||||
return chapters
|
return chapters
|
||||||
}
|
}
|
||||||
} else if (audioFiles.length > 1) {
|
} else if (audioFiles.length > 1) {
|
||||||
|
|
||||||
// In some cases the ID3 title tag for each file is the chapter title, the criteria to determine if this will be used
|
// In some cases the ID3 title tag for each file is the chapter title, the criteria to determine if this will be used
|
||||||
// 1. Every audio file has an ID3 title tag set
|
// 1. Every audio file has an ID3 title tag set
|
||||||
// 2. None of the title tags are the same as the book title
|
// 2. None of the title tags are the same as the book title
|
||||||
// 3. Every ID3 title tag is unique
|
// 3. Every ID3 title tag is unique
|
||||||
const metaTagTitlesFound = [...new Set(audioFiles.map(af => af.metaTags?.tagTitle).filter(tagTitle => !!tagTitle && tagTitle !== bookTitle))]
|
const metaTagTitlesFound = [...new Set(audioFiles.map((af) => af.metaTags?.tagTitle).filter((tagTitle) => !!tagTitle && tagTitle !== bookTitle))]
|
||||||
const useMetaTagAsTitle = metaTagTitlesFound.length === audioFiles.length
|
const useMetaTagAsTitle = metaTagTitlesFound.length === audioFiles.length
|
||||||
|
|
||||||
// Build chapters from audio files
|
// Build chapters from audio files
|
||||||
|
|
@ -537,7 +532,10 @@ class AudioFileScanner {
|
||||||
const separators = ['/', '//', ';']
|
const separators = ['/', '//', ';']
|
||||||
for (let i = 0; i < separators.length; i++) {
|
for (let i = 0; i < separators.length; i++) {
|
||||||
if (genreTag.includes(separators[i])) {
|
if (genreTag.includes(separators[i])) {
|
||||||
return genreTag.split(separators[i]).map(genre => genre.trim()).filter(g => !!g)
|
return genreTag
|
||||||
|
.split(separators[i])
|
||||||
|
.map((genre) => genre.trim())
|
||||||
|
.filter((g) => !!g)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [genreTag]
|
return [genreTag]
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ function tryGrabBitRate(stream, all_streams, total_bit_rate) {
|
||||||
var tagDuration = stream.tags.DURATION || stream.tags['DURATION-eng'] || stream.tags['DURATION_eng']
|
var tagDuration = stream.tags.DURATION || stream.tags['DURATION-eng'] || stream.tags['DURATION_eng']
|
||||||
var tagBytes = stream.tags.NUMBER_OF_BYTES || stream.tags['NUMBER_OF_BYTES-eng'] || stream.tags['NUMBER_OF_BYTES_eng']
|
var tagBytes = stream.tags.NUMBER_OF_BYTES || stream.tags['NUMBER_OF_BYTES-eng'] || stream.tags['NUMBER_OF_BYTES_eng']
|
||||||
if (tagDuration && tagBytes && !isNaN(tagDuration) && !isNaN(tagBytes)) {
|
if (tagDuration && tagBytes && !isNaN(tagDuration) && !isNaN(tagBytes)) {
|
||||||
var bps = Math.floor(Number(tagBytes) * 8 / Number(tagDuration))
|
var bps = Math.floor((Number(tagBytes) * 8) / Number(tagDuration))
|
||||||
if (bps && !isNaN(bps)) {
|
if (bps && !isNaN(bps)) {
|
||||||
return bps
|
return bps
|
||||||
}
|
}
|
||||||
|
|
@ -33,7 +33,7 @@ function tryGrabBitRate(stream, all_streams, total_bit_rate) {
|
||||||
estimated_bit_rate -= Number(stream.bit_rate)
|
estimated_bit_rate -= Number(stream.bit_rate)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (!all_streams.find(s => s.codec_type === 'audio' && s.bit_rate && Number(s.bit_rate) > estimated_bit_rate)) {
|
if (!all_streams.find((s) => s.codec_type === 'audio' && s.bit_rate && Number(s.bit_rate) > estimated_bit_rate)) {
|
||||||
return estimated_bit_rate
|
return estimated_bit_rate
|
||||||
} else {
|
} else {
|
||||||
return total_bit_rate
|
return total_bit_rate
|
||||||
|
|
@ -73,7 +73,7 @@ function tryGrabChannelLayout(stream) {
|
||||||
function tryGrabTags(stream, ...tags) {
|
function tryGrabTags(stream, ...tags) {
|
||||||
if (!stream.tags) return null
|
if (!stream.tags) return null
|
||||||
for (let i = 0; i < tags.length; i++) {
|
for (let i = 0; i < tags.length; i++) {
|
||||||
const tagKey = Object.keys(stream.tags).find(t => t.toLowerCase() === tags[i].toLowerCase())
|
const tagKey = Object.keys(stream.tags).find((t) => t.toLowerCase() === tags[i].toLowerCase())
|
||||||
const value = stream.tags[tagKey]
|
const value = stream.tags[tagKey]
|
||||||
if (value && value.trim()) return value.trim()
|
if (value && value.trim()) return value.trim()
|
||||||
}
|
}
|
||||||
|
|
@ -101,7 +101,7 @@ function parseMediaStreamInfo(stream, all_streams, total_bit_rate) {
|
||||||
|
|
||||||
if (info.type === 'video') {
|
if (info.type === 'video') {
|
||||||
info.profile = stream.profile || null
|
info.profile = stream.profile || null
|
||||||
info.is_avc = (stream.is_avc !== '0' && stream.is_avc !== 'false')
|
info.is_avc = stream.is_avc !== '0' && stream.is_avc !== 'false'
|
||||||
info.pix_fmt = stream.pix_fmt || null
|
info.pix_fmt = stream.pix_fmt || null
|
||||||
info.frame_rate = tryGrabFrameRate(stream)
|
info.frame_rate = tryGrabFrameRate(stream)
|
||||||
info.width = !isNaN(stream.width) ? Number(stream.width) : null
|
info.width = !isNaN(stream.width) ? Number(stream.width) : null
|
||||||
|
|
@ -123,7 +123,6 @@ function isNullOrNaN(val) {
|
||||||
return val === null || isNaN(val)
|
return val === null || isNaN(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Example chapter object
|
/* Example chapter object
|
||||||
* {
|
* {
|
||||||
"id": 71,
|
"id": 71,
|
||||||
|
|
@ -137,10 +136,11 @@ function isNullOrNaN(val) {
|
||||||
}
|
}
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
function parseChapters(chapters) {
|
function parseChapters(_chapters) {
|
||||||
if (!chapters) return []
|
if (!_chapters) return []
|
||||||
let index = 0
|
|
||||||
return chapters.map(chap => {
|
return _chapters
|
||||||
|
.map((chap) => {
|
||||||
let title = chap['TAG:title'] || chap.title || ''
|
let title = chap['TAG:title'] || chap.title || ''
|
||||||
if (!title && chap.tags?.title) title = chap.tags.title
|
if (!title && chap.tags?.title) title = chap.tags.title
|
||||||
|
|
||||||
|
|
@ -148,12 +148,16 @@ function parseChapters(chapters) {
|
||||||
const start = !isNullOrNaN(chap.start_time) ? Number(chap.start_time) : !isNullOrNaN(chap.start) ? Number(chap.start) / timebase : 0
|
const start = !isNullOrNaN(chap.start_time) ? Number(chap.start_time) : !isNullOrNaN(chap.start) ? Number(chap.start) / timebase : 0
|
||||||
const end = !isNullOrNaN(chap.end_time) ? Number(chap.end_time) : !isNullOrNaN(chap.end) ? Number(chap.end) / timebase : 0
|
const end = !isNullOrNaN(chap.end_time) ? Number(chap.end_time) : !isNullOrNaN(chap.end) ? Number(chap.end) / timebase : 0
|
||||||
return {
|
return {
|
||||||
id: index++,
|
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
title
|
title
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.sort((a, b) => a.start - b.start)
|
||||||
|
.map((chap, index) => {
|
||||||
|
chap.id = index
|
||||||
|
return chap
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseTags(format, verbose) {
|
function parseTags(format, verbose) {
|
||||||
|
|
@ -210,7 +214,7 @@ function parseTags(format, verbose) {
|
||||||
file_tag_movement: tryGrabTags(format, 'movement', 'mvin'),
|
file_tag_movement: tryGrabTags(format, 'movement', 'mvin'),
|
||||||
file_tag_genre1: tryGrabTags(format, 'tmp_genre1', 'genre1'),
|
file_tag_genre1: tryGrabTags(format, 'tmp_genre1', 'genre1'),
|
||||||
file_tag_genre2: tryGrabTags(format, 'tmp_genre2', 'genre2'),
|
file_tag_genre2: tryGrabTags(format, 'tmp_genre2', 'genre2'),
|
||||||
file_tag_overdrive_media_marker: tryGrabTags(format, 'OverDrive MediaMarkers'),
|
file_tag_overdrive_media_marker: tryGrabTags(format, 'OverDrive MediaMarkers')
|
||||||
}
|
}
|
||||||
for (const key in tags) {
|
for (const key in tags) {
|
||||||
if (!tags[key]) {
|
if (!tags[key]) {
|
||||||
|
|
@ -224,7 +228,7 @@ function parseTags(format, verbose) {
|
||||||
function getDefaultAudioStream(audioStreams) {
|
function getDefaultAudioStream(audioStreams) {
|
||||||
if (!audioStreams || !audioStreams.length) return null
|
if (!audioStreams || !audioStreams.length) return null
|
||||||
if (audioStreams.length === 1) return audioStreams[0]
|
if (audioStreams.length === 1) return audioStreams[0]
|
||||||
var defaultStream = audioStreams.find(a => a.is_default)
|
var defaultStream = audioStreams.find((a) => a.is_default)
|
||||||
if (!defaultStream) return audioStreams[0]
|
if (!defaultStream) return audioStreams[0]
|
||||||
return defaultStream
|
return defaultStream
|
||||||
}
|
}
|
||||||
|
|
@ -248,9 +252,9 @@ function parseProbeData(data, verbose = false) {
|
||||||
cleanedData.rawTags = format.tags
|
cleanedData.rawTags = format.tags
|
||||||
}
|
}
|
||||||
|
|
||||||
const cleaned_streams = streams.map(s => parseMediaStreamInfo(s, streams, cleanedData.bit_rate))
|
const cleaned_streams = streams.map((s) => parseMediaStreamInfo(s, streams, cleanedData.bit_rate))
|
||||||
cleanedData.video_stream = cleaned_streams.find(s => s.type === 'video')
|
cleanedData.video_stream = cleaned_streams.find((s) => s.type === 'video')
|
||||||
const audioStreams = cleaned_streams.filter(s => s.type === 'audio')
|
const audioStreams = cleaned_streams.filter((s) => s.type === 'audio')
|
||||||
cleanedData.audio_stream = getDefaultAudioStream(audioStreams)
|
cleanedData.audio_stream = getDefaultAudioStream(audioStreams)
|
||||||
|
|
||||||
if (cleanedData.audio_stream && cleanedData.video_stream) {
|
if (cleanedData.audio_stream && cleanedData.video_stream) {
|
||||||
|
|
@ -290,7 +294,7 @@ function probe(filepath, verbose = false) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return ffprobe(filepath)
|
return ffprobe(filepath)
|
||||||
.then(raw => {
|
.then((raw) => {
|
||||||
if (raw.error) {
|
if (raw.error) {
|
||||||
return {
|
return {
|
||||||
error: raw.error.string
|
error: raw.error.string
|
||||||
|
|
@ -327,8 +331,7 @@ function rawProbe(filepath) {
|
||||||
ffprobe.FFPROBE_PATH = process.env.FFPROBE_PATH
|
ffprobe.FFPROBE_PATH = process.env.FFPROBE_PATH
|
||||||
}
|
}
|
||||||
|
|
||||||
return ffprobe(filepath)
|
return ffprobe(filepath).catch((err) => {
|
||||||
.catch((err) => {
|
|
||||||
return {
|
return {
|
||||||
error: err
|
error: err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue