mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-03-03 06:29:42 +00:00
Merge remote-tracking branch 'remotes/upstream/master'
This commit is contained in:
commit
fbed92dacf
68 changed files with 6264 additions and 1208 deletions
|
|
@ -11,7 +11,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div v-else-if="loaded && !shelves.length && search" class="w-full h-40 flex items-center justify-center">
|
||||
<p class="text-center text-xl py-4">No results for query</p>
|
||||
<p class="text-center text-xl py-4">{{ $strings.MessageBookshelfNoResultsForQuery }}</p>
|
||||
</div>
|
||||
<!-- Alternate plain view -->
|
||||
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24">
|
||||
|
|
|
|||
|
|
@ -37,12 +37,12 @@
|
|||
<span class="material-icons text-2xl">arrow_left</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-normal block truncate">Back</span>
|
||||
<span class="font-normal block truncate">{{ $strings.ButtonBack }}</span>
|
||||
</div>
|
||||
</li>
|
||||
<li v-if="!sublistItems.length" class="text-gray-400 select-none relative px-2" role="option">
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="font-normal block truncate py-2">No {{ sublist }}</span>
|
||||
<span class="font-normal block truncate py-2">{{ $getString('LabelLibraryFilterSublistEmpty', [selectedSublistText]) }}</span>
|
||||
</div>
|
||||
</li>
|
||||
<template v-for="item in sublistItems">
|
||||
|
|
@ -106,31 +106,37 @@ export default {
|
|||
},
|
||||
{
|
||||
text: this.$strings.LabelGenre,
|
||||
textPlural: this.$strings.LabelGenres,
|
||||
value: 'genres',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelTag,
|
||||
textPlural: this.$strings.LabelTags,
|
||||
value: 'tags',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelAuthor,
|
||||
textPlural: this.$strings.LabelAuthors,
|
||||
value: 'authors',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelNarrator,
|
||||
textPlural: this.$strings.LabelNarrators,
|
||||
value: 'narrators',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelPublisher,
|
||||
textPlural: this.$strings.LabelPublishers,
|
||||
value: 'publishers',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelLanguage,
|
||||
textPlural: this.$strings.LabelLanguages,
|
||||
value: 'languages',
|
||||
sublist: true
|
||||
},
|
||||
|
|
@ -149,36 +155,43 @@ export default {
|
|||
},
|
||||
{
|
||||
text: this.$strings.LabelGenre,
|
||||
textPlural: this.$strings.LabelGenres,
|
||||
value: 'genres',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelTag,
|
||||
textPlural: this.$strings.LabelTags,
|
||||
value: 'tags',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelSeries,
|
||||
textPlural: this.$strings.LabelSeries,
|
||||
value: 'series',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelAuthor,
|
||||
textPlural: this.$strings.LabelAuthors,
|
||||
value: 'authors',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelNarrator,
|
||||
textPlural: this.$strings.LabelNarrators,
|
||||
value: 'narrators',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelPublisher,
|
||||
textPlural: this.$strings.LabelPublishers,
|
||||
value: 'publishers',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelLanguage,
|
||||
textPlural: this.$strings.LabelLanguages,
|
||||
value: 'languages',
|
||||
sublist: true
|
||||
},
|
||||
|
|
@ -227,16 +240,19 @@ export default {
|
|||
},
|
||||
{
|
||||
text: this.$strings.LabelGenre,
|
||||
textPlural: this.$strings.LabelGenres,
|
||||
value: 'genres',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelTag,
|
||||
textPlural: this.$strings.LabelTags,
|
||||
value: 'tags',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelLanguage,
|
||||
textPlural: this.$strings.LabelLanguages,
|
||||
value: 'languages',
|
||||
sublist: true
|
||||
},
|
||||
|
|
@ -255,11 +271,13 @@ export default {
|
|||
},
|
||||
{
|
||||
text: this.$strings.LabelGenre,
|
||||
textPlural: this.$strings.LabelGenres,
|
||||
value: 'genres',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelTag,
|
||||
textPlural: this.$strings.LabelTags,
|
||||
value: 'tags',
|
||||
sublist: true
|
||||
},
|
||||
|
|
@ -279,6 +297,13 @@ export default {
|
|||
selectedItemSublist() {
|
||||
return this.selected?.includes('.') ? this.selected.split('.')[0] : null
|
||||
},
|
||||
selectedSublistText() {
|
||||
if (!this.sublist) {
|
||||
return ''
|
||||
}
|
||||
const sublistItem = this.selectItems.find((i) => i.value === this.sublist)
|
||||
return sublistItem?.textPlural || sublistItem?.text || ''
|
||||
},
|
||||
selectedText() {
|
||||
if (!this.selected) return ''
|
||||
const parts = this.selected.split('.')
|
||||
|
|
@ -505,4 +530,4 @@ export default {
|
|||
.libraryFilterMenu {
|
||||
max-height: calc(100vh - 125px);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@
|
|||
<p class="font-mono text-sm hidden sm:block text-gray-100 pointer-events-auto"> / {{ progressPercent }}%</p>
|
||||
<div class="flex-grow" />
|
||||
<p class="text-xs sm:text-sm text-gray-300 pt-0.5">
|
||||
{{ currentChapterName }} <span v-if="useChapterTrack" class="text-xs text-gray-400"> ({{ currentChapterIndex + 1 }} of {{ chapters.length }})</span>
|
||||
{{ currentChapterName }} <span v-if="useChapterTrack" class="text-xs text-gray-400"> ({{ $getString('LabelPlayerChapterNumberMarker', [currentChapterIndex + 1, chapters.length]) }})</span>
|
||||
</p>
|
||||
<div class="flex-grow" />
|
||||
<p class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
<div class="absolute top-0 left-0 right-0 w-full h-36 bg-gradient-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" />
|
||||
<div ref="content" class="relative text-white" :style="{ height: modalHeight, width: modalWidth }" v-click-outside="clickedOutside">
|
||||
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
|
||||
<p class="text-lg mb-6 mt-2 px-1" v-html="message" />
|
||||
<p id="confirm-prompt-message" class="text-lg mb-6 mt-2 px-1" v-html="message" />
|
||||
|
||||
<ui-checkbox v-if="checkboxLabel" v-model="checkboxValue" checkbox-bg="bg" :label="checkboxLabel" label-class="pl-2 text-base" class="mb-6 px-1" />
|
||||
|
||||
|
|
@ -131,4 +131,14 @@ export default {
|
|||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#confirm-prompt-message code {
|
||||
font-size: 1rem;
|
||||
border-radius: 6px;
|
||||
background-color: rgb(82, 82, 82);
|
||||
color: white;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ export default {
|
|||
ebookLocation: this.page,
|
||||
ebookProgress: Math.max(0, Math.min(1, (Number(this.page) - 1) / Number(this.numPages)))
|
||||
}
|
||||
this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload).catch((error) => {
|
||||
this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload, { progress: false }).catch((error) => {
|
||||
console.error('ComicReader.updateProgress failed:', error)
|
||||
})
|
||||
},
|
||||
|
|
@ -386,4 +386,4 @@ export default {
|
|||
.pagemenu {
|
||||
max-height: calc(100% - 48px);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ export default {
|
|||
*/
|
||||
updateProgress(payload) {
|
||||
if (!this.keepProgress) return
|
||||
this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload).catch((error) => {
|
||||
this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload, { progress: false }).catch((error) => {
|
||||
console.error('EpubReader.updateProgress failed:', error)
|
||||
})
|
||||
},
|
||||
|
|
|
|||
|
|
@ -23,13 +23,10 @@
|
|||
<div class="flex items-center justify-center">
|
||||
<div :style="{ width: pdfWidth + 'px', height: pdfHeight + 'px' }" class="overflow-auto">
|
||||
<div v-if="loadedRatio > 0 && loadedRatio < 1" style="background-color: green; color: white; text-align: center" :style="{ width: loadedRatio * 100 + '%' }">{{ Math.floor(loadedRatio * 100) }}%</div>
|
||||
<pdf ref="pdf" class="m-auto z-10 border border-black border-opacity-20 shadow-md" :src="pdfDocInitParams" :page="page" :rotate="rotate" @progress="progressEvt" @error="error" @num-pages="numPagesLoaded" @link-clicked="page = $event" @loaded="loadedEvt"></pdf>
|
||||
<pdf v-if="pdfDocInitParams" ref="pdf" class="m-auto z-10 border border-black border-opacity-20 shadow-md" :src="pdfDocInitParams" :page="page" :rotate="rotate" @progress="progressEvt" @error="error" @num-pages="numPagesLoaded" @link-clicked="page = $event" @loaded="loadedEvt"></pdf>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="text-center py-2 text-lg">
|
||||
<p>{{ page }} / {{ numPages }}</p>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
@ -57,7 +54,8 @@ export default {
|
|||
rotate: 0,
|
||||
loadedRatio: 0,
|
||||
page: 1,
|
||||
numPages: 0
|
||||
numPages: 0,
|
||||
pdfDocInitParams: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -108,14 +106,6 @@ export default {
|
|||
return `/api/items/${this.libraryItemId}/ebook/${this.fileId}`
|
||||
}
|
||||
return `/api/items/${this.libraryItemId}/ebook`
|
||||
},
|
||||
pdfDocInitParams() {
|
||||
return {
|
||||
url: this.ebookUrl,
|
||||
httpHeaders: {
|
||||
Authorization: `Bearer ${this.userToken}`
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
|
@ -136,7 +126,7 @@ export default {
|
|||
ebookLocation: this.page,
|
||||
ebookProgress: Math.max(0, Math.min(1, (Number(this.page) - 1) / Number(this.numPages)))
|
||||
}
|
||||
this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload).catch((error) => {
|
||||
this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload, { progress: false }).catch((error) => {
|
||||
console.error('EpubReader.updateProgress failed:', error)
|
||||
})
|
||||
},
|
||||
|
|
@ -149,6 +139,7 @@ export default {
|
|||
this.loadedRatio = progress
|
||||
},
|
||||
numPagesLoaded(e) {
|
||||
if (!e) return
|
||||
this.numPages = e
|
||||
},
|
||||
prev() {
|
||||
|
|
@ -167,15 +158,25 @@ export default {
|
|||
resize() {
|
||||
this.windowWidth = window.innerWidth
|
||||
this.windowHeight = window.innerHeight
|
||||
},
|
||||
init() {
|
||||
this.pdfDocInitParams = {
|
||||
url: this.ebookUrl,
|
||||
httpHeaders: {
|
||||
Authorization: `Bearer ${this.userToken}`
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.windowWidth = window.innerWidth
|
||||
this.windowHeight = window.innerHeight
|
||||
window.addEventListener('resize', this.resize)
|
||||
|
||||
this.init()
|
||||
},
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.resize)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
</tr>
|
||||
</table>
|
||||
<div v-else-if="!processing" class="text-center py-8">
|
||||
<p class="text-lg">No custom metadata providers</p>
|
||||
<p class="text-lg">{{ $strings.LabelNoCustomMetadataProviders }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="processing" class="absolute inset-0 h-full flex items-center justify-center bg-black/40 rounded-md">
|
||||
|
|
|
|||
4672
client/cypress/support/tailwind.compiled.css
Normal file
4672
client/cypress/support/tailwind.compiled.css
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -4,7 +4,7 @@
|
|||
<div class="w-full border border-white/10 rounded-xl p-4 my-4 bg-primary/25">
|
||||
<div class="flex items-center">
|
||||
<ui-checkbox v-model="showCustomLoginMessage" checkbox-bg="bg" />
|
||||
<p class="text-lg pl-4">Custom Message on Login</p>
|
||||
<p class="text-lg pl-4">{{ $strings.HeaderCustomMessageOnLogin }}</p>
|
||||
</div>
|
||||
<transition name="slide">
|
||||
<div v-if="showCustomLoginMessage" class="w-full pt-4">
|
||||
|
|
|
|||
|
|
@ -20,13 +20,30 @@
|
|||
</div>
|
||||
|
||||
<div class="flex items-center mb-2 py-3">
|
||||
<ui-toggle-switch labeledBy="email-settings-secure" v-model="newSettings.secure" :disabled="savingSettings" />
|
||||
<ui-tooltip :text="$strings.LabelEmailSettingsSecureHelp">
|
||||
<div class="pl-4 flex items-center">
|
||||
<span id="email-settings-secure">{{ $strings.LabelEmailSettingsSecure }}</span>
|
||||
<span class="material-icons text-lg pl-1">info_outlined</span>
|
||||
<div class="w-full md:w-1/2 px-1">
|
||||
<!-- secure toggle -->
|
||||
<div class="flex items-center">
|
||||
<ui-toggle-switch labeledBy="email-settings-secure" v-model="newSettings.secure" :disabled="savingSettings" />
|
||||
<ui-tooltip :text="$strings.LabelEmailSettingsSecureHelp">
|
||||
<div class="pl-4 flex items-center">
|
||||
<span id="email-settings-secure">{{ $strings.LabelEmailSettingsSecure }}</span>
|
||||
<span class="material-icons text-lg pl-1">info_outlined</span>
|
||||
</div>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
<div class="w-full md:w-1/2 px-1">
|
||||
<!-- reject unauthorized toggle -->
|
||||
<div class="flex items-center">
|
||||
<ui-toggle-switch labeledBy="email-settings-reject-unauthorized" v-model="newSettings.rejectUnauthorized" :disabled="savingSettings" />
|
||||
<ui-tooltip :text="$strings.LabelEmailSettingsRejectUnauthorizedHelp">
|
||||
<div class="pl-4 flex items-center">
|
||||
<span id="email-settings-reject-unauthorized">{{ $strings.LabelEmailSettingsRejectUnauthorized }}</span>
|
||||
<span class="material-icons text-lg pl-1">info_outlined</span>
|
||||
</div>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center -mx-1 mb-2">
|
||||
|
|
@ -119,6 +136,7 @@ export default {
|
|||
host: null,
|
||||
port: 465,
|
||||
secure: true,
|
||||
rejectUnauthorized: true,
|
||||
user: null,
|
||||
pass: null,
|
||||
testAddress: null,
|
||||
|
|
@ -257,6 +275,7 @@ export default {
|
|||
host: this.newSettings.host,
|
||||
port: this.newSettings.port,
|
||||
secure: this.newSettings.secure,
|
||||
rejectUnauthorized: this.newSettings.rejectUnauthorized,
|
||||
user: this.newSettings.user,
|
||||
pass: this.newSettings.pass,
|
||||
testAddress: this.newSettings.testAddress,
|
||||
|
|
|
|||
|
|
@ -368,7 +368,8 @@ export default {
|
|||
},
|
||||
purgeItemsCache() {
|
||||
const payload = {
|
||||
message: `<span class="text-warning text-base">Warning! This will delete the entire folder at /metadata/cache/items.</span><br />Are you sure you want to purge items cache?`,
|
||||
// message: `This will delete the entire folder at <code>/metadata/cache/items</code>.<br />Are you sure you want to purge items cache?`,
|
||||
message: this.$strings.MessageConfirmPurgeItemsCache,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.sendPurgeItemsCache()
|
||||
|
|
|
|||
|
|
@ -29,4 +29,4 @@ export default class AudioTrack {
|
|||
|
||||
return this.contentUrl + `?token=${this.userToken}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,22 @@
|
|||
|
||||
function getMediaInfoFromTrack(libraryItem, castImage, track) {
|
||||
// https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.media.AudiobookChapterMediaMetadata
|
||||
var metadata = new chrome.cast.media.AudiobookChapterMediaMetadata()
|
||||
metadata.bookTitle = libraryItem.media.metadata.title
|
||||
metadata.chapterNumber = track.index
|
||||
metadata.chapterTitle = track.title
|
||||
metadata.images = [castImage]
|
||||
metadata.title = track.title
|
||||
metadata.subtitle = libraryItem.media.metadata.title
|
||||
let metadata = null
|
||||
if (libraryItem.mediaType === 'podcast') {
|
||||
metadata = new chrome.cast.media.MusicTrackMediaMetadata()
|
||||
metadata.albumArtist = libraryItem.media.metadata.author
|
||||
metadata.artist = libraryItem.media.metadata.author
|
||||
metadata.title = track.title
|
||||
metadata.albumName = libraryItem.media.metadata.title
|
||||
metadata.images = [castImage]
|
||||
} else {
|
||||
// https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.media.AudiobookChapterMediaMetadata
|
||||
metadata = new chrome.cast.media.AudiobookChapterMediaMetadata()
|
||||
metadata.bookTitle = libraryItem.media.metadata.title
|
||||
metadata.chapterNumber = track.index
|
||||
metadata.chapterTitle = track.title
|
||||
metadata.images = [castImage]
|
||||
metadata.title = track.title
|
||||
metadata.subtitle = libraryItem.media.metadata.title
|
||||
}
|
||||
|
||||
var trackurl = track.fullContentUrl
|
||||
var mimeType = track.mimeType
|
||||
|
|
@ -20,17 +29,25 @@ function getMediaInfoFromTrack(libraryItem, castImage, track) {
|
|||
|
||||
function buildCastMediaInfo(libraryItem, coverUrl, tracks) {
|
||||
const castImage = new chrome.cast.Image(coverUrl)
|
||||
return tracks.map(t => getMediaInfoFromTrack(libraryItem, castImage, t))
|
||||
return tracks.map((t) => getMediaInfoFromTrack(libraryItem, castImage, t))
|
||||
}
|
||||
|
||||
function buildCastQueueRequest(libraryItem, coverUrl, tracks, startTime) {
|
||||
var mediaInfoItems = buildCastMediaInfo(libraryItem, coverUrl, tracks)
|
||||
|
||||
var containerMetadata = new chrome.cast.media.AudiobookContainerMetadata()
|
||||
containerMetadata.authors = libraryItem.media.metadata.authors.map(a => a.name)
|
||||
containerMetadata.narrators = libraryItem.media.metadata.narrators || []
|
||||
containerMetadata.publisher = libraryItem.media.metadata.publisher || undefined
|
||||
containerMetadata.title = libraryItem.media.metadata.title
|
||||
let containerMetadata = null
|
||||
let queueType = chrome.cast.media.QueueType.AUDIOBOOK
|
||||
if (libraryItem.mediaType === 'podcast') {
|
||||
queueType = chrome.cast.media.QueueType.PODCAST_SERIES
|
||||
containerMetadata = new chrome.cast.media.ContainerMetadata(chrome.cast.media.ContainerType.GENERIC_CONTAINER)
|
||||
containerMetadata.title = libraryItem.media.metadata.title
|
||||
} else {
|
||||
containerMetadata = new chrome.cast.media.AudiobookContainerMetadata()
|
||||
containerMetadata.authors = libraryItem.media.metadata.authors?.map((a) => a.name)
|
||||
containerMetadata.narrators = libraryItem.media.metadata.narrators || []
|
||||
containerMetadata.publisher = libraryItem.media.metadata.publisher || undefined
|
||||
containerMetadata.title = libraryItem.media.metadata.title
|
||||
}
|
||||
|
||||
var mediaQueueItems = mediaInfoItems.map((mi) => {
|
||||
var queueItem = new chrome.cast.media.QueueItem(mi)
|
||||
|
|
@ -38,23 +55,25 @@ function buildCastQueueRequest(libraryItem, coverUrl, tracks, startTime) {
|
|||
})
|
||||
|
||||
// Find track to start playback and calculate track start offset
|
||||
var track = tracks.find(at => at.startOffset <= startTime && at.startOffset + at.duration > startTime)
|
||||
var track = tracks.find((at) => at.startOffset <= startTime && at.startOffset + at.duration > startTime)
|
||||
var trackStartIndex = track ? track.index - 1 : 0
|
||||
var trackStartTime = Math.floor(track ? startTime - track.startOffset : 0)
|
||||
|
||||
var queueData = new chrome.cast.media.QueueData(libraryItem.id, libraryItem.media.metadata.title, '', false, mediaQueueItems, trackStartIndex, trackStartTime)
|
||||
queueData.containerMetadata = containerMetadata
|
||||
queueData.queueType = chrome.cast.media.QueueType.AUDIOBOOK
|
||||
queueData.queueType = queueType
|
||||
return queueData
|
||||
}
|
||||
|
||||
function castLoadMedia(castSession, request) {
|
||||
return new Promise((resolve) => {
|
||||
castSession.loadMedia(request)
|
||||
.then(() => resolve(true), (reason) => {
|
||||
castSession.loadMedia(request).then(
|
||||
() => resolve(true),
|
||||
(reason) => {
|
||||
console.error('Load media failed', reason)
|
||||
resolve(false)
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +88,4 @@ function buildCastLoadRequest(libraryItem, coverUrl, tracks, startTime, autoplay
|
|||
return request
|
||||
}
|
||||
|
||||
export {
|
||||
buildCastLoadRequest,
|
||||
castLoadMedia
|
||||
}
|
||||
export { buildCastLoadRequest, castLoadMedia }
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "Приложи",
|
||||
"ButtonApplyChapters": "Приложи Глави",
|
||||
"ButtonAuthors": "Автори",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "Прегледай за папка",
|
||||
"ButtonCancel": "Откажи",
|
||||
"ButtonCancelEncode": "Откажи закодирането",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "Предишна Глава",
|
||||
"ButtonPurgeAllCache": "Изчисти Всички Кешове",
|
||||
"ButtonPurgeItemsCache": "Изчисти Кеша на Елементи",
|
||||
"ButtonPurgeMediaProgress": "Изчисти Прогреса на Медията",
|
||||
"ButtonQueueAddItem": "Добави към опашката",
|
||||
"ButtonQueueRemoveItem": "Премахни от опашката",
|
||||
"ButtonQuickMatch": "Бързо Съпоставяне",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "Елементи на Колекция",
|
||||
"HeaderCover": "Корица",
|
||||
"HeaderCurrentDownloads": "Текущи Сваляния",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "Потребителски Доставчици на Метаданни",
|
||||
"HeaderDetails": "Детайли",
|
||||
"HeaderDownloadQueue": "Опашка за Сваляне",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "Редакция",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "От Адрес",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "Сигурна",
|
||||
"LabelEmailSettingsSecureHelp": "Ако е вярно възката ще изполва TLS когате се свързва със сървъра. Ако не е то TLS ще се използва ако сървъра поддържа разширението STARTTLS. В повечето случаи задайте тази стойност на истина ако се свързвате към порт 465. За порт 587 или 25 оставете я на лъжа. (от nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Тестов Адрес",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "Елемент",
|
||||
"LabelLanguage": "Език",
|
||||
"LabelLanguageDefaultServer": "Език по подразбиране на сървъра",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "Последно Добавена Книга",
|
||||
"LabelLastBookUpdated": "Последно Обновена Книга",
|
||||
"LabelLastSeen": "Последно Видян",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "По-малко",
|
||||
"LabelLibrariesAccessibleToUser": "Библиотеки Достъпни за Потребителя",
|
||||
"LabelLibrary": "Библиотека",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "Елемент на Библиотека",
|
||||
"LabelLibraryName": "Име на Библиотека",
|
||||
"LabelLimit": "Лимит",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "Нова Парола",
|
||||
"LabelNextBackupDate": "Следваща Дата на Архивиране",
|
||||
"LabelNextScheduledRun": "Следващо Планирано Изпълнение",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "Няма избрани епизоди",
|
||||
"LabelNotes": "Бележки",
|
||||
"LabelNotFinished": "Не е завършено",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "Може да качва",
|
||||
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||
"LabelPhotoPathURL": "Път/URL на Снимка",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "Плейлисти",
|
||||
"LabelPlayMethod": "Метод на Пускане",
|
||||
"LabelPodcast": "Подкаст",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "Доставчик",
|
||||
"LabelPubDate": "Дата на Издаване",
|
||||
"LabelPublisher": "Издател",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "Година на Издаване",
|
||||
"LabelRead": "Прочети",
|
||||
"LabelReadAgain": "Прочети Отново",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "Бързото Съпоставяне ще опита да добави липсващи корици и метаданни за избраните елементи. Активирайте опциите по-долу, за да позволите на Бързото съпоставяне да презапише съществуващите корици и/или метаданни.",
|
||||
"MessageBookshelfNoCollections": "Все още нямате създадени колекции",
|
||||
"MessageBookshelfNoResultsForFilter": "Няма резултат за филтер \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "Няма отворени RSS feed-ове",
|
||||
"MessageBookshelfNoSeries": "Нямаш сеЗЙ",
|
||||
"MessageChapterEndIsAfter": "Краят на главата е след края на вашата аудиокнига",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като завършени?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като незавършени?",
|
||||
"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?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "Внимание! Бързото вграждане няма да архивира вашите аудио файлове. Уверете се, че имате резервно копие на вашите аудио файлове. <br><br>Искате ли да продължите?",
|
||||
"MessageConfirmRemoveAllChapters": "Сигурни ли сте, че искате да премахнете всички глави?",
|
||||
"MessageConfirmRemoveAuthor": "Сигурни ли сте, че искате да премахнете автор \"{0}\"?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "প্রয়োগ করুন",
|
||||
"ButtonApplyChapters": "অধ্যায় প্রয়োগ করুন",
|
||||
"ButtonAuthors": "লেখক",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "ফোল্ডারের জন্য ব্রাউজ করুন",
|
||||
"ButtonCancel": "বাতিল করুন",
|
||||
"ButtonCancelEncode": "এনকোড বাতিল করুন",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "আগের অধ্যায়",
|
||||
"ButtonPurgeAllCache": "সমস্ত ক্যাশে পরিষ্কার করুন",
|
||||
"ButtonPurgeItemsCache": "আইটেম ক্যাশে পরিষ্কার করুন",
|
||||
"ButtonPurgeMediaProgress": "মিডিয়া ক্যাশে পরিষ্কার করুন",
|
||||
"ButtonQueueAddItem": "সারিতে যোগ করুন",
|
||||
"ButtonQueueRemoveItem": "সারি থেকে মুছে ফেলুন",
|
||||
"ButtonQuickMatch": "দ্রুত ম্যাচ",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "সংগ্রহ আইটেম",
|
||||
"HeaderCover": "কভার",
|
||||
"HeaderCurrentDownloads": "বর্তমান ডাউনলোডগুলি",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "কাস্টম মেটাডেটা প্রদানকারী",
|
||||
"HeaderDetails": "বিস্তারিত",
|
||||
"HeaderDownloadQueue": "ডাউনলোড সারি",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "সম্পাদনা করুন",
|
||||
"LabelEmail": "ইমেইল",
|
||||
"LabelEmailSettingsFromAddress": "ঠিকানা থেকে",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "নিরাপদ",
|
||||
"LabelEmailSettingsSecureHelp": "যদি সত্য হয় সার্ভারের সাথে সংযোগ করার সময় সংযোগটি TLS ব্যবহার করবে। মিথ্যা হলে TLS ব্যবহার করা হবে যদি সার্ভার STARTTLS এক্সটেনশন সমর্থন করে। বেশিরভাগ ক্ষেত্রে এই মানটিকে সত্য হিসাবে সেট করুন যদি আপনি পোর্ট 465-এর সাথে সংযোগ করছেন। পোর্ট 587 বা পোর্টের জন্য 25 এটি মিথ্যা রাখুন। (nodemailer.com/smtp/#authentication থেকে)",
|
||||
"LabelEmailSettingsTestAddress": "পরীক্ষার ঠিকানা",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "আইটেম",
|
||||
"LabelLanguage": "ভাষা",
|
||||
"LabelLanguageDefaultServer": "সার্ভারের ডিফল্ট ভাষা",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "শেষ বই যোগ করা হয়েছে",
|
||||
"LabelLastBookUpdated": "শেষ বই আপডেট করা হয়েছে",
|
||||
"LabelLastSeen": "শেষ দেখা",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "কম",
|
||||
"LabelLibrariesAccessibleToUser": "ব্যবহারকারীর কাছে অ্যাক্সেসযোগ্য লাইব্রেরি",
|
||||
"LabelLibrary": "লাইব্রেরি",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "লাইব্রেরি আইটেম",
|
||||
"LabelLibraryName": "লাইব্রেরির নাম",
|
||||
"LabelLimit": "সীমা",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "নতুন পাসওয়ার্ড",
|
||||
"LabelNextBackupDate": "পরবর্তী ব্যাকআপ তারিখ",
|
||||
"LabelNextScheduledRun": "পরবর্তী নির্ধারিত দৌড়",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "কোন পর্ব নির্বাচন করা হয়নি",
|
||||
"LabelNotes": "নোটস",
|
||||
"LabelNotFinished": "সমাপ্ত হয়নি",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "আপলোড করতে পারবে",
|
||||
"LabelPersonalYearReview": "আপনার বছরের পর্যালোচনা ({0})",
|
||||
"LabelPhotoPathURL": "ছবি পথ/ইউআরএল",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "প্লেলিস্ট",
|
||||
"LabelPlayMethod": "প্লে পদ্ধতি",
|
||||
"LabelPodcast": "পডকাস্ট",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "প্রদানকারী",
|
||||
"LabelPubDate": "প্রকাশের তারিখ",
|
||||
"LabelPublisher": "প্রকাশক",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "প্রকাশের বছর",
|
||||
"LabelRead": "পড়ুন",
|
||||
"LabelReadAgain": "আবার পড়ুন",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "কুইক ম্যাচ নির্বাচিত আইটেমগুলির জন্য অনুপস্থিত কভার এবং মেটাডেটা যোগ করার চেষ্টা করবে। বিদ্যমান কভার এবং/অথবা মেটাডেটা ওভাররাইট করার জন্য দ্রুত ম্যাচকে অনুমতি দিতে নীচের বিকল্পগুলি সক্ষম করুন।",
|
||||
"MessageBookshelfNoCollections": "আপনি এখনও কোনো সংগ্রহ করেননি",
|
||||
"MessageBookshelfNoResultsForFilter": "ফিল্টার \"{0}: {1}\" এর জন্য কোন ফলাফল নেই",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "কোনও RSS ফিড খোলা নেই",
|
||||
"MessageBookshelfNoSeries": "আপনার কোনো সিরিজ নেই",
|
||||
"MessageChapterEndIsAfter": "অধ্যায়ের সমাপ্তি আপনার অডিওবুকের শেষে",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "আপনি কি নিশ্চিত যে আপনি এই সিরিজের সমস্ত বইকে সমাপ্ত হিসাবে চিহ্নিত করতে চান?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "আপনি কি নিশ্চিত যে আপনি এই সিরিজের সমস্ত বইকে শেষ হয়নি বলে চিহ্নিত করতে চান?",
|
||||
"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?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "সতর্কতা! দ্রুত এম্বেড আপনার অডিও ফাইলের ব্যাকআপ করবে না। নিশ্চিত করুন যে আপনার অডিও ফাইলগুলির একটি ব্যাকআপ আছে। <br><br>আপনি কি চালিয়ে যেতে চান?",
|
||||
"MessageConfirmRemoveAllChapters": "আপনি কি নিশ্চিত যে আপনি সমস্ত অধ্যায় সরাতে চান?",
|
||||
"MessageConfirmRemoveAuthor": "আপনি কি নিশ্চিত যে আপনি লেখক \"{0}\" অপসারণ করতে চান?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "Aplikovat",
|
||||
"ButtonApplyChapters": "Aplikovat kapitoly",
|
||||
"ButtonAuthors": "Autoři",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "Vyhledat složku",
|
||||
"ButtonCancel": "Zrušit",
|
||||
"ButtonCancelEncode": "Zrušit kódování",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "Previous Chapter",
|
||||
"ButtonPurgeAllCache": "Vyčistit veškerou mezipaměť",
|
||||
"ButtonPurgeItemsCache": "Vyčistit mezipaměť položek",
|
||||
"ButtonPurgeMediaProgress": "Vyčistit průběh médií",
|
||||
"ButtonQueueAddItem": "Přidat do fronty",
|
||||
"ButtonQueueRemoveItem": "Odstranit z fronty",
|
||||
"ButtonQuickMatch": "Rychlé přiřazení",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "Položky kolekce",
|
||||
"HeaderCover": "Obálka",
|
||||
"HeaderCurrentDownloads": "Aktuální stahování",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||
"HeaderDetails": "Podrobnosti",
|
||||
"HeaderDownloadQueue": "Fronta stahování",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "Upravit",
|
||||
"LabelEmail": "E-mail",
|
||||
"LabelEmailSettingsFromAddress": "Z adresy",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "Zabezpečené",
|
||||
"LabelEmailSettingsSecureHelp": "Pokud je true, připojení bude při připojování k serveru používat TLS. Pokud je false, použije se protokol TLS, pokud server podporuje rozšíření STARTTLS. Ve většině případů nastavte tuto hodnotu na true, pokud se připojujete k portu 465. Pro port 587 nebo 25 ponechte hodnotu false. (z nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Testovací adresa",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "Položka",
|
||||
"LabelLanguage": "Jazyk",
|
||||
"LabelLanguageDefaultServer": "Výchozí jazyk serveru",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "Poslední kniha přidána",
|
||||
"LabelLastBookUpdated": "Poslední kniha aktualizována",
|
||||
"LabelLastSeen": "Naposledy viděno",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "Méně",
|
||||
"LabelLibrariesAccessibleToUser": "Knihovny přístupné uživateli",
|
||||
"LabelLibrary": "Knihovna",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "Položka knihovny",
|
||||
"LabelLibraryName": "Název knihovny",
|
||||
"LabelLimit": "Omezit",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "Nové heslo",
|
||||
"LabelNextBackupDate": "Datum příští zálohy",
|
||||
"LabelNextScheduledRun": "Další naplánované spuštění",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "Nebyly vybrány žádné epizody",
|
||||
"LabelNotes": "Poznámky",
|
||||
"LabelNotFinished": "Nedokončeno",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "Může nahrávat",
|
||||
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||
"LabelPhotoPathURL": "Cesta k fotografii/URL",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "Seznamy skladeb",
|
||||
"LabelPlayMethod": "Metoda přehrávání",
|
||||
"LabelPodcast": "Podcast",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "Poskytovatel",
|
||||
"LabelPubDate": "Datum vydání",
|
||||
"LabelPublisher": "Vydavatel",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "Rok vydání",
|
||||
"LabelRead": "Číst",
|
||||
"LabelReadAgain": "Číst znovu",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "Rychlá párování se pokusí přidat chybějící obálky a metadata pro vybrané položky. Povolením níže uvedených možností umožníte funkci Rychlé párování přepsat stávající obálky a/nebo metadata.",
|
||||
"MessageBookshelfNoCollections": "Ještě jste nevytvořili žádnou sbírku",
|
||||
"MessageBookshelfNoResultsForFilter": "Filtr \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "Nejsou otevřeny žádné RSS kanály",
|
||||
"MessageBookshelfNoSeries": "Nemáte žádnou sérii",
|
||||
"MessageChapterEndIsAfter": "Konec kapitoly přesahuje konec audioknihy",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "Opravdu chcete označit všechny knihy z této série jako dokončené?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Opravdu chcete označit všechny knihy z této série jako nedokončené?",
|
||||
"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?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "Varování! Rychlé vložení nezálohuje vaše zvukové soubory. Ujistěte se, že máte zálohu zvukových souborů. <br><br>Chcete pokračovat?",
|
||||
"MessageConfirmRemoveAllChapters": "Opravdu chcete odstranit všechny kapitoly?",
|
||||
"MessageConfirmRemoveAuthor": "Opravdu chcete odstranit autora \"{0}\"?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "Anvend",
|
||||
"ButtonApplyChapters": "Anvend kapitler",
|
||||
"ButtonAuthors": "Forfattere",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "Gennemse mappe",
|
||||
"ButtonCancel": "Annuller",
|
||||
"ButtonCancelEncode": "Annuller kodning",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "Previous Chapter",
|
||||
"ButtonPurgeAllCache": "Ryd al cache",
|
||||
"ButtonPurgeItemsCache": "Ryd elementcache",
|
||||
"ButtonPurgeMediaProgress": "Ryd Medieforløb",
|
||||
"ButtonQueueAddItem": "Tilføj til kø",
|
||||
"ButtonQueueRemoveItem": "Fjern fra kø",
|
||||
"ButtonQuickMatch": "Hurtig Match",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "Samlingselementer",
|
||||
"HeaderCover": "Omslag",
|
||||
"HeaderCurrentDownloads": "Nuværende Downloads",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||
"HeaderDetails": "Detaljer",
|
||||
"HeaderDownloadQueue": "Download Kø",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "Rediger",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "Fra Adresse",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "Sikker",
|
||||
"LabelEmailSettingsSecureHelp": "Hvis sandt, vil forbindelsen bruge TLS ved tilslutning til serveren. Hvis falsk, bruges TLS, hvis serveren understøtter STARTTLS-udvidelsen. I de fleste tilfælde skal denne værdi sættes til sandt, hvis du tilslutter til port 465. Til port 587 eller 25 skal du holde det falsk. (fra nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test Adresse",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "Element",
|
||||
"LabelLanguage": "Sprog",
|
||||
"LabelLanguageDefaultServer": "Standard server sprog",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "Senest tilføjede bog",
|
||||
"LabelLastBookUpdated": "Senest opdaterede bog",
|
||||
"LabelLastSeen": "Sidst set",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "Mindre",
|
||||
"LabelLibrariesAccessibleToUser": "Biblioteker tilgængelige for bruger",
|
||||
"LabelLibrary": "Bibliotek",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "Bibliotekselement",
|
||||
"LabelLibraryName": "Biblioteksnavn",
|
||||
"LabelLimit": "Grænse",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "Nyt kodeord",
|
||||
"LabelNextBackupDate": "Næste sikkerhedskopi dato",
|
||||
"LabelNextScheduledRun": "Næste planlagte kørsel",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "Ingen episoder valgt",
|
||||
"LabelNotes": "Noter",
|
||||
"LabelNotFinished": "Ikke færdig",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "Kan uploade",
|
||||
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||
"LabelPhotoPathURL": "Foto sti/URL",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "Afspilningslister",
|
||||
"LabelPlayMethod": "Afspilningsmetode",
|
||||
"LabelPodcast": "Podcast",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "Udbyder",
|
||||
"LabelPubDate": "Udgivelsesdato",
|
||||
"LabelPublisher": "Forlag",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "Udgivelsesår",
|
||||
"LabelRead": "Læst",
|
||||
"LabelReadAgain": "Læs igen",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "Quick Match vil forsøge at tilføje manglende omslag og metadata til de valgte elementer. Aktivér indstillingerne nedenfor for at tillade Quick Match at overskrive eksisterende omslag og/eller metadata.",
|
||||
"MessageBookshelfNoCollections": "Du har ikke oprettet nogen samlinger endnu",
|
||||
"MessageBookshelfNoResultsForFilter": "Ingen resultater for filter \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "Ingen RSS-feeds er åbne",
|
||||
"MessageBookshelfNoSeries": "Du har ingen serier",
|
||||
"MessageChapterEndIsAfter": "Kapitelslutningen er efter slutningen af din lydbog",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "Er du sikker på, at du vil markere alle bøger i denne serie som afsluttet?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Er du sikker på, at du vil markere alle bøger i denne serie som ikke afsluttet?",
|
||||
"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?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?",
|
||||
"MessageConfirmRemoveAllChapters": "Er du sikker på, at du vil fjerne alle kapitler?",
|
||||
"MessageConfirmRemoveAuthor": "Er du sikker på, at du vil fjerne forfatteren \"{0}\"?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "Übernehmen",
|
||||
"ButtonApplyChapters": "Kapitel anwenden",
|
||||
"ButtonAuthors": "Autoren",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "Ordnersuche",
|
||||
"ButtonCancel": "Abbrechen",
|
||||
"ButtonCancelEncode": "Codierung abbrechen",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "Vorheriges Kapitel",
|
||||
"ButtonPurgeAllCache": "Cache leeren",
|
||||
"ButtonPurgeItemsCache": "Lösche Medien-Cache",
|
||||
"ButtonPurgeMediaProgress": "Lösche Hörfortschritte",
|
||||
"ButtonQueueAddItem": "Zur Warteschlange hinzufügen",
|
||||
"ButtonQueueRemoveItem": "Aus der Warteschlange entfernen",
|
||||
"ButtonQuickMatch": "Schnellabgleich",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "Sammlungseinträge",
|
||||
"HeaderCover": "Titelbild",
|
||||
"HeaderCurrentDownloads": "Aktuelle Downloads",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "Benutzerdefinierte Metadata Anbieter",
|
||||
"HeaderDetails": "Details",
|
||||
"HeaderDownloadQueue": "Download Warteschlange",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "Bearbeiten",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "Von Adresse",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "Sicher",
|
||||
"LabelEmailSettingsSecureHelp": "Wenn \"an\", verwendet die Verbindung TLS, wenn du eine Verbindung zum Server herstellst. Bei \"aus\" wird TLS verwendet, wenn der Server die STARTTLS-Erweiterung unterstützt. In den meisten Fällen solltest du diesen Wert auf \"an\" schalten, wenn du eine Verbindung zu Port 465 herstellst. Für Port 587 oder 25 behalte den Wert \"aus\" bei. (von nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test Adresse",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "Medium",
|
||||
"LabelLanguage": "Sprache",
|
||||
"LabelLanguageDefaultServer": "Standard-Server-Sprache",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "Zuletzt hinzugefügtes Buch",
|
||||
"LabelLastBookUpdated": "Zuletzt aktualisiertes Buch",
|
||||
"LabelLastSeen": "Zuletzt gesehen",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "Weniger",
|
||||
"LabelLibrariesAccessibleToUser": "Für Benutzer zugängliche Bibliotheken",
|
||||
"LabelLibrary": "Bibliothek",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "Bibliothekseintrag",
|
||||
"LabelLibraryName": "Bibliotheksname",
|
||||
"LabelLimit": "Begrenzung",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "Neues Passwort",
|
||||
"LabelNextBackupDate": "Nächstes Sicherungsdatum",
|
||||
"LabelNextScheduledRun": "Nächster planmäßiger Durchlauf",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "Keine Episoden ausgewählt",
|
||||
"LabelNotes": "Notizen",
|
||||
"LabelNotFinished": "Nicht beendet",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "Hochladen",
|
||||
"LabelPersonalYearReview": "Dein Jahr in Übersicht ({0})",
|
||||
"LabelPhotoPathURL": "Foto Pfad/URL",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "Wiedergabelisten",
|
||||
"LabelPlayMethod": "Abspielmethode",
|
||||
"LabelPodcast": "Podcast",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "Anbieter",
|
||||
"LabelPubDate": "Veröffentlichungsdatum",
|
||||
"LabelPublisher": "Herausgeber",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "Jahr",
|
||||
"LabelRead": "Lesen",
|
||||
"LabelReadAgain": "Noch einmal Lesen",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "Der Schnellabgleich versucht, fehlende Titelbilder und Metadaten für die ausgewählten Artikel hinzuzufügen. Aktiviere die nachstehenden Optionen, damit der Schnellabgleich vorhandene Titelbilder und/oder Metadaten überschreiben kann.",
|
||||
"MessageBookshelfNoCollections": "Es wurden noch keine Sammlungen erstellt",
|
||||
"MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für Filter \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "Keine RSS-Feeds geöffnet",
|
||||
"MessageBookshelfNoSeries": "Keine Serien vorhanden",
|
||||
"MessageChapterEndIsAfter": "Ungültige Kapitelendzeit: Kapitelende > Mediumende (Kapitelende liegt nach dem Ende des Mediums)",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "Alle Medien dieser Reihe werden als abgeschlossen markiert! Bist du dir sicher?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Alle Medien dieser Reihe werden als nicht abgeschlossen markiert! Bist du dir sicher?",
|
||||
"MessageConfirmPurgeCache": "Cache leeren wird das ganze Verzeichnis <code>/metadata/cache</code> löschen. <br /><br />Bist du dir sicher, dass das Cache Verzeichnis gelöscht werden soll?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "Warnung! Audiodateien werden bei der Schnelleinbettung nicht gesichert! Achte darauf, dass du eine Sicherungskopie der Audiodateien besitzt. <br><br>Möchtest du fortfahren?",
|
||||
"MessageConfirmRemoveAllChapters": "Alle Kapitel werden entfernt! Bist du dir sicher?",
|
||||
"MessageConfirmRemoveAuthor": "Autor \"{0}\" wird enfernt! Bist du dir sicher?",
|
||||
|
|
@ -807,4 +817,4 @@
|
|||
"ToastSortingPrefixesUpdateSuccess": "Die Sortier-Prefixe wirden geupdated ({0} Einträge)",
|
||||
"ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden",
|
||||
"ToastUserDeleteSuccess": "Benutzer gelöscht"
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "Apply",
|
||||
"ButtonApplyChapters": "Apply Chapters",
|
||||
"ButtonAuthors": "Authors",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "Browse for Folder",
|
||||
"ButtonCancel": "Cancel",
|
||||
"ButtonCancelEncode": "Cancel Encode",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "Previous Chapter",
|
||||
"ButtonPurgeAllCache": "Purge All Cache",
|
||||
"ButtonPurgeItemsCache": "Purge Items Cache",
|
||||
"ButtonPurgeMediaProgress": "Purge Media Progress",
|
||||
"ButtonQueueAddItem": "Add to queue",
|
||||
"ButtonQueueRemoveItem": "Remove from queue",
|
||||
"ButtonQuickMatch": "Quick Match",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "Collection Items",
|
||||
"HeaderCover": "Cover",
|
||||
"HeaderCurrentDownloads": "Current Downloads",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||
"HeaderDetails": "Details",
|
||||
"HeaderDownloadQueue": "Download Queue",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "Edit",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "From Address",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "Secure",
|
||||
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test Address",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "Item",
|
||||
"LabelLanguage": "Language",
|
||||
"LabelLanguageDefaultServer": "Default Server Language",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "Last Book Added",
|
||||
"LabelLastBookUpdated": "Last Book Updated",
|
||||
"LabelLastSeen": "Last Seen",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "Less",
|
||||
"LabelLibrariesAccessibleToUser": "Libraries Accessible to User",
|
||||
"LabelLibrary": "Library",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "Library Item",
|
||||
"LabelLibraryName": "Library Name",
|
||||
"LabelLimit": "Limit",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "New Password",
|
||||
"LabelNextBackupDate": "Next backup date",
|
||||
"LabelNextScheduledRun": "Next scheduled run",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "No episodes selected",
|
||||
"LabelNotes": "Notes",
|
||||
"LabelNotFinished": "Not Finished",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "Can Upload",
|
||||
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||
"LabelPhotoPathURL": "Photo Path/URL",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "Playlists",
|
||||
"LabelPlayMethod": "Play Method",
|
||||
"LabelPodcast": "Podcast",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "Provider",
|
||||
"LabelPubDate": "Pub Date",
|
||||
"LabelPublisher": "Publisher",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "Publish Year",
|
||||
"LabelRead": "Read",
|
||||
"LabelReadAgain": "Read Again",
|
||||
|
|
@ -590,7 +598,8 @@
|
|||
"MessageBackupsDescription": "Backups include users, user progress, library item details, server settings, and images stored in <code>/metadata/items</code> & <code>/metadata/authors</code>. Backups <strong>do not</strong> include any files stored in your library folders.",
|
||||
"MessageBatchQuickMatchDescription": "Quick Match will attempt to add missing covers and metadata for the selected items. Enable the options below to allow Quick Match to overwrite existing covers and/or metadata.",
|
||||
"MessageBookshelfNoCollections": "You haven't made any collections yet",
|
||||
"MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForFilter": "No results for filter \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "No RSS feeds are open",
|
||||
"MessageBookshelfNoSeries": "You have no series",
|
||||
"MessageChapterEndIsAfter": "Chapter end is after the end of your audiobook",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||
"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?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?",
|
||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "Aplicar",
|
||||
"ButtonApplyChapters": "Aplicar Capítulos",
|
||||
"ButtonAuthors": "Autores",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "Buscar por Carpeta",
|
||||
"ButtonCancel": "Cancelar",
|
||||
"ButtonCancelEncode": "Cancelar Codificador",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "Capítulo Anterior",
|
||||
"ButtonPurgeAllCache": "Purgar Todo el Cache",
|
||||
"ButtonPurgeItemsCache": "Purgar Elementos de Cache",
|
||||
"ButtonPurgeMediaProgress": "Purgar Progreso de Multimedia",
|
||||
"ButtonQueueAddItem": "Agregar a la Fila",
|
||||
"ButtonQueueRemoveItem": "Remover de la Fila",
|
||||
"ButtonQuickMatch": "Encontrar Rápido",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "Elementos en la Colección",
|
||||
"HeaderCover": "Portada",
|
||||
"HeaderCurrentDownloads": "Descargando Actualmente",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "Proveedores de metadatos personalizados",
|
||||
"HeaderDetails": "Detalles",
|
||||
"HeaderDownloadQueue": "Lista de Descarga",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "Editar",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "Remitente",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "Seguridad",
|
||||
"LabelEmailSettingsSecureHelp": "Si está activado, se usará TLS para conectarse al servidor. Si está apagado, se usará TLS si su servidor tiene soporte para la extensión STARTTLS. En la mayoría de los casos, puede dejar esta opción activada si se está conectando al puerto 465. Apáguela en el caso de usar los puertos 587 o 25. (de nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Probar Dirección",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "Elemento",
|
||||
"LabelLanguage": "Lenguaje",
|
||||
"LabelLanguageDefaultServer": "Lenguaje Predeterminado del Servidor",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "Último Libro Agregado",
|
||||
"LabelLastBookUpdated": "Último Libro Actualizado",
|
||||
"LabelLastSeen": "Última Vez Visto",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "Menos",
|
||||
"LabelLibrariesAccessibleToUser": "Bibliotecas Disponibles para el Usuario",
|
||||
"LabelLibrary": "Biblioteca",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "Elemento de Biblioteca",
|
||||
"LabelLibraryName": "Nombre de Biblioteca",
|
||||
"LabelLimit": "Limites",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "Nueva Contraseña",
|
||||
"LabelNextBackupDate": "Fecha del Siguiente Respaldo",
|
||||
"LabelNextScheduledRun": "Próxima Ejecución Programada",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "Ningún Episodio Seleccionado",
|
||||
"LabelNotes": "Notas",
|
||||
"LabelNotFinished": "No Terminado",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "Puede Subir",
|
||||
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||
"LabelPhotoPathURL": "Ruta de Acceso/URL de Foto",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "Lista de Reproducción",
|
||||
"LabelPlayMethod": "Método de Reproducción",
|
||||
"LabelPodcast": "Podcast",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "Proveedor",
|
||||
"LabelPubDate": "Fecha de Publicación",
|
||||
"LabelPublisher": "Editor",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "Año de Publicación",
|
||||
"LabelRead": "Leído",
|
||||
"LabelReadAgain": "Volver a leer",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "\"Encontrar Rápido\" tratará de agregar portadas y metadatos faltantes de los elementos seleccionados. Habilite la opción de abajo para que \"Encontrar Rápido\" pueda sobrescribir portadas y/o metadatos existentes.",
|
||||
"MessageBookshelfNoCollections": "No tienes ninguna colección.",
|
||||
"MessageBookshelfNoResultsForFilter": "Ningún Resultado para el filtro \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "Ninguna Fuente RSS esta abierta",
|
||||
"MessageBookshelfNoSeries": "No tienes ninguna serie",
|
||||
"MessageChapterEndIsAfter": "El final del capítulo es después del final de su audiolibro.",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"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?",
|
||||
"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é?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"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?",
|
||||
"MessageConfirmRemoveAuthor": "¿Está seguro de que desea remover el autor \"{0}\"?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "Rakenda",
|
||||
"ButtonApplyChapters": "Rakenda peatükid",
|
||||
"ButtonAuthors": "Autorid",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "Sirvi kausta",
|
||||
"ButtonCancel": "Tühista",
|
||||
"ButtonCancelEncode": "Tühista kodeerimine",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "Eelmine peatükk",
|
||||
"ButtonPurgeAllCache": "Tühjenda kogu vahemälu",
|
||||
"ButtonPurgeItemsCache": "Tühjenda esemete vahemälu",
|
||||
"ButtonPurgeMediaProgress": "Tühjenda meedia edenemine",
|
||||
"ButtonQueueAddItem": "Lisa järjekorda",
|
||||
"ButtonQueueRemoveItem": "Eemalda järjekorrast",
|
||||
"ButtonQuickMatch": "Kiire sobitamine",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "Kogu esemed",
|
||||
"HeaderCover": "Ümbris",
|
||||
"HeaderCurrentDownloads": "Praegused allalaadimised",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "Kohandatud metaandmete pakkujad",
|
||||
"HeaderDetails": "Detailid",
|
||||
"HeaderDownloadQueue": "Allalaadimise järjekord",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "Muuda",
|
||||
"LabelEmail": "E-post",
|
||||
"LabelEmailSettingsFromAddress": "Saatja aadress",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "Turvaline",
|
||||
"LabelEmailSettingsSecureHelp": "Kui see on tõene, kasutab ühendus serveriga ühenduse loomisel TLS-i. Kui see on väär, kasutatakse TLS-i, kui server toetab STARTTLS-i laiendust. Enamikul juhtudest seadke see väärtus tõeks, kui ühendate pordile 465. Pordi 587 või 25 korral hoidke seda väär. (nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Testi aadress",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "Kirje",
|
||||
"LabelLanguage": "Keel",
|
||||
"LabelLanguageDefaultServer": "Vaikeserveri keel",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "Viimati lisatud raamat",
|
||||
"LabelLastBookUpdated": "Viimati uuendatud raamat",
|
||||
"LabelLastSeen": "Viimati nähtud",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "Vähem",
|
||||
"LabelLibrariesAccessibleToUser": "Kasutajale ligipääsetavad raamatukogud",
|
||||
"LabelLibrary": "Raamatukogu",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "Raamatukogu kirje",
|
||||
"LabelLibraryName": "Raamatukogu nimi",
|
||||
"LabelLimit": "Piirang",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "Uus parool",
|
||||
"LabelNextBackupDate": "Järgmine varukoopia kuupäev",
|
||||
"LabelNextScheduledRun": "Järgmine ajakava järgmine",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "Episoodid pole valitud",
|
||||
"LabelNotes": "Märkused",
|
||||
"LabelNotFinished": "Ei ole lõpetatud",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "Saab üles laadida",
|
||||
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||
"LabelPhotoPathURL": "Foto tee/URL",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "Mänguloendid",
|
||||
"LabelPlayMethod": "Esitusmeetod",
|
||||
"LabelPodcast": "Podcast",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "Pakkuja",
|
||||
"LabelPubDate": "Avaldamise kuupäev",
|
||||
"LabelPublisher": "Kirjastaja",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "Aasta avaldamine",
|
||||
"LabelRead": "Lugenud",
|
||||
"LabelReadAgain": "Loe uuesti",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "Kiire sobitamine üritab lisada valitud üksustele puuduvad kaaned ja metaandmed. Luba allpool olevad valikud, et lubada Kiire sobitamine'il üle kirjutada olemasolevaid kaasi ja/või metaandmeid.",
|
||||
"MessageBookshelfNoCollections": "Te pole veel ühtegi kogumit teinud",
|
||||
"MessageBookshelfNoResultsForFilter": "Filtrile \"{0}: {1}\" pole tulemusi",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "Ühtegi RSS-i voogu pole avatud",
|
||||
"MessageBookshelfNoSeries": "Teil pole ühtegi seeriat",
|
||||
"MessageChapterEndIsAfter": "Peatüki lõpp on pärast teie heliraamatu lõppu",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "Olete kindel, et soovite selle seeria kõik raamatud lõpetatuks märkida?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Olete kindel, et soovite selle seeria kõik raamatud mitte lõpetatuks märkida?",
|
||||
"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?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "Hoiatus! Quick Embed ei tee varukoopiaid teie helifailidest. Veenduge, et teil oleks varukoopia oma helifailidest. <br><br>Kas soovite jätkata?",
|
||||
"MessageConfirmRemoveAllChapters": "Olete kindel, et soovite eemaldada kõik peatükid?",
|
||||
"MessageConfirmRemoveAuthor": "Olete kindel, et soovite autori \"{0}\" eemaldada?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "Appliquer",
|
||||
"ButtonApplyChapters": "Appliquer aux chapitres",
|
||||
"ButtonAuthors": "Auteurs",
|
||||
"ButtonBack": "Retour",
|
||||
"ButtonBrowseForFolder": "Naviguer vers le répertoire",
|
||||
"ButtonCancel": "Annuler",
|
||||
"ButtonCancelEncode": "Annuler l’encodage",
|
||||
|
|
@ -54,9 +55,8 @@
|
|||
"ButtonPlaylists": "Listes de lecture",
|
||||
"ButtonPrevious": "Précédent",
|
||||
"ButtonPreviousChapter": "Chapitre précédent",
|
||||
"ButtonPurgeAllCache": "Purger le cache",
|
||||
"ButtonPurgeItemsCache": "Purger le cache des articles",
|
||||
"ButtonPurgeMediaProgress": "Purger la progression des médias",
|
||||
"ButtonPurgeAllCache": "Purger tout le cache",
|
||||
"ButtonPurgeItemsCache": "Purger le cache des éléments",
|
||||
"ButtonQueueAddItem": "Ajouter à la liste de lecture",
|
||||
"ButtonQueueRemoveItem": "Supprimer de la liste de lecture",
|
||||
"ButtonQuickMatch": "Recherche rapide",
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
"ButtonRefresh": "Rafraîchir",
|
||||
"ButtonRemove": "Supprimer",
|
||||
"ButtonRemoveAll": "Supprimer tout",
|
||||
"ButtonRemoveAllLibraryItems": "Supprimer tous les articles de la bibliothèque",
|
||||
"ButtonRemoveAllLibraryItems": "Supprimer tous les éléments de la bibliothèque",
|
||||
"ButtonRemoveFromContinueListening": "Ne plus continuer à écouter",
|
||||
"ButtonRemoveFromContinueReading": "Ne plus continuer à lire",
|
||||
"ButtonRemoveSeriesFromContinueSeries": "Ne plus continuer à écouter la série",
|
||||
|
|
@ -75,7 +75,7 @@
|
|||
"ButtonResetToDefault": "Réinitialiser aux valeurs par défaut",
|
||||
"ButtonRestore": "Rétablir",
|
||||
"ButtonSave": "Sauvegarder",
|
||||
"ButtonSaveAndClose": "Sauvegarder et Fermer",
|
||||
"ButtonSaveAndClose": "Sauvegarder et fermer",
|
||||
"ButtonSaveTracklist": "Sauvegarder la liste de lecture",
|
||||
"ButtonScan": "Analyser",
|
||||
"ButtonScanLibrary": "Analyser la bibliothèque",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "Entrées de la collection",
|
||||
"HeaderCover": "Couverture",
|
||||
"HeaderCurrentDownloads": "Téléchargements en cours",
|
||||
"HeaderCustomMessageOnLogin": "Message personnalisé lors de la connexion",
|
||||
"HeaderCustomMetadataProviders": "Fournisseurs de métadonnées personnalisés",
|
||||
"HeaderDetails": "Détails",
|
||||
"HeaderDownloadQueue": "File d’attente de téléchargements",
|
||||
|
|
@ -127,7 +128,7 @@
|
|||
"HeaderFiles": "Fichiers",
|
||||
"HeaderFindChapters": "Trouver les chapitres",
|
||||
"HeaderIgnoredFiles": "Fichiers ignorés",
|
||||
"HeaderItemFiles": "Fichiers des articles",
|
||||
"HeaderItemFiles": "Fichiers des éléments",
|
||||
"HeaderItemMetadataUtils": "Outils de gestion des métadonnées",
|
||||
"HeaderLastListeningSession": "Dernière session d’écoute",
|
||||
"HeaderLatestEpisodes": "Dernier épisodes",
|
||||
|
|
@ -173,8 +174,8 @@
|
|||
"HeaderSettingsGeneral": "Général",
|
||||
"HeaderSettingsScanner": "Analyseur",
|
||||
"HeaderSleepTimer": "Minuterie",
|
||||
"HeaderStatsLargestItems": "Articles les plus lourd",
|
||||
"HeaderStatsLongestItems": "Articles les plus long (heures)",
|
||||
"HeaderStatsLargestItems": "Éléments les plus grands",
|
||||
"HeaderStatsLongestItems": "Éléments les plus long (hrs)",
|
||||
"HeaderStatsMinutesListeningChart": "Minutes d’écoute (7 derniers jours)",
|
||||
"HeaderStatsRecentSessions": "Sessions récentes",
|
||||
"HeaderStatsTop10Authors": "Top 10 Auteurs",
|
||||
|
|
@ -191,7 +192,7 @@
|
|||
"LabelAbridged": "Version courte",
|
||||
"LabelAbridgedChecked": "Abrégé (vérifié)",
|
||||
"LabelAbridgedUnchecked": "Intégral (non vérifié)",
|
||||
"LabelAccessibleBy": "Accessible by",
|
||||
"LabelAccessibleBy": "Accessible par",
|
||||
"LabelAccountType": "Type de compte",
|
||||
"LabelAccountTypeAdmin": "Admin",
|
||||
"LabelAccountTypeGuest": "Invité",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "Modifier",
|
||||
"LabelEmail": "Courriel",
|
||||
"LabelEmailSettingsFromAddress": "Expéditeur",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Rejeter les certificats non autorisés",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Désactiver la validation du certificat SSL peut exposer votre connexion à des risques de sécurité, tels que des attaques de type « man-in-the-middle ». Ne désactivez cette option que si vous en comprenez les implications et si vous faites confiance au serveur de messagerie auquel vous vous connectez.",
|
||||
"LabelEmailSettingsSecure": "Sécurisé",
|
||||
"LabelEmailSettingsSecureHelp": "Utiliser TLS lors de la connexion au serveur, autrement TLS sera utilisé si le serveur prend en charge l’extension STARTTLS. Dans la plupart des cas, actviez l’option si vous vous connectez au port 465. Désactivez l’option pour utiliser port 587 ou 25. (source: nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Adresse de test",
|
||||
|
|
@ -332,9 +335,10 @@
|
|||
"LabelIntervalEveryDay": "Tous les jours",
|
||||
"LabelIntervalEveryHour": "Toutes les heures",
|
||||
"LabelInvert": "Inverser",
|
||||
"LabelItem": "Article",
|
||||
"LabelItem": "Élément",
|
||||
"LabelLanguage": "Langue",
|
||||
"LabelLanguageDefaultServer": "Langue par défaut",
|
||||
"LabelLanguages": "Langues",
|
||||
"LabelLastBookAdded": "Dernier livre ajouté",
|
||||
"LabelLastBookUpdated": "Dernier livre mis à jour",
|
||||
"LabelLastSeen": "Vu dernièrement",
|
||||
|
|
@ -346,17 +350,18 @@
|
|||
"LabelLess": "Moins",
|
||||
"LabelLibrariesAccessibleToUser": "Bibliothèque accessible à l’utilisateur",
|
||||
"LabelLibrary": "Bibliothèque",
|
||||
"LabelLibraryItem": "Article de bibliothèque",
|
||||
"LabelLibraryFilterSublistEmpty": "Aucun {0}",
|
||||
"LabelLibraryItem": "Élément de bibliothèque",
|
||||
"LabelLibraryName": "Nom de la bibliothèque",
|
||||
"LabelLimit": "Limite",
|
||||
"LabelLineSpacing": "Interligne",
|
||||
"LabelLineSpacing": "Espacement des lignes",
|
||||
"LabelListenAgain": "Écouter à nouveau",
|
||||
"LabelLogLevelDebug": "Debug",
|
||||
"LabelLogLevelInfo": "Info",
|
||||
"LabelLogLevelWarn": "Warn",
|
||||
"LabelLookForNewEpisodesAfterDate": "Chercher de nouveaux épisode après cette date",
|
||||
"LabelLookForNewEpisodesAfterDate": "Rechercher les nouveaux épisodes après cette date",
|
||||
"LabelLowestPriority": "Priorité la plus basse",
|
||||
"LabelMatchExistingUsersBy": "Faire correspondre les utilisateurs existants par",
|
||||
"LabelMatchExistingUsersBy": "Correspondance avec les utilisateurs existants",
|
||||
"LabelMatchExistingUsersByDescription": "Utilisé pour connecter les utilisateurs existants. Une fois connectés, les utilisateurs seront associés à un identifiant unique provenant de votre fournisseur SSO.",
|
||||
"LabelMediaPlayer": "Lecteur multimédia",
|
||||
"LabelMediaType": "Type de média",
|
||||
|
|
@ -366,8 +371,8 @@
|
|||
"LabelMetaTags": "Balises de métadonnée",
|
||||
"LabelMinute": "Minute",
|
||||
"LabelMissing": "Manquant",
|
||||
"LabelMissingEbook": "Ne possède pas de livre numérique",
|
||||
"LabelMissingSupplementaryEbook": "Ne possède pas de livre numérique supplémentaire",
|
||||
"LabelMissingEbook": "Ne possède aucun livre numérique",
|
||||
"LabelMissingSupplementaryEbook": "Ne possède aucun livre numérique supplémentaire",
|
||||
"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.",
|
||||
"LabelMore": "Plus",
|
||||
|
|
@ -381,12 +386,13 @@
|
|||
"LabelNewPassword": "Nouveau mot de passe",
|
||||
"LabelNextBackupDate": "Date de la prochaine sauvegarde",
|
||||
"LabelNextScheduledRun": "Prochain lancement prévu",
|
||||
"LabelNoCustomMetadataProviders": "Aucun fournisseurs de métadonnées personnalisés",
|
||||
"LabelNoEpisodesSelected": "Aucun épisode sélectionné",
|
||||
"LabelNotes": "Notes",
|
||||
"LabelNotFinished": "Non terminé",
|
||||
"LabelNotificationAppriseURL": "URL(s) d’Apprise",
|
||||
"LabelNotificationAvailableVariables": "Variables disponibles",
|
||||
"LabelNotificationBodyTemplate": "Modèle de Message",
|
||||
"LabelNotificationBodyTemplate": "Modèle de message",
|
||||
"LabelNotificationEvent": "Evènement de Notification",
|
||||
"LabelNotificationsMaxFailedAttempts": "Nombres de tentatives d’envoi",
|
||||
"LabelNotificationsMaxFailedAttemptsHelp": "La notification est abandonnée une fois ce seuil atteint",
|
||||
|
|
@ -410,8 +416,9 @@
|
|||
"LabelPermissionsDownload": "Peut télécharger",
|
||||
"LabelPermissionsUpdate": "Peut mettre à jour",
|
||||
"LabelPermissionsUpload": "Peut téléverser",
|
||||
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||
"LabelPersonalYearReview": "Bilan de l’année ({0})",
|
||||
"LabelPhotoPathURL": "Chemin / URL des photos",
|
||||
"LabelPlayerChapterNumberMarker": "{0} sur {1}",
|
||||
"LabelPlaylists": "Listes de lecture",
|
||||
"LabelPlayMethod": "Méthode d’écoute",
|
||||
"LabelPodcast": "Podcast",
|
||||
|
|
@ -426,7 +433,8 @@
|
|||
"LabelProvider": "Fournisseur",
|
||||
"LabelPubDate": "Date de publication",
|
||||
"LabelPublisher": "Éditeur",
|
||||
"LabelPublishYear": "Année d’édition",
|
||||
"LabelPublishers": "Éditeurs",
|
||||
"LabelPublishYear": "Année de publication",
|
||||
"LabelRead": "Lire",
|
||||
"LabelReadAgain": "Lire à nouveau",
|
||||
"LabelReadEbookWithoutProgress": "Lire le livre numérique sans sauvegarder la progression",
|
||||
|
|
@ -462,7 +470,7 @@
|
|||
"LabelSetEbookAsSupplementary": "Définir comme supplémentaire",
|
||||
"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.",
|
||||
"LabelSettingsBookshelfViewHelp": "Interface skeuomorphique avec une étagère en bois",
|
||||
"LabelSettingsBookshelfViewHelp": "Interface skeumorphique avec étagères en bois",
|
||||
"LabelSettingsChromecastSupport": "Support du Chromecast",
|
||||
"LabelSettingsDateFormat": "Format de date",
|
||||
"LabelSettingsDisableWatcher": "Désactiver la surveillance",
|
||||
|
|
@ -471,43 +479,43 @@
|
|||
"LabelSettingsEnableWatcher": "Activer la veille",
|
||||
"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",
|
||||
"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.",
|
||||
"LabelSettingsEpubsAllowScriptedContent": "Autoriser le contenu scénarisé pour les fichiers EPUB",
|
||||
"LabelSettingsEpubsAllowScriptedContentHelp": "Autoriser les fichiers EPUB à exécuter des scripts. Il est recommandé de laisser ce paramètre désactivé, sauf si vous faites confiance à la source des fichiers EPUB.",
|
||||
"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.",
|
||||
"LabelSettingsFindCovers": "Chercher des couvertures de livre",
|
||||
"LabelSettingsFindCoversHelp": "Si votre livre audio ne possède pas de couverture intégrée ou une image de couverture dans le dossier, l’analyseur tentera de récupérer une couverture.<br>Attention, cela peut augmenter le temps d’analyse.",
|
||||
"LabelSettingsFindCoversHelp": "Si votre livre audio ne possède aucune couverture intégrée ou une image de couverture dans le dossier, l’analyseur tentera de récupérer une couverture.<br>Attention, cela peut augmenter le temps d’analyse.",
|
||||
"LabelSettingsHideSingleBookSeries": "Masquer les séries de livres uniques",
|
||||
"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",
|
||||
"LabelSettingsLibraryBookshelfView": "La bibliothèque utilise la vue étagère",
|
||||
"LabelSettingsHomePageBookshelfView": "Utiliser la vue étagère sur la page d’accueil",
|
||||
"LabelSettingsLibraryBookshelfView": "Utiliser la vue étagère pour la bibliothèque",
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Sauter les livres précédents dans « Continuer la série »",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"LabelSettingsSkipMatchingBooksWithISBN": "Ignorer la recherche par correspondance sur les livres ayant déjà un ISBN",
|
||||
"LabelSettingsPreferMatchedMetadataHelp": "Les métadonnées mises en correspondance remplaceront les détails de l’élément lors de l’utilisation de la correspondance rapide. Par défaut, la correspondance rapide ne remplira que les détails manquants.",
|
||||
"LabelSettingsSkipMatchingBooksWithASIN": "Ignorer la recherche par correspondance pour les livres ayant déjà un ASIN",
|
||||
"LabelSettingsSkipMatchingBooksWithISBN": "Ignorer la recherche par correspondance pour les livres ayant déjà un ISBN",
|
||||
"LabelSettingsSortingIgnorePrefixes": "Ignorer les préfixes lors du tri",
|
||||
"LabelSettingsSortingIgnorePrefixesHelp": "c’est-à-dire : pour le préfixe « le », le livre avec pour titre « Le Titre du Livre » sera trié en tant que « Titre du Livre, Le »",
|
||||
"LabelSettingsSquareBookCovers": "Utiliser des couvertures carrées",
|
||||
"LabelSettingsSquareBookCoversHelp": "Préférer les couvertures carrées par rapport aux couvertures standards de ratio 1.6:1.",
|
||||
"LabelSettingsStoreCoversWithItem": "Enregistrer la couverture avec les articles",
|
||||
"LabelSettingsStoreCoversWithItemHelp": "Par défaut, les couvertures sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les couvertures dans le dossier avec les fichiers de l’article. Seul un fichier nommé « cover » sera conservé.",
|
||||
"LabelSettingsStoreMetadataWithItem": "Enregistrer les Métadonnées avec les articles",
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les métadonnées sont enregistrées dans /metadata/items",
|
||||
"LabelSettingsStoreCoversWithItem": "Enregistrer la couverture avec les éléments",
|
||||
"LabelSettingsStoreCoversWithItemHelp": "Par défaut, les couvertures sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les couvertures dans le dossier avec les fichiers de élément. Seul un fichier nommé « cover » sera conservé.",
|
||||
"LabelSettingsStoreMetadataWithItem": "Enregistrer les métadonnées avec l’élément",
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les fichiers de métadonnées sont stockés dans /metadata/items. En activant ce paramètre, les fichiers de métadonnées seront stockés dans les dossiers des éléments de votre bibliothèque.",
|
||||
"LabelSettingsTimeFormat": "Format d’heure",
|
||||
"LabelShowAll": "Tout afficher",
|
||||
"LabelShowSeconds": "Afficher le seondes",
|
||||
"LabelShowSeconds": "Afficher les seondes",
|
||||
"LabelSize": "Taille",
|
||||
"LabelSleepTimer": "Minuterie",
|
||||
"LabelSleepTimer": "Minuterie de mise en veille",
|
||||
"LabelSlug": "Balise",
|
||||
"LabelStart": "Démarrer",
|
||||
"LabelStarted": "Démarré",
|
||||
"LabelStartedAt": "Démarré à",
|
||||
"LabelStartTime": "Heure de démarrage",
|
||||
"LabelStatsAudioTracks": "Pistes Audios",
|
||||
"LabelStatsAudioTracks": "Pistes audio",
|
||||
"LabelStatsAuthors": "Auteurs",
|
||||
"LabelStatsBestDay": "Meilleur jour",
|
||||
"LabelStatsDailyAverage": "Moyenne journalière",
|
||||
|
|
@ -515,8 +523,8 @@
|
|||
"LabelStatsDaysListened": "Jours d’écoute",
|
||||
"LabelStatsHours": "Heures",
|
||||
"LabelStatsInARow": "d’affilée(s)",
|
||||
"LabelStatsItemsFinished": "Articles terminés",
|
||||
"LabelStatsItemsInLibrary": "Articles dans la bibliothèque",
|
||||
"LabelStatsItemsFinished": "Élément(s) terminé(s)",
|
||||
"LabelStatsItemsInLibrary": "Éléments dans la bibliothèque",
|
||||
"LabelStatsMinutes": "minutes",
|
||||
"LabelStatsMinutesListening": "Minutes d’écoute",
|
||||
"LabelStatsOverallDays": "Nombre total de jours",
|
||||
|
|
@ -587,10 +595,11 @@
|
|||
"LabelYourProgress": "Votre progression",
|
||||
"MessageAddToPlayerQueue": "Ajouter en file d’attente",
|
||||
"MessageAppriseDescription": "Nécessite une instance d’<a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API Apprise</a> pour utiliser cette fonctionnalité ou une api qui prend en charge les mêmes requêtes.<br>L’URL de l’API Apprise doit comprendre le chemin complet pour envoyer la notification. Par exemple, si votre instance écoute sur <code>http://192.168.1.1:8337</code> alors vous devez mettre <code>http://192.168.1.1:8337/notify</code>.",
|
||||
"MessageBackupsDescription": "Les sauvegardes incluent les utilisateurs, la progression de lecture par utilisateur, les détails des articles des bibliothèques, les paramètres du serveur et les images sauvegardées. Les sauvegardes n’incluent pas les fichiers de votre bibliothèque.",
|
||||
"MessageBatchQuickMatchDescription": "La recherche par correspondance rapide tentera d’ajouter les couvertures et les métadonnées manquantes pour les articles sélectionnés. Activer l’option suivante pour autoriser la recherche par correspondance à écraser les données existantes.",
|
||||
"MessageBackupsDescription": "Les sauvegardes incluent les utilisateurs, la progression des utilisateurs, les détails des éléments de la bibliothèque, les paramètres du serveur et les images stockées dans <code>/metadata/items</code> & <code>/metadata/authors</code>. Les sauvegardes <strong>n’incluent pas</strong> les fichiers stockés dans les dossiers de votre bibliothèque.",
|
||||
"MessageBatchQuickMatchDescription": "La recherche par correspondance rapide tentera d’ajouter les couvertures et métadonnées manquantes pour les éléments sélectionnés. Activez les options ci-dessous pour permettre la Recherche par correspondance d’écraser les couvertures et/ou métadonnées existantes.",
|
||||
"MessageBookshelfNoCollections": "Vous n’avez pas encore de collections",
|
||||
"MessageBookshelfNoResultsForFilter": "Aucun résultat pour le filtre « {0} : {1} »",
|
||||
"MessageBookshelfNoResultsForQuery": "Aucun résultat pour la requête",
|
||||
"MessageBookshelfNoRSSFeeds": "Aucun flux RSS n’est ouvert",
|
||||
"MessageBookshelfNoSeries": "Vous n’avez aucune série",
|
||||
"MessageChapterEndIsAfter": "La fin du chapitre se situe après la fin de votre livre audio.",
|
||||
|
|
@ -612,20 +621,21 @@
|
|||
"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 ?",
|
||||
"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 ?",
|
||||
"MessageConfirmPurgeItemsCache": "Purger le cache des éléments supprimera l'ensemble du répertoire <code>/metadata/cache/items</code>.<br>Êtes-vous sûr ?",
|
||||
"MessageConfirmQuickEmbed": "Attention ! L'intégration rapide ne permet pas de sauvegarder 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 ?",
|
||||
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
|
||||
"MessageConfirmRemoveAuthor": "Êtes-vous sûr de vouloir supprimer l’auteur « {0} » ?",
|
||||
"MessageConfirmRemoveCollection": "Êtes-vous sûr de vouloir supprimer la collection « {0} » ?",
|
||||
"MessageConfirmRemoveEpisode": "Êtes-vous sûr de vouloir supprimer l’épisode « {0} » ?",
|
||||
"MessageConfirmRemoveEpisodes": "Êtes-vous sûr de vouloir supprimer {0} épisodes ?",
|
||||
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
|
||||
"MessageConfirmRemoveNarrator": "Êtes-vous sûr de vouloir supprimer le narrateur « {0} » ?",
|
||||
"MessageConfirmRemovePlaylist": "Êtes-vous sûr de vouloir supprimer la liste de lecture « {0} » ?",
|
||||
"MessageConfirmRenameGenre": "Êtes-vous sûr de vouloir renommer le genre « {0} » en « {1} » pour tous les articles ?",
|
||||
"MessageConfirmRenameGenreMergeNote": "Information: Ce genre existe déjà et sera fusionné.",
|
||||
"MessageConfirmRenameGenre": "Êtes-vous sûr de vouloir renommer le genre « {0} » en « {1} » pour tous les éléments ?",
|
||||
"MessageConfirmRenameGenreMergeNote": "Information : ce genre existe déjà et sera fusionné.",
|
||||
"MessageConfirmRenameGenreWarning": "Attention ! Un genre similaire avec une casse différente existe déjà « {0} ».",
|
||||
"MessageConfirmRenameTag": "Êtes-vous sûr de vouloir renommer l’étiquette « {0} » en « {1} » pour tous les articles ?",
|
||||
"MessageConfirmRenameTagMergeNote": "Information: Cette étiquette existe déjà et sera fusionnée.",
|
||||
"MessageConfirmRenameTag": "Êtes-vous sûr de vouloir renommer l’étiquette « {0} » en « {1} » pour tous les éléments ?",
|
||||
"MessageConfirmRenameTagMergeNote": "Information : Cette étiquette existe déjà et sera fusionnée.",
|
||||
"MessageConfirmRenameTagWarning": "Attention ! Une étiquette similaire avec une casse différente existe déjà « {0} ».",
|
||||
"MessageConfirmReScanLibraryItems": "Êtes-vous sûr de vouloir re-analyser {0} éléments ?",
|
||||
"MessageConfirmSendEbookToDevice": "Êtes-vous sûr de vouloir envoyer le livre numérique {0} « {1} » à l’appareil « {2} »?",
|
||||
|
|
@ -633,22 +643,22 @@
|
|||
"MessageDragFilesIntoTrackOrder": "Faites glisser les fichiers dans l’ordre correct des pistes",
|
||||
"MessageEmbedFinished": "Intégration terminée !",
|
||||
"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.",
|
||||
"MessageEreaderDevices": "Pour garantir l’envoie des livres électroniques, il se peut que vous deviez ajouter l’adresse électronique ci-dessus en tant qu’expéditeur valide pour chaque appareil répertorié ci-dessous.",
|
||||
"MessageFeedURLWillBe": "L’URL du flux sera {0}",
|
||||
"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.",
|
||||
"MessageImportantNotice": "Information importante !",
|
||||
"MessageInsertChapterBelow": "Insérer le chapitre ci-dessous",
|
||||
"MessageItemsSelected": "{0} articles sélectionnés",
|
||||
"MessageItemsUpdated": "{0} articles mis à jour",
|
||||
"MessageItemsSelected": "{0} éléments sélectionnés",
|
||||
"MessageItemsUpdated": "{0} éléments mis à jour",
|
||||
"MessageJoinUsOn": "Rejoignez-nous sur",
|
||||
"MessageListeningSessionsInTheLastYear": "{0} sessions d’écoute l’an dernier",
|
||||
"MessageLoading": "Chargement…",
|
||||
"MessageLoadingFolders": "Chargement des dossiers…",
|
||||
"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",
|
||||
"MessageM4BFinished": "M4B terminé",
|
||||
"MessageMapChapterTitles": "Faire correspondre les titres des chapitres aux chapitres existants de votre livre audio sans ajuster l’horodatage.",
|
||||
"MessageM4BFailed": "M4B a échoué !",
|
||||
"MessageM4BFinished": "M4B terminé !",
|
||||
"MessageMapChapterTitles": "Faire correspondre les titres de chapitres avec ceux de vos livres audio existants sans ajuster les horodatages.",
|
||||
"MessageMarkAllEpisodesFinished": "Marquer tous les épisodes terminés",
|
||||
"MessageMarkAllEpisodesNotFinished": "Marquer tous les épisodes non terminés",
|
||||
"MessageMarkAsFinished": "Marquer comme terminé",
|
||||
|
|
@ -663,14 +673,14 @@
|
|||
"MessageNoCoversFound": "Aucune couverture trouvée",
|
||||
"MessageNoDescription": "Aucune description",
|
||||
"MessageNoDownloadsInProgress": "Aucun téléchargement en cours",
|
||||
"MessageNoDownloadsQueued": "Aucun téléchargement en file d’attente",
|
||||
"MessageNoDownloadsQueued": "Aucun téléchargement en attente",
|
||||
"MessageNoEpisodeMatchesFound": "Aucune correspondance d’épisode trouvée",
|
||||
"MessageNoEpisodes": "Aucun épisode",
|
||||
"MessageNoFoldersAvailable": "Aucun dossier disponible",
|
||||
"MessageNoFoldersAvailable": "Aucun dossiers disponible",
|
||||
"MessageNoGenres": "Aucun genre",
|
||||
"MessageNoIssues": "Aucune parution",
|
||||
"MessageNoItems": "Aucun article",
|
||||
"MessageNoItemsFound": "Aucun article trouvé",
|
||||
"MessageNoItems": "Aucun élément",
|
||||
"MessageNoItemsFound": "Aucun élément trouvé",
|
||||
"MessageNoListeningSessions": "Aucune session d’écoute en cours",
|
||||
"MessageNoLogs": "Aucun journaux",
|
||||
"MessageNoMediaProgress": "Aucun média en cours",
|
||||
|
|
@ -679,7 +689,7 @@
|
|||
"MessageNoResults": "Aucun résultat",
|
||||
"MessageNoSearchResultsFor": "Aucun résultat pour la recherche « {0} »",
|
||||
"MessageNoSeries": "Aucune série",
|
||||
"MessageNoTags": "Aucune d’étiquettes",
|
||||
"MessageNoTags": "Aucune étiquette",
|
||||
"MessageNoTasksRunning": "Aucune tâche en cours",
|
||||
"MessageNotYetImplemented": "Non implémenté",
|
||||
"MessageNoUpdateNecessary": "Aucune mise à jour nécessaire",
|
||||
|
|
@ -753,8 +763,8 @@
|
|||
"ToastCachePurgeSuccess": "Cache purgé avec succès",
|
||||
"ToastChaptersHaveErrors": "Les chapitres contiennent des erreurs",
|
||||
"ToastChaptersMustHaveTitles": "Les chapitre doivent avoir un titre",
|
||||
"ToastCollectionItemsRemoveFailed": "Échec de la suppression de(s) article(s) de la collection",
|
||||
"ToastCollectionItemsRemoveSuccess": "Article(s) supprimé(s) de la collection",
|
||||
"ToastCollectionItemsRemoveFailed": "Échec de la suppression d’un ou plusieurs éléments de la collection",
|
||||
"ToastCollectionItemsRemoveSuccess": "Élément(s) supprimé(s) de la collection",
|
||||
"ToastCollectionRemoveFailed": "Échec de la suppression de la collection",
|
||||
"ToastCollectionRemoveSuccess": "Collection supprimée",
|
||||
"ToastCollectionUpdateFailed": "Échec de la mise à jour de la collection",
|
||||
|
|
@ -762,11 +772,11 @@
|
|||
"ToastDeleteFileFailed": "Échec de la suppression du fichier",
|
||||
"ToastDeleteFileSuccess": "Fichier supprimé",
|
||||
"ToastFailedToLoadData": "Échec du chargement des données",
|
||||
"ToastItemCoverUpdateFailed": "Échec de la mise à jour de la couverture de l’article",
|
||||
"ToastItemCoverUpdateSuccess": "Couverture de l’article mise à jour",
|
||||
"ToastItemDetailsUpdateFailed": "Échec de la mise à jour des détails de l’article",
|
||||
"ToastItemDetailsUpdateSuccess": "Détails de l’article mis à jour",
|
||||
"ToastItemDetailsUpdateUnneeded": "Pas de mise à jour nécessaire sur les détails de l’article",
|
||||
"ToastItemCoverUpdateFailed": "Échec de la mise à jour de la couverture de l’élément",
|
||||
"ToastItemCoverUpdateSuccess": "Couverture mise à jour",
|
||||
"ToastItemDetailsUpdateFailed": "Échec de la mise à jour des détails de l’élément",
|
||||
"ToastItemDetailsUpdateSuccess": "Détails de l’élément mis à jour",
|
||||
"ToastItemDetailsUpdateUnneeded": "Aucune mise à jour n’est nécessaire pour les détails de l’élément",
|
||||
"ToastItemMarkedAsFinishedFailed": "Échec de l’annotation terminée",
|
||||
"ToastItemMarkedAsFinishedSuccess": "Article marqué comme terminé",
|
||||
"ToastItemMarkedAsNotFinishedFailed": "Échec de l’annotation non-terminée",
|
||||
|
|
@ -785,10 +795,10 @@
|
|||
"ToastPlaylistRemoveSuccess": "Liste de lecture supprimée",
|
||||
"ToastPlaylistUpdateFailed": "Échec de la mise à jour de la liste de lecture",
|
||||
"ToastPlaylistUpdateSuccess": "Liste de lecture mise à jour",
|
||||
"ToastPodcastCreateFailed": "Échec de la création du Podcast",
|
||||
"ToastPodcastCreateSuccess": "Podcast créé",
|
||||
"ToastRemoveItemFromCollectionFailed": "Échec de la suppression de l’article de la collection",
|
||||
"ToastRemoveItemFromCollectionSuccess": "Article supprimé de la collection",
|
||||
"ToastPodcastCreateFailed": "Échec de la création du podcast",
|
||||
"ToastPodcastCreateSuccess": "Podcast créé avec succès",
|
||||
"ToastRemoveItemFromCollectionFailed": "Échec de la suppression d’un élément de la collection",
|
||||
"ToastRemoveItemFromCollectionSuccess": "Élément supprimé de la collection",
|
||||
"ToastRSSFeedCloseFailed": "Échec de la fermeture du flux RSS",
|
||||
"ToastRSSFeedCloseSuccess": "Flux RSS fermé",
|
||||
"ToastSendEbookToDeviceFailed": "Échec de l’envoi du livre numérique à l’appareil",
|
||||
|
|
@ -807,4 +817,4 @@
|
|||
"ToastSortingPrefixesUpdateSuccess": "Mise à jour des préfixes de tri ({0} élément)",
|
||||
"ToastUserDeleteFailed": "Échec de la suppression de l’utilisateur",
|
||||
"ToastUserDeleteSuccess": "Utilisateur supprimé"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "લાગુ કરો",
|
||||
"ButtonApplyChapters": "પ્રકરણો લાગુ કરો",
|
||||
"ButtonAuthors": "લેખકો",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "ફોલ્ડર માટે જુઓ",
|
||||
"ButtonCancel": "રદ કરો",
|
||||
"ButtonCancelEncode": "એન્કોડ રદ કરો",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "Previous Chapter",
|
||||
"ButtonPurgeAllCache": "બધો Cache કાઢી નાખો",
|
||||
"ButtonPurgeItemsCache": "વસ્તુઓનો Cache કાઢી નાખો",
|
||||
"ButtonPurgeMediaProgress": "બધું સાંભળ્યું કાઢી નાખો",
|
||||
"ButtonQueueAddItem": "કતારમાં ઉમેરો",
|
||||
"ButtonQueueRemoveItem": "કતારથી કાઢી નાખો",
|
||||
"ButtonQuickMatch": "ઝડપી મેળ ખવડાવો",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "સંગ્રહ વસ્તુઓ",
|
||||
"HeaderCover": "આવરણ",
|
||||
"HeaderCurrentDownloads": "વર્તમાન ડાઉનલોડ્સ",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||
"HeaderDetails": "વિગતો",
|
||||
"HeaderDownloadQueue": "ડાઉનલોડ કતાર",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "Edit",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "From Address",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "Secure",
|
||||
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test Address",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "Item",
|
||||
"LabelLanguage": "Language",
|
||||
"LabelLanguageDefaultServer": "Default Server Language",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "Last Book Added",
|
||||
"LabelLastBookUpdated": "Last Book Updated",
|
||||
"LabelLastSeen": "Last Seen",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "Less",
|
||||
"LabelLibrariesAccessibleToUser": "Libraries Accessible to User",
|
||||
"LabelLibrary": "Library",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "Library Item",
|
||||
"LabelLibraryName": "Library Name",
|
||||
"LabelLimit": "Limit",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "New Password",
|
||||
"LabelNextBackupDate": "Next backup date",
|
||||
"LabelNextScheduledRun": "Next scheduled run",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "No episodes selected",
|
||||
"LabelNotes": "Notes",
|
||||
"LabelNotFinished": "Not Finished",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "Can Upload",
|
||||
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||
"LabelPhotoPathURL": "Photo Path/URL",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "Playlists",
|
||||
"LabelPlayMethod": "Play Method",
|
||||
"LabelPodcast": "Podcast",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "Provider",
|
||||
"LabelPubDate": "Pub Date",
|
||||
"LabelPublisher": "Publisher",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "Publish Year",
|
||||
"LabelRead": "Read",
|
||||
"LabelReadAgain": "Read Again",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "Quick Match will attempt to add missing covers and metadata for the selected items. Enable the options below to allow Quick Match to overwrite existing covers and/or metadata.",
|
||||
"MessageBookshelfNoCollections": "You haven't made any collections yet",
|
||||
"MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "No RSS feeds are open",
|
||||
"MessageBookshelfNoSeries": "You have no series",
|
||||
"MessageChapterEndIsAfter": "Chapter end is after the end of your audiobook",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||
"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?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?",
|
||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "החל",
|
||||
"ButtonApplyChapters": "החל פרקים",
|
||||
"ButtonAuthors": "יוצרים",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "עיין בתיקייה",
|
||||
"ButtonCancel": "בטל",
|
||||
"ButtonCancelEncode": "בטל קידוד",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "פרק קודם",
|
||||
"ButtonPurgeAllCache": "נקה את כל המטמון",
|
||||
"ButtonPurgeItemsCache": "נקה את מטמון הפריטים",
|
||||
"ButtonPurgeMediaProgress": "נקה את ההתקדמות במדיה",
|
||||
"ButtonQueueAddItem": "הוסף לתור",
|
||||
"ButtonQueueRemoveItem": "הסר מהתור",
|
||||
"ButtonQuickMatch": "התאמה מהירה",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "פריטי אוסף",
|
||||
"HeaderCover": "כריכה",
|
||||
"HeaderCurrentDownloads": "הורדות נוכחיות",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "ספקי מטא-נתונים מותאמים אישית",
|
||||
"HeaderDetails": "פרטים",
|
||||
"HeaderDownloadQueue": "תור הורדה",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "עריכה",
|
||||
"LabelEmail": "דואר אלקטרוני",
|
||||
"LabelEmailSettingsFromAddress": "מאת",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "מאובטח",
|
||||
"LabelEmailSettingsSecureHelp": "אם מופעל, החיבור ישתמש ב-TLS בעת ההתחברות לשרת. אם לא, אז TLS יהיה בשימוש אם השרת תומך בהרחבת STARTTLS. ברוב המקרים מומלץ להפעיל את הגדרה זו אם אתה מתחבר לפורט 465. לפורט 587 או 25, השאר כבוי. (from nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "כתובת לבדיקה",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "פריט",
|
||||
"LabelLanguage": "שפה",
|
||||
"LabelLanguageDefaultServer": "שפת ברירת המחדל של השרת",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "הספר האחרון שנוסף",
|
||||
"LabelLastBookUpdated": "הספר האחרון שעודכן",
|
||||
"LabelLastSeen": "נראה לאחרונה",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "פחות",
|
||||
"LabelLibrariesAccessibleToUser": "ספריות נגישות למשתמש",
|
||||
"LabelLibrary": "ספרייה",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "פריט ספרייה",
|
||||
"LabelLibraryName": "שם הספרייה",
|
||||
"LabelLimit": "מגבלה",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "סיסמה חדשה",
|
||||
"LabelNextBackupDate": "תאריך הגיבוי הבא",
|
||||
"LabelNextScheduledRun": "הרצה מתוזמנת הבאה",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "לא נבחרו פרקים",
|
||||
"LabelNotes": "הערות",
|
||||
"LabelNotFinished": "לא הושלם",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "מותר להעלות",
|
||||
"LabelPersonalYearReview": "השנה שלך בסקירה ({0})",
|
||||
"LabelPhotoPathURL": "נתיב/URL לתמונה",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "רשימות השמעה",
|
||||
"LabelPlayMethod": "שיטת הפעלה",
|
||||
"LabelPodcast": "פודקאסט",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "ספק",
|
||||
"LabelPubDate": "תאריך פרסום",
|
||||
"LabelPublisher": "מוציא לאור",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "שנת הפרסום",
|
||||
"LabelRead": "קריאה",
|
||||
"LabelReadAgain": "קרא שוב",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "התאמה מהירה תנסה להוסיף כריכות ומטה-נתונים חסרים עבור הפריטים הנבחרים. הפעל את האפשרויות למטה כדי לאפשר להתאמה מהירה להחליף כריכות קיימות ו/או מטה-נתונים.",
|
||||
"MessageBookshelfNoCollections": "עדיין לא יצרת אוספים",
|
||||
"MessageBookshelfNoResultsForFilter": "אין תוצאות עבור סינון \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "אין ערוצי RSS פתוחים",
|
||||
"MessageBookshelfNoSeries": "אין לך סדרות",
|
||||
"MessageChapterEndIsAfter": "זמן סיום הפרק אחרי סיום הספר הקולי שלך",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "האם אתה בטוח שברצונך לסמן את כל הספרים בסדרה זו כהסתיימו?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "האם אתה בטוח שברצונך לסמן את כל הספרים בסדרה זו כלא הסתיימו?",
|
||||
"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?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "אזהרה! הטמעה מהירה לא תגבה גיבוי של קבצי האודיו שלך. וודא שיש לך גיבוי של קבצי האודיו שלך. <br><br>האם ברצונך להמשיך?",
|
||||
"MessageConfirmRemoveAllChapters": "האם אתה בטוח שברצונך להסיר את כל הפרקים?",
|
||||
"MessageConfirmRemoveAuthor": "האם אתה בטוח שברצונך להסיר את המחבר \"{0}\"?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "लागू करें",
|
||||
"ButtonApplyChapters": "अध्यायों में परिवर्तन लागू करें",
|
||||
"ButtonAuthors": "लेखक",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "फ़ोल्डर खोजें",
|
||||
"ButtonCancel": "रद्द करें",
|
||||
"ButtonCancelEncode": "एनकोड रद्द करें",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "Previous Chapter",
|
||||
"ButtonPurgeAllCache": "सभी Cache मिटाएं",
|
||||
"ButtonPurgeItemsCache": "आइटम Cache मिटाएं",
|
||||
"ButtonPurgeMediaProgress": "अभी तक सुना हुआ सब हटा दे",
|
||||
"ButtonQueueAddItem": "क़तार में जोड़ें",
|
||||
"ButtonQueueRemoveItem": "कतार से हटाएं",
|
||||
"ButtonQuickMatch": "जल्दी से समानता की तलाश करें",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "Collection Items",
|
||||
"HeaderCover": "Cover",
|
||||
"HeaderCurrentDownloads": "Current Downloads",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||
"HeaderDetails": "Details",
|
||||
"HeaderDownloadQueue": "Download Queue",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "Edit",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "From Address",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "Secure",
|
||||
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test Address",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "Item",
|
||||
"LabelLanguage": "Language",
|
||||
"LabelLanguageDefaultServer": "Default Server Language",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "Last Book Added",
|
||||
"LabelLastBookUpdated": "Last Book Updated",
|
||||
"LabelLastSeen": "Last Seen",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "Less",
|
||||
"LabelLibrariesAccessibleToUser": "Libraries Accessible to User",
|
||||
"LabelLibrary": "Library",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "Library Item",
|
||||
"LabelLibraryName": "Library Name",
|
||||
"LabelLimit": "Limit",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "New Password",
|
||||
"LabelNextBackupDate": "Next backup date",
|
||||
"LabelNextScheduledRun": "Next scheduled run",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "No episodes selected",
|
||||
"LabelNotes": "Notes",
|
||||
"LabelNotFinished": "Not Finished",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "Can Upload",
|
||||
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||
"LabelPhotoPathURL": "Photo Path/URL",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "Playlists",
|
||||
"LabelPlayMethod": "Play Method",
|
||||
"LabelPodcast": "Podcast",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "Provider",
|
||||
"LabelPubDate": "Pub Date",
|
||||
"LabelPublisher": "Publisher",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "Publish Year",
|
||||
"LabelRead": "Read",
|
||||
"LabelReadAgain": "Read Again",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "Quick Match will attempt to add missing covers and metadata for the selected items. Enable the options below to allow Quick Match to overwrite existing covers and/or metadata.",
|
||||
"MessageBookshelfNoCollections": "You haven't made any collections yet",
|
||||
"MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "No RSS feeds are open",
|
||||
"MessageBookshelfNoSeries": "You have no series",
|
||||
"MessageChapterEndIsAfter": "Chapter end is after the end of your audiobook",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||
"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?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?",
|
||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "Primijeni",
|
||||
"ButtonApplyChapters": "Primijeni poglavlja",
|
||||
"ButtonAuthors": "Autori",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "Browse for Folder",
|
||||
"ButtonCancel": "Odustani",
|
||||
"ButtonCancelEncode": "Otkaži kodiranje",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "Previous Chapter",
|
||||
"ButtonPurgeAllCache": "Isprazni sav cache",
|
||||
"ButtonPurgeItemsCache": "Isprazni Items Cache",
|
||||
"ButtonPurgeMediaProgress": "Purge Media Progress",
|
||||
"ButtonQueueAddItem": "Add to queue",
|
||||
"ButtonQueueRemoveItem": "Remove from queue",
|
||||
"ButtonQuickMatch": "Brzi match",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "Stvari u kolekciji",
|
||||
"HeaderCover": "Cover",
|
||||
"HeaderCurrentDownloads": "Current Downloads",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||
"HeaderDetails": "Detalji",
|
||||
"HeaderDownloadQueue": "Download Queue",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "Uredi",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "From Address",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "Secure",
|
||||
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test Address",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "Stavka",
|
||||
"LabelLanguage": "Jezik",
|
||||
"LabelLanguageDefaultServer": "Default jezik servera",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "Last Book Added",
|
||||
"LabelLastBookUpdated": "Last Book Updated",
|
||||
"LabelLastSeen": "Zadnje pogledano",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "Manje",
|
||||
"LabelLibrariesAccessibleToUser": "Biblioteke pristupačne korisniku",
|
||||
"LabelLibrary": "Biblioteka",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "Stavka biblioteke",
|
||||
"LabelLibraryName": "Ime biblioteke",
|
||||
"LabelLimit": "Limit",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "Nova lozinka",
|
||||
"LabelNextBackupDate": "Next backup date",
|
||||
"LabelNextScheduledRun": "Next scheduled run",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "No episodes selected",
|
||||
"LabelNotes": "Bilješke",
|
||||
"LabelNotFinished": "Nedovršeno",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "Smije uploadati",
|
||||
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||
"LabelPhotoPathURL": "Slika putanja/URL",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "Playlists",
|
||||
"LabelPlayMethod": "Vrsta reprodukcije",
|
||||
"LabelPodcast": "Podcast",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "Dobavljač",
|
||||
"LabelPubDate": "Datam izdavanja",
|
||||
"LabelPublisher": "Izdavač",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "Godina izdavanja",
|
||||
"LabelRead": "Read",
|
||||
"LabelReadAgain": "Read Again",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "Quick Match će probati dodati nedostale covere i metapodatke za odabrane stavke. Uključi postavke ispod da omočutie Quick Mathchu da zamijeni postojeće covere i/ili metapodatke.",
|
||||
"MessageBookshelfNoCollections": "You haven't made any collections yet",
|
||||
"MessageBookshelfNoResultsForFilter": "No Results for filter \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "No RSS feeds are open",
|
||||
"MessageBookshelfNoSeries": "You have no series",
|
||||
"MessageChapterEndIsAfter": "Kraj poglavlja je nakon kraja audioknjige.",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||
"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?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?",
|
||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "Alkalmaz",
|
||||
"ButtonApplyChapters": "Fejezetek alkalmazása",
|
||||
"ButtonAuthors": "Szerzők",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "Mappa keresése",
|
||||
"ButtonCancel": "Mégse",
|
||||
"ButtonCancelEncode": "Kódolás megszakítása",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "Előző fejezet",
|
||||
"ButtonPurgeAllCache": "Összes gyorsítótár törlése",
|
||||
"ButtonPurgeItemsCache": "Elemek gyorsítótárának törlése",
|
||||
"ButtonPurgeMediaProgress": "Médialejátszás állapotának törlése",
|
||||
"ButtonQueueAddItem": "Hozzáadás a sorhoz",
|
||||
"ButtonQueueRemoveItem": "Eltávolítás a sorból",
|
||||
"ButtonQuickMatch": "Gyors egyeztetés",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "Gyűjtemény elemek",
|
||||
"HeaderCover": "Borító",
|
||||
"HeaderCurrentDownloads": "Jelenlegi letöltések",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "Egyéni metaadat-szolgáltatók",
|
||||
"HeaderDetails": "Részletek",
|
||||
"HeaderDownloadQueue": "Letöltési sor",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "Szerkesztés",
|
||||
"LabelEmail": "E-mail",
|
||||
"LabelEmailSettingsFromAddress": "Feladó címe",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "Biztonságos",
|
||||
"LabelEmailSettingsSecureHelp": "Ha igaz, a kapcsolat TLS-t használ a szerverhez való csatlakozáskor. Ha hamis, akkor TLS-t használ, ha a szerver támogatja a STARTTLS kiterjesztést. A legtöbb esetben állítsa ezt az értéket igazra, ha a 465-ös portra csatlakozik. A 587-es vagy 25-ös port esetében tartsa hamis értéken. (a nodemailer.com/smtp/#authentication oldalról)",
|
||||
"LabelEmailSettingsTestAddress": "Teszt cím",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "Elem",
|
||||
"LabelLanguage": "Nyelv",
|
||||
"LabelLanguageDefaultServer": "Szerver alapértelmezett nyelve",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "Utolsó hozzáadott könyv",
|
||||
"LabelLastBookUpdated": "Utolsó frissített könyv",
|
||||
"LabelLastSeen": "Utolsó látogatás",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "Kevesebb",
|
||||
"LabelLibrariesAccessibleToUser": "A felhasználó számára elérhető könyvtárak",
|
||||
"LabelLibrary": "Könyvtár",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "Könyvtári elem",
|
||||
"LabelLibraryName": "Könyvtár neve",
|
||||
"LabelLimit": "Korlát",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "Új jelszó",
|
||||
"LabelNextBackupDate": "Következő biztonsági másolat dátuma",
|
||||
"LabelNextScheduledRun": "Következő ütemezett futtatás",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "Nincsenek kiválasztott epizódok",
|
||||
"LabelNotes": "Megjegyzések",
|
||||
"LabelNotFinished": "Nem befejezett",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "Feltölthet",
|
||||
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||
"LabelPhotoPathURL": "Fénykép útvonal/URL",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "Lejátszási listák",
|
||||
"LabelPlayMethod": "Lejátszási módszer",
|
||||
"LabelPodcast": "Podcast",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "Szolgáltató",
|
||||
"LabelPubDate": "Kiadás dátuma",
|
||||
"LabelPublisher": "Kiadó",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "Kiadás éve",
|
||||
"LabelRead": "Olvasás",
|
||||
"LabelReadAgain": "Újraolvasás",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "A Gyors egyeztetés megpróbálja hozzáadni a hiányzó borítókat és metaadatokat a kiválasztott elemekhez. Engedélyezze az alábbi opciókat, hogy a Gyors egyeztetés felülírhassa a meglévő borítókat és/vagy metaadatokat.",
|
||||
"MessageBookshelfNoCollections": "Még nem készített gyűjteményeket",
|
||||
"MessageBookshelfNoResultsForFilter": "Nincs eredmény a \"{0}: {1}\" szűrőre",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "Nincsenek nyitott RSS hírcsatornák",
|
||||
"MessageBookshelfNoSeries": "Nincsenek sorozatai",
|
||||
"MessageChapterEndIsAfter": "A fejezet vége a hangoskönyv végét követi",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "Biztosan meg szeretné jelölni a sorozat összes könyvét befejezettnek?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Biztosan meg szeretné jelölni a sorozat összes könyvét nem befejezettnek?",
|
||||
"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?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "Figyelem! A Gyors beágyazás nem készít biztonsági másolatot az audiofájlokról. Győződjön meg arról, hogy van biztonsági másolata az audiofájlokról. <br><br>Szeretné folytatni?",
|
||||
"MessageConfirmRemoveAllChapters": "Biztosan eltávolítja az összes fejezetet?",
|
||||
"MessageConfirmRemoveAuthor": "Biztosan eltávolítja a(z) \"{0}\" szerzőt?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "Applica",
|
||||
"ButtonApplyChapters": "Applica",
|
||||
"ButtonAuthors": "Autori",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "Per Cartella",
|
||||
"ButtonCancel": "Cancella",
|
||||
"ButtonCancelEncode": "Ferma la codifica",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "Capitolo Precendente",
|
||||
"ButtonPurgeAllCache": "Elimina tutta la Cache",
|
||||
"ButtonPurgeItemsCache": "Elimina la Cache selezionata",
|
||||
"ButtonPurgeMediaProgress": "Elimina info dei media ascoltati",
|
||||
"ButtonQueueAddItem": "Aggiungi alla Coda",
|
||||
"ButtonQueueRemoveItem": "Rimuovi dalla Coda",
|
||||
"ButtonQuickMatch": "Controlla Metadata Auto",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "Elementi della Raccolta",
|
||||
"HeaderCover": "Cover",
|
||||
"HeaderCurrentDownloads": "Download Correnti",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": " Metadata Providers Personalizzato",
|
||||
"HeaderDetails": "Dettagli",
|
||||
"HeaderDownloadQueue": "Download coda",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "Modifica",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "Da Indirizzo",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "Secure",
|
||||
"LabelEmailSettingsSecureHelp": "Se vero, la connessione utilizzerà TLS durante la connessione al server. Se false, viene utilizzato TLS se il server supporta l'estensione STARTTLS. Nella maggior parte dei casi impostare questo valore su true se ci si connette alla porta 465. Per la porta 587 o 25 mantenerlo false. (da nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test Indirizzo",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "Oggetti",
|
||||
"LabelLanguage": "Lingua",
|
||||
"LabelLanguageDefaultServer": "Lingua di Default",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "Ultimo Libro Aggiunto",
|
||||
"LabelLastBookUpdated": "Ultimo Libro Aggiornato",
|
||||
"LabelLastSeen": "Ultimi Visti",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "Poco",
|
||||
"LabelLibrariesAccessibleToUser": "Librerie Accessibili agli Utenti",
|
||||
"LabelLibrary": "Libreria",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "Elementi della Library",
|
||||
"LabelLibraryName": "Nome Libreria",
|
||||
"LabelLimit": "Limiti",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "Nuova Password",
|
||||
"LabelNextBackupDate": "Data Prossimo Backup",
|
||||
"LabelNextScheduledRun": "Data prossima esecuzione schedulata",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "Nessun Episodio Selezionato",
|
||||
"LabelNotes": "Note",
|
||||
"LabelNotFinished": "Da Completare",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "Può caricare",
|
||||
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||
"LabelPhotoPathURL": "foto Path/URL",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "Playlists",
|
||||
"LabelPlayMethod": "Metodo di riproduzione",
|
||||
"LabelPodcast": "Podcast",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "Provider",
|
||||
"LabelPubDate": "Data Pubblicazione",
|
||||
"LabelPublisher": "Editore",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "Anno Pubblicazione",
|
||||
"LabelRead": "Leggi",
|
||||
"LabelReadAgain": "Leggi Ancora",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "Quick Match tenterà di aggiungere copertine e metadati mancanti per gli elementi selezionati. Attiva l'opzione per consentire a Quick Match di sovrascrivere copertine e/o metadati esistenti.",
|
||||
"MessageBookshelfNoCollections": "Non hai ancora creato nessuna raccolta ",
|
||||
"MessageBookshelfNoResultsForFilter": "Nessun risultato per il filtro \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "Nessun RSS feeds aperto",
|
||||
"MessageBookshelfNoSeries": "Non c'è nessuna Serie",
|
||||
"MessageChapterEndIsAfter": "La fine del capitolo è dopo la fine del tuo audiolibro",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come completati?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come non completati?",
|
||||
"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?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "Attenzione! L'incorporamento rapido non eseguirà il backup dei file audio. Assicurati di avere un backup dei tuoi file audio. <br><br>Vuoi Continuare?",
|
||||
"MessageConfirmRemoveAllChapters": "Sei sicuro di voler rimuovere tutti i capitoli?",
|
||||
"MessageConfirmRemoveAuthor": "Sei sicuro di voler rimuovere l'autore? \"{0}\"?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "Taikyti",
|
||||
"ButtonApplyChapters": "Taikyti skyrius",
|
||||
"ButtonAuthors": "Autoriai",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "Naršyti aplanko",
|
||||
"ButtonCancel": "Atšaukti",
|
||||
"ButtonCancelEncode": "Atšaukti kodavimą",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "Previous Chapter",
|
||||
"ButtonPurgeAllCache": "Valyti visą saugyklą",
|
||||
"ButtonPurgeItemsCache": "Valyti elementų saugyklą",
|
||||
"ButtonPurgeMediaProgress": "Valyti medijos progresą",
|
||||
"ButtonQueueAddItem": "Pridėti į eilę",
|
||||
"ButtonQueueRemoveItem": "Pašalinti iš eilės",
|
||||
"ButtonQuickMatch": "Greitas pritaikymas",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "Kolekcijos elementai",
|
||||
"HeaderCover": "Viršelis",
|
||||
"HeaderCurrentDownloads": "Dabartiniai parsisiuntimai",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||
"HeaderDetails": "Detalės",
|
||||
"HeaderDownloadQueue": "Parsisiuntimo eilė",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "Redaguoti",
|
||||
"LabelEmail": "El. paštas",
|
||||
"LabelEmailSettingsFromAddress": "Siuntėjo adresas",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "Apsaugota",
|
||||
"LabelEmailSettingsSecureHelp": "Jei ši reikšmė yra \"true\", ryšys naudos TLS protokolą. Jei \"false\", TLS bus naudojamas tik tada, jei serveris palaiko STARTTLS plėtinį. Daugumos atveju, jei jungiamasi prie 465 prievado, šią reikšmę turėtumėte nustatyti kaip \"true\". Jei jungiamasi prie 587 arba 25 prievado, turi būti nustatyta \"false\". (iš nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Testinis adresas",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "Elementas",
|
||||
"LabelLanguage": "Kalba",
|
||||
"LabelLanguageDefaultServer": "Numatytoji serverio kalba",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "Paskutinė pridėta knyga",
|
||||
"LabelLastBookUpdated": "Paskutinė atnaujinta knyga",
|
||||
"LabelLastSeen": "Paskutinį kartą matyta",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "Mažiau",
|
||||
"LabelLibrariesAccessibleToUser": "Naudotojui pasiekiamos bibliotekos",
|
||||
"LabelLibrary": "Biblioteka",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "Bibliotekos elementas",
|
||||
"LabelLibraryName": "Bibliotekos pavadinimas",
|
||||
"LabelLimit": "Limitas",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "Naujas slaptažodis",
|
||||
"LabelNextBackupDate": "Kitos atsarginės kopijos data",
|
||||
"LabelNextScheduledRun": "Kito planuoto vykdymo data",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "Nepasirinkti jokie epizodai",
|
||||
"LabelNotes": "Užrašai",
|
||||
"LabelNotFinished": "Nebaigta",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "Gali įkelti",
|
||||
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||
"LabelPhotoPathURL": "Nuotraukos kelias/URL",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "Grojaraščiai",
|
||||
"LabelPlayMethod": "Grojimo metodas",
|
||||
"LabelPodcast": "Tinklalaidė",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "Tiekėjas",
|
||||
"LabelPubDate": "Publikavimo data",
|
||||
"LabelPublisher": "Leidėjas",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "Leidimo metai",
|
||||
"LabelRead": "Skaityta",
|
||||
"LabelReadAgain": "Skaityti dar kartą",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "Greitas atitikmens rasti bandys pridėti trūkstamus viršelius ir metaduomenis pasirinktiems elementams. Įjunkite žemiau esančias parinktis, kad leistumėte Greitajam atitikmeniui perrašyti esamus viršelius ir/ar metaduomenis.",
|
||||
"MessageBookshelfNoCollections": "Dar nepridėjote jokių kolekcijų",
|
||||
"MessageBookshelfNoResultsForFilter": "Rezultatų pagal filtrą \"{0}: {1}\" nėra",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "Nėra atvertų RSS srautų",
|
||||
"MessageBookshelfNoSeries": "Neturite jokių serijų",
|
||||
"MessageChapterEndIsAfter": "Skyriaus pabaiga yra po jūsų garso knygos pabaigos",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "Ar tikrai norite pažymėti visas knygas šioje serijoje kaip užbaigtas?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Ar tikrai norite pažymėti visas knygas šioje serijoje kaip nebaigtas?",
|
||||
"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?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?",
|
||||
"MessageConfirmRemoveAllChapters": "Ar tikrai norite pašalinti visus skyrius?",
|
||||
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "Pas toe",
|
||||
"ButtonApplyChapters": "Hoofdstukken toepassen",
|
||||
"ButtonAuthors": "Auteurs",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "Bladeren naar map",
|
||||
"ButtonCancel": "Annuleren",
|
||||
"ButtonCancelEncode": "Encoding annuleren",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "Previous Chapter",
|
||||
"ButtonPurgeAllCache": "Volledige cache legen",
|
||||
"ButtonPurgeItemsCache": "Onderdelen-cache legen",
|
||||
"ButtonPurgeMediaProgress": "Mediavoortgang legen",
|
||||
"ButtonQueueAddItem": "In wachtrij zetten",
|
||||
"ButtonQueueRemoveItem": "Uit wachtrij verwijderen",
|
||||
"ButtonQuickMatch": "Snelle match",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "Collectie-objecten",
|
||||
"HeaderCover": "Cover",
|
||||
"HeaderCurrentDownloads": "Huidige downloads",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||
"HeaderDetails": "Details",
|
||||
"HeaderDownloadQueue": "Download-wachtrij",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "Wijzig",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "Van-adres",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "Veilig",
|
||||
"LabelEmailSettingsSecureHelp": "Als 'waar', dan gebruikt de verbinding TLS om met de server te verbinden. Als 'onwaar', dan wordt TLS gebruikt als de server de STARTTLS-extensie ondersteunt. In de meeste gevallen kies je voor 'waar' verbindt met poort 465. Voo poort 587 of 25, laat op 'onwaar'. (van nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test-adres",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "Onderdeel",
|
||||
"LabelLanguage": "Taal",
|
||||
"LabelLanguageDefaultServer": "Standaard servertaal",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "Laatst toegevoegde boek",
|
||||
"LabelLastBookUpdated": "Laatst bijgewerkte boek",
|
||||
"LabelLastSeen": "Laatst gezien",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "Minder",
|
||||
"LabelLibrariesAccessibleToUser": "Voor gebruiker toegankelijke bibliotheken",
|
||||
"LabelLibrary": "Bibliotheek",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "Bibliotheekonderdeel",
|
||||
"LabelLibraryName": "Bibliotheeknaam",
|
||||
"LabelLimit": "Limiet",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "Nieuw wachtwoord",
|
||||
"LabelNextBackupDate": "Volgende back-up datum",
|
||||
"LabelNextScheduledRun": "Volgende geplande run",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "Geen afleveringen geselecteerd",
|
||||
"LabelNotes": "Notities",
|
||||
"LabelNotFinished": "Niet Voltooid",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "Kan uploaden",
|
||||
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||
"LabelPhotoPathURL": "Foto pad/URL",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "Afspeellijsten",
|
||||
"LabelPlayMethod": "Afspeelwijze",
|
||||
"LabelPodcast": "Podcast",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "Bron",
|
||||
"LabelPubDate": "Publicatiedatum",
|
||||
"LabelPublisher": "Uitgever",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "Jaar van uitgave",
|
||||
"LabelRead": "Lees",
|
||||
"LabelReadAgain": "Lees opnieuw",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "Quick Match zal proberen ontbrekende covers en metadata voor de geselecteerde onderdelen te matchten. Schakel de opties hieronder in om Quick Match toe te staan bestaande covers en/of metadata te overschrijven.",
|
||||
"MessageBookshelfNoCollections": "Je hebt nog geen collecties gemaakt",
|
||||
"MessageBookshelfNoResultsForFilter": "Geen resultaten voor filter \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "Geen RSS-feeds geopend",
|
||||
"MessageBookshelfNoSeries": "Je hebt geen series",
|
||||
"MessageChapterEndIsAfter": "Hoofdstukeinde is na het einde van je audioboek",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als voltooid?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als niet voltooid?",
|
||||
"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?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?",
|
||||
"MessageConfirmRemoveAllChapters": "Weet je zeker dat je alle hoofdstukken wil verwijderen?",
|
||||
"MessageConfirmRemoveAuthor": "Weet je zeker dat je auteur \"{0}\" wil verwijderen?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "Bruk",
|
||||
"ButtonApplyChapters": "Bruk kapittel",
|
||||
"ButtonAuthors": "Forfatter",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "Bla gjennom mappe",
|
||||
"ButtonCancel": "Avbryt",
|
||||
"ButtonCancelEncode": "Avbryt Encode",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "Previous Chapter",
|
||||
"ButtonPurgeAllCache": "Tøm alle mellomlager",
|
||||
"ButtonPurgeItemsCache": "Tøm mellomlager",
|
||||
"ButtonPurgeMediaProgress": "Slett medie fremgang",
|
||||
"ButtonQueueAddItem": "Legg til kø",
|
||||
"ButtonQueueRemoveItem": "Fjern fra kø",
|
||||
"ButtonQuickMatch": "Kjapt søk",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "Samlingsgjenstander",
|
||||
"HeaderCover": "Omslag",
|
||||
"HeaderCurrentDownloads": "Aktive nedlastinger",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||
"HeaderDetails": "Detaljer",
|
||||
"HeaderDownloadQueue": "Last ned kø",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "Rediger",
|
||||
"LabelEmail": "Epost",
|
||||
"LabelEmailSettingsFromAddress": "Fra Adresse",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "Sikker",
|
||||
"LabelEmailSettingsSecureHelp": "Hvis aktivert, vil tilkoblingen bruke TLS under tilkobling til tjeneren. Ellers vil TLS bli brukt hvis tjeneren støtter STARTTLS utvidelsen. I de fleste tilfeller aktiver valget hvis du kobler til med port 465. Med port 587 eller 25 deaktiver valget. (fra nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test Adresse",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "Enhet",
|
||||
"LabelLanguage": "Språk",
|
||||
"LabelLanguageDefaultServer": "Standard tjener språk",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "Siste bok lagt til",
|
||||
"LabelLastBookUpdated": "Siste bok oppdatert",
|
||||
"LabelLastSeen": "Sist sett",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "Mindre",
|
||||
"LabelLibrariesAccessibleToUser": "Biblioteker tilgjengelig for bruker",
|
||||
"LabelLibrary": "Bibliotek",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "Bibliotek enhet",
|
||||
"LabelLibraryName": "Bibliotek navn",
|
||||
"LabelLimit": "Begrensning",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "Nytt passord",
|
||||
"LabelNextBackupDate": "Neste sikkerhetskopi dato",
|
||||
"LabelNextScheduledRun": "Neste planlagte kjøring",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "Ingen episoder valgt",
|
||||
"LabelNotes": "Notat",
|
||||
"LabelNotFinished": "Ikke fullført",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "Kan laste opp",
|
||||
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||
"LabelPhotoPathURL": "Bilde sti/URL",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "Spilleliste",
|
||||
"LabelPlayMethod": "Avspillingsmetode",
|
||||
"LabelPodcast": "Podcast",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "Tilbyder",
|
||||
"LabelPubDate": "Publiseringsdato",
|
||||
"LabelPublisher": "Forlegger",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "Publikasjonsår",
|
||||
"LabelRead": "Les",
|
||||
"LabelReadAgain": "Les igjen",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "Kjapt søk vil forsøke å legge til manglende omslag og metadata for de valgte gjenstandene. Aktiver dette valget for å tillate Kjapt søk til å overskrive eksisterende omslag og/eller metadata.",
|
||||
"MessageBookshelfNoCollections": "Du har ikke laget noen samlinger ennå",
|
||||
"MessageBookshelfNoResultsForFilter": "Ingen resultat for filter \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "Ingen RSS feed er åpen",
|
||||
"MessageBookshelfNoSeries": "Du har ingen serier",
|
||||
"MessageChapterEndIsAfter": "Kapittel slutt er etter slutt av lydboken",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "Er du sikker på at du vil markere alle bøkene i serien som fullført?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Er du sikker på at du vil markere alle bøkene i serien som ikke fullført?",
|
||||
"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?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?",
|
||||
"MessageConfirmRemoveAllChapters": "Er du sikker på at du vil fjerne alle kapitler?",
|
||||
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "Zatwierdź",
|
||||
"ButtonApplyChapters": "Zatwierdź rozdziały",
|
||||
"ButtonAuthors": "Autorzy",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "Wyszukaj folder",
|
||||
"ButtonCancel": "Anuluj",
|
||||
"ButtonCancelEncode": "Anuluj enkodowanie",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "Poprzedni rozdział",
|
||||
"ButtonPurgeAllCache": "Wyczyść dane tymczasowe",
|
||||
"ButtonPurgeItemsCache": "Wyczyść dane tymczasowe pozycji",
|
||||
"ButtonPurgeMediaProgress": "Wyczyść postęp",
|
||||
"ButtonQueueAddItem": "Dodaj do kolejki",
|
||||
"ButtonQueueRemoveItem": "Usuń z kolejki",
|
||||
"ButtonQuickMatch": "Szybkie dopasowanie",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "Elementy kolekcji",
|
||||
"HeaderCover": "Okładka",
|
||||
"HeaderCurrentDownloads": "Current Downloads",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||
"HeaderDetails": "Szczegóły",
|
||||
"HeaderDownloadQueue": "Download Queue",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "Edytuj",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "From Address",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "Secure",
|
||||
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Test Address",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "Pozycja",
|
||||
"LabelLanguage": "Język",
|
||||
"LabelLanguageDefaultServer": "Domyślny język serwera",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "Last Book Added",
|
||||
"LabelLastBookUpdated": "Last Book Updated",
|
||||
"LabelLastSeen": "Ostatnio widziany",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "Mniej",
|
||||
"LabelLibrariesAccessibleToUser": "Biblioteki dostępne dla użytkownika",
|
||||
"LabelLibrary": "Biblioteka",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "Element biblioteki",
|
||||
"LabelLibraryName": "Nazwa biblioteki",
|
||||
"LabelLimit": "Limit",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "Nowe hasło",
|
||||
"LabelNextBackupDate": "Next backup date",
|
||||
"LabelNextScheduledRun": "Next scheduled run",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "No episodes selected",
|
||||
"LabelNotes": "Uwagi",
|
||||
"LabelNotFinished": "Nieukończone",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "Ma możliwość dodawania",
|
||||
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||
"LabelPhotoPathURL": "Scieżka/URL do zdjęcia",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "Playlists",
|
||||
"LabelPlayMethod": "Metoda odtwarzania",
|
||||
"LabelPodcast": "Podcast",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "Dostawca",
|
||||
"LabelPubDate": "Data publikacji",
|
||||
"LabelPublisher": "Wydawca",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "Rok publikacji",
|
||||
"LabelRead": "Read",
|
||||
"LabelReadAgain": "Read Again",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "Quick Match będzie próbował dodać brakujące okładki i metadane dla wybranych elementów. Włącz poniższe opcje, aby umożliwić Quick Match nadpisanie istniejących okładek i/lub metadanych.",
|
||||
"MessageBookshelfNoCollections": "Nie posiadasz jeszcze żadnych kolekcji",
|
||||
"MessageBookshelfNoResultsForFilter": "Nie znaleziono żadnych pozycji przy aktualnym filtrowaniu \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "Nie posiadasz żadnych otwartych feedów RSS",
|
||||
"MessageBookshelfNoSeries": "Nie masz jeszcze żadnych serii",
|
||||
"MessageChapterEndIsAfter": "Koniec rozdziału następuje po zakończeniu audiobooka",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||
"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?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?",
|
||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "Aplicar",
|
||||
"ButtonApplyChapters": "Aplicar Capítulos",
|
||||
"ButtonAuthors": "Autores",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "Procurar por Pasta",
|
||||
"ButtonCancel": "Cancelar",
|
||||
"ButtonCancelEncode": "Cancelar Codificação",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "Capítulo Anterior",
|
||||
"ButtonPurgeAllCache": "Apagar Todo o Cache",
|
||||
"ButtonPurgeItemsCache": "Apagar o Cache de Itens",
|
||||
"ButtonPurgeMediaProgress": "Apagar o Progresso nas Mídias",
|
||||
"ButtonQueueAddItem": "Adicionar à Lista",
|
||||
"ButtonQueueRemoveItem": "Remover da Lista",
|
||||
"ButtonQuickMatch": "Consulta rápida",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "Itens da Coleção",
|
||||
"HeaderCover": "Capas",
|
||||
"HeaderCurrentDownloads": "Downloads em andamento",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "Fontes de Metadados Customizados",
|
||||
"HeaderDetails": "Detalhes",
|
||||
"HeaderDownloadQueue": "Fila de Download",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "Editar",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "Remetente",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "Seguro",
|
||||
"LabelEmailSettingsSecureHelp": "Se ativado, a conexão utilizará TLS para a conexão ao servidor. Se desativado TLS será usado se o servidor suportar a extensão STARTTLS. Na maioria dos casos ative esse valor se estiver conectando pela porta 465. Para portas 587 ou 25, mantenha inativo. (de nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Endereço de teste",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "Item",
|
||||
"LabelLanguage": "Idioma",
|
||||
"LabelLanguageDefaultServer": "Idioma Padrão do Servidor",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "Último Livro Acrescentado",
|
||||
"LabelLastBookUpdated": "Último Livro Atualizado",
|
||||
"LabelLastSeen": "Visto pela Última Vez",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "Menos",
|
||||
"LabelLibrariesAccessibleToUser": "Bibliotecas Acessíveis ao Usuário",
|
||||
"LabelLibrary": "Biblioteca",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "Item da Biblioteca",
|
||||
"LabelLibraryName": "Nome da Biblioteca",
|
||||
"LabelLimit": "Limite",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "Nova Senha",
|
||||
"LabelNextBackupDate": "Data do próximo backup",
|
||||
"LabelNextScheduledRun": "Próxima execução programada",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "Nenhum episódio selecionado",
|
||||
"LabelNotes": "Notas",
|
||||
"LabelNotFinished": "Não concluído",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "Pode Fazer Upload",
|
||||
"LabelPersonalYearReview": "Sua Retrospectiva Anual ({0})",
|
||||
"LabelPhotoPathURL": "Caminho/URL para Foto",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "Listas de Reprodução",
|
||||
"LabelPlayMethod": "Método de Reprodução",
|
||||
"LabelPodcast": "Podcast",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "Fonte",
|
||||
"LabelPubDate": "Data de Publicação",
|
||||
"LabelPublisher": "Editora",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "Ano de Publicação",
|
||||
"LabelRead": "Lido",
|
||||
"LabelReadAgain": "Ler novamente",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "Consulta Rápida tentará adicionar capas e metadados ausentes para os itens selecionados. Ative as opções abaixo para permitir que a Consulta Rápida sobrescreva capas e/ou metadados existentes.",
|
||||
"MessageBookshelfNoCollections": "Você ainda não criou coleções",
|
||||
"MessageBookshelfNoResultsForFilter": "Sem Resultados para o filtro \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "Não existem feeds RSS abertos",
|
||||
"MessageBookshelfNoSeries": "Você não tem séries",
|
||||
"MessageChapterEndIsAfter": "O final do capítulo está além do final do seu audiobook",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "Tem certeza de que deseja marcar todos os livros nesta série como concluídos?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Tem certeza de que deseja marcar todos os livros nesta série como não concluídos?",
|
||||
"MessageConfirmPurgeCache": "Apagar o cache irá apagar o diretório todo localizado em <code>/metadata/cache</code>. <br /><br />Tem certeza que deseja apagar o diretório de cache?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "Aviso! Inclusão rápida não fará backup dos seus arquivos de áudio. Verifique se tem um backup dos seus arquivos de áudio. <br><br>Quer continuar?",
|
||||
"MessageConfirmRemoveAllChapters": "Tem certeza de que deseja remover todos os capítulos?",
|
||||
"MessageConfirmRemoveAuthor": "Tem certeza de que deseja remover o autor \"{0}\"?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "Применить",
|
||||
"ButtonApplyChapters": "Применить главы",
|
||||
"ButtonAuthors": "Авторы",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "Выбрать папку",
|
||||
"ButtonCancel": "Отмена",
|
||||
"ButtonCancelEncode": "Отменить кодирование",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "Previous Chapter",
|
||||
"ButtonPurgeAllCache": "Очистить весь кэш",
|
||||
"ButtonPurgeItemsCache": "Очистить кэш элементов",
|
||||
"ButtonPurgeMediaProgress": "Очистить прогресс медиа",
|
||||
"ButtonQueueAddItem": "Добавить в очередь",
|
||||
"ButtonQueueRemoveItem": "Удалить из очереди",
|
||||
"ButtonQuickMatch": "Быстрый поиск",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "Элементы коллекции",
|
||||
"HeaderCover": "Обложка",
|
||||
"HeaderCurrentDownloads": "Текущие закачки",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||
"HeaderDetails": "Подробности",
|
||||
"HeaderDownloadQueue": "Очередь скачивания",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "Редактировать",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "Адрес От",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "Безопасность",
|
||||
"LabelEmailSettingsSecureHelp": "Если значение истинно, то соединение будет использовать TLS при подключении к серверу. Если значение ложно, то TLS будет использован, если сервер поддерживает расширение STARTTLS. В большинстве случаев установите это значение в истину, если вы подключаетесь к порту 465. Для порта 587 или 25 оставьте значение ложным. (из nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Тестовый адрес",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "Элемент",
|
||||
"LabelLanguage": "Язык",
|
||||
"LabelLanguageDefaultServer": "Язык сервера по умолчанию",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "Последняя книга добавлена",
|
||||
"LabelLastBookUpdated": "Последняя книга обновлена",
|
||||
"LabelLastSeen": "Последнее сканирование",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "Менее",
|
||||
"LabelLibrariesAccessibleToUser": "Библиотеки доступные для пользователя",
|
||||
"LabelLibrary": "Библиотека",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "Элемент библиотеки",
|
||||
"LabelLibraryName": "Имя библиотеки",
|
||||
"LabelLimit": "Лимит",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "Новый пароль",
|
||||
"LabelNextBackupDate": "Следующая дата бэкапирования",
|
||||
"LabelNextScheduledRun": "Следущий запланированный запуск",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "Эпизоды не выбраны",
|
||||
"LabelNotes": "Заметки",
|
||||
"LabelNotFinished": "Не завершено",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "Может закачивать",
|
||||
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||
"LabelPhotoPathURL": "Путь к фото/URL",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "Плейлисты",
|
||||
"LabelPlayMethod": "Метод воспроизведения",
|
||||
"LabelPodcast": "Подкаст",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "Провайдер",
|
||||
"LabelPubDate": "Дата публикации",
|
||||
"LabelPublisher": "Издатель",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "Год публикации",
|
||||
"LabelRead": "Читать",
|
||||
"LabelReadAgain": "Читать снова",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "Быстрый Поиск попытается добавить отсутствующие обложки и метаданные для выбранных элементов. Включите параметры ниже, чтобы разрешить Быстрому Поиску перезаписывать существующие обложки и/или метаданные.",
|
||||
"MessageBookshelfNoCollections": "Вы еще не создали ни одной коллекции",
|
||||
"MessageBookshelfNoResultsForFilter": "Нет Результатов для фильтра \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "Нет открытых RSS-каналов",
|
||||
"MessageBookshelfNoSeries": "У вас нет серий",
|
||||
"MessageChapterEndIsAfter": "Конец главы после окончания вашей аудиокниги",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "Вы уверены, что хотите отметить все книги этой серии как завершенные?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Вы уверены, что хотите отметить все книги этой серии как не завершенные?",
|
||||
"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?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "Предупреждение! Быстрое встраивание не позволяет создавать резервные копии аудиофайлов. Убедитесь, что у вас есть резервная копия аудиофайлов. <br><br>Хотите продолжить?",
|
||||
"MessageConfirmRemoveAllChapters": "Вы уверены, что хотите удалить все главы?",
|
||||
"MessageConfirmRemoveAuthor": "Вы уверены, что хотите удалить автора \"{0}\"?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "Tillämpa",
|
||||
"ButtonApplyChapters": "Tillämpa kapitel",
|
||||
"ButtonAuthors": "Författare",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "Bläddra efter mapp",
|
||||
"ButtonCancel": "Avbryt",
|
||||
"ButtonCancelEncode": "Avbryt kodning",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "Previous Chapter",
|
||||
"ButtonPurgeAllCache": "Rensa all cache",
|
||||
"ButtonPurgeItemsCache": "Rensa föremåls-cache",
|
||||
"ButtonPurgeMediaProgress": "Rensa medieförlopp",
|
||||
"ButtonQueueAddItem": "Lägg till i kön",
|
||||
"ButtonQueueRemoveItem": "Ta bort från kön",
|
||||
"ButtonQuickMatch": "Snabb matchning",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "Samlingselement",
|
||||
"HeaderCover": "Omslag",
|
||||
"HeaderCurrentDownloads": "Aktuella nedladdningar",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||
"HeaderDetails": "Detaljer",
|
||||
"HeaderDownloadQueue": "Nedladdningskö",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "Redigera",
|
||||
"LabelEmail": "E-post",
|
||||
"LabelEmailSettingsFromAddress": "Från adress",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "Säker",
|
||||
"LabelEmailSettingsSecureHelp": "Om sant kommer anslutningen att använda TLS vid anslutning till servern. Om falskt används TLS om servern stöder STARTTLS-tillägget. I de flesta fall, om du ansluter till port 465, bör du ställa in detta värde till sant. För port 587 eller 25, låt det vara falskt. (från nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Testadress",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "Objekt",
|
||||
"LabelLanguage": "Språk",
|
||||
"LabelLanguageDefaultServer": "Standardspråk för server",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "Senaste bok tillagd",
|
||||
"LabelLastBookUpdated": "Senaste bok uppdaterad",
|
||||
"LabelLastSeen": "Senast sedd",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "Mindre",
|
||||
"LabelLibrariesAccessibleToUser": "Åtkomliga bibliotek för användare",
|
||||
"LabelLibrary": "Bibliotek",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "Biblioteksobjekt",
|
||||
"LabelLibraryName": "Biblioteksnamn",
|
||||
"LabelLimit": "Begränsning",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "Nytt lösenord",
|
||||
"LabelNextBackupDate": "Nästa säkerhetskopia datum",
|
||||
"LabelNextScheduledRun": "Nästa schemalagda körning",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "Inga avsnitt valda",
|
||||
"LabelNotes": "Anteckningar",
|
||||
"LabelNotFinished": "Ej avslutad",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "Kan ladda upp",
|
||||
"LabelPersonalYearReview": "Your Year in Review ({0})",
|
||||
"LabelPhotoPathURL": "Bildsökväg/URL",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "Spellistor",
|
||||
"LabelPlayMethod": "Spelläge",
|
||||
"LabelPodcast": "Podcast",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "Leverantör",
|
||||
"LabelPubDate": "Publiceringsdatum",
|
||||
"LabelPublisher": "Utgivare",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "Publiceringsår",
|
||||
"LabelRead": "Läst",
|
||||
"LabelReadAgain": "Läs igen",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "Quick Match kommer försöka lägga till saknade omslag och metadata för de valda föremålen. Aktivera alternativen nedan för att tillåta Quick Match att överskriva befintliga omslag och/eller metadata.",
|
||||
"MessageBookshelfNoCollections": "Du har ännu inte skapat några samlingar",
|
||||
"MessageBookshelfNoResultsForFilter": "Inga resultat för filter \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "Inga RSS-flöden är öppna",
|
||||
"MessageBookshelfNoSeries": "Du har inga serier",
|
||||
"MessageChapterEndIsAfter": "Kapitelns slut är efter din ljudboks slut",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "Är du säker på att du vill markera alla böcker i denna serie som avslutade?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Är du säker på att du vill markera alla böcker i denna serie som inte avslutade?",
|
||||
"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?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "Varning! Quick embed kommer inte att säkerhetskopiera dina ljudfiler. Se till att du har en säkerhetskopia av dina ljudfiler. <br><br>Vill du fortsätta?",
|
||||
"MessageConfirmRemoveAllChapters": "Är du säker på att du vill ta bort alla kapitel?",
|
||||
"MessageConfirmRemoveAuthor": "Är du säker på att du vill ta bort författaren \"{0}\"?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "Застосувати",
|
||||
"ButtonApplyChapters": "Зберегти глави",
|
||||
"ButtonAuthors": "Автори",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "Огляд тек",
|
||||
"ButtonCancel": "Скасувати",
|
||||
"ButtonCancelEncode": "Скасувати кодування",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "Попередня глава",
|
||||
"ButtonPurgeAllCache": "Очистити весь кеш",
|
||||
"ButtonPurgeItemsCache": "Очистити кеш елементів",
|
||||
"ButtonPurgeMediaProgress": "Очистити прогрес",
|
||||
"ButtonQueueAddItem": "Додати до черги",
|
||||
"ButtonQueueRemoveItem": "Вилучити з черги",
|
||||
"ButtonQuickMatch": "Швидкий пошук",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "Елементи добірки",
|
||||
"HeaderCover": "Обкладинка",
|
||||
"HeaderCurrentDownloads": "Поточні завантаження",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "Постачальники метаданих",
|
||||
"HeaderDetails": "Подробиці",
|
||||
"HeaderDownloadQueue": "Черга завантажень",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "Редагувати",
|
||||
"LabelEmail": "Електронна пошта",
|
||||
"LabelEmailSettingsFromAddress": "Адреса відправника",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "Безпечне",
|
||||
"LabelEmailSettingsSecureHelp": "Увімкніть, аби використовувати TLS при підключенні до сервера. Якщо вимкнути, то TLS буде використано, якщо сервер підтримує STARTTLS. Увімкніть, якщо ви підключаєтеся до порту 465. Вимкніть для портів 587 або 25. (з nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Тестова адреса",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "Елемент",
|
||||
"LabelLanguage": "Мова",
|
||||
"LabelLanguageDefaultServer": "Типова мова сервера",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "Останню книгу додано",
|
||||
"LabelLastBookUpdated": "Останню книгу оновлено",
|
||||
"LabelLastSeen": "Активність",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "Менше",
|
||||
"LabelLibrariesAccessibleToUser": "Бібліотеки, доступні користувачу",
|
||||
"LabelLibrary": "Бібліотека",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "Елемент бібліотеки",
|
||||
"LabelLibraryName": "Назва бібліотеки",
|
||||
"LabelLimit": "Обмеження",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "Новий пароль",
|
||||
"LabelNextBackupDate": "Дата наступного резервного копіювання",
|
||||
"LabelNextScheduledRun": "Наступний запланований запуск",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "Не вибрано жодного епізоду",
|
||||
"LabelNotes": "Примітки",
|
||||
"LabelNotFinished": "Незавершені",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "Може завантажувати",
|
||||
"LabelPersonalYearReview": "Ваші підсумки року ({0})",
|
||||
"LabelPhotoPathURL": "Шлях/URL фото",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "Списки відтворення",
|
||||
"LabelPlayMethod": "Метод відтворення",
|
||||
"LabelPodcast": "Подкаст",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "Джерело",
|
||||
"LabelPubDate": "Дата публікації",
|
||||
"LabelPublisher": "Видавець",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "Рік публікації",
|
||||
"LabelRead": "Читати",
|
||||
"LabelReadAgain": "Читати знову",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "Швидкий пошук спробує знайти відсутні обкладинки та метадані обраних елементів. Увімкніть налаштування нижче, аби дозволити заміну наявних обкладинок та/або метаданих під час швидкого пошуку.",
|
||||
"MessageBookshelfNoCollections": "Ви не створили жодної добірки",
|
||||
"MessageBookshelfNoResultsForFilter": "Немає результатів з фільтром \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "Немає відкритих RSS-каналів",
|
||||
"MessageBookshelfNoSeries": "Серії відсутні",
|
||||
"MessageChapterEndIsAfter": "Кінець глави знаходиться після закінчення книги",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "Ви дійсно бажаєте позначити усі книги серії завершеними?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Ви дійсно бажаєте позначити всі книги серії незавершеними?",
|
||||
"MessageConfirmPurgeCache": "Очищення кешу видалить усю теку <code>/metadata/cache</code>. <br /><br />Ви дійсно бажаєте видалити теку кешу?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "Увага! Швидке вбудування не створює резервних копій ваших аудіо. Переконайтеся, що маєте копію ваших файлів.<br><br>Продовжити?",
|
||||
"MessageConfirmRemoveAllChapters": "Ви дійсно бажаєте видалити усі глави?",
|
||||
"MessageConfirmRemoveAuthor": "Ви дійсно бажаєте видалити автора \"{0}\"?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "Áp Dụng",
|
||||
"ButtonApplyChapters": "Áp Dụng Chương",
|
||||
"ButtonAuthors": "Tác Giả",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "Duyệt Thư Mục",
|
||||
"ButtonCancel": "Hủy",
|
||||
"ButtonCancelEncode": "Hủy Mã Hóa",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "Chương Trước",
|
||||
"ButtonPurgeAllCache": "Xóa Sạch Tất Cả Bộ Nhớ Cache",
|
||||
"ButtonPurgeItemsCache": "Xóa Sạch Bộ Nhớ Cache Các Mục",
|
||||
"ButtonPurgeMediaProgress": "Xóa Sạch Tiến Trình Phương Tiện",
|
||||
"ButtonQueueAddItem": "Thêm vào hàng đợi",
|
||||
"ButtonQueueRemoveItem": "Xóa khỏi hàng đợi",
|
||||
"ButtonQuickMatch": "Khớp Nhanh",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "Các Mục Bộ Sưu Tập",
|
||||
"HeaderCover": "Bìa",
|
||||
"HeaderCurrentDownloads": "Tải Xuống Hiện Tại",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "Các Nhà Cung Cấp Metadata Tùy Chỉnh",
|
||||
"HeaderDetails": "Chi Tiết",
|
||||
"HeaderDownloadQueue": "Hàng Đợi Tải Xuống",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "Chỉnh Sửa",
|
||||
"LabelEmail": "Email",
|
||||
"LabelEmailSettingsFromAddress": "Địa chỉ Gửi từ",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "Bảo Mật",
|
||||
"LabelEmailSettingsSecureHelp": "Nếu đúng thì kết nối sẽ sử dụng TLS khi kết nối đến máy chủ. Nếu sai thì TLS sẽ được sử dụng nếu máy chủ hỗ trợ phần mở rộng STARTTLS. Trong hầu hết các trường hợp, hãy đặt giá trị này là đúng nếu bạn kết nối đến cổng 465. Đối với cổng 587 hoặc 25, giữ nó sai. (từ nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "Địa Chỉ Kiểm Tra",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "Mục",
|
||||
"LabelLanguage": "Ngôn ngữ",
|
||||
"LabelLanguageDefaultServer": "Ngôn ngữ Máy chủ mặc định",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "Sách mới nhất được thêm",
|
||||
"LabelLastBookUpdated": "Sách mới nhất được cập nhật",
|
||||
"LabelLastSeen": "Lần cuối nhìn thấy",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "Ít hơn",
|
||||
"LabelLibrariesAccessibleToUser": "Thư viện có thể truy cập cho người dùng",
|
||||
"LabelLibrary": "Thư viện",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "Mục thư viện",
|
||||
"LabelLibraryName": "Tên thư viện",
|
||||
"LabelLimit": "Giới hạn",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "Mật khẩu mới",
|
||||
"LabelNextBackupDate": "Ngày sao lưu tiếp theo",
|
||||
"LabelNextScheduledRun": "Chạy tiếp theo theo lịch trình",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "Không có tập nào được chọn",
|
||||
"LabelNotes": "Ghi chú",
|
||||
"LabelNotFinished": "Chưa hoàn thành",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "Có Thể Tải Lên",
|
||||
"LabelPersonalYearReview": "Năm của Bạn trong Bài Đánh Giá ({0})",
|
||||
"LabelPhotoPathURL": "Đường dẫn/URL ảnh",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "Danh sách phát",
|
||||
"LabelPlayMethod": "Phương pháp phát",
|
||||
"LabelPodcast": "Podcast",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "Nhà cung cấp",
|
||||
"LabelPubDate": "Ngày Xuất bản",
|
||||
"LabelPublisher": "Nhà xuất bản",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "Năm Xuất bản",
|
||||
"LabelRead": "Đọc",
|
||||
"LabelReadAgain": "Đọc lại",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "Quick Match sẽ cố gắng thêm các ảnh bìa và siêu dữ liệu bị thiếu cho các mục đã chọn. Bật các tùy chọn dưới đây để cho phép Quick Match ghi đè lên các ảnh bìa hiện có và / hoặc siêu dữ liệu.",
|
||||
"MessageBookshelfNoCollections": "Bạn chưa tạo bất kỳ bộ sưu tập nào",
|
||||
"MessageBookshelfNoResultsForFilter": "Không có Kết quả cho bộ lọc \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "Không có nguồn cung cấp RSS nào đang mở",
|
||||
"MessageBookshelfNoSeries": "Bạn không có bộ sách",
|
||||
"MessageChapterEndIsAfter": "Kết thúc chương sau khi kết thúc sách nói của bạn",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "Bạn có chắc chắn muốn đánh dấu tất cả các sách trong loạt sách này đã kết thúc không?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Bạn có chắc chắn muốn đánh dấu tất cả các sách trong loạt sách này chưa kết thúc không?",
|
||||
"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?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "Cảnh báo! Quick embed sẽ không sao lưu các tệp âm thanh của bạn. Đảm bảo bạn có một bản sao lưu của các tệp âm thanh của bạn. <br><br>Bạn có muốn tiếp tục không?",
|
||||
"MessageConfirmRemoveAllChapters": "Bạn có chắc chắn muốn xóa tất cả các chương không?",
|
||||
"MessageConfirmRemoveAuthor": "Bạn có chắc chắn muốn xóa tác giả \"{0}\" không?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "应用",
|
||||
"ButtonApplyChapters": "应用到章节",
|
||||
"ButtonAuthors": "作者",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "浏览文件夹",
|
||||
"ButtonCancel": "取消",
|
||||
"ButtonCancelEncode": "取消编码",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "上一章节",
|
||||
"ButtonPurgeAllCache": "清理所有缓存",
|
||||
"ButtonPurgeItemsCache": "清理项目缓存",
|
||||
"ButtonPurgeMediaProgress": "清理媒体进度",
|
||||
"ButtonQueueAddItem": "添加到队列",
|
||||
"ButtonQueueRemoveItem": "从队列中移除",
|
||||
"ButtonQuickMatch": "快速匹配",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "收藏项目",
|
||||
"HeaderCover": "封面",
|
||||
"HeaderCurrentDownloads": "当前下载",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "自定义元数据提供者",
|
||||
"HeaderDetails": "详情",
|
||||
"HeaderDownloadQueue": "下载队列",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "编辑",
|
||||
"LabelEmail": "邮箱",
|
||||
"LabelEmailSettingsFromAddress": "发件人地址",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "安全",
|
||||
"LabelEmailSettingsSecureHelp": "如果选是, 则连接将在连接到服务器时使用TLS. 如果选否, 则若服务器支持STARTTLS扩展, 则使用TLS. 在大多数情况下, 如果连接到端口465, 请将该值设置为是. 对于端口587或25, 请保持为否. (来自nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "测试地址",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "项目",
|
||||
"LabelLanguage": "语言",
|
||||
"LabelLanguageDefaultServer": "默认服务器语言",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "最后添加的书",
|
||||
"LabelLastBookUpdated": "最后更新的书",
|
||||
"LabelLastSeen": "上次查看时间",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "较少",
|
||||
"LabelLibrariesAccessibleToUser": "用户可访问的媒体库",
|
||||
"LabelLibrary": "媒体库",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "媒体库项目",
|
||||
"LabelLibraryName": "媒体库名称",
|
||||
"LabelLimit": "限制",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "新密码",
|
||||
"LabelNextBackupDate": "下次备份日期",
|
||||
"LabelNextScheduledRun": "下次任务运行",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "未选择任何剧集",
|
||||
"LabelNotes": "注释",
|
||||
"LabelNotFinished": "未听完",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "可以上传",
|
||||
"LabelPersonalYearReview": "你的年度回顾 ({0})",
|
||||
"LabelPhotoPathURL": "图片路径或 URL",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "播放列表",
|
||||
"LabelPlayMethod": "播放方法",
|
||||
"LabelPodcast": "播客",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "供应商",
|
||||
"LabelPubDate": "出版日期",
|
||||
"LabelPublisher": "出版商",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "发布年份",
|
||||
"LabelRead": "阅读",
|
||||
"LabelReadAgain": "再次阅读",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "快速匹配将尝试为所选项目添加缺少的封面和元数据. 启用以下选项以允许快速匹配覆盖现有封面和或元数据.",
|
||||
"MessageBookshelfNoCollections": "你尚未进行任何收藏",
|
||||
"MessageBookshelfNoResultsForFilter": "过滤器无结果 \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "没有打开的 RSS 源",
|
||||
"MessageBookshelfNoSeries": "你没有系列",
|
||||
"MessageChapterEndIsAfter": "章节结束是在有声读物结束之后",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "你确定要将此系列中的所有书籍都标记为已听完吗?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "你确定要将此系列中的所有书籍都标记为未听完吗?",
|
||||
"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?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "警告! 快速嵌入不会备份你的音频文件. 确保你有音频文件的备份. <br><br>你是否想继续吗?",
|
||||
"MessageConfirmRemoveAllChapters": "你确定要移除所有章节吗?",
|
||||
"MessageConfirmRemoveAuthor": "你确定要删除作者 \"{0}\"?",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"ButtonApply": "應用",
|
||||
"ButtonApplyChapters": "應用到章節",
|
||||
"ButtonAuthors": "作者",
|
||||
"ButtonBack": "Back",
|
||||
"ButtonBrowseForFolder": "瀏覽資料夾",
|
||||
"ButtonCancel": "取消",
|
||||
"ButtonCancelEncode": "取消編碼",
|
||||
|
|
@ -56,7 +57,6 @@
|
|||
"ButtonPreviousChapter": "過去的章節",
|
||||
"ButtonPurgeAllCache": "清理所有快取",
|
||||
"ButtonPurgeItemsCache": "清理項目快取",
|
||||
"ButtonPurgeMediaProgress": "清理媒體進度",
|
||||
"ButtonQueueAddItem": "新增到佇列",
|
||||
"ButtonQueueRemoveItem": "從佇列中移除",
|
||||
"ButtonQuickMatch": "快速匹配",
|
||||
|
|
@ -115,6 +115,7 @@
|
|||
"HeaderCollectionItems": "收藏項目",
|
||||
"HeaderCover": "封面",
|
||||
"HeaderCurrentDownloads": "當前下載",
|
||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||
"HeaderCustomMetadataProviders": "自訂 Metadata 提供者",
|
||||
"HeaderDetails": "詳情",
|
||||
"HeaderDownloadQueue": "下載佇列",
|
||||
|
|
@ -279,6 +280,8 @@
|
|||
"LabelEdit": "編輯",
|
||||
"LabelEmail": "郵箱",
|
||||
"LabelEmailSettingsFromAddress": "發件人位址",
|
||||
"LabelEmailSettingsRejectUnauthorized": "Reject unauthorized certificates",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "Disabling SSL certificate validation may expose your connection to security risks, such as man-in-the-middle attacks. Only disable this option if you understand the implications and trust the mail server you are connecting to.",
|
||||
"LabelEmailSettingsSecure": "安全",
|
||||
"LabelEmailSettingsSecureHelp": "如果選是, 則連接將在連接到伺服器時使用TLS. 如果選否, 則若伺服器支援STARTTLS擴展, 則使用TLS. 在大多數情況下, 如果連接到465埠, 請將該值設定為是. 對於587或25埠, 請保持為否. (來自nodemailer.com/smtp/#authentication)",
|
||||
"LabelEmailSettingsTestAddress": "測試位址",
|
||||
|
|
@ -335,6 +338,7 @@
|
|||
"LabelItem": "項目",
|
||||
"LabelLanguage": "語言",
|
||||
"LabelLanguageDefaultServer": "預設伺服器語言",
|
||||
"LabelLanguages": "Languages",
|
||||
"LabelLastBookAdded": "最後新增的書",
|
||||
"LabelLastBookUpdated": "最後更新的書",
|
||||
"LabelLastSeen": "上次查看時間",
|
||||
|
|
@ -346,6 +350,7 @@
|
|||
"LabelLess": "較少",
|
||||
"LabelLibrariesAccessibleToUser": "使用者可存取的媒體庫",
|
||||
"LabelLibrary": "媒體庫",
|
||||
"LabelLibraryFilterSublistEmpty": "No {0}",
|
||||
"LabelLibraryItem": "媒體庫項目",
|
||||
"LabelLibraryName": "媒體庫名稱",
|
||||
"LabelLimit": "限制",
|
||||
|
|
@ -381,6 +386,7 @@
|
|||
"LabelNewPassword": "新密碼",
|
||||
"LabelNextBackupDate": "下次備份日期",
|
||||
"LabelNextScheduledRun": "下次任務運行",
|
||||
"LabelNoCustomMetadataProviders": "No custom metadata providers",
|
||||
"LabelNoEpisodesSelected": "未選擇任何劇集",
|
||||
"LabelNotes": "注釋",
|
||||
"LabelNotFinished": "未聽完",
|
||||
|
|
@ -412,6 +418,7 @@
|
|||
"LabelPermissionsUpload": "可以上傳",
|
||||
"LabelPersonalYearReview": "你的年度回顧 ({0})",
|
||||
"LabelPhotoPathURL": "圖片路徑或 URL",
|
||||
"LabelPlayerChapterNumberMarker": "{0} of {1}",
|
||||
"LabelPlaylists": "播放列表",
|
||||
"LabelPlayMethod": "播放方法",
|
||||
"LabelPodcast": "播客",
|
||||
|
|
@ -426,6 +433,7 @@
|
|||
"LabelProvider": "供應商",
|
||||
"LabelPubDate": "出版日期",
|
||||
"LabelPublisher": "出版商",
|
||||
"LabelPublishers": "Publishers",
|
||||
"LabelPublishYear": "發布年份",
|
||||
"LabelRead": "閱讀",
|
||||
"LabelReadAgain": "再次閱讀",
|
||||
|
|
@ -591,6 +599,7 @@
|
|||
"MessageBatchQuickMatchDescription": "快速匹配將嘗試為所選項目新增缺少的封面和元數據. 啟用以下選項以允許快速匹配覆蓋現有封面和或元數據.",
|
||||
"MessageBookshelfNoCollections": "你尚未進行任何收藏",
|
||||
"MessageBookshelfNoResultsForFilter": "過濾器無結果 \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForQuery": "No results for query",
|
||||
"MessageBookshelfNoRSSFeeds": "沒有打開的 RSS 源",
|
||||
"MessageBookshelfNoSeries": "你沒有系列",
|
||||
"MessageChapterEndIsAfter": "章節結束是在有聲書結束之後",
|
||||
|
|
@ -612,6 +621,7 @@
|
|||
"MessageConfirmMarkSeriesFinished": "你確定要將此系列中的所有書籍都標記為已聽完嗎?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "你確定要將此系列中的所有書籍都標記為未聽完嗎?",
|
||||
"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?",
|
||||
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
|
||||
"MessageConfirmQuickEmbed": "警告! 快速嵌入不會備份你的音頻檔案. 確保你有音頻檔案的備份. <br><br>你是否想繼續嗎?",
|
||||
"MessageConfirmRemoveAllChapters": "你確定要移除所有章節嗎?",
|
||||
"MessageConfirmRemoveAuthor": "你確定要刪除作者 \"{0}\"?",
|
||||
|
|
|
|||
8
package-lock.json
generated
8
package-lock.json
generated
|
|
@ -17,7 +17,7 @@
|
|||
"htmlparser2": "^8.0.1",
|
||||
"lru-cache": "^10.0.3",
|
||||
"node-tone": "^1.0.1",
|
||||
"nodemailer": "^6.9.2",
|
||||
"nodemailer": "^6.9.13",
|
||||
"openid-client": "^5.6.1",
|
||||
"passport": "^0.6.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
|
|
@ -3619,9 +3619,9 @@
|
|||
"integrity": "sha512-wi7L0taDZMN6tM5l85TDKHsYzdhqJTtPNgvgpk2zHeZzPt6ZIUZ9vBLTJRRDpm0xzCvbsvFHjAaudeQjLHTE4w=="
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "6.9.8",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.8.tgz",
|
||||
"integrity": "sha512-cfrYUk16e67Ks051i4CntM9kshRYei1/o/Gi8K1d+R34OIs21xdFnW7Pt7EucmVKA0LKtqUGNcjMZ7ehjl49mQ==",
|
||||
"version": "6.9.13",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.13.tgz",
|
||||
"integrity": "sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
"htmlparser2": "^8.0.1",
|
||||
"lru-cache": "^10.0.3",
|
||||
"node-tone": "^1.0.1",
|
||||
"nodemailer": "^6.9.2",
|
||||
"nodemailer": "^6.9.13",
|
||||
"openid-client": "^5.6.1",
|
||||
"passport": "^0.6.0",
|
||||
"passport-jwt": "^4.0.1",
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ class Server {
|
|||
global.RouterBasePath = ROUTER_BASE_PATH
|
||||
global.XAccel = process.env.USE_X_ACCEL
|
||||
global.AllowCors = process.env.ALLOW_CORS === '1'
|
||||
global.DisableSsrfRequestFilter = process.env.DISABLE_SSRF_REQUEST_FILTER === '1'
|
||||
|
||||
if (!fs.pathExistsSync(global.ConfigPath)) {
|
||||
fs.mkdirSync(global.ConfigPath)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ var ffCodecRegexp = /^\s*([D\.])([E\.])([VAS])([I\.])([L\.])([S\.]) ([^ ]+) +(.*
|
|||
var ffEncodersRegexp = /\(encoders:([^\)]+)\)/;
|
||||
var ffDecodersRegexp = /\(decoders:([^\)]+)\)/;
|
||||
var encodersRegexp = /^\s*([VAS\.])([F\.])([S\.])([X\.])([B\.])([D\.]) ([^ ]+) +(.*)$/;
|
||||
var formatRegexp = /^\s*([D ])([E ]) ([^ ]+) +(.*)$/;
|
||||
var formatRegexp = /^\s*([D ])([E ])\s+([^ ]+)\s+(.*)$/;
|
||||
var lineBreakRegexp = /\r\n|\r|\n/;
|
||||
var filterRegexp = /^(?: [T\.][S\.][C\.] )?([^ ]+) +(AA?|VV?|\|)->(AA?|VV?|\|) +(.*)$/;
|
||||
|
||||
|
|
|
|||
|
|
@ -26,11 +26,6 @@ class Author extends Model {
|
|||
this.createdAt
|
||||
}
|
||||
|
||||
static async getOldAuthors() {
|
||||
const authors = await this.findAll()
|
||||
return authors.map(au => au.getOldAuthor())
|
||||
}
|
||||
|
||||
getOldAuthor() {
|
||||
return new oldAuthor({
|
||||
id: this.id,
|
||||
|
|
@ -85,7 +80,7 @@ class Author extends Model {
|
|||
|
||||
/**
|
||||
* Get oldAuthor by id
|
||||
* @param {string} authorId
|
||||
* @param {string} authorId
|
||||
* @returns {Promise<oldAuthor>}
|
||||
*/
|
||||
static async getOldById(authorId) {
|
||||
|
|
@ -96,7 +91,7 @@ class Author extends Model {
|
|||
|
||||
/**
|
||||
* Check if author exists
|
||||
* @param {string} authorId
|
||||
* @param {string} authorId
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
static async checkExistsById(authorId) {
|
||||
|
|
@ -106,60 +101,67 @@ class Author extends Model {
|
|||
/**
|
||||
* Get old author by name and libraryId. name case insensitive
|
||||
* TODO: Look for authors ignoring punctuation
|
||||
*
|
||||
* @param {string} authorName
|
||||
* @param {string} libraryId
|
||||
*
|
||||
* @param {string} authorName
|
||||
* @param {string} libraryId
|
||||
* @returns {Promise<oldAuthor>}
|
||||
*/
|
||||
static async getOldByNameAndLibrary(authorName, libraryId) {
|
||||
const author = (await this.findOne({
|
||||
where: [
|
||||
where(fn('lower', col('name')), authorName.toLowerCase()),
|
||||
{
|
||||
libraryId
|
||||
}
|
||||
]
|
||||
}))?.getOldAuthor()
|
||||
const author = (
|
||||
await this.findOne({
|
||||
where: [
|
||||
where(fn('lower', col('name')), authorName.toLowerCase()),
|
||||
{
|
||||
libraryId
|
||||
}
|
||||
]
|
||||
})
|
||||
)?.getOldAuthor()
|
||||
return author
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize model
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
*/
|
||||
static init(sequelize) {
|
||||
super.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
name: DataTypes.STRING,
|
||||
lastFirst: DataTypes.STRING,
|
||||
asin: DataTypes.STRING,
|
||||
description: DataTypes.TEXT,
|
||||
imagePath: DataTypes.STRING
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'author',
|
||||
indexes: [
|
||||
{
|
||||
fields: [{
|
||||
name: 'name',
|
||||
collate: 'NOCASE'
|
||||
}]
|
||||
super.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
// {
|
||||
// fields: [{
|
||||
// name: 'lastFirst',
|
||||
// collate: 'NOCASE'
|
||||
// }]
|
||||
// },
|
||||
{
|
||||
fields: ['libraryId']
|
||||
}
|
||||
]
|
||||
})
|
||||
name: DataTypes.STRING,
|
||||
lastFirst: DataTypes.STRING,
|
||||
asin: DataTypes.STRING,
|
||||
description: DataTypes.TEXT,
|
||||
imagePath: DataTypes.STRING
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'author',
|
||||
indexes: [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
collate: 'NOCASE'
|
||||
}
|
||||
]
|
||||
},
|
||||
// {
|
||||
// fields: [{
|
||||
// name: 'lastFirst',
|
||||
// collate: 'NOCASE'
|
||||
// }]
|
||||
// },
|
||||
{
|
||||
fields: ['libraryId']
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
const { library } = sequelize.models
|
||||
library.hasMany(Author, {
|
||||
|
|
|
|||
|
|
@ -21,13 +21,13 @@ const Logger = require('../Logger')
|
|||
/**
|
||||
* @typedef SeriesExpandedProperties
|
||||
* @property {{sequence:string}} bookSeries
|
||||
*
|
||||
*
|
||||
* @typedef {import('./Series') & SeriesExpandedProperties} SeriesExpanded
|
||||
*
|
||||
*
|
||||
* @typedef BookExpandedProperties
|
||||
* @property {import('./Author')[]} authors
|
||||
* @property {SeriesExpanded[]} series
|
||||
*
|
||||
*
|
||||
* @typedef {Book & BookExpandedProperties} BookExpanded
|
||||
*/
|
||||
|
||||
|
|
@ -112,29 +112,31 @@ class Book extends Model {
|
|||
const bookExpanded = libraryItemExpanded.media
|
||||
let authors = []
|
||||
if (bookExpanded.authors?.length) {
|
||||
authors = bookExpanded.authors.map(au => {
|
||||
authors = bookExpanded.authors.map((au) => {
|
||||
return {
|
||||
id: au.id,
|
||||
name: au.name
|
||||
}
|
||||
})
|
||||
} else if (bookExpanded.bookAuthors?.length) {
|
||||
authors = bookExpanded.bookAuthors.map(ba => {
|
||||
if (ba.author) {
|
||||
return {
|
||||
id: ba.author.id,
|
||||
name: ba.author.name
|
||||
authors = bookExpanded.bookAuthors
|
||||
.map((ba) => {
|
||||
if (ba.author) {
|
||||
return {
|
||||
id: ba.author.id,
|
||||
name: ba.author.name
|
||||
}
|
||||
} else {
|
||||
Logger.error(`[Book] Invalid bookExpanded bookAuthors: no author`, ba)
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
Logger.error(`[Book] Invalid bookExpanded bookAuthors: no author`, ba)
|
||||
return null
|
||||
}
|
||||
}).filter(a => a)
|
||||
})
|
||||
.filter((a) => a)
|
||||
}
|
||||
|
||||
let series = []
|
||||
if (bookExpanded.series?.length) {
|
||||
series = bookExpanded.series.map(se => {
|
||||
series = bookExpanded.series.map((se) => {
|
||||
return {
|
||||
id: se.id,
|
||||
name: se.name,
|
||||
|
|
@ -142,18 +144,20 @@ class Book extends Model {
|
|||
}
|
||||
})
|
||||
} else if (bookExpanded.bookSeries?.length) {
|
||||
series = bookExpanded.bookSeries.map(bs => {
|
||||
if (bs.series) {
|
||||
return {
|
||||
id: bs.series.id,
|
||||
name: bs.series.name,
|
||||
sequence: bs.sequence
|
||||
series = bookExpanded.bookSeries
|
||||
.map((bs) => {
|
||||
if (bs.series) {
|
||||
return {
|
||||
id: bs.series.id,
|
||||
name: bs.series.name,
|
||||
sequence: bs.sequence
|
||||
}
|
||||
} else {
|
||||
Logger.error(`[Book] Invalid bookExpanded bookSeries: no series`, bs)
|
||||
return null
|
||||
}
|
||||
} else {
|
||||
Logger.error(`[Book] Invalid bookExpanded bookSeries: no series`, bs)
|
||||
return null
|
||||
}
|
||||
}).filter(s => s)
|
||||
})
|
||||
.filter((s) => s)
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -185,7 +189,7 @@ class Book extends Model {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {object} oldBook
|
||||
* @param {object} oldBook
|
||||
* @returns {boolean} true if updated
|
||||
*/
|
||||
static saveFromOld(oldBook) {
|
||||
|
|
@ -194,10 +198,12 @@ class Book extends Model {
|
|||
where: {
|
||||
id: book.id
|
||||
}
|
||||
}).then(result => result[0] > 0).catch((error) => {
|
||||
Logger.error(`[Book] Failed to save book ${book.id}`, error)
|
||||
return false
|
||||
})
|
||||
.then((result) => result[0] > 0)
|
||||
.catch((error) => {
|
||||
Logger.error(`[Book] Failed to save book ${book.id}`, error)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
static getFromOld(oldBook) {
|
||||
|
|
@ -219,7 +225,7 @@ class Book extends Model {
|
|||
ebookFile: oldBook.ebookFile?.toJSON() || null,
|
||||
coverPath: oldBook.coverPath,
|
||||
duration: oldBook.duration,
|
||||
audioFiles: oldBook.audioFiles?.map(af => af.toJSON()) || [],
|
||||
audioFiles: oldBook.audioFiles?.map((af) => af.toJSON()) || [],
|
||||
chapters: oldBook.chapters,
|
||||
tags: oldBook.tags,
|
||||
genres: oldBook.metadata.genres
|
||||
|
|
@ -229,12 +235,12 @@ class Book extends Model {
|
|||
getAbsMetadataJson() {
|
||||
return {
|
||||
tags: this.tags || [],
|
||||
chapters: this.chapters?.map(c => ({ ...c })) || [],
|
||||
chapters: this.chapters?.map((c) => ({ ...c })) || [],
|
||||
title: this.title,
|
||||
subtitle: this.subtitle,
|
||||
authors: this.authors.map(a => a.name),
|
||||
authors: this.authors.map((a) => a.name),
|
||||
narrators: this.narrators,
|
||||
series: this.series.map(se => {
|
||||
series: this.series.map((se) => {
|
||||
const sequence = se.bookSeries?.sequence || ''
|
||||
if (!sequence) return se.name
|
||||
return `${se.name} #${sequence}`
|
||||
|
|
@ -254,61 +260,66 @@ class Book extends Model {
|
|||
|
||||
/**
|
||||
* Initialize model
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
*/
|
||||
static init(sequelize) {
|
||||
super.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
title: DataTypes.STRING,
|
||||
titleIgnorePrefix: DataTypes.STRING,
|
||||
subtitle: DataTypes.STRING,
|
||||
publishedYear: DataTypes.STRING,
|
||||
publishedDate: DataTypes.STRING,
|
||||
publisher: DataTypes.STRING,
|
||||
description: DataTypes.TEXT,
|
||||
isbn: DataTypes.STRING,
|
||||
asin: DataTypes.STRING,
|
||||
language: DataTypes.STRING,
|
||||
explicit: DataTypes.BOOLEAN,
|
||||
abridged: DataTypes.BOOLEAN,
|
||||
coverPath: DataTypes.STRING,
|
||||
duration: DataTypes.FLOAT,
|
||||
super.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
title: DataTypes.STRING,
|
||||
titleIgnorePrefix: DataTypes.STRING,
|
||||
subtitle: DataTypes.STRING,
|
||||
publishedYear: DataTypes.STRING,
|
||||
publishedDate: DataTypes.STRING,
|
||||
publisher: DataTypes.STRING,
|
||||
description: DataTypes.TEXT,
|
||||
isbn: DataTypes.STRING,
|
||||
asin: DataTypes.STRING,
|
||||
language: DataTypes.STRING,
|
||||
explicit: DataTypes.BOOLEAN,
|
||||
abridged: DataTypes.BOOLEAN,
|
||||
coverPath: DataTypes.STRING,
|
||||
duration: DataTypes.FLOAT,
|
||||
|
||||
narrators: DataTypes.JSON,
|
||||
audioFiles: DataTypes.JSON,
|
||||
ebookFile: DataTypes.JSON,
|
||||
chapters: DataTypes.JSON,
|
||||
tags: DataTypes.JSON,
|
||||
genres: DataTypes.JSON
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'book',
|
||||
indexes: [
|
||||
{
|
||||
fields: [{
|
||||
name: 'title',
|
||||
collate: 'NOCASE'
|
||||
}]
|
||||
},
|
||||
// {
|
||||
// fields: [{
|
||||
// name: 'titleIgnorePrefix',
|
||||
// collate: 'NOCASE'
|
||||
// }]
|
||||
// },
|
||||
{
|
||||
fields: ['publishedYear']
|
||||
},
|
||||
// {
|
||||
// fields: ['duration']
|
||||
// }
|
||||
]
|
||||
})
|
||||
narrators: DataTypes.JSON,
|
||||
audioFiles: DataTypes.JSON,
|
||||
ebookFile: DataTypes.JSON,
|
||||
chapters: DataTypes.JSON,
|
||||
tags: DataTypes.JSON,
|
||||
genres: DataTypes.JSON
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'book',
|
||||
indexes: [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'title',
|
||||
collate: 'NOCASE'
|
||||
}
|
||||
]
|
||||
},
|
||||
// {
|
||||
// fields: [{
|
||||
// name: 'titleIgnorePrefix',
|
||||
// collate: 'NOCASE'
|
||||
// }]
|
||||
// },
|
||||
{
|
||||
fields: ['publishedYear']
|
||||
}
|
||||
// {
|
||||
// fields: ['duration']
|
||||
// }
|
||||
]
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Book
|
||||
module.exports = Book
|
||||
|
|
|
|||
|
|
@ -25,21 +25,24 @@ class BookAuthor extends Model {
|
|||
|
||||
/**
|
||||
* Initialize model
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
*/
|
||||
static init(sequelize) {
|
||||
super.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
super.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
}
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'bookAuthor',
|
||||
timestamps: true,
|
||||
updatedAt: false
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'bookAuthor',
|
||||
timestamps: true,
|
||||
updatedAt: false
|
||||
})
|
||||
)
|
||||
|
||||
// Super Many-to-Many
|
||||
// ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship
|
||||
|
|
@ -58,4 +61,4 @@ class BookAuthor extends Model {
|
|||
BookAuthor.belongsTo(author)
|
||||
}
|
||||
}
|
||||
module.exports = BookAuthor
|
||||
module.exports = BookAuthor
|
||||
|
|
|
|||
|
|
@ -27,22 +27,25 @@ class BookSeries extends Model {
|
|||
|
||||
/**
|
||||
* Initialize model
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
*/
|
||||
static init(sequelize) {
|
||||
super.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
super.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
sequence: DataTypes.STRING
|
||||
},
|
||||
sequence: DataTypes.STRING
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'bookSeries',
|
||||
timestamps: true,
|
||||
updatedAt: false
|
||||
})
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'bookSeries',
|
||||
timestamps: true,
|
||||
updatedAt: false
|
||||
}
|
||||
)
|
||||
|
||||
// Super Many-to-Many
|
||||
// ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship
|
||||
|
|
@ -62,4 +65,4 @@ class BookSeries extends Model {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = BookSeries
|
||||
module.exports = BookSeries
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ const { DataTypes, Model, Sequelize } = require('sequelize')
|
|||
|
||||
const oldCollection = require('../objects/Collection')
|
||||
|
||||
|
||||
class Collection extends Model {
|
||||
constructor(values, options) {
|
||||
super(values, options)
|
||||
|
|
@ -20,27 +19,13 @@ class Collection extends Model {
|
|||
/** @type {Date} */
|
||||
this.createdAt
|
||||
}
|
||||
/**
|
||||
* Get all old collections
|
||||
* @returns {Promise<oldCollection[]>}
|
||||
*/
|
||||
static async getOldCollections() {
|
||||
const collections = await this.findAll({
|
||||
include: {
|
||||
model: this.sequelize.models.book,
|
||||
include: this.sequelize.models.libraryItem
|
||||
},
|
||||
order: [[this.sequelize.models.book, this.sequelize.models.collectionBook, 'order', 'ASC']]
|
||||
})
|
||||
return collections.map(c => this.getOldCollection(c))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all old collections toJSONExpanded, items filtered for user permissions
|
||||
* @param {[oldUser]} user
|
||||
* @param {[string]} libraryId
|
||||
* @param {[string[]]} include
|
||||
* @returns {Promise<object[]>} oldCollection.toJSONExpanded
|
||||
* @param {oldUser} [user]
|
||||
* @param {string} [libraryId]
|
||||
* @param {string[]} [include]
|
||||
* @returns {Promise<oldCollection[]>} oldCollection.toJSONExpanded
|
||||
*/
|
||||
static async getOldCollectionsJsonExpanded(user, libraryId, include) {
|
||||
let collectionWhere = null
|
||||
|
|
@ -78,8 +63,7 @@ class Collection extends Model {
|
|||
through: {
|
||||
attributes: ['sequence']
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
]
|
||||
},
|
||||
...collectionIncludes
|
||||
|
|
@ -87,11 +71,84 @@ class Collection extends Model {
|
|||
order: [[this.sequelize.models.book, this.sequelize.models.collectionBook, 'order', 'ASC']]
|
||||
})
|
||||
// TODO: Handle user permission restrictions on initial query
|
||||
return collections.map(c => {
|
||||
const oldCollection = this.getOldCollection(c)
|
||||
return collections
|
||||
.map((c) => {
|
||||
const oldCollection = this.getOldCollection(c)
|
||||
|
||||
// Filter books using user permissions
|
||||
const books = c.books?.filter(b => {
|
||||
// Filter books using user permissions
|
||||
const books =
|
||||
c.books?.filter((b) => {
|
||||
if (user) {
|
||||
if (b.tags?.length && !user.checkCanAccessLibraryItemWithTags(b.tags)) {
|
||||
return false
|
||||
}
|
||||
if (b.explicit === true && !user.canAccessExplicitContent) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}) || []
|
||||
|
||||
// Map to library items
|
||||
const libraryItems = books.map((b) => {
|
||||
const libraryItem = b.libraryItem
|
||||
delete b.libraryItem
|
||||
libraryItem.media = b
|
||||
return this.sequelize.models.libraryItem.getOldLibraryItem(libraryItem)
|
||||
})
|
||||
|
||||
// Users with restricted permissions will not see this collection
|
||||
if (!books.length && oldCollection.books.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const collectionExpanded = oldCollection.toJSONExpanded(libraryItems)
|
||||
|
||||
// Map feed if found
|
||||
if (c.feeds?.length) {
|
||||
collectionExpanded.rssFeed = this.sequelize.models.feed.getOldFeed(c.feeds[0])
|
||||
}
|
||||
|
||||
return collectionExpanded
|
||||
})
|
||||
.filter((c) => c)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get old collection toJSONExpanded, items filtered for user permissions
|
||||
* @param {oldUser} [user]
|
||||
* @param {string[]} [include]
|
||||
* @returns {Promise<oldCollection>} oldCollection.toJSONExpanded
|
||||
*/
|
||||
async getOldJsonExpanded(user, include) {
|
||||
this.books =
|
||||
(await this.getBooks({
|
||||
include: [
|
||||
{
|
||||
model: this.sequelize.models.libraryItem
|
||||
},
|
||||
{
|
||||
model: this.sequelize.models.author,
|
||||
through: {
|
||||
attributes: []
|
||||
}
|
||||
},
|
||||
{
|
||||
model: this.sequelize.models.series,
|
||||
through: {
|
||||
attributes: ['sequence']
|
||||
}
|
||||
}
|
||||
],
|
||||
order: [Sequelize.literal('`collectionBook.order` ASC')]
|
||||
})) || []
|
||||
|
||||
const oldCollection = this.sequelize.models.collection.getOldCollection(this)
|
||||
|
||||
// Filter books using user permissions
|
||||
// TODO: Handle user permission restrictions on initial query
|
||||
const books =
|
||||
this.books?.filter((b) => {
|
||||
if (user) {
|
||||
if (b.tags?.length && !user.checkCanAccessLibraryItemWithTags(b.tags)) {
|
||||
return false
|
||||
|
|
@ -103,77 +160,8 @@ class Collection extends Model {
|
|||
return true
|
||||
}) || []
|
||||
|
||||
// Map to library items
|
||||
const libraryItems = books.map(b => {
|
||||
const libraryItem = b.libraryItem
|
||||
delete b.libraryItem
|
||||
libraryItem.media = b
|
||||
return this.sequelize.models.libraryItem.getOldLibraryItem(libraryItem)
|
||||
})
|
||||
|
||||
// Users with restricted permissions will not see this collection
|
||||
if (!books.length && oldCollection.books.length) {
|
||||
return null
|
||||
}
|
||||
|
||||
const collectionExpanded = oldCollection.toJSONExpanded(libraryItems)
|
||||
|
||||
// Map feed if found
|
||||
if (c.feeds?.length) {
|
||||
collectionExpanded.rssFeed = this.sequelize.models.feed.getOldFeed(c.feeds[0])
|
||||
}
|
||||
|
||||
return collectionExpanded
|
||||
}).filter(c => c)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get old collection toJSONExpanded, items filtered for user permissions
|
||||
* @param {[oldUser]} user
|
||||
* @param {[string[]]} include
|
||||
* @returns {Promise<object>} oldCollection.toJSONExpanded
|
||||
*/
|
||||
async getOldJsonExpanded(user, include) {
|
||||
this.books = await this.getBooks({
|
||||
include: [
|
||||
{
|
||||
model: this.sequelize.models.libraryItem
|
||||
},
|
||||
{
|
||||
model: this.sequelize.models.author,
|
||||
through: {
|
||||
attributes: []
|
||||
}
|
||||
},
|
||||
{
|
||||
model: this.sequelize.models.series,
|
||||
through: {
|
||||
attributes: ['sequence']
|
||||
}
|
||||
},
|
||||
|
||||
],
|
||||
order: [Sequelize.literal('`collectionBook.order` ASC')]
|
||||
}) || []
|
||||
|
||||
const oldCollection = this.sequelize.models.collection.getOldCollection(this)
|
||||
|
||||
// Filter books using user permissions
|
||||
// TODO: Handle user permission restrictions on initial query
|
||||
const books = this.books?.filter(b => {
|
||||
if (user) {
|
||||
if (b.tags?.length && !user.checkCanAccessLibraryItemWithTags(b.tags)) {
|
||||
return false
|
||||
}
|
||||
if (b.explicit === true && !user.canAccessExplicitContent) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}) || []
|
||||
|
||||
// Map to library items
|
||||
const libraryItems = books.map(b => {
|
||||
const libraryItems = books.map((b) => {
|
||||
const libraryItem = b.libraryItem
|
||||
delete b.libraryItem
|
||||
libraryItem.media = b
|
||||
|
|
@ -199,11 +187,11 @@ class Collection extends Model {
|
|||
|
||||
/**
|
||||
* Get old collection from Collection
|
||||
* @param {Collection} collectionExpanded
|
||||
* @param {Collection} collectionExpanded
|
||||
* @returns {oldCollection}
|
||||
*/
|
||||
static getOldCollection(collectionExpanded) {
|
||||
const libraryItemIds = collectionExpanded.books?.map(b => b.libraryItem?.id || null).filter(lid => lid) || []
|
||||
const libraryItemIds = collectionExpanded.books?.map((b) => b.libraryItem?.id || null).filter((lid) => lid) || []
|
||||
return new oldCollection({
|
||||
id: collectionExpanded.id,
|
||||
libraryId: collectionExpanded.libraryId,
|
||||
|
|
@ -215,6 +203,11 @@ class Collection extends Model {
|
|||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {oldCollection} oldCollection
|
||||
* @returns {Promise<Collection>}
|
||||
*/
|
||||
static createFromOld(oldCollection) {
|
||||
const collection = this.getFromOld(oldCollection)
|
||||
return this.create(collection)
|
||||
|
|
@ -239,7 +232,7 @@ class Collection extends Model {
|
|||
|
||||
/**
|
||||
* Get old collection by id
|
||||
* @param {string} collectionId
|
||||
* @param {string} collectionId
|
||||
* @returns {Promise<oldCollection|null>} returns null if not found
|
||||
*/
|
||||
static async getOldById(collectionId) {
|
||||
|
|
@ -260,34 +253,34 @@ class Collection extends Model {
|
|||
* @returns {Promise<oldCollection>}
|
||||
*/
|
||||
async getOld() {
|
||||
this.books = await this.getBooks({
|
||||
include: [
|
||||
{
|
||||
model: this.sequelize.models.libraryItem
|
||||
},
|
||||
{
|
||||
model: this.sequelize.models.author,
|
||||
through: {
|
||||
attributes: []
|
||||
this.books =
|
||||
(await this.getBooks({
|
||||
include: [
|
||||
{
|
||||
model: this.sequelize.models.libraryItem
|
||||
},
|
||||
{
|
||||
model: this.sequelize.models.author,
|
||||
through: {
|
||||
attributes: []
|
||||
}
|
||||
},
|
||||
{
|
||||
model: this.sequelize.models.series,
|
||||
through: {
|
||||
attributes: ['sequence']
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
model: this.sequelize.models.series,
|
||||
through: {
|
||||
attributes: ['sequence']
|
||||
}
|
||||
},
|
||||
|
||||
],
|
||||
order: [Sequelize.literal('`collectionBook.order` ASC')]
|
||||
}) || []
|
||||
],
|
||||
order: [Sequelize.literal('`collectionBook.order` ASC')]
|
||||
})) || []
|
||||
|
||||
return this.sequelize.models.collection.getOldCollection(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all collections belonging to library
|
||||
* @param {string} libraryId
|
||||
* @param {string} libraryId
|
||||
* @returns {Promise<number>} number of collections destroyed
|
||||
*/
|
||||
static async removeAllForLibrary(libraryId) {
|
||||
|
|
@ -299,38 +292,26 @@ class Collection extends Model {
|
|||
})
|
||||
}
|
||||
|
||||
static async getAllForBook(bookId) {
|
||||
const collections = await this.findAll({
|
||||
include: {
|
||||
model: this.sequelize.models.book,
|
||||
where: {
|
||||
id: bookId
|
||||
},
|
||||
required: true,
|
||||
include: this.sequelize.models.libraryItem
|
||||
},
|
||||
order: [[this.sequelize.models.book, this.sequelize.models.collectionBook, 'order', 'ASC']]
|
||||
})
|
||||
return collections.map(c => this.getOldCollection(c))
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize model
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
*/
|
||||
static init(sequelize) {
|
||||
super.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
super.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
name: DataTypes.STRING,
|
||||
description: DataTypes.TEXT
|
||||
},
|
||||
name: DataTypes.STRING,
|
||||
description: DataTypes.TEXT
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'collection'
|
||||
})
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'collection'
|
||||
}
|
||||
)
|
||||
|
||||
const { library } = sequelize.models
|
||||
|
||||
|
|
@ -339,4 +320,4 @@ class Collection extends Model {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = Collection
|
||||
module.exports = Collection
|
||||
|
|
|
|||
|
|
@ -26,19 +26,22 @@ class CollectionBook extends Model {
|
|||
}
|
||||
|
||||
static init(sequelize) {
|
||||
super.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
super.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
order: DataTypes.INTEGER
|
||||
},
|
||||
order: DataTypes.INTEGER
|
||||
}, {
|
||||
sequelize,
|
||||
timestamps: true,
|
||||
updatedAt: false,
|
||||
modelName: 'collectionBook'
|
||||
})
|
||||
{
|
||||
sequelize,
|
||||
timestamps: true,
|
||||
updatedAt: false,
|
||||
modelName: 'collectionBook'
|
||||
}
|
||||
)
|
||||
|
||||
// Super Many-to-Many
|
||||
// ref: https://sequelize.org/docs/v6/advanced-association-concepts/advanced-many-to-many/#the-best-of-both-worlds-the-super-many-to-many-relationship
|
||||
|
|
@ -58,4 +61,4 @@ class CollectionBook extends Model {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = CollectionBook
|
||||
module.exports = CollectionBook
|
||||
|
|
|
|||
|
|
@ -114,26 +114,29 @@ class Device extends Model {
|
|||
|
||||
/**
|
||||
* Initialize model
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
*/
|
||||
static init(sequelize) {
|
||||
super.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
super.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
deviceId: DataTypes.STRING,
|
||||
clientName: DataTypes.STRING, // e.g. Abs Web, Abs Android
|
||||
clientVersion: DataTypes.STRING, // e.g. Server version or mobile version
|
||||
ipAddress: DataTypes.STRING,
|
||||
deviceName: DataTypes.STRING, // e.g. Windows 10 Chrome, Google Pixel 6, Apple iPhone 10,3
|
||||
deviceVersion: DataTypes.STRING, // e.g. Browser version or Android SDK
|
||||
extraData: DataTypes.JSON
|
||||
},
|
||||
deviceId: DataTypes.STRING,
|
||||
clientName: DataTypes.STRING, // e.g. Abs Web, Abs Android
|
||||
clientVersion: DataTypes.STRING, // e.g. Server version or mobile version
|
||||
ipAddress: DataTypes.STRING,
|
||||
deviceName: DataTypes.STRING, // e.g. Windows 10 Chrome, Google Pixel 6, Apple iPhone 10,3
|
||||
deviceVersion: DataTypes.STRING, // e.g. Browser version or Android SDK
|
||||
extraData: DataTypes.JSON
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'device'
|
||||
})
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'device'
|
||||
}
|
||||
)
|
||||
|
||||
const { user } = sequelize.models
|
||||
|
||||
|
|
@ -144,4 +147,4 @@ class Device extends Model {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = Device
|
||||
module.exports = Device
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ class Feed extends Model {
|
|||
model: this.sequelize.models.feedEpisode
|
||||
}
|
||||
})
|
||||
return feeds.map(f => this.getOldFeed(f))
|
||||
return feeds.map((f) => this.getOldFeed(f))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -117,7 +117,7 @@ class Feed extends Model {
|
|||
entityType: 'libraryItem'
|
||||
}
|
||||
})
|
||||
return feeds.map(f => f.entityId).filter(f => f) || []
|
||||
return feeds.map((f) => f.entityId).filter((f) => f) || []
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -179,7 +179,7 @@ class Feed extends Model {
|
|||
|
||||
// Remove and update existing feed episodes
|
||||
for (const feedEpisode of existingFeed.feedEpisodes) {
|
||||
const oldFeedEpisode = oldFeedEpisodes.find(ep => ep.id === feedEpisode.id)
|
||||
const oldFeedEpisode = oldFeedEpisodes.find((ep) => ep.id === feedEpisode.id)
|
||||
// Episode removed
|
||||
if (!oldFeedEpisode) {
|
||||
feedEpisode.destroy()
|
||||
|
|
@ -200,7 +200,7 @@ class Feed extends Model {
|
|||
|
||||
// Add new feed episodes
|
||||
for (const episode of oldFeedEpisodes) {
|
||||
if (!existingFeed.feedEpisodes.some(fe => fe.id === episode.id)) {
|
||||
if (!existingFeed.feedEpisodes.some((fe) => fe.id === episode.id)) {
|
||||
await this.sequelize.models.feedEpisode.createFromOld(feedObj.id, episode)
|
||||
hasUpdates = true
|
||||
}
|
||||
|
|
@ -258,41 +258,44 @@ class Feed extends Model {
|
|||
|
||||
/**
|
||||
* Initialize model
|
||||
*
|
||||
*
|
||||
* Polymorphic association: Feeds can be created from LibraryItem, Collection, Playlist or Series
|
||||
* @see https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/
|
||||
*
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
*
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
*/
|
||||
static init(sequelize) {
|
||||
super.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
super.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
slug: DataTypes.STRING,
|
||||
entityType: DataTypes.STRING,
|
||||
entityId: DataTypes.UUIDV4,
|
||||
entityUpdatedAt: DataTypes.DATE,
|
||||
serverAddress: DataTypes.STRING,
|
||||
feedURL: DataTypes.STRING,
|
||||
imageURL: DataTypes.STRING,
|
||||
siteURL: DataTypes.STRING,
|
||||
title: DataTypes.STRING,
|
||||
description: DataTypes.TEXT,
|
||||
author: DataTypes.STRING,
|
||||
podcastType: DataTypes.STRING,
|
||||
language: DataTypes.STRING,
|
||||
ownerName: DataTypes.STRING,
|
||||
ownerEmail: DataTypes.STRING,
|
||||
explicit: DataTypes.BOOLEAN,
|
||||
preventIndexing: DataTypes.BOOLEAN,
|
||||
coverPath: DataTypes.STRING
|
||||
},
|
||||
slug: DataTypes.STRING,
|
||||
entityType: DataTypes.STRING,
|
||||
entityId: DataTypes.UUIDV4,
|
||||
entityUpdatedAt: DataTypes.DATE,
|
||||
serverAddress: DataTypes.STRING,
|
||||
feedURL: DataTypes.STRING,
|
||||
imageURL: DataTypes.STRING,
|
||||
siteURL: DataTypes.STRING,
|
||||
title: DataTypes.STRING,
|
||||
description: DataTypes.TEXT,
|
||||
author: DataTypes.STRING,
|
||||
podcastType: DataTypes.STRING,
|
||||
language: DataTypes.STRING,
|
||||
ownerName: DataTypes.STRING,
|
||||
ownerEmail: DataTypes.STRING,
|
||||
explicit: DataTypes.BOOLEAN,
|
||||
preventIndexing: DataTypes.BOOLEAN,
|
||||
coverPath: DataTypes.STRING
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'feed'
|
||||
})
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'feed'
|
||||
}
|
||||
)
|
||||
|
||||
const { user, libraryItem, collection, series, playlist } = sequelize.models
|
||||
|
||||
|
|
@ -335,7 +338,7 @@ class Feed extends Model {
|
|||
})
|
||||
Feed.belongsTo(playlist, { foreignKey: 'entityId', constraints: false })
|
||||
|
||||
Feed.addHook('afterFind', findResult => {
|
||||
Feed.addHook('afterFind', (findResult) => {
|
||||
if (!findResult) return
|
||||
|
||||
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||
|
|
@ -368,4 +371,4 @@ class Feed extends Model {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = Feed
|
||||
module.exports = Feed
|
||||
|
|
|
|||
|
|
@ -65,9 +65,9 @@ class FeedEpisode extends Model {
|
|||
|
||||
/**
|
||||
* Create feed episode from old model
|
||||
*
|
||||
* @param {string} feedId
|
||||
* @param {Object} oldFeedEpisode
|
||||
*
|
||||
* @param {string} feedId
|
||||
* @param {Object} oldFeedEpisode
|
||||
* @returns {Promise<FeedEpisode>}
|
||||
*/
|
||||
static createFromOld(feedId, oldFeedEpisode) {
|
||||
|
|
@ -98,33 +98,36 @@ class FeedEpisode extends Model {
|
|||
|
||||
/**
|
||||
* Initialize model
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
*/
|
||||
static init(sequelize) {
|
||||
super.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
super.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
title: DataTypes.STRING,
|
||||
author: DataTypes.STRING,
|
||||
description: DataTypes.TEXT,
|
||||
siteURL: DataTypes.STRING,
|
||||
enclosureURL: DataTypes.STRING,
|
||||
enclosureType: DataTypes.STRING,
|
||||
enclosureSize: DataTypes.BIGINT,
|
||||
pubDate: DataTypes.STRING,
|
||||
season: DataTypes.STRING,
|
||||
episode: DataTypes.STRING,
|
||||
episodeType: DataTypes.STRING,
|
||||
duration: DataTypes.FLOAT,
|
||||
filePath: DataTypes.STRING,
|
||||
explicit: DataTypes.BOOLEAN
|
||||
},
|
||||
title: DataTypes.STRING,
|
||||
author: DataTypes.STRING,
|
||||
description: DataTypes.TEXT,
|
||||
siteURL: DataTypes.STRING,
|
||||
enclosureURL: DataTypes.STRING,
|
||||
enclosureType: DataTypes.STRING,
|
||||
enclosureSize: DataTypes.BIGINT,
|
||||
pubDate: DataTypes.STRING,
|
||||
season: DataTypes.STRING,
|
||||
episode: DataTypes.STRING,
|
||||
episodeType: DataTypes.STRING,
|
||||
duration: DataTypes.FLOAT,
|
||||
filePath: DataTypes.STRING,
|
||||
explicit: DataTypes.BOOLEAN
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'feedEpisode'
|
||||
})
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'feedEpisode'
|
||||
}
|
||||
)
|
||||
|
||||
const { feed } = sequelize.models
|
||||
|
||||
|
|
@ -135,4 +138,4 @@ class FeedEpisode extends Model {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = FeedEpisode
|
||||
module.exports = FeedEpisode
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ const oldLibrary = require('../objects/Library')
|
|||
* @property {boolean} skipMatchingMediaWithIsbn
|
||||
* @property {string} autoScanCronExpression
|
||||
* @property {boolean} audiobooksOnly
|
||||
* @property {boolean} hideSingleBookSeries Do not show series that only have 1 book
|
||||
* @property {boolean} hideSingleBookSeries Do not show series that only have 1 book
|
||||
* @property {boolean} onlyShowLaterBooksInContinueSeries Skip showing books that are earlier than the max sequence read
|
||||
* @property {string[]} metadataPrecedence
|
||||
*/
|
||||
|
|
@ -54,16 +54,16 @@ class Library extends Model {
|
|||
include: this.sequelize.models.libraryFolder,
|
||||
order: [['displayOrder', 'ASC']]
|
||||
})
|
||||
return libraries.map(lib => this.getOldLibrary(lib))
|
||||
return libraries.map((lib) => this.getOldLibrary(lib))
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert expanded Library to oldLibrary
|
||||
* @param {Library} libraryExpanded
|
||||
* @param {Library} libraryExpanded
|
||||
* @returns {Promise<oldLibrary>}
|
||||
*/
|
||||
static getOldLibrary(libraryExpanded) {
|
||||
const folders = libraryExpanded.libraryFolders.map(folder => {
|
||||
const folders = libraryExpanded.libraryFolders.map((folder) => {
|
||||
return {
|
||||
id: folder.id,
|
||||
fullPath: folder.path,
|
||||
|
|
@ -90,13 +90,13 @@ class Library extends Model {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {object} oldLibrary
|
||||
* @param {object} oldLibrary
|
||||
* @returns {Library|null}
|
||||
*/
|
||||
static async createFromOld(oldLibrary) {
|
||||
const library = this.getFromOld(oldLibrary)
|
||||
|
||||
library.libraryFolders = oldLibrary.folders.map(folder => {
|
||||
library.libraryFolders = oldLibrary.folders.map((folder) => {
|
||||
return {
|
||||
id: folder.id,
|
||||
path: folder.fullPath
|
||||
|
|
@ -113,8 +113,8 @@ class Library extends Model {
|
|||
|
||||
/**
|
||||
* Update library and library folders
|
||||
* @param {object} oldLibrary
|
||||
* @returns
|
||||
* @param {object} oldLibrary
|
||||
* @returns
|
||||
*/
|
||||
static async updateFromOld(oldLibrary) {
|
||||
const existingLibrary = await this.findByPk(oldLibrary.id, {
|
||||
|
|
@ -127,7 +127,7 @@ class Library extends Model {
|
|||
|
||||
const library = this.getFromOld(oldLibrary)
|
||||
|
||||
const libraryFolders = oldLibrary.folders.map(folder => {
|
||||
const libraryFolders = oldLibrary.folders.map((folder) => {
|
||||
return {
|
||||
id: folder.id,
|
||||
path: folder.fullPath,
|
||||
|
|
@ -135,7 +135,7 @@ class Library extends Model {
|
|||
}
|
||||
})
|
||||
for (const libraryFolder of libraryFolders) {
|
||||
const existingLibraryFolder = existingLibrary.libraryFolders.find(lf => lf.id === libraryFolder.id)
|
||||
const existingLibraryFolder = existingLibrary.libraryFolders.find((lf) => lf.id === libraryFolder.id)
|
||||
if (!existingLibraryFolder) {
|
||||
await this.sequelize.models.libraryFolder.create(libraryFolder)
|
||||
} else if (existingLibraryFolder.path !== libraryFolder.path) {
|
||||
|
|
@ -143,7 +143,7 @@ class Library extends Model {
|
|||
}
|
||||
}
|
||||
|
||||
const libraryFoldersRemoved = existingLibrary.libraryFolders.filter(lf => !libraryFolders.some(_lf => _lf.id === lf.id))
|
||||
const libraryFoldersRemoved = existingLibrary.libraryFolders.filter((lf) => !libraryFolders.some((_lf) => _lf.id === lf.id))
|
||||
for (const existingLibraryFolder of libraryFoldersRemoved) {
|
||||
await existingLibraryFolder.destroy()
|
||||
}
|
||||
|
|
@ -177,8 +177,8 @@ class Library extends Model {
|
|||
|
||||
/**
|
||||
* Destroy library by id
|
||||
* @param {string} libraryId
|
||||
* @returns
|
||||
* @param {string} libraryId
|
||||
* @returns
|
||||
*/
|
||||
static removeById(libraryId) {
|
||||
return this.destroy({
|
||||
|
|
@ -197,12 +197,12 @@ class Library extends Model {
|
|||
attributes: ['id', 'displayOrder'],
|
||||
order: [['displayOrder', 'ASC']]
|
||||
})
|
||||
return libraries.map(l => l.id)
|
||||
return libraries.map((l) => l.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Find Library by primary key & return oldLibrary
|
||||
* @param {string} libraryId
|
||||
* @param {string} libraryId
|
||||
* @returns {Promise<oldLibrary|null>} Returns null if not found
|
||||
*/
|
||||
static async getOldById(libraryId) {
|
||||
|
|
@ -244,29 +244,32 @@ class Library extends Model {
|
|||
|
||||
/**
|
||||
* Initialize model
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
*/
|
||||
static init(sequelize) {
|
||||
super.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
super.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
name: DataTypes.STRING,
|
||||
displayOrder: DataTypes.INTEGER,
|
||||
icon: DataTypes.STRING,
|
||||
mediaType: DataTypes.STRING,
|
||||
provider: DataTypes.STRING,
|
||||
lastScan: DataTypes.DATE,
|
||||
lastScanVersion: DataTypes.STRING,
|
||||
settings: DataTypes.JSON,
|
||||
extraData: DataTypes.JSON
|
||||
},
|
||||
name: DataTypes.STRING,
|
||||
displayOrder: DataTypes.INTEGER,
|
||||
icon: DataTypes.STRING,
|
||||
mediaType: DataTypes.STRING,
|
||||
provider: DataTypes.STRING,
|
||||
lastScan: DataTypes.DATE,
|
||||
lastScanVersion: DataTypes.STRING,
|
||||
settings: DataTypes.JSON,
|
||||
extraData: DataTypes.JSON
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'library'
|
||||
})
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'library'
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Library
|
||||
module.exports = Library
|
||||
|
|
|
|||
|
|
@ -16,33 +16,25 @@ class LibraryFolder extends Model {
|
|||
this.updatedAt
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all library folder path strings
|
||||
* @returns {Promise<string[]>} array of library folder paths
|
||||
*/
|
||||
static async getAllLibraryFolderPaths() {
|
||||
const libraryFolders = await this.findAll({
|
||||
attributes: ['path']
|
||||
})
|
||||
return libraryFolders.map(l => l.path)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize model
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
*/
|
||||
static init(sequelize) {
|
||||
super.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
super.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
path: DataTypes.STRING
|
||||
},
|
||||
path: DataTypes.STRING
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'libraryFolder'
|
||||
})
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'libraryFolder'
|
||||
}
|
||||
)
|
||||
|
||||
const { library } = sequelize.models
|
||||
library.hasMany(LibraryFolder, {
|
||||
|
|
@ -52,4 +44,4 @@ class LibraryFolder extends Model {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = LibraryFolder
|
||||
module.exports = LibraryFolder
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ const Podcast = require('./Podcast')
|
|||
|
||||
/**
|
||||
* @typedef LibraryItemExpandedProperties
|
||||
* @property {Book.BookExpanded|Podcast.PodcastExpanded} media
|
||||
*
|
||||
* @property {Book.BookExpanded|Podcast.PodcastExpanded} media
|
||||
*
|
||||
* @typedef {LibraryItem & LibraryItemExpandedProperties} LibraryItemExpanded
|
||||
*/
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ class LibraryItem extends Model {
|
|||
/**
|
||||
* Gets library items partially expanded, not including podcast episodes
|
||||
* @todo temporary solution
|
||||
*
|
||||
*
|
||||
* @param {number} offset
|
||||
* @param {number} limit
|
||||
* @returns {Promise<LibraryItem[]>} LibraryItem
|
||||
|
|
@ -154,13 +154,13 @@ class LibraryItem extends Model {
|
|||
}
|
||||
]
|
||||
})
|
||||
return libraryItems.map(ti => this.getOldLibraryItem(ti))
|
||||
return libraryItems.map((ti) => this.getOldLibraryItem(ti))
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an expanded LibraryItem into an old library item
|
||||
*
|
||||
* @param {Model<LibraryItem>} libraryItemExpanded
|
||||
*
|
||||
* @param {Model<LibraryItem>} libraryItemExpanded
|
||||
* @returns {oldLibraryItem}
|
||||
*/
|
||||
static getOldLibraryItem(libraryItemExpanded) {
|
||||
|
|
@ -231,8 +231,8 @@ class LibraryItem extends Model {
|
|||
|
||||
/**
|
||||
* Updates libraryItem, book, authors and series from old library item
|
||||
*
|
||||
* @param {oldLibraryItem} oldLibraryItem
|
||||
*
|
||||
* @param {oldLibraryItem} oldLibraryItem
|
||||
* @returns {Promise<boolean>} true if updates were made
|
||||
*/
|
||||
static async fullUpdateFromOld(oldLibraryItem) {
|
||||
|
|
@ -280,14 +280,14 @@ class LibraryItem extends Model {
|
|||
|
||||
for (const existingPodcastEpisode of existingPodcastEpisodes) {
|
||||
// Episode was removed
|
||||
if (!updatedPodcastEpisodes.some(ep => ep.id === existingPodcastEpisode.id)) {
|
||||
if (!updatedPodcastEpisodes.some((ep) => ep.id === existingPodcastEpisode.id)) {
|
||||
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${existingPodcastEpisode.title}" was removed`)
|
||||
await existingPodcastEpisode.destroy()
|
||||
hasUpdates = true
|
||||
}
|
||||
}
|
||||
for (const updatedPodcastEpisode of updatedPodcastEpisodes) {
|
||||
const existingEpisodeMatch = existingPodcastEpisodes.find(ep => ep.id === updatedPodcastEpisode.id)
|
||||
const existingEpisodeMatch = existingPodcastEpisodes.find((ep) => ep.id === updatedPodcastEpisode.id)
|
||||
if (!existingEpisodeMatch) {
|
||||
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" episode "${updatedPodcastEpisode.title}" was added`)
|
||||
await this.sequelize.models.podcastEpisode.createFromOld(updatedPodcastEpisode)
|
||||
|
|
@ -316,12 +316,12 @@ class LibraryItem extends Model {
|
|||
const existingAuthors = libraryItemExpanded.media.authors || []
|
||||
const existingSeriesAll = libraryItemExpanded.media.series || []
|
||||
const updatedAuthors = oldLibraryItem.media.metadata.authors || []
|
||||
const uniqueUpdatedAuthors = updatedAuthors.filter((au, idx) => updatedAuthors.findIndex(a => a.id === au.id) === idx)
|
||||
const uniqueUpdatedAuthors = updatedAuthors.filter((au, idx) => updatedAuthors.findIndex((a) => a.id === au.id) === idx)
|
||||
const updatedSeriesAll = oldLibraryItem.media.metadata.series || []
|
||||
|
||||
for (const existingAuthor of existingAuthors) {
|
||||
// Author was removed from Book
|
||||
if (!uniqueUpdatedAuthors.some(au => au.id === existingAuthor.id)) {
|
||||
if (!uniqueUpdatedAuthors.some((au) => au.id === existingAuthor.id)) {
|
||||
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${existingAuthor.name}" was removed`)
|
||||
await this.sequelize.models.bookAuthor.removeByIds(existingAuthor.id, libraryItemExpanded.media.id)
|
||||
hasUpdates = true
|
||||
|
|
@ -329,7 +329,7 @@ class LibraryItem extends Model {
|
|||
}
|
||||
for (const updatedAuthor of uniqueUpdatedAuthors) {
|
||||
// Author was added
|
||||
if (!existingAuthors.some(au => au.id === updatedAuthor.id)) {
|
||||
if (!existingAuthors.some((au) => au.id === updatedAuthor.id)) {
|
||||
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" author "${updatedAuthor.name}" was added`)
|
||||
await this.sequelize.models.bookAuthor.create({ authorId: updatedAuthor.id, bookId: libraryItemExpanded.media.id })
|
||||
hasUpdates = true
|
||||
|
|
@ -337,7 +337,7 @@ class LibraryItem extends Model {
|
|||
}
|
||||
for (const existingSeries of existingSeriesAll) {
|
||||
// Series was removed
|
||||
if (!updatedSeriesAll.some(se => se.id === existingSeries.id)) {
|
||||
if (!updatedSeriesAll.some((se) => se.id === existingSeries.id)) {
|
||||
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${existingSeries.name}" was removed`)
|
||||
await this.sequelize.models.bookSeries.removeByIds(existingSeries.id, libraryItemExpanded.media.id)
|
||||
hasUpdates = true
|
||||
|
|
@ -345,7 +345,7 @@ class LibraryItem extends Model {
|
|||
}
|
||||
for (const updatedSeries of updatedSeriesAll) {
|
||||
// Series was added/updated
|
||||
const existingSeriesMatch = existingSeriesAll.find(se => se.id === updatedSeries.id)
|
||||
const existingSeriesMatch = existingSeriesAll.find((se) => se.id === updatedSeries.id)
|
||||
if (!existingSeriesMatch) {
|
||||
Logger.debug(`[LibraryItem] "${libraryItemExpanded.media.title}" series "${updatedSeries.name}" was added`)
|
||||
await this.sequelize.models.bookSeries.create({ seriesId: updatedSeries.id, bookId: libraryItemExpanded.media.id, sequence: updatedSeries.sequence })
|
||||
|
|
@ -420,7 +420,7 @@ class LibraryItem extends Model {
|
|||
lastScanVersion: oldLibraryItem.scanVersion,
|
||||
libraryId: oldLibraryItem.libraryId,
|
||||
libraryFolderId: oldLibraryItem.folderId,
|
||||
libraryFiles: oldLibraryItem.libraryFiles?.map(lf => lf.toJSON()) || [],
|
||||
libraryFiles: oldLibraryItem.libraryFiles?.map((lf) => lf.toJSON()) || [],
|
||||
extraData
|
||||
}
|
||||
}
|
||||
|
|
@ -435,8 +435,8 @@ class LibraryItem extends Model {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} libraryItemId
|
||||
*
|
||||
* @param {string} libraryItemId
|
||||
* @returns {Promise<LibraryItemExpanded>}
|
||||
*/
|
||||
static async getExpandedById(libraryItemId) {
|
||||
|
|
@ -485,7 +485,7 @@ class LibraryItem extends Model {
|
|||
|
||||
/**
|
||||
* Get old library item by id
|
||||
* @param {string} libraryItemId
|
||||
* @param {string} libraryItemId
|
||||
* @returns {oldLibraryItem}
|
||||
*/
|
||||
static async getOldById(libraryItemId) {
|
||||
|
|
@ -534,9 +534,9 @@ class LibraryItem extends Model {
|
|||
|
||||
/**
|
||||
* Get library items using filter and sort
|
||||
* @param {oldLibrary} library
|
||||
* @param {oldUser} user
|
||||
* @param {object} options
|
||||
* @param {oldLibrary} library
|
||||
* @param {oldUser} user
|
||||
* @param {object} options
|
||||
* @returns {object} { libraryItems:oldLibraryItem[], count:number }
|
||||
*/
|
||||
static async getByFilterAndSort(library, user, options) {
|
||||
|
|
@ -545,7 +545,7 @@ class LibraryItem extends Model {
|
|||
Logger.debug(`Loaded ${libraryItems.length} of ${count} items for libary page in ${((Date.now() - start) / 1000).toFixed(2)}s`)
|
||||
|
||||
return {
|
||||
libraryItems: libraryItems.map(li => {
|
||||
libraryItems: libraryItems.map((li) => {
|
||||
const oldLibraryItem = this.getOldLibraryItem(li).toJSONMinified()
|
||||
if (li.collapsedSeries) {
|
||||
oldLibraryItem.collapsedSeries = li.collapsedSeries
|
||||
|
|
@ -574,10 +574,10 @@ class LibraryItem extends Model {
|
|||
|
||||
/**
|
||||
* Get home page data personalized shelves
|
||||
* @param {oldLibrary} library
|
||||
* @param {oldUser} user
|
||||
* @param {string[]} include
|
||||
* @param {number} limit
|
||||
* @param {oldLibrary} library
|
||||
* @param {oldUser} user
|
||||
* @param {string[]} include
|
||||
* @param {number} limit
|
||||
* @returns {object[]} array of shelf objects
|
||||
*/
|
||||
static async getPersonalizedShelves(library, user, include, limit) {
|
||||
|
|
@ -588,8 +588,8 @@ class LibraryItem extends Model {
|
|||
// "Continue Listening" shelf
|
||||
const itemsInProgressPayload = await libraryFilters.getMediaItemsInProgress(library, user, include, limit, false)
|
||||
if (itemsInProgressPayload.items.length) {
|
||||
const ebookOnlyItemsInProgress = itemsInProgressPayload.items.filter(li => li.media.isEBookOnly)
|
||||
const audioOnlyItemsInProgress = itemsInProgressPayload.items.filter(li => !li.media.isEBookOnly)
|
||||
const ebookOnlyItemsInProgress = itemsInProgressPayload.items.filter((li) => li.media.isEBookOnly)
|
||||
const audioOnlyItemsInProgress = itemsInProgressPayload.items.filter((li) => !li.media.isEBookOnly)
|
||||
|
||||
shelves.push({
|
||||
id: 'continue-listening',
|
||||
|
|
@ -697,8 +697,8 @@ class LibraryItem extends Model {
|
|||
// "Listen Again" shelf
|
||||
const mediaFinishedPayload = await libraryFilters.getMediaFinished(library, user, include, limit)
|
||||
if (mediaFinishedPayload.items.length) {
|
||||
const ebookOnlyItemsInProgress = mediaFinishedPayload.items.filter(li => li.media.isEBookOnly)
|
||||
const audioOnlyItemsInProgress = mediaFinishedPayload.items.filter(li => !li.media.isEBookOnly)
|
||||
const ebookOnlyItemsInProgress = mediaFinishedPayload.items.filter((li) => li.media.isEBookOnly)
|
||||
const audioOnlyItemsInProgress = mediaFinishedPayload.items.filter((li) => !li.media.isEBookOnly)
|
||||
|
||||
shelves.push({
|
||||
id: 'listen-again',
|
||||
|
|
@ -748,27 +748,27 @@ class LibraryItem extends Model {
|
|||
/**
|
||||
* Get book library items for author, optional use user permissions
|
||||
* @param {oldAuthor} author
|
||||
* @param {[oldUser]} user
|
||||
* @param {[oldUser]} user
|
||||
* @returns {Promise<oldLibraryItem[]>}
|
||||
*/
|
||||
static async getForAuthor(author, user = null) {
|
||||
const { libraryItems } = await libraryFilters.getLibraryItemsForAuthor(author, user, undefined, undefined)
|
||||
return libraryItems.map(li => this.getOldLibraryItem(li))
|
||||
return libraryItems.map((li) => this.getOldLibraryItem(li))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get book library items in a collection
|
||||
* @param {oldCollection} collection
|
||||
* @param {oldCollection} collection
|
||||
* @returns {Promise<oldLibraryItem[]>}
|
||||
*/
|
||||
static async getForCollection(collection) {
|
||||
const libraryItems = await libraryFilters.getLibraryItemsForCollection(collection)
|
||||
return libraryItems.map(li => this.getOldLibraryItem(li))
|
||||
return libraryItems.map((li) => this.getOldLibraryItem(li))
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if library item exists
|
||||
* @param {string} libraryItemId
|
||||
* @param {string} libraryItemId
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
static async checkExistsById(libraryItemId) {
|
||||
|
|
@ -776,8 +776,8 @@ class LibraryItem extends Model {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('sequelize').WhereOptions} where
|
||||
*
|
||||
* @param {import('sequelize').WhereOptions} where
|
||||
* @param {import('sequelize').BindOrReplacements} replacements
|
||||
* @returns {Object} oldLibraryItem
|
||||
*/
|
||||
|
|
@ -822,8 +822,8 @@ class LibraryItem extends Model {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('sequelize').FindOptions} options
|
||||
*
|
||||
* @param {import('sequelize').FindOptions} options
|
||||
* @returns {Promise<Book|Podcast>}
|
||||
*/
|
||||
getMedia(options) {
|
||||
|
|
@ -833,7 +833,7 @@ class LibraryItem extends Model {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @returns {Promise<Book|Podcast>}
|
||||
*/
|
||||
getMediaExpanded() {
|
||||
|
|
@ -870,7 +870,7 @@ class LibraryItem extends Model {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
async saveMetadataFile() {
|
||||
|
|
@ -887,18 +887,18 @@ class LibraryItem extends Model {
|
|||
const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`)
|
||||
|
||||
// Expanded with series, authors, podcastEpisodes
|
||||
const mediaExpanded = this.media || await this.getMediaExpanded()
|
||||
const mediaExpanded = this.media || (await this.getMediaExpanded())
|
||||
|
||||
let jsonObject = {}
|
||||
if (this.mediaType === 'book') {
|
||||
jsonObject = {
|
||||
tags: mediaExpanded.tags || [],
|
||||
chapters: mediaExpanded.chapters?.map(c => ({ ...c })) || [],
|
||||
chapters: mediaExpanded.chapters?.map((c) => ({ ...c })) || [],
|
||||
title: mediaExpanded.title,
|
||||
subtitle: mediaExpanded.subtitle,
|
||||
authors: mediaExpanded.authors.map(a => a.name),
|
||||
authors: mediaExpanded.authors.map((a) => a.name),
|
||||
narrators: mediaExpanded.narrators,
|
||||
series: mediaExpanded.series.map(se => {
|
||||
series: mediaExpanded.series.map((se) => {
|
||||
const sequence = se.bookSeries?.sequence || ''
|
||||
if (!sequence) return se.name
|
||||
return `${se.name} #${sequence}`
|
||||
|
|
@ -934,96 +934,101 @@ class LibraryItem extends Model {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
return fsExtra.writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2)).then(async () => {
|
||||
// Add metadata.json to libraryFiles array if it is new
|
||||
let metadataLibraryFile = this.libraryFiles.find(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath))
|
||||
if (storeMetadataWithItem) {
|
||||
if (!metadataLibraryFile) {
|
||||
const newLibraryFile = new LibraryFile()
|
||||
await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
|
||||
metadataLibraryFile = newLibraryFile.toJSON()
|
||||
this.libraryFiles.push(metadataLibraryFile)
|
||||
} else {
|
||||
const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath)
|
||||
if (fileTimestamps) {
|
||||
metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs
|
||||
metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs
|
||||
metadataLibraryFile.metadata.size = fileTimestamps.size
|
||||
metadataLibraryFile.ino = fileTimestamps.ino
|
||||
return fsExtra
|
||||
.writeFile(metadataFilePath, JSON.stringify(jsonObject, null, 2))
|
||||
.then(async () => {
|
||||
// Add metadata.json to libraryFiles array if it is new
|
||||
let metadataLibraryFile = this.libraryFiles.find((lf) => lf.metadata.path === filePathToPOSIX(metadataFilePath))
|
||||
if (storeMetadataWithItem) {
|
||||
if (!metadataLibraryFile) {
|
||||
const newLibraryFile = new LibraryFile()
|
||||
await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
|
||||
metadataLibraryFile = newLibraryFile.toJSON()
|
||||
this.libraryFiles.push(metadataLibraryFile)
|
||||
} else {
|
||||
const fileTimestamps = await getFileTimestampsWithIno(metadataFilePath)
|
||||
if (fileTimestamps) {
|
||||
metadataLibraryFile.metadata.mtimeMs = fileTimestamps.mtimeMs
|
||||
metadataLibraryFile.metadata.ctimeMs = fileTimestamps.ctimeMs
|
||||
metadataLibraryFile.metadata.size = fileTimestamps.size
|
||||
metadataLibraryFile.ino = fileTimestamps.ino
|
||||
}
|
||||
}
|
||||
const libraryItemDirTimestamps = await getFileTimestampsWithIno(this.path)
|
||||
if (libraryItemDirTimestamps) {
|
||||
this.mtime = libraryItemDirTimestamps.mtimeMs
|
||||
this.ctime = libraryItemDirTimestamps.ctimeMs
|
||||
let size = 0
|
||||
this.libraryFiles.forEach((lf) => (size += !isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0))
|
||||
this.size = size
|
||||
await this.save()
|
||||
}
|
||||
}
|
||||
const libraryItemDirTimestamps = await getFileTimestampsWithIno(this.path)
|
||||
if (libraryItemDirTimestamps) {
|
||||
this.mtime = libraryItemDirTimestamps.mtimeMs
|
||||
this.ctime = libraryItemDirTimestamps.ctimeMs
|
||||
let size = 0
|
||||
this.libraryFiles.forEach((lf) => size += (!isNaN(lf.metadata.size) ? Number(lf.metadata.size) : 0))
|
||||
this.size = size
|
||||
await this.save()
|
||||
}
|
||||
}
|
||||
|
||||
Logger.debug(`Success saving abmetadata to "${metadataFilePath}"`)
|
||||
Logger.debug(`Success saving abmetadata to "${metadataFilePath}"`)
|
||||
|
||||
return metadataLibraryFile
|
||||
}).catch((error) => {
|
||||
Logger.error(`Failed to save json file at "${metadataFilePath}"`, error)
|
||||
return null
|
||||
})
|
||||
return metadataLibraryFile
|
||||
})
|
||||
.catch((error) => {
|
||||
Logger.error(`Failed to save json file at "${metadataFilePath}"`, error)
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize model
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
*/
|
||||
static init(sequelize) {
|
||||
super.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
super.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
ino: DataTypes.STRING,
|
||||
path: DataTypes.STRING,
|
||||
relPath: DataTypes.STRING,
|
||||
mediaId: DataTypes.UUIDV4,
|
||||
mediaType: DataTypes.STRING,
|
||||
isFile: DataTypes.BOOLEAN,
|
||||
isMissing: DataTypes.BOOLEAN,
|
||||
isInvalid: DataTypes.BOOLEAN,
|
||||
mtime: DataTypes.DATE(6),
|
||||
ctime: DataTypes.DATE(6),
|
||||
birthtime: DataTypes.DATE(6),
|
||||
size: DataTypes.BIGINT,
|
||||
lastScan: DataTypes.DATE,
|
||||
lastScanVersion: DataTypes.STRING,
|
||||
libraryFiles: DataTypes.JSON,
|
||||
extraData: DataTypes.JSON
|
||||
},
|
||||
ino: DataTypes.STRING,
|
||||
path: DataTypes.STRING,
|
||||
relPath: DataTypes.STRING,
|
||||
mediaId: DataTypes.UUIDV4,
|
||||
mediaType: DataTypes.STRING,
|
||||
isFile: DataTypes.BOOLEAN,
|
||||
isMissing: DataTypes.BOOLEAN,
|
||||
isInvalid: DataTypes.BOOLEAN,
|
||||
mtime: DataTypes.DATE(6),
|
||||
ctime: DataTypes.DATE(6),
|
||||
birthtime: DataTypes.DATE(6),
|
||||
size: DataTypes.BIGINT,
|
||||
lastScan: DataTypes.DATE,
|
||||
lastScanVersion: DataTypes.STRING,
|
||||
libraryFiles: DataTypes.JSON,
|
||||
extraData: DataTypes.JSON
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'libraryItem',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['createdAt']
|
||||
},
|
||||
{
|
||||
fields: ['mediaId']
|
||||
},
|
||||
{
|
||||
fields: ['libraryId', 'mediaType']
|
||||
},
|
||||
{
|
||||
fields: ['libraryId', 'mediaId', 'mediaType']
|
||||
},
|
||||
{
|
||||
fields: ['birthtime']
|
||||
},
|
||||
{
|
||||
fields: ['mtime']
|
||||
}
|
||||
]
|
||||
})
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'libraryItem',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['createdAt']
|
||||
},
|
||||
{
|
||||
fields: ['mediaId']
|
||||
},
|
||||
{
|
||||
fields: ['libraryId', 'mediaType']
|
||||
},
|
||||
{
|
||||
fields: ['libraryId', 'mediaId', 'mediaType']
|
||||
},
|
||||
{
|
||||
fields: ['birthtime']
|
||||
},
|
||||
{
|
||||
fields: ['mtime']
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
const { library, libraryFolder, book, podcast } = sequelize.models
|
||||
library.hasMany(LibraryItem)
|
||||
|
|
@ -1050,7 +1055,7 @@ class LibraryItem extends Model {
|
|||
})
|
||||
LibraryItem.belongsTo(podcast, { foreignKey: 'mediaId', constraints: false })
|
||||
|
||||
LibraryItem.addHook('afterFind', findResult => {
|
||||
LibraryItem.addHook('afterFind', (findResult) => {
|
||||
if (!findResult) return
|
||||
|
||||
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||
|
|
@ -1070,7 +1075,7 @@ class LibraryItem extends Model {
|
|||
}
|
||||
})
|
||||
|
||||
LibraryItem.addHook('afterDestroy', async instance => {
|
||||
LibraryItem.addHook('afterDestroy', async (instance) => {
|
||||
if (!instance) return
|
||||
const media = await instance.getMedia()
|
||||
if (media) {
|
||||
|
|
|
|||
|
|
@ -100,38 +100,41 @@ class MediaProgress extends Model {
|
|||
|
||||
/**
|
||||
* Initialize model
|
||||
*
|
||||
*
|
||||
* Polymorphic association: Book has many MediaProgress. PodcastEpisode has many MediaProgress.
|
||||
* @see https://sequelize.org/docs/v6/advanced-association-concepts/polymorphic-associations/
|
||||
*
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
*
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
*/
|
||||
static init(sequelize) {
|
||||
super.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
super.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
mediaItemId: DataTypes.UUIDV4,
|
||||
mediaItemType: DataTypes.STRING,
|
||||
duration: DataTypes.FLOAT,
|
||||
currentTime: DataTypes.FLOAT,
|
||||
isFinished: DataTypes.BOOLEAN,
|
||||
hideFromContinueListening: DataTypes.BOOLEAN,
|
||||
ebookLocation: DataTypes.STRING,
|
||||
ebookProgress: DataTypes.FLOAT,
|
||||
finishedAt: DataTypes.DATE,
|
||||
extraData: DataTypes.JSON
|
||||
},
|
||||
mediaItemId: DataTypes.UUIDV4,
|
||||
mediaItemType: DataTypes.STRING,
|
||||
duration: DataTypes.FLOAT,
|
||||
currentTime: DataTypes.FLOAT,
|
||||
isFinished: DataTypes.BOOLEAN,
|
||||
hideFromContinueListening: DataTypes.BOOLEAN,
|
||||
ebookLocation: DataTypes.STRING,
|
||||
ebookProgress: DataTypes.FLOAT,
|
||||
finishedAt: DataTypes.DATE,
|
||||
extraData: DataTypes.JSON
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'mediaProgress',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['updatedAt']
|
||||
}
|
||||
]
|
||||
})
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'mediaProgress',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['updatedAt']
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
const { book, podcastEpisode, user } = sequelize.models
|
||||
|
||||
|
|
@ -153,7 +156,7 @@ class MediaProgress extends Model {
|
|||
})
|
||||
MediaProgress.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false })
|
||||
|
||||
MediaProgress.addHook('afterFind', findResult => {
|
||||
MediaProgress.addHook('afterFind', (findResult) => {
|
||||
if (!findResult) return
|
||||
|
||||
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||
|
|
@ -181,4 +184,4 @@ class MediaProgress extends Model {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = MediaProgress
|
||||
module.exports = MediaProgress
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ const { DataTypes, Model } = require('sequelize')
|
|||
|
||||
const oldPlaybackSession = require('../objects/PlaybackSession')
|
||||
|
||||
|
||||
class PlaybackSession extends Model {
|
||||
constructor(values, options) {
|
||||
super(values, options)
|
||||
|
|
@ -62,7 +61,7 @@ class PlaybackSession extends Model {
|
|||
}
|
||||
]
|
||||
})
|
||||
return playbackSessions.map(session => this.getOldPlaybackSession(session))
|
||||
return playbackSessions.map((session) => this.getOldPlaybackSession(session))
|
||||
}
|
||||
|
||||
static async getById(sessionId) {
|
||||
|
|
@ -170,35 +169,38 @@ class PlaybackSession extends Model {
|
|||
|
||||
/**
|
||||
* Initialize model
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
*/
|
||||
static init(sequelize) {
|
||||
super.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
super.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
mediaItemId: DataTypes.UUIDV4,
|
||||
mediaItemType: DataTypes.STRING,
|
||||
displayTitle: DataTypes.STRING,
|
||||
displayAuthor: DataTypes.STRING,
|
||||
duration: DataTypes.FLOAT,
|
||||
playMethod: DataTypes.INTEGER,
|
||||
mediaPlayer: DataTypes.STRING,
|
||||
startTime: DataTypes.FLOAT,
|
||||
currentTime: DataTypes.FLOAT,
|
||||
serverVersion: DataTypes.STRING,
|
||||
coverPath: DataTypes.STRING,
|
||||
timeListening: DataTypes.INTEGER,
|
||||
mediaMetadata: DataTypes.JSON,
|
||||
date: DataTypes.STRING,
|
||||
dayOfWeek: DataTypes.STRING,
|
||||
extraData: DataTypes.JSON
|
||||
},
|
||||
mediaItemId: DataTypes.UUIDV4,
|
||||
mediaItemType: DataTypes.STRING,
|
||||
displayTitle: DataTypes.STRING,
|
||||
displayAuthor: DataTypes.STRING,
|
||||
duration: DataTypes.FLOAT,
|
||||
playMethod: DataTypes.INTEGER,
|
||||
mediaPlayer: DataTypes.STRING,
|
||||
startTime: DataTypes.FLOAT,
|
||||
currentTime: DataTypes.FLOAT,
|
||||
serverVersion: DataTypes.STRING,
|
||||
coverPath: DataTypes.STRING,
|
||||
timeListening: DataTypes.INTEGER,
|
||||
mediaMetadata: DataTypes.JSON,
|
||||
date: DataTypes.STRING,
|
||||
dayOfWeek: DataTypes.STRING,
|
||||
extraData: DataTypes.JSON
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'playbackSession'
|
||||
})
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'playbackSession'
|
||||
}
|
||||
)
|
||||
|
||||
const { book, podcastEpisode, user, device, library } = sequelize.models
|
||||
|
||||
|
|
@ -229,7 +231,7 @@ class PlaybackSession extends Model {
|
|||
})
|
||||
PlaybackSession.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false })
|
||||
|
||||
PlaybackSession.addHook('afterFind', findResult => {
|
||||
PlaybackSession.addHook('afterFind', (findResult) => {
|
||||
if (!findResult) return
|
||||
|
||||
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||
|
|
|
|||
|
|
@ -23,29 +23,6 @@ class Playlist extends Model {
|
|||
this.updatedAt
|
||||
}
|
||||
|
||||
static async getOldPlaylists() {
|
||||
const playlists = await this.findAll({
|
||||
include: {
|
||||
model: this.sequelize.models.playlistMediaItem,
|
||||
include: [
|
||||
{
|
||||
model: this.sequelize.models.book,
|
||||
include: this.sequelize.models.libraryItem
|
||||
},
|
||||
{
|
||||
model: this.sequelize.models.podcastEpisode,
|
||||
include: {
|
||||
model: this.sequelize.models.podcast,
|
||||
include: this.sequelize.models.libraryItem
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
order: [['playlistMediaItems', 'order', 'ASC']]
|
||||
})
|
||||
return playlists.map((p) => this.getOldPlaylist(p))
|
||||
}
|
||||
|
||||
static getOldPlaylist(playlistExpanded) {
|
||||
const items = playlistExpanded.playlistMediaItems
|
||||
.map((pmi) => {
|
||||
|
|
@ -76,8 +53,8 @@ class Playlist extends Model {
|
|||
|
||||
/**
|
||||
* Get old playlist toJSONExpanded
|
||||
* @param {[string[]]} include
|
||||
* @returns {Promise<object>} oldPlaylist.toJSONExpanded
|
||||
* @param {string[]} [include]
|
||||
* @returns {Promise<oldPlaylist>} oldPlaylist.toJSONExpanded
|
||||
*/
|
||||
async getOldJsonExpanded(include) {
|
||||
this.playlistMediaItems =
|
||||
|
|
|
|||
|
|
@ -35,24 +35,27 @@ class PlaylistMediaItem extends Model {
|
|||
|
||||
/**
|
||||
* Initialize model
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
*/
|
||||
static init(sequelize) {
|
||||
super.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
super.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
mediaItemId: DataTypes.UUIDV4,
|
||||
mediaItemType: DataTypes.STRING,
|
||||
order: DataTypes.INTEGER
|
||||
},
|
||||
mediaItemId: DataTypes.UUIDV4,
|
||||
mediaItemType: DataTypes.STRING,
|
||||
order: DataTypes.INTEGER
|
||||
}, {
|
||||
sequelize,
|
||||
timestamps: true,
|
||||
updatedAt: false,
|
||||
modelName: 'playlistMediaItem'
|
||||
})
|
||||
{
|
||||
sequelize,
|
||||
timestamps: true,
|
||||
updatedAt: false,
|
||||
modelName: 'playlistMediaItem'
|
||||
}
|
||||
)
|
||||
|
||||
const { book, podcastEpisode, playlist } = sequelize.models
|
||||
|
||||
|
|
@ -74,7 +77,7 @@ class PlaylistMediaItem extends Model {
|
|||
})
|
||||
PlaylistMediaItem.belongsTo(podcastEpisode, { foreignKey: 'mediaItemId', constraints: false })
|
||||
|
||||
PlaylistMediaItem.addHook('afterFind', findResult => {
|
||||
PlaylistMediaItem.addHook('afterFind', (findResult) => {
|
||||
if (!findResult) return
|
||||
|
||||
if (!Array.isArray(findResult)) findResult = [findResult]
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ const { DataTypes, Model } = require('sequelize')
|
|||
/**
|
||||
* @typedef PodcastExpandedProperties
|
||||
* @property {import('./PodcastEpisode')[]} podcastEpisodes
|
||||
*
|
||||
*
|
||||
* @typedef {Podcast & PodcastExpandedProperties} PodcastExpanded
|
||||
*/
|
||||
|
||||
|
|
@ -61,7 +61,7 @@ class Podcast extends Model {
|
|||
|
||||
static getOldPodcast(libraryItemExpanded) {
|
||||
const podcastExpanded = libraryItemExpanded.media
|
||||
const podcastEpisodes = podcastExpanded.podcastEpisodes?.map(ep => ep.getOldPodcastEpisode(libraryItemExpanded.id).toJSON()).sort((a, b) => a.index - b.index)
|
||||
const podcastEpisodes = podcastExpanded.podcastEpisodes?.map((ep) => ep.getOldPodcastEpisode(libraryItemExpanded.id).toJSON()).sort((a, b) => a.index - b.index)
|
||||
return {
|
||||
id: podcastExpanded.id,
|
||||
libraryItemId: libraryItemExpanded.id,
|
||||
|
|
@ -140,42 +140,45 @@ class Podcast extends Model {
|
|||
|
||||
/**
|
||||
* Initialize model
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
*/
|
||||
static init(sequelize) {
|
||||
super.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
title: DataTypes.STRING,
|
||||
titleIgnorePrefix: DataTypes.STRING,
|
||||
author: DataTypes.STRING,
|
||||
releaseDate: DataTypes.STRING,
|
||||
feedURL: DataTypes.STRING,
|
||||
imageURL: DataTypes.STRING,
|
||||
description: DataTypes.TEXT,
|
||||
itunesPageURL: DataTypes.STRING,
|
||||
itunesId: DataTypes.STRING,
|
||||
itunesArtistId: DataTypes.STRING,
|
||||
language: DataTypes.STRING,
|
||||
podcastType: DataTypes.STRING,
|
||||
explicit: DataTypes.BOOLEAN,
|
||||
super.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
title: DataTypes.STRING,
|
||||
titleIgnorePrefix: DataTypes.STRING,
|
||||
author: DataTypes.STRING,
|
||||
releaseDate: DataTypes.STRING,
|
||||
feedURL: DataTypes.STRING,
|
||||
imageURL: DataTypes.STRING,
|
||||
description: DataTypes.TEXT,
|
||||
itunesPageURL: DataTypes.STRING,
|
||||
itunesId: DataTypes.STRING,
|
||||
itunesArtistId: DataTypes.STRING,
|
||||
language: DataTypes.STRING,
|
||||
podcastType: DataTypes.STRING,
|
||||
explicit: DataTypes.BOOLEAN,
|
||||
|
||||
autoDownloadEpisodes: DataTypes.BOOLEAN,
|
||||
autoDownloadSchedule: DataTypes.STRING,
|
||||
lastEpisodeCheck: DataTypes.DATE,
|
||||
maxEpisodesToKeep: DataTypes.INTEGER,
|
||||
maxNewEpisodesToDownload: DataTypes.INTEGER,
|
||||
coverPath: DataTypes.STRING,
|
||||
tags: DataTypes.JSON,
|
||||
genres: DataTypes.JSON
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'podcast'
|
||||
})
|
||||
autoDownloadEpisodes: DataTypes.BOOLEAN,
|
||||
autoDownloadSchedule: DataTypes.STRING,
|
||||
lastEpisodeCheck: DataTypes.DATE,
|
||||
maxEpisodesToKeep: DataTypes.INTEGER,
|
||||
maxNewEpisodesToDownload: DataTypes.INTEGER,
|
||||
coverPath: DataTypes.STRING,
|
||||
tags: DataTypes.JSON,
|
||||
genres: DataTypes.JSON
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'podcast'
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Podcast
|
||||
module.exports = Podcast
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class PodcastEpisode extends Model {
|
|||
}
|
||||
|
||||
/**
|
||||
* @param {string} libraryItemId
|
||||
* @param {string} libraryItemId
|
||||
* @returns {oldPodcastEpisode}
|
||||
*/
|
||||
getOldPodcastEpisode(libraryItemId = null) {
|
||||
|
|
@ -125,40 +125,43 @@ class PodcastEpisode extends Model {
|
|||
|
||||
/**
|
||||
* Initialize model
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
*/
|
||||
static init(sequelize) {
|
||||
super.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
index: DataTypes.INTEGER,
|
||||
season: DataTypes.STRING,
|
||||
episode: DataTypes.STRING,
|
||||
episodeType: DataTypes.STRING,
|
||||
title: DataTypes.STRING,
|
||||
subtitle: DataTypes.STRING(1000),
|
||||
description: DataTypes.TEXT,
|
||||
pubDate: DataTypes.STRING,
|
||||
enclosureURL: DataTypes.STRING,
|
||||
enclosureSize: DataTypes.BIGINT,
|
||||
enclosureType: DataTypes.STRING,
|
||||
publishedAt: DataTypes.DATE,
|
||||
super.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
index: DataTypes.INTEGER,
|
||||
season: DataTypes.STRING,
|
||||
episode: DataTypes.STRING,
|
||||
episodeType: DataTypes.STRING,
|
||||
title: DataTypes.STRING,
|
||||
subtitle: DataTypes.STRING(1000),
|
||||
description: DataTypes.TEXT,
|
||||
pubDate: DataTypes.STRING,
|
||||
enclosureURL: DataTypes.STRING,
|
||||
enclosureSize: DataTypes.BIGINT,
|
||||
enclosureType: DataTypes.STRING,
|
||||
publishedAt: DataTypes.DATE,
|
||||
|
||||
audioFile: DataTypes.JSON,
|
||||
chapters: DataTypes.JSON,
|
||||
extraData: DataTypes.JSON
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'podcastEpisode',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['createdAt']
|
||||
}
|
||||
]
|
||||
})
|
||||
audioFile: DataTypes.JSON,
|
||||
chapters: DataTypes.JSON,
|
||||
extraData: DataTypes.JSON
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'podcastEpisode',
|
||||
indexes: [
|
||||
{
|
||||
fields: ['createdAt']
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
const { podcast } = sequelize.models
|
||||
podcast.hasMany(PodcastEpisode, {
|
||||
|
|
@ -168,4 +171,4 @@ class PodcastEpisode extends Model {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = PodcastEpisode
|
||||
module.exports = PodcastEpisode
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class Series extends Model {
|
|||
|
||||
static async getAllOldSeries() {
|
||||
const series = await this.findAll()
|
||||
return series.map(se => se.getOldSeries())
|
||||
return series.map((se) => se.getOldSeries())
|
||||
}
|
||||
|
||||
getOldSeries() {
|
||||
|
|
@ -77,7 +77,7 @@ class Series extends Model {
|
|||
|
||||
/**
|
||||
* Get oldSeries by id
|
||||
* @param {string} seriesId
|
||||
* @param {string} seriesId
|
||||
* @returns {Promise<oldSeries>}
|
||||
*/
|
||||
static async getOldById(seriesId) {
|
||||
|
|
@ -88,7 +88,7 @@ class Series extends Model {
|
|||
|
||||
/**
|
||||
* Check if series exists
|
||||
* @param {string} seriesId
|
||||
* @param {string} seriesId
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
static async checkExistsById(seriesId) {
|
||||
|
|
@ -97,58 +97,65 @@ class Series extends Model {
|
|||
|
||||
/**
|
||||
* Get old series by name and libraryId. name case insensitive
|
||||
*
|
||||
* @param {string} seriesName
|
||||
* @param {string} libraryId
|
||||
*
|
||||
* @param {string} seriesName
|
||||
* @param {string} libraryId
|
||||
* @returns {Promise<oldSeries>}
|
||||
*/
|
||||
static async getOldByNameAndLibrary(seriesName, libraryId) {
|
||||
const series = (await this.findOne({
|
||||
where: [
|
||||
where(fn('lower', col('name')), seriesName.toLowerCase()),
|
||||
{
|
||||
libraryId
|
||||
}
|
||||
]
|
||||
}))?.getOldSeries()
|
||||
const series = (
|
||||
await this.findOne({
|
||||
where: [
|
||||
where(fn('lower', col('name')), seriesName.toLowerCase()),
|
||||
{
|
||||
libraryId
|
||||
}
|
||||
]
|
||||
})
|
||||
)?.getOldSeries()
|
||||
return series
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize model
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
*/
|
||||
static init(sequelize) {
|
||||
super.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
name: DataTypes.STRING,
|
||||
nameIgnorePrefix: DataTypes.STRING,
|
||||
description: DataTypes.TEXT
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'series',
|
||||
indexes: [
|
||||
{
|
||||
fields: [{
|
||||
name: 'name',
|
||||
collate: 'NOCASE'
|
||||
}]
|
||||
super.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
// {
|
||||
// fields: [{
|
||||
// name: 'nameIgnorePrefix',
|
||||
// collate: 'NOCASE'
|
||||
// }]
|
||||
// },
|
||||
{
|
||||
fields: ['libraryId']
|
||||
}
|
||||
]
|
||||
})
|
||||
name: DataTypes.STRING,
|
||||
nameIgnorePrefix: DataTypes.STRING,
|
||||
description: DataTypes.TEXT
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'series',
|
||||
indexes: [
|
||||
{
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
collate: 'NOCASE'
|
||||
}
|
||||
]
|
||||
},
|
||||
// {
|
||||
// fields: [{
|
||||
// name: 'nameIgnorePrefix',
|
||||
// collate: 'NOCASE'
|
||||
// }]
|
||||
// },
|
||||
{
|
||||
fields: ['libraryId']
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
const { library } = sequelize.models
|
||||
library.hasMany(Series, {
|
||||
|
|
@ -158,4 +165,4 @@ class Series extends Model {
|
|||
}
|
||||
}
|
||||
|
||||
module.exports = Series
|
||||
module.exports = Series
|
||||
|
|
|
|||
|
|
@ -19,12 +19,11 @@ class Setting extends Model {
|
|||
}
|
||||
|
||||
static async getOldSettings() {
|
||||
const settings = (await this.findAll()).map(se => se.value)
|
||||
const settings = (await this.findAll()).map((se) => se.value)
|
||||
|
||||
|
||||
const emailSettingsJson = settings.find(se => se.id === 'email-settings')
|
||||
const serverSettingsJson = settings.find(se => se.id === 'server-settings')
|
||||
const notificationSettingsJson = settings.find(se => se.id === 'notification-settings')
|
||||
const emailSettingsJson = settings.find((se) => se.id === 'email-settings')
|
||||
const serverSettingsJson = settings.find((se) => se.id === 'server-settings')
|
||||
const notificationSettingsJson = settings.find((se) => se.id === 'notification-settings')
|
||||
|
||||
return {
|
||||
settings,
|
||||
|
|
@ -43,20 +42,23 @@ class Setting extends Model {
|
|||
|
||||
/**
|
||||
* Initialize model
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
*/
|
||||
static init(sequelize) {
|
||||
super.init({
|
||||
key: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true
|
||||
super.init(
|
||||
{
|
||||
key: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true
|
||||
},
|
||||
value: DataTypes.JSON
|
||||
},
|
||||
value: DataTypes.JSON
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'setting'
|
||||
})
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'setting'
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Setting
|
||||
module.exports = Setting
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
const uuidv4 = require("uuid").v4
|
||||
const uuidv4 = require('uuid').v4
|
||||
const sequelize = require('sequelize')
|
||||
const Logger = require('../Logger')
|
||||
const oldUser = require('../objects/user/User')
|
||||
|
|
@ -45,17 +45,17 @@ class User extends Model {
|
|||
const users = await this.findAll({
|
||||
include: this.sequelize.models.mediaProgress
|
||||
})
|
||||
return users.map(u => this.getOldUser(u))
|
||||
return users.map((u) => this.getOldUser(u))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get old user model from new
|
||||
*
|
||||
* @param {Object} userExpanded
|
||||
*
|
||||
* @param {Object} userExpanded
|
||||
* @returns {oldUser}
|
||||
*/
|
||||
static getOldUser(userExpanded) {
|
||||
const mediaProgress = userExpanded.mediaProgresses.map(mp => mp.getOldMediaProgress())
|
||||
const mediaProgress = userExpanded.mediaProgresses.map((mp) => mp.getOldMediaProgress())
|
||||
|
||||
const librariesAccessible = userExpanded.permissions?.librariesAccessible || []
|
||||
const itemTagsSelected = userExpanded.permissions?.itemTagsSelected || []
|
||||
|
|
@ -86,8 +86,8 @@ class User extends Model {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {oldUser} oldUser
|
||||
*
|
||||
* @param {oldUser} oldUser
|
||||
* @returns {Promise<User>}
|
||||
*/
|
||||
static createFromOld(oldUser) {
|
||||
|
|
@ -97,8 +97,8 @@ class User extends Model {
|
|||
|
||||
/**
|
||||
* Update User from old user model
|
||||
*
|
||||
* @param {oldUser} oldUser
|
||||
*
|
||||
* @param {oldUser} oldUser
|
||||
* @param {boolean} [hooks=true] Run before / after bulk update hooks?
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
|
|
@ -109,16 +109,18 @@ class User extends Model {
|
|||
where: {
|
||||
id: user.id
|
||||
}
|
||||
}).then((result) => result[0] > 0).catch((error) => {
|
||||
Logger.error(`[User] Failed to save user ${oldUser.id}`, error)
|
||||
return false
|
||||
})
|
||||
.then((result) => result[0] > 0)
|
||||
.catch((error) => {
|
||||
Logger.error(`[User] Failed to save user ${oldUser.id}`, error)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get new User model from old
|
||||
*
|
||||
* @param {oldUser} oldUser
|
||||
*
|
||||
* @param {oldUser} oldUser
|
||||
* @returns {Object}
|
||||
*/
|
||||
static getFromOld(oldUser) {
|
||||
|
|
@ -160,9 +162,9 @@ class User extends Model {
|
|||
|
||||
/**
|
||||
* Create root user
|
||||
* @param {string} username
|
||||
* @param {string} pash
|
||||
* @param {Auth} auth
|
||||
* @param {string} username
|
||||
* @param {string} pash
|
||||
* @param {Auth} auth
|
||||
* @returns {Promise<oldUser>}
|
||||
*/
|
||||
static async createRootUser(username, pash, auth) {
|
||||
|
|
@ -185,15 +187,15 @@ class User extends Model {
|
|||
|
||||
/**
|
||||
* Create user from openid userinfo
|
||||
* @param {Object} userinfo
|
||||
* @param {Auth} auth
|
||||
* @param {Object} userinfo
|
||||
* @param {Auth} auth
|
||||
* @returns {Promise<oldUser>}
|
||||
*/
|
||||
static async createUserFromOpenIdUserInfo(userinfo, auth) {
|
||||
const userId = uuidv4()
|
||||
// TODO: Ensure username is unique?
|
||||
const username = userinfo.preferred_username || userinfo.name || userinfo.sub
|
||||
const email = (userinfo.email && userinfo.email_verified) ? userinfo.email : null
|
||||
const email = userinfo.email && userinfo.email_verified ? userinfo.email : null
|
||||
|
||||
const token = await auth.generateAccessToken({ id: userId, username })
|
||||
|
||||
|
|
@ -218,7 +220,7 @@ class User extends Model {
|
|||
/**
|
||||
* Get a user by id or by the old database id
|
||||
* @temp User ids were updated in v2.3.0 migration and old API tokens may still use that id
|
||||
* @param {string} userId
|
||||
* @param {string} userId
|
||||
* @returns {Promise<oldUser|null>} null if not found
|
||||
*/
|
||||
static async getUserByIdOrOldId(userId) {
|
||||
|
|
@ -244,7 +246,7 @@ class User extends Model {
|
|||
|
||||
/**
|
||||
* Get user by username case insensitive
|
||||
* @param {string} username
|
||||
* @param {string} username
|
||||
* @returns {Promise<oldUser|null>} returns null if not found
|
||||
*/
|
||||
static async getUserByUsername(username) {
|
||||
|
|
@ -263,7 +265,7 @@ class User extends Model {
|
|||
|
||||
/**
|
||||
* Get user by email case insensitive
|
||||
* @param {string} username
|
||||
* @param {string} username
|
||||
* @returns {Promise<oldUser|null>} returns null if not found
|
||||
*/
|
||||
static async getUserByEmail(email) {
|
||||
|
|
@ -282,7 +284,7 @@ class User extends Model {
|
|||
|
||||
/**
|
||||
* Get user by id
|
||||
* @param {string} userId
|
||||
* @param {string} userId
|
||||
* @returns {Promise<oldUser|null>} returns null if not found
|
||||
*/
|
||||
static async getUserById(userId) {
|
||||
|
|
@ -296,7 +298,7 @@ class User extends Model {
|
|||
|
||||
/**
|
||||
* Get user by openid sub
|
||||
* @param {string} sub
|
||||
* @param {string} sub
|
||||
* @returns {Promise<oldUser|null>} returns null if not found
|
||||
*/
|
||||
static async getUserByOpenIDSub(sub) {
|
||||
|
|
@ -317,7 +319,7 @@ class User extends Model {
|
|||
const users = await this.findAll({
|
||||
attributes: ['id', 'username']
|
||||
})
|
||||
return users.map(u => {
|
||||
return users.map((u) => {
|
||||
return {
|
||||
id: u.id,
|
||||
username: u.username
|
||||
|
|
@ -340,37 +342,40 @@ class User extends Model {
|
|||
|
||||
/**
|
||||
* Initialize model
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
* @param {import('../Database').sequelize} sequelize
|
||||
*/
|
||||
static init(sequelize) {
|
||||
super.init({
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
super.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true
|
||||
},
|
||||
username: DataTypes.STRING,
|
||||
email: DataTypes.STRING,
|
||||
pash: DataTypes.STRING,
|
||||
type: DataTypes.STRING,
|
||||
token: DataTypes.STRING,
|
||||
isActive: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
},
|
||||
isLocked: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
},
|
||||
lastSeen: DataTypes.DATE,
|
||||
permissions: DataTypes.JSON,
|
||||
bookmarks: DataTypes.JSON,
|
||||
extraData: DataTypes.JSON
|
||||
},
|
||||
username: DataTypes.STRING,
|
||||
email: DataTypes.STRING,
|
||||
pash: DataTypes.STRING,
|
||||
type: DataTypes.STRING,
|
||||
token: DataTypes.STRING,
|
||||
isActive: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
},
|
||||
isLocked: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
},
|
||||
lastSeen: DataTypes.DATE,
|
||||
permissions: DataTypes.JSON,
|
||||
bookmarks: DataTypes.JSON,
|
||||
extraData: DataTypes.JSON
|
||||
}, {
|
||||
sequelize,
|
||||
modelName: 'user'
|
||||
})
|
||||
{
|
||||
sequelize,
|
||||
modelName: 'user'
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = User
|
||||
module.exports = User
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ class EmailSettings {
|
|||
this.host = null
|
||||
this.port = 465
|
||||
this.secure = true
|
||||
this.rejectUnauthorized = true
|
||||
this.user = null
|
||||
this.pass = null
|
||||
this.testAddress = null
|
||||
|
|
@ -33,11 +34,17 @@ class EmailSettings {
|
|||
this.host = settings.host
|
||||
this.port = settings.port
|
||||
this.secure = !!settings.secure
|
||||
this.rejectUnauthorized = !!settings.rejectUnauthorized
|
||||
this.user = settings.user
|
||||
this.pass = settings.pass
|
||||
this.testAddress = settings.testAddress
|
||||
this.fromAddress = settings.fromAddress
|
||||
this.ereaderDevices = settings.ereaderDevices?.map(d => ({ ...d })) || []
|
||||
this.ereaderDevices = settings.ereaderDevices?.map((d) => ({ ...d })) || []
|
||||
|
||||
// rejectUnauthorized added after v2.10.1 - defaults to true
|
||||
if (settings.rejectUnauthorized === undefined) {
|
||||
this.rejectUnauthorized = true
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
|
|
@ -46,11 +53,12 @@ class EmailSettings {
|
|||
host: this.host,
|
||||
port: this.port,
|
||||
secure: this.secure,
|
||||
rejectUnauthorized: this.rejectUnauthorized,
|
||||
user: this.user,
|
||||
pass: this.pass,
|
||||
testAddress: this.testAddress,
|
||||
fromAddress: this.fromAddress,
|
||||
ereaderDevices: this.ereaderDevices.map(d => ({ ...d }))
|
||||
ereaderDevices: this.ereaderDevices.map((d) => ({ ...d }))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,27 +70,30 @@ class EmailSettings {
|
|||
else payload.port = Number(payload.port)
|
||||
}
|
||||
if (payload.secure !== undefined) payload.secure = !!payload.secure
|
||||
if (payload.rejectUnauthorized !== undefined) payload.rejectUnauthorized = !!payload.rejectUnauthorized
|
||||
|
||||
if (payload.ereaderDevices !== undefined && !Array.isArray(payload.ereaderDevices)) payload.ereaderDevices = undefined
|
||||
|
||||
if (payload.ereaderDevices?.length) {
|
||||
// Validate ereader devices
|
||||
payload.ereaderDevices = payload.ereaderDevices.map((device) => {
|
||||
if (!device.name || !device.email) {
|
||||
Logger.error(`[EmailSettings] Update ereader device is invalid`, device)
|
||||
return null
|
||||
}
|
||||
if (!device.availabilityOption || !['adminOrUp', 'userOrUp', 'guestOrUp', 'specificUsers'].includes(device.availabilityOption)) {
|
||||
device.availabilityOption = 'adminOrUp'
|
||||
}
|
||||
if (device.availabilityOption === 'specificUsers' && !device.users?.length) {
|
||||
device.availabilityOption = 'adminOrUp'
|
||||
}
|
||||
if (device.availabilityOption !== 'specificUsers' && device.users?.length) {
|
||||
device.users = []
|
||||
}
|
||||
return device
|
||||
}).filter(d => d)
|
||||
payload.ereaderDevices = payload.ereaderDevices
|
||||
.map((device) => {
|
||||
if (!device.name || !device.email) {
|
||||
Logger.error(`[EmailSettings] Update ereader device is invalid`, device)
|
||||
return null
|
||||
}
|
||||
if (!device.availabilityOption || !['adminOrUp', 'userOrUp', 'guestOrUp', 'specificUsers'].includes(device.availabilityOption)) {
|
||||
device.availabilityOption = 'adminOrUp'
|
||||
}
|
||||
if (device.availabilityOption === 'specificUsers' && !device.users?.length) {
|
||||
device.availabilityOption = 'adminOrUp'
|
||||
}
|
||||
if (device.availabilityOption !== 'specificUsers' && device.users?.length) {
|
||||
device.users = []
|
||||
}
|
||||
return device
|
||||
})
|
||||
.filter((d) => d)
|
||||
}
|
||||
|
||||
let hasUpdates = false
|
||||
|
|
@ -116,14 +127,20 @@ class EmailSettings {
|
|||
pass: this.pass
|
||||
}
|
||||
}
|
||||
// Allow self-signed certs (https://nodemailer.com/smtp/#3-allow-self-signed-certificates)
|
||||
if (!this.rejectUnauthorized) {
|
||||
payload.tls = {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
}
|
||||
|
||||
return payload
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {EreaderDeviceObject} device
|
||||
* @param {import('../user/User')} user
|
||||
*
|
||||
* @param {EreaderDeviceObject} device
|
||||
* @param {import('../user/User')} user
|
||||
* @returns {boolean}
|
||||
*/
|
||||
checkUserCanAccessDevice(device, user) {
|
||||
|
|
@ -140,8 +157,8 @@ class EmailSettings {
|
|||
|
||||
/**
|
||||
* Get ereader devices accessible to user
|
||||
*
|
||||
* @param {import('../user/User')} user
|
||||
*
|
||||
* @param {import('../user/User')} user
|
||||
* @returns {EreaderDeviceObject[]}
|
||||
*/
|
||||
getEReaderDevices(user) {
|
||||
|
|
@ -150,12 +167,12 @@ class EmailSettings {
|
|||
|
||||
/**
|
||||
* Get ereader device by name
|
||||
*
|
||||
* @param {string} deviceName
|
||||
*
|
||||
* @param {string} deviceName
|
||||
* @returns {EreaderDeviceObject}
|
||||
*/
|
||||
getEReaderDevice(deviceName) {
|
||||
return this.ereaderDevices.find(d => d.name === deviceName)
|
||||
return this.ereaderDevices.find((d) => d.name === deviceName)
|
||||
}
|
||||
}
|
||||
module.exports = EmailSettings
|
||||
module.exports = EmailSettings
|
||||
|
|
|
|||
|
|
@ -7,13 +7,12 @@ const rra = require('../libs/recursiveReaddirAsync')
|
|||
const Logger = require('../Logger')
|
||||
const { AudioMimeType } = require('./constants')
|
||||
|
||||
|
||||
/**
|
||||
* Make sure folder separator is POSIX for Windows file paths. e.g. "C:\Users\Abs" becomes "C:/Users/Abs"
|
||||
*
|
||||
* @param {String} path - Ugly file path
|
||||
* @return {String} Pretty posix file path
|
||||
*/
|
||||
* Make sure folder separator is POSIX for Windows file paths. e.g. "C:\Users\Abs" becomes "C:/Users/Abs"
|
||||
*
|
||||
* @param {String} path - Ugly file path
|
||||
* @return {String} Pretty posix file path
|
||||
*/
|
||||
const filePathToPOSIX = (path) => {
|
||||
if (!global.isWin || !path) return path
|
||||
return path.replace(/\\/g, '/')
|
||||
|
|
@ -22,9 +21,9 @@ module.exports.filePathToPOSIX = filePathToPOSIX
|
|||
|
||||
/**
|
||||
* Check path is a child of or equal to another path
|
||||
*
|
||||
* @param {string} parentPath
|
||||
* @param {string} childPath
|
||||
*
|
||||
* @param {string} parentPath
|
||||
* @param {string} childPath
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isSameOrSubPath(parentPath, childPath) {
|
||||
|
|
@ -33,8 +32,8 @@ function isSameOrSubPath(parentPath, childPath) {
|
|||
if (parentPath === childPath) return true
|
||||
const relativePath = Path.relative(parentPath, childPath)
|
||||
return (
|
||||
relativePath === '' // Same path (e.g. parentPath = '/a/b/', childPath = '/a/b')
|
||||
|| !relativePath.startsWith('..') && !Path.isAbsolute(relativePath) // Sub path
|
||||
relativePath === '' || // Same path (e.g. parentPath = '/a/b/', childPath = '/a/b')
|
||||
(!relativePath.startsWith('..') && !Path.isAbsolute(relativePath)) // Sub path
|
||||
)
|
||||
}
|
||||
module.exports.isSameOrSubPath = isSameOrSubPath
|
||||
|
|
@ -67,8 +66,8 @@ module.exports.getFileTimestampsWithIno = getFileTimestampsWithIno
|
|||
|
||||
/**
|
||||
* Get file size
|
||||
*
|
||||
* @param {string} path
|
||||
*
|
||||
* @param {string} path
|
||||
* @returns {Promise<number>}
|
||||
*/
|
||||
module.exports.getFileSize = async (path) => {
|
||||
|
|
@ -77,8 +76,8 @@ module.exports.getFileSize = async (path) => {
|
|||
|
||||
/**
|
||||
* Get file mtimeMs
|
||||
*
|
||||
* @param {string} path
|
||||
*
|
||||
* @param {string} path
|
||||
* @returns {Promise<number>} epoch timestamp
|
||||
*/
|
||||
module.exports.getFileMTimeMs = async (path) => {
|
||||
|
|
@ -91,8 +90,8 @@ module.exports.getFileMTimeMs = async (path) => {
|
|||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} filepath
|
||||
*
|
||||
* @param {string} filepath
|
||||
* @returns {boolean}
|
||||
*/
|
||||
async function checkPathIsFile(filepath) {
|
||||
|
|
@ -106,16 +105,19 @@ async function checkPathIsFile(filepath) {
|
|||
module.exports.checkPathIsFile = checkPathIsFile
|
||||
|
||||
function getIno(path) {
|
||||
return fs.stat(path, { bigint: true }).then((data => String(data.ino))).catch((err) => {
|
||||
Logger.error('[Utils] Failed to get ino for path', path, err)
|
||||
return null
|
||||
})
|
||||
return fs
|
||||
.stat(path, { bigint: true })
|
||||
.then((data) => String(data.ino))
|
||||
.catch((err) => {
|
||||
Logger.error('[Utils] Failed to get ino for path', path, err)
|
||||
return null
|
||||
})
|
||||
}
|
||||
module.exports.getIno = getIno
|
||||
|
||||
/**
|
||||
* Read contents of file
|
||||
* @param {string} path
|
||||
* @param {string} path
|
||||
* @returns {string}
|
||||
*/
|
||||
async function readTextFile(path) {
|
||||
|
|
@ -144,8 +146,8 @@ module.exports.bytesPretty = bytesPretty
|
|||
|
||||
/**
|
||||
* Get array of files inside dir
|
||||
* @param {string} path
|
||||
* @param {string} [relPathToReplace]
|
||||
* @param {string} path
|
||||
* @param {string} [relPathToReplace]
|
||||
* @returns {{name:string, path:string, dirpath:string, reldirpath:string, fullpath:string, extension:string, deep:number}[]}
|
||||
*/
|
||||
async function recurseFiles(path, relPathToReplace = null) {
|
||||
|
|
@ -177,55 +179,58 @@ async function recurseFiles(path, relPathToReplace = null) {
|
|||
|
||||
const directoriesToIgnore = []
|
||||
|
||||
list = list.filter((item) => {
|
||||
if (item.error) {
|
||||
Logger.error(`[fileUtils] Recurse files file "${item.fullname}" has error`, item.error)
|
||||
return false
|
||||
}
|
||||
list = list
|
||||
.filter((item) => {
|
||||
if (item.error) {
|
||||
Logger.error(`[fileUtils] Recurse files file "${item.fullname}" has error`, item.error)
|
||||
return false
|
||||
}
|
||||
|
||||
const relpath = item.fullname.replace(relPathToReplace, '')
|
||||
let reldirname = Path.dirname(relpath)
|
||||
if (reldirname === '.') reldirname = ''
|
||||
const dirname = Path.dirname(item.fullname)
|
||||
const relpath = item.fullname.replace(relPathToReplace, '')
|
||||
let reldirname = Path.dirname(relpath)
|
||||
if (reldirname === '.') reldirname = ''
|
||||
const dirname = Path.dirname(item.fullname)
|
||||
|
||||
// Directory has a file named ".ignore" flag directory and ignore
|
||||
if (item.name === '.ignore' && reldirname && reldirname !== '.' && !directoriesToIgnore.includes(dirname)) {
|
||||
Logger.debug(`[fileUtils] .ignore found - ignoring directory "${reldirname}"`)
|
||||
directoriesToIgnore.push(dirname)
|
||||
return false
|
||||
}
|
||||
// Directory has a file named ".ignore" flag directory and ignore
|
||||
if (item.name === '.ignore' && reldirname && reldirname !== '.' && !directoriesToIgnore.includes(dirname)) {
|
||||
Logger.debug(`[fileUtils] .ignore found - ignoring directory "${reldirname}"`)
|
||||
directoriesToIgnore.push(dirname)
|
||||
return false
|
||||
}
|
||||
|
||||
if (item.extension === '.part') {
|
||||
Logger.debug(`[fileUtils] Ignoring .part file "${relpath}"`)
|
||||
return false
|
||||
}
|
||||
if (item.extension === '.part') {
|
||||
Logger.debug(`[fileUtils] Ignoring .part file "${relpath}"`)
|
||||
return false
|
||||
}
|
||||
|
||||
// Ignore any file if a directory or the filename starts with "."
|
||||
if (relpath.split('/').find(p => p.startsWith('.'))) {
|
||||
Logger.debug(`[fileUtils] Ignoring path has . "${relpath}"`)
|
||||
return false
|
||||
}
|
||||
// Ignore any file if a directory or the filename starts with "."
|
||||
if (relpath.split('/').find((p) => p.startsWith('.'))) {
|
||||
Logger.debug(`[fileUtils] Ignoring path has . "${relpath}"`)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}).filter(item => {
|
||||
// Filter out items in ignore directories
|
||||
if (directoriesToIgnore.some(dir => item.fullname.startsWith(dir))) {
|
||||
Logger.debug(`[fileUtils] Ignoring path in dir with .ignore "${item.fullname}"`)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}).map((item) => {
|
||||
var isInRoot = (item.path + '/' === relPathToReplace)
|
||||
return {
|
||||
name: item.name,
|
||||
path: item.fullname.replace(relPathToReplace, ''),
|
||||
dirpath: item.path,
|
||||
reldirpath: isInRoot ? '' : item.path.replace(relPathToReplace, ''),
|
||||
fullpath: item.fullname,
|
||||
extension: item.extension,
|
||||
deep: item.deep
|
||||
}
|
||||
})
|
||||
return true
|
||||
})
|
||||
.filter((item) => {
|
||||
// Filter out items in ignore directories
|
||||
if (directoriesToIgnore.some((dir) => item.fullname.startsWith(dir))) {
|
||||
Logger.debug(`[fileUtils] Ignoring path in dir with .ignore "${item.fullname}"`)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
.map((item) => {
|
||||
var isInRoot = item.path + '/' === relPathToReplace
|
||||
return {
|
||||
name: item.name,
|
||||
path: item.fullname.replace(relPathToReplace, ''),
|
||||
dirpath: item.path,
|
||||
reldirpath: isInRoot ? '' : item.path.replace(relPathToReplace, ''),
|
||||
fullpath: item.fullname,
|
||||
extension: item.extension,
|
||||
deep: item.deep
|
||||
}
|
||||
})
|
||||
|
||||
// Sort from least deep to most
|
||||
list.sort((a, b) => a.deep - b.deep)
|
||||
|
|
@ -237,8 +242,8 @@ module.exports.recurseFiles = recurseFiles
|
|||
/**
|
||||
* Download file from web to local file system
|
||||
* Uses SSRF filter to prevent internal URLs
|
||||
*
|
||||
* @param {string} url
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {string} filepath path to download the file to
|
||||
* @param {Function} [contentTypeFilter] validate content type before writing
|
||||
* @returns {Promise}
|
||||
|
|
@ -251,33 +256,35 @@ module.exports.downloadFile = (url, filepath, contentTypeFilter = null) => {
|
|||
method: 'GET',
|
||||
responseType: 'stream',
|
||||
timeout: 30000,
|
||||
httpAgent: ssrfFilter(url),
|
||||
httpsAgent: ssrfFilter(url)
|
||||
}).then((response) => {
|
||||
// Validate content type
|
||||
if (contentTypeFilter && !contentTypeFilter?.(response.headers?.['content-type'])) {
|
||||
return reject(new Error(`Invalid content type "${response.headers?.['content-type'] || ''}"`))
|
||||
}
|
||||
|
||||
// Write to filepath
|
||||
const writer = fs.createWriteStream(filepath)
|
||||
response.data.pipe(writer)
|
||||
|
||||
writer.on('finish', resolve)
|
||||
writer.on('error', reject)
|
||||
}).catch((err) => {
|
||||
Logger.error(`[fileUtils] Failed to download file "${filepath}"`, err)
|
||||
reject(err)
|
||||
httpAgent: global.DisableSsrfRequestFilter ? null : ssrfFilter(url),
|
||||
httpsAgent: global.DisableSsrfRequestFilter ? null : ssrfFilter(url)
|
||||
})
|
||||
.then((response) => {
|
||||
// Validate content type
|
||||
if (contentTypeFilter && !contentTypeFilter?.(response.headers?.['content-type'])) {
|
||||
return reject(new Error(`Invalid content type "${response.headers?.['content-type'] || ''}"`))
|
||||
}
|
||||
|
||||
// Write to filepath
|
||||
const writer = fs.createWriteStream(filepath)
|
||||
response.data.pipe(writer)
|
||||
|
||||
writer.on('finish', resolve)
|
||||
writer.on('error', reject)
|
||||
})
|
||||
.catch((err) => {
|
||||
Logger.error(`[fileUtils] Failed to download file "${filepath}"`, err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Download image file from web to local file system
|
||||
* Response header must have content-type of image/ (excluding svg)
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {string} filepath
|
||||
*
|
||||
* @param {string} url
|
||||
* @param {string} filepath
|
||||
* @returns {Promise}
|
||||
*/
|
||||
module.exports.downloadImageFile = (url, filepath) => {
|
||||
|
|
@ -350,14 +357,17 @@ module.exports.getAudioMimeTypeFromExtname = (extname) => {
|
|||
|
||||
module.exports.removeFile = (path) => {
|
||||
if (!path) return false
|
||||
return fs.remove(path).then(() => true).catch((error) => {
|
||||
Logger.error(`[fileUtils] Failed remove file "${path}"`, error)
|
||||
return false
|
||||
})
|
||||
return fs
|
||||
.remove(path)
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
Logger.error(`[fileUtils] Failed remove file "${path}"`, error)
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
module.exports.encodeUriPath = (path) => {
|
||||
const uri = new URL('/', "file://")
|
||||
const uri = new URL('/', 'file://')
|
||||
// we assign the path here to assure that URL control characters like # are
|
||||
// actually interpreted as part of the URL path
|
||||
uri.pathname = path
|
||||
|
|
@ -367,8 +377,8 @@ module.exports.encodeUriPath = (path) => {
|
|||
/**
|
||||
* Check if directory is writable.
|
||||
* This method is necessary because fs.access(directory, fs.constants.W_OK) does not work on Windows
|
||||
*
|
||||
* @param {string} directory
|
||||
*
|
||||
* @param {string} directory
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
module.exports.isWritable = async (directory) => {
|
||||
|
|
@ -385,7 +395,7 @@ module.exports.isWritable = async (directory) => {
|
|||
|
||||
/**
|
||||
* Get Windows drives as array e.g. ["C:/", "F:/"]
|
||||
*
|
||||
*
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
module.exports.getWindowsDrives = async () => {
|
||||
|
|
@ -398,7 +408,11 @@ module.exports.getWindowsDrives = async () => {
|
|||
reject(error)
|
||||
return
|
||||
}
|
||||
let drives = stdout?.split(/\r?\n/).map(line => line.trim()).filter(line => line).slice(1)
|
||||
let drives = stdout
|
||||
?.split(/\r?\n/)
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line)
|
||||
.slice(1)
|
||||
const validDrives = []
|
||||
for (const drive of drives) {
|
||||
let drivepath = drive + '/'
|
||||
|
|
@ -415,33 +429,35 @@ module.exports.getWindowsDrives = async () => {
|
|||
|
||||
/**
|
||||
* Get array of directory paths in a directory
|
||||
*
|
||||
* @param {string} dirPath
|
||||
*
|
||||
* @param {string} dirPath
|
||||
* @param {number} level
|
||||
* @returns {Promise<{ path:string, dirname:string, level:number }[]>}
|
||||
*/
|
||||
module.exports.getDirectoriesInPath = async (dirPath, level) => {
|
||||
try {
|
||||
const paths = await fs.readdir(dirPath)
|
||||
let dirs = await Promise.all(paths.map(async dirname => {
|
||||
const fullPath = Path.join(dirPath, dirname)
|
||||
let dirs = await Promise.all(
|
||||
paths.map(async (dirname) => {
|
||||
const fullPath = Path.join(dirPath, dirname)
|
||||
|
||||
const lstat = await fs.lstat(fullPath).catch((error) => {
|
||||
Logger.debug(`Failed to lstat "${fullPath}"`, error)
|
||||
return null
|
||||
const lstat = await fs.lstat(fullPath).catch((error) => {
|
||||
Logger.debug(`Failed to lstat "${fullPath}"`, error)
|
||||
return null
|
||||
})
|
||||
if (!lstat?.isDirectory()) return null
|
||||
|
||||
return {
|
||||
path: this.filePathToPOSIX(fullPath),
|
||||
dirname,
|
||||
level
|
||||
}
|
||||
})
|
||||
if (!lstat?.isDirectory()) return null
|
||||
|
||||
return {
|
||||
path: this.filePathToPOSIX(fullPath),
|
||||
dirname,
|
||||
level
|
||||
}
|
||||
}))
|
||||
dirs = dirs.filter(d => d)
|
||||
)
|
||||
dirs = dirs.filter((d) => d)
|
||||
return dirs
|
||||
} catch (error) {
|
||||
Logger.error('Failed to readdir', dirPath, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -220,8 +220,8 @@ module.exports.parsePodcastRssFeedXml = async (xml, excludeEpisodeMetadata = fal
|
|||
/**
|
||||
* Get podcast RSS feed as JSON
|
||||
* Uses SSRF filter to prevent internal URLs
|
||||
*
|
||||
* @param {string} feedUrl
|
||||
*
|
||||
* @param {string} feedUrl
|
||||
* @param {boolean} [excludeEpisodeMetadata=false]
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
|
@ -234,37 +234,38 @@ module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => {
|
|||
timeout: 12000,
|
||||
responseType: 'arraybuffer',
|
||||
headers: { Accept: 'application/rss+xml, application/xhtml+xml, application/xml, */*;q=0.8' },
|
||||
httpAgent: ssrfFilter(feedUrl),
|
||||
httpsAgent: ssrfFilter(feedUrl)
|
||||
}).then(async (data) => {
|
||||
|
||||
// Adding support for ios-8859-1 encoded RSS feeds.
|
||||
// See: https://github.com/advplyr/audiobookshelf/issues/1489
|
||||
const contentType = data.headers?.['content-type'] || '' // e.g. text/xml; charset=iso-8859-1
|
||||
if (contentType.toLowerCase().includes('iso-8859-1')) {
|
||||
data.data = data.data.toString('latin1')
|
||||
} else {
|
||||
data.data = data.data.toString()
|
||||
}
|
||||
|
||||
if (!data?.data) {
|
||||
Logger.error(`[podcastUtils] getPodcastFeed: Invalid podcast feed request response (${feedUrl})`)
|
||||
return null
|
||||
}
|
||||
Logger.debug(`[podcastUtils] getPodcastFeed for "${feedUrl}" success - parsing xml`)
|
||||
const payload = await this.parsePodcastRssFeedXml(data.data, excludeEpisodeMetadata)
|
||||
if (!payload) {
|
||||
return null
|
||||
}
|
||||
|
||||
// RSS feed may be a private RSS feed
|
||||
payload.podcast.metadata.feedUrl = feedUrl
|
||||
|
||||
return payload.podcast
|
||||
}).catch((error) => {
|
||||
Logger.error('[podcastUtils] getPodcastFeed Error', error)
|
||||
return null
|
||||
httpAgent: global.DisableSsrfRequestFilter ? null : ssrfFilter(feedUrl),
|
||||
httpsAgent: global.DisableSsrfRequestFilter ? null : ssrfFilter(feedUrl)
|
||||
})
|
||||
.then(async (data) => {
|
||||
// Adding support for ios-8859-1 encoded RSS feeds.
|
||||
// See: https://github.com/advplyr/audiobookshelf/issues/1489
|
||||
const contentType = data.headers?.['content-type'] || '' // e.g. text/xml; charset=iso-8859-1
|
||||
if (contentType.toLowerCase().includes('iso-8859-1')) {
|
||||
data.data = data.data.toString('latin1')
|
||||
} else {
|
||||
data.data = data.data.toString()
|
||||
}
|
||||
|
||||
if (!data?.data) {
|
||||
Logger.error(`[podcastUtils] getPodcastFeed: Invalid podcast feed request response (${feedUrl})`)
|
||||
return null
|
||||
}
|
||||
Logger.debug(`[podcastUtils] getPodcastFeed for "${feedUrl}" success - parsing xml`)
|
||||
const payload = await this.parsePodcastRssFeedXml(data.data, excludeEpisodeMetadata)
|
||||
if (!payload) {
|
||||
return null
|
||||
}
|
||||
|
||||
// RSS feed may be a private RSS feed
|
||||
payload.podcast.metadata.feedUrl = feedUrl
|
||||
|
||||
return payload.podcast
|
||||
})
|
||||
.catch((error) => {
|
||||
Logger.error('[podcastUtils] getPodcastFeed Error', error)
|
||||
return null
|
||||
})
|
||||
}
|
||||
|
||||
// Return array of episodes ordered by closest match (Levenshtein distance of 6 or less)
|
||||
|
|
@ -283,7 +284,7 @@ module.exports.findMatchingEpisodesInFeed = (feed, searchTitle) => {
|
|||
}
|
||||
|
||||
const matches = []
|
||||
feed.episodes.forEach(ep => {
|
||||
feed.episodes.forEach((ep) => {
|
||||
if (!ep.title) return
|
||||
|
||||
const epTitle = ep.title.toLowerCase().trim()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue