mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-01-01 04:00:45 -05: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>
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
@@ -361,13 +361,15 @@ export default {
|
||||
}
|
||||
},
|
||||
streamProgress(data) {
|
||||
if (!data.numSegments) return
|
||||
var chunks = data.chunks
|
||||
console.log(`[StreamContainer] Stream Progress ${data.percent}`)
|
||||
if (this.$refs.audioPlayer) {
|
||||
this.$refs.audioPlayer.setChunksReady(chunks, data.numSegments)
|
||||
} else {
|
||||
console.error('No Audio Ref')
|
||||
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === data.stream) {
|
||||
if (!data.numSegments) return
|
||||
var chunks = data.chunks
|
||||
console.log(`[StreamContainer] Stream Progress ${data.percent}`)
|
||||
if (this.$refs.audioPlayer) {
|
||||
this.$refs.audioPlayer.setChunksReady(chunks, data.numSegments)
|
||||
} else {
|
||||
console.error('No Audio Ref')
|
||||
}
|
||||
}
|
||||
},
|
||||
sessionOpen(session) {
|
||||
|
||||
@@ -820,7 +820,6 @@ export default {
|
||||
return null
|
||||
})
|
||||
if (!libraryItem) return
|
||||
console.log('Got library itemn', libraryItem)
|
||||
this.store.commit('showEReader', libraryItem)
|
||||
},
|
||||
selectBtnClick(evt) {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<ui-dropdown v-model="mediaType" :items="mediaTypes" :label="$strings.LabelMediaType" :disabled="!isNew" small @input="changedMediaType" />
|
||||
</div>
|
||||
<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 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" />
|
||||
@@ -20,12 +20,12 @@
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<ui-btn class="w-full mt-2" color="primary" @click="browseForFolder">{{ $strings.ButtonBrowseForFolder }}</ui-btn>
|
||||
@@ -82,6 +82,19 @@ export default {
|
||||
}
|
||||
},
|
||||
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() {
|
||||
this.showDirectoryPicker = true
|
||||
},
|
||||
|
||||
@@ -144,8 +144,6 @@ export default {
|
||||
return true
|
||||
},
|
||||
submit() {
|
||||
if (!this.validate()) return
|
||||
|
||||
// If custom expression input is focused then unfocus it instead of submitting
|
||||
if (this.$refs.tabComponent && this.$refs.tabComponent.checkBlurExpressionInput) {
|
||||
if (this.$refs.tabComponent.checkBlurExpressionInput()) {
|
||||
@@ -153,6 +151,8 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.validate()) return
|
||||
|
||||
if (this.library) {
|
||||
this.submitUpdateLibrary()
|
||||
} else {
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<div class="absolute -bottom-2 left-0 flex ml-6">
|
||||
<template v-for="dayObj in last7Days">
|
||||
<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>
|
||||
</template>
|
||||
</div>
|
||||
@@ -108,6 +108,7 @@ export default {
|
||||
var _date = this.$addDaysToToday(i * -1)
|
||||
days.push({
|
||||
dayOfWeek: this.$formatJsDate(_date, 'EEEE'),
|
||||
dayOfWeekAbbr: this.$formatJsDate(_date, 'EEE'),
|
||||
date: this.$formatJsDate(_date, 'yyyy-MM-dd')
|
||||
})
|
||||
}
|
||||
@@ -218,4 +219,4 @@ export default {
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -68,7 +68,7 @@ export default {
|
||||
dayLabels() {
|
||||
return [
|
||||
{
|
||||
label: 'Mon',
|
||||
label: this.$formatJsDate(new Date(2023, 0, 2), 'EEE'),
|
||||
style: {
|
||||
transform: `translate(${-25}px, ${13}px)`,
|
||||
lineHeight: '10px',
|
||||
@@ -76,7 +76,7 @@ export default {
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Wed',
|
||||
label: this.$formatJsDate(new Date(2023, 0, 4), 'EEE'),
|
||||
style: {
|
||||
transform: `translate(${-25}px, ${13 * 3}px)`,
|
||||
lineHeight: '10px',
|
||||
@@ -84,7 +84,7 @@ export default {
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'Fri',
|
||||
label: this.$formatJsDate(new Date(2023, 0, 6), 'EEE'),
|
||||
style: {
|
||||
transform: `translate(${-25}px, ${13 * 5}px)`,
|
||||
lineHeight: '10px',
|
||||
@@ -270,4 +270,4 @@ export default {
|
||||
},
|
||||
beforeDestroy() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -38,10 +38,10 @@
|
||||
<div class="w-full flex justify-left">
|
||||
<!-- 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)">
|
||||
<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 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>
|
||||
</td>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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">
|
||||
<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'">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -137,31 +137,31 @@ export default {
|
||||
weekdays() {
|
||||
return [
|
||||
{
|
||||
text: this.$strings.WeekdaySunday,
|
||||
text: this.$formatJsDate(new Date(2023, 0, 1), 'EEEE'),
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
text: this.$strings.WeekdayMonday,
|
||||
text: this.$formatJsDate(new Date(2023, 0, 2), 'EEEE'),
|
||||
value: 1
|
||||
},
|
||||
{
|
||||
text: this.$strings.WeekdayTuesday,
|
||||
text: this.$formatJsDate(new Date(2023, 0, 3), 'EEEE'),
|
||||
value: 2
|
||||
},
|
||||
{
|
||||
text: this.$strings.WeekdayWednesday,
|
||||
text: this.$formatJsDate(new Date(2023, 0, 4), 'EEEE'),
|
||||
value: 3
|
||||
},
|
||||
{
|
||||
text: this.$strings.WeekdayThursday,
|
||||
text: this.$formatJsDate(new Date(2023, 0, 5), 'EEEE'),
|
||||
value: 4
|
||||
},
|
||||
{
|
||||
text: this.$strings.WeekdayFriday,
|
||||
text: this.$formatJsDate(new Date(2023, 0, 6), 'EEEE'),
|
||||
value: 5
|
||||
},
|
||||
{
|
||||
text: this.$strings.WeekdaySaturday,
|
||||
text: this.$formatJsDate(new Date(2023, 0, 7), 'EEEE'),
|
||||
value: 6
|
||||
}
|
||||
]
|
||||
|
||||
4
client/package-lock.json
generated
4
client/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.2.12",
|
||||
"version": "2.2.13",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.2.12",
|
||||
"version": "2.2.13",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.2.12",
|
||||
"version": "2.2.13",
|
||||
"description": "Self-hosted audiobook and podcast client",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
</template>
|
||||
</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">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
||||
</div>
|
||||
@@ -415,6 +415,8 @@ export default {
|
||||
})
|
||||
},
|
||||
durationPretty() {
|
||||
if (this.isPodcast) return this.$elapsedPrettyExtended(this.totalPodcastDuration)
|
||||
|
||||
if (!this.tracks.length && !this.audioFile) return 'N/A'
|
||||
if (this.audioFile) return this.$elapsedPrettyExtended(this.duration)
|
||||
return this.$elapsedPretty(this.duration)
|
||||
@@ -423,6 +425,12 @@ export default {
|
||||
if (!this.tracks.length && !this.audioFile) return 0
|
||||
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() {
|
||||
return this.$bytesPretty(this.media.size)
|
||||
},
|
||||
|
||||
@@ -5,18 +5,18 @@ import { supplant } from './utils'
|
||||
const defaultCode = 'en-us'
|
||||
|
||||
const languageCodeMap = {
|
||||
'de': 'Deutsch',
|
||||
'en-us': 'English',
|
||||
// 'es': 'Español',
|
||||
'fr': 'Français',
|
||||
'hr': 'Hrvatski',
|
||||
'it': 'Italiano',
|
||||
'pl': 'Polski',
|
||||
'zh-cn': '简体中文 (Simplified Chinese)'
|
||||
'de': { label: 'Deutsch', dateFnsLocale: 'de' },
|
||||
'en-us': { label: 'English', dateFnsLocale: 'enUS' },
|
||||
// 'es': { label: 'Español', dateFnsLocale: 'es' },
|
||||
'fr': { label: 'Français', dateFnsLocale: 'fr' },
|
||||
'hr': { label: 'Hrvatski', dateFnsLocale: 'hr' },
|
||||
'it': { label: 'Italiano', dateFnsLocale: 'it' },
|
||||
'pl': { label: 'Polski', dateFnsLocale: 'pl' },
|
||||
'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' },
|
||||
}
|
||||
Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => {
|
||||
return {
|
||||
text: languageCodeMap[code],
|
||||
text: languageCodeMap[code].label,
|
||||
value: code
|
||||
}
|
||||
})
|
||||
@@ -73,6 +73,8 @@ async function loadi18n(code) {
|
||||
for (const key in Vue.prototype.$strings) {
|
||||
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)
|
||||
Vue.prototype.$eventBus.$emit('change-lang', code)
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import Vue from 'vue'
|
||||
import Path from 'path'
|
||||
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.prototype.$setDateFnsLocale = (localeString) => {
|
||||
if (!locale[localeString]) return 0
|
||||
return setDefaultOptions({ locale: locale[localeString] })
|
||||
}
|
||||
Vue.prototype.$dateDistanceFromNow = (unixms) => {
|
||||
if (!unixms) return ''
|
||||
return formatDistance(unixms, Date.now(), { addSuffix: true })
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
"ButtonCreate": "Ertsellen",
|
||||
"ButtonCreateBackup": "Sicherung erstellen",
|
||||
"ButtonDelete": "Löschen",
|
||||
"ButtonEdit": "Edit",
|
||||
"ButtonEdit": "Bearbeiten",
|
||||
"ButtonEditChapters": "Kapitel bearbeiten",
|
||||
"ButtonEditPodcast": "Podcast bearbeiten",
|
||||
"ButtonForceReScan": "Erzwinge kompletten Neu-Scan",
|
||||
"ButtonForceReScan": "Komplett-Scan (alle Medien)",
|
||||
"ButtonFullPath": "Vollständiger Pfad",
|
||||
"ButtonHide": "Ausblenden",
|
||||
"ButtonHome": "Startseite",
|
||||
@@ -34,8 +34,8 @@
|
||||
"ButtonLookup": "Online-Suche",
|
||||
"ButtonManageTracks": "Tracks verwalten",
|
||||
"ButtonMapChapterTitles": "Kapitelüberschriften zuordnen",
|
||||
"ButtonMatchAllAuthors": "Online-Suche für alle Autoren",
|
||||
"ButtonMatchBooks": "Online-Suche für alle Hörbücher",
|
||||
"ButtonMatchAllAuthors": "Online Metadaten-Abgleich (alle Autoren)",
|
||||
"ButtonMatchBooks": "Online Metadaten-Abgleich (alle Medien)",
|
||||
"ButtonNevermind": "Vergiss es",
|
||||
"ButtonOk": "Ok",
|
||||
"ButtonOpenFeed": "Feed öffnen",
|
||||
@@ -61,7 +61,7 @@
|
||||
"ButtonSave": "Speichern",
|
||||
"ButtonSaveAndClose": "Speichern & Schließen",
|
||||
"ButtonSaveTracklist": "Speichere die Titelliste",
|
||||
"ButtonScan": "Scan",
|
||||
"ButtonScan": "Partial-Scan (nur geänderte/neue Medien)",
|
||||
"ButtonScanLibrary": "Bibliothek scannen",
|
||||
"ButtonSearch": "Suchen",
|
||||
"ButtonSelectFolderPath": "Auswahl Ordnerpfad",
|
||||
@@ -77,7 +77,7 @@
|
||||
"ButtonUploadCover": "Titelbild hochladen",
|
||||
"ButtonUploadOPMLFile": "OPML-Datei hochladen",
|
||||
"ButtonUserDelete": "Benutzer {0} löschen",
|
||||
"ButtonUserEdit": "Benutzer {0} editieren",
|
||||
"ButtonUserEdit": "Benutzer {0} bearbeiten",
|
||||
"ButtonViewAll": "Alles anzeigen",
|
||||
"ButtonYes": "Ja",
|
||||
"HeaderAccount": "Konto",
|
||||
@@ -111,7 +111,7 @@
|
||||
"HeaderManageGenres": "Kategorien verwalten",
|
||||
"HeaderManageTags": "Tags verwalten",
|
||||
"HeaderMapDetails": "Stapelverarbeitung",
|
||||
"HeaderMatch": "Online-Suche",
|
||||
"HeaderMatch": "Metadaten",
|
||||
"HeaderMetadataToEmbed": "Einzubettende Metadaten",
|
||||
"HeaderNewAccount": "Neues Konto",
|
||||
"HeaderNewLibrary": "Neue Bibliothek",
|
||||
@@ -308,6 +308,7 @@
|
||||
"LabelPublishYear": "Jahr",
|
||||
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
||||
"LabelRecentSeries": "Aktuelle Serien",
|
||||
"LabelRecommended": "Recommended",
|
||||
"LabelRegion": "Region",
|
||||
"LabelReleaseDate": "Veröffentlichungsdatum",
|
||||
"LabelRemoveCover": "Lösche Titelbild",
|
||||
@@ -333,23 +334,23 @@
|
||||
"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.",
|
||||
"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",
|
||||
"LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"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.",
|
||||
"LabelSettingsPreferOPFMetadata": "Bevorzuge OPF-Metadaten aus dem Hörbuchordner",
|
||||
"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.",
|
||||
"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",
|
||||
"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",
|
||||
"LabelSettingsSkipMatchingBooksWithISBN": "Überspringe beim Online-Abgleich alle Bücher die bereits eine ISBN haben",
|
||||
"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",
|
||||
"LabelSettingsSquareBookCoversHelp": "Bevorzugen quadratische Titelbilder gegenüber den Standardtielbildern im Verhältnis 1,6:1",
|
||||
"LabelSettingsStoreCoversWithItem": "Titelbilder im Medienordner speichern",
|
||||
@@ -390,10 +391,10 @@
|
||||
"LabelTitle": "Titel",
|
||||
"LabelToolsEmbedMetadata": "Metadaten einbetten",
|
||||
"LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodatein ein.",
|
||||
"LabelToolsMakeM4b": "M4B-Hörbuchdatei erstellen",
|
||||
"LabelToolsMakeM4bDescription": "Erstellt eine M4B-Hörbuchdatei mit eingebetteten Metadaten, Titelbild und Kapiteln.",
|
||||
"LabelToolsMakeM4b": "M4B-Datei erstellen",
|
||||
"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",
|
||||
"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",
|
||||
"LabelTotalTimeListened": "Gehörte Gesamtzeit",
|
||||
"LabelTrackFromFilename": "Titel von Dateiname",
|
||||
@@ -421,30 +422,30 @@
|
||||
"LabelViewQueue": "Spieler-Warteschlange anzeigen",
|
||||
"LabelVolume": "Volume",
|
||||
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
||||
"LabelYourAudiobookDuration": "Laufzeit Ihres Hörbuchs",
|
||||
"LabelYourAudiobookDuration": "Laufzeit Ihres Mediums",
|
||||
"LabelYourBookmarks": "Lesezeichen",
|
||||
"LabelYourPlaylists": "Eigene Wiedergabelisten",
|
||||
"LabelYourProgress": "Fortschritt",
|
||||
"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.",
|
||||
"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.",
|
||||
"MessageBookshelfNoCollections": "Es wurden noch keine Sammlungen erstellt",
|
||||
"MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für filter \"{0}: {1}\"",
|
||||
"MessageBookshelfNoRSSFeeds": "Keine RSS-Feeds geöffnet",
|
||||
"MessageBookshelfNoSeries": "Keine Serien vorhanden",
|
||||
"MessageChapterEndIsAfter": "Das Kapitelende liegt nach dem Ende Ihres Hörbuchs",
|
||||
"MessageChapterErrorFirstNotZero": "Das erste Kapitel muss bei 0 beginnen",
|
||||
"MessageChapterErrorStartGteDuration": "Die ungültige Startzeit darf nicht größer als die gesamte Hörbuchdauer sein",
|
||||
"MessageChapterErrorStartLtPrev": "Die ungültige Startzeit darf nicht größer oder gleich der Startzeit des vorherigen Kapitels sein",
|
||||
"MessageChapterStartIsAfter": "Der Kapitelanfang liegt nach dem Ende Ihres Hörbuchs",
|
||||
"MessageChapterEndIsAfter": "Ungültige Kapitelendzeit: Kapitelende > Mediumende (Kapitelende liegt nach dem Ende des Mediums)",
|
||||
"MessageChapterErrorFirstNotZero": "Ungültige Kapitelstartzeit: Das erste Kapitel muss bei 0 beginnen",
|
||||
"MessageChapterErrorStartGteDuration": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumlänge (Kapitelanfang liegt zeitlich nach dem Ende des Mediums -> Lösung: Kapitelanfang < Mediumlänge)",
|
||||
"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": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumende (Kapitelanfang liegt nach dem Ende des Mediums)",
|
||||
"MessageCheckingCron": "Überprüfe cron...",
|
||||
"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?",
|
||||
"MessageConfirmDeleteSession": "Sind Sie sicher, dass Sie diese Sitzung löschen möchten?",
|
||||
"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?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||
"MessageConfirmMarkSeriesFinished": "Sind Sie sicher, dass Sie alle Medien dieser Reihe als abgeschlossen markieren wollen?",
|
||||
"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?",
|
||||
"MessageConfirmRemoveEpisode": "Sind Sie sicher, dass Sie die Episode \"{0}\" löschen möchten?",
|
||||
"MessageConfirmRemoveEpisodes": "Sind Sie sicher, dass Sie {0} Episoden löschen wollen?",
|
||||
@@ -472,7 +473,7 @@
|
||||
"MessageLoadingFolders": "Lade Ordner...",
|
||||
"MessageM4BFailed": "M4B fehlgeschlagen!",
|
||||
"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",
|
||||
"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.",
|
||||
@@ -530,16 +531,16 @@
|
||||
"MessageValidCronExpression": "Gültiger cron-ausdruck",
|
||||
"MessageWatcherIsDisabledGlobally": "Überwachung ist in den Servereinstellungen global deaktiviert",
|
||||
"MessageXLibraryIsEmpty": "{0} Bibliothek ist leer!",
|
||||
"MessageYourAudiobookDurationIsLonger": "Die Dauer Ihres Hörbuchs ist länger als die gefundene Dauer",
|
||||
"MessageYourAudiobookDurationIsShorter": "Die Dauer Ihres Hörbuchs ist kürzer als die gefundene Dauer",
|
||||
"MessageYourAudiobookDurationIsLonger": "Die Dauer Ihres Mediums ist länger 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",
|
||||
"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.",
|
||||
"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.",
|
||||
"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.",
|
||||
"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.",
|
||||
"PlaceholderNewCollection": "Neuer Sammlungsname",
|
||||
"PlaceholderNewFolderPath": "Neuer Ordnerpfad",
|
||||
@@ -605,20 +606,13 @@
|
||||
"ToastRemoveItemFromCollectionSuccess": "Medium aus der Sammlung gelöscht",
|
||||
"ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden",
|
||||
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen",
|
||||
"ToastSeriesUpdateFailed": "Series update failed",
|
||||
"ToastSeriesUpdateSuccess": "Series update success",
|
||||
"ToastSeriesUpdateFailed": "Aktualisierung der Serien fehlgeschlagen",
|
||||
"ToastSeriesUpdateSuccess": "Serien aktualisiert",
|
||||
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden",
|
||||
"ToastSessionDeleteSuccess": "Sitzung gelöscht",
|
||||
"ToastSocketConnected": "Verbindung zum WebSocket hergestellt",
|
||||
"ToastSocketDisconnected": "Verbindung zum WebSocket verloren",
|
||||
"ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen",
|
||||
"ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden",
|
||||
"ToastUserDeleteSuccess": "Benutzer gelöscht",
|
||||
"WeekdayFriday": "Freitag",
|
||||
"WeekdayMonday": "Montag",
|
||||
"WeekdaySaturday": "Samstag",
|
||||
"WeekdaySunday": "Sonntag",
|
||||
"WeekdayThursday": "Donnerstag",
|
||||
"WeekdayTuesday": "Dienstag",
|
||||
"WeekdayWednesday": "Mittwoch"
|
||||
"ToastUserDeleteSuccess": "Benutzer gelöscht"
|
||||
}
|
||||
@@ -308,6 +308,7 @@
|
||||
"LabelPublishYear": "Publish Year",
|
||||
"LabelRecentlyAdded": "Recently Added",
|
||||
"LabelRecentSeries": "Recent Series",
|
||||
"LabelRecommended": "Recommended",
|
||||
"LabelRegion": "Region",
|
||||
"LabelReleaseDate": "Release Date",
|
||||
"LabelRemoveCover": "Remove cover",
|
||||
@@ -613,12 +614,5 @@
|
||||
"ToastSocketDisconnected": "Socket disconnected",
|
||||
"ToastSocketFailedToConnect": "Socket failed to connect",
|
||||
"ToastUserDeleteFailed": "Failed to delete user",
|
||||
"ToastUserDeleteSuccess": "User deleted",
|
||||
"WeekdayFriday": "Friday",
|
||||
"WeekdayMonday": "Monday",
|
||||
"WeekdaySaturday": "Saturday",
|
||||
"WeekdaySunday": "Sunday",
|
||||
"WeekdayThursday": "Thursday",
|
||||
"WeekdayTuesday": "Tuesday",
|
||||
"WeekdayWednesday": "Wednesday"
|
||||
"ToastUserDeleteSuccess": "User deleted"
|
||||
}
|
||||
@@ -308,6 +308,7 @@
|
||||
"LabelPublishYear": "Publish Year",
|
||||
"LabelRecentlyAdded": "Recently Added",
|
||||
"LabelRecentSeries": "Recent Series",
|
||||
"LabelRecommended": "Recommended",
|
||||
"LabelRegion": "Region",
|
||||
"LabelReleaseDate": "Release Date",
|
||||
"LabelRemoveCover": "Remove cover",
|
||||
@@ -613,12 +614,5 @@
|
||||
"ToastSocketDisconnected": "Socket disconnected",
|
||||
"ToastSocketFailedToConnect": "Socket failed to connect",
|
||||
"ToastUserDeleteFailed": "Failed to delete user",
|
||||
"ToastUserDeleteSuccess": "User deleted",
|
||||
"WeekdayFriday": "Friday",
|
||||
"WeekdayMonday": "Monday",
|
||||
"WeekdaySaturday": "Saturday",
|
||||
"WeekdaySunday": "Sunday",
|
||||
"WeekdayThursday": "Thursday",
|
||||
"WeekdayTuesday": "Tuesday",
|
||||
"WeekdayWednesday": "Wednesday"
|
||||
"ToastUserDeleteSuccess": "User deleted"
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
"ButtonCreate": "Créer",
|
||||
"ButtonCreateBackup": "Créer une Sauvegarde",
|
||||
"ButtonDelete": "Effacer",
|
||||
"ButtonEdit": "Edit",
|
||||
"ButtonEdit": "Editer",
|
||||
"ButtonEditChapters": "Editer Chapitre",
|
||||
"ButtonEditPodcast": "Editer Podcast",
|
||||
"ButtonForceReScan": "Forcer un Re-Scan",
|
||||
@@ -308,6 +308,7 @@
|
||||
"LabelPublishYear": "Année d'Edition",
|
||||
"LabelRecentlyAdded": "Derniers Ajouts",
|
||||
"LabelRecentSeries": "Séries Récentes",
|
||||
"LabelRecommended": "Recommended",
|
||||
"LabelRegion": "Région",
|
||||
"LabelReleaseDate": "Date de Parution",
|
||||
"LabelRemoveCover": "Supprimer la Couverture",
|
||||
@@ -613,12 +614,5 @@
|
||||
"ToastSocketDisconnected": "WebSocket déconnecté",
|
||||
"ToastSocketFailedToConnect": "Échec de la connexion WebSocket",
|
||||
"ToastUserDeleteFailed": "Échec de la suppression de l'utilisateur",
|
||||
"ToastUserDeleteSuccess": "Utilisateur supprimé",
|
||||
"WeekdayFriday": "Vendredi",
|
||||
"WeekdayMonday": "Lundi",
|
||||
"WeekdaySaturday": "Samedi",
|
||||
"WeekdaySunday": "Dimanche",
|
||||
"WeekdayThursday": "Jeudi",
|
||||
"WeekdayTuesday": "Mardi",
|
||||
"WeekdayWednesday": "Mercredi"
|
||||
"ToastUserDeleteSuccess": "Utilisateur supprimé"
|
||||
}
|
||||
@@ -308,6 +308,7 @@
|
||||
"LabelPublishYear": "Godina izdavanja",
|
||||
"LabelRecentlyAdded": "Nedavno dodano",
|
||||
"LabelRecentSeries": "Nedavne serije",
|
||||
"LabelRecommended": "Recommended",
|
||||
"LabelRegion": "Regija",
|
||||
"LabelReleaseDate": "Datum izlaska",
|
||||
"LabelRemoveCover": "Remove cover",
|
||||
@@ -613,12 +614,5 @@
|
||||
"ToastSocketDisconnected": "Socket disconnected",
|
||||
"ToastSocketFailedToConnect": "Socket failed to connect",
|
||||
"ToastUserDeleteFailed": "Neuspješno brisanje korisnika",
|
||||
"ToastUserDeleteSuccess": "Korisnik obrisan",
|
||||
"WeekdayFriday": "Petak",
|
||||
"WeekdayMonday": "Ponedjeljak",
|
||||
"WeekdaySaturday": "Subota",
|
||||
"WeekdaySunday": "Nedjelja",
|
||||
"WeekdayThursday": "Četvrtak",
|
||||
"WeekdayTuesday": "Utorak",
|
||||
"WeekdayWednesday": "Srijeda"
|
||||
"ToastUserDeleteSuccess": "Korisnik obrisan"
|
||||
}
|
||||
@@ -76,8 +76,8 @@
|
||||
"ButtonUploadBackup": "Carica Backup",
|
||||
"ButtonUploadCover": "Carica Cover",
|
||||
"ButtonUploadOPMLFile": "Carica File OPML",
|
||||
"ButtonUserDelete": "Delete user {0}",
|
||||
"ButtonUserEdit": "Edit user {0}",
|
||||
"ButtonUserDelete": "Cancella Utente {0}",
|
||||
"ButtonUserEdit": "Modifica Utente {0}",
|
||||
"ButtonViewAll": "Mostra Tutto",
|
||||
"ButtonYes": "Si",
|
||||
"HeaderAccount": "Account",
|
||||
@@ -244,7 +244,7 @@
|
||||
"LabelLastSeen": "Ultimi Visti",
|
||||
"LabelLastTime": "Ultima Volta",
|
||||
"LabelLastUpdate": "Ultimo Aggiornamento",
|
||||
"LabelLess": "Meno",
|
||||
"LabelLess": "Poco",
|
||||
"LabelLibrariesAccessibleToUser": "Librerie Accessibili agli Utenti",
|
||||
"LabelLibrary": "Libreria",
|
||||
"LabelLibraryItem": "Elementi della Library",
|
||||
@@ -262,7 +262,7 @@
|
||||
"LabelMinute": "Minuto",
|
||||
"LabelMissing": "Altro",
|
||||
"LabelMissingParts": "Parti rimantenti",
|
||||
"LabelMore": "Espandi",
|
||||
"LabelMore": "Molto",
|
||||
"LabelName": "Nome",
|
||||
"LabelNarrator": "Narratore",
|
||||
"LabelNarrators": "Narratori",
|
||||
@@ -308,6 +308,7 @@
|
||||
"LabelPublishYear": "Anno Pubblicazione",
|
||||
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
||||
"LabelRecentSeries": "Serie Recenti",
|
||||
"LabelRecommended": "Recommended",
|
||||
"LabelRegion": "Regione",
|
||||
"LabelReleaseDate": "Data Release",
|
||||
"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.",
|
||||
"MessageBatchQuickMatchDescription": "Quick Match tenterà di aggiungere copertine e metadati mancanti per gli elementi selezionati. Attiva l'opzione per consentire a Quick Match di sovrascrivere copertine e/o metadati esistenti.",
|
||||
"MessageBookshelfNoCollections": "Non hai ancora creato nessuna raccolta ",
|
||||
"MessageBookshelfNoResultsForFilter": "Nessul risultato per il filtro \"{0}: {1}\"",
|
||||
"MessageBookshelfNoResultsForFilter": "Nessun risultato per il filtro \"{0}: {1}\"",
|
||||
"MessageBookshelfNoRSSFeeds": "Nessun RSS feeds aperto",
|
||||
"MessageBookshelfNoSeries": "Non c'è nessuna Serie",
|
||||
"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}\"?",
|
||||
"MessageConfirmDeleteSession": "Sei sicuro di voler eliminare questa sessione?",
|
||||
"MessageConfirmForceReScan": "Sei sicuro di voler forzare una nuova scansione?",
|
||||
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||
"MessageConfirmMarkSeriesFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come completati?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come non completati?",
|
||||
"MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "Sei sicuro di voler rimuovere l'episodio \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "Sei sicuro di voler rimuovere {0} episodi?",
|
||||
@@ -453,7 +454,7 @@
|
||||
"MessageConfirmRenameGenreMergeNote": "Note: Questo genere esiste già quindi verra unito.",
|
||||
"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?",
|
||||
"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}\".",
|
||||
"MessageDownloadingEpisode": "Download episodio in corso",
|
||||
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
|
||||
@@ -600,25 +601,18 @@
|
||||
"ToastPlaylistUpdateFailed": "Aggiornamento Playlist Fallita",
|
||||
"ToastPlaylistUpdateSuccess": "Playlist Aggiornata",
|
||||
"ToastPodcastCreateFailed": "Errore Creazione podcast",
|
||||
"ToastPodcastCreateSuccess": "Podcast creato Correttamwnte",
|
||||
"ToastPodcastCreateSuccess": "Podcast creato Correttamente",
|
||||
"ToastRemoveItemFromCollectionFailed": "Errore rimozione file dalla Raccolta",
|
||||
"ToastRemoveItemFromCollectionSuccess": "Oggetto rimosso dalla Raccolta",
|
||||
"ToastRSSFeedCloseFailed": "Errore chiusura RSS feed",
|
||||
"ToastRSSFeedCloseSuccess": "RSS feed chiuso",
|
||||
"ToastSeriesUpdateFailed": "Series update failed",
|
||||
"ToastSeriesUpdateSuccess": "Series update success",
|
||||
"ToastSeriesUpdateFailed": "Aggiornaemnto Serie Fallito",
|
||||
"ToastSeriesUpdateSuccess": "Serie Aggornate",
|
||||
"ToastSessionDeleteFailed": "Errore eliminazione sessione",
|
||||
"ToastSessionDeleteSuccess": "Sessione cancellata",
|
||||
"ToastSocketConnected": "Socket connesso",
|
||||
"ToastSocketDisconnected": "Socket disconnesso",
|
||||
"ToastSocketFailedToConnect": "Socket non riesce a connettersi",
|
||||
"ToastUserDeleteFailed": "Errore eliminazione utente",
|
||||
"ToastUserDeleteSuccess": "Utente eliminato",
|
||||
"WeekdayFriday": "Venerdì",
|
||||
"WeekdayMonday": "Lunedì",
|
||||
"WeekdaySaturday": "Sabato",
|
||||
"WeekdaySunday": "Domenica",
|
||||
"WeekdayThursday": "Giovedi",
|
||||
"WeekdayTuesday": "Martedì",
|
||||
"WeekdayWednesday": "Mercoledì"
|
||||
"ToastUserDeleteSuccess": "Utente eliminato"
|
||||
}
|
||||
@@ -308,6 +308,7 @@
|
||||
"LabelPublishYear": "Rok publikacji",
|
||||
"LabelRecentlyAdded": "Niedawno dodany",
|
||||
"LabelRecentSeries": "Ostatnie serie",
|
||||
"LabelRecommended": "Recommended",
|
||||
"LabelRegion": "Region",
|
||||
"LabelReleaseDate": "Data wydania",
|
||||
"LabelRemoveCover": "Remove cover",
|
||||
@@ -613,12 +614,5 @@
|
||||
"ToastSocketDisconnected": "Połączenie z serwerem zostało zamknięte",
|
||||
"ToastSocketFailedToConnect": "Poączenie z serwerem nie powiodło się",
|
||||
"ToastUserDeleteFailed": "Nie udało się usunąć użytkownika",
|
||||
"ToastUserDeleteSuccess": "Użytkownik usunięty",
|
||||
"WeekdayFriday": "Piątek",
|
||||
"WeekdayMonday": "Poniedziałek",
|
||||
"WeekdaySaturday": "Sobota",
|
||||
"WeekdaySunday": "Niedziela",
|
||||
"WeekdayThursday": "Czwartek",
|
||||
"WeekdayTuesday": "Wtorek",
|
||||
"WeekdayWednesday": "Środa"
|
||||
"ToastUserDeleteSuccess": "Użytkownik usunięty"
|
||||
}
|
||||
@@ -20,7 +20,7 @@
|
||||
"ButtonCreate": "创建",
|
||||
"ButtonCreateBackup": "创建备份",
|
||||
"ButtonDelete": "删除",
|
||||
"ButtonEdit": "Edit",
|
||||
"ButtonEdit": "编辑",
|
||||
"ButtonEditChapters": "编辑章节",
|
||||
"ButtonEditPodcast": "编辑播客",
|
||||
"ButtonForceReScan": "强制重新扫描",
|
||||
@@ -76,8 +76,8 @@
|
||||
"ButtonUploadBackup": "上传备份",
|
||||
"ButtonUploadCover": "上传封面",
|
||||
"ButtonUploadOPMLFile": "上传 OPML 文件",
|
||||
"ButtonUserDelete": "Delete user {0}",
|
||||
"ButtonUserEdit": "Edit user {0}",
|
||||
"ButtonUserDelete": "删除用户 {0}",
|
||||
"ButtonUserEdit": "编辑用户 {0}",
|
||||
"ButtonViewAll": "查看全部",
|
||||
"ButtonYes": "确定",
|
||||
"HeaderAccount": "帐户",
|
||||
@@ -308,6 +308,7 @@
|
||||
"LabelPublishYear": "发布年份",
|
||||
"LabelRecentlyAdded": "最近添加",
|
||||
"LabelRecentSeries": "最近添加系列",
|
||||
"LabelRecommended": "推荐内容",
|
||||
"LabelRegion": "区域",
|
||||
"LabelReleaseDate": "发布日期",
|
||||
"LabelRemoveCover": "移除封面",
|
||||
@@ -443,8 +444,8 @@
|
||||
"MessageConfirmDeleteLibrary": "你确定要永久删除媒体库 \"{0}\"?",
|
||||
"MessageConfirmDeleteSession": "你确定要删除此会话吗?",
|
||||
"MessageConfirmForceReScan": "你确定要强制重新扫描吗?",
|
||||
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||
"MessageConfirmMarkSeriesFinished": "你确定要将此系列中的所有书籍都标记为已听完吗?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "你确定要将此系列中的所有书籍都标记为未听完吗?",
|
||||
"MessageConfirmRemoveCollection": "您确定要移除收藏 \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisode": "您确定要移除剧集 \"{0}\"?",
|
||||
"MessageConfirmRemoveEpisodes": "你确定要移除 {0} 剧集?",
|
||||
@@ -605,20 +606,13 @@
|
||||
"ToastRemoveItemFromCollectionSuccess": "项目已从收藏中删除",
|
||||
"ToastRSSFeedCloseFailed": "关闭 RSS 源失败",
|
||||
"ToastRSSFeedCloseSuccess": "RSS 源已关闭",
|
||||
"ToastSeriesUpdateFailed": "Series update failed",
|
||||
"ToastSeriesUpdateSuccess": "Series update success",
|
||||
"ToastSeriesUpdateFailed": "更新系列失败",
|
||||
"ToastSeriesUpdateSuccess": "系列已更新",
|
||||
"ToastSessionDeleteFailed": "删除会话失败",
|
||||
"ToastSessionDeleteSuccess": "会话已删除",
|
||||
"ToastSocketConnected": "网络已连接",
|
||||
"ToastSocketDisconnected": "网络已断开",
|
||||
"ToastSocketFailedToConnect": "网络连接失败",
|
||||
"ToastUserDeleteFailed": "删除用户失败",
|
||||
"ToastUserDeleteSuccess": "用户已删除",
|
||||
"WeekdayFriday": "星期五",
|
||||
"WeekdayMonday": "星期一",
|
||||
"WeekdaySaturday": "星期六",
|
||||
"WeekdaySunday": "星期日",
|
||||
"WeekdayThursday": "星期四",
|
||||
"WeekdayTuesday": "星期二",
|
||||
"WeekdayWednesday": "星期三"
|
||||
"ToastUserDeleteSuccess": "用户已删除"
|
||||
}
|
||||
@@ -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) -->
|
||||
<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">
|
||||
<style type="text/css">
|
||||
|
||||
<svg
|
||||
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;}
|
||||
.st1{fill:url(#SVGID_1_);}
|
||||
.st2{fill:#C9C9C9;}
|
||||
@@ -12,38 +28,82 @@
|
||||
.st6{font-family:'GentiumBasic';}
|
||||
.st7{font-size:305px;}
|
||||
</style>
|
||||
<title>bgAsset 6</title>
|
||||
<g id="Layer_2_1_">
|
||||
<g id="Layer_2-2">
|
||||
<g id="Layer_4">
|
||||
<g id="Layer_5">
|
||||
<circle class="st0" cx="618.6" cy="618.6" r="618.6"/>
|
||||
<title
|
||||
id="title72">bgAsset 6</title>
|
||||
<g
|
||||
id="Layer_2_1_">
|
||||
<g
|
||||
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>
|
||||
|
||||
<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)">
|
||||
<stop offset="0.32" style="stop-color:#CD9D49"/>
|
||||
<stop offset="0.99" style="stop-color:#875D27"/>
|
||||
<linearGradient
|
||||
id="SVGID_1_"
|
||||
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>
|
||||
<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>
|
||||
<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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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"/>
|
||||
<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
|
||||
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"/>
|
||||
<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
|
||||
v484.8c0,23.9,19.4,43.3,43.3,43.3H639.7z M568,514.6h98.7v10.3H568V514.6z"/>
|
||||
<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
|
||||
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"/>
|
||||
<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
|
||||
C294.5,999.3,309.1,984.7,327.1,984.7z"/>
|
||||
<path
|
||||
class="st0"
|
||||
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"
|
||||
id="path85" />
|
||||
<path
|
||||
class="st0"
|
||||
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"
|
||||
id="path87" />
|
||||
<path
|
||||
class="st0"
|
||||
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"
|
||||
id="path89" />
|
||||
<path
|
||||
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 id="Layer_6">
|
||||
<text transform="matrix(1 0 0 1 1492.27 735.42)" class="st2 st3 st4">audiobookshelf</text>
|
||||
<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>
|
||||
<g
|
||||
id="Layer_6">
|
||||
<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>
|
||||
</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 |
4
index.js
4
index.js
@@ -18,8 +18,8 @@ const PORT = process.env.PORT || 80
|
||||
const HOST = process.env.HOST
|
||||
const CONFIG_PATH = process.env.CONFIG_PATH || '/config'
|
||||
const METADATA_PATH = process.env.METADATA_PATH || '/metadata'
|
||||
const UID = process.env.AUDIOBOOKSHELF_UID || 99
|
||||
const GID = process.env.AUDIOBOOKSHELF_GID || 100
|
||||
const UID = process.env.AUDIOBOOKSHELF_UID
|
||||
const GID = process.env.AUDIOBOOKSHELF_GID
|
||||
const SOURCE = process.env.SOURCE || 'docker'
|
||||
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || ''
|
||||
|
||||
|
||||
189
package-lock.json
generated
189
package-lock.json
generated
@@ -1,20 +1,20 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.2.12",
|
||||
"version": "2.2.13",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.2.12",
|
||||
"version": "2.2.13",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"axios": "^0.26.1",
|
||||
"axios": "^1.2.2",
|
||||
"express": "^4.17.1",
|
||||
"graceful-fs": "^4.2.10",
|
||||
"htmlparser2": "^8.0.1",
|
||||
"node-tone": "^1.0.1",
|
||||
"socket.io": "^4.4.1",
|
||||
"socket.io": "^4.5.4",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"bin": {
|
||||
@@ -35,14 +35,17 @@
|
||||
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
|
||||
},
|
||||
"node_modules/@types/cors": {
|
||||
"version": "2.8.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz",
|
||||
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw=="
|
||||
"version": "2.8.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz",
|
||||
"integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.11.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
|
||||
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg=="
|
||||
"version": "18.11.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA=="
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "1.1.1",
|
||||
@@ -63,9 +66,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
|
||||
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
@@ -80,12 +83,19 @@
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"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": {
|
||||
"version": "0.26.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
|
||||
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.2.2.tgz",
|
||||
"integrity": "sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==",
|
||||
"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": {
|
||||
@@ -203,6 +213,17 @@
|
||||
"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": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -261,6 +282,14 @@
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
@@ -343,9 +372,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz",
|
||||
"integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz",
|
||||
"integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==",
|
||||
"dependencies": {
|
||||
"@types/cookie": "^0.4.1",
|
||||
"@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": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -925,6 +967,11 @@
|
||||
"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": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||
@@ -1078,9 +1125,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/simple-update-notifier": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz",
|
||||
"integrity": "sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz",
|
||||
"integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"semver": "~7.0.0"
|
||||
@@ -1099,16 +1146,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io": {
|
||||
"version": "4.5.3",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.3.tgz",
|
||||
"integrity": "sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg==",
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.4.tgz",
|
||||
"integrity": "sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ==",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io": "~6.2.0",
|
||||
"engine.io": "~6.2.1",
|
||||
"socket.io-adapter": "~2.4.0",
|
||||
"socket.io-parser": "~4.2.0"
|
||||
"socket.io-parser": "~4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
@@ -1320,14 +1367,17 @@
|
||||
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
|
||||
},
|
||||
"@types/cors": {
|
||||
"version": "2.8.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz",
|
||||
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw=="
|
||||
"version": "2.8.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz",
|
||||
"integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==",
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/node": {
|
||||
"version": "18.11.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
|
||||
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg=="
|
||||
"version": "18.11.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA=="
|
||||
},
|
||||
"abbrev": {
|
||||
"version": "1.1.1",
|
||||
@@ -1345,9 +1395,9 @@
|
||||
}
|
||||
},
|
||||
"anymatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
|
||||
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"normalize-path": "^3.0.0",
|
||||
@@ -1359,12 +1409,19 @@
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"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": {
|
||||
"version": "0.26.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
|
||||
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.2.2.tgz",
|
||||
"integrity": "sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.14.8"
|
||||
"follow-redirects": "^1.15.0",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"balanced-match": {
|
||||
@@ -1452,6 +1509,14 @@
|
||||
"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": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -1498,6 +1563,11 @@
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
@@ -1552,9 +1622,9 @@
|
||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
|
||||
},
|
||||
"engine.io": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz",
|
||||
"integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==",
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz",
|
||||
"integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==",
|
||||
"requires": {
|
||||
"@types/cookie": "^0.4.1",
|
||||
"@types/cors": "^2.8.12",
|
||||
@@ -1674,6 +1744,16 @@
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||
"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": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@@ -1966,6 +2046,11 @@
|
||||
"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": {
|
||||
"version": "1.1.8",
|
||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||
@@ -2080,9 +2165,9 @@
|
||||
}
|
||||
},
|
||||
"simple-update-notifier": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz",
|
||||
"integrity": "sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz",
|
||||
"integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"semver": "~7.0.0"
|
||||
@@ -2097,16 +2182,16 @@
|
||||
}
|
||||
},
|
||||
"socket.io": {
|
||||
"version": "4.5.3",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.3.tgz",
|
||||
"integrity": "sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg==",
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.4.tgz",
|
||||
"integrity": "sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ==",
|
||||
"requires": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io": "~6.2.0",
|
||||
"engine.io": "~6.2.1",
|
||||
"socket.io-adapter": "~2.4.0",
|
||||
"socket.io-parser": "~4.2.0"
|
||||
"socket.io-parser": "~4.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.2.12",
|
||||
"version": "2.2.13",
|
||||
"description": "Self-hosted audiobook and podcast server",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -30,12 +30,12 @@
|
||||
"author": "advplyr",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"axios": "^0.26.1",
|
||||
"axios": "^1.2.2",
|
||||
"express": "^4.17.1",
|
||||
"graceful-fs": "^4.2.10",
|
||||
"htmlparser2": "^8.0.1",
|
||||
"node-tone": "^1.0.1",
|
||||
"socket.io": "^4.4.1",
|
||||
"socket.io": "^4.5.4",
|
||||
"xml2js": "^0.4.23"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
9
prod.js
9
prod.js
@@ -20,12 +20,13 @@ var inputConfig = options.config ? Path.resolve(options.config) : null
|
||||
var inputMetadata = options.metadata ? Path.resolve(options.metadata) : null
|
||||
|
||||
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 METADATA_PATH = inputMetadata || process.env.METADATA_PATH || Path.resolve('metadata')
|
||||
const UID = 99
|
||||
const GID = 100
|
||||
const SOURCE = options.source || 'debian'
|
||||
const UID = process.env.AUDIOBOOKSHELF_UID
|
||||
const GID = process.env.AUDIOBOOKSHELF_GID
|
||||
const SOURCE = options.source || process.env.SOURCE || 'debian'
|
||||
|
||||
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || ''
|
||||
|
||||
console.log(process.env.NODE_ENV, 'Config', CONFIG_PATH, METADATA_PATH)
|
||||
|
||||
@@ -43,11 +43,12 @@ class Server {
|
||||
this.Host = HOST
|
||||
global.Source = SOURCE
|
||||
global.isWin = process.platform === 'win32'
|
||||
global.Uid = isNaN(UID) ? 0 : Number(UID)
|
||||
global.Gid = isNaN(GID) ? 0 : Number(GID)
|
||||
global.Uid = isNaN(UID) ? undefined : Number(UID)
|
||||
global.Gid = isNaN(GID) ? undefined : Number(GID)
|
||||
global.ConfigPath = fileUtils.filePathToPOSIX(Path.normalize(CONFIG_PATH))
|
||||
global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH))
|
||||
global.RouterBasePath = ROUTER_BASE_PATH
|
||||
global.XAccel = process.env.USE_X_ACCEL
|
||||
|
||||
if (!fs.pathExistsSync(global.ConfigPath)) {
|
||||
fs.mkdirSync(global.ConfigPath)
|
||||
@@ -142,6 +143,7 @@ class Server {
|
||||
const app = express()
|
||||
const router = express.Router()
|
||||
app.use(global.RouterBasePath, router)
|
||||
app.disable('x-powered-by')
|
||||
|
||||
this.server = http.createServer(app)
|
||||
|
||||
|
||||
@@ -319,7 +319,7 @@ class LibraryController {
|
||||
// series represents in the filtered series
|
||||
if (filterSeries) {
|
||||
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) => {
|
||||
let lastRange = ranges.at(-1)
|
||||
let isNumber = /^(\d+|\d+\.\d*|\d*\.\d+)$/.test(currentSequence)
|
||||
|
||||
@@ -201,6 +201,10 @@ class LibraryItemController {
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -192,7 +192,8 @@ class MeController {
|
||||
}
|
||||
const updatedLocalMediaProgress = []
|
||||
var numServerProgressUpdates = 0
|
||||
var localMediaProgress = req.body.localMediaProgress || []
|
||||
const updatedServerMediaProgress = []
|
||||
const localMediaProgress = req.body.localMediaProgress || []
|
||||
|
||||
localMediaProgress.forEach(localProgress => {
|
||||
if (!localProgress.libraryItemId) {
|
||||
@@ -205,18 +206,22 @@ class MeController {
|
||||
return
|
||||
}
|
||||
|
||||
var mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId)
|
||||
let mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId)
|
||||
if (!mediaProgress) {
|
||||
// New media progress from mobile
|
||||
Logger.debug(`[MeController] syncLocalMediaProgress local progress is new - creating ${localProgress.id}`)
|
||||
req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId)
|
||||
mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId)
|
||||
updatedServerMediaProgress.push(mediaProgress)
|
||||
numServerProgressUpdates++
|
||||
} else if (mediaProgress.lastUpdate < localProgress.lastUpdate) {
|
||||
Logger.debug(`[MeController] syncLocalMediaProgress local progress is more recent - updating ${mediaProgress.id}`)
|
||||
req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId)
|
||||
mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId)
|
||||
updatedServerMediaProgress.push(mediaProgress)
|
||||
numServerProgressUpdates++
|
||||
} 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}`)
|
||||
|
||||
for (const key in localProgress) {
|
||||
@@ -240,7 +245,8 @@ class MeController {
|
||||
|
||||
res.json({
|
||||
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,
|
||||
}
|
||||
|
||||
if (libraryItem.media.coverPath) {
|
||||
task.data.toneJsonObject['CoverFile'] = libraryItem.media.coverPath
|
||||
}
|
||||
|
||||
const workerData = {
|
||||
inputs: ffmpegInputs,
|
||||
options: ffmpegOptions,
|
||||
|
||||
@@ -60,7 +60,7 @@ class AudioMetadataMangaer {
|
||||
|
||||
const results = []
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -82,9 +82,9 @@ class AudioMetadataMangaer {
|
||||
SocketAuthority.emitter('audio_metadata_finished', itemAudioMetadataPayload)
|
||||
}
|
||||
|
||||
async updateAudioFileMetadataWithTone(libraryItemId, audioFile, toneJsonPath, itemCacheDir, backupFiles) {
|
||||
async updateAudioFileMetadataWithTone(libraryItem, audioFile, toneJsonPath, itemCacheDir, backupFiles) {
|
||||
const resultPayload = {
|
||||
libraryItemId,
|
||||
libraryItemId: libraryItem.id,
|
||||
index: audioFile.index,
|
||||
ino: audioFile.ino,
|
||||
filename: audioFile.metadata.filename
|
||||
@@ -107,6 +107,10 @@ class AudioMetadataMangaer {
|
||||
'TrackNumber': audioFile.index,
|
||||
}
|
||||
|
||||
if (libraryItem.media.coverPath) {
|
||||
_toneMetadataObject['CoverFile'] = libraryItem.media.coverPath
|
||||
}
|
||||
|
||||
resultPayload.success = await toneHelpers.tagAudioFile(audioFile.metadata.path, _toneMetadataObject)
|
||||
if (resultPayload.success) {
|
||||
Logger.info(`[AudioMetadataManager] Successfully tagged audio file "${audioFile.metadata.path}"`)
|
||||
|
||||
@@ -51,6 +51,11 @@ class CacheManager {
|
||||
|
||||
// Cache exists
|
||||
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 ps = new stream.PassThrough()
|
||||
stream.pipeline(r, ps, (err) => {
|
||||
@@ -72,6 +77,11 @@ class CacheManager {
|
||||
// Set owner and permissions of cache image
|
||||
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)
|
||||
readStream.pipe(res)
|
||||
}
|
||||
|
||||
@@ -73,6 +73,17 @@ class PlaybackSessionManager {
|
||||
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
|
||||
|
||||
let session = await this.db.getPlaybackSession(sessionJson.id)
|
||||
|
||||
@@ -81,9 +81,17 @@ class RssFeedManager {
|
||||
}
|
||||
|
||||
// Check if feed needs to be updated
|
||||
if (feed.entityType === 'item') {
|
||||
if (feed.entityType === 'libraryItem') {
|
||||
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}"`)
|
||||
feed.updateFromItem(libraryItem)
|
||||
await this.db.updateEntity('feed', feed)
|
||||
|
||||
@@ -110,13 +110,15 @@ class Feed {
|
||||
this.episodes = []
|
||||
if (isPodcast) { // PODCAST EPISODES
|
||||
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)
|
||||
this.episodes.push(feedEpisode)
|
||||
})
|
||||
} else { // AUDIOBOOK EPISODES
|
||||
media.tracks.forEach((audioTrack) => {
|
||||
var feedEpisode = new FeedEpisode()
|
||||
const feedEpisode = new FeedEpisode()
|
||||
feedEpisode.setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, this.meta)
|
||||
this.episodes.push(feedEpisode)
|
||||
})
|
||||
@@ -144,13 +146,15 @@ class Feed {
|
||||
this.episodes = []
|
||||
if (isPodcast) { // PODCAST EPISODES
|
||||
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)
|
||||
this.episodes.push(feedEpisode)
|
||||
})
|
||||
} else { // AUDIOBOOK EPISODES
|
||||
media.tracks.forEach((audioTrack) => {
|
||||
var feedEpisode = new FeedEpisode()
|
||||
const feedEpisode = new FeedEpisode()
|
||||
feedEpisode.setFromAudiobookTrack(libraryItem, this.serverAddress, this.slug, audioTrack, this.meta)
|
||||
this.episodes.push(feedEpisode)
|
||||
})
|
||||
|
||||
@@ -420,10 +420,38 @@ class Book {
|
||||
}
|
||||
}
|
||||
|
||||
// IF first audio file has embedded chapters then use embedded chapters
|
||||
if (includedAudioFiles[0].chapters && includedAudioFiles[0].chapters.length) {
|
||||
Logger.debug(`[Book] setChapters: Using embedded chapters in audio file ${includedAudioFiles[0].metadata.path}`)
|
||||
this.chapters = includedAudioFiles[0].chapters.map(c => ({ ...c }))
|
||||
// If first audio file has embedded chapters then use embedded chapters
|
||||
if (includedAudioFiles[0].chapters?.length) {
|
||||
// If all files chapters are the same, then only make chapters for the first file
|
||||
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) {
|
||||
// Build chapters from audio files
|
||||
this.chapters = []
|
||||
|
||||
@@ -37,7 +37,7 @@ class ServerSettings {
|
||||
this.loggerScannerLogsToKeep = 2
|
||||
|
||||
// Bookshelf Display
|
||||
this.homeBookshelfView = BookshelfView.STANDARD
|
||||
this.homeBookshelfView = BookshelfView.DETAIL
|
||||
this.bookshelfView = BookshelfView.DETAIL
|
||||
|
||||
// Podcasts
|
||||
|
||||
@@ -72,7 +72,7 @@ class MediaProgress {
|
||||
}
|
||||
|
||||
update(payload) {
|
||||
var hasUpdates = false
|
||||
let hasUpdates = false
|
||||
for (const key in payload) {
|
||||
if (this[key] !== undefined && payload[key] !== this[key]) {
|
||||
if (key === 'isFinished') {
|
||||
|
||||
@@ -314,18 +314,18 @@ class User {
|
||||
}
|
||||
|
||||
createUpdateMediaProgress(libraryItem, updatePayload, episodeId = null) {
|
||||
var itemProgress = this.mediaProgress.find(li => {
|
||||
const itemProgress = this.mediaProgress.find(li => {
|
||||
if (episodeId && li.episodeId !== episodeId) return false
|
||||
return li.libraryItemId === libraryItem.id
|
||||
})
|
||||
if (!itemProgress) {
|
||||
var newItemProgress = new MediaProgress()
|
||||
const newItemProgress = new MediaProgress()
|
||||
|
||||
newItemProgress.setData(libraryItem.id, updatePayload, episodeId)
|
||||
this.mediaProgress.push(newItemProgress)
|
||||
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
|
||||
return wasUpdated
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const express = require('express')
|
||||
const Path = require('path')
|
||||
const Logger = require('../Logger')
|
||||
const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils')
|
||||
|
||||
class StaticRouter {
|
||||
@@ -13,13 +14,18 @@ class StaticRouter {
|
||||
init() {
|
||||
// Library Item static file routes
|
||||
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)
|
||||
|
||||
var remainingPath = req.params['0']
|
||||
var fullPath = null
|
||||
if (item.isFile) fullPath = item.path
|
||||
else fullPath = Path.join(item.path, remainingPath)
|
||||
const remainingPath = req.params['0']
|
||||
const fullPath = item.isFile ? item.path : 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 = {}
|
||||
|
||||
|
||||
@@ -91,7 +91,11 @@ module.exports.setDefault = (path, silent = false) => {
|
||||
const uid = global.Uid
|
||||
const gid = global.Gid
|
||||
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)
|
||||
})
|
||||
}
|
||||
@@ -102,6 +106,10 @@ module.exports.setDefaultDirSync = (path, silent = false) => {
|
||||
const mode = 0o744
|
||||
const uid = global.Uid
|
||||
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}"`)
|
||||
try {
|
||||
fs.chmodSync(path, mode)
|
||||
|
||||
@@ -10,7 +10,7 @@ function sanitize(html) {
|
||||
allowedAttributes: {
|
||||
a: ['href', 'name', 'target']
|
||||
},
|
||||
allowedSchemes: ['https'],
|
||||
allowedSchemes: ['http', 'https', 'mailto'],
|
||||
allowProtocolRelative: false
|
||||
}
|
||||
|
||||
|
||||
@@ -339,6 +339,14 @@ module.exports = {
|
||||
entities: [],
|
||||
category: 'continueSeries'
|
||||
},
|
||||
{
|
||||
id: 'episodes-recently-added',
|
||||
label: 'Newest Episodes',
|
||||
labelStringKey: 'LabelNewestEpisodes',
|
||||
type: 'episode',
|
||||
entities: [],
|
||||
category: 'newestEpisodes'
|
||||
},
|
||||
{
|
||||
id: 'recently-added',
|
||||
label: 'Recently Added',
|
||||
@@ -347,14 +355,6 @@ module.exports = {
|
||||
entities: [],
|
||||
category: 'newestItems'
|
||||
},
|
||||
{
|
||||
id: 'listen-again',
|
||||
label: 'Listen Again',
|
||||
labelStringKey: 'LabelListenAgain',
|
||||
type: isPodcastLibrary ? 'episode' : mediaType,
|
||||
entities: [],
|
||||
category: 'recentlyFinished'
|
||||
},
|
||||
{
|
||||
id: 'recent-series',
|
||||
label: 'Recent Series',
|
||||
@@ -363,6 +363,22 @@ module.exports = {
|
||||
entities: [],
|
||||
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',
|
||||
label: 'Newest Authors',
|
||||
@@ -370,22 +386,13 @@ module.exports = {
|
||||
type: 'authors',
|
||||
entities: [],
|
||||
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 = {}
|
||||
categories.forEach((cat) => {
|
||||
categoryMap[cat] = {
|
||||
category: cat,
|
||||
shelves.forEach((shelf) => {
|
||||
categoryMap[shelf.category] = {
|
||||
category: shelf.category,
|
||||
biggest: 0,
|
||||
smallest: 0,
|
||||
items: []
|
||||
@@ -395,6 +402,12 @@ module.exports = {
|
||||
const seriesMap = {}
|
||||
const authorMap = {}
|
||||
|
||||
// For use with recommended
|
||||
const topGenresListened = {}
|
||||
const topAuthorsListened = {}
|
||||
const topTagsListened = {}
|
||||
const notStartedBooks = []
|
||||
|
||||
for (const libraryItem of libraryItems) {
|
||||
if (libraryItem.addedAt > categoryMap.newestItems.smallest) {
|
||||
|
||||
@@ -494,11 +507,30 @@ module.exports = {
|
||||
} else if (libraryItem.isBook) {
|
||||
// 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
|
||||
if (libraryItem.media.metadata.series.length) {
|
||||
for (const librarySeries of libraryItem.media.metadata.series) {
|
||||
const mediaProgress = allItemProgress.length ? allItemProgress[0] : null
|
||||
|
||||
const bookInProgress = mediaProgress && (mediaProgress.inProgress || mediaProgress.isFinished)
|
||||
const bookActive = mediaProgress && mediaProgress.inProgress && !mediaProgress.isFinished
|
||||
const libraryItemJson = libraryItem.toJSONMinified()
|
||||
libraryItemJson.seriesSequence = librarySeries.sequence
|
||||
|
||||
@@ -511,6 +543,7 @@ module.exports = {
|
||||
...seriesObj.toJSON(),
|
||||
books: [libraryItemJson],
|
||||
inProgress: bookInProgress,
|
||||
hasActiveBook: bookActive,
|
||||
hideFromContinueListening,
|
||||
bookInProgressLastUpdate: bookInProgress ? mediaProgress.lastUpdate : null,
|
||||
firstBookUnread: bookInProgress ? null : libraryItemJson
|
||||
@@ -553,6 +586,11 @@ module.exports = {
|
||||
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
|
||||
var mediaProgress = allItemProgress.length ? allItemProgress[0] : null
|
||||
if (mediaProgress) {
|
||||
// Handle most recently finished
|
||||
if (mediaProgress.isFinished) {
|
||||
@@ -605,7 +642,7 @@ module.exports = {
|
||||
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) {
|
||||
categoryMap.recentlyFinished.items.splice(indexToPut, 0, libraryItemObj)
|
||||
} else {
|
||||
@@ -625,7 +662,7 @@ module.exports = {
|
||||
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) {
|
||||
categoryMap.recentlyListened.items.splice(indexToPut, 0, libraryItemObj)
|
||||
} 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 (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
|
||||
|
||||
if (nextBookInSeries) {
|
||||
if (!hasActiveBook && nextBookInSeries) {
|
||||
const bookForContinueSeries = {
|
||||
...nextBookInSeries,
|
||||
prevBookInProgressLastUpdate: seriesMap[seriesId].bookInProgressLastUpdate
|
||||
@@ -663,10 +702,82 @@ module.exports = {
|
||||
}
|
||||
|
||||
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) {
|
||||
categoryMap.continueSeries.items.splice(indexToPut, 0, bookForContinueSeries)
|
||||
} else if (categoryMap.continueSeries.items.length < 10) { // Max 10 books
|
||||
categoryMap.continueSeries.items.push(bookForContinueSeries)
|
||||
categoryMap.recommended.items.splice(indexToPut, 0, libraryItemObj)
|
||||
} else {
|
||||
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