mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-04 09:50:42 +02:00
Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea60f19e7a | |||
| cefc75a4ed | |||
| d8753aafb9 | |||
| ba5ad228cc | |||
| 0203f9cc1b | |||
| 4770be5a39 | |||
| 1bac395bed | |||
| e818f270cd | |||
| c4e2726622 | |||
| 74d8a09f31 | |||
| 618338165e | |||
| db494001a2 | |||
| a67213fb7b | |||
| 5d96b2cc6e | |||
| 72d0b097ab | |||
| 36d2957fb4 | |||
| b5de517aad | |||
| 41db0e3bb1 | |||
| e8d582269b | |||
| 80ef8ee890 | |||
| a65859f575 | |||
| 5724887785 | |||
| 8908aa7a82 | |||
| f83dd29213 | |||
| 99d90778f4 | |||
| 49279430fc | |||
| 030c20b12e | |||
| 5e943ef152 | |||
| 4ae057f626 | |||
| 9ebe4b55dd | |||
| 2f7403adec | |||
| 2777b496ad | |||
| f7a3dbf209 | |||
| d900093976 | |||
| 08250e266e | |||
| da2d1455d7 | |||
| b6c6c4c939 | |||
| 22179d82b8 | |||
| 343ce312f1 | |||
| 10677d6fb0 | |||
| 49a8aead9b | |||
| 274b0e48be | |||
| 4d8ffc5d99 | |||
| 4f3029e5b2 | |||
| a1b49f5fcf | |||
| 89d497a305 | |||
| 9e095a4bc1 | |||
| 024d052a7b | |||
| c312979aec | |||
| 773e621944 | |||
| ed4f33b565 | |||
| f8a0852dfc | |||
| 6dec750d3e | |||
| 3c98a5fb24 | |||
| 702ee3d350 | |||
| fcc2f3650b | |||
| e4ad622c01 | |||
| 458403eec9 | |||
| aaede2752c | |||
| 39d8c2cf04 | |||
| dd5c940d36 | |||
| 277f024bbc | |||
| 59ad1e5e36 | |||
| 02c4b21d3f | |||
| 33ae5445be | |||
| 5ed06871b6 | |||
| e98eb8f1eb | |||
| f095d89980 |
@@ -4,7 +4,7 @@
|
|||||||
<h1 class="text-xl">{{ headerText }}</h1>
|
<h1 class="text-xl">{{ headerText }}</h1>
|
||||||
|
|
||||||
<div v-if="showAddButton" class="mx-2 w-7 h-7 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center" @click="clicked">
|
<div v-if="showAddButton" class="mx-2 w-7 h-7 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center" @click="clicked">
|
||||||
<button class="material-icons" :aria-label="$strings.ButtonAdd + ': ' + headerText" style="font-size: 1.4rem">add</button>
|
<button type="button" class="material-icons" :aria-label="$strings.ButtonAdd + ': ' + headerText" style="font-size: 1.4rem">add</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -361,13 +361,15 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
streamProgress(data) {
|
streamProgress(data) {
|
||||||
if (!data.numSegments) return
|
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === data.stream) {
|
||||||
var chunks = data.chunks
|
if (!data.numSegments) return
|
||||||
console.log(`[StreamContainer] Stream Progress ${data.percent}`)
|
var chunks = data.chunks
|
||||||
if (this.$refs.audioPlayer) {
|
console.log(`[StreamContainer] Stream Progress ${data.percent}`)
|
||||||
this.$refs.audioPlayer.setChunksReady(chunks, data.numSegments)
|
if (this.$refs.audioPlayer) {
|
||||||
} else {
|
this.$refs.audioPlayer.setChunksReady(chunks, data.numSegments)
|
||||||
console.error('No Audio Ref')
|
} else {
|
||||||
|
console.error('No Audio Ref')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sessionOpen(session) {
|
sessionOpen(session) {
|
||||||
|
|||||||
@@ -820,7 +820,6 @@ export default {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
if (!libraryItem) return
|
if (!libraryItem) return
|
||||||
console.log('Got library itemn', libraryItem)
|
|
||||||
this.store.commit('showEReader', libraryItem)
|
this.store.commit('showEReader', libraryItem)
|
||||||
},
|
},
|
||||||
selectBtnClick(evt) {
|
selectBtnClick(evt) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<ui-dropdown v-model="mediaType" :items="mediaTypes" :label="$strings.LabelMediaType" :disabled="!isNew" small @input="changedMediaType" />
|
<ui-dropdown v-model="mediaType" :items="mediaTypes" :label="$strings.LabelMediaType" :disabled="!isNew" small @input="changedMediaType" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full md:flex-grow px-1 py-1 md:py-0">
|
<div class="w-full md:flex-grow px-1 py-1 md:py-0">
|
||||||
<ui-text-input-with-label v-model="name" :label="$strings.LabelLibraryName" @blur="nameBlurred" />
|
<ui-text-input-with-label ref="nameInput" v-model="name" :label="$strings.LabelLibraryName" @blur="nameBlurred" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/5 md:w-18 px-1 py-1 md:py-0">
|
<div class="w-1/5 md:w-18 px-1 py-1 md:py-0">
|
||||||
<ui-media-icon-picker v-model="icon" :label="$strings.LabelIcon" @input="iconChanged" />
|
<ui-media-icon-picker v-model="icon" :label="$strings.LabelIcon" @input="iconChanged" />
|
||||||
@@ -20,12 +20,12 @@
|
|||||||
<p class="px-1 text-sm font-semibold">{{ $strings.LabelFolders }}</p>
|
<p class="px-1 text-sm font-semibold">{{ $strings.LabelFolders }}</p>
|
||||||
<div v-for="(folder, index) in folders" :key="index" class="w-full flex items-center py-1 px-2">
|
<div v-for="(folder, index) in folders" :key="index" class="w-full flex items-center py-1 px-2">
|
||||||
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||||
<ui-editable-text v-model="folder.fullPath" readonly type="text" class="w-full" />
|
<ui-editable-text ref="folderInput" v-model="folder.fullPath" readonly type="text" class="w-full" />
|
||||||
<span v-show="folders.length > 1" class="material-icons text-2xl ml-2 cursor-pointer hover:text-error" @click="removeFolder(folder)">close</span>
|
<span v-show="folders.length > 1" class="material-icons text-2xl ml-2 cursor-pointer hover:text-error" @click="removeFolder(folder)">close</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex py-1 px-2 items-center w-full">
|
<div class="flex py-1 px-2 items-center w-full">
|
||||||
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||||
<ui-editable-text v-model="newFolderPath" :placeholder="$strings.PlaceholderNewFolderPath" type="text" class="w-full" @blur="newFolderInputBlurred" />
|
<ui-editable-text ref="newFolderInput" v-model="newFolderPath" :placeholder="$strings.PlaceholderNewFolderPath" type="text" class="w-full" @blur="newFolderInputBlurred" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ui-btn class="w-full mt-2" color="primary" @click="browseForFolder">{{ $strings.ButtonBrowseForFolder }}</ui-btn>
|
<ui-btn class="w-full mt-2" color="primary" @click="browseForFolder">{{ $strings.ButtonBrowseForFolder }}</ui-btn>
|
||||||
@@ -82,6 +82,19 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
checkBlurExpressionInput() {
|
||||||
|
if (this.$refs.nameInput) {
|
||||||
|
this.$refs.nameInput.blur()
|
||||||
|
}
|
||||||
|
if (this.$refs.folderInput && this.$refs.folderInput.length) {
|
||||||
|
this.$refs.folderInput.forEach((input) => {
|
||||||
|
if (input.blur) input.blur()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.$refs.newFolderInput) {
|
||||||
|
this.$refs.newFolderInput.blur()
|
||||||
|
}
|
||||||
|
},
|
||||||
browseForFolder() {
|
browseForFolder() {
|
||||||
this.showDirectoryPicker = true
|
this.showDirectoryPicker = true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -144,8 +144,6 @@ export default {
|
|||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
submit() {
|
submit() {
|
||||||
if (!this.validate()) return
|
|
||||||
|
|
||||||
// If custom expression input is focused then unfocus it instead of submitting
|
// If custom expression input is focused then unfocus it instead of submitting
|
||||||
if (this.$refs.tabComponent && this.$refs.tabComponent.checkBlurExpressionInput) {
|
if (this.$refs.tabComponent && this.$refs.tabComponent.checkBlurExpressionInput) {
|
||||||
if (this.$refs.tabComponent.checkBlurExpressionInput()) {
|
if (this.$refs.tabComponent.checkBlurExpressionInput()) {
|
||||||
@@ -153,6 +151,8 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.validate()) return
|
||||||
|
|
||||||
if (this.library) {
|
if (this.library) {
|
||||||
this.submitUpdateLibrary()
|
this.submitUpdateLibrary()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<div class="absolute -bottom-2 left-0 flex ml-6">
|
<div class="absolute -bottom-2 left-0 flex ml-6">
|
||||||
<template v-for="dayObj in last7Days">
|
<template v-for="dayObj in last7Days">
|
||||||
<div :key="dayObj.date" :style="{ width: daySpacing + daySpacing / 14 + 'px' }">
|
<div :key="dayObj.date" :style="{ width: daySpacing + daySpacing / 14 + 'px' }">
|
||||||
<p class="text-sm font-book">{{ dayObj.dayOfWeek.slice(0, 3) }}</p>
|
<p class="text-sm font-book">{{ dayObj.dayOfWeekAbbr }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,6 +108,7 @@ export default {
|
|||||||
var _date = this.$addDaysToToday(i * -1)
|
var _date = this.$addDaysToToday(i * -1)
|
||||||
days.push({
|
days.push({
|
||||||
dayOfWeek: this.$formatJsDate(_date, 'EEEE'),
|
dayOfWeek: this.$formatJsDate(_date, 'EEEE'),
|
||||||
|
dayOfWeekAbbr: this.$formatJsDate(_date, 'EEE'),
|
||||||
date: this.$formatJsDate(_date, 'yyyy-MM-dd')
|
date: this.$formatJsDate(_date, 'yyyy-MM-dd')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -218,4 +219,4 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export default {
|
|||||||
dayLabels() {
|
dayLabels() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Mon',
|
label: this.$formatJsDate(new Date(2023, 0, 2), 'EEE'),
|
||||||
style: {
|
style: {
|
||||||
transform: `translate(${-25}px, ${13}px)`,
|
transform: `translate(${-25}px, ${13}px)`,
|
||||||
lineHeight: '10px',
|
lineHeight: '10px',
|
||||||
@@ -76,7 +76,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Wed',
|
label: this.$formatJsDate(new Date(2023, 0, 4), 'EEE'),
|
||||||
style: {
|
style: {
|
||||||
transform: `translate(${-25}px, ${13 * 3}px)`,
|
transform: `translate(${-25}px, ${13 * 3}px)`,
|
||||||
lineHeight: '10px',
|
lineHeight: '10px',
|
||||||
@@ -84,7 +84,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Fri',
|
label: this.$formatJsDate(new Date(2023, 0, 6), 'EEE'),
|
||||||
style: {
|
style: {
|
||||||
transform: `translate(${-25}px, ${13 * 5}px)`,
|
transform: `translate(${-25}px, ${13 * 5}px)`,
|
||||||
lineHeight: '10px',
|
lineHeight: '10px',
|
||||||
@@ -270,4 +270,4 @@ export default {
|
|||||||
},
|
},
|
||||||
beforeDestroy() {}
|
beforeDestroy() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -38,10 +38,10 @@
|
|||||||
<div class="w-full flex justify-left">
|
<div class="w-full flex justify-left">
|
||||||
<!-- Dont show edit for non-root users -->
|
<!-- Dont show edit for non-root users -->
|
||||||
<div v-if="user.type !== 'root' || userIsRoot" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-opacity-100 cursor-pointer" @click.stop="editUser(user)">
|
<div v-if="user.type !== 'root' || userIsRoot" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-opacity-100 cursor-pointer" @click.stop="editUser(user)">
|
||||||
<button :aria-label="$getString('ButtonUserEdit', [user.username])" class="material-icons text-base">edit</button>
|
<button type="button" :aria-label="$getString('ButtonUserEdit', [user.username])" class="material-icons text-base">edit</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="user.type !== 'root'" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="deleteUserClick(user)">
|
<div v-show="user.type !== 'root'" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="deleteUserClick(user)">
|
||||||
<button :aria-label="$getString('ButtonUserDelete', [user.username])" class="material-icons text-base">delete</button>
|
<button type="button" :aria-label="$getString('ButtonUserDelete', [user.username])" class="material-icons text-base">delete</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</p>
|
<label class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</label>
|
||||||
<div ref="wrapper" class="relative">
|
<div ref="wrapper" class="relative">
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
<div ref="inputWrapper" class="input-wrapper flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-2" :class="disabled ? 'pointer-events-none bg-black-300 text-gray-400' : 'bg-primary'">
|
<div ref="inputWrapper" class="input-wrapper flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-2" :class="disabled ? 'pointer-events-none bg-black-300 text-gray-400' : 'bg-primary'">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<button :aria-labelledby="labeledBy" role="checkbox" class="border rounded-full border-black-100 flex items-center cursor-pointer w-10 justify-start" :aria-checked="toggleValue" :class="className" @click="clickToggle">
|
<button :aria-labelledby="labeledBy" role="checkbox" type="button" class="border rounded-full border-black-100 flex items-center cursor-pointer w-10 justify-start" :aria-checked="toggleValue" :class="className" @click="clickToggle">
|
||||||
<span class="rounded-full border w-5 h-5 border-black-50 shadow transform transition-transform duration-100" :class="switchClassName"></span>
|
<span class="rounded-full border w-5 h-5 border-black-50 shadow transform transition-transform duration-100" :class="switchClassName"></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -137,31 +137,31 @@ export default {
|
|||||||
weekdays() {
|
weekdays() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
text: this.$strings.WeekdaySunday,
|
text: this.$formatJsDate(new Date(2023, 0, 1), 'EEEE'),
|
||||||
value: 0
|
value: 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.WeekdayMonday,
|
text: this.$formatJsDate(new Date(2023, 0, 2), 'EEEE'),
|
||||||
value: 1
|
value: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.WeekdayTuesday,
|
text: this.$formatJsDate(new Date(2023, 0, 3), 'EEEE'),
|
||||||
value: 2
|
value: 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.WeekdayWednesday,
|
text: this.$formatJsDate(new Date(2023, 0, 4), 'EEEE'),
|
||||||
value: 3
|
value: 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.WeekdayThursday,
|
text: this.$formatJsDate(new Date(2023, 0, 5), 'EEEE'),
|
||||||
value: 4
|
value: 4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.WeekdayFriday,
|
text: this.$formatJsDate(new Date(2023, 0, 6), 'EEEE'),
|
||||||
value: 5
|
value: 5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.WeekdaySaturday,
|
text: this.$formatJsDate(new Date(2023, 0, 7), 'EEEE'),
|
||||||
value: 6
|
value: 6
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.12",
|
"version": "2.2.13",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.12",
|
"version": "2.2.13",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.12",
|
"version": "2.2.13",
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -105,7 +105,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="tracks.length || audioFile" class="flex py-0.5">
|
<div v-if="tracks.length || audioFile || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
|
||||||
<div class="w-32">
|
<div class="w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -415,6 +415,8 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
durationPretty() {
|
durationPretty() {
|
||||||
|
if (this.isPodcast) return this.$elapsedPrettyExtended(this.totalPodcastDuration)
|
||||||
|
|
||||||
if (!this.tracks.length && !this.audioFile) return 'N/A'
|
if (!this.tracks.length && !this.audioFile) return 'N/A'
|
||||||
if (this.audioFile) return this.$elapsedPrettyExtended(this.duration)
|
if (this.audioFile) return this.$elapsedPrettyExtended(this.duration)
|
||||||
return this.$elapsedPretty(this.duration)
|
return this.$elapsedPretty(this.duration)
|
||||||
@@ -423,6 +425,12 @@ export default {
|
|||||||
if (!this.tracks.length && !this.audioFile) return 0
|
if (!this.tracks.length && !this.audioFile) return 0
|
||||||
return this.media.duration
|
return this.media.duration
|
||||||
},
|
},
|
||||||
|
totalPodcastDuration() {
|
||||||
|
if (!this.podcastEpisodes.length) return 0
|
||||||
|
let totalDuration = 0
|
||||||
|
this.podcastEpisodes.forEach((ep) => (totalDuration += ep.duration || 0))
|
||||||
|
return totalDuration
|
||||||
|
},
|
||||||
sizePretty() {
|
sizePretty() {
|
||||||
return this.$bytesPretty(this.media.size)
|
return this.$bytesPretty(this.media.size)
|
||||||
},
|
},
|
||||||
|
|||||||
+11
-9
@@ -5,18 +5,18 @@ import { supplant } from './utils'
|
|||||||
const defaultCode = 'en-us'
|
const defaultCode = 'en-us'
|
||||||
|
|
||||||
const languageCodeMap = {
|
const languageCodeMap = {
|
||||||
'de': 'Deutsch',
|
'de': { label: 'Deutsch', dateFnsLocale: 'de' },
|
||||||
'en-us': 'English',
|
'en-us': { label: 'English', dateFnsLocale: 'enUS' },
|
||||||
// 'es': 'Español',
|
// 'es': { label: 'Español', dateFnsLocale: 'es' },
|
||||||
'fr': 'Français',
|
'fr': { label: 'Français', dateFnsLocale: 'fr' },
|
||||||
'hr': 'Hrvatski',
|
'hr': { label: 'Hrvatski', dateFnsLocale: 'hr' },
|
||||||
'it': 'Italiano',
|
'it': { label: 'Italiano', dateFnsLocale: 'it' },
|
||||||
'pl': 'Polski',
|
'pl': { label: 'Polski', dateFnsLocale: 'pl' },
|
||||||
'zh-cn': '简体中文 (Simplified Chinese)'
|
'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' },
|
||||||
}
|
}
|
||||||
Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => {
|
Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => {
|
||||||
return {
|
return {
|
||||||
text: languageCodeMap[code],
|
text: languageCodeMap[code].label,
|
||||||
value: code
|
value: code
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -73,6 +73,8 @@ async function loadi18n(code) {
|
|||||||
for (const key in Vue.prototype.$strings) {
|
for (const key in Vue.prototype.$strings) {
|
||||||
Vue.prototype.$strings[key] = strings[key] || translations[defaultCode][key]
|
Vue.prototype.$strings[key] = strings[key] || translations[defaultCode][key]
|
||||||
}
|
}
|
||||||
|
console.log(`dateFnsLocale = ${languageCodeMap[code].dateFnsLocale}`)
|
||||||
|
Vue.prototype.$setDateFnsLocale(languageCodeMap[code].dateFnsLocale)
|
||||||
|
|
||||||
console.log('i18n strings=', Vue.prototype.$strings)
|
console.log('i18n strings=', Vue.prototype.$strings)
|
||||||
Vue.prototype.$eventBus.$emit('change-lang', code)
|
Vue.prototype.$eventBus.$emit('change-lang', code)
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Path from 'path'
|
import Path from 'path'
|
||||||
import vClickOutside from 'v-click-outside'
|
import vClickOutside from 'v-click-outside'
|
||||||
import { formatDistance, format, addDays, isDate } from 'date-fns'
|
import { formatDistance, format, addDays, isDate, setDefaultOptions } from 'date-fns'
|
||||||
|
import * as locale from 'date-fns/locale'
|
||||||
|
|
||||||
Vue.directive('click-outside', vClickOutside.directive)
|
Vue.directive('click-outside', vClickOutside.directive)
|
||||||
|
|
||||||
|
|
||||||
|
Vue.prototype.$setDateFnsLocale = (localeString) => {
|
||||||
|
if (!locale[localeString]) return 0
|
||||||
|
return setDefaultOptions({ locale: locale[localeString] })
|
||||||
|
}
|
||||||
Vue.prototype.$dateDistanceFromNow = (unixms) => {
|
Vue.prototype.$dateDistanceFromNow = (unixms) => {
|
||||||
if (!unixms) return ''
|
if (!unixms) return ''
|
||||||
return formatDistance(unixms, Date.now(), { addSuffix: true })
|
return formatDistance(unixms, Date.now(), { addSuffix: true })
|
||||||
|
|||||||
+35
-41
@@ -20,10 +20,10 @@
|
|||||||
"ButtonCreate": "Ertsellen",
|
"ButtonCreate": "Ertsellen",
|
||||||
"ButtonCreateBackup": "Sicherung erstellen",
|
"ButtonCreateBackup": "Sicherung erstellen",
|
||||||
"ButtonDelete": "Löschen",
|
"ButtonDelete": "Löschen",
|
||||||
"ButtonEdit": "Edit",
|
"ButtonEdit": "Bearbeiten",
|
||||||
"ButtonEditChapters": "Kapitel bearbeiten",
|
"ButtonEditChapters": "Kapitel bearbeiten",
|
||||||
"ButtonEditPodcast": "Podcast bearbeiten",
|
"ButtonEditPodcast": "Podcast bearbeiten",
|
||||||
"ButtonForceReScan": "Erzwinge kompletten Neu-Scan",
|
"ButtonForceReScan": "Komplett-Scan (alle Medien)",
|
||||||
"ButtonFullPath": "Vollständiger Pfad",
|
"ButtonFullPath": "Vollständiger Pfad",
|
||||||
"ButtonHide": "Ausblenden",
|
"ButtonHide": "Ausblenden",
|
||||||
"ButtonHome": "Startseite",
|
"ButtonHome": "Startseite",
|
||||||
@@ -34,8 +34,8 @@
|
|||||||
"ButtonLookup": "Online-Suche",
|
"ButtonLookup": "Online-Suche",
|
||||||
"ButtonManageTracks": "Tracks verwalten",
|
"ButtonManageTracks": "Tracks verwalten",
|
||||||
"ButtonMapChapterTitles": "Kapitelüberschriften zuordnen",
|
"ButtonMapChapterTitles": "Kapitelüberschriften zuordnen",
|
||||||
"ButtonMatchAllAuthors": "Online-Suche für alle Autoren",
|
"ButtonMatchAllAuthors": "Online Metadaten-Abgleich (alle Autoren)",
|
||||||
"ButtonMatchBooks": "Online-Suche für alle Hörbücher",
|
"ButtonMatchBooks": "Online Metadaten-Abgleich (alle Medien)",
|
||||||
"ButtonNevermind": "Vergiss es",
|
"ButtonNevermind": "Vergiss es",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Feed öffnen",
|
"ButtonOpenFeed": "Feed öffnen",
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
"ButtonSave": "Speichern",
|
"ButtonSave": "Speichern",
|
||||||
"ButtonSaveAndClose": "Speichern & Schließen",
|
"ButtonSaveAndClose": "Speichern & Schließen",
|
||||||
"ButtonSaveTracklist": "Speichere die Titelliste",
|
"ButtonSaveTracklist": "Speichere die Titelliste",
|
||||||
"ButtonScan": "Scan",
|
"ButtonScan": "Partial-Scan (nur geänderte/neue Medien)",
|
||||||
"ButtonScanLibrary": "Bibliothek scannen",
|
"ButtonScanLibrary": "Bibliothek scannen",
|
||||||
"ButtonSearch": "Suchen",
|
"ButtonSearch": "Suchen",
|
||||||
"ButtonSelectFolderPath": "Auswahl Ordnerpfad",
|
"ButtonSelectFolderPath": "Auswahl Ordnerpfad",
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
"ButtonUploadCover": "Titelbild hochladen",
|
"ButtonUploadCover": "Titelbild hochladen",
|
||||||
"ButtonUploadOPMLFile": "OPML-Datei hochladen",
|
"ButtonUploadOPMLFile": "OPML-Datei hochladen",
|
||||||
"ButtonUserDelete": "Benutzer {0} löschen",
|
"ButtonUserDelete": "Benutzer {0} löschen",
|
||||||
"ButtonUserEdit": "Benutzer {0} editieren",
|
"ButtonUserEdit": "Benutzer {0} bearbeiten",
|
||||||
"ButtonViewAll": "Alles anzeigen",
|
"ButtonViewAll": "Alles anzeigen",
|
||||||
"ButtonYes": "Ja",
|
"ButtonYes": "Ja",
|
||||||
"HeaderAccount": "Konto",
|
"HeaderAccount": "Konto",
|
||||||
@@ -111,7 +111,7 @@
|
|||||||
"HeaderManageGenres": "Kategorien verwalten",
|
"HeaderManageGenres": "Kategorien verwalten",
|
||||||
"HeaderManageTags": "Tags verwalten",
|
"HeaderManageTags": "Tags verwalten",
|
||||||
"HeaderMapDetails": "Stapelverarbeitung",
|
"HeaderMapDetails": "Stapelverarbeitung",
|
||||||
"HeaderMatch": "Online-Suche",
|
"HeaderMatch": "Metadaten",
|
||||||
"HeaderMetadataToEmbed": "Einzubettende Metadaten",
|
"HeaderMetadataToEmbed": "Einzubettende Metadaten",
|
||||||
"HeaderNewAccount": "Neues Konto",
|
"HeaderNewAccount": "Neues Konto",
|
||||||
"HeaderNewLibrary": "Neue Bibliothek",
|
"HeaderNewLibrary": "Neue Bibliothek",
|
||||||
@@ -308,6 +308,7 @@
|
|||||||
"LabelPublishYear": "Jahr",
|
"LabelPublishYear": "Jahr",
|
||||||
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
||||||
"LabelRecentSeries": "Aktuelle Serien",
|
"LabelRecentSeries": "Aktuelle Serien",
|
||||||
|
"LabelRecommended": "Recommended",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Veröffentlichungsdatum",
|
"LabelReleaseDate": "Veröffentlichungsdatum",
|
||||||
"LabelRemoveCover": "Lösche Titelbild",
|
"LabelRemoveCover": "Lösche Titelbild",
|
||||||
@@ -333,23 +334,23 @@
|
|||||||
"LabelSettingsExperimentalFeatures": "Experimentelle Funktionen",
|
"LabelSettingsExperimentalFeatures": "Experimentelle Funktionen",
|
||||||
"LabelSettingsExperimentalFeaturesHelp": "Funktionen welche sich in der Entwicklung befinden, benötigen Ihr Feedback und Ihre Hilfe beim Testen. Klicken Sie hier, um die Github-Diskussion zu öffnen.",
|
"LabelSettingsExperimentalFeaturesHelp": "Funktionen welche sich in der Entwicklung befinden, benötigen Ihr Feedback und Ihre Hilfe beim Testen. Klicken Sie hier, um die Github-Diskussion zu öffnen.",
|
||||||
"LabelSettingsFindCovers": "Suche Titelbilder",
|
"LabelSettingsFindCovers": "Suche Titelbilder",
|
||||||
"LabelSettingsFindCoversHelp": "Wenn Ihr Hörbuch kein eingebettetes Titelbild oder kein Titelbild im Ordner hat, versucht der Scanner, ein Titelbild online zu finden.<br>Hinweis: Dies verlängert die Scandauer",
|
"LabelSettingsFindCoversHelp": "Wenn Ihr Medium kein eingebettetes Titelbild oder kein Titelbild im Ordner hat, versucht der Scanner, ein Titelbild online zu finden.<br>Hinweis: Dies verlängert die Scandauer",
|
||||||
"LabelSettingsHomePageBookshelfView": "Starseite verwendet die Bücherregalansicht",
|
"LabelSettingsHomePageBookshelfView": "Starseite verwendet die Bücherregalansicht",
|
||||||
"LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht",
|
"LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht",
|
||||||
"LabelSettingsOverdriveMediaMarkers": "Verwende Overdrive Media Marker für Kapitel",
|
"LabelSettingsOverdriveMediaMarkers": "Verwende Overdrive Media Marker für Kapitel",
|
||||||
"LabelSettingsOverdriveMediaMarkersHelp": "MP3-Dateien von Overdrive werden mit eingebetteten Kapitel-Timings als benutzerdefinierte Metadaten geliefert. Wenn Sie dies aktivieren, werden diese Markierungen automatisch für die Kapiteltaktung verwendet",
|
"LabelSettingsOverdriveMediaMarkersHelp": "MP3-Dateien von Overdrive werden mit eingebetteten Kapitel-Timings als benutzerdefinierte Metadaten geliefert. Wenn Sie dies aktivieren, werden diese Markierungen automatisch für die Kapiteltaktung verwendet",
|
||||||
"LabelSettingsParseSubtitles": "Analysiere Untertitel",
|
"LabelSettingsParseSubtitles": "Analysiere Untertitel",
|
||||||
"LabelSettingsParseSubtitlesHelp": "Extrahiere den Untertitel von Hörbuchordnernamen.<br>Untertitel müssen vom eigentlichem Titel durch ein \" - \" getrennt sein. <br>Beispiel: \"Titel - Untertitel\"",
|
"LabelSettingsParseSubtitlesHelp": "Extrahiere den Untertitel von Medium-Ordnernamen.<br>Untertitel müssen vom eigentlichem Titel durch ein \" - \" getrennt sein. <br>Beispiel: \"Titel - Untertitel\"",
|
||||||
"LabelSettingsPreferAudioMetadata": "Bevorzuge lokale ID3-Audiometadaten",
|
"LabelSettingsPreferAudioMetadata": "Bevorzuge lokale ID3-Audiometadaten",
|
||||||
"LabelSettingsPreferAudioMetadataHelp": "In den Audiodateien eingebettete ID3 Metadaten werden für die Metadaten eines Hörbuchs anstelle der Ordnernamen verwendet. Wenn keine ID3 Metadaten zur Verfügung stehen, werden die Ordnernamen verwendet.",
|
"LabelSettingsPreferAudioMetadataHelp": "In den Audiodateien eingebettete ID3 Tags werden anstelle der Ordnernamen für die Bereitstellung der Metadaten verwendet. Wenn keine ID3 Tags zur Verfügung stehen, werden die Ordnernamen verwendet.",
|
||||||
"LabelSettingsPreferMatchedMetadata": "Bevorzuge online abgestimmte Metadaten",
|
"LabelSettingsPreferMatchedMetadata": "Bevorzuge online abgestimmte Metadaten",
|
||||||
"LabelSettingsPreferMatchedMetadataHelp": "Bei einem Schnellabgleich überschreiben neu abgestimmte online Metadaten alle schon vorhandenen Metadaten eines Hörbuchs. Standardmäßig werden bei einem Schnellabgleich nur fehlende Metadaten ersetzt.",
|
"LabelSettingsPreferMatchedMetadataHelp": "Bei einem Schnellabgleich überschreiben online neu abgestimmte Metadaten alle schon vorhandenen Metadaten eines Mediums. Standardmäßig werden bei einem Schnellabgleich nur fehlende Metadaten ersetzt.",
|
||||||
"LabelSettingsPreferOPFMetadata": "Bevorzuge OPF-Metadaten aus dem Hörbuchordner",
|
"LabelSettingsPreferOPFMetadata": "Bevorzuge OPF-Metadaten",
|
||||||
"LabelSettingsPreferOPFMetadataHelp": "In OPF-Dateien gespeicherte Metadaten werden anstelle der Ordnernamen für die Bereitstellung der Metadaten eines Hörbuchs verwendet. OPF-Datein sind seperate \"Textdateien\" mit der Endung \".abs\" welche in dem gleichen Ordner liegen wie das Hörbuch selber. In dieser sind verschiedene Matadaten (z.B. Titel, Autor, Jahr, Erzähler, Handlung, ISBN, ...) gespeichert. Wenn keine OPF Datei zur Verfügung steht, wird standardmäßig der Ordnername verwendet.",
|
"LabelSettingsPreferOPFMetadataHelp": "In OPF-Dateien gespeicherte Metadaten werden anstelle der Ordnernamen für die Bereitstellung der Metadaten verwendet. OPF-Datein sind seperate \"Textdateien\" mit der Endung \".abs\" welche in dem gleichen Ordner liegen wie das Medium selber. In dieser sind verschiedene Metadaten (z.B. Titel, Autor, Jahr, Erzähler, Handlung, ISBN, ...) gespeichert. Wenn keine OPF Datei zur Verfügung steht, wird der Ordnername verwendet.",
|
||||||
"LabelSettingsSkipMatchingBooksWithASIN": "Überspringe beim Online-Abgleich alle Bücher die bereits eine ASIN haben",
|
"LabelSettingsSkipMatchingBooksWithASIN": "Überspringe beim Online-Abgleich alle Bücher die bereits eine ASIN haben",
|
||||||
"LabelSettingsSkipMatchingBooksWithISBN": "Überspringe beim Online-Abgleich alle Bücher die bereits eine ISBN haben",
|
"LabelSettingsSkipMatchingBooksWithISBN": "Überspringe beim Online-Abgleich alle Bücher die bereits eine ISBN haben",
|
||||||
"LabelSettingsSortingIgnorePrefixes": "Vorwort/Artikel beim Sortieren ignorieren",
|
"LabelSettingsSortingIgnorePrefixes": "Vorwort/Artikel beim Sortieren ignorieren",
|
||||||
"LabelSettingsSortingIgnorePrefixesHelp": "Beispiel: für den Artikel \"der\" würde der Hörbuchtitel \"Der Buchtitel\" als \"Buchtitel, Der\" sortiert werden.",
|
"LabelSettingsSortingIgnorePrefixesHelp": "Beispiel: für den Artikel \"der\" würde der Mediumtitel \"Der Buchtitel\" als \"Buchtitel, Der\" sortiert werden.",
|
||||||
"LabelSettingsSquareBookCovers": "Benutze quadratische Titelbilder",
|
"LabelSettingsSquareBookCovers": "Benutze quadratische Titelbilder",
|
||||||
"LabelSettingsSquareBookCoversHelp": "Bevorzugen quadratische Titelbilder gegenüber den Standardtielbildern im Verhältnis 1,6:1",
|
"LabelSettingsSquareBookCoversHelp": "Bevorzugen quadratische Titelbilder gegenüber den Standardtielbildern im Verhältnis 1,6:1",
|
||||||
"LabelSettingsStoreCoversWithItem": "Titelbilder im Medienordner speichern",
|
"LabelSettingsStoreCoversWithItem": "Titelbilder im Medienordner speichern",
|
||||||
@@ -390,10 +391,10 @@
|
|||||||
"LabelTitle": "Titel",
|
"LabelTitle": "Titel",
|
||||||
"LabelToolsEmbedMetadata": "Metadaten einbetten",
|
"LabelToolsEmbedMetadata": "Metadaten einbetten",
|
||||||
"LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodatein ein.",
|
"LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodatein ein.",
|
||||||
"LabelToolsMakeM4b": "M4B-Hörbuchdatei erstellen",
|
"LabelToolsMakeM4b": "M4B-Datei erstellen",
|
||||||
"LabelToolsMakeM4bDescription": "Erstellt eine M4B-Hörbuchdatei mit eingebetteten Metadaten, Titelbild und Kapiteln.",
|
"LabelToolsMakeM4bDescription": "Erstellt eine M4B-Datei (Endung \".m4b\") welche mehrere mp3-Dateien in einer einzigen Datei inkl. derer Metadaten (Beschreibung, Titelbild, Kapitel, ....) zusammenfasst. M4B-Datei können darüber hinaus Lesezeichen speichern und mit einem Abspielschutz (Passwort) versehen werden.",
|
||||||
"LabelToolsSplitM4b": "M4B in MP3's aufteilen",
|
"LabelToolsSplitM4b": "M4B in MP3's aufteilen",
|
||||||
"LabelToolsSplitM4bDescription": "Erstellt aus einer mit Metadaten und nach Kapiteln aufgeteilten M4B-Hörbuchdastei seperate MP3's mit eingebetteten Metadaten, Coverbild und Kapiteln.",
|
"LabelToolsSplitM4bDescription": "Erstellt aus einer mit Metadaten und nach Kapiteln aufgeteilten M4B-Datei seperate MP3's mit eingebetteten Metadaten, Coverbild und Kapiteln.",
|
||||||
"LabelTotalDuration": "Gesamtdauer",
|
"LabelTotalDuration": "Gesamtdauer",
|
||||||
"LabelTotalTimeListened": "Gehörte Gesamtzeit",
|
"LabelTotalTimeListened": "Gehörte Gesamtzeit",
|
||||||
"LabelTrackFromFilename": "Titel von Dateiname",
|
"LabelTrackFromFilename": "Titel von Dateiname",
|
||||||
@@ -421,30 +422,30 @@
|
|||||||
"LabelViewQueue": "Spieler-Warteschlange anzeigen",
|
"LabelViewQueue": "Spieler-Warteschlange anzeigen",
|
||||||
"LabelVolume": "Volume",
|
"LabelVolume": "Volume",
|
||||||
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
||||||
"LabelYourAudiobookDuration": "Laufzeit Ihres Hörbuchs",
|
"LabelYourAudiobookDuration": "Laufzeit Ihres Mediums",
|
||||||
"LabelYourBookmarks": "Lesezeichen",
|
"LabelYourBookmarks": "Lesezeichen",
|
||||||
"LabelYourPlaylists": "Eigene Wiedergabelisten",
|
"LabelYourPlaylists": "Eigene Wiedergabelisten",
|
||||||
"LabelYourProgress": "Fortschritt",
|
"LabelYourProgress": "Fortschritt",
|
||||||
"MessageAddToPlayerQueue": "Zur Abspielwarteliste hinzufügen",
|
"MessageAddToPlayerQueue": "Zur Abspielwarteliste hinzufügen",
|
||||||
"MessageAppriseDescription": "Um diese Funktion nutzen zu können, müssen Sie eine Instanz von <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> laufen haben oder eine API verwenden welche dieselbe Anfragen bearbeiten kann. <br />Die Apprise API Url muss der vollständige URL-Pfad sein, an den die Benachrichtigung gesendet werden soll, z.B. wenn Ihre API-Instanz unter <code>http://192.168.1.1:8337</code> läuft, würden Sie <code>http://192.168.1.1:8337/notify</code> eingeben.",
|
"MessageAppriseDescription": "Um diese Funktion nutzen zu können, müssen Sie eine Instanz von <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> laufen haben oder eine API verwenden welche dieselbe Anfragen bearbeiten kann. <br />Die Apprise API Url muss der vollständige URL-Pfad sein, an den die Benachrichtigung gesendet werden soll, z.B. wenn Ihre API-Instanz unter <code>http://192.168.1.1:8337</code> läuft, würden Sie <code>http://192.168.1.1:8337/notify</code> eingeben.",
|
||||||
"MessageBackupsDescription": "In einer Sicherung werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder welche in <code>/metadata/items</code> & <code>/metadata/authors</code> gespeichert sind gespeichert. Sicherungen enthalten keine Dateien welche in den einzelnen Bibliotheksordnern (Hörbuch-/Podcastordnern) gespeichert sind.",
|
"MessageBackupsDescription": "In einer Sicherung werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder welche in <code>/metadata/items</code> & <code>/metadata/authors</code> gespeichert sind gespeichert. Sicherungen enthalten keine Dateien welche in den einzelnen Bibliotheksordnern (Medien-Ordnern) gespeichert sind.",
|
||||||
"MessageBatchQuickMatchDescription": "Der Schnellabgleich versucht, fehlende Titelbilder und Metadaten für die ausgewählten Artikel hinzuzufügen. Aktivieren Sie die nachstehenden Optionen, damit der Schnellabgleich vorhandene Titelbilder und/oder Metadaten überschreiben kann.",
|
"MessageBatchQuickMatchDescription": "Der Schnellabgleich versucht, fehlende Titelbilder und Metadaten für die ausgewählten Artikel hinzuzufügen. Aktivieren Sie die nachstehenden Optionen, damit der Schnellabgleich vorhandene Titelbilder und/oder Metadaten überschreiben kann.",
|
||||||
"MessageBookshelfNoCollections": "Es wurden noch keine Sammlungen erstellt",
|
"MessageBookshelfNoCollections": "Es wurden noch keine Sammlungen erstellt",
|
||||||
"MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für filter \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für filter \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoRSSFeeds": "Keine RSS-Feeds geöffnet",
|
"MessageBookshelfNoRSSFeeds": "Keine RSS-Feeds geöffnet",
|
||||||
"MessageBookshelfNoSeries": "Keine Serien vorhanden",
|
"MessageBookshelfNoSeries": "Keine Serien vorhanden",
|
||||||
"MessageChapterEndIsAfter": "Das Kapitelende liegt nach dem Ende Ihres Hörbuchs",
|
"MessageChapterEndIsAfter": "Ungültige Kapitelendzeit: Kapitelende > Mediumende (Kapitelende liegt nach dem Ende des Mediums)",
|
||||||
"MessageChapterErrorFirstNotZero": "Das erste Kapitel muss bei 0 beginnen",
|
"MessageChapterErrorFirstNotZero": "Ungültige Kapitelstartzeit: Das erste Kapitel muss bei 0 beginnen",
|
||||||
"MessageChapterErrorStartGteDuration": "Die ungültige Startzeit darf nicht größer als die gesamte Hörbuchdauer sein",
|
"MessageChapterErrorStartGteDuration": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumlänge (Kapitelanfang liegt zeitlich nach dem Ende des Mediums -> Lösung: Kapitelanfang < Mediumlänge)",
|
||||||
"MessageChapterErrorStartLtPrev": "Die ungültige Startzeit darf nicht größer oder gleich der Startzeit des vorherigen Kapitels sein",
|
"MessageChapterErrorStartLtPrev": "Ungültige Kapitelstartzeit: Kapitelanfang < Kapitelanfang vorheriges Kapitel (Kapitelanfang liegt zeitlich vor dem Beginn des vorherigen Kapitels -> Lösung: Kapitelanfang >= Startzeit des vorherigen Kapitels)",
|
||||||
"MessageChapterStartIsAfter": "Der Kapitelanfang liegt nach dem Ende Ihres Hörbuchs",
|
"MessageChapterStartIsAfter": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumende (Kapitelanfang liegt nach dem Ende des Mediums)",
|
||||||
"MessageCheckingCron": "Überprüfe cron...",
|
"MessageCheckingCron": "Überprüfe cron...",
|
||||||
"MessageConfirmDeleteBackup": "Sind Sie sicher, dass Sie die Sicherung für {0} löschen wollen?",
|
"MessageConfirmDeleteBackup": "Sind Sie sicher, dass Sie die Sicherung für {0} löschen wollen?",
|
||||||
"MessageConfirmDeleteLibrary": "Sind Sie sicher, dass Sie die Bibliothek \"{0}\" dauerhaft löschen wollen?",
|
"MessageConfirmDeleteLibrary": "Sind Sie sicher, dass Sie die Bibliothek \"{0}\" dauerhaft löschen wollen?",
|
||||||
"MessageConfirmDeleteSession": "Sind Sie sicher, dass Sie diese Sitzung löschen möchten?",
|
"MessageConfirmDeleteSession": "Sind Sie sicher, dass Sie diese Sitzung löschen möchten?",
|
||||||
"MessageConfirmForceReScan": "Sind Sie sicher, dass Sie einen erneuten Scanvorgang erzwingen wollen?",
|
"MessageConfirmForceReScan": "Sind Sie sicher, dass Sie einen erneuten Scanvorgang erzwingen wollen?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
"MessageConfirmMarkSeriesFinished": "Sind Sie sicher, dass Sie alle Medien dieser Reihe als abgeschlossen markieren wollen?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
"MessageConfirmMarkSeriesNotFinished": "Sind Sie sicher, dass Sie alle Medien dieser Reihe als nicht abgeschlossen markieren wollen?",
|
||||||
"MessageConfirmRemoveCollection": "Sind Sie sicher, dass Sie die Sammlung \"{0}\" löschen wollen?",
|
"MessageConfirmRemoveCollection": "Sind Sie sicher, dass Sie die Sammlung \"{0}\" löschen wollen?",
|
||||||
"MessageConfirmRemoveEpisode": "Sind Sie sicher, dass Sie die Episode \"{0}\" löschen möchten?",
|
"MessageConfirmRemoveEpisode": "Sind Sie sicher, dass Sie die Episode \"{0}\" löschen möchten?",
|
||||||
"MessageConfirmRemoveEpisodes": "Sind Sie sicher, dass Sie {0} Episoden löschen wollen?",
|
"MessageConfirmRemoveEpisodes": "Sind Sie sicher, dass Sie {0} Episoden löschen wollen?",
|
||||||
@@ -472,7 +473,7 @@
|
|||||||
"MessageLoadingFolders": "Lade Ordner...",
|
"MessageLoadingFolders": "Lade Ordner...",
|
||||||
"MessageM4BFailed": "M4B fehlgeschlagen!",
|
"MessageM4BFailed": "M4B fehlgeschlagen!",
|
||||||
"MessageM4BFinished": "M4B beendet!",
|
"MessageM4BFinished": "M4B beendet!",
|
||||||
"MessageMapChapterTitles": "Zuordnen von Kapiteltiteln zu Ihren vorhandenen Hörbuchkapiteln ohne Anpassung der Zeitangaben",
|
"MessageMapChapterTitles": "Zuordnen von Kapiteltiteln zu Ihren vorhandenen Medienkapiteln ohne Anpassung der Zeitangaben",
|
||||||
"MessageMarkAsFinished": "Als beendet markieren",
|
"MessageMarkAsFinished": "Als beendet markieren",
|
||||||
"MessageMarkAsNotFinished": "Als nicht abgeschlossen markieren",
|
"MessageMarkAsNotFinished": "Als nicht abgeschlossen markieren",
|
||||||
"MessageMatchBooksDescription": "versucht, Bücher in der Bibliothek mit einem Buch des ausgewählten Suchanbieters abzugleichen und leere Details und das Titelbild auszufüllen. Details werden nicht überschrieben.",
|
"MessageMatchBooksDescription": "versucht, Bücher in der Bibliothek mit einem Buch des ausgewählten Suchanbieters abzugleichen und leere Details und das Titelbild auszufüllen. Details werden nicht überschrieben.",
|
||||||
@@ -530,16 +531,16 @@
|
|||||||
"MessageValidCronExpression": "Gültiger cron-ausdruck",
|
"MessageValidCronExpression": "Gültiger cron-ausdruck",
|
||||||
"MessageWatcherIsDisabledGlobally": "Überwachung ist in den Servereinstellungen global deaktiviert",
|
"MessageWatcherIsDisabledGlobally": "Überwachung ist in den Servereinstellungen global deaktiviert",
|
||||||
"MessageXLibraryIsEmpty": "{0} Bibliothek ist leer!",
|
"MessageXLibraryIsEmpty": "{0} Bibliothek ist leer!",
|
||||||
"MessageYourAudiobookDurationIsLonger": "Die Dauer Ihres Hörbuchs ist länger als die gefundene Dauer",
|
"MessageYourAudiobookDurationIsLonger": "Die Dauer Ihres Mediums ist länger als die gefundene Dauer",
|
||||||
"MessageYourAudiobookDurationIsShorter": "Die Dauer Ihres Hörbuchs ist kürzer als die gefundene Dauer",
|
"MessageYourAudiobookDurationIsShorter": "Die Dauer Ihres Mediums ist kürzer als die gefundene Dauer",
|
||||||
"NoteChangeRootPassword": "Der Root-Benutzer (Hauptbenutzer) ist der einzige Benutzer, der ein leeres Passwort haben kann",
|
"NoteChangeRootPassword": "Der Root-Benutzer (Hauptbenutzer) ist der einzige Benutzer, der ein leeres Passwort haben kann",
|
||||||
"NoteChapterEditorTimes": "Hinweis: Die Anfangszeit des ersten Kapitels muss bei 0:00 beginnen und die Anfangszeit des letzten Kapitels darf die Dauer des Hörbuchs nicht überschreiten.",
|
"NoteChapterEditorTimes": "Hinweis: Die Anfangszeit des ersten Kapitels muss bei 0:00 beginnen und die Anfangszeit des letzten Kapitels darf die Dauer des Mediums nicht überschreiten.",
|
||||||
"NoteFolderPicker": "Hinweis: Bereits zugeordnete Ordner werden nicht angezeigt.",
|
"NoteFolderPicker": "Hinweis: Bereits zugeordnete Ordner werden nicht angezeigt.",
|
||||||
"NoteFolderPickerDebian": "Hinweis: Der Ordnerauswahldialog für die Debian-Installation ist nicht vollständig implementiert. Sie sollten den Pfad zu Ihrer Bibliothek direkt eingeben.",
|
"NoteFolderPickerDebian": "Hinweis: Der Ordnerauswahldialog für die Debian-Installation ist nicht vollständig implementiert. Sie sollten den Pfad zu Ihrer Bibliothek direkt eingeben.",
|
||||||
"NoteRSSFeedPodcastAppsHttps": "Warnung: Die meisten Podcast-Apps verlangen, dass die URL des RSS-Feeds HTTPS verwendet.",
|
"NoteRSSFeedPodcastAppsHttps": "Warnung: Die meisten Podcast-Apps verlangen, dass die URL des RSS-Feeds HTTPS verwendet.",
|
||||||
"NoteRSSFeedPodcastAppsPubDate": "Warnung: 1 oder mehrere Ihrer Episoden haben kein Veröffentlichungsdatum. Einige Podcast-Apps verlangen dies.",
|
"NoteRSSFeedPodcastAppsPubDate": "Warnung: 1 oder mehrere Ihrer Episoden haben kein Veröffentlichungsdatum. Einige Podcast-Apps verlangen dies.",
|
||||||
"NoteUploaderFoldersWithMediaFiles": "Ordner mit Mediendateien werden als separate Bibliothekselemente behandelt.",
|
"NoteUploaderFoldersWithMediaFiles": "Ordner mit Mediendateien werden als separate Bibliothekselemente behandelt.",
|
||||||
"NoteUploaderOnlyAudioFiles": "Wenn Sie nur Audiodateien hochladen, wird jede Audiodatei als ein separates Hörbuch behandelt.",
|
"NoteUploaderOnlyAudioFiles": "Wenn Sie nur Audiodateien hochladen, wird jede Audiodatei als ein separates Medium behandelt.",
|
||||||
"NoteUploaderUnsupportedFiles": "Nicht unterstützte Dateien werden ignoriert. Bei der Auswahl oder dem Löschen eines Ordners werden andere Dateien, die sich nicht in einem Elementordner befinden, ignoriert.",
|
"NoteUploaderUnsupportedFiles": "Nicht unterstützte Dateien werden ignoriert. Bei der Auswahl oder dem Löschen eines Ordners werden andere Dateien, die sich nicht in einem Elementordner befinden, ignoriert.",
|
||||||
"PlaceholderNewCollection": "Neuer Sammlungsname",
|
"PlaceholderNewCollection": "Neuer Sammlungsname",
|
||||||
"PlaceholderNewFolderPath": "Neuer Ordnerpfad",
|
"PlaceholderNewFolderPath": "Neuer Ordnerpfad",
|
||||||
@@ -605,20 +606,13 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "Medium aus der Sammlung gelöscht",
|
"ToastRemoveItemFromCollectionSuccess": "Medium aus der Sammlung gelöscht",
|
||||||
"ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden",
|
"ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen",
|
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen",
|
||||||
"ToastSeriesUpdateFailed": "Series update failed",
|
"ToastSeriesUpdateFailed": "Aktualisierung der Serien fehlgeschlagen",
|
||||||
"ToastSeriesUpdateSuccess": "Series update success",
|
"ToastSeriesUpdateSuccess": "Serien aktualisiert",
|
||||||
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden",
|
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden",
|
||||||
"ToastSessionDeleteSuccess": "Sitzung gelöscht",
|
"ToastSessionDeleteSuccess": "Sitzung gelöscht",
|
||||||
"ToastSocketConnected": "Verbindung zum WebSocket hergestellt",
|
"ToastSocketConnected": "Verbindung zum WebSocket hergestellt",
|
||||||
"ToastSocketDisconnected": "Verbindung zum WebSocket verloren",
|
"ToastSocketDisconnected": "Verbindung zum WebSocket verloren",
|
||||||
"ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen",
|
"ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen",
|
||||||
"ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden",
|
"ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden",
|
||||||
"ToastUserDeleteSuccess": "Benutzer gelöscht",
|
"ToastUserDeleteSuccess": "Benutzer gelöscht"
|
||||||
"WeekdayFriday": "Freitag",
|
|
||||||
"WeekdayMonday": "Montag",
|
|
||||||
"WeekdaySaturday": "Samstag",
|
|
||||||
"WeekdaySunday": "Sonntag",
|
|
||||||
"WeekdayThursday": "Donnerstag",
|
|
||||||
"WeekdayTuesday": "Dienstag",
|
|
||||||
"WeekdayWednesday": "Mittwoch"
|
|
||||||
}
|
}
|
||||||
@@ -308,6 +308,7 @@
|
|||||||
"LabelPublishYear": "Publish Year",
|
"LabelPublishYear": "Publish Year",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
|
"LabelRecommended": "Recommended",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Release Date",
|
"LabelReleaseDate": "Release Date",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Remove cover",
|
||||||
@@ -613,12 +614,5 @@
|
|||||||
"ToastSocketDisconnected": "Socket disconnected",
|
"ToastSocketDisconnected": "Socket disconnected",
|
||||||
"ToastSocketFailedToConnect": "Socket failed to connect",
|
"ToastSocketFailedToConnect": "Socket failed to connect",
|
||||||
"ToastUserDeleteFailed": "Failed to delete user",
|
"ToastUserDeleteFailed": "Failed to delete user",
|
||||||
"ToastUserDeleteSuccess": "User deleted",
|
"ToastUserDeleteSuccess": "User deleted"
|
||||||
"WeekdayFriday": "Friday",
|
|
||||||
"WeekdayMonday": "Monday",
|
|
||||||
"WeekdaySaturday": "Saturday",
|
|
||||||
"WeekdaySunday": "Sunday",
|
|
||||||
"WeekdayThursday": "Thursday",
|
|
||||||
"WeekdayTuesday": "Tuesday",
|
|
||||||
"WeekdayWednesday": "Wednesday"
|
|
||||||
}
|
}
|
||||||
@@ -308,6 +308,7 @@
|
|||||||
"LabelPublishYear": "Publish Year",
|
"LabelPublishYear": "Publish Year",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
|
"LabelRecommended": "Recommended",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Release Date",
|
"LabelReleaseDate": "Release Date",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Remove cover",
|
||||||
@@ -613,12 +614,5 @@
|
|||||||
"ToastSocketDisconnected": "Socket disconnected",
|
"ToastSocketDisconnected": "Socket disconnected",
|
||||||
"ToastSocketFailedToConnect": "Socket failed to connect",
|
"ToastSocketFailedToConnect": "Socket failed to connect",
|
||||||
"ToastUserDeleteFailed": "Failed to delete user",
|
"ToastUserDeleteFailed": "Failed to delete user",
|
||||||
"ToastUserDeleteSuccess": "User deleted",
|
"ToastUserDeleteSuccess": "User deleted"
|
||||||
"WeekdayFriday": "Friday",
|
|
||||||
"WeekdayMonday": "Monday",
|
|
||||||
"WeekdaySaturday": "Saturday",
|
|
||||||
"WeekdaySunday": "Sunday",
|
|
||||||
"WeekdayThursday": "Thursday",
|
|
||||||
"WeekdayTuesday": "Tuesday",
|
|
||||||
"WeekdayWednesday": "Wednesday"
|
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"ButtonCreate": "Créer",
|
"ButtonCreate": "Créer",
|
||||||
"ButtonCreateBackup": "Créer une Sauvegarde",
|
"ButtonCreateBackup": "Créer une Sauvegarde",
|
||||||
"ButtonDelete": "Effacer",
|
"ButtonDelete": "Effacer",
|
||||||
"ButtonEdit": "Edit",
|
"ButtonEdit": "Editer",
|
||||||
"ButtonEditChapters": "Editer Chapitre",
|
"ButtonEditChapters": "Editer Chapitre",
|
||||||
"ButtonEditPodcast": "Editer Podcast",
|
"ButtonEditPodcast": "Editer Podcast",
|
||||||
"ButtonForceReScan": "Forcer un Re-Scan",
|
"ButtonForceReScan": "Forcer un Re-Scan",
|
||||||
@@ -308,6 +308,7 @@
|
|||||||
"LabelPublishYear": "Année d'Edition",
|
"LabelPublishYear": "Année d'Edition",
|
||||||
"LabelRecentlyAdded": "Derniers Ajouts",
|
"LabelRecentlyAdded": "Derniers Ajouts",
|
||||||
"LabelRecentSeries": "Séries Récentes",
|
"LabelRecentSeries": "Séries Récentes",
|
||||||
|
"LabelRecommended": "Recommended",
|
||||||
"LabelRegion": "Région",
|
"LabelRegion": "Région",
|
||||||
"LabelReleaseDate": "Date de Parution",
|
"LabelReleaseDate": "Date de Parution",
|
||||||
"LabelRemoveCover": "Supprimer la Couverture",
|
"LabelRemoveCover": "Supprimer la Couverture",
|
||||||
@@ -613,12 +614,5 @@
|
|||||||
"ToastSocketDisconnected": "WebSocket déconnecté",
|
"ToastSocketDisconnected": "WebSocket déconnecté",
|
||||||
"ToastSocketFailedToConnect": "Échec de la connexion WebSocket",
|
"ToastSocketFailedToConnect": "Échec de la connexion WebSocket",
|
||||||
"ToastUserDeleteFailed": "Échec de la suppression de l'utilisateur",
|
"ToastUserDeleteFailed": "Échec de la suppression de l'utilisateur",
|
||||||
"ToastUserDeleteSuccess": "Utilisateur supprimé",
|
"ToastUserDeleteSuccess": "Utilisateur supprimé"
|
||||||
"WeekdayFriday": "Vendredi",
|
|
||||||
"WeekdayMonday": "Lundi",
|
|
||||||
"WeekdaySaturday": "Samedi",
|
|
||||||
"WeekdaySunday": "Dimanche",
|
|
||||||
"WeekdayThursday": "Jeudi",
|
|
||||||
"WeekdayTuesday": "Mardi",
|
|
||||||
"WeekdayWednesday": "Mercredi"
|
|
||||||
}
|
}
|
||||||
@@ -308,6 +308,7 @@
|
|||||||
"LabelPublishYear": "Godina izdavanja",
|
"LabelPublishYear": "Godina izdavanja",
|
||||||
"LabelRecentlyAdded": "Nedavno dodano",
|
"LabelRecentlyAdded": "Nedavno dodano",
|
||||||
"LabelRecentSeries": "Nedavne serije",
|
"LabelRecentSeries": "Nedavne serije",
|
||||||
|
"LabelRecommended": "Recommended",
|
||||||
"LabelRegion": "Regija",
|
"LabelRegion": "Regija",
|
||||||
"LabelReleaseDate": "Datum izlaska",
|
"LabelReleaseDate": "Datum izlaska",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Remove cover",
|
||||||
@@ -613,12 +614,5 @@
|
|||||||
"ToastSocketDisconnected": "Socket disconnected",
|
"ToastSocketDisconnected": "Socket disconnected",
|
||||||
"ToastSocketFailedToConnect": "Socket failed to connect",
|
"ToastSocketFailedToConnect": "Socket failed to connect",
|
||||||
"ToastUserDeleteFailed": "Neuspješno brisanje korisnika",
|
"ToastUserDeleteFailed": "Neuspješno brisanje korisnika",
|
||||||
"ToastUserDeleteSuccess": "Korisnik obrisan",
|
"ToastUserDeleteSuccess": "Korisnik obrisan"
|
||||||
"WeekdayFriday": "Petak",
|
|
||||||
"WeekdayMonday": "Ponedjeljak",
|
|
||||||
"WeekdaySaturday": "Subota",
|
|
||||||
"WeekdaySunday": "Nedjelja",
|
|
||||||
"WeekdayThursday": "Četvrtak",
|
|
||||||
"WeekdayTuesday": "Utorak",
|
|
||||||
"WeekdayWednesday": "Srijeda"
|
|
||||||
}
|
}
|
||||||
+13
-19
@@ -76,8 +76,8 @@
|
|||||||
"ButtonUploadBackup": "Carica Backup",
|
"ButtonUploadBackup": "Carica Backup",
|
||||||
"ButtonUploadCover": "Carica Cover",
|
"ButtonUploadCover": "Carica Cover",
|
||||||
"ButtonUploadOPMLFile": "Carica File OPML",
|
"ButtonUploadOPMLFile": "Carica File OPML",
|
||||||
"ButtonUserDelete": "Delete user {0}",
|
"ButtonUserDelete": "Cancella Utente {0}",
|
||||||
"ButtonUserEdit": "Edit user {0}",
|
"ButtonUserEdit": "Modifica Utente {0}",
|
||||||
"ButtonViewAll": "Mostra Tutto",
|
"ButtonViewAll": "Mostra Tutto",
|
||||||
"ButtonYes": "Si",
|
"ButtonYes": "Si",
|
||||||
"HeaderAccount": "Account",
|
"HeaderAccount": "Account",
|
||||||
@@ -244,7 +244,7 @@
|
|||||||
"LabelLastSeen": "Ultimi Visti",
|
"LabelLastSeen": "Ultimi Visti",
|
||||||
"LabelLastTime": "Ultima Volta",
|
"LabelLastTime": "Ultima Volta",
|
||||||
"LabelLastUpdate": "Ultimo Aggiornamento",
|
"LabelLastUpdate": "Ultimo Aggiornamento",
|
||||||
"LabelLess": "Meno",
|
"LabelLess": "Poco",
|
||||||
"LabelLibrariesAccessibleToUser": "Librerie Accessibili agli Utenti",
|
"LabelLibrariesAccessibleToUser": "Librerie Accessibili agli Utenti",
|
||||||
"LabelLibrary": "Libreria",
|
"LabelLibrary": "Libreria",
|
||||||
"LabelLibraryItem": "Elementi della Library",
|
"LabelLibraryItem": "Elementi della Library",
|
||||||
@@ -262,7 +262,7 @@
|
|||||||
"LabelMinute": "Minuto",
|
"LabelMinute": "Minuto",
|
||||||
"LabelMissing": "Altro",
|
"LabelMissing": "Altro",
|
||||||
"LabelMissingParts": "Parti rimantenti",
|
"LabelMissingParts": "Parti rimantenti",
|
||||||
"LabelMore": "Espandi",
|
"LabelMore": "Molto",
|
||||||
"LabelName": "Nome",
|
"LabelName": "Nome",
|
||||||
"LabelNarrator": "Narratore",
|
"LabelNarrator": "Narratore",
|
||||||
"LabelNarrators": "Narratori",
|
"LabelNarrators": "Narratori",
|
||||||
@@ -308,6 +308,7 @@
|
|||||||
"LabelPublishYear": "Anno Pubblicazione",
|
"LabelPublishYear": "Anno Pubblicazione",
|
||||||
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
||||||
"LabelRecentSeries": "Serie Recenti",
|
"LabelRecentSeries": "Serie Recenti",
|
||||||
|
"LabelRecommended": "Recommended",
|
||||||
"LabelRegion": "Regione",
|
"LabelRegion": "Regione",
|
||||||
"LabelReleaseDate": "Data Release",
|
"LabelReleaseDate": "Data Release",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Remove cover",
|
||||||
@@ -430,7 +431,7 @@
|
|||||||
"MessageBackupsDescription": "I backup includono utenti, progressi degli utenti, dettagli sugli elementi della libreria, impostazioni del server e immagini archiviate in <code>/metadata/items</code> & <code>/metadata/authors</code>. I backup non includono i file archiviati nelle cartelle della libreria.",
|
"MessageBackupsDescription": "I backup includono utenti, progressi degli utenti, dettagli sugli elementi della libreria, impostazioni del server e immagini archiviate in <code>/metadata/items</code> & <code>/metadata/authors</code>. I backup non includono i file archiviati nelle cartelle della libreria.",
|
||||||
"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.",
|
"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 ",
|
"MessageBookshelfNoCollections": "Non hai ancora creato nessuna raccolta ",
|
||||||
"MessageBookshelfNoResultsForFilter": "Nessul risultato per il filtro \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Nessun risultato per il filtro \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoRSSFeeds": "Nessun RSS feeds aperto",
|
"MessageBookshelfNoRSSFeeds": "Nessun RSS feeds aperto",
|
||||||
"MessageBookshelfNoSeries": "Non c'è nessuna Serie",
|
"MessageBookshelfNoSeries": "Non c'è nessuna Serie",
|
||||||
"MessageChapterEndIsAfter": "La fine del capitolo è dopo la fine del tuo audiolibro",
|
"MessageChapterEndIsAfter": "La fine del capitolo è dopo la fine del tuo audiolibro",
|
||||||
@@ -443,8 +444,8 @@
|
|||||||
"MessageConfirmDeleteLibrary": "Sei sicuro di voler eliminare definitivamente la libreria \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Sei sicuro di voler eliminare definitivamente la libreria \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "Sei sicuro di voler eliminare questa sessione?",
|
"MessageConfirmDeleteSession": "Sei sicuro di voler eliminare questa sessione?",
|
||||||
"MessageConfirmForceReScan": "Sei sicuro di voler forzare una nuova scansione?",
|
"MessageConfirmForceReScan": "Sei sicuro di voler forzare una nuova scansione?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
"MessageConfirmMarkSeriesFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come completati?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
"MessageConfirmMarkSeriesNotFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come non completati?",
|
||||||
"MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "Sei sicuro di voler rimuovere l'episodio \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Sei sicuro di voler rimuovere l'episodio \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "Sei sicuro di voler rimuovere {0} episodi?",
|
"MessageConfirmRemoveEpisodes": "Sei sicuro di voler rimuovere {0} episodi?",
|
||||||
@@ -453,7 +454,7 @@
|
|||||||
"MessageConfirmRenameGenreMergeNote": "Note: Questo genere esiste già quindi verra unito.",
|
"MessageConfirmRenameGenreMergeNote": "Note: Questo genere esiste già quindi verra unito.",
|
||||||
"MessageConfirmRenameGenreWarning": "Avvertimento! Esiste già un genere simile con un nome simile \"{0}\".",
|
"MessageConfirmRenameGenreWarning": "Avvertimento! Esiste già un genere simile con un nome simile \"{0}\".",
|
||||||
"MessageConfirmRenameTag": "Sei sicuro di voler rinominare il tag \"{0}\" in \"{1}\" per tutti gli oggetti?",
|
"MessageConfirmRenameTag": "Sei sicuro di voler rinominare il tag \"{0}\" in \"{1}\" per tutti gli oggetti?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Note: Questo tag esiste già e verrà unito nel vecchio.",
|
"MessageConfirmRenameTagMergeNote": "Nota: Questo tag esiste già e verrà unito nel vecchio.",
|
||||||
"MessageConfirmRenameTagWarning": "Avvertimento! Esiste già un tag simile con un nome simile \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Avvertimento! Esiste già un tag simile con un nome simile \"{0}\".",
|
||||||
"MessageDownloadingEpisode": "Download episodio in corso",
|
"MessageDownloadingEpisode": "Download episodio in corso",
|
||||||
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
|
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
|
||||||
@@ -600,25 +601,18 @@
|
|||||||
"ToastPlaylistUpdateFailed": "Aggiornamento Playlist Fallita",
|
"ToastPlaylistUpdateFailed": "Aggiornamento Playlist Fallita",
|
||||||
"ToastPlaylistUpdateSuccess": "Playlist Aggiornata",
|
"ToastPlaylistUpdateSuccess": "Playlist Aggiornata",
|
||||||
"ToastPodcastCreateFailed": "Errore Creazione podcast",
|
"ToastPodcastCreateFailed": "Errore Creazione podcast",
|
||||||
"ToastPodcastCreateSuccess": "Podcast creato Correttamwnte",
|
"ToastPodcastCreateSuccess": "Podcast creato Correttamente",
|
||||||
"ToastRemoveItemFromCollectionFailed": "Errore rimozione file dalla Raccolta",
|
"ToastRemoveItemFromCollectionFailed": "Errore rimozione file dalla Raccolta",
|
||||||
"ToastRemoveItemFromCollectionSuccess": "Oggetto rimosso dalla Raccolta",
|
"ToastRemoveItemFromCollectionSuccess": "Oggetto rimosso dalla Raccolta",
|
||||||
"ToastRSSFeedCloseFailed": "Errore chiusura RSS feed",
|
"ToastRSSFeedCloseFailed": "Errore chiusura RSS feed",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS feed chiuso",
|
"ToastRSSFeedCloseSuccess": "RSS feed chiuso",
|
||||||
"ToastSeriesUpdateFailed": "Series update failed",
|
"ToastSeriesUpdateFailed": "Aggiornaemnto Serie Fallito",
|
||||||
"ToastSeriesUpdateSuccess": "Series update success",
|
"ToastSeriesUpdateSuccess": "Serie Aggornate",
|
||||||
"ToastSessionDeleteFailed": "Errore eliminazione sessione",
|
"ToastSessionDeleteFailed": "Errore eliminazione sessione",
|
||||||
"ToastSessionDeleteSuccess": "Sessione cancellata",
|
"ToastSessionDeleteSuccess": "Sessione cancellata",
|
||||||
"ToastSocketConnected": "Socket connesso",
|
"ToastSocketConnected": "Socket connesso",
|
||||||
"ToastSocketDisconnected": "Socket disconnesso",
|
"ToastSocketDisconnected": "Socket disconnesso",
|
||||||
"ToastSocketFailedToConnect": "Socket non riesce a connettersi",
|
"ToastSocketFailedToConnect": "Socket non riesce a connettersi",
|
||||||
"ToastUserDeleteFailed": "Errore eliminazione utente",
|
"ToastUserDeleteFailed": "Errore eliminazione utente",
|
||||||
"ToastUserDeleteSuccess": "Utente eliminato",
|
"ToastUserDeleteSuccess": "Utente eliminato"
|
||||||
"WeekdayFriday": "Venerdì",
|
|
||||||
"WeekdayMonday": "Lunedì",
|
|
||||||
"WeekdaySaturday": "Sabato",
|
|
||||||
"WeekdaySunday": "Domenica",
|
|
||||||
"WeekdayThursday": "Giovedi",
|
|
||||||
"WeekdayTuesday": "Martedì",
|
|
||||||
"WeekdayWednesday": "Mercoledì"
|
|
||||||
}
|
}
|
||||||
@@ -308,6 +308,7 @@
|
|||||||
"LabelPublishYear": "Rok publikacji",
|
"LabelPublishYear": "Rok publikacji",
|
||||||
"LabelRecentlyAdded": "Niedawno dodany",
|
"LabelRecentlyAdded": "Niedawno dodany",
|
||||||
"LabelRecentSeries": "Ostatnie serie",
|
"LabelRecentSeries": "Ostatnie serie",
|
||||||
|
"LabelRecommended": "Recommended",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Data wydania",
|
"LabelReleaseDate": "Data wydania",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Remove cover",
|
||||||
@@ -613,12 +614,5 @@
|
|||||||
"ToastSocketDisconnected": "Połączenie z serwerem zostało zamknięte",
|
"ToastSocketDisconnected": "Połączenie z serwerem zostało zamknięte",
|
||||||
"ToastSocketFailedToConnect": "Poączenie z serwerem nie powiodło się",
|
"ToastSocketFailedToConnect": "Poączenie z serwerem nie powiodło się",
|
||||||
"ToastUserDeleteFailed": "Nie udało się usunąć użytkownika",
|
"ToastUserDeleteFailed": "Nie udało się usunąć użytkownika",
|
||||||
"ToastUserDeleteSuccess": "Użytkownik usunięty",
|
"ToastUserDeleteSuccess": "Użytkownik usunięty"
|
||||||
"WeekdayFriday": "Piątek",
|
|
||||||
"WeekdayMonday": "Poniedziałek",
|
|
||||||
"WeekdaySaturday": "Sobota",
|
|
||||||
"WeekdaySunday": "Niedziela",
|
|
||||||
"WeekdayThursday": "Czwartek",
|
|
||||||
"WeekdayTuesday": "Wtorek",
|
|
||||||
"WeekdayWednesday": "Środa"
|
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"ButtonCreate": "创建",
|
"ButtonCreate": "创建",
|
||||||
"ButtonCreateBackup": "创建备份",
|
"ButtonCreateBackup": "创建备份",
|
||||||
"ButtonDelete": "删除",
|
"ButtonDelete": "删除",
|
||||||
"ButtonEdit": "Edit",
|
"ButtonEdit": "编辑",
|
||||||
"ButtonEditChapters": "编辑章节",
|
"ButtonEditChapters": "编辑章节",
|
||||||
"ButtonEditPodcast": "编辑播客",
|
"ButtonEditPodcast": "编辑播客",
|
||||||
"ButtonForceReScan": "强制重新扫描",
|
"ButtonForceReScan": "强制重新扫描",
|
||||||
@@ -76,8 +76,8 @@
|
|||||||
"ButtonUploadBackup": "上传备份",
|
"ButtonUploadBackup": "上传备份",
|
||||||
"ButtonUploadCover": "上传封面",
|
"ButtonUploadCover": "上传封面",
|
||||||
"ButtonUploadOPMLFile": "上传 OPML 文件",
|
"ButtonUploadOPMLFile": "上传 OPML 文件",
|
||||||
"ButtonUserDelete": "Delete user {0}",
|
"ButtonUserDelete": "删除用户 {0}",
|
||||||
"ButtonUserEdit": "Edit user {0}",
|
"ButtonUserEdit": "编辑用户 {0}",
|
||||||
"ButtonViewAll": "查看全部",
|
"ButtonViewAll": "查看全部",
|
||||||
"ButtonYes": "确定",
|
"ButtonYes": "确定",
|
||||||
"HeaderAccount": "帐户",
|
"HeaderAccount": "帐户",
|
||||||
@@ -308,6 +308,7 @@
|
|||||||
"LabelPublishYear": "发布年份",
|
"LabelPublishYear": "发布年份",
|
||||||
"LabelRecentlyAdded": "最近添加",
|
"LabelRecentlyAdded": "最近添加",
|
||||||
"LabelRecentSeries": "最近添加系列",
|
"LabelRecentSeries": "最近添加系列",
|
||||||
|
"LabelRecommended": "推荐内容",
|
||||||
"LabelRegion": "区域",
|
"LabelRegion": "区域",
|
||||||
"LabelReleaseDate": "发布日期",
|
"LabelReleaseDate": "发布日期",
|
||||||
"LabelRemoveCover": "移除封面",
|
"LabelRemoveCover": "移除封面",
|
||||||
@@ -443,8 +444,8 @@
|
|||||||
"MessageConfirmDeleteLibrary": "你确定要永久删除媒体库 \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "你确定要永久删除媒体库 \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "你确定要删除此会话吗?",
|
"MessageConfirmDeleteSession": "你确定要删除此会话吗?",
|
||||||
"MessageConfirmForceReScan": "你确定要强制重新扫描吗?",
|
"MessageConfirmForceReScan": "你确定要强制重新扫描吗?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
"MessageConfirmMarkSeriesFinished": "你确定要将此系列中的所有书籍都标记为已听完吗?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
"MessageConfirmMarkSeriesNotFinished": "你确定要将此系列中的所有书籍都标记为未听完吗?",
|
||||||
"MessageConfirmRemoveCollection": "您确定要移除收藏 \"{0}\"?",
|
"MessageConfirmRemoveCollection": "您确定要移除收藏 \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "您确定要移除剧集 \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "您确定要移除剧集 \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "你确定要移除 {0} 剧集?",
|
"MessageConfirmRemoveEpisodes": "你确定要移除 {0} 剧集?",
|
||||||
@@ -605,20 +606,13 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "项目已从收藏中删除",
|
"ToastRemoveItemFromCollectionSuccess": "项目已从收藏中删除",
|
||||||
"ToastRSSFeedCloseFailed": "关闭 RSS 源失败",
|
"ToastRSSFeedCloseFailed": "关闭 RSS 源失败",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS 源已关闭",
|
"ToastRSSFeedCloseSuccess": "RSS 源已关闭",
|
||||||
"ToastSeriesUpdateFailed": "Series update failed",
|
"ToastSeriesUpdateFailed": "更新系列失败",
|
||||||
"ToastSeriesUpdateSuccess": "Series update success",
|
"ToastSeriesUpdateSuccess": "系列已更新",
|
||||||
"ToastSessionDeleteFailed": "删除会话失败",
|
"ToastSessionDeleteFailed": "删除会话失败",
|
||||||
"ToastSessionDeleteSuccess": "会话已删除",
|
"ToastSessionDeleteSuccess": "会话已删除",
|
||||||
"ToastSocketConnected": "网络已连接",
|
"ToastSocketConnected": "网络已连接",
|
||||||
"ToastSocketDisconnected": "网络已断开",
|
"ToastSocketDisconnected": "网络已断开",
|
||||||
"ToastSocketFailedToConnect": "网络连接失败",
|
"ToastSocketFailedToConnect": "网络连接失败",
|
||||||
"ToastUserDeleteFailed": "删除用户失败",
|
"ToastUserDeleteFailed": "删除用户失败",
|
||||||
"ToastUserDeleteSuccess": "用户已删除",
|
"ToastUserDeleteSuccess": "用户已删除"
|
||||||
"WeekdayFriday": "星期五",
|
|
||||||
"WeekdayMonday": "星期一",
|
|
||||||
"WeekdaySaturday": "星期六",
|
|
||||||
"WeekdaySunday": "星期日",
|
|
||||||
"WeekdayThursday": "星期四",
|
|
||||||
"WeekdayTuesday": "星期二",
|
|
||||||
"WeekdayWednesday": "星期三"
|
|
||||||
}
|
}
|
||||||
+92
-32
@@ -1,8 +1,24 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
<!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
viewBox="0 0 6702.7 1277.4" style="enable-background:new 0 0 6702.7 1277.4;" xml:space="preserve">
|
<svg
|
||||||
<style type="text/css">
|
version="1.1"
|
||||||
|
id="Layer_1"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 7501.0735 1237.1999"
|
||||||
|
xml:space="preserve"
|
||||||
|
width="7501.0737"
|
||||||
|
height="1237.2"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"><defs
|
||||||
|
id="defs104" />
|
||||||
|
<style
|
||||||
|
type="text/css"
|
||||||
|
id="style70">
|
||||||
.st0{fill:#FFFFFF;}
|
.st0{fill:#FFFFFF;}
|
||||||
.st1{fill:url(#SVGID_1_);}
|
.st1{fill:url(#SVGID_1_);}
|
||||||
.st2{fill:#C9C9C9;}
|
.st2{fill:#C9C9C9;}
|
||||||
@@ -12,38 +28,82 @@
|
|||||||
.st6{font-family:'GentiumBasic';}
|
.st6{font-family:'GentiumBasic';}
|
||||||
.st7{font-size:305px;}
|
.st7{font-size:305px;}
|
||||||
</style>
|
</style>
|
||||||
<title>bgAsset 6</title>
|
<title
|
||||||
<g id="Layer_2_1_">
|
id="title72">bgAsset 6</title>
|
||||||
<g id="Layer_2-2">
|
<g
|
||||||
<g id="Layer_4">
|
id="Layer_2_1_">
|
||||||
<g id="Layer_5">
|
<g
|
||||||
<circle class="st0" cx="618.6" cy="618.6" r="618.6"/>
|
id="Layer_2-2">
|
||||||
|
<g
|
||||||
|
id="Layer_4">
|
||||||
|
<g
|
||||||
|
id="Layer_5">
|
||||||
|
<circle
|
||||||
|
class="st0"
|
||||||
|
cx="618.59998"
|
||||||
|
cy="618.59998"
|
||||||
|
r="618.59998"
|
||||||
|
id="circle74" />
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="617.37" y1="1257.3" x2="617.37" y2="61.4399" gradientTransform="matrix(1 0 0 -1 0 1278)">
|
<linearGradient
|
||||||
<stop offset="0.32" style="stop-color:#CD9D49"/>
|
id="SVGID_1_"
|
||||||
<stop offset="0.99" style="stop-color:#875D27"/>
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="617.37"
|
||||||
|
y1="1257.3"
|
||||||
|
x2="617.37"
|
||||||
|
y2="61.439899"
|
||||||
|
gradientTransform="matrix(1,0,0,-1,0,1278)">
|
||||||
|
<stop
|
||||||
|
offset="0.32"
|
||||||
|
style="stop-color:#CD9D49"
|
||||||
|
id="stop77" />
|
||||||
|
<stop
|
||||||
|
offset="0.99"
|
||||||
|
style="stop-color:#875D27"
|
||||||
|
id="stop79" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<circle class="st1" cx="617.4" cy="618.6" r="597.9"/>
|
<circle
|
||||||
|
class="st1"
|
||||||
|
cx="617.40002"
|
||||||
|
cy="618.59998"
|
||||||
|
r="597.90002"
|
||||||
|
id="circle82"
|
||||||
|
style="fill:url(#SVGID_1_)" />
|
||||||
</g>
|
</g>
|
||||||
<path class="st0" d="M1005.6,574.1c-4.8-4-12.4-10-22.6-17v-79.2c0-201.9-163.7-365.6-365.6-365.6l0,0
|
<path
|
||||||
c-201.9,0-365.6,163.7-365.6,365.6v79.2c-10.2,7-17.7,13-22.6,17c-4.1,3.4-6.5,8.5-6.5,13.9v94.9c0,5.4,2.4,10.5,6.5,14
|
class="st0"
|
||||||
c11.3,9.4,37.2,29.1,77.5,49.3v9.2c0,24.9,16,45,35.8,45l0,0c19.8,0,35.8-20.2,35.8-45V527.8c0-24.9-16-45-35.8-45l0,0
|
d="m 1005.6,574.1 c -4.8,-4 -12.4,-10 -22.6,-17 V 477.9 C 983,276 819.3,112.3 617.4,112.3 v 0 C 415.5,112.3 251.8,276 251.8,477.9 v 79.2 c -10.2,7 -17.7,13 -22.6,17 -4.1,3.4 -6.5,8.5 -6.5,13.9 v 94.9 c 0,5.4 2.4,10.5 6.5,14 11.3,9.4 37.2,29.1 77.5,49.3 v 9.2 c 0,24.9 16,45 35.8,45 v 0 c 19.8,0 35.8,-20.2 35.8,-45 V 527.8 c 0,-24.9 -16,-45 -35.8,-45 v 0 c -19,0 -34.5,18.5 -35.8,41.9 h -0.1 v -46.9 c 0,-171.6 139.1,-310.7 310.7,-310.7 v 0 C 789,167.2 928,306.3 928,477.9 v 46.9 0 c -1.3,-23.4 -16.8,-41.9 -35.8,-41.9 v 0 c -19.8,0 -35.8,20.2 -35.8,45 v 227.6 c 0,24.9 16,45 35.8,45 v 0 c 19.8,0 35.8,-20.2 35.8,-45 v -9.2 c 40.3,-20.2 66.2,-39.9 77.5,-49.3 4.2,-3.5 6.5,-8.6 6.5,-14 v -95 c 0.1,-5.4 -2.3,-10.5 -6.4,-13.9 z"
|
||||||
c-19,0-34.5,18.5-35.8,41.9h-0.1v-46.9c0-171.6,139.1-310.7,310.7-310.7l0,0C789,167.2,928,306.3,928,477.9v46.9H928
|
id="path85" />
|
||||||
c-1.3-23.4-16.8-41.9-35.8-41.9l0,0c-19.8,0-35.8,20.2-35.8,45v227.6c0,24.9,16,45,35.8,45l0,0c19.8,0,35.8-20.2,35.8-45v-9.2
|
<path
|
||||||
c40.3-20.2,66.2-39.9,77.5-49.3c4.2-3.5,6.5-8.6,6.5-14V588C1012.1,582.6,1009.7,577.5,1005.6,574.1z"/>
|
class="st0"
|
||||||
<path class="st0" d="M489.9,969.7c23.9,0,43.3-19.4,43.3-43.3V441.6c0-23.9-19.4-43.3-43.3-43.3h-44.7
|
d="m 489.9,969.7 c 23.9,0 43.3,-19.4 43.3,-43.3 V 441.6 c 0,-23.9 -19.4,-43.3 -43.3,-43.3 h -44.7 c -23.9,0 -43.3,19.4 -43.3,43.3 v 484.8 c 0,23.9 19.4,43.3 43.3,43.3 z M 418.2,514.6 h 98.7 v 10.3 h -98.7 z"
|
||||||
c-23.9,0-43.3,19.4-43.3,43.3v484.8c0,23.9,19.4,43.3,43.3,43.3L489.9,969.7z M418.2,514.6h98.7v10.3h-98.7V514.6z"/>
|
id="path87" />
|
||||||
<path class="st0" d="M639.7,969.7c23.9,0,43.3-19.4,43.3-43.3V441.6c0-23.9-19.4-43.3-43.3-43.3H595c-23.9,0-43.3,19.4-43.3,43.3
|
<path
|
||||||
v484.8c0,23.9,19.4,43.3,43.3,43.3H639.7z M568,514.6h98.7v10.3H568V514.6z"/>
|
class="st0"
|
||||||
<path class="st0" d="M789.6,969.7c23.9,0,43.3-19.4,43.3-43.3V441.6c0-23.9-19.4-43.3-43.3-43.3h-44.7
|
d="m 639.7,969.7 c 23.9,0 43.3,-19.4 43.3,-43.3 V 441.6 c 0,-23.9 -19.4,-43.3 -43.3,-43.3 H 595 c -23.9,0 -43.3,19.4 -43.3,43.3 v 484.8 c 0,23.9 19.4,43.3 43.3,43.3 z M 568,514.6 h 98.7 v 10.3 H 568 Z"
|
||||||
c-23.9,0-43.3,19.4-43.3,43.3v484.8c0,23.9,19.4,43.3,43.3,43.3L789.6,969.7z M717.9,514.6h98.7v10.3h-98.7V514.6z"/>
|
id="path89" />
|
||||||
<path class="st0" d="M327.1,984.7h580.5c18,0,32.6,14.6,32.6,32.6v0c0,18-14.6,32.6-32.6,32.6H327.1c-18,0-32.6-14.6-32.6-32.6v0
|
<path
|
||||||
C294.5,999.3,309.1,984.7,327.1,984.7z"/>
|
class="st0"
|
||||||
|
d="m 789.6,969.7 c 23.9,0 43.3,-19.4 43.3,-43.3 V 441.6 c 0,-23.9 -19.4,-43.3 -43.3,-43.3 h -44.7 c -23.9,0 -43.3,19.4 -43.3,43.3 v 484.8 c 0,23.9 19.4,43.3 43.3,43.3 z M 717.9,514.6 h 98.7 v 10.3 h -98.7 z"
|
||||||
|
id="path91" />
|
||||||
|
<path
|
||||||
|
class="st0"
|
||||||
|
d="m 327.1,984.7 h 580.5 c 18,0 32.6,14.6 32.6,32.6 v 0 c 0,18 -14.6,32.6 -32.6,32.6 H 327.1 c -18,0 -32.6,-14.6 -32.6,-32.6 v 0 c 0,-18 14.6,-32.6 32.6,-32.6 z"
|
||||||
|
id="path93" />
|
||||||
</g>
|
</g>
|
||||||
<g id="Layer_6">
|
<g
|
||||||
<text transform="matrix(1 0 0 1 1492.27 735.42)" class="st2 st3 st4">audiobookshelf</text>
|
id="Layer_6">
|
||||||
<text id="self-hosted_audiobook_and_podcast_server" transform="matrix(1 0 0 1 1492.27 1103.6899)" class="st5 st6 st7">self-hosted audiobook and podcast server</text>
|
<text
|
||||||
|
transform="translate(1492.27,735.42)"
|
||||||
|
class="st2 st3 st4"
|
||||||
|
id="text96">audiobookshelf</text>
|
||||||
|
<text
|
||||||
|
id="self-hosted_audiobook_and_podcast_server"
|
||||||
|
transform="translate(1492.27,1103.6899)"
|
||||||
|
class="st5 st6 st7">self-hosted audiobook and podcast server</text>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
<metadata
|
||||||
|
id="metadata210"><rdf:RDF><cc:Work
|
||||||
|
rdf:about=""><dc:title>bgAsset 6</dc:title></cc:Work></rdf:RDF></metadata></svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.7 KiB |
@@ -18,8 +18,8 @@ const PORT = process.env.PORT || 80
|
|||||||
const HOST = process.env.HOST
|
const HOST = process.env.HOST
|
||||||
const CONFIG_PATH = process.env.CONFIG_PATH || '/config'
|
const CONFIG_PATH = process.env.CONFIG_PATH || '/config'
|
||||||
const METADATA_PATH = process.env.METADATA_PATH || '/metadata'
|
const METADATA_PATH = process.env.METADATA_PATH || '/metadata'
|
||||||
const UID = process.env.AUDIOBOOKSHELF_UID || 99
|
const UID = process.env.AUDIOBOOKSHELF_UID
|
||||||
const GID = process.env.AUDIOBOOKSHELF_GID || 100
|
const GID = process.env.AUDIOBOOKSHELF_GID
|
||||||
const SOURCE = process.env.SOURCE || 'docker'
|
const SOURCE = process.env.SOURCE || 'docker'
|
||||||
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || ''
|
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || ''
|
||||||
|
|
||||||
|
|||||||
Generated
+137
-52
@@ -1,20 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.12",
|
"version": "2.2.13",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.12",
|
"version": "2.2.13",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.26.1",
|
"axios": "^1.2.2",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"graceful-fs": "^4.2.10",
|
"graceful-fs": "^4.2.10",
|
||||||
"htmlparser2": "^8.0.1",
|
"htmlparser2": "^8.0.1",
|
||||||
"node-tone": "^1.0.1",
|
"node-tone": "^1.0.1",
|
||||||
"socket.io": "^4.4.1",
|
"socket.io": "^4.5.4",
|
||||||
"xml2js": "^0.4.23"
|
"xml2js": "^0.4.23"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -35,14 +35,17 @@
|
|||||||
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
|
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
|
||||||
},
|
},
|
||||||
"node_modules/@types/cors": {
|
"node_modules/@types/cors": {
|
||||||
"version": "2.8.12",
|
"version": "2.8.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz",
|
||||||
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw=="
|
"integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "18.11.9",
|
"version": "18.11.18",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||||
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg=="
|
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA=="
|
||||||
},
|
},
|
||||||
"node_modules/abbrev": {
|
"node_modules/abbrev": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@@ -63,9 +66,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/anymatch": {
|
"node_modules/anymatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||||
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
|
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"normalize-path": "^3.0.0",
|
"normalize-path": "^3.0.0",
|
||||||
@@ -80,12 +83,19 @@
|
|||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "0.26.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.2.2.tgz",
|
||||||
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
|
"integrity": "sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.14.8"
|
"follow-redirects": "^1.15.0",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
@@ -203,6 +213,17 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -261,6 +282,14 @@
|
|||||||
"ms": "2.0.0"
|
"ms": "2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
@@ -343,9 +372,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/engine.io": {
|
"node_modules/engine.io": {
|
||||||
"version": "6.2.0",
|
"version": "6.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz",
|
||||||
"integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==",
|
"integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/cookie": "^0.4.1",
|
"@types/cookie": "^0.4.1",
|
||||||
"@types/cors": "^2.8.12",
|
"@types/cors": "^2.8.12",
|
||||||
@@ -512,6 +541,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@@ -925,6 +967,11 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||||
|
},
|
||||||
"node_modules/pstree.remy": {
|
"node_modules/pstree.remy": {
|
||||||
"version": "1.1.8",
|
"version": "1.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||||
@@ -1078,9 +1125,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/simple-update-notifier": {
|
"node_modules/simple-update-notifier": {
|
||||||
"version": "1.0.7",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz",
|
||||||
"integrity": "sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew==",
|
"integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"semver": "~7.0.0"
|
"semver": "~7.0.0"
|
||||||
@@ -1099,16 +1146,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socket.io": {
|
"node_modules/socket.io": {
|
||||||
"version": "4.5.3",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.4.tgz",
|
||||||
"integrity": "sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg==",
|
"integrity": "sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.4",
|
"accepts": "~1.3.4",
|
||||||
"base64id": "~2.0.0",
|
"base64id": "~2.0.0",
|
||||||
"debug": "~4.3.2",
|
"debug": "~4.3.2",
|
||||||
"engine.io": "~6.2.0",
|
"engine.io": "~6.2.1",
|
||||||
"socket.io-adapter": "~2.4.0",
|
"socket.io-adapter": "~2.4.0",
|
||||||
"socket.io-parser": "~4.2.0"
|
"socket.io-parser": "~4.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
@@ -1320,14 +1367,17 @@
|
|||||||
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
|
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
|
||||||
},
|
},
|
||||||
"@types/cors": {
|
"@types/cors": {
|
||||||
"version": "2.8.12",
|
"version": "2.8.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz",
|
||||||
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw=="
|
"integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==",
|
||||||
|
"requires": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "18.11.9",
|
"version": "18.11.18",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||||
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg=="
|
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA=="
|
||||||
},
|
},
|
||||||
"abbrev": {
|
"abbrev": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@@ -1345,9 +1395,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"anymatch": {
|
"anymatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||||
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
|
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"normalize-path": "^3.0.0",
|
"normalize-path": "^3.0.0",
|
||||||
@@ -1359,12 +1409,19 @@
|
|||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
||||||
},
|
},
|
||||||
|
"asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
|
},
|
||||||
"axios": {
|
"axios": {
|
||||||
"version": "0.26.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.2.2.tgz",
|
||||||
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
|
"integrity": "sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"follow-redirects": "^1.14.8"
|
"follow-redirects": "^1.15.0",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
@@ -1452,6 +1509,14 @@
|
|||||||
"readdirp": "~3.6.0"
|
"readdirp": "~3.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"requires": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -1498,6 +1563,11 @@
|
|||||||
"ms": "2.0.0"
|
"ms": "2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
||||||
|
},
|
||||||
"depd": {
|
"depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
@@ -1552,9 +1622,9 @@
|
|||||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
|
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
|
||||||
},
|
},
|
||||||
"engine.io": {
|
"engine.io": {
|
||||||
"version": "6.2.0",
|
"version": "6.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz",
|
||||||
"integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==",
|
"integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/cookie": "^0.4.1",
|
"@types/cookie": "^0.4.1",
|
||||||
"@types/cors": "^2.8.12",
|
"@types/cors": "^2.8.12",
|
||||||
@@ -1674,6 +1744,16 @@
|
|||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
|
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
|
||||||
},
|
},
|
||||||
|
"form-data": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
|
"requires": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"forwarded": {
|
"forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@@ -1966,6 +2046,11 @@
|
|||||||
"ipaddr.js": "1.9.1"
|
"ipaddr.js": "1.9.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||||
|
},
|
||||||
"pstree.remy": {
|
"pstree.remy": {
|
||||||
"version": "1.1.8",
|
"version": "1.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||||
@@ -2080,9 +2165,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"simple-update-notifier": {
|
"simple-update-notifier": {
|
||||||
"version": "1.0.7",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz",
|
||||||
"integrity": "sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew==",
|
"integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"semver": "~7.0.0"
|
"semver": "~7.0.0"
|
||||||
@@ -2097,16 +2182,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"socket.io": {
|
"socket.io": {
|
||||||
"version": "4.5.3",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.4.tgz",
|
||||||
"integrity": "sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg==",
|
"integrity": "sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"accepts": "~1.3.4",
|
"accepts": "~1.3.4",
|
||||||
"base64id": "~2.0.0",
|
"base64id": "~2.0.0",
|
||||||
"debug": "~4.3.2",
|
"debug": "~4.3.2",
|
||||||
"engine.io": "~6.2.0",
|
"engine.io": "~6.2.1",
|
||||||
"socket.io-adapter": "~2.4.0",
|
"socket.io-adapter": "~2.4.0",
|
||||||
"socket.io-parser": "~4.2.0"
|
"socket.io-parser": "~4.2.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": {
|
"debug": {
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.12",
|
"version": "2.2.13",
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -30,12 +30,12 @@
|
|||||||
"author": "advplyr",
|
"author": "advplyr",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.26.1",
|
"axios": "^1.2.2",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"graceful-fs": "^4.2.10",
|
"graceful-fs": "^4.2.10",
|
||||||
"htmlparser2": "^8.0.1",
|
"htmlparser2": "^8.0.1",
|
||||||
"node-tone": "^1.0.1",
|
"node-tone": "^1.0.1",
|
||||||
"socket.io": "^4.4.1",
|
"socket.io": "^4.5.4",
|
||||||
"xml2js": "^0.4.23"
|
"xml2js": "^0.4.23"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -20,12 +20,13 @@ var inputConfig = options.config ? Path.resolve(options.config) : null
|
|||||||
var inputMetadata = options.metadata ? Path.resolve(options.metadata) : null
|
var inputMetadata = options.metadata ? Path.resolve(options.metadata) : null
|
||||||
|
|
||||||
const PORT = options.port || process.env.PORT || 3333
|
const PORT = options.port || process.env.PORT || 3333
|
||||||
const HOST = options.host || process.env.HOST || "0.0.0.0"
|
const HOST = options.host || process.env.HOST
|
||||||
const CONFIG_PATH = inputConfig || process.env.CONFIG_PATH || Path.resolve('config')
|
const CONFIG_PATH = inputConfig || process.env.CONFIG_PATH || Path.resolve('config')
|
||||||
const METADATA_PATH = inputMetadata || process.env.METADATA_PATH || Path.resolve('metadata')
|
const METADATA_PATH = inputMetadata || process.env.METADATA_PATH || Path.resolve('metadata')
|
||||||
const UID = 99
|
const UID = process.env.AUDIOBOOKSHELF_UID
|
||||||
const GID = 100
|
const GID = process.env.AUDIOBOOKSHELF_GID
|
||||||
const SOURCE = options.source || 'debian'
|
const SOURCE = options.source || process.env.SOURCE || 'debian'
|
||||||
|
|
||||||
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || ''
|
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || ''
|
||||||
|
|
||||||
console.log(process.env.NODE_ENV, 'Config', CONFIG_PATH, METADATA_PATH)
|
console.log(process.env.NODE_ENV, 'Config', CONFIG_PATH, METADATA_PATH)
|
||||||
|
|||||||
+4
-2
@@ -43,11 +43,12 @@ class Server {
|
|||||||
this.Host = HOST
|
this.Host = HOST
|
||||||
global.Source = SOURCE
|
global.Source = SOURCE
|
||||||
global.isWin = process.platform === 'win32'
|
global.isWin = process.platform === 'win32'
|
||||||
global.Uid = isNaN(UID) ? 0 : Number(UID)
|
global.Uid = isNaN(UID) ? undefined : Number(UID)
|
||||||
global.Gid = isNaN(GID) ? 0 : Number(GID)
|
global.Gid = isNaN(GID) ? undefined : Number(GID)
|
||||||
global.ConfigPath = fileUtils.filePathToPOSIX(Path.normalize(CONFIG_PATH))
|
global.ConfigPath = fileUtils.filePathToPOSIX(Path.normalize(CONFIG_PATH))
|
||||||
global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH))
|
global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH))
|
||||||
global.RouterBasePath = ROUTER_BASE_PATH
|
global.RouterBasePath = ROUTER_BASE_PATH
|
||||||
|
global.XAccel = process.env.USE_X_ACCEL
|
||||||
|
|
||||||
if (!fs.pathExistsSync(global.ConfigPath)) {
|
if (!fs.pathExistsSync(global.ConfigPath)) {
|
||||||
fs.mkdirSync(global.ConfigPath)
|
fs.mkdirSync(global.ConfigPath)
|
||||||
@@ -142,6 +143,7 @@ class Server {
|
|||||||
const app = express()
|
const app = express()
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
app.use(global.RouterBasePath, router)
|
app.use(global.RouterBasePath, router)
|
||||||
|
app.disable('x-powered-by')
|
||||||
|
|
||||||
this.server = http.createServer(app)
|
this.server = http.createServer(app)
|
||||||
|
|
||||||
|
|||||||
@@ -319,7 +319,7 @@ class LibraryController {
|
|||||||
// series represents in the filtered series
|
// series represents in the filtered series
|
||||||
if (filterSeries) {
|
if (filterSeries) {
|
||||||
json.collapsedSeries.seriesSequenceList =
|
json.collapsedSeries.seriesSequenceList =
|
||||||
naturalSort(li.collapsedSeries.books.map(b => b.filterSeriesSequence)).asc()
|
naturalSort(li.collapsedSeries.books.filter(b => b.filterSeriesSequence).map(b => b.filterSeriesSequence)).asc()
|
||||||
.reduce((ranges, currentSequence) => {
|
.reduce((ranges, currentSequence) => {
|
||||||
let lastRange = ranges.at(-1)
|
let lastRange = ranges.at(-1)
|
||||||
let isNumber = /^(\d+|\d+\.\d*|\d*\.\d+)$/.test(currentSequence)
|
let isNumber = /^(\d+|\d+\.\d*|\d*\.\d+)$/.test(currentSequence)
|
||||||
|
|||||||
@@ -201,6 +201,10 @@ class LibraryItemController {
|
|||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (global.XAccel) {
|
||||||
|
Logger.debug(`Use X-Accel to serve static file ${libraryItem.media.coverPath}`)
|
||||||
|
return res.status(204).header({'X-Accel-Redirect': global.XAccel + libraryItem.media.coverPath}).send()
|
||||||
|
}
|
||||||
return res.sendFile(libraryItem.media.coverPath)
|
return res.sendFile(libraryItem.media.coverPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -192,7 +192,8 @@ class MeController {
|
|||||||
}
|
}
|
||||||
const updatedLocalMediaProgress = []
|
const updatedLocalMediaProgress = []
|
||||||
var numServerProgressUpdates = 0
|
var numServerProgressUpdates = 0
|
||||||
var localMediaProgress = req.body.localMediaProgress || []
|
const updatedServerMediaProgress = []
|
||||||
|
const localMediaProgress = req.body.localMediaProgress || []
|
||||||
|
|
||||||
localMediaProgress.forEach(localProgress => {
|
localMediaProgress.forEach(localProgress => {
|
||||||
if (!localProgress.libraryItemId) {
|
if (!localProgress.libraryItemId) {
|
||||||
@@ -205,18 +206,22 @@ class MeController {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId)
|
let mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId)
|
||||||
if (!mediaProgress) {
|
if (!mediaProgress) {
|
||||||
// New media progress from mobile
|
// New media progress from mobile
|
||||||
Logger.debug(`[MeController] syncLocalMediaProgress local progress is new - creating ${localProgress.id}`)
|
Logger.debug(`[MeController] syncLocalMediaProgress local progress is new - creating ${localProgress.id}`)
|
||||||
req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId)
|
req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId)
|
||||||
|
mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId)
|
||||||
|
updatedServerMediaProgress.push(mediaProgress)
|
||||||
numServerProgressUpdates++
|
numServerProgressUpdates++
|
||||||
} else if (mediaProgress.lastUpdate < localProgress.lastUpdate) {
|
} else if (mediaProgress.lastUpdate < localProgress.lastUpdate) {
|
||||||
Logger.debug(`[MeController] syncLocalMediaProgress local progress is more recent - updating ${mediaProgress.id}`)
|
Logger.debug(`[MeController] syncLocalMediaProgress local progress is more recent - updating ${mediaProgress.id}`)
|
||||||
req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId)
|
req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId)
|
||||||
|
mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId)
|
||||||
|
updatedServerMediaProgress.push(mediaProgress)
|
||||||
numServerProgressUpdates++
|
numServerProgressUpdates++
|
||||||
} else if (mediaProgress.lastUpdate > localProgress.lastUpdate) {
|
} else if (mediaProgress.lastUpdate > localProgress.lastUpdate) {
|
||||||
var updateTimeDifference = mediaProgress.lastUpdate - localProgress.lastUpdate
|
const updateTimeDifference = mediaProgress.lastUpdate - localProgress.lastUpdate
|
||||||
Logger.debug(`[MeController] syncLocalMediaProgress server progress is more recent by ${updateTimeDifference}ms - ${mediaProgress.id}`)
|
Logger.debug(`[MeController] syncLocalMediaProgress server progress is more recent by ${updateTimeDifference}ms - ${mediaProgress.id}`)
|
||||||
|
|
||||||
for (const key in localProgress) {
|
for (const key in localProgress) {
|
||||||
@@ -240,7 +245,8 @@ class MeController {
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
numServerProgressUpdates,
|
numServerProgressUpdates,
|
||||||
localProgressUpdates: updatedLocalMediaProgress
|
localProgressUpdates: updatedLocalMediaProgress, // Array of LocalMediaProgress that were updated from server (server more recent)
|
||||||
|
serverProgressUpdates: updatedServerMediaProgress // Array of MediaProgress that made updates to server (local more recent)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -141,6 +141,10 @@ class AbMergeManager {
|
|||||||
'TrackNumber': 1,
|
'TrackNumber': 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (libraryItem.media.coverPath) {
|
||||||
|
task.data.toneJsonObject['CoverFile'] = libraryItem.media.coverPath
|
||||||
|
}
|
||||||
|
|
||||||
const workerData = {
|
const workerData = {
|
||||||
inputs: ffmpegInputs,
|
inputs: ffmpegInputs,
|
||||||
options: ffmpegOptions,
|
options: ffmpegOptions,
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class AudioMetadataMangaer {
|
|||||||
|
|
||||||
const results = []
|
const results = []
|
||||||
for (const af of audioFiles) {
|
for (const af of audioFiles) {
|
||||||
const result = await this.updateAudioFileMetadataWithTone(libraryItem.id, af, toneJsonPath, itemCacheDir, backupFiles)
|
const result = await this.updateAudioFileMetadataWithTone(libraryItem, af, toneJsonPath, itemCacheDir, backupFiles)
|
||||||
results.push(result)
|
results.push(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,9 +82,9 @@ class AudioMetadataMangaer {
|
|||||||
SocketAuthority.emitter('audio_metadata_finished', itemAudioMetadataPayload)
|
SocketAuthority.emitter('audio_metadata_finished', itemAudioMetadataPayload)
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateAudioFileMetadataWithTone(libraryItemId, audioFile, toneJsonPath, itemCacheDir, backupFiles) {
|
async updateAudioFileMetadataWithTone(libraryItem, audioFile, toneJsonPath, itemCacheDir, backupFiles) {
|
||||||
const resultPayload = {
|
const resultPayload = {
|
||||||
libraryItemId,
|
libraryItemId: libraryItem.id,
|
||||||
index: audioFile.index,
|
index: audioFile.index,
|
||||||
ino: audioFile.ino,
|
ino: audioFile.ino,
|
||||||
filename: audioFile.metadata.filename
|
filename: audioFile.metadata.filename
|
||||||
@@ -107,6 +107,10 @@ class AudioMetadataMangaer {
|
|||||||
'TrackNumber': audioFile.index,
|
'TrackNumber': audioFile.index,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (libraryItem.media.coverPath) {
|
||||||
|
_toneMetadataObject['CoverFile'] = libraryItem.media.coverPath
|
||||||
|
}
|
||||||
|
|
||||||
resultPayload.success = await toneHelpers.tagAudioFile(audioFile.metadata.path, _toneMetadataObject)
|
resultPayload.success = await toneHelpers.tagAudioFile(audioFile.metadata.path, _toneMetadataObject)
|
||||||
if (resultPayload.success) {
|
if (resultPayload.success) {
|
||||||
Logger.info(`[AudioMetadataManager] Successfully tagged audio file "${audioFile.metadata.path}"`)
|
Logger.info(`[AudioMetadataManager] Successfully tagged audio file "${audioFile.metadata.path}"`)
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ class CacheManager {
|
|||||||
|
|
||||||
// Cache exists
|
// Cache exists
|
||||||
if (await fs.pathExists(path)) {
|
if (await fs.pathExists(path)) {
|
||||||
|
if (global.XAccel) {
|
||||||
|
Logger.debug(`Use X-Accel to serve static file ${path}`)
|
||||||
|
return res.status(204).header({'X-Accel-Redirect': global.XAccel + path}).send()
|
||||||
|
}
|
||||||
|
|
||||||
const r = fs.createReadStream(path)
|
const r = fs.createReadStream(path)
|
||||||
const ps = new stream.PassThrough()
|
const ps = new stream.PassThrough()
|
||||||
stream.pipeline(r, ps, (err) => {
|
stream.pipeline(r, ps, (err) => {
|
||||||
@@ -72,6 +77,11 @@ class CacheManager {
|
|||||||
// Set owner and permissions of cache image
|
// Set owner and permissions of cache image
|
||||||
await filePerms.setDefault(path)
|
await filePerms.setDefault(path)
|
||||||
|
|
||||||
|
if (global.XAccel) {
|
||||||
|
Logger.debug(`Use X-Accel to serve static file ${writtenFile}`)
|
||||||
|
return res.status(204).header({'X-Accel-Redirect': global.XAccel + writtenFile}).send()
|
||||||
|
}
|
||||||
|
|
||||||
var readStream = fs.createReadStream(writtenFile)
|
var readStream = fs.createReadStream(writtenFile)
|
||||||
readStream.pipe(res)
|
readStream.pipe(res)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,17 @@ class PlaybackSessionManager {
|
|||||||
return res.status(500).send('Library item not found')
|
return res.status(500).send('Library item not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If server session is open for this same media item then close it
|
||||||
|
const userSessionForThisItem = this.sessions.find(playbackSession => {
|
||||||
|
if (playbackSession.userId !== user.id) return false
|
||||||
|
if (sessionJson.episodeId) return playbackSession.episodeId !== sessionJson.episodeId
|
||||||
|
return playbackSession.libraryItemId === sessionJson.libraryItemId
|
||||||
|
})
|
||||||
|
if (userSessionForThisItem) {
|
||||||
|
Logger.info(`[PlaybackSessionManager] syncLocalSessionRequest: Closing open session "${userSessionForThisItem.displayTitle}" for user "${user.username}"`)
|
||||||
|
await this.closeSession(user, userSessionForThisItem, null)
|
||||||
|
}
|
||||||
|
|
||||||
this.localSessionLock[sessionJson.id] = true // Lock local session
|
this.localSessionLock[sessionJson.id] = true // Lock local session
|
||||||
|
|
||||||
let session = await this.db.getPlaybackSession(sessionJson.id)
|
let session = await this.db.getPlaybackSession(sessionJson.id)
|
||||||
|
|||||||
@@ -81,9 +81,17 @@ class RssFeedManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if feed needs to be updated
|
// Check if feed needs to be updated
|
||||||
if (feed.entityType === 'item') {
|
if (feed.entityType === 'libraryItem') {
|
||||||
const libraryItem = this.db.getLibraryItem(feed.entityId)
|
const libraryItem = this.db.getLibraryItem(feed.entityId)
|
||||||
if (libraryItem && (!feed.entityUpdatedAt || libraryItem.updatedAt > feed.entityUpdatedAt)) {
|
|
||||||
|
let mostRecentlyUpdatedAt = libraryItem.updatedAt
|
||||||
|
if (libraryItem.isPodcast) {
|
||||||
|
libraryItem.media.episodes.forEach((episode) => {
|
||||||
|
if (episode.updatedAt > mostRecentlyUpdatedAt) mostRecentlyUpdatedAt = episode.updatedAt
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (libraryItem && (!feed.entityUpdatedAt || mostRecentlyUpdatedAt > feed.entityUpdatedAt)) {
|
||||||
Logger.debug(`[RssFeedManager] Updating RSS feed for item ${libraryItem.id} "${libraryItem.media.metadata.title}"`)
|
Logger.debug(`[RssFeedManager] Updating RSS feed for item ${libraryItem.id} "${libraryItem.media.metadata.title}"`)
|
||||||
feed.updateFromItem(libraryItem)
|
feed.updateFromItem(libraryItem)
|
||||||
await this.db.updateEntity('feed', feed)
|
await this.db.updateEntity('feed', feed)
|
||||||
|
|||||||
@@ -110,13 +110,15 @@ class Feed {
|
|||||||
this.episodes = []
|
this.episodes = []
|
||||||
if (isPodcast) { // PODCAST EPISODES
|
if (isPodcast) { // PODCAST EPISODES
|
||||||
media.episodes.forEach((episode) => {
|
media.episodes.forEach((episode) => {
|
||||||
var feedEpisode = new FeedEpisode()
|
if (episode.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = episode.updatedAt
|
||||||
|
|
||||||
|
const feedEpisode = new FeedEpisode()
|
||||||
feedEpisode.setFromPodcastEpisode(libraryItem, serverAddress, slug, episode, this.meta)
|
feedEpisode.setFromPodcastEpisode(libraryItem, serverAddress, slug, episode, this.meta)
|
||||||
this.episodes.push(feedEpisode)
|
this.episodes.push(feedEpisode)
|
||||||
})
|
})
|
||||||
} else { // AUDIOBOOK EPISODES
|
} else { // AUDIOBOOK EPISODES
|
||||||
media.tracks.forEach((audioTrack) => {
|
media.tracks.forEach((audioTrack) => {
|
||||||
var feedEpisode = new FeedEpisode()
|
const feedEpisode = new FeedEpisode()
|
||||||
feedEpisode.setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, this.meta)
|
feedEpisode.setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, this.meta)
|
||||||
this.episodes.push(feedEpisode)
|
this.episodes.push(feedEpisode)
|
||||||
})
|
})
|
||||||
@@ -144,13 +146,15 @@ class Feed {
|
|||||||
this.episodes = []
|
this.episodes = []
|
||||||
if (isPodcast) { // PODCAST EPISODES
|
if (isPodcast) { // PODCAST EPISODES
|
||||||
media.episodes.forEach((episode) => {
|
media.episodes.forEach((episode) => {
|
||||||
var feedEpisode = new FeedEpisode()
|
if (episode.updatedAt > this.entityUpdatedAt) this.entityUpdatedAt = episode.updatedAt
|
||||||
|
|
||||||
|
const feedEpisode = new FeedEpisode()
|
||||||
feedEpisode.setFromPodcastEpisode(libraryItem, this.serverAddress, this.slug, episode, this.meta)
|
feedEpisode.setFromPodcastEpisode(libraryItem, this.serverAddress, this.slug, episode, this.meta)
|
||||||
this.episodes.push(feedEpisode)
|
this.episodes.push(feedEpisode)
|
||||||
})
|
})
|
||||||
} else { // AUDIOBOOK EPISODES
|
} else { // AUDIOBOOK EPISODES
|
||||||
media.tracks.forEach((audioTrack) => {
|
media.tracks.forEach((audioTrack) => {
|
||||||
var feedEpisode = new FeedEpisode()
|
const feedEpisode = new FeedEpisode()
|
||||||
feedEpisode.setFromAudiobookTrack(libraryItem, this.serverAddress, this.slug, audioTrack, this.meta)
|
feedEpisode.setFromAudiobookTrack(libraryItem, this.serverAddress, this.slug, audioTrack, this.meta)
|
||||||
this.episodes.push(feedEpisode)
|
this.episodes.push(feedEpisode)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -420,10 +420,38 @@ class Book {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IF first audio file has embedded chapters then use embedded chapters
|
// If first audio file has embedded chapters then use embedded chapters
|
||||||
if (includedAudioFiles[0].chapters && includedAudioFiles[0].chapters.length) {
|
if (includedAudioFiles[0].chapters?.length) {
|
||||||
Logger.debug(`[Book] setChapters: Using embedded chapters in audio file ${includedAudioFiles[0].metadata.path}`)
|
// If all files chapters are the same, then only make chapters for the first file
|
||||||
this.chapters = includedAudioFiles[0].chapters.map(c => ({ ...c }))
|
if (
|
||||||
|
includedAudioFiles.length === 1 ||
|
||||||
|
includedAudioFiles.length > 1 &&
|
||||||
|
includedAudioFiles[0].chapters.length === includedAudioFiles[1].chapters?.length &&
|
||||||
|
includedAudioFiles[0].chapters.every((c, i) => c.title === includedAudioFiles[1].chapters[i].title)
|
||||||
|
) {
|
||||||
|
Logger.debug(`[Book] setChapters: Using embedded chapters in first audio file ${includedAudioFiles[0].metadata?.path}`)
|
||||||
|
this.chapters = includedAudioFiles[0].chapters.map((c) => ({ ...c }))
|
||||||
|
} else {
|
||||||
|
Logger.debug(`[Book] setChapters: Using embedded chapters from all audio files ${includedAudioFiles[0].metadata?.path}`)
|
||||||
|
this.chapters = []
|
||||||
|
let currChapterId = 0
|
||||||
|
let currStartTime = 0
|
||||||
|
|
||||||
|
includedAudioFiles.forEach((file) => {
|
||||||
|
if (file.duration) {
|
||||||
|
const chapters = file.chapters?.map((c) => ({
|
||||||
|
...c,
|
||||||
|
id: c.id + currChapterId,
|
||||||
|
start: c.start + currStartTime,
|
||||||
|
end: c.end + currStartTime,
|
||||||
|
})) ?? []
|
||||||
|
this.chapters = this.chapters.concat(chapters)
|
||||||
|
|
||||||
|
currChapterId += file.chapters?.length ?? 0
|
||||||
|
currStartTime += file.duration
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
} else if (includedAudioFiles.length > 1) {
|
} else if (includedAudioFiles.length > 1) {
|
||||||
// Build chapters from audio files
|
// Build chapters from audio files
|
||||||
this.chapters = []
|
this.chapters = []
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class ServerSettings {
|
|||||||
this.loggerScannerLogsToKeep = 2
|
this.loggerScannerLogsToKeep = 2
|
||||||
|
|
||||||
// Bookshelf Display
|
// Bookshelf Display
|
||||||
this.homeBookshelfView = BookshelfView.STANDARD
|
this.homeBookshelfView = BookshelfView.DETAIL
|
||||||
this.bookshelfView = BookshelfView.DETAIL
|
this.bookshelfView = BookshelfView.DETAIL
|
||||||
|
|
||||||
// Podcasts
|
// Podcasts
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ class MediaProgress {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update(payload) {
|
update(payload) {
|
||||||
var hasUpdates = false
|
let hasUpdates = false
|
||||||
for (const key in payload) {
|
for (const key in payload) {
|
||||||
if (this[key] !== undefined && payload[key] !== this[key]) {
|
if (this[key] !== undefined && payload[key] !== this[key]) {
|
||||||
if (key === 'isFinished') {
|
if (key === 'isFinished') {
|
||||||
|
|||||||
@@ -314,18 +314,18 @@ class User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createUpdateMediaProgress(libraryItem, updatePayload, episodeId = null) {
|
createUpdateMediaProgress(libraryItem, updatePayload, episodeId = null) {
|
||||||
var itemProgress = this.mediaProgress.find(li => {
|
const itemProgress = this.mediaProgress.find(li => {
|
||||||
if (episodeId && li.episodeId !== episodeId) return false
|
if (episodeId && li.episodeId !== episodeId) return false
|
||||||
return li.libraryItemId === libraryItem.id
|
return li.libraryItemId === libraryItem.id
|
||||||
})
|
})
|
||||||
if (!itemProgress) {
|
if (!itemProgress) {
|
||||||
var newItemProgress = new MediaProgress()
|
const newItemProgress = new MediaProgress()
|
||||||
|
|
||||||
newItemProgress.setData(libraryItem.id, updatePayload, episodeId)
|
newItemProgress.setData(libraryItem.id, updatePayload, episodeId)
|
||||||
this.mediaProgress.push(newItemProgress)
|
this.mediaProgress.push(newItemProgress)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
var wasUpdated = itemProgress.update(updatePayload)
|
const wasUpdated = itemProgress.update(updatePayload)
|
||||||
|
|
||||||
if (updatePayload.lastUpdate) itemProgress.lastUpdate = updatePayload.lastUpdate // For local to keep update times in sync
|
if (updatePayload.lastUpdate) itemProgress.lastUpdate = updatePayload.lastUpdate // For local to keep update times in sync
|
||||||
return wasUpdated
|
return wasUpdated
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
const express = require('express')
|
const express = require('express')
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
|
const Logger = require('../Logger')
|
||||||
const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils')
|
const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils')
|
||||||
|
|
||||||
class StaticRouter {
|
class StaticRouter {
|
||||||
@@ -13,13 +14,18 @@ class StaticRouter {
|
|||||||
init() {
|
init() {
|
||||||
// Library Item static file routes
|
// Library Item static file routes
|
||||||
this.router.get('/item/:id/*', (req, res) => {
|
this.router.get('/item/:id/*', (req, res) => {
|
||||||
var item = this.db.libraryItems.find(ab => ab.id === req.params.id)
|
const item = this.db.libraryItems.find(ab => ab.id === req.params.id)
|
||||||
if (!item) return res.status(404).send('Item not found with id ' + req.params.id)
|
if (!item) return res.status(404).send('Item not found with id ' + req.params.id)
|
||||||
|
|
||||||
var remainingPath = req.params['0']
|
const remainingPath = req.params['0']
|
||||||
var fullPath = null
|
const fullPath = item.isFile ? item.path : Path.join(item.path, remainingPath)
|
||||||
if (item.isFile) fullPath = item.path
|
|
||||||
else fullPath = Path.join(item.path, remainingPath)
|
// Allow reverse proxy to serve files directly
|
||||||
|
// See: https://www.nginx.com/resources/wiki/start/topics/examples/x-accel/
|
||||||
|
if (global.XAccel) {
|
||||||
|
Logger.debug(`Use X-Accel to serve static file ${fullPath}`)
|
||||||
|
return res.status(204).header({'X-Accel-Redirect': global.XAccel + fullPath}).send()
|
||||||
|
}
|
||||||
|
|
||||||
var opts = {}
|
var opts = {}
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,11 @@ module.exports.setDefault = (path, silent = false) => {
|
|||||||
const uid = global.Uid
|
const uid = global.Uid
|
||||||
const gid = global.Gid
|
const gid = global.Gid
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (!silent) Logger.debug(`[FilePerms] Setting permission "${mode}" for uid ${uid} and gid ${gid} | "${path}"`)
|
if (isNaN(uid) || isNaN(gid)) {
|
||||||
|
if (!silent) Logger.debug('Not modifying permissions since no uid/gid is specified')
|
||||||
|
return resolve()
|
||||||
|
}
|
||||||
|
if (!silent) Logger.debug(`Setting permission "${mode}" for uid ${uid} and gid ${gid} | "${path}"`)
|
||||||
chmodr(path, mode, uid, gid, resolve)
|
chmodr(path, mode, uid, gid, resolve)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -102,6 +106,10 @@ module.exports.setDefaultDirSync = (path, silent = false) => {
|
|||||||
const mode = 0o744
|
const mode = 0o744
|
||||||
const uid = global.Uid
|
const uid = global.Uid
|
||||||
const gid = global.Gid
|
const gid = global.Gid
|
||||||
|
if (isNaN(uid) || isNaN(gid)) {
|
||||||
|
if (!silent) Logger.debug('Not modifying permissions since no uid/gid is specified')
|
||||||
|
return true
|
||||||
|
}
|
||||||
if (!silent) Logger.debug(`[FilePerms] Setting dir permission "${mode}" for uid ${uid} and gid ${gid} | "${path}"`)
|
if (!silent) Logger.debug(`[FilePerms] Setting dir permission "${mode}" for uid ${uid} and gid ${gid} | "${path}"`)
|
||||||
try {
|
try {
|
||||||
fs.chmodSync(path, mode)
|
fs.chmodSync(path, mode)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ function sanitize(html) {
|
|||||||
allowedAttributes: {
|
allowedAttributes: {
|
||||||
a: ['href', 'name', 'target']
|
a: ['href', 'name', 'target']
|
||||||
},
|
},
|
||||||
allowedSchemes: ['https'],
|
allowedSchemes: ['http', 'https', 'mailto'],
|
||||||
allowProtocolRelative: false
|
allowProtocolRelative: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+142
-31
@@ -339,6 +339,14 @@ module.exports = {
|
|||||||
entities: [],
|
entities: [],
|
||||||
category: 'continueSeries'
|
category: 'continueSeries'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'episodes-recently-added',
|
||||||
|
label: 'Newest Episodes',
|
||||||
|
labelStringKey: 'LabelNewestEpisodes',
|
||||||
|
type: 'episode',
|
||||||
|
entities: [],
|
||||||
|
category: 'newestEpisodes'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'recently-added',
|
id: 'recently-added',
|
||||||
label: 'Recently Added',
|
label: 'Recently Added',
|
||||||
@@ -347,14 +355,6 @@ module.exports = {
|
|||||||
entities: [],
|
entities: [],
|
||||||
category: 'newestItems'
|
category: 'newestItems'
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'listen-again',
|
|
||||||
label: 'Listen Again',
|
|
||||||
labelStringKey: 'LabelListenAgain',
|
|
||||||
type: isPodcastLibrary ? 'episode' : mediaType,
|
|
||||||
entities: [],
|
|
||||||
category: 'recentlyFinished'
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'recent-series',
|
id: 'recent-series',
|
||||||
label: 'Recent Series',
|
label: 'Recent Series',
|
||||||
@@ -363,6 +363,22 @@ module.exports = {
|
|||||||
entities: [],
|
entities: [],
|
||||||
category: 'newestSeries'
|
category: 'newestSeries'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'recommended',
|
||||||
|
label: 'Recommended',
|
||||||
|
labelStringKey: 'LabelRecommended',
|
||||||
|
type: mediaType,
|
||||||
|
entities: [],
|
||||||
|
category: 'recommended'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'listen-again',
|
||||||
|
label: 'Listen Again',
|
||||||
|
labelStringKey: 'LabelListenAgain',
|
||||||
|
type: isPodcastLibrary ? 'episode' : mediaType,
|
||||||
|
entities: [],
|
||||||
|
category: 'recentlyFinished'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'newest-authors',
|
id: 'newest-authors',
|
||||||
label: 'Newest Authors',
|
label: 'Newest Authors',
|
||||||
@@ -370,22 +386,13 @@ module.exports = {
|
|||||||
type: 'authors',
|
type: 'authors',
|
||||||
entities: [],
|
entities: [],
|
||||||
category: 'newestAuthors'
|
category: 'newestAuthors'
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'episodes-recently-added',
|
|
||||||
label: 'Newest Episodes',
|
|
||||||
labelStringKey: 'LabelNewestEpisodes',
|
|
||||||
type: 'episode',
|
|
||||||
entities: [],
|
|
||||||
category: 'newestEpisodes'
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
const categories = ['recentlyListened', 'continueSeries', 'newestEpisodes', 'newestItems', 'newestSeries', 'recentlyFinished', 'newestAuthors']
|
|
||||||
const categoryMap = {}
|
const categoryMap = {}
|
||||||
categories.forEach((cat) => {
|
shelves.forEach((shelf) => {
|
||||||
categoryMap[cat] = {
|
categoryMap[shelf.category] = {
|
||||||
category: cat,
|
category: shelf.category,
|
||||||
biggest: 0,
|
biggest: 0,
|
||||||
smallest: 0,
|
smallest: 0,
|
||||||
items: []
|
items: []
|
||||||
@@ -395,6 +402,12 @@ module.exports = {
|
|||||||
const seriesMap = {}
|
const seriesMap = {}
|
||||||
const authorMap = {}
|
const authorMap = {}
|
||||||
|
|
||||||
|
// For use with recommended
|
||||||
|
const topGenresListened = {}
|
||||||
|
const topAuthorsListened = {}
|
||||||
|
const topTagsListened = {}
|
||||||
|
const notStartedBooks = []
|
||||||
|
|
||||||
for (const libraryItem of libraryItems) {
|
for (const libraryItem of libraryItems) {
|
||||||
if (libraryItem.addedAt > categoryMap.newestItems.smallest) {
|
if (libraryItem.addedAt > categoryMap.newestItems.smallest) {
|
||||||
|
|
||||||
@@ -494,11 +507,30 @@ module.exports = {
|
|||||||
} else if (libraryItem.isBook) {
|
} else if (libraryItem.isBook) {
|
||||||
// Book categories
|
// Book categories
|
||||||
|
|
||||||
|
const mediaProgress = allItemProgress.length ? allItemProgress[0] : null
|
||||||
|
|
||||||
|
// Used for recommended. Tally up most listened to authors/genres/tags
|
||||||
|
if (mediaProgress && (mediaProgress.inProgress || mediaProgress.isFinished)) {
|
||||||
|
libraryItem.media.metadata.authors.forEach((author) => {
|
||||||
|
topAuthorsListened[author.id] = (topAuthorsListened[author.id] || 0) + 1
|
||||||
|
})
|
||||||
|
libraryItem.media.metadata.genres.forEach((genre) => {
|
||||||
|
topGenresListened[genre] = (topGenresListened[genre] || 0) + 1
|
||||||
|
})
|
||||||
|
libraryItem.media.tags.forEach((tag) => {
|
||||||
|
topTagsListened[tag] = (topTagsListened[tag] || 0) + 1
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Insert in random position to add randomization to equal weighted items
|
||||||
|
notStartedBooks.splice(Math.floor(Math.random() * (notStartedBooks.length + 1)), 0, libraryItem)
|
||||||
|
}
|
||||||
|
|
||||||
// Newest series
|
// Newest series
|
||||||
if (libraryItem.media.metadata.series.length) {
|
if (libraryItem.media.metadata.series.length) {
|
||||||
for (const librarySeries of libraryItem.media.metadata.series) {
|
for (const librarySeries of libraryItem.media.metadata.series) {
|
||||||
const mediaProgress = allItemProgress.length ? allItemProgress[0] : null
|
|
||||||
const bookInProgress = mediaProgress && (mediaProgress.inProgress || mediaProgress.isFinished)
|
const bookInProgress = mediaProgress && (mediaProgress.inProgress || mediaProgress.isFinished)
|
||||||
|
const bookActive = mediaProgress && mediaProgress.inProgress && !mediaProgress.isFinished
|
||||||
const libraryItemJson = libraryItem.toJSONMinified()
|
const libraryItemJson = libraryItem.toJSONMinified()
|
||||||
libraryItemJson.seriesSequence = librarySeries.sequence
|
libraryItemJson.seriesSequence = librarySeries.sequence
|
||||||
|
|
||||||
@@ -511,6 +543,7 @@ module.exports = {
|
|||||||
...seriesObj.toJSON(),
|
...seriesObj.toJSON(),
|
||||||
books: [libraryItemJson],
|
books: [libraryItemJson],
|
||||||
inProgress: bookInProgress,
|
inProgress: bookInProgress,
|
||||||
|
hasActiveBook: bookActive,
|
||||||
hideFromContinueListening,
|
hideFromContinueListening,
|
||||||
bookInProgressLastUpdate: bookInProgress ? mediaProgress.lastUpdate : null,
|
bookInProgressLastUpdate: bookInProgress ? mediaProgress.lastUpdate : null,
|
||||||
firstBookUnread: bookInProgress ? null : libraryItemJson
|
firstBookUnread: bookInProgress ? null : libraryItemJson
|
||||||
@@ -553,6 +586,11 @@ module.exports = {
|
|||||||
seriesMap[librarySeries.id].firstBookUnread = libraryItemJson
|
seriesMap[librarySeries.id].firstBookUnread = libraryItemJson
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update if series has an active (progress < 100%) book
|
||||||
|
if (bookActive) {
|
||||||
|
seriesMap[librarySeries.id].hasActiveBook = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -595,7 +633,6 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Book listening and finished
|
// Book listening and finished
|
||||||
var mediaProgress = allItemProgress.length ? allItemProgress[0] : null
|
|
||||||
if (mediaProgress) {
|
if (mediaProgress) {
|
||||||
// Handle most recently finished
|
// Handle most recently finished
|
||||||
if (mediaProgress.isFinished) {
|
if (mediaProgress.isFinished) {
|
||||||
@@ -605,7 +642,7 @@ module.exports = {
|
|||||||
finishedAt: mediaProgress.finishedAt
|
finishedAt: mediaProgress.finishedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
var indexToPut = categoryMap.recentlyFinished.items.findIndex(i => mediaProgress.finishedAt > i.finishedAt)
|
const indexToPut = categoryMap.recentlyFinished.items.findIndex(i => mediaProgress.finishedAt > i.finishedAt)
|
||||||
if (indexToPut >= 0) {
|
if (indexToPut >= 0) {
|
||||||
categoryMap.recentlyFinished.items.splice(indexToPut, 0, libraryItemObj)
|
categoryMap.recentlyFinished.items.splice(indexToPut, 0, libraryItemObj)
|
||||||
} else {
|
} else {
|
||||||
@@ -625,7 +662,7 @@ module.exports = {
|
|||||||
progressLastUpdate: mediaProgress.lastUpdate
|
progressLastUpdate: mediaProgress.lastUpdate
|
||||||
}
|
}
|
||||||
|
|
||||||
var indexToPut = categoryMap.recentlyListened.items.findIndex(i => mediaProgress.lastUpdate > i.progressLastUpdate)
|
const indexToPut = categoryMap.recentlyListened.items.findIndex(i => mediaProgress.lastUpdate > i.progressLastUpdate)
|
||||||
if (indexToPut >= 0) {
|
if (indexToPut >= 0) {
|
||||||
categoryMap.recentlyListened.items.splice(indexToPut, 0, libraryItemObj)
|
categoryMap.recentlyListened.items.splice(indexToPut, 0, libraryItemObj)
|
||||||
} else { // Should only happen when array is < max
|
} else { // Should only happen when array is < max
|
||||||
@@ -645,13 +682,15 @@ module.exports = {
|
|||||||
|
|
||||||
// For Continue Series - Find next book in series for series that are in progress
|
// For Continue Series - Find next book in series for series that are in progress
|
||||||
for (const seriesId in seriesMap) {
|
for (const seriesId in seriesMap) {
|
||||||
if (seriesMap[seriesId].inProgress && !seriesMap[seriesId].hideFromContinueListening) {
|
seriesMap[seriesId].books = naturalSort(seriesMap[seriesId].books).asc(li => li.seriesSequence)
|
||||||
seriesMap[seriesId].books = naturalSort(seriesMap[seriesId].books).asc(li => li.seriesSequence)
|
|
||||||
|
|
||||||
// NEW implementation takes the first book unread with the smallest series sequence
|
if (seriesMap[seriesId].inProgress && !seriesMap[seriesId].hideFromContinueListening) {
|
||||||
|
// take the first book unread with the smallest series sequence
|
||||||
|
// unless the user is already listening to a book from this series
|
||||||
|
const hasActiveBook = seriesMap[seriesId].hasActiveBook
|
||||||
const nextBookInSeries = seriesMap[seriesId].firstBookUnread
|
const nextBookInSeries = seriesMap[seriesId].firstBookUnread
|
||||||
|
|
||||||
if (nextBookInSeries) {
|
if (!hasActiveBook && nextBookInSeries) {
|
||||||
const bookForContinueSeries = {
|
const bookForContinueSeries = {
|
||||||
...nextBookInSeries,
|
...nextBookInSeries,
|
||||||
prevBookInProgressLastUpdate: seriesMap[seriesId].bookInProgressLastUpdate
|
prevBookInProgressLastUpdate: seriesMap[seriesId].bookInProgressLastUpdate
|
||||||
@@ -663,10 +702,82 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const indexToPut = categoryMap.continueSeries.items.findIndex(i => i.prevBookInProgressLastUpdate < bookForContinueSeries.prevBookInProgressLastUpdate)
|
const indexToPut = categoryMap.continueSeries.items.findIndex(i => i.prevBookInProgressLastUpdate < bookForContinueSeries.prevBookInProgressLastUpdate)
|
||||||
|
if (!categoryMap.continueSeries.items.find(book => book.id === bookForContinueSeries.id)) {
|
||||||
|
if (indexToPut >= 0) {
|
||||||
|
categoryMap.continueSeries.items.splice(indexToPut, 0, bookForContinueSeries)
|
||||||
|
} else if (categoryMap.continueSeries.items.length < 10) { // Max 10 books
|
||||||
|
categoryMap.continueSeries.items.push(bookForContinueSeries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For recommended
|
||||||
|
if (!isPodcastLibrary && notStartedBooks.length) {
|
||||||
|
const genresCount = Object.values(topGenresListened).reduce((a, b) => a + b, 0)
|
||||||
|
const authorsCount = Object.values(topAuthorsListened).reduce((a, b) => a + b, 0)
|
||||||
|
const tagsCount = Object.values(topTagsListened).reduce((a, b) => a + b, 0)
|
||||||
|
|
||||||
|
for (const libraryItem of notStartedBooks) {
|
||||||
|
// dont include books in an unfinished series and books that are not first in an unstarted series
|
||||||
|
let shouldContinue = !libraryItem.media.metadata.series.length
|
||||||
|
libraryItem.media.metadata.series.forEach((se) => {
|
||||||
|
if (seriesMap[se.id]) {
|
||||||
|
if (seriesMap[se.id].inProgress) {
|
||||||
|
shouldContinue = false
|
||||||
|
return
|
||||||
|
} else if (seriesMap[se.id].books[0].id === libraryItem.id) {
|
||||||
|
shouldContinue = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (!shouldContinue) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalWeight = 0
|
||||||
|
|
||||||
|
if (authorsCount > 0) {
|
||||||
|
libraryItem.media.metadata.authors.forEach((author) => {
|
||||||
|
if (topAuthorsListened[author.id]) {
|
||||||
|
totalWeight += topAuthorsListened[author.id] / authorsCount
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (genresCount > 0) {
|
||||||
|
libraryItem.media.metadata.genres.forEach((genre) => {
|
||||||
|
if (topGenresListened[genre]) {
|
||||||
|
totalWeight += topGenresListened[genre] / genresCount
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tagsCount > 0) {
|
||||||
|
libraryItem.media.tags.forEach((tag) => {
|
||||||
|
if (topTagsListened[tag]) {
|
||||||
|
totalWeight += topTagsListened[tag] / tagsCount
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!categoryMap.recommended.smallest || totalWeight > categoryMap.recommended.smallest) {
|
||||||
|
const libraryItemObj = {
|
||||||
|
...libraryItem.toJSONMinified(),
|
||||||
|
weight: totalWeight
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexToPut = categoryMap.recommended.items.findIndex(i => totalWeight > i.weight)
|
||||||
if (indexToPut >= 0) {
|
if (indexToPut >= 0) {
|
||||||
categoryMap.continueSeries.items.splice(indexToPut, 0, bookForContinueSeries)
|
categoryMap.recommended.items.splice(indexToPut, 0, libraryItemObj)
|
||||||
} else if (categoryMap.continueSeries.items.length < 10) { // Max 10 books
|
} else {
|
||||||
categoryMap.continueSeries.items.push(bookForContinueSeries)
|
categoryMap.recommended.items.push(libraryItemObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categoryMap.recommended.items.length > maxEntitiesPerShelf) {
|
||||||
|
categoryMap.recommended.items.pop()
|
||||||
|
categoryMap.recommended.smallest = categoryMap.recommended.items[categoryMap.recommended.items.length - 1].weight
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user