mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-06-03 13:44:36 -04:00
Merge branch 'advplyr:master' into ffmpeg-progress
This commit is contained in:
commit
7faf42d892
@ -167,8 +167,19 @@ export default {
|
|||||||
this.loaded = true
|
this.loaded = true
|
||||||
},
|
},
|
||||||
async fetchCategories() {
|
async fetchCategories() {
|
||||||
|
// Sets the limit for the number of items to be displayed based on the viewport width.
|
||||||
|
const viewportWidth = window.innerWidth
|
||||||
|
let limit
|
||||||
|
if (viewportWidth >= 3240) {
|
||||||
|
limit = 15
|
||||||
|
} else if (viewportWidth >= 2880 && viewportWidth < 3240) {
|
||||||
|
limit = 12
|
||||||
|
}
|
||||||
|
|
||||||
|
const limitQuery = limit ? `&limit=${limit}` : ''
|
||||||
|
|
||||||
const categories = await this.$axios
|
const categories = await this.$axios
|
||||||
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed,numEpisodesIncomplete,share`)
|
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed,numEpisodesIncomplete,share${limitQuery}`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
|
@ -114,9 +114,9 @@ export default {
|
|||||||
|
|
||||||
if (this.currentLibraryId) {
|
if (this.currentLibraryId) {
|
||||||
configRoutes.push({
|
configRoutes.push({
|
||||||
id: 'config-library-stats',
|
id: 'library-stats',
|
||||||
title: this.$strings.HeaderLibraryStats,
|
title: this.$strings.HeaderLibraryStats,
|
||||||
path: '/config/library-stats'
|
path: `/library/${this.currentLibraryId}/stats`
|
||||||
})
|
})
|
||||||
configRoutes.push({
|
configRoutes.push({
|
||||||
id: 'config-stats',
|
id: 'config-stats',
|
||||||
@ -182,4 +182,4 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -35,11 +35,13 @@
|
|||||||
<player-ui
|
<player-ui
|
||||||
ref="audioPlayer"
|
ref="audioPlayer"
|
||||||
:chapters="chapters"
|
:chapters="chapters"
|
||||||
|
:current-chapter="currentChapter"
|
||||||
:paused="!isPlaying"
|
:paused="!isPlaying"
|
||||||
:loading="playerLoading"
|
:loading="playerLoading"
|
||||||
:bookmarks="bookmarks"
|
:bookmarks="bookmarks"
|
||||||
:sleep-timer-set="sleepTimerSet"
|
:sleep-timer-set="sleepTimerSet"
|
||||||
:sleep-timer-remaining="sleepTimerRemaining"
|
:sleep-timer-remaining="sleepTimerRemaining"
|
||||||
|
:sleep-timer-type="sleepTimerType"
|
||||||
:is-podcast="isPodcast"
|
:is-podcast="isPodcast"
|
||||||
@playPause="playPause"
|
@playPause="playPause"
|
||||||
@jumpForward="jumpForward"
|
@jumpForward="jumpForward"
|
||||||
@ -51,13 +53,16 @@
|
|||||||
@showBookmarks="showBookmarks"
|
@showBookmarks="showBookmarks"
|
||||||
@showSleepTimer="showSleepTimerModal = true"
|
@showSleepTimer="showSleepTimerModal = true"
|
||||||
@showPlayerQueueItems="showPlayerQueueItemsModal = true"
|
@showPlayerQueueItems="showPlayerQueueItemsModal = true"
|
||||||
|
@showPlayerSettings="showPlayerSettingsModal = true"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="bookmarkCurrentTime" :library-item-id="libraryItemId" @select="selectBookmark" />
|
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="bookmarkCurrentTime" :library-item-id="libraryItemId" @select="selectBookmark" />
|
||||||
|
|
||||||
<modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-time="sleepTimerTime" :remaining="sleepTimerRemaining" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" />
|
<modals-sleep-timer-modal v-model="showSleepTimerModal" :timer-set="sleepTimerSet" :timer-type="sleepTimerType" :remaining="sleepTimerRemaining" :has-chapters="!!chapters.length" @set="setSleepTimer" @cancel="cancelSleepTimer" @increment="incrementSleepTimer" @decrement="decrementSleepTimer" />
|
||||||
|
|
||||||
<modals-player-queue-items-modal v-model="showPlayerQueueItemsModal" :library-item-id="libraryItemId" />
|
<modals-player-queue-items-modal v-model="showPlayerQueueItemsModal" :library-item-id="libraryItemId" />
|
||||||
|
|
||||||
|
<modals-player-settings-modal v-model="showPlayerSettingsModal" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -76,9 +81,10 @@ export default {
|
|||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
showSleepTimerModal: false,
|
showSleepTimerModal: false,
|
||||||
showPlayerQueueItemsModal: false,
|
showPlayerQueueItemsModal: false,
|
||||||
|
showPlayerSettingsModal: false,
|
||||||
sleepTimerSet: false,
|
sleepTimerSet: false,
|
||||||
sleepTimerTime: 0,
|
|
||||||
sleepTimerRemaining: 0,
|
sleepTimerRemaining: 0,
|
||||||
|
sleepTimerType: null,
|
||||||
sleepTimer: null,
|
sleepTimer: null,
|
||||||
displayTitle: null,
|
displayTitle: null,
|
||||||
currentPlaybackRate: 1,
|
currentPlaybackRate: 1,
|
||||||
@ -145,6 +151,9 @@ export default {
|
|||||||
if (this.streamEpisode) return this.streamEpisode.chapters || []
|
if (this.streamEpisode) return this.streamEpisode.chapters || []
|
||||||
return this.media.chapters || []
|
return this.media.chapters || []
|
||||||
},
|
},
|
||||||
|
currentChapter() {
|
||||||
|
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
|
||||||
|
},
|
||||||
title() {
|
title() {
|
||||||
if (this.playerHandler.displayTitle) return this.playerHandler.displayTitle
|
if (this.playerHandler.displayTitle) return this.playerHandler.displayTitle
|
||||||
return this.mediaMetadata.title || 'No Title'
|
return this.mediaMetadata.title || 'No Title'
|
||||||
@ -204,14 +213,18 @@ export default {
|
|||||||
this.$store.commit('setIsPlaying', isPlaying)
|
this.$store.commit('setIsPlaying', isPlaying)
|
||||||
this.updateMediaSessionPlaybackState()
|
this.updateMediaSessionPlaybackState()
|
||||||
},
|
},
|
||||||
setSleepTimer(seconds) {
|
setSleepTimer(time) {
|
||||||
this.sleepTimerSet = true
|
this.sleepTimerSet = true
|
||||||
this.sleepTimerTime = seconds
|
|
||||||
this.sleepTimerRemaining = seconds
|
|
||||||
this.runSleepTimer()
|
|
||||||
this.showSleepTimerModal = false
|
this.showSleepTimerModal = false
|
||||||
|
|
||||||
|
this.sleepTimerType = time.timerType
|
||||||
|
if (this.sleepTimerType === this.$constants.SleepTimerTypes.COUNTDOWN) {
|
||||||
|
this.runSleepTimer(time)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
runSleepTimer() {
|
runSleepTimer(time) {
|
||||||
|
this.sleepTimerRemaining = time.seconds
|
||||||
|
|
||||||
var lastTick = Date.now()
|
var lastTick = Date.now()
|
||||||
clearInterval(this.sleepTimer)
|
clearInterval(this.sleepTimer)
|
||||||
this.sleepTimer = setInterval(() => {
|
this.sleepTimer = setInterval(() => {
|
||||||
@ -220,12 +233,23 @@ export default {
|
|||||||
this.sleepTimerRemaining -= elapsed / 1000
|
this.sleepTimerRemaining -= elapsed / 1000
|
||||||
|
|
||||||
if (this.sleepTimerRemaining <= 0) {
|
if (this.sleepTimerRemaining <= 0) {
|
||||||
this.clearSleepTimer()
|
this.sleepTimerEnd()
|
||||||
this.playerHandler.pause()
|
|
||||||
this.$toast.info('Sleep Timer Done.. zZzzZz')
|
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
},
|
},
|
||||||
|
checkChapterEnd(time) {
|
||||||
|
if (!this.currentChapter) return
|
||||||
|
const chapterEndTime = this.currentChapter.end
|
||||||
|
const tolerance = 0.75
|
||||||
|
if (time >= chapterEndTime - tolerance) {
|
||||||
|
this.sleepTimerEnd()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sleepTimerEnd() {
|
||||||
|
this.clearSleepTimer()
|
||||||
|
this.playerHandler.pause()
|
||||||
|
this.$toast.info('Sleep Timer Done.. zZzzZz')
|
||||||
|
},
|
||||||
cancelSleepTimer() {
|
cancelSleepTimer() {
|
||||||
this.showSleepTimerModal = false
|
this.showSleepTimerModal = false
|
||||||
this.clearSleepTimer()
|
this.clearSleepTimer()
|
||||||
@ -235,6 +259,7 @@ export default {
|
|||||||
this.sleepTimerRemaining = 0
|
this.sleepTimerRemaining = 0
|
||||||
this.sleepTimer = null
|
this.sleepTimer = null
|
||||||
this.sleepTimerSet = false
|
this.sleepTimerSet = false
|
||||||
|
this.sleepTimerType = null
|
||||||
},
|
},
|
||||||
incrementSleepTimer(amount) {
|
incrementSleepTimer(amount) {
|
||||||
if (!this.sleepTimerSet) return
|
if (!this.sleepTimerSet) return
|
||||||
@ -275,6 +300,10 @@ export default {
|
|||||||
if (this.$refs.audioPlayer) {
|
if (this.$refs.audioPlayer) {
|
||||||
this.$refs.audioPlayer.setCurrentTime(time)
|
this.$refs.audioPlayer.setCurrentTime(time)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.sleepTimerType === this.$constants.SleepTimerTypes.CHAPTER && this.sleepTimerSet) {
|
||||||
|
this.checkChapterEnd(time)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
setDuration(duration) {
|
setDuration(duration) {
|
||||||
this.totalDuration = duration
|
this.totalDuration = duration
|
||||||
|
@ -79,6 +79,14 @@
|
|||||||
<div v-show="isNarratorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isNarratorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
|
<nuxt-link v-if="isBookLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/stats`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isStatsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
|
<span class="material-symbols text-2xl">monitoring</span>
|
||||||
|
|
||||||
|
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonStats }}</p>
|
||||||
|
|
||||||
|
<div v-show="isStatsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="abs-icons icon-podcast text-xl"></span>
|
<span class="abs-icons icon-podcast text-xl"></span>
|
||||||
|
|
||||||
@ -103,7 +111,7 @@
|
|||||||
<div v-show="isPodcastDownloadQueuePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isPodcastDownloadQueuePage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : ' bg-error bg-opacity-20'">
|
<nuxt-link v-if="numIssues" :to="`/library/${currentLibraryId}/bookshelf?filter=issues`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-opacity-40 cursor-pointer relative" :class="showingIssues ? 'bg-error bg-opacity-40' : 'bg-error bg-opacity-20'">
|
||||||
<span class="material-symbols text-2xl">warning</span>
|
<span class="material-symbols text-2xl">warning</span>
|
||||||
|
|
||||||
<p class="pt-1.5 text-center leading-4" style="font-size: 1rem">{{ $strings.ButtonIssues }}</p>
|
<p class="pt-1.5 text-center leading-4" style="font-size: 1rem">{{ $strings.ButtonIssues }}</p>
|
||||||
@ -194,6 +202,9 @@ export default {
|
|||||||
isPlaylistsPage() {
|
isPlaylistsPage() {
|
||||||
return this.paramId === 'playlists'
|
return this.paramId === 'playlists'
|
||||||
},
|
},
|
||||||
|
isStatsPage() {
|
||||||
|
return this.$route.name === 'library-library-stats'
|
||||||
|
},
|
||||||
libraryBookshelfPage() {
|
libraryBookshelfPage() {
|
||||||
return this.$route.name === 'library-library-bookshelf-id'
|
return this.$route.name === 'library-library-bookshelf-id'
|
||||||
},
|
},
|
||||||
|
@ -81,16 +81,16 @@ export default {
|
|||||||
return this.store.getters['user/getSizeMultiplier']
|
return this.store.getters['user/getSizeMultiplier']
|
||||||
},
|
},
|
||||||
seriesId() {
|
seriesId() {
|
||||||
return this.series ? this.series.id : ''
|
return this.series?.id || ''
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.series ? this.series.name : ''
|
return this.series?.name || ''
|
||||||
},
|
},
|
||||||
nameIgnorePrefix() {
|
nameIgnorePrefix() {
|
||||||
return this.series ? this.series.nameIgnorePrefix : ''
|
return this.series?.nameIgnorePrefix || ''
|
||||||
},
|
},
|
||||||
displayTitle() {
|
displayTitle() {
|
||||||
if (this.sortingIgnorePrefix) return this.nameIgnorePrefix || this.title
|
if (this.sortingIgnorePrefix) return this.nameIgnorePrefix || this.title || '\u00A0'
|
||||||
return this.title || '\u00A0'
|
return this.title || '\u00A0'
|
||||||
},
|
},
|
||||||
displaySortLine() {
|
displaySortLine() {
|
||||||
@ -110,13 +110,13 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
books() {
|
books() {
|
||||||
return this.series ? this.series.books || [] : []
|
return this.series?.books || []
|
||||||
},
|
},
|
||||||
addedAt() {
|
addedAt() {
|
||||||
return this.series ? this.series.addedAt : 0
|
return this.series?.addedAt || 0
|
||||||
},
|
},
|
||||||
totalDuration() {
|
totalDuration() {
|
||||||
return this.series ? this.series.totalDuration : 0
|
return this.series?.totalDuration || 0
|
||||||
},
|
},
|
||||||
seriesBookProgress() {
|
seriesBookProgress() {
|
||||||
return this.books
|
return this.books
|
||||||
@ -161,7 +161,7 @@ export default {
|
|||||||
return this.bookshelfView == constants.BookshelfView.DETAIL
|
return this.bookshelfView == constants.BookshelfView.DETAIL
|
||||||
},
|
},
|
||||||
rssFeed() {
|
rssFeed() {
|
||||||
return this.series ? this.series.rssFeed : null
|
return this.series?.rssFeed
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
70
client/components/modals/PlayerSettingsModal.vue
Normal file
70
client/components/modals/PlayerSettingsModal.vue
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<template>
|
||||||
|
<modals-modal v-model="show" name="player-settings" :width="500" :height="'unset'">
|
||||||
|
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden p-4" style="max-height: 80vh; min-height: 40vh">
|
||||||
|
<h3 class="text-xl font-semibold mb-8">{{ $strings.HeaderPlayerSettings }}</h3>
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<ui-toggle-switch v-model="useChapterTrack" @input="setUseChapterTrack" />
|
||||||
|
<div class="pl-4">
|
||||||
|
<span>{{ $strings.LabelUseChapterTrack }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
<ui-select-input v-model="jumpForwardAmount" :label="$strings.LabelJumpForwardAmount" menuMaxHeight="250px" :items="jumpValues" @input="setJumpForwardAmount" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<ui-select-input v-model="jumpBackwardAmount" :label="$strings.LabelJumpBackwardAmount" menuMaxHeight="250px" :items="jumpValues" @input="setJumpBackwardAmount" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modals-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: Boolean
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
useChapterTrack: false,
|
||||||
|
jumpValues: [
|
||||||
|
{ text: this.$getString('LabelTimeDurationXSeconds', ['10']), value: 10 },
|
||||||
|
{ text: this.$getString('LabelTimeDurationXSeconds', ['15']), value: 15 },
|
||||||
|
{ text: this.$getString('LabelTimeDurationXSeconds', ['30']), value: 30 },
|
||||||
|
{ text: this.$getString('LabelTimeDurationXSeconds', ['60']), value: 60 },
|
||||||
|
{ text: this.$getString('LabelTimeDurationXMinutes', ['2']), value: 120 },
|
||||||
|
{ text: this.$getString('LabelTimeDurationXMinutes', ['5']), value: 300 }
|
||||||
|
],
|
||||||
|
jumpForwardAmount: 10,
|
||||||
|
jumpBackwardAmount: 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setUseChapterTrack() {
|
||||||
|
this.$store.dispatch('user/updateUserSettings', { useChapterTrack: this.useChapterTrack })
|
||||||
|
},
|
||||||
|
setJumpForwardAmount(val) {
|
||||||
|
this.jumpForwardAmount = val
|
||||||
|
this.$store.dispatch('user/updateUserSettings', { jumpForwardAmount: val })
|
||||||
|
},
|
||||||
|
setJumpBackwardAmount(val) {
|
||||||
|
this.jumpBackwardAmount = val
|
||||||
|
this.$store.dispatch('user/updateUserSettings', { jumpBackwardAmount: val })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack')
|
||||||
|
this.jumpForwardAmount = this.$store.getters['user/getUserSetting']('jumpForwardAmount')
|
||||||
|
this.jumpBackwardAmount = this.$store.getters['user/getUserSetting']('jumpBackwardAmount')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -6,34 +6,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div ref="container" class="w-full rounded-lg bg-primary box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
<div ref="container" class="w-full rounded-lg bg-bg box-shadow-md overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
|
||||||
<div v-if="!timerSet" class="w-full">
|
<div class="w-full">
|
||||||
<template v-for="time in sleepTimes">
|
<template v-for="time in sleepTimes">
|
||||||
<div :key="time.text" class="flex items-center px-6 py-3 justify-center cursor-pointer hover:bg-bg relative" @click="setTime(time.seconds)">
|
<div :key="time.text" class="flex items-center px-6 py-3 justify-center cursor-pointer hover:bg-primary/25 relative" @click="setTime(time)">
|
||||||
<p class="text-xl text-center">{{ time.text }}</p>
|
<p class="text-lg text-center">{{ time.text }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<form class="flex items-center justify-center px-6 py-3" @submit.prevent="submitCustomTime">
|
<form class="flex items-center justify-center px-6 py-3" @submit.prevent="submitCustomTime">
|
||||||
<ui-text-input v-model="customTime" type="number" step="any" min="0.1" placeholder="Time in minutes" class="w-48" />
|
<ui-text-input v-model="customTime" type="number" step="any" min="0.1" :placeholder="$strings.LabelTimeInMinutes" class="w-48" />
|
||||||
<ui-btn color="success" type="submit" :padding-x="0" class="h-9 w-12 flex items-center justify-center ml-1">Set</ui-btn>
|
<ui-btn color="success" type="submit" :padding-x="0" class="h-9 w-12 flex items-center justify-center ml-1">Set</ui-btn>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-full p-4">
|
<div v-if="timerSet" class="w-full p-4">
|
||||||
<div class="mb-4 flex items-center justify-center">
|
<div class="mb-4 h-px w-full bg-white/10" />
|
||||||
<ui-btn :padding-x="2" small :disabled="remaining < 30 * 60" class="flex items-center mr-4" @click="decrement(30 * 60)">
|
|
||||||
|
<div v-if="timerType === $constants.SleepTimerTypes.COUNTDOWN" class="mb-4 flex items-center justify-center space-x-4">
|
||||||
|
<ui-btn :padding-x="2" small :disabled="remaining < 30 * 60" class="flex items-center h-9" @click="decrement(30 * 60)">
|
||||||
<span class="material-symbols text-lg">remove</span>
|
<span class="material-symbols text-lg">remove</span>
|
||||||
<span class="pl-1 text-base font-mono">30m</span>
|
<span class="pl-1 text-sm">30m</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
||||||
<ui-icon-btn icon="remove" @click="decrement(60 * 5)" />
|
<ui-icon-btn icon="remove" class="min-w-9" @click="decrement(60 * 5)" />
|
||||||
|
|
||||||
<p class="mx-6 text-2xl font-mono">{{ $secondsToTimestamp(remaining) }}</p>
|
<p class="text-2xl font-mono">{{ $secondsToTimestamp(remaining) }}</p>
|
||||||
|
|
||||||
<ui-icon-btn icon="add" @click="increment(60 * 5)" />
|
<ui-icon-btn icon="add" class="min-w-9" @click="increment(60 * 5)" />
|
||||||
|
|
||||||
<ui-btn :padding-x="2" small class="flex items-center ml-4" @click="increment(30 * 60)">
|
<ui-btn :padding-x="2" small class="flex items-center h-9" @click="increment(30 * 60)">
|
||||||
<span class="material-symbols text-lg">add</span>
|
<span class="material-symbols text-lg">add</span>
|
||||||
<span class="pl-1 text-base font-mono">30m</span>
|
<span class="pl-1 text-sm">30m</span>
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
<ui-btn class="w-full" @click="$emit('cancel')">{{ $strings.ButtonCancel }}</ui-btn>
|
<ui-btn class="w-full" @click="$emit('cancel')">{{ $strings.ButtonCancel }}</ui-btn>
|
||||||
@ -47,52 +49,13 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
value: Boolean,
|
value: Boolean,
|
||||||
timerSet: Boolean,
|
timerSet: Boolean,
|
||||||
timerTime: Number,
|
timerType: String,
|
||||||
remaining: Number
|
remaining: Number,
|
||||||
|
hasChapters: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
customTime: null,
|
customTime: null
|
||||||
sleepTimes: [
|
|
||||||
{
|
|
||||||
seconds: 60 * 5,
|
|
||||||
text: '5 minutes'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
seconds: 60 * 15,
|
|
||||||
text: '15 minutes'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
seconds: 60 * 20,
|
|
||||||
text: '20 minutes'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
seconds: 60 * 30,
|
|
||||||
text: '30 minutes'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
seconds: 60 * 45,
|
|
||||||
text: '45 minutes'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
seconds: 60 * 60,
|
|
||||||
text: '60 minutes'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
seconds: 60 * 90,
|
|
||||||
text: '90 minutes'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
seconds: 60 * 120,
|
|
||||||
text: '2 hours'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
show(newVal) {
|
|
||||||
if (newVal) {
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@ -103,6 +66,54 @@ export default {
|
|||||||
set(val) {
|
set(val) {
|
||||||
this.$emit('input', val)
|
this.$emit('input', val)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
sleepTimes() {
|
||||||
|
const times = [
|
||||||
|
{
|
||||||
|
seconds: 60 * 5,
|
||||||
|
text: this.$getString('LabelTimeDurationXMinutes', ['5']),
|
||||||
|
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seconds: 60 * 15,
|
||||||
|
text: this.$getString('LabelTimeDurationXMinutes', ['15']),
|
||||||
|
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seconds: 60 * 20,
|
||||||
|
text: this.$getString('LabelTimeDurationXMinutes', ['20']),
|
||||||
|
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seconds: 60 * 30,
|
||||||
|
text: this.$getString('LabelTimeDurationXMinutes', ['30']),
|
||||||
|
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seconds: 60 * 45,
|
||||||
|
text: this.$getString('LabelTimeDurationXMinutes', ['45']),
|
||||||
|
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seconds: 60 * 60,
|
||||||
|
text: this.$getString('LabelTimeDurationXMinutes', ['60']),
|
||||||
|
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seconds: 60 * 90,
|
||||||
|
text: this.$getString('LabelTimeDurationXMinutes', ['90']),
|
||||||
|
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||||
|
},
|
||||||
|
{
|
||||||
|
seconds: 60 * 120,
|
||||||
|
text: this.$getString('LabelTimeDurationXHours', ['2']),
|
||||||
|
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||||
|
}
|
||||||
|
]
|
||||||
|
if (this.hasChapters) {
|
||||||
|
times.push({ seconds: -1, text: this.$strings.LabelEndOfChapter, timerType: this.$constants.SleepTimerTypes.CHAPTER })
|
||||||
|
}
|
||||||
|
return times
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -113,10 +124,14 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const timeInSeconds = Math.round(Number(this.customTime) * 60)
|
const timeInSeconds = Math.round(Number(this.customTime) * 60)
|
||||||
this.setTime(timeInSeconds)
|
const time = {
|
||||||
|
seconds: timeInSeconds,
|
||||||
|
timerType: this.$constants.SleepTimerTypes.COUNTDOWN
|
||||||
|
}
|
||||||
|
this.setTime(time)
|
||||||
},
|
},
|
||||||
setTime(seconds) {
|
setTime(time) {
|
||||||
this.$emit('set', seconds)
|
this.$emit('set', time)
|
||||||
},
|
},
|
||||||
increment(amount) {
|
increment(amount) {
|
||||||
this.$emit('increment', amount)
|
this.$emit('increment', amount)
|
||||||
@ -130,4 +145,4 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -16,11 +16,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-lg font-semibold mb-2">{{ $strings.HeaderPodcastsToAdd }}</p>
|
<p class="text-lg font-semibold mb-1">{{ $strings.HeaderPodcastsToAdd }}</p>
|
||||||
|
<p class="text-sm text-gray-300 mb-4">{{ $strings.MessageOpmlPreviewNote }}</p>
|
||||||
|
|
||||||
<div class="w-full overflow-y-auto" style="max-height: 50vh">
|
<div class="w-full overflow-y-auto" style="max-height: 50vh">
|
||||||
<template v-for="(feed, index) in feedMetadata">
|
<template v-for="(feed, index) in feeds">
|
||||||
<cards-podcast-feed-summary-card :key="index" :feed="feed" :library-folder-path="selectedFolderPath" class="my-1" />
|
<div :key="index" class="py-1 flex items-center">
|
||||||
|
<p class="text-lg font-semibold">{{ index + 1 }}.</p>
|
||||||
|
<div class="pl-2">
|
||||||
|
<p v-if="feed.title" class="text-sm font-semibold">{{ feed.title }}</p>
|
||||||
|
<p class="text-xs text-gray-400">{{ feed.feedUrl }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -45,9 +52,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
processing: false,
|
processing: false,
|
||||||
selectedFolderId: null,
|
selectedFolderId: null,
|
||||||
fullPath: null,
|
autoDownloadEpisodes: false
|
||||||
autoDownloadEpisodes: false,
|
|
||||||
feedMetadata: []
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@ -96,73 +101,36 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toFeedMetadata(feed) {
|
|
||||||
const metadata = feed.metadata
|
|
||||||
return {
|
|
||||||
title: metadata.title,
|
|
||||||
author: metadata.author,
|
|
||||||
description: metadata.description,
|
|
||||||
releaseDate: '',
|
|
||||||
genres: [...metadata.categories],
|
|
||||||
feedUrl: metadata.feedUrl,
|
|
||||||
imageUrl: metadata.image,
|
|
||||||
itunesPageUrl: '',
|
|
||||||
itunesId: '',
|
|
||||||
itunesArtistId: '',
|
|
||||||
language: '',
|
|
||||||
numEpisodes: feed.numEpisodes
|
|
||||||
}
|
|
||||||
},
|
|
||||||
init() {
|
init() {
|
||||||
this.feedMetadata = this.feeds.map(this.toFeedMetadata)
|
|
||||||
|
|
||||||
if (this.folderItems[0]) {
|
if (this.folderItems[0]) {
|
||||||
this.selectedFolderId = this.folderItems[0].value
|
this.selectedFolderId = this.folderItems[0].value
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async submit() {
|
async submit() {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
const newFeedPayloads = this.feedMetadata.map((metadata) => {
|
|
||||||
return {
|
|
||||||
path: `${this.selectedFolderPath}/${this.$sanitizeFilename(metadata.title)}`,
|
|
||||||
folderId: this.selectedFolderId,
|
|
||||||
libraryId: this.currentLibrary.id,
|
|
||||||
media: {
|
|
||||||
metadata: {
|
|
||||||
...metadata
|
|
||||||
},
|
|
||||||
autoDownloadEpisodes: this.autoDownloadEpisodes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
console.log('New feed payloads', newFeedPayloads)
|
|
||||||
|
|
||||||
for (const podcastPayload of newFeedPayloads) {
|
const payload = {
|
||||||
await this.$axios
|
feeds: this.feeds.map((f) => f.feedUrl),
|
||||||
.$post('/api/podcasts', podcastPayload)
|
folderId: this.selectedFolderId,
|
||||||
.then(() => {
|
libraryId: this.currentLibrary.id,
|
||||||
this.$toast.success(`${podcastPayload.media.metadata.title}: ${this.$strings.ToastPodcastCreateSuccess}`)
|
autoDownloadEpisodes: this.autoDownloadEpisodes
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
var errorMsg = error.response && error.response.data ? error.response.data : this.$strings.ToastPodcastCreateFailed
|
|
||||||
console.error('Failed to create podcast', podcastPayload, error)
|
|
||||||
this.$toast.error(`${podcastPayload.media.metadata.title}: ${errorMsg}`)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
this.processing = false
|
this.$axios
|
||||||
this.show = false
|
.$post('/api/podcasts/opml/create', payload)
|
||||||
|
.then(() => {
|
||||||
|
this.show = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
const errorMsg = error.response?.data || this.$strings.ToastPodcastCreateFailed
|
||||||
|
console.error('Failed to create podcast', payload, error)
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
#podcast-wrapper {
|
|
||||||
min-height: 400px;
|
|
||||||
max-height: 80vh;
|
|
||||||
}
|
|
||||||
#episodes-scroll {
|
|
||||||
max-height: calc(80vh - 200px);
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -132,7 +132,7 @@ export default {
|
|||||||
this.searchedTitle = this.episodeTitle
|
this.searchedTitle = this.episodeTitle
|
||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$get(`/api/podcasts/${this.libraryItem.id}/search-episode?title=${this.$encodeUriPath(this.episodeTitle)}`)
|
.$get(`/api/podcasts/${this.libraryItem.id}/search-episode?title=${encodeURIComponent(this.episodeTitle)}`)
|
||||||
.then((results) => {
|
.then((results) => {
|
||||||
this.episodesFound = results.episodes.map((ep) => ep.episode)
|
this.episodesFound = results.episodes.map((ep) => ep.episode)
|
||||||
console.log('Episodes found', this.episodesFound)
|
console.log('Episodes found', this.episodesFound)
|
||||||
@ -153,4 +153,4 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -7,17 +7,17 @@
|
|||||||
<span class="material-symbols text-2xl sm:text-3xl">first_page</span>
|
<span class="material-symbols text-2xl sm:text-3xl">first_page</span>
|
||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<ui-tooltip direction="top" :text="$strings.ButtonJumpBackward">
|
<ui-tooltip direction="top" :text="jumpBackwardText">
|
||||||
<button :aria-label="$strings.ButtonJumpBackward" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
|
<button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
|
||||||
<span class="material-symbols text-2xl sm:text-3xl">replay_10</span>
|
<span class="material-symbols text-2xl sm:text-3xl">replay</span>
|
||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 lg:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
<button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 lg:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
|
||||||
<span class="material-symbols fill text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
<span class="material-symbols fill text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
|
||||||
</button>
|
</button>
|
||||||
<ui-tooltip direction="top" :text="$strings.ButtonJumpForward">
|
<ui-tooltip direction="top" :text="jumpForwardText">
|
||||||
<button :aria-label="$strings.ButtonJumpForward" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
<button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
|
||||||
<span class="material-symbols text-2xl sm:text-3xl">forward_10</span>
|
<span class="material-symbols text-2xl sm:text-3xl">forward_media</span>
|
||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<ui-tooltip direction="top" :text="$strings.ButtonNextChapter" class="ml-4 lg:ml-8">
|
<ui-tooltip direction="top" :text="$strings.ButtonNextChapter" class="ml-4 lg:ml-8">
|
||||||
@ -29,7 +29,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin">
|
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin">
|
||||||
<span class="material-symbols">autorenew</span>
|
<span class="material-symbols text-2xl">autorenew</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
@ -56,6 +56,12 @@ export default {
|
|||||||
set(val) {
|
set(val) {
|
||||||
this.$emit('update:playbackRate', val)
|
this.$emit('update:playbackRate', val)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
jumpForwardText() {
|
||||||
|
return this.getJumpText('jumpForwardAmount', this.$strings.ButtonJumpForward)
|
||||||
|
},
|
||||||
|
jumpBackwardText() {
|
||||||
|
return this.getJumpText('jumpBackwardAmount', this.$strings.ButtonJumpBackward)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -83,8 +89,22 @@ export default {
|
|||||||
this.$store.dispatch('user/updateUserSettings', { playbackRate }).catch((err) => {
|
this.$store.dispatch('user/updateUserSettings', { playbackRate }).catch((err) => {
|
||||||
console.error('Failed to update settings', err)
|
console.error('Failed to update settings', err)
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
getJumpText(setting, prefix) {
|
||||||
|
const amount = this.$store.getters['user/getUserSetting'](setting)
|
||||||
|
if (!amount) return prefix
|
||||||
|
|
||||||
|
let formattedTime = ''
|
||||||
|
if (amount <= 60) {
|
||||||
|
formattedTime = this.$getString('LabelTimeDurationXSeconds', [amount])
|
||||||
|
} else {
|
||||||
|
const minutes = Math.floor(amount / 60)
|
||||||
|
formattedTime = this.$getString('LabelTimeDurationXMinutes', [minutes])
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${prefix} - ${formattedTime}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
<span v-if="!sleepTimerSet" class="material-symbols text-2xl">snooze</span>
|
<span v-if="!sleepTimerSet" class="material-symbols text-2xl">snooze</span>
|
||||||
<div v-else class="flex items-center">
|
<div v-else class="flex items-center">
|
||||||
<span class="material-symbols text-lg text-warning">snooze</span>
|
<span class="material-symbols text-lg text-warning">snooze</span>
|
||||||
<p class="text-xl text-warning font-mono font-semibold text-center px-0.5 pb-0.5" style="min-width: 30px">{{ sleepTimerRemainingString }}</p>
|
<p class="text-sm sm:text-lg text-warning font-mono font-semibold text-center px-0.5 sm:pb-0.5 sm:min-w-8">{{ sleepTimerRemainingString }}</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@ -36,9 +36,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip v-if="chapters.length" direction="top" :text="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack">
|
<ui-tooltip direction="top" :text="$strings.LabelViewPlayerSettings">
|
||||||
<button :aria-label="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack" class="text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="setUseChapterTrack">
|
<button :aria-label="$strings.LabelViewPlayerSettings" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerSettings')">
|
||||||
<span class="material-symbols text-2xl sm:text-3xl transform transition-transform" :class="useChapterTrack ? 'rotate-180' : ''">timelapse</span>
|
<span class="material-symbols text-2xl sm:text-2.5xl">settings_slow_motion</span>
|
||||||
</button>
|
</button>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
@ -72,12 +72,14 @@ export default {
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
currentChapter: Object,
|
||||||
bookmarks: {
|
bookmarks: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
sleepTimerSet: Boolean,
|
sleepTimerSet: Boolean,
|
||||||
sleepTimerRemaining: Number,
|
sleepTimerRemaining: Number,
|
||||||
|
sleepTimerType: String,
|
||||||
isPodcast: Boolean,
|
isPodcast: Boolean,
|
||||||
hideBookmarks: Boolean,
|
hideBookmarks: Boolean,
|
||||||
hideSleepTimer: Boolean
|
hideSleepTimer: Boolean
|
||||||
@ -90,27 +92,34 @@ export default {
|
|||||||
seekLoading: false,
|
seekLoading: false,
|
||||||
showChaptersModal: false,
|
showChaptersModal: false,
|
||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
duration: 0,
|
duration: 0
|
||||||
useChapterTrack: false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
playbackRate() {
|
playbackRate() {
|
||||||
this.updateTimestamp()
|
this.updateTimestamp()
|
||||||
|
},
|
||||||
|
useChapterTrack() {
|
||||||
|
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack)
|
||||||
|
this.updateTimestamp()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
sleepTimerRemainingString() {
|
sleepTimerRemainingString() {
|
||||||
var rounded = Math.round(this.sleepTimerRemaining)
|
if (this.sleepTimerType === this.$constants.SleepTimerTypes.CHAPTER) {
|
||||||
if (rounded < 90) {
|
return 'EoC'
|
||||||
return `${rounded}s`
|
} else {
|
||||||
|
var rounded = Math.round(this.sleepTimerRemaining)
|
||||||
|
if (rounded < 90) {
|
||||||
|
return `${rounded}s`
|
||||||
|
}
|
||||||
|
var minutesRounded = Math.round(rounded / 60)
|
||||||
|
if (minutesRounded <= 90) {
|
||||||
|
return `${minutesRounded}m`
|
||||||
|
}
|
||||||
|
var hoursRounded = Math.round(minutesRounded / 60)
|
||||||
|
return `${hoursRounded}h`
|
||||||
}
|
}
|
||||||
var minutesRounded = Math.round(rounded / 60)
|
|
||||||
if (minutesRounded < 90) {
|
|
||||||
return `${minutesRounded}m`
|
|
||||||
}
|
|
||||||
var hoursRounded = Math.round(minutesRounded / 60)
|
|
||||||
return `${hoursRounded}h`
|
|
||||||
},
|
},
|
||||||
token() {
|
token() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
@ -135,9 +144,6 @@ export default {
|
|||||||
if (!duration) return 0
|
if (!duration) return 0
|
||||||
return Math.round((100 * time) / duration)
|
return Math.round((100 * time) / duration)
|
||||||
},
|
},
|
||||||
currentChapter() {
|
|
||||||
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
|
|
||||||
},
|
|
||||||
currentChapterName() {
|
currentChapterName() {
|
||||||
return this.currentChapter ? this.currentChapter.title : ''
|
return this.currentChapter ? this.currentChapter.title : ''
|
||||||
},
|
},
|
||||||
@ -162,6 +168,10 @@ export default {
|
|||||||
},
|
},
|
||||||
playerQueueItems() {
|
playerQueueItems() {
|
||||||
return this.$store.state.playerQueueItems || []
|
return this.$store.state.playerQueueItems || []
|
||||||
|
},
|
||||||
|
useChapterTrack() {
|
||||||
|
const _useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') || false
|
||||||
|
return this.chapters.length ? _useChapterTrack : false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -310,9 +320,6 @@ export default {
|
|||||||
init() {
|
init() {
|
||||||
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
|
this.playbackRate = this.$store.getters['user/getUserSetting']('playbackRate') || 1
|
||||||
|
|
||||||
const _useChapterTrack = this.$store.getters['user/getUserSetting']('useChapterTrack') || false
|
|
||||||
this.useChapterTrack = this.chapters.length ? _useChapterTrack : false
|
|
||||||
|
|
||||||
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack)
|
if (this.$refs.trackbar) this.$refs.trackbar.setUseChapterTrack(this.useChapterTrack)
|
||||||
this.setPlaybackRate(this.playbackRate)
|
this.setPlaybackRate(this.playbackRate)
|
||||||
},
|
},
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
|
<div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
|
||||||
<slot :disabled="disabled" :showMenu="showMenu" :clickShowMenu="clickShowMenu" :processing="processing">
|
<slot :disabled="disabled" :showMenu="showMenu" :clickShowMenu="clickShowMenu" :processing="processing">
|
||||||
<button v-if="!processing" type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
<button v-if="!processing" type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||||
<span class="material-symbols" :class="iconClass">more_vert</span>
|
<span class="material-symbols text-2xl" :class="iconClass">more_vert</span>
|
||||||
</button>
|
</button>
|
||||||
<div v-else class="h-full w-full flex items-center justify-center">
|
<div v-else class="h-full w-full flex items-center justify-center">
|
||||||
<widgets-loading-spinner />
|
<widgets-loading-spinner />
|
||||||
@ -116,4 +116,4 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
151
client/components/ui/SelectInput.vue
Normal file
151
client/components/ui/SelectInput.vue
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative w-full">
|
||||||
|
<p v-if="label" class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
|
||||||
|
<button ref="buttonWrapper" type="button" :aria-label="longLabel" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left sm:text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
||||||
|
<span class="flex items-center">
|
||||||
|
<span class="block truncate font-sans" :class="{ 'font-semibold': selectedSubtext, 'text-sm': small }">{{ selectedText }}</span>
|
||||||
|
<span v-if="selectedSubtext">: </span>
|
||||||
|
<span v-if="selectedSubtext" class="font-normal block truncate font-sans text-sm text-gray-400">{{ selectedSubtext }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||||
|
<span class="material-symbols text-2xl">expand_more</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<transition name="menu">
|
||||||
|
<ul ref="menu" v-show="showMenu" class="absolute z-60 -mt-px w-full bg-primary border border-black-200 shadow-lg rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm" tabindex="-1" role="listbox" :style="{ maxHeight: menuMaxHeight }" v-click-outside="clickOutsideObj">
|
||||||
|
<template v-for="item in itemsToShow">
|
||||||
|
<li :key="item.value" class="text-gray-100 relative py-2 cursor-pointer hover:bg-black-400" :id="'listbox-option-' + item.value" role="option" tabindex="0" @keyup.enter="clickedOption(item.value)" @click.stop.prevent="clickedOption(item.value)">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="ml-3 block truncate font-sans text-sm" :class="{ 'font-semibold': item.subtext }">{{ item.text }}</span>
|
||||||
|
<span v-if="item.subtext">: </span>
|
||||||
|
<span v-if="item.subtext" class="font-normal block truncate font-sans text-sm text-gray-400">{{ item.subtext }}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: [String, Number],
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
disabled: Boolean,
|
||||||
|
small: Boolean,
|
||||||
|
menuMaxHeight: {
|
||||||
|
type: String,
|
||||||
|
default: '224px'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
clickOutsideObj: {
|
||||||
|
handler: this.clickedOutside,
|
||||||
|
events: ['click'],
|
||||||
|
isActive: true
|
||||||
|
},
|
||||||
|
menu: null,
|
||||||
|
showMenu: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
selected: {
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemsToShow() {
|
||||||
|
return this.items.map((i) => {
|
||||||
|
if (typeof i === 'string' || typeof i === 'number') {
|
||||||
|
return {
|
||||||
|
text: i,
|
||||||
|
value: i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
})
|
||||||
|
},
|
||||||
|
selectedItem() {
|
||||||
|
return this.itemsToShow.find((i) => i.value === this.selected)
|
||||||
|
},
|
||||||
|
selectedText() {
|
||||||
|
return this.selectedItem ? this.selectedItem.text : ''
|
||||||
|
},
|
||||||
|
selectedSubtext() {
|
||||||
|
return this.selectedItem ? this.selectedItem.subtext : ''
|
||||||
|
},
|
||||||
|
buttonClass() {
|
||||||
|
var classes = []
|
||||||
|
if (this.small) classes.push('h-9')
|
||||||
|
else classes.push('h-10')
|
||||||
|
|
||||||
|
if (this.disabled) classes.push('cursor-not-allowed border-gray-600 bg-primary bg-opacity-70 border-opacity-70 text-gray-400')
|
||||||
|
else classes.push('cursor-pointer border-gray-600 bg-primary text-gray-100')
|
||||||
|
|
||||||
|
return classes.join(' ')
|
||||||
|
},
|
||||||
|
longLabel() {
|
||||||
|
let result = ''
|
||||||
|
if (this.label) result += this.label + ': '
|
||||||
|
if (this.selectedText) result += this.selectedText
|
||||||
|
if (this.selectedSubtext) result += ' ' + this.selectedSubtext
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
recalcMenuPos() {
|
||||||
|
if (!this.menu || !this.$refs.buttonWrapper) return
|
||||||
|
const boundingBox = this.$refs.buttonWrapper.getBoundingClientRect()
|
||||||
|
this.menu.style.top = boundingBox.y + boundingBox.height - 4 + 'px'
|
||||||
|
this.menu.style.left = boundingBox.x + 'px'
|
||||||
|
this.menu.style.width = boundingBox.width + 'px'
|
||||||
|
},
|
||||||
|
unmountMountMenu() {
|
||||||
|
if (!this.$refs.menu || !this.$refs.buttonWrapper) return
|
||||||
|
this.menu = this.$refs.menu
|
||||||
|
this.menu.remove()
|
||||||
|
},
|
||||||
|
clickShowMenu() {
|
||||||
|
if (this.disabled) return
|
||||||
|
if (!this.showMenu) this.handleShowMenu()
|
||||||
|
else this.handleCloseMenu()
|
||||||
|
},
|
||||||
|
handleShowMenu() {
|
||||||
|
if (!this.menu) {
|
||||||
|
this.unmountMountMenu()
|
||||||
|
}
|
||||||
|
document.body.appendChild(this.menu)
|
||||||
|
this.recalcMenuPos()
|
||||||
|
this.showMenu = true
|
||||||
|
},
|
||||||
|
handleCloseMenu() {
|
||||||
|
this.showMenu = false
|
||||||
|
if (this.menu) this.menu.remove()
|
||||||
|
},
|
||||||
|
clickedOutside() {
|
||||||
|
this.handleCloseMenu()
|
||||||
|
},
|
||||||
|
clickedOption(itemValue) {
|
||||||
|
this.selected = itemValue
|
||||||
|
this.handleCloseMenu()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.menu) this.menu.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -52,7 +52,6 @@ export default {
|
|||||||
else if (pageName === 'notifications') return this.$strings.HeaderNotifications
|
else if (pageName === 'notifications') return this.$strings.HeaderNotifications
|
||||||
else if (pageName === 'sessions') return this.$strings.HeaderListeningSessions
|
else if (pageName === 'sessions') return this.$strings.HeaderListeningSessions
|
||||||
else if (pageName === 'stats') return this.$strings.HeaderYourStats
|
else if (pageName === 'stats') return this.$strings.HeaderYourStats
|
||||||
else if (pageName === 'library-stats') return this.$strings.HeaderLibraryStats
|
|
||||||
else if (pageName === 'users') return this.$strings.HeaderUsers
|
else if (pageName === 'users') return this.$strings.HeaderUsers
|
||||||
else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils
|
else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils
|
||||||
else if (pageName === 'rss-feeds') return this.$strings.HeaderRSSFeeds
|
else if (pageName === 'rss-feeds') return this.$strings.HeaderRSSFeeds
|
||||||
@ -94,4 +93,4 @@ export default {
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -170,7 +170,7 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
updateBackupsSettings() {
|
updateBackupsSettings() {
|
||||||
if (isNaN(this.maxBackupSize) || this.maxBackupSize <= 0) {
|
if (isNaN(this.maxBackupSize) || this.maxBackupSize < 0) {
|
||||||
this.$toast.error('Invalid maximum backup size')
|
this.$toast.error('Invalid maximum backup size')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -200,10 +200,9 @@ export default {
|
|||||||
},
|
},
|
||||||
initServerSettings() {
|
initServerSettings() {
|
||||||
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
|
||||||
|
|
||||||
this.backupsToKeep = this.newServerSettings.backupsToKeep || 2
|
this.backupsToKeep = this.newServerSettings.backupsToKeep || 2
|
||||||
this.enableBackups = !!this.newServerSettings.backupSchedule
|
this.enableBackups = !!this.newServerSettings.backupSchedule
|
||||||
this.maxBackupSize = this.newServerSettings.maxBackupSize || 1
|
this.maxBackupSize = this.newServerSettings.maxBackupSize === 0 ? 0 : this.newServerSettings.maxBackupSize || 1
|
||||||
this.cronExpression = this.newServerSettings.backupSchedule || '30 1 * * *'
|
this.cronExpression = this.newServerSettings.backupSchedule || '30 1 * * *'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1,175 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<app-settings-content :header-text="$strings.HeaderLibraryStats + ': ' + currentLibraryName">
|
|
||||||
<stats-preview-icons v-if="totalItems" :library-stats="libraryStats" />
|
|
||||||
|
|
||||||
<div class="flex lg:flex-row flex-wrap justify-between flex-col mt-8">
|
|
||||||
<div class="w-80 my-6 mx-auto">
|
|
||||||
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsTop5Genres }}</h1>
|
|
||||||
<p v-if="!top5Genres.length">{{ $strings.MessageNoGenres }}</p>
|
|
||||||
<template v-for="genre in top5Genres">
|
|
||||||
<div :key="genre.genre" class="w-full py-2">
|
|
||||||
<div class="flex items-end mb-1">
|
|
||||||
<p class="text-2xl font-bold">{{ Math.round((100 * genre.count) / totalItems) }} %</p>
|
|
||||||
<div class="flex-grow" />
|
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=genres.${$encode(genre.genre)}`" class="text-base text-white text-opacity-70 hover:underline">
|
|
||||||
{{ genre.genre }}
|
|
||||||
</nuxt-link>
|
|
||||||
</div>
|
|
||||||
<div class="w-full rounded-full h-3 bg-primary bg-opacity-50 overflow-hidden">
|
|
||||||
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * genre.count) / totalItems) + '%' }" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div v-if="isBookLibrary" class="w-80 my-6 mx-auto">
|
|
||||||
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsTop10Authors }}</h1>
|
|
||||||
<p v-if="!top10Authors.length">{{ $strings.MessageNoAuthors }}</p>
|
|
||||||
<template v-for="(author, index) in top10Authors">
|
|
||||||
<div :key="author.id" class="w-full py-2">
|
|
||||||
<div class="flex items-center mb-1">
|
|
||||||
<p class="text-sm text-white text-opacity-70 w-36 pr-2 truncate">
|
|
||||||
{{ index + 1 }}. <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(author.id)}`" class="hover:underline">{{ author.name }}</nuxt-link>
|
|
||||||
</p>
|
|
||||||
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
|
||||||
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * author.count) / mostUsedAuthorCount) + '%' }" />
|
|
||||||
</div>
|
|
||||||
<div class="w-4 ml-3">
|
|
||||||
<p class="text-sm font-bold">{{ author.count }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="w-80 my-6 mx-auto">
|
|
||||||
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsLongestItems }}</h1>
|
|
||||||
<p v-if="!top10LongestItems.length">{{ $strings.MessageNoItems }}</p>
|
|
||||||
<template v-for="(ab, index) in top10LongestItems">
|
|
||||||
<div :key="index" class="w-full py-2">
|
|
||||||
<div class="flex items-center mb-1">
|
|
||||||
<p class="text-sm text-white text-opacity-70 w-44 pr-2 truncate">
|
|
||||||
{{ index + 1 }}. <nuxt-link :to="`/item/${ab.id}`" class="hover:underline">{{ ab.title }}</nuxt-link>
|
|
||||||
</p>
|
|
||||||
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
|
||||||
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * ab.duration) / longestItemDuration) + '%' }" />
|
|
||||||
</div>
|
|
||||||
<div class="w-4 ml-3">
|
|
||||||
<p class="text-sm font-bold">{{ (ab.duration / 3600).toFixed(1) }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="w-80 my-6 mx-auto">
|
|
||||||
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsLargestItems }}</h1>
|
|
||||||
<p v-if="!top10LargestItems.length">{{ $strings.MessageNoItems }}</p>
|
|
||||||
<template v-for="(ab, index) in top10LargestItems">
|
|
||||||
<div :key="index" class="w-full py-2">
|
|
||||||
<div class="flex items-center mb-1">
|
|
||||||
<p class="text-sm text-white text-opacity-70 w-44 pr-2 truncate">
|
|
||||||
{{ index + 1 }}. <nuxt-link :to="`/item/${ab.id}`" class="hover:underline">{{ ab.title }}</nuxt-link>
|
|
||||||
</p>
|
|
||||||
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
|
||||||
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * ab.size) / largestItemSize) + '%' }" />
|
|
||||||
</div>
|
|
||||||
<div class="w-4 ml-3">
|
|
||||||
<p class="text-sm font-bold">{{ $bytesPretty(ab.size) }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</app-settings-content>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
asyncData({ redirect, store }) {
|
|
||||||
if (!store.getters['user/getIsAdminOrUp']) {
|
|
||||||
redirect('/')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!store.state.libraries.currentLibraryId) {
|
|
||||||
return redirect('/config')
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
libraryStats: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
currentLibraryId(newVal, oldVal) {
|
|
||||||
if (newVal) {
|
|
||||||
this.init()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
user() {
|
|
||||||
return this.$store.state.user.user
|
|
||||||
},
|
|
||||||
totalItems() {
|
|
||||||
return this.libraryStats?.totalItems || 0
|
|
||||||
},
|
|
||||||
genresWithCount() {
|
|
||||||
return this.libraryStats?.genresWithCount || []
|
|
||||||
},
|
|
||||||
top5Genres() {
|
|
||||||
return this.genresWithCount?.slice(0, 5) || []
|
|
||||||
},
|
|
||||||
top10LongestItems() {
|
|
||||||
return this.libraryStats?.longestItems || []
|
|
||||||
},
|
|
||||||
longestItemDuration() {
|
|
||||||
if (!this.top10LongestItems.length) return 0
|
|
||||||
return this.top10LongestItems[0].duration
|
|
||||||
},
|
|
||||||
top10LargestItems() {
|
|
||||||
return this.libraryStats?.largestItems || []
|
|
||||||
},
|
|
||||||
largestItemSize() {
|
|
||||||
if (!this.top10LargestItems.length) return 0
|
|
||||||
return this.top10LargestItems[0].size
|
|
||||||
},
|
|
||||||
authorsWithCount() {
|
|
||||||
return this.libraryStats?.authorsWithCount || []
|
|
||||||
},
|
|
||||||
mostUsedAuthorCount() {
|
|
||||||
if (!this.authorsWithCount.length) return 0
|
|
||||||
return this.authorsWithCount[0].count
|
|
||||||
},
|
|
||||||
top10Authors() {
|
|
||||||
return this.authorsWithCount?.slice(0, 10) || []
|
|
||||||
},
|
|
||||||
currentLibraryId() {
|
|
||||||
return this.$store.state.libraries.currentLibraryId
|
|
||||||
},
|
|
||||||
currentLibraryName() {
|
|
||||||
return this.$store.getters['libraries/getCurrentLibraryName']
|
|
||||||
},
|
|
||||||
currentLibraryMediaType() {
|
|
||||||
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
|
||||||
},
|
|
||||||
isBookLibrary() {
|
|
||||||
return this.currentLibraryMediaType === 'book'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async init() {
|
|
||||||
this.libraryStats = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/stats`).catch((err) => {
|
|
||||||
console.error('Failed to get library stats', err)
|
|
||||||
var errorMsg = err.response ? err.response.data || 'Unknown Error' : 'Unknown Error'
|
|
||||||
this.$toast.error(`Failed to get library stats: ${errorMsg}`)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.init()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
@ -121,7 +121,7 @@
|
|||||||
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="148" @action="contextMenuAction">
|
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="148" @action="contextMenuAction">
|
||||||
<template #default="{ showMenu, clickShowMenu, disabled }">
|
<template #default="{ showMenu, clickShowMenu, disabled }">
|
||||||
<button type="button" :disabled="disabled" class="mx-0.5 icon-btn bg-primary border border-gray-600 w-9 h-9 rounded-md flex items-center justify-center relative" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
<button type="button" :disabled="disabled" class="mx-0.5 icon-btn bg-primary border border-gray-600 w-9 h-9 rounded-md flex items-center justify-center relative" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||||
<span class="material-symbols">more_horiz</span>
|
<span class="material-symbols text-2xl">more_horiz</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</ui-context-menu-dropdown>
|
</ui-context-menu-dropdown>
|
||||||
|
@ -113,18 +113,23 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.$axios
|
this.$axios
|
||||||
.$post(`/api/podcasts/opml`, { opmlText: txt })
|
.$post(`/api/podcasts/opml/parse`, { opmlText: txt })
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
console.log(data)
|
if (!data.feeds?.length) {
|
||||||
this.opmlFeeds = data.feeds || []
|
this.$toast.error('No feeds found in OPML file')
|
||||||
this.showOPMLFeedsModal = true
|
} else {
|
||||||
|
this.opmlFeeds = data.feeds || []
|
||||||
|
this.showOPMLFeedsModal = true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
this.$toast.error('Failed to parse OPML file')
|
this.$toast.error('Failed to parse OPML file')
|
||||||
})
|
})
|
||||||
this.processing = false
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
},
|
},
|
||||||
submit() {
|
submit() {
|
||||||
if (!this.searchInput) return
|
if (!this.searchInput) return
|
||||||
|
181
client/pages/library/_library/stats.vue
Normal file
181
client/pages/library/_library/stats.vue
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page relative" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
|
<app-book-shelf-toolbar page="library-stats" is-home />
|
||||||
|
<div id="bookshelf" class="w-full h-full px-1 py-4 md:p-8 relative overflow-y-auto">
|
||||||
|
<div class="w-full max-w-4xl mx-auto">
|
||||||
|
<stats-preview-icons v-if="totalItems" :library-stats="libraryStats" />
|
||||||
|
|
||||||
|
<div class="flex lg:flex-row flex-wrap justify-between flex-col mt-8">
|
||||||
|
<div class="w-80 my-6 mx-auto">
|
||||||
|
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsTop5Genres }}</h1>
|
||||||
|
<p v-if="!top5Genres.length">{{ $strings.MessageNoGenres }}</p>
|
||||||
|
<template v-for="genre in top5Genres">
|
||||||
|
<div :key="genre.genre" class="w-full py-2">
|
||||||
|
<div class="flex items-end mb-1">
|
||||||
|
<p class="text-2xl font-bold">{{ Math.round((100 * genre.count) / totalItems) }} %</p>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=genres.${$encode(genre.genre)}`" class="text-base text-white text-opacity-70 hover:underline">
|
||||||
|
{{ genre.genre }}
|
||||||
|
</nuxt-link>
|
||||||
|
</div>
|
||||||
|
<div class="w-full rounded-full h-3 bg-primary bg-opacity-50 overflow-hidden">
|
||||||
|
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * genre.count) / totalItems) + '%' }" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div v-if="isBookLibrary" class="w-80 my-6 mx-auto">
|
||||||
|
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsTop10Authors }}</h1>
|
||||||
|
<p v-if="!top10Authors.length">{{ $strings.MessageNoAuthors }}</p>
|
||||||
|
<template v-for="(author, index) in top10Authors">
|
||||||
|
<div :key="author.id" class="w-full py-2">
|
||||||
|
<div class="flex items-center mb-1">
|
||||||
|
<p class="text-sm text-white text-opacity-70 w-36 pr-2 truncate">
|
||||||
|
{{ index + 1 }}. <nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=authors.${$encode(author.id)}`" class="hover:underline">{{ author.name }}</nuxt-link>
|
||||||
|
</p>
|
||||||
|
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
||||||
|
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * author.count) / mostUsedAuthorCount) + '%' }" />
|
||||||
|
</div>
|
||||||
|
<div class="w-4 ml-3">
|
||||||
|
<p class="text-sm font-bold">{{ author.count }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="w-80 my-6 mx-auto">
|
||||||
|
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsLongestItems }}</h1>
|
||||||
|
<p v-if="!top10LongestItems.length">{{ $strings.MessageNoItems }}</p>
|
||||||
|
<template v-for="(ab, index) in top10LongestItems">
|
||||||
|
<div :key="index" class="w-full py-2">
|
||||||
|
<div class="flex items-center mb-1">
|
||||||
|
<p class="text-sm text-white text-opacity-70 w-44 pr-2 truncate">
|
||||||
|
{{ index + 1 }}. <nuxt-link :to="`/item/${ab.id}`" class="hover:underline">{{ ab.title }}</nuxt-link>
|
||||||
|
</p>
|
||||||
|
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
||||||
|
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * ab.duration) / longestItemDuration) + '%' }" />
|
||||||
|
</div>
|
||||||
|
<div class="w-4 ml-3">
|
||||||
|
<p class="text-sm font-bold">{{ (ab.duration / 3600).toFixed(1) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="w-80 my-6 mx-auto">
|
||||||
|
<h1 class="text-2xl mb-4">{{ $strings.HeaderStatsLargestItems }}</h1>
|
||||||
|
<p v-if="!top10LargestItems.length">{{ $strings.MessageNoItems }}</p>
|
||||||
|
<template v-for="(ab, index) in top10LargestItems">
|
||||||
|
<div :key="index" class="w-full py-2">
|
||||||
|
<div class="flex items-center mb-1">
|
||||||
|
<p class="text-sm text-white text-opacity-70 w-44 pr-2 truncate">
|
||||||
|
{{ index + 1 }}. <nuxt-link :to="`/item/${ab.id}`" class="hover:underline">{{ ab.title }}</nuxt-link>
|
||||||
|
</p>
|
||||||
|
<div class="flex-grow rounded-full h-2.5 bg-primary bg-opacity-0 overflow-hidden">
|
||||||
|
<div class="bg-yellow-400 h-full rounded-full" :style="{ width: Math.round((100 * ab.size) / largestItemSize) + '%' }" />
|
||||||
|
</div>
|
||||||
|
<div class="w-4 ml-3">
|
||||||
|
<p class="text-sm font-bold">{{ $bytesPretty(ab.size) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
asyncData({ redirect, store }) {
|
||||||
|
if (!store.getters['user/getIsAdminOrUp']) {
|
||||||
|
redirect('/')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!store.state.libraries.currentLibraryId) {
|
||||||
|
return redirect('/config')
|
||||||
|
}
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
libraryStats: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
currentLibraryId(newVal, oldVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
streamLibraryItem() {
|
||||||
|
return this.$store.state.streamLibraryItem
|
||||||
|
},
|
||||||
|
user() {
|
||||||
|
return this.$store.state.user.user
|
||||||
|
},
|
||||||
|
totalItems() {
|
||||||
|
return this.libraryStats?.totalItems || 0
|
||||||
|
},
|
||||||
|
genresWithCount() {
|
||||||
|
return this.libraryStats?.genresWithCount || []
|
||||||
|
},
|
||||||
|
top5Genres() {
|
||||||
|
return this.genresWithCount?.slice(0, 5) || []
|
||||||
|
},
|
||||||
|
top10LongestItems() {
|
||||||
|
return this.libraryStats?.longestItems || []
|
||||||
|
},
|
||||||
|
longestItemDuration() {
|
||||||
|
if (!this.top10LongestItems.length) return 0
|
||||||
|
return this.top10LongestItems[0].duration
|
||||||
|
},
|
||||||
|
top10LargestItems() {
|
||||||
|
return this.libraryStats?.largestItems || []
|
||||||
|
},
|
||||||
|
largestItemSize() {
|
||||||
|
if (!this.top10LargestItems.length) return 0
|
||||||
|
return this.top10LargestItems[0].size
|
||||||
|
},
|
||||||
|
authorsWithCount() {
|
||||||
|
return this.libraryStats?.authorsWithCount || []
|
||||||
|
},
|
||||||
|
mostUsedAuthorCount() {
|
||||||
|
if (!this.authorsWithCount.length) return 0
|
||||||
|
return this.authorsWithCount[0].count
|
||||||
|
},
|
||||||
|
top10Authors() {
|
||||||
|
return this.authorsWithCount?.slice(0, 10) || []
|
||||||
|
},
|
||||||
|
currentLibraryId() {
|
||||||
|
return this.$store.state.libraries.currentLibraryId
|
||||||
|
},
|
||||||
|
currentLibraryName() {
|
||||||
|
return this.$store.getters['libraries/getCurrentLibraryName']
|
||||||
|
},
|
||||||
|
currentLibraryMediaType() {
|
||||||
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||||
|
},
|
||||||
|
isBookLibrary() {
|
||||||
|
return this.currentLibraryMediaType === 'book'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async init() {
|
||||||
|
this.libraryStats = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/stats`).catch((err) => {
|
||||||
|
console.error('Failed to get library stats', err)
|
||||||
|
var errorMsg = err.response ? err.response.data || 'Unknown Error' : 'Unknown Error'
|
||||||
|
this.$toast.error(`Failed to get library stats: ${errorMsg}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -36,10 +36,10 @@ export default class PlayerHandler {
|
|||||||
return this.libraryItem ? this.libraryItem.id : null
|
return this.libraryItem ? this.libraryItem.id : null
|
||||||
}
|
}
|
||||||
get isPlayingCastedItem() {
|
get isPlayingCastedItem() {
|
||||||
return this.libraryItem && (this.player instanceof CastPlayer)
|
return this.libraryItem && this.player instanceof CastPlayer
|
||||||
}
|
}
|
||||||
get isPlayingLocalItem() {
|
get isPlayingLocalItem() {
|
||||||
return this.libraryItem && (this.player instanceof LocalAudioPlayer)
|
return this.libraryItem && this.player instanceof LocalAudioPlayer
|
||||||
}
|
}
|
||||||
get userToken() {
|
get userToken() {
|
||||||
return this.ctx.$store.getters['user/getToken']
|
return this.ctx.$store.getters['user/getToken']
|
||||||
@ -49,7 +49,13 @@ export default class PlayerHandler {
|
|||||||
}
|
}
|
||||||
get episode() {
|
get episode() {
|
||||||
if (!this.episodeId) return null
|
if (!this.episodeId) return null
|
||||||
return this.libraryItem.media.episodes.find(ep => ep.id === this.episodeId)
|
return this.libraryItem.media.episodes.find((ep) => ep.id === this.episodeId)
|
||||||
|
}
|
||||||
|
get jumpForwardAmount() {
|
||||||
|
return this.ctx.$store.getters['user/getUserSetting']('jumpForwardAmount')
|
||||||
|
}
|
||||||
|
get jumpBackwardAmount() {
|
||||||
|
return this.ctx.$store.getters['user/getUserSetting']('jumpBackwardAmount')
|
||||||
}
|
}
|
||||||
|
|
||||||
setSessionId(sessionId) {
|
setSessionId(sessionId) {
|
||||||
@ -66,7 +72,7 @@ export default class PlayerHandler {
|
|||||||
this.playWhenReady = playWhenReady
|
this.playWhenReady = playWhenReady
|
||||||
this.initialPlaybackRate = this.isMusic ? 1 : playbackRate
|
this.initialPlaybackRate = this.isMusic ? 1 : playbackRate
|
||||||
|
|
||||||
this.startTimeOverride = (startTimeOverride == null || isNaN(startTimeOverride)) ? undefined : Number(startTimeOverride)
|
this.startTimeOverride = startTimeOverride == null || isNaN(startTimeOverride) ? undefined : Number(startTimeOverride)
|
||||||
|
|
||||||
if (!this.player) this.switchPlayer(playWhenReady)
|
if (!this.player) this.switchPlayer(playWhenReady)
|
||||||
else this.prepare()
|
else this.prepare()
|
||||||
@ -127,7 +133,7 @@ export default class PlayerHandler {
|
|||||||
|
|
||||||
playerError() {
|
playerError() {
|
||||||
// Switch to HLS stream on error
|
// Switch to HLS stream on error
|
||||||
if (!this.isCasting && (this.player instanceof LocalAudioPlayer)) {
|
if (!this.isCasting && this.player instanceof LocalAudioPlayer) {
|
||||||
console.log(`[PlayerHandler] Audio player error switching to HLS stream`)
|
console.log(`[PlayerHandler] Audio player error switching to HLS stream`)
|
||||||
this.prepare(true)
|
this.prepare(true)
|
||||||
}
|
}
|
||||||
@ -207,7 +213,8 @@ export default class PlayerHandler {
|
|||||||
this.prepareSession(session)
|
this.prepareSession(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareOpenSession(session, playbackRate) { // Session opened on init socket
|
prepareOpenSession(session, playbackRate) {
|
||||||
|
// Session opened on init socket
|
||||||
if (!this.player) this.switchPlayer() // Must set player first for open sessions
|
if (!this.player) this.switchPlayer() // Must set player first for open sessions
|
||||||
|
|
||||||
this.libraryItem = session.libraryItem
|
this.libraryItem = session.libraryItem
|
||||||
@ -241,7 +248,7 @@ export default class PlayerHandler {
|
|||||||
|
|
||||||
this.player.set(this.libraryItem, videoTrack, this.isHlsTranscode, this.startTime, this.playWhenReady)
|
this.player.set(this.libraryItem, videoTrack, this.isHlsTranscode, this.startTime, this.playWhenReady)
|
||||||
} else {
|
} else {
|
||||||
var audioTracks = session.audioTracks.map(at => new AudioTrack(at, this.userToken))
|
var audioTracks = session.audioTracks.map((at) => new AudioTrack(at, this.userToken))
|
||||||
|
|
||||||
this.ctx.playerLoading = true
|
this.ctx.playerLoading = true
|
||||||
this.isHlsTranscode = true
|
this.isHlsTranscode = true
|
||||||
@ -295,7 +302,7 @@ export default class PlayerHandler {
|
|||||||
const currentTime = this.player.getCurrentTime()
|
const currentTime = this.player.getCurrentTime()
|
||||||
this.ctx.setCurrentTime(currentTime)
|
this.ctx.setCurrentTime(currentTime)
|
||||||
|
|
||||||
const exactTimeElapsed = ((Date.now() - lastTick) / 1000)
|
const exactTimeElapsed = (Date.now() - lastTick) / 1000
|
||||||
lastTick = Date.now()
|
lastTick = Date.now()
|
||||||
this.listeningTimeSinceSync += exactTimeElapsed
|
this.listeningTimeSinceSync += exactTimeElapsed
|
||||||
const TimeToWaitBeforeSync = this.lastSyncTime > 0 ? 10 : 20
|
const TimeToWaitBeforeSync = this.lastSyncTime > 0 ? 10 : 20
|
||||||
@ -320,7 +327,7 @@ export default class PlayerHandler {
|
|||||||
}
|
}
|
||||||
this.listeningTimeSinceSync = 0
|
this.listeningTimeSinceSync = 0
|
||||||
this.lastSyncTime = 0
|
this.lastSyncTime = 0
|
||||||
return this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/close`, syncData, { timeout: 6000 }).catch((error) => {
|
return this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/close`, syncData, { timeout: 6000, progress: false }).catch((error) => {
|
||||||
console.error('Failed to close session', error)
|
console.error('Failed to close session', error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -340,17 +347,20 @@ export default class PlayerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.listeningTimeSinceSync = 0
|
this.listeningTimeSinceSync = 0
|
||||||
this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/sync`, syncData, { timeout: 9000 }).then(() => {
|
this.ctx.$axios
|
||||||
this.failedProgressSyncs = 0
|
.$post(`/api/session/${this.currentSessionId}/sync`, syncData, { timeout: 9000, progress: false })
|
||||||
}).catch((error) => {
|
.then(() => {
|
||||||
console.error('Failed to update session progress', error)
|
|
||||||
// After 4 failed sync attempts show an alert toast
|
|
||||||
this.failedProgressSyncs++
|
|
||||||
if (this.failedProgressSyncs >= 4) {
|
|
||||||
this.ctx.showFailedProgressSyncs()
|
|
||||||
this.failedProgressSyncs = 0
|
this.failedProgressSyncs = 0
|
||||||
}
|
})
|
||||||
})
|
.catch((error) => {
|
||||||
|
console.error('Failed to update session progress', error)
|
||||||
|
// After 4 failed sync attempts show an alert toast
|
||||||
|
this.failedProgressSyncs++
|
||||||
|
if (this.failedProgressSyncs >= 4) {
|
||||||
|
this.ctx.showFailedProgressSyncs()
|
||||||
|
this.failedProgressSyncs = 0
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
stopPlayInterval() {
|
stopPlayInterval() {
|
||||||
@ -381,13 +391,15 @@ export default class PlayerHandler {
|
|||||||
jumpBackward() {
|
jumpBackward() {
|
||||||
if (!this.player) return
|
if (!this.player) return
|
||||||
var currentTime = this.getCurrentTime()
|
var currentTime = this.getCurrentTime()
|
||||||
this.seek(Math.max(0, currentTime - 10))
|
const jumpAmount = this.jumpBackwardAmount
|
||||||
|
this.seek(Math.max(0, currentTime - jumpAmount))
|
||||||
}
|
}
|
||||||
|
|
||||||
jumpForward() {
|
jumpForward() {
|
||||||
if (!this.player) return
|
if (!this.player) return
|
||||||
var currentTime = this.getCurrentTime()
|
var currentTime = this.getCurrentTime()
|
||||||
this.seek(Math.min(currentTime + 10, this.getDuration()))
|
const jumpAmount = this.jumpForwardAmount
|
||||||
|
this.seek(Math.min(currentTime + jumpAmount, this.getDuration()))
|
||||||
}
|
}
|
||||||
|
|
||||||
setVolume(volume) {
|
setVolume(volume) {
|
||||||
@ -411,4 +423,4 @@ export default class PlayerHandler {
|
|||||||
this.sendProgressSync(time)
|
this.sendProgressSync(time)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,12 +32,18 @@ const PlayMethod = {
|
|||||||
LOCAL: 3
|
LOCAL: 3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const SleepTimerTypes = {
|
||||||
|
COUNTDOWN: 'countdown',
|
||||||
|
CHAPTER: 'chapter'
|
||||||
|
}
|
||||||
|
|
||||||
const Constants = {
|
const Constants = {
|
||||||
SupportedFileTypes,
|
SupportedFileTypes,
|
||||||
DownloadStatus,
|
DownloadStatus,
|
||||||
BookCoverAspectRatio,
|
BookCoverAspectRatio,
|
||||||
BookshelfView,
|
BookshelfView,
|
||||||
PlayMethod
|
PlayMethod,
|
||||||
|
SleepTimerTypes
|
||||||
}
|
}
|
||||||
|
|
||||||
const KeyNames = {
|
const KeyNames = {
|
||||||
|
@ -6,7 +6,6 @@ import * as locale from 'date-fns/locale'
|
|||||||
|
|
||||||
Vue.directive('click-outside', vClickOutside.directive)
|
Vue.directive('click-outside', vClickOutside.directive)
|
||||||
|
|
||||||
|
|
||||||
Vue.prototype.$setDateFnsLocale = (localeString) => {
|
Vue.prototype.$setDateFnsLocale = (localeString) => {
|
||||||
if (!locale[localeString]) return 0
|
if (!locale[localeString]) return 0
|
||||||
return setDefaultOptions({ locale: locale[localeString] })
|
return setDefaultOptions({ locale: locale[localeString] })
|
||||||
@ -112,14 +111,15 @@ Vue.prototype.$sanitizeSlug = (str) => {
|
|||||||
str = str.toLowerCase()
|
str = str.toLowerCase()
|
||||||
|
|
||||||
// remove accents, swap ñ for n, etc
|
// remove accents, swap ñ for n, etc
|
||||||
var from = "àáäâèéëêìíïîòóöôùúüûñçěščřžýúůďťň·/,:;"
|
var from = 'àáäâèéëêìíïîòóöôùúüûñçěščřžýúůďťň·/,:;'
|
||||||
var to = "aaaaeeeeiiiioooouuuuncescrzyuudtn-----"
|
var to = 'aaaaeeeeiiiioooouuuuncescrzyuudtn-----'
|
||||||
|
|
||||||
for (var i = 0, l = from.length; i < l; i++) {
|
for (var i = 0, l = from.length; i < l; i++) {
|
||||||
str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i))
|
str = str.replace(new RegExp(from.charAt(i), 'g'), to.charAt(i))
|
||||||
}
|
}
|
||||||
|
|
||||||
str = str.replace('.', '-') // replace a dot by a dash
|
str = str
|
||||||
|
.replace('.', '-') // replace a dot by a dash
|
||||||
.replace(/[^a-z0-9 -_]/g, '') // remove invalid chars
|
.replace(/[^a-z0-9 -_]/g, '') // remove invalid chars
|
||||||
.replace(/\s+/g, '-') // collapse whitespace and replace by a dash
|
.replace(/\s+/g, '-') // collapse whitespace and replace by a dash
|
||||||
.replace(/-+/g, '-') // collapse dashes
|
.replace(/-+/g, '-') // collapse dashes
|
||||||
@ -131,13 +131,16 @@ Vue.prototype.$sanitizeSlug = (str) => {
|
|||||||
Vue.prototype.$copyToClipboard = (str, ctx) => {
|
Vue.prototype.$copyToClipboard = (str, ctx) => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (navigator.clipboard) {
|
if (navigator.clipboard) {
|
||||||
navigator.clipboard.writeText(str).then(() => {
|
navigator.clipboard.writeText(str).then(
|
||||||
if (ctx) ctx.$toast.success('Copied to clipboard')
|
() => {
|
||||||
resolve(true)
|
if (ctx) ctx.$toast.success('Copied to clipboard')
|
||||||
}, (err) => {
|
resolve(true)
|
||||||
console.error('Clipboard copy failed', str, err)
|
},
|
||||||
resolve(false)
|
(err) => {
|
||||||
})
|
console.error('Clipboard copy failed', str, err)
|
||||||
|
resolve(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
const el = document.createElement('textarea')
|
const el = document.createElement('textarea')
|
||||||
el.value = str
|
el.value = str
|
||||||
@ -160,26 +163,18 @@ function xmlToJson(xml) {
|
|||||||
for (const res of xml.matchAll(/(?:<(\w*)(?:\s[^>]*)*>)((?:(?!<\1).)*)(?:<\/\1>)|<(\w*)(?:\s*)*\/>/gm)) {
|
for (const res of xml.matchAll(/(?:<(\w*)(?:\s[^>]*)*>)((?:(?!<\1).)*)(?:<\/\1>)|<(\w*)(?:\s*)*\/>/gm)) {
|
||||||
const key = res[1] || res[3]
|
const key = res[1] || res[3]
|
||||||
const value = res[2] && xmlToJson(res[2])
|
const value = res[2] && xmlToJson(res[2])
|
||||||
json[key] = ((value && Object.keys(value).length) ? value : res[2]) || null
|
json[key] = (value && Object.keys(value).length ? value : res[2]) || null
|
||||||
|
|
||||||
}
|
}
|
||||||
return json
|
return json
|
||||||
}
|
}
|
||||||
Vue.prototype.$xmlToJson = xmlToJson
|
Vue.prototype.$xmlToJson = xmlToJson
|
||||||
|
|
||||||
Vue.prototype.$encodeUriPath = (path) => {
|
|
||||||
return path.replace(/\\/g, '/').replace(/%/g, '%25').replace(/#/g, '%23')
|
|
||||||
}
|
|
||||||
|
|
||||||
const encode = (text) => encodeURIComponent(Buffer.from(text).toString('base64'))
|
const encode = (text) => encodeURIComponent(Buffer.from(text).toString('base64'))
|
||||||
Vue.prototype.$encode = encode
|
Vue.prototype.$encode = encode
|
||||||
const decode = (text) => Buffer.from(decodeURIComponent(text), 'base64').toString()
|
const decode = (text) => Buffer.from(decodeURIComponent(text), 'base64').toString()
|
||||||
Vue.prototype.$decode = decode
|
Vue.prototype.$decode = decode
|
||||||
|
|
||||||
export {
|
export { encode, decode }
|
||||||
encode,
|
|
||||||
decode
|
|
||||||
}
|
|
||||||
export default ({ app, store }, inject) => {
|
export default ({ app, store }, inject) => {
|
||||||
app.$decode = decode
|
app.$decode = decode
|
||||||
app.$encode = encode
|
app.$encode = encode
|
||||||
|
@ -14,7 +14,9 @@ export const state = () => ({
|
|||||||
seriesSortDesc: false,
|
seriesSortDesc: false,
|
||||||
seriesFilterBy: 'all',
|
seriesFilterBy: 'all',
|
||||||
authorSortBy: 'name',
|
authorSortBy: 'name',
|
||||||
authorSortDesc: false
|
authorSortDesc: false,
|
||||||
|
jumpForwardAmount: 10,
|
||||||
|
jumpBackwardAmount: 10,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -59,6 +59,7 @@
|
|||||||
"ButtonPurgeItemsCache": "Lösche Medien-Cache",
|
"ButtonPurgeItemsCache": "Lösche Medien-Cache",
|
||||||
"ButtonQueueAddItem": "Zur Warteschlange hinzufügen",
|
"ButtonQueueAddItem": "Zur Warteschlange hinzufügen",
|
||||||
"ButtonQueueRemoveItem": "Aus der Warteschlange entfernen",
|
"ButtonQueueRemoveItem": "Aus der Warteschlange entfernen",
|
||||||
|
"ButtonQuickEmbedMetadata": "Schnelles Hinzufügen von Metadaten",
|
||||||
"ButtonQuickMatch": "Schnellabgleich",
|
"ButtonQuickMatch": "Schnellabgleich",
|
||||||
"ButtonReScan": "Neu scannen",
|
"ButtonReScan": "Neu scannen",
|
||||||
"ButtonRead": "Lesen",
|
"ButtonRead": "Lesen",
|
||||||
@ -66,11 +67,11 @@
|
|||||||
"ButtonReadMore": "Mehr anzeigen",
|
"ButtonReadMore": "Mehr anzeigen",
|
||||||
"ButtonRefresh": "Neu Laden",
|
"ButtonRefresh": "Neu Laden",
|
||||||
"ButtonRemove": "Entfernen",
|
"ButtonRemove": "Entfernen",
|
||||||
"ButtonRemoveAll": "Alles löschen",
|
"ButtonRemoveAll": "Alles entfernen",
|
||||||
"ButtonRemoveAllLibraryItems": "Lösche alle Bibliothekseinträge",
|
"ButtonRemoveAllLibraryItems": "Entferne alle Bibliothekseinträge",
|
||||||
"ButtonRemoveFromContinueListening": "Lösche den Eintrag aus der Fortsetzungsliste",
|
"ButtonRemoveFromContinueListening": "Entferne den Eintrag aus der Fortsetzungsliste",
|
||||||
"ButtonRemoveFromContinueReading": "Lösche die Serie aus der Lesefortsetzungsliste",
|
"ButtonRemoveFromContinueReading": "Entferne die Serie aus der Lesefortsetzungsliste",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Lösche die Serie aus der Serienfortsetzungsliste",
|
"ButtonRemoveSeriesFromContinueSeries": "Entferne die Serie aus der Serienfortsetzungsliste",
|
||||||
"ButtonReset": "Zurücksetzen",
|
"ButtonReset": "Zurücksetzen",
|
||||||
"ButtonResetToDefault": "Zurücksetzen auf Standard",
|
"ButtonResetToDefault": "Zurücksetzen auf Standard",
|
||||||
"ButtonRestore": "Wiederherstellen",
|
"ButtonRestore": "Wiederherstellen",
|
||||||
@ -88,6 +89,7 @@
|
|||||||
"ButtonShow": "Anzeigen",
|
"ButtonShow": "Anzeigen",
|
||||||
"ButtonStartM4BEncode": "M4B-Kodierung starten",
|
"ButtonStartM4BEncode": "M4B-Kodierung starten",
|
||||||
"ButtonStartMetadataEmbed": "Metadateneinbettung starten",
|
"ButtonStartMetadataEmbed": "Metadateneinbettung starten",
|
||||||
|
"ButtonStats": "Statistiken",
|
||||||
"ButtonSubmit": "Ok",
|
"ButtonSubmit": "Ok",
|
||||||
"ButtonTest": "Test",
|
"ButtonTest": "Test",
|
||||||
"ButtonUpload": "Hochladen",
|
"ButtonUpload": "Hochladen",
|
||||||
@ -154,6 +156,7 @@
|
|||||||
"HeaderPasswordAuthentication": "Passwort Authentifizierung",
|
"HeaderPasswordAuthentication": "Passwort Authentifizierung",
|
||||||
"HeaderPermissions": "Berechtigungen",
|
"HeaderPermissions": "Berechtigungen",
|
||||||
"HeaderPlayerQueue": "Player Warteschlange",
|
"HeaderPlayerQueue": "Player Warteschlange",
|
||||||
|
"HeaderPlayerSettings": "Player Einstellungen",
|
||||||
"HeaderPlaylist": "Wiedergabeliste",
|
"HeaderPlaylist": "Wiedergabeliste",
|
||||||
"HeaderPlaylistItems": "Einträge in der Wiedergabeliste",
|
"HeaderPlaylistItems": "Einträge in der Wiedergabeliste",
|
||||||
"HeaderPodcastsToAdd": "Podcasts zum Hinzufügen",
|
"HeaderPodcastsToAdd": "Podcasts zum Hinzufügen",
|
||||||
@ -161,8 +164,8 @@
|
|||||||
"HeaderRSSFeedGeneral": "RSS Details",
|
"HeaderRSSFeedGeneral": "RSS Details",
|
||||||
"HeaderRSSFeedIsOpen": "RSS-Feed ist geöffnet",
|
"HeaderRSSFeedIsOpen": "RSS-Feed ist geöffnet",
|
||||||
"HeaderRSSFeeds": "RSS-Feeds",
|
"HeaderRSSFeeds": "RSS-Feeds",
|
||||||
"HeaderRemoveEpisode": "Episode löschen",
|
"HeaderRemoveEpisode": "Episode entfernen",
|
||||||
"HeaderRemoveEpisodes": "Lösche {0} Episoden",
|
"HeaderRemoveEpisodes": "Entferne {0} Episoden",
|
||||||
"HeaderSavedMediaProgress": "Gespeicherte Hörfortschritte",
|
"HeaderSavedMediaProgress": "Gespeicherte Hörfortschritte",
|
||||||
"HeaderSchedule": "Zeitplan",
|
"HeaderSchedule": "Zeitplan",
|
||||||
"HeaderScheduleLibraryScans": "Automatische Bibliotheksscans",
|
"HeaderScheduleLibraryScans": "Automatische Bibliotheksscans",
|
||||||
@ -259,7 +262,7 @@
|
|||||||
"LabelCustomCronExpression": "Benutzerdefinierter Cron-Ausdruck:",
|
"LabelCustomCronExpression": "Benutzerdefinierter Cron-Ausdruck:",
|
||||||
"LabelDatetime": "Datum & Uhrzeit",
|
"LabelDatetime": "Datum & Uhrzeit",
|
||||||
"LabelDays": "Tage",
|
"LabelDays": "Tage",
|
||||||
"LabelDeleteFromFileSystemCheckbox": "Löschen von der Festplatte + Datenbank (deaktivieren um nur aus der Datenbank zu löschen)",
|
"LabelDeleteFromFileSystemCheckbox": "Löschen von der Festplatte + Datenbank (deaktivieren um nur aus der Datenbank zu entfernen)",
|
||||||
"LabelDescription": "Beschreibung",
|
"LabelDescription": "Beschreibung",
|
||||||
"LabelDeselectAll": "Alles abwählen",
|
"LabelDeselectAll": "Alles abwählen",
|
||||||
"LabelDevice": "Gerät",
|
"LabelDevice": "Gerät",
|
||||||
@ -289,13 +292,16 @@
|
|||||||
"LabelEmbeddedCover": "Eingebettetes Cover",
|
"LabelEmbeddedCover": "Eingebettetes Cover",
|
||||||
"LabelEnable": "Aktivieren",
|
"LabelEnable": "Aktivieren",
|
||||||
"LabelEnd": "Ende",
|
"LabelEnd": "Ende",
|
||||||
|
"LabelEndOfChapter": "Ende des Kapitels",
|
||||||
"LabelEpisode": "Episode",
|
"LabelEpisode": "Episode",
|
||||||
"LabelEpisodeTitle": "Episodentitel",
|
"LabelEpisodeTitle": "Episodentitel",
|
||||||
"LabelEpisodeType": "Episodentyp",
|
"LabelEpisodeType": "Episodentyp",
|
||||||
"LabelExample": "Beispiel",
|
"LabelExample": "Beispiel",
|
||||||
|
"LabelExpandSeries": "Serie erweitern",
|
||||||
"LabelExplicit": "Explizit (Altersbeschränkung)",
|
"LabelExplicit": "Explizit (Altersbeschränkung)",
|
||||||
"LabelExplicitChecked": "Explicit (Altersbeschränkung) (angehakt)",
|
"LabelExplicitChecked": "Explicit (Altersbeschränkung) (angehakt)",
|
||||||
"LabelExplicitUnchecked": "Not Explicit (Altersbeschränkung) (nicht angehakt)",
|
"LabelExplicitUnchecked": "Not Explicit (Altersbeschränkung) (nicht angehakt)",
|
||||||
|
"LabelExportOPML": "OPML exportieren",
|
||||||
"LabelFeedURL": "Feed URL",
|
"LabelFeedURL": "Feed URL",
|
||||||
"LabelFetchingMetadata": "Abholen der Metadaten",
|
"LabelFetchingMetadata": "Abholen der Metadaten",
|
||||||
"LabelFile": "Datei",
|
"LabelFile": "Datei",
|
||||||
@ -319,6 +325,7 @@
|
|||||||
"LabelHardDeleteFile": "Datei dauerhaft löschen",
|
"LabelHardDeleteFile": "Datei dauerhaft löschen",
|
||||||
"LabelHasEbook": "E-Book verfügbar",
|
"LabelHasEbook": "E-Book verfügbar",
|
||||||
"LabelHasSupplementaryEbook": "Ergänzendes E-Book verfügbar",
|
"LabelHasSupplementaryEbook": "Ergänzendes E-Book verfügbar",
|
||||||
|
"LabelHideSubtitles": "Untertitel ausblenden",
|
||||||
"LabelHighestPriority": "Höchste Priorität",
|
"LabelHighestPriority": "Höchste Priorität",
|
||||||
"LabelHost": "Host",
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Stunde",
|
"LabelHour": "Stunde",
|
||||||
@ -339,6 +346,8 @@
|
|||||||
"LabelIntervalEveryHour": "Jede Stunde",
|
"LabelIntervalEveryHour": "Jede Stunde",
|
||||||
"LabelInvert": "Umkehren",
|
"LabelInvert": "Umkehren",
|
||||||
"LabelItem": "Medium",
|
"LabelItem": "Medium",
|
||||||
|
"LabelJumpBackwardAmount": "Zurückspringen Zeit",
|
||||||
|
"LabelJumpForwardAmount": "Vorwärtsspringn Zeit",
|
||||||
"LabelLanguage": "Sprache",
|
"LabelLanguage": "Sprache",
|
||||||
"LabelLanguageDefaultServer": "Standard-Server-Sprache",
|
"LabelLanguageDefaultServer": "Standard-Server-Sprache",
|
||||||
"LabelLanguages": "Sprachen",
|
"LabelLanguages": "Sprachen",
|
||||||
@ -446,6 +455,7 @@
|
|||||||
"LabelRSSFeedPreventIndexing": "Indizierung verhindern",
|
"LabelRSSFeedPreventIndexing": "Indizierung verhindern",
|
||||||
"LabelRSSFeedSlug": "RSS-Feed-Schlagwort",
|
"LabelRSSFeedSlug": "RSS-Feed-Schlagwort",
|
||||||
"LabelRSSFeedURL": "RSS Feed URL",
|
"LabelRSSFeedURL": "RSS Feed URL",
|
||||||
|
"LabelReAddSeriesToContinueListening": "Serien erneut zur Fortsetzungsliste hinzufügen",
|
||||||
"LabelRead": "Lesen",
|
"LabelRead": "Lesen",
|
||||||
"LabelReadAgain": "Noch einmal Lesen",
|
"LabelReadAgain": "Noch einmal Lesen",
|
||||||
"LabelReadEbookWithoutProgress": "E-Book lesen und Fortschritt verwerfen",
|
"LabelReadEbookWithoutProgress": "E-Book lesen und Fortschritt verwerfen",
|
||||||
@ -455,7 +465,7 @@
|
|||||||
"LabelRedo": "Wiederholen",
|
"LabelRedo": "Wiederholen",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Veröffentlichungsdatum",
|
"LabelReleaseDate": "Veröffentlichungsdatum",
|
||||||
"LabelRemoveCover": "Lösche Titelbild",
|
"LabelRemoveCover": "Entferne Titelbild",
|
||||||
"LabelRowsPerPage": "Zeilen pro Seite",
|
"LabelRowsPerPage": "Zeilen pro Seite",
|
||||||
"LabelSearchTerm": "Begriff suchen",
|
"LabelSearchTerm": "Begriff suchen",
|
||||||
"LabelSearchTitle": "Titel suchen",
|
"LabelSearchTitle": "Titel suchen",
|
||||||
@ -512,10 +522,11 @@
|
|||||||
"LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet",
|
"LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet",
|
||||||
"LabelSettingsTimeFormat": "Zeitformat",
|
"LabelSettingsTimeFormat": "Zeitformat",
|
||||||
"LabelShare": "Teilen",
|
"LabelShare": "Teilen",
|
||||||
"LabelShareOpen": "Teilen Offen",
|
"LabelShareOpen": "Teilen öffnen",
|
||||||
"LabelShareURL": "URL teilen",
|
"LabelShareURL": "URL teilen",
|
||||||
"LabelShowAll": "Alles anzeigen",
|
"LabelShowAll": "Alles anzeigen",
|
||||||
"LabelShowSeconds": "Zeige Sekunden",
|
"LabelShowSeconds": "Zeige Sekunden",
|
||||||
|
"LabelShowSubtitles": "Untertitel anzeigen",
|
||||||
"LabelSize": "Größe",
|
"LabelSize": "Größe",
|
||||||
"LabelSleepTimer": "Schlummerfunktion",
|
"LabelSleepTimer": "Schlummerfunktion",
|
||||||
"LabelSlug": "URL Teil",
|
"LabelSlug": "URL Teil",
|
||||||
@ -553,6 +564,10 @@
|
|||||||
"LabelThemeDark": "Dunkel",
|
"LabelThemeDark": "Dunkel",
|
||||||
"LabelThemeLight": "Hell",
|
"LabelThemeLight": "Hell",
|
||||||
"LabelTimeBase": "Basiszeit",
|
"LabelTimeBase": "Basiszeit",
|
||||||
|
"LabelTimeDurationXHours": "{0} Stunden",
|
||||||
|
"LabelTimeDurationXMinutes": "{0} Minuten",
|
||||||
|
"LabelTimeDurationXSeconds": "{0} Sekunden",
|
||||||
|
"LabelTimeInMinutes": "Zeit in Minuten",
|
||||||
"LabelTimeListened": "Gehörte Zeit",
|
"LabelTimeListened": "Gehörte Zeit",
|
||||||
"LabelTimeListenedToday": "Heute gehörte Zeit",
|
"LabelTimeListenedToday": "Heute gehörte Zeit",
|
||||||
"LabelTimeRemaining": "{0} verbleibend",
|
"LabelTimeRemaining": "{0} verbleibend",
|
||||||
@ -592,6 +607,7 @@
|
|||||||
"LabelVersion": "Version",
|
"LabelVersion": "Version",
|
||||||
"LabelViewBookmarks": "Lesezeichen anzeigen",
|
"LabelViewBookmarks": "Lesezeichen anzeigen",
|
||||||
"LabelViewChapters": "Kapitel anzeigen",
|
"LabelViewChapters": "Kapitel anzeigen",
|
||||||
|
"LabelViewPlayerSettings": "Zeige player Einstellungen",
|
||||||
"LabelViewQueue": "Player-Warteschlange anzeigen",
|
"LabelViewQueue": "Player-Warteschlange anzeigen",
|
||||||
"LabelVolume": "Lautstärke",
|
"LabelVolume": "Lautstärke",
|
||||||
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
||||||
@ -637,11 +653,11 @@
|
|||||||
"MessageConfirmReScanLibraryItems": "{0} Elemente werden erneut gescannt! Bist du dir sicher?",
|
"MessageConfirmReScanLibraryItems": "{0} Elemente werden erneut gescannt! Bist du dir sicher?",
|
||||||
"MessageConfirmRemoveAllChapters": "Alle Kapitel werden entfernt! Bist du dir sicher?",
|
"MessageConfirmRemoveAllChapters": "Alle Kapitel werden entfernt! Bist du dir sicher?",
|
||||||
"MessageConfirmRemoveAuthor": "Autor \"{0}\" wird enfernt! Bist du dir sicher?",
|
"MessageConfirmRemoveAuthor": "Autor \"{0}\" wird enfernt! Bist du dir sicher?",
|
||||||
"MessageConfirmRemoveCollection": "Sammlung \"{0}\" wird gelöscht! Bist du dir sicher?",
|
"MessageConfirmRemoveCollection": "Sammlung \"{0}\" wird entfernt! Bist du dir sicher?",
|
||||||
"MessageConfirmRemoveEpisode": "Episode \"{0}\" wird geloscht! Bist du dir sicher?",
|
"MessageConfirmRemoveEpisode": "Episode \"{0}\" wird entfernt! Bist du dir sicher?",
|
||||||
"MessageConfirmRemoveEpisodes": "{0} Episoden werden gelöscht! Bist du dir sicher?",
|
"MessageConfirmRemoveEpisodes": "{0} Episoden werden entfernt! Bist du dir sicher?",
|
||||||
"MessageConfirmRemoveListeningSessions": "Bist du dir sicher, dass du {0} Hörsitzungen enfernen möchtest?",
|
"MessageConfirmRemoveListeningSessions": "Bist du dir sicher, dass du {0} Hörsitzungen enfernen möchtest?",
|
||||||
"MessageConfirmRemoveNarrator": "Erzähler \"{0}\" wird gelöscht! Bist du dir sicher?",
|
"MessageConfirmRemoveNarrator": "Erzähler \"{0}\" wird entfernt! Bist du dir sicher?",
|
||||||
"MessageConfirmRemovePlaylist": "Wiedergabeliste \"{0}\" wird entfernt! Bist du dir sicher?",
|
"MessageConfirmRemovePlaylist": "Wiedergabeliste \"{0}\" wird entfernt! Bist du dir sicher?",
|
||||||
"MessageConfirmRenameGenre": "Kategorie \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Bist du dir sicher?",
|
"MessageConfirmRenameGenre": "Kategorie \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Bist du dir sicher?",
|
||||||
"MessageConfirmRenameGenreMergeNote": "Hinweis: Kategorie existiert bereits -> Kategorien werden zusammengelegt.",
|
"MessageConfirmRenameGenreMergeNote": "Hinweis: Kategorie existiert bereits -> Kategorien werden zusammengelegt.",
|
||||||
@ -712,9 +728,9 @@
|
|||||||
"MessagePlaylistCreateFromCollection": "Erstelle eine Wiedergabeliste aus der Sammlung",
|
"MessagePlaylistCreateFromCollection": "Erstelle eine Wiedergabeliste aus der Sammlung",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Der Podcast hat keine RSS-Feed-Url welche für den Online-Abgleich verwendet werden kann",
|
"MessagePodcastHasNoRSSFeedForMatching": "Der Podcast hat keine RSS-Feed-Url welche für den Online-Abgleich verwendet werden kann",
|
||||||
"MessageQuickMatchDescription": "Füllt leere Details und Titelbilder mit dem ersten Treffer aus '{0}'. Überschreibt keine Details, es sei denn, die Server-Einstellung \"Passende Metadaten bevorzugen\" ist aktiviert.",
|
"MessageQuickMatchDescription": "Füllt leere Details und Titelbilder mit dem ersten Treffer aus '{0}'. Überschreibt keine Details, es sei denn, die Server-Einstellung \"Passende Metadaten bevorzugen\" ist aktiviert.",
|
||||||
"MessageRemoveChapter": "Kapitel löschen",
|
"MessageRemoveChapter": "Kapitel entfernen",
|
||||||
"MessageRemoveEpisodes": "Entferne {0} Episode(n)",
|
"MessageRemoveEpisodes": "Entferne {0} Episode(n)",
|
||||||
"MessageRemoveFromPlayerQueue": "Aus der Abspielwarteliste löschen",
|
"MessageRemoveFromPlayerQueue": "Aus der Abspielwarteliste entfernen",
|
||||||
"MessageRemoveUserWarning": "Benutzer \"{0}\" wird dauerhaft gelöscht! Bist du dir sicher?",
|
"MessageRemoveUserWarning": "Benutzer \"{0}\" wird dauerhaft gelöscht! Bist du dir sicher?",
|
||||||
"MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und mitwirken",
|
"MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und mitwirken",
|
||||||
"MessageResetChaptersConfirm": "Kapitel und vorgenommenen Änderungen werden zurückgesetzt und rückgängig gemacht! Bist du dir sicher?",
|
"MessageResetChaptersConfirm": "Kapitel und vorgenommenen Änderungen werden zurückgesetzt und rückgängig gemacht! Bist du dir sicher?",
|
||||||
@ -769,8 +785,8 @@
|
|||||||
"ToastBatchUpdateSuccess": "Stapelaktualisierung erfolgreich",
|
"ToastBatchUpdateSuccess": "Stapelaktualisierung erfolgreich",
|
||||||
"ToastBookmarkCreateFailed": "Lesezeichen konnte nicht erstellt werden",
|
"ToastBookmarkCreateFailed": "Lesezeichen konnte nicht erstellt werden",
|
||||||
"ToastBookmarkCreateSuccess": "Lesezeichen hinzugefügt",
|
"ToastBookmarkCreateSuccess": "Lesezeichen hinzugefügt",
|
||||||
"ToastBookmarkRemoveFailed": "Lesezeichen konnte nicht gelöscht werden",
|
"ToastBookmarkRemoveFailed": "Lesezeichen konnte nicht entfernt werden",
|
||||||
"ToastBookmarkRemoveSuccess": "Lesezeichen gelöscht",
|
"ToastBookmarkRemoveSuccess": "Lesezeichen entfernt",
|
||||||
"ToastBookmarkUpdateFailed": "Lesezeichenaktualisierung fehlgeschlagen",
|
"ToastBookmarkUpdateFailed": "Lesezeichenaktualisierung fehlgeschlagen",
|
||||||
"ToastBookmarkUpdateSuccess": "Lesezeichen aktualisiert",
|
"ToastBookmarkUpdateSuccess": "Lesezeichen aktualisiert",
|
||||||
"ToastCachePurgeFailed": "Cache leeren fehlgeschlagen",
|
"ToastCachePurgeFailed": "Cache leeren fehlgeschlagen",
|
||||||
@ -780,7 +796,7 @@
|
|||||||
"ToastCollectionItemsRemoveFailed": "Fehler beim Entfernen der Medien aus der Sammlung",
|
"ToastCollectionItemsRemoveFailed": "Fehler beim Entfernen der Medien aus der Sammlung",
|
||||||
"ToastCollectionItemsRemoveSuccess": "Medien aus der Sammlung entfernt",
|
"ToastCollectionItemsRemoveSuccess": "Medien aus der Sammlung entfernt",
|
||||||
"ToastCollectionRemoveFailed": "Sammlung konnte nicht entfernt werden",
|
"ToastCollectionRemoveFailed": "Sammlung konnte nicht entfernt werden",
|
||||||
"ToastCollectionRemoveSuccess": "Sammlung gelöscht",
|
"ToastCollectionRemoveSuccess": "Sammlung entfernt",
|
||||||
"ToastCollectionUpdateFailed": "Sammlung konnte nicht aktualisiert werden",
|
"ToastCollectionUpdateFailed": "Sammlung konnte nicht aktualisiert werden",
|
||||||
"ToastCollectionUpdateSuccess": "Sammlung aktualisiert",
|
"ToastCollectionUpdateSuccess": "Sammlung aktualisiert",
|
||||||
"ToastDeleteFileFailed": "Die Datei konnte nicht gelöscht werden",
|
"ToastDeleteFileFailed": "Die Datei konnte nicht gelöscht werden",
|
||||||
|
@ -89,6 +89,7 @@
|
|||||||
"ButtonShow": "Show",
|
"ButtonShow": "Show",
|
||||||
"ButtonStartM4BEncode": "Start M4B Encode",
|
"ButtonStartM4BEncode": "Start M4B Encode",
|
||||||
"ButtonStartMetadataEmbed": "Start Metadata Embed",
|
"ButtonStartMetadataEmbed": "Start Metadata Embed",
|
||||||
|
"ButtonStats": "Stats",
|
||||||
"ButtonSubmit": "Submit",
|
"ButtonSubmit": "Submit",
|
||||||
"ButtonTest": "Test",
|
"ButtonTest": "Test",
|
||||||
"ButtonUpload": "Upload",
|
"ButtonUpload": "Upload",
|
||||||
@ -155,6 +156,7 @@
|
|||||||
"HeaderPasswordAuthentication": "Password Authentication",
|
"HeaderPasswordAuthentication": "Password Authentication",
|
||||||
"HeaderPermissions": "Permissions",
|
"HeaderPermissions": "Permissions",
|
||||||
"HeaderPlayerQueue": "Player Queue",
|
"HeaderPlayerQueue": "Player Queue",
|
||||||
|
"HeaderPlayerSettings": "Player Settings",
|
||||||
"HeaderPlaylist": "Playlist",
|
"HeaderPlaylist": "Playlist",
|
||||||
"HeaderPlaylistItems": "Playlist Items",
|
"HeaderPlaylistItems": "Playlist Items",
|
||||||
"HeaderPodcastsToAdd": "Podcasts to Add",
|
"HeaderPodcastsToAdd": "Podcasts to Add",
|
||||||
@ -227,7 +229,7 @@
|
|||||||
"LabelBackupLocation": "Backup Location",
|
"LabelBackupLocation": "Backup Location",
|
||||||
"LabelBackupsEnableAutomaticBackups": "Enable automatic backups",
|
"LabelBackupsEnableAutomaticBackups": "Enable automatic backups",
|
||||||
"LabelBackupsEnableAutomaticBackupsHelp": "Backups saved in /metadata/backups",
|
"LabelBackupsEnableAutomaticBackupsHelp": "Backups saved in /metadata/backups",
|
||||||
"LabelBackupsMaxBackupSize": "Maximum backup size (in GB)",
|
"LabelBackupsMaxBackupSize": "Maximum backup size (in GB) (0 for unlimited)",
|
||||||
"LabelBackupsMaxBackupSizeHelp": "As a safeguard against misconfiguration, backups will fail if they exceed the configured size.",
|
"LabelBackupsMaxBackupSizeHelp": "As a safeguard against misconfiguration, backups will fail if they exceed the configured size.",
|
||||||
"LabelBackupsNumberToKeep": "Number of backups to keep",
|
"LabelBackupsNumberToKeep": "Number of backups to keep",
|
||||||
"LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.",
|
"LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.",
|
||||||
@ -290,6 +292,7 @@
|
|||||||
"LabelEmbeddedCover": "Embedded Cover",
|
"LabelEmbeddedCover": "Embedded Cover",
|
||||||
"LabelEnable": "Enable",
|
"LabelEnable": "Enable",
|
||||||
"LabelEnd": "End",
|
"LabelEnd": "End",
|
||||||
|
"LabelEndOfChapter": "End of Chapter",
|
||||||
"LabelEpisode": "Episode",
|
"LabelEpisode": "Episode",
|
||||||
"LabelEpisodeTitle": "Episode Title",
|
"LabelEpisodeTitle": "Episode Title",
|
||||||
"LabelEpisodeType": "Episode Type",
|
"LabelEpisodeType": "Episode Type",
|
||||||
@ -343,6 +346,8 @@
|
|||||||
"LabelIntervalEveryHour": "Every hour",
|
"LabelIntervalEveryHour": "Every hour",
|
||||||
"LabelInvert": "Invert",
|
"LabelInvert": "Invert",
|
||||||
"LabelItem": "Item",
|
"LabelItem": "Item",
|
||||||
|
"LabelJumpBackwardAmount": "Jump backward amount",
|
||||||
|
"LabelJumpForwardAmount": "Jump forward amount",
|
||||||
"LabelLanguage": "Language",
|
"LabelLanguage": "Language",
|
||||||
"LabelLanguageDefaultServer": "Default Server Language",
|
"LabelLanguageDefaultServer": "Default Server Language",
|
||||||
"LabelLanguages": "Languages",
|
"LabelLanguages": "Languages",
|
||||||
@ -559,6 +564,10 @@
|
|||||||
"LabelThemeDark": "Dark",
|
"LabelThemeDark": "Dark",
|
||||||
"LabelThemeLight": "Light",
|
"LabelThemeLight": "Light",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "Time Base",
|
||||||
|
"LabelTimeDurationXHours": "{0} hours",
|
||||||
|
"LabelTimeDurationXMinutes": "{0} minutes",
|
||||||
|
"LabelTimeDurationXSeconds": "{0} seconds",
|
||||||
|
"LabelTimeInMinutes": "Time in minutes",
|
||||||
"LabelTimeListened": "Time Listened",
|
"LabelTimeListened": "Time Listened",
|
||||||
"LabelTimeListenedToday": "Time Listened Today",
|
"LabelTimeListenedToday": "Time Listened Today",
|
||||||
"LabelTimeRemaining": "{0} remaining",
|
"LabelTimeRemaining": "{0} remaining",
|
||||||
@ -598,6 +607,7 @@
|
|||||||
"LabelVersion": "Version",
|
"LabelVersion": "Version",
|
||||||
"LabelViewBookmarks": "View bookmarks",
|
"LabelViewBookmarks": "View bookmarks",
|
||||||
"LabelViewChapters": "View chapters",
|
"LabelViewChapters": "View chapters",
|
||||||
|
"LabelViewPlayerSettings": "View player settings",
|
||||||
"LabelViewQueue": "View player queue",
|
"LabelViewQueue": "View player queue",
|
||||||
"LabelVolume": "Volume",
|
"LabelVolume": "Volume",
|
||||||
"LabelWeekdaysToRun": "Weekdays to run",
|
"LabelWeekdaysToRun": "Weekdays to run",
|
||||||
@ -713,6 +723,7 @@
|
|||||||
"MessageNoUpdatesWereNecessary": "No updates were necessary",
|
"MessageNoUpdatesWereNecessary": "No updates were necessary",
|
||||||
"MessageNoUserPlaylists": "You have no playlists",
|
"MessageNoUserPlaylists": "You have no playlists",
|
||||||
"MessageNotYetImplemented": "Not yet implemented",
|
"MessageNotYetImplemented": "Not yet implemented",
|
||||||
|
"MessageOpmlPreviewNote": "Note: This is a preview of the parsed OPML file. The actual podcast title will be taken from the RSS feed.",
|
||||||
"MessageOr": "or",
|
"MessageOr": "or",
|
||||||
"MessagePauseChapter": "Pause chapter playback",
|
"MessagePauseChapter": "Pause chapter playback",
|
||||||
"MessagePlayChapter": "Listen to beginning of chapter",
|
"MessagePlayChapter": "Listen to beginning of chapter",
|
||||||
|
@ -59,6 +59,7 @@
|
|||||||
"ButtonPurgeItemsCache": "Purgar Elementos de Cache",
|
"ButtonPurgeItemsCache": "Purgar Elementos de Cache",
|
||||||
"ButtonQueueAddItem": "Agregar a la Fila",
|
"ButtonQueueAddItem": "Agregar a la Fila",
|
||||||
"ButtonQueueRemoveItem": "Remover de la Fila",
|
"ButtonQueueRemoveItem": "Remover de la Fila",
|
||||||
|
"ButtonQuickEmbedMetadata": "Agregue metadatos rápidamente",
|
||||||
"ButtonQuickMatch": "Encontrar Rápido",
|
"ButtonQuickMatch": "Encontrar Rápido",
|
||||||
"ButtonReScan": "Re-Escanear",
|
"ButtonReScan": "Re-Escanear",
|
||||||
"ButtonRead": "Leer",
|
"ButtonRead": "Leer",
|
||||||
@ -88,6 +89,7 @@
|
|||||||
"ButtonShow": "Mostrar",
|
"ButtonShow": "Mostrar",
|
||||||
"ButtonStartM4BEncode": "Iniciar Codificación M4B",
|
"ButtonStartM4BEncode": "Iniciar Codificación M4B",
|
||||||
"ButtonStartMetadataEmbed": "Iniciar la Inserción de Metadata",
|
"ButtonStartMetadataEmbed": "Iniciar la Inserción de Metadata",
|
||||||
|
"ButtonStats": "Estadísticas",
|
||||||
"ButtonSubmit": "Enviar",
|
"ButtonSubmit": "Enviar",
|
||||||
"ButtonTest": "Prueba",
|
"ButtonTest": "Prueba",
|
||||||
"ButtonUpload": "Subir",
|
"ButtonUpload": "Subir",
|
||||||
@ -154,6 +156,7 @@
|
|||||||
"HeaderPasswordAuthentication": "Autenticación por contraseña",
|
"HeaderPasswordAuthentication": "Autenticación por contraseña",
|
||||||
"HeaderPermissions": "Permisos",
|
"HeaderPermissions": "Permisos",
|
||||||
"HeaderPlayerQueue": "Fila del Reproductor",
|
"HeaderPlayerQueue": "Fila del Reproductor",
|
||||||
|
"HeaderPlayerSettings": "Ajustes del reproductor",
|
||||||
"HeaderPlaylist": "Lista de reproducción",
|
"HeaderPlaylist": "Lista de reproducción",
|
||||||
"HeaderPlaylistItems": "Elementos de lista de reproducción",
|
"HeaderPlaylistItems": "Elementos de lista de reproducción",
|
||||||
"HeaderPodcastsToAdd": "Podcasts para agregar",
|
"HeaderPodcastsToAdd": "Podcasts para agregar",
|
||||||
@ -289,13 +292,16 @@
|
|||||||
"LabelEmbeddedCover": "Portada Integrada",
|
"LabelEmbeddedCover": "Portada Integrada",
|
||||||
"LabelEnable": "Habilitar",
|
"LabelEnable": "Habilitar",
|
||||||
"LabelEnd": "Fin",
|
"LabelEnd": "Fin",
|
||||||
|
"LabelEndOfChapter": "Fin del capítulo",
|
||||||
"LabelEpisode": "Episodio",
|
"LabelEpisode": "Episodio",
|
||||||
"LabelEpisodeTitle": "Titulo de Episodio",
|
"LabelEpisodeTitle": "Titulo de Episodio",
|
||||||
"LabelEpisodeType": "Tipo de Episodio",
|
"LabelEpisodeType": "Tipo de Episodio",
|
||||||
"LabelExample": "Ejemplo",
|
"LabelExample": "Ejemplo",
|
||||||
|
"LabelExpandSeries": "Ampliar serie",
|
||||||
"LabelExplicit": "Explicito",
|
"LabelExplicit": "Explicito",
|
||||||
"LabelExplicitChecked": "Explícito (marcado)",
|
"LabelExplicitChecked": "Explícito (marcado)",
|
||||||
"LabelExplicitUnchecked": "No Explícito (sin marcar)",
|
"LabelExplicitUnchecked": "No Explícito (sin marcar)",
|
||||||
|
"LabelExportOPML": "Exportar OPML",
|
||||||
"LabelFeedURL": "Fuente de URL",
|
"LabelFeedURL": "Fuente de URL",
|
||||||
"LabelFetchingMetadata": "Obteniendo metadatos",
|
"LabelFetchingMetadata": "Obteniendo metadatos",
|
||||||
"LabelFile": "Archivo",
|
"LabelFile": "Archivo",
|
||||||
@ -319,6 +325,7 @@
|
|||||||
"LabelHardDeleteFile": "Eliminar Definitivamente",
|
"LabelHardDeleteFile": "Eliminar Definitivamente",
|
||||||
"LabelHasEbook": "Tiene un libro",
|
"LabelHasEbook": "Tiene un libro",
|
||||||
"LabelHasSupplementaryEbook": "Tiene un libro complementario",
|
"LabelHasSupplementaryEbook": "Tiene un libro complementario",
|
||||||
|
"LabelHideSubtitles": "Ocultar subtítulos",
|
||||||
"LabelHighestPriority": "Mayor prioridad",
|
"LabelHighestPriority": "Mayor prioridad",
|
||||||
"LabelHost": "Host",
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Hora",
|
"LabelHour": "Hora",
|
||||||
@ -339,6 +346,8 @@
|
|||||||
"LabelIntervalEveryHour": "Cada Hora",
|
"LabelIntervalEveryHour": "Cada Hora",
|
||||||
"LabelInvert": "Invertir",
|
"LabelInvert": "Invertir",
|
||||||
"LabelItem": "Elemento",
|
"LabelItem": "Elemento",
|
||||||
|
"LabelJumpBackwardAmount": "Cantidad de saltos hacia atrás",
|
||||||
|
"LabelJumpForwardAmount": "Cantidad de saltos hacia adelante",
|
||||||
"LabelLanguage": "Idioma",
|
"LabelLanguage": "Idioma",
|
||||||
"LabelLanguageDefaultServer": "Lenguaje Predeterminado del Servidor",
|
"LabelLanguageDefaultServer": "Lenguaje Predeterminado del Servidor",
|
||||||
"LabelLanguages": "Idiomas",
|
"LabelLanguages": "Idiomas",
|
||||||
@ -446,6 +455,7 @@
|
|||||||
"LabelRSSFeedPreventIndexing": "Prevenir indexado",
|
"LabelRSSFeedPreventIndexing": "Prevenir indexado",
|
||||||
"LabelRSSFeedSlug": "Fuente RSS Slug",
|
"LabelRSSFeedSlug": "Fuente RSS Slug",
|
||||||
"LabelRSSFeedURL": "URL de Fuente RSS",
|
"LabelRSSFeedURL": "URL de Fuente RSS",
|
||||||
|
"LabelReAddSeriesToContinueListening": "Volver a agregar la serie para continuar escuchándola",
|
||||||
"LabelRead": "Leído",
|
"LabelRead": "Leído",
|
||||||
"LabelReadAgain": "Volver a leer",
|
"LabelReadAgain": "Volver a leer",
|
||||||
"LabelReadEbookWithoutProgress": "Leer Ebook sin guardar progreso",
|
"LabelReadEbookWithoutProgress": "Leer Ebook sin guardar progreso",
|
||||||
@ -512,9 +522,11 @@
|
|||||||
"LabelSettingsStoreMetadataWithItemHelp": "Por defecto, los archivos de metadatos se almacenan en /metadata/items. Si habilita esta opción, los archivos de metadatos se guardarán en la carpeta de elementos de su biblioteca",
|
"LabelSettingsStoreMetadataWithItemHelp": "Por defecto, los archivos de metadatos se almacenan en /metadata/items. Si habilita esta opción, los archivos de metadatos se guardarán en la carpeta de elementos de su biblioteca",
|
||||||
"LabelSettingsTimeFormat": "Formato de Tiempo",
|
"LabelSettingsTimeFormat": "Formato de Tiempo",
|
||||||
"LabelShare": "Compartir",
|
"LabelShare": "Compartir",
|
||||||
|
"LabelShareOpen": "abrir un recurso compartido",
|
||||||
"LabelShareURL": "Compartir la URL",
|
"LabelShareURL": "Compartir la URL",
|
||||||
"LabelShowAll": "Mostrar Todos",
|
"LabelShowAll": "Mostrar Todos",
|
||||||
"LabelShowSeconds": "Mostrar segundos",
|
"LabelShowSeconds": "Mostrar segundos",
|
||||||
|
"LabelShowSubtitles": "Mostrar subtítulos",
|
||||||
"LabelSize": "Tamaño",
|
"LabelSize": "Tamaño",
|
||||||
"LabelSleepTimer": "Temporizador de apagado",
|
"LabelSleepTimer": "Temporizador de apagado",
|
||||||
"LabelSlug": "Slug",
|
"LabelSlug": "Slug",
|
||||||
@ -552,6 +564,10 @@
|
|||||||
"LabelThemeDark": "Oscuro",
|
"LabelThemeDark": "Oscuro",
|
||||||
"LabelThemeLight": "Claro",
|
"LabelThemeLight": "Claro",
|
||||||
"LabelTimeBase": "Tiempo Base",
|
"LabelTimeBase": "Tiempo Base",
|
||||||
|
"LabelTimeDurationXHours": "{0} horas",
|
||||||
|
"LabelTimeDurationXMinutes": "{0} minutos",
|
||||||
|
"LabelTimeDurationXSeconds": "{0} segundos",
|
||||||
|
"LabelTimeInMinutes": "Tiempo en minutos",
|
||||||
"LabelTimeListened": "Tiempo Escuchando",
|
"LabelTimeListened": "Tiempo Escuchando",
|
||||||
"LabelTimeListenedToday": "Tiempo Escuchando Hoy",
|
"LabelTimeListenedToday": "Tiempo Escuchando Hoy",
|
||||||
"LabelTimeRemaining": "{0} restante",
|
"LabelTimeRemaining": "{0} restante",
|
||||||
@ -591,6 +607,7 @@
|
|||||||
"LabelVersion": "Versión",
|
"LabelVersion": "Versión",
|
||||||
"LabelViewBookmarks": "Ver Marcadores",
|
"LabelViewBookmarks": "Ver Marcadores",
|
||||||
"LabelViewChapters": "Ver Capítulos",
|
"LabelViewChapters": "Ver Capítulos",
|
||||||
|
"LabelViewPlayerSettings": "Ver los ajustes del reproductor",
|
||||||
"LabelViewQueue": "Ver Fila del Reproductor",
|
"LabelViewQueue": "Ver Fila del Reproductor",
|
||||||
"LabelVolume": "Volumen",
|
"LabelVolume": "Volumen",
|
||||||
"LabelWeekdaysToRun": "Correr en Días de la Semana",
|
"LabelWeekdaysToRun": "Correr en Días de la Semana",
|
||||||
|
@ -136,7 +136,7 @@
|
|||||||
"HeaderYourStats": "Tilastosi",
|
"HeaderYourStats": "Tilastosi",
|
||||||
"LabelAddToPlaylist": "Lisää soittolistaan",
|
"LabelAddToPlaylist": "Lisää soittolistaan",
|
||||||
"LabelAdded": "Lisätty",
|
"LabelAdded": "Lisätty",
|
||||||
"LabelAddedAt": "Lisätty",
|
"LabelAddedAt": "Lisätty listalle",
|
||||||
"LabelAll": "Kaikki",
|
"LabelAll": "Kaikki",
|
||||||
"LabelAuthor": "Tekijä",
|
"LabelAuthor": "Tekijä",
|
||||||
"LabelAuthorFirstLast": "Tekijä (Etunimi Sukunimi)",
|
"LabelAuthorFirstLast": "Tekijä (Etunimi Sukunimi)",
|
||||||
@ -152,11 +152,34 @@
|
|||||||
"LabelContinueReading": "Jatka lukemista",
|
"LabelContinueReading": "Jatka lukemista",
|
||||||
"LabelContinueSeries": "Jatka sarjoja",
|
"LabelContinueSeries": "Jatka sarjoja",
|
||||||
"LabelDescription": "Kuvaus",
|
"LabelDescription": "Kuvaus",
|
||||||
|
"LabelDownload": "Lataa",
|
||||||
"LabelDuration": "Kesto",
|
"LabelDuration": "Kesto",
|
||||||
"LabelEbook": "E-kirja",
|
"LabelEbook": "E-kirja",
|
||||||
"LabelEbooks": "E-kirjat",
|
"LabelEbooks": "E-kirjat",
|
||||||
|
"LabelEnable": "Ota käyttöön",
|
||||||
"LabelFile": "Tiedosto",
|
"LabelFile": "Tiedosto",
|
||||||
"LabelFileBirthtime": "Tiedoston syntymäaika",
|
"LabelFileBirthtime": "Tiedoston syntymäaika",
|
||||||
"LabelFileModified": "Muutettu tiedosto",
|
"LabelFileModified": "Muutettu tiedosto",
|
||||||
"LabelFilename": "Tiedostonimi"
|
"LabelFilename": "Tiedostonimi",
|
||||||
|
"LabelFolder": "Kansio",
|
||||||
|
"LabelLanguage": "Kieli",
|
||||||
|
"LabelMore": "Lisää",
|
||||||
|
"LabelNarrator": "Lukija",
|
||||||
|
"LabelNarrators": "Lukijat",
|
||||||
|
"LabelNewestAuthors": "Uusimmat kirjailijat",
|
||||||
|
"LabelNewestEpisodes": "Uusimmat jaksot",
|
||||||
|
"LabelPassword": "Salasana",
|
||||||
|
"LabelPath": "Polku",
|
||||||
|
"LabelRead": "Lue",
|
||||||
|
"LabelReadAgain": "Lue uudelleen",
|
||||||
|
"LabelSeason": "Kausi",
|
||||||
|
"LabelShowAll": "Näytä kaikki",
|
||||||
|
"LabelSize": "Koko",
|
||||||
|
"LabelSleepTimer": "Uniajastin",
|
||||||
|
"LabelTheme": "Teema",
|
||||||
|
"LabelThemeDark": "Tumma",
|
||||||
|
"LabelThemeLight": "Kirkas",
|
||||||
|
"LabelUser": "Käyttäjä",
|
||||||
|
"LabelUsername": "Käyttäjätunnus",
|
||||||
|
"MessageDownloadingEpisode": "Ladataan jaksoa"
|
||||||
}
|
}
|
||||||
|
@ -258,6 +258,7 @@
|
|||||||
"LabelCurrently": "Actuellement :",
|
"LabelCurrently": "Actuellement :",
|
||||||
"LabelCustomCronExpression": "Expression cron personnalisée :",
|
"LabelCustomCronExpression": "Expression cron personnalisée :",
|
||||||
"LabelDatetime": "Date",
|
"LabelDatetime": "Date",
|
||||||
|
"LabelDays": "Jours",
|
||||||
"LabelDeleteFromFileSystemCheckbox": "Supprimer du système de fichiers (décocher pour ne supprimer que de la base de données)",
|
"LabelDeleteFromFileSystemCheckbox": "Supprimer du système de fichiers (décocher pour ne supprimer que de la base de données)",
|
||||||
"LabelDescription": "Description",
|
"LabelDescription": "Description",
|
||||||
"LabelDeselectAll": "Tout déselectionner",
|
"LabelDeselectAll": "Tout déselectionner",
|
||||||
@ -321,6 +322,7 @@
|
|||||||
"LabelHighestPriority": "Priorité la plus élevée",
|
"LabelHighestPriority": "Priorité la plus élevée",
|
||||||
"LabelHost": "Hôte",
|
"LabelHost": "Hôte",
|
||||||
"LabelHour": "Heure",
|
"LabelHour": "Heure",
|
||||||
|
"LabelHours": "Heures",
|
||||||
"LabelIcon": "Icône",
|
"LabelIcon": "Icône",
|
||||||
"LabelImageURLFromTheWeb": "URL de l’image à partir du web",
|
"LabelImageURLFromTheWeb": "URL de l’image à partir du web",
|
||||||
"LabelInProgress": "En cours",
|
"LabelInProgress": "En cours",
|
||||||
@ -371,6 +373,7 @@
|
|||||||
"LabelMetadataOrderOfPrecedenceDescription": "Les sources de métadonnées ayant une priorité plus élevée auront la priorité sur celles ayant une priorité moins élevée",
|
"LabelMetadataOrderOfPrecedenceDescription": "Les sources de métadonnées ayant une priorité plus élevée auront la priorité sur celles ayant une priorité moins élevée",
|
||||||
"LabelMetadataProvider": "Fournisseur de métadonnées",
|
"LabelMetadataProvider": "Fournisseur de métadonnées",
|
||||||
"LabelMinute": "Minute",
|
"LabelMinute": "Minute",
|
||||||
|
"LabelMinutes": "Minutes",
|
||||||
"LabelMissing": "Manquant",
|
"LabelMissing": "Manquant",
|
||||||
"LabelMissingEbook": "Ne possède aucun livre numérique",
|
"LabelMissingEbook": "Ne possède aucun livre numérique",
|
||||||
"LabelMissingSupplementaryEbook": "Ne possède aucun livre numérique supplémentaire",
|
"LabelMissingSupplementaryEbook": "Ne possède aucun livre numérique supplémentaire",
|
||||||
@ -410,6 +413,7 @@
|
|||||||
"LabelOverwrite": "Écraser",
|
"LabelOverwrite": "Écraser",
|
||||||
"LabelPassword": "Mot de passe",
|
"LabelPassword": "Mot de passe",
|
||||||
"LabelPath": "Chemin",
|
"LabelPath": "Chemin",
|
||||||
|
"LabelPermanent": "Permanent",
|
||||||
"LabelPermissionsAccessAllLibraries": "Peut accéder à toutes les bibliothèque",
|
"LabelPermissionsAccessAllLibraries": "Peut accéder à toutes les bibliothèque",
|
||||||
"LabelPermissionsAccessAllTags": "Peut accéder à toutes les étiquettes",
|
"LabelPermissionsAccessAllTags": "Peut accéder à toutes les étiquettes",
|
||||||
"LabelPermissionsAccessExplicitContent": "Peut accéder au contenu restreint",
|
"LabelPermissionsAccessExplicitContent": "Peut accéder au contenu restreint",
|
||||||
@ -507,6 +511,9 @@
|
|||||||
"LabelSettingsStoreMetadataWithItem": "Enregistrer les métadonnées avec l’élément",
|
"LabelSettingsStoreMetadataWithItem": "Enregistrer les métadonnées avec l’élément",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les fichiers de métadonnées sont stockés dans /metadata/items. En activant ce paramètre, les fichiers de métadonnées seront stockés dans les dossiers des éléments de votre bibliothèque",
|
"LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les fichiers de métadonnées sont stockés dans /metadata/items. En activant ce paramètre, les fichiers de métadonnées seront stockés dans les dossiers des éléments de votre bibliothèque",
|
||||||
"LabelSettingsTimeFormat": "Format d’heure",
|
"LabelSettingsTimeFormat": "Format d’heure",
|
||||||
|
"LabelShare": "Partager",
|
||||||
|
"LabelShareOpen": "Ouvrir le partage",
|
||||||
|
"LabelShareURL": "Partager l’URL",
|
||||||
"LabelShowAll": "Tout afficher",
|
"LabelShowAll": "Tout afficher",
|
||||||
"LabelShowSeconds": "Afficher les seondes",
|
"LabelShowSeconds": "Afficher les seondes",
|
||||||
"LabelSize": "Taille",
|
"LabelSize": "Taille",
|
||||||
@ -598,6 +605,7 @@
|
|||||||
"MessageAppriseDescription": "Nécessite une instance d’<a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API Apprise</a> pour utiliser cette fonctionnalité ou une api qui prend en charge les mêmes requêtes.<br />L’URL de l’API Apprise doit comprendre le chemin complet pour envoyer la notification. Par exemple, si votre instance écoute sur <code>http://192.168.1.1:8337</code> alors vous devez mettre <code>http://192.168.1.1:8337/notify</code>.",
|
"MessageAppriseDescription": "Nécessite une instance d’<a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API Apprise</a> pour utiliser cette fonctionnalité ou une api qui prend en charge les mêmes requêtes.<br />L’URL de l’API Apprise doit comprendre le chemin complet pour envoyer la notification. Par exemple, si votre instance écoute sur <code>http://192.168.1.1:8337</code> alors vous devez mettre <code>http://192.168.1.1:8337/notify</code>.",
|
||||||
"MessageBackupsDescription": "Les sauvegardes incluent les utilisateurs, la progression des utilisateurs, les détails des éléments de la bibliothèque, les paramètres du serveur et les images stockées dans <code>/metadata/items</code> & <code>/metadata/authors</code>. Les sauvegardes <strong>n’incluent pas</strong> les fichiers stockés dans les dossiers de votre bibliothèque.",
|
"MessageBackupsDescription": "Les sauvegardes incluent les utilisateurs, la progression des utilisateurs, les détails des éléments de la bibliothèque, les paramètres du serveur et les images stockées dans <code>/metadata/items</code> & <code>/metadata/authors</code>. Les sauvegardes <strong>n’incluent pas</strong> les fichiers stockés dans les dossiers de votre bibliothèque.",
|
||||||
"MessageBackupsLocationEditNote": "Remarque : Mettre à jour l'emplacement de sauvegarde ne déplacera pas ou ne modifiera pas les sauvegardes existantes",
|
"MessageBackupsLocationEditNote": "Remarque : Mettre à jour l'emplacement de sauvegarde ne déplacera pas ou ne modifiera pas les sauvegardes existantes",
|
||||||
|
"MessageBackupsLocationNoEditNote": "Remarque : l’emplacement de sauvegarde est défini via une variable d’environnement et ne peut pas être modifié ici.",
|
||||||
"MessageBackupsLocationPathEmpty": "L'emplacement de secours ne peut pas être vide",
|
"MessageBackupsLocationPathEmpty": "L'emplacement de secours ne peut pas être vide",
|
||||||
"MessageBatchQuickMatchDescription": "La recherche par correspondance rapide tentera d’ajouter les couvertures et métadonnées manquantes pour les éléments sélectionnés. Activez les options ci-dessous pour permettre la Recherche par correspondance d’écraser les couvertures et/ou métadonnées existantes.",
|
"MessageBatchQuickMatchDescription": "La recherche par correspondance rapide tentera d’ajouter les couvertures et métadonnées manquantes pour les éléments sélectionnés. Activez les options ci-dessous pour permettre la Recherche par correspondance d’écraser les couvertures et/ou métadonnées existantes.",
|
||||||
"MessageBookshelfNoCollections": "Vous n’avez pas encore de collections",
|
"MessageBookshelfNoCollections": "Vous n’avez pas encore de collections",
|
||||||
@ -716,6 +724,9 @@
|
|||||||
"MessageSelected": "{0} sélectionnés",
|
"MessageSelected": "{0} sélectionnés",
|
||||||
"MessageServerCouldNotBeReached": "Serveur inaccessible",
|
"MessageServerCouldNotBeReached": "Serveur inaccessible",
|
||||||
"MessageSetChaptersFromTracksDescription": "Positionne un chapitre par fichier audio, avec le titre du fichier comme titre de chapitre",
|
"MessageSetChaptersFromTracksDescription": "Positionne un chapitre par fichier audio, avec le titre du fichier comme titre de chapitre",
|
||||||
|
"MessageShareExpirationWillBe": "Expire le <strong>{0}</strong>",
|
||||||
|
"MessageShareExpiresIn": "Expire dans {0}",
|
||||||
|
"MessageShareURLWillBe": "L’adresse de partage sera <strong>{0}</strong>",
|
||||||
"MessageStartPlaybackAtTime": "Démarrer la lecture pour « {0} » à {1} ?",
|
"MessageStartPlaybackAtTime": "Démarrer la lecture pour « {0} » à {1} ?",
|
||||||
"MessageThinking": "Je cherche…",
|
"MessageThinking": "Je cherche…",
|
||||||
"MessageUploaderItemFailed": "Échec du téléversement",
|
"MessageUploaderItemFailed": "Échec du téléversement",
|
||||||
@ -730,7 +741,7 @@
|
|||||||
"NoteChapterEditorTimes": "Information : l’horodatage du premier chapitre doit être à 0:00 et celui du dernier chapitre ne peut se situer au-delà de la durée du livre audio.",
|
"NoteChapterEditorTimes": "Information : l’horodatage du premier chapitre doit être à 0:00 et celui du dernier chapitre ne peut se situer au-delà de la durée du livre audio.",
|
||||||
"NoteFolderPicker": "Information : les dossiers déjà surveillés ne sont pas affichés",
|
"NoteFolderPicker": "Information : les dossiers déjà surveillés ne sont pas affichés",
|
||||||
"NoteRSSFeedPodcastAppsHttps": "Attention : la majorité des application de podcast nécessite une adresse de flux HTTPS",
|
"NoteRSSFeedPodcastAppsHttps": "Attention : la majorité des application de podcast nécessite une adresse de flux HTTPS",
|
||||||
"NoteRSSFeedPodcastAppsPubDate": "Attention : un ou plusieurs de vos épisodes ne possèdent pas de date de publication. Certaines applications de podcast le requièrent.",
|
"NoteRSSFeedPodcastAppsPubDate": "Attention : un ou plusieurs de vos épisodes ne possèdent pas de date de publication. Certaines applications de podcast le requièrent.",
|
||||||
"NoteUploaderFoldersWithMediaFiles": "Les dossiers contenant des fichiers multimédias seront traités comme des éléments distincts de la bibliothèque.",
|
"NoteUploaderFoldersWithMediaFiles": "Les dossiers contenant des fichiers multimédias seront traités comme des éléments distincts de la bibliothèque.",
|
||||||
"NoteUploaderOnlyAudioFiles": "Si vous téléversez uniquement des fichiers audio, chaque fichier audio sera traité comme un livre audio distinct.",
|
"NoteUploaderOnlyAudioFiles": "Si vous téléversez uniquement des fichiers audio, chaque fichier audio sera traité comme un livre audio distinct.",
|
||||||
"NoteUploaderUnsupportedFiles": "Les fichiers non pris en charge sont ignorés. Lorsque vous choisissez ou déposez un dossier, les autres fichiers qui ne sont pas dans un dossier d’élément sont ignorés.",
|
"NoteUploaderUnsupportedFiles": "Les fichiers non pris en charge sont ignorés. Lorsque vous choisissez ou déposez un dossier, les autres fichiers qui ne sont pas dans un dossier d’élément sont ignorés.",
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
"ButtonApply": "החל",
|
"ButtonApply": "החל",
|
||||||
"ButtonApplyChapters": "החל פרקים",
|
"ButtonApplyChapters": "החל פרקים",
|
||||||
"ButtonAuthors": "יוצרים",
|
"ButtonAuthors": "יוצרים",
|
||||||
"ButtonBack": "Back",
|
"ButtonBack": "חזור",
|
||||||
"ButtonBrowseForFolder": "עיין בתיקייה",
|
"ButtonBrowseForFolder": "עיין בתיקייה",
|
||||||
"ButtonCancel": "בטל",
|
"ButtonCancel": "בטל",
|
||||||
"ButtonCancelEncode": "בטל קידוד",
|
"ButtonCancelEncode": "בטל קידוד",
|
||||||
@ -62,8 +62,8 @@
|
|||||||
"ButtonQuickMatch": "התאמה מהירה",
|
"ButtonQuickMatch": "התאמה מהירה",
|
||||||
"ButtonReScan": "סרוק מחדש",
|
"ButtonReScan": "סרוק מחדש",
|
||||||
"ButtonRead": "קרא",
|
"ButtonRead": "קרא",
|
||||||
"ButtonReadLess": "Read less",
|
"ButtonReadLess": "קרא פחות",
|
||||||
"ButtonReadMore": "Read more",
|
"ButtonReadMore": "קרא יותר",
|
||||||
"ButtonRefresh": "רענן",
|
"ButtonRefresh": "רענן",
|
||||||
"ButtonRemove": "הסר",
|
"ButtonRemove": "הסר",
|
||||||
"ButtonRemoveAll": "הסר הכל",
|
"ButtonRemoveAll": "הסר הכל",
|
||||||
@ -115,7 +115,7 @@
|
|||||||
"HeaderCollectionItems": "פריטי אוסף",
|
"HeaderCollectionItems": "פריטי אוסף",
|
||||||
"HeaderCover": "כריכה",
|
"HeaderCover": "כריכה",
|
||||||
"HeaderCurrentDownloads": "הורדות נוכחיות",
|
"HeaderCurrentDownloads": "הורדות נוכחיות",
|
||||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
"HeaderCustomMessageOnLogin": "הודעה מותאמת אישית בהתחברות",
|
||||||
"HeaderCustomMetadataProviders": "ספקי מטא-נתונים מותאמים אישית",
|
"HeaderCustomMetadataProviders": "ספקי מטא-נתונים מותאמים אישית",
|
||||||
"HeaderDetails": "פרטים",
|
"HeaderDetails": "פרטים",
|
||||||
"HeaderDownloadQueue": "תור הורדה",
|
"HeaderDownloadQueue": "תור הורדה",
|
||||||
@ -806,8 +806,8 @@
|
|||||||
"ToastSendEbookToDeviceSuccess": "הספר נשלח אל המכשיר \"{0}\"",
|
"ToastSendEbookToDeviceSuccess": "הספר נשלח אל המכשיר \"{0}\"",
|
||||||
"ToastSeriesUpdateFailed": "עדכון הסדרה נכשל",
|
"ToastSeriesUpdateFailed": "עדכון הסדרה נכשל",
|
||||||
"ToastSeriesUpdateSuccess": "הסדרה עודכנה בהצלחה",
|
"ToastSeriesUpdateSuccess": "הסדרה עודכנה בהצלחה",
|
||||||
"ToastServerSettingsUpdateFailed": "Failed to update server settings",
|
"ToastServerSettingsUpdateFailed": "כשל בעדכון הגדרות שרת",
|
||||||
"ToastServerSettingsUpdateSuccess": "Server settings updated",
|
"ToastServerSettingsUpdateSuccess": "הגדרות שרת עודכנו בהצלחה",
|
||||||
"ToastSessionDeleteFailed": "מחיקת הפעולה נכשלה",
|
"ToastSessionDeleteFailed": "מחיקת הפעולה נכשלה",
|
||||||
"ToastSessionDeleteSuccess": "הפעולה נמחקה בהצלחה",
|
"ToastSessionDeleteSuccess": "הפעולה נמחקה בהצלחה",
|
||||||
"ToastSocketConnected": "קצה תקשורת חובר",
|
"ToastSocketConnected": "קצה תקשורת חובר",
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"ButtonAdd": "Toevoegen",
|
"ButtonAdd": "Toevoegen",
|
||||||
"ButtonAddChapters": "Hoofdstukken toevoegen",
|
"ButtonAddChapters": "Hoofdstukken toevoegen",
|
||||||
"ButtonAddDevice": "Add Device",
|
"ButtonAddDevice": "Toestel toevoegen",
|
||||||
"ButtonAddLibrary": "Add Library",
|
"ButtonAddLibrary": "Bibliotheek toevoegen",
|
||||||
"ButtonAddPodcasts": "Podcasts toevoegen",
|
"ButtonAddPodcasts": "Podcasts toevoegen",
|
||||||
"ButtonAddUser": "Add User",
|
"ButtonAddUser": "Gebruiker toevoegen",
|
||||||
"ButtonAddYourFirstLibrary": "Voeg je eerste bibliotheek toe",
|
"ButtonAddYourFirstLibrary": "Voeg je eerste bibliotheek toe",
|
||||||
"ButtonApply": "Pas toe",
|
"ButtonApply": "Pas toe",
|
||||||
"ButtonApplyChapters": "Hoofdstukken toepassen",
|
"ButtonApplyChapters": "Hoofdstukken toepassen",
|
||||||
"ButtonAuthors": "Auteurs",
|
"ButtonAuthors": "Auteurs",
|
||||||
"ButtonBack": "Back",
|
"ButtonBack": "Terug",
|
||||||
"ButtonBrowseForFolder": "Bladeren naar map",
|
"ButtonBrowseForFolder": "Bladeren naar map",
|
||||||
"ButtonCancel": "Annuleren",
|
"ButtonCancel": "Annuleren",
|
||||||
"ButtonCancelEncode": "Encoding annuleren",
|
"ButtonCancelEncode": "Encoding annuleren",
|
||||||
@ -32,9 +32,9 @@
|
|||||||
"ButtonFullPath": "Volledig pad",
|
"ButtonFullPath": "Volledig pad",
|
||||||
"ButtonHide": "Verberg",
|
"ButtonHide": "Verberg",
|
||||||
"ButtonHome": "Home",
|
"ButtonHome": "Home",
|
||||||
"ButtonIssues": "Issues",
|
"ButtonIssues": "Problemen",
|
||||||
"ButtonJumpBackward": "Jump Backward",
|
"ButtonJumpBackward": "Spring achteruit",
|
||||||
"ButtonJumpForward": "Jump Forward",
|
"ButtonJumpForward": "Spring vooruit",
|
||||||
"ButtonLatest": "Meest recent",
|
"ButtonLatest": "Meest recent",
|
||||||
"ButtonLibrary": "Bibliotheek",
|
"ButtonLibrary": "Bibliotheek",
|
||||||
"ButtonLogout": "Log uit",
|
"ButtonLogout": "Log uit",
|
||||||
@ -44,17 +44,17 @@
|
|||||||
"ButtonMatchAllAuthors": "Alle auteurs matchen",
|
"ButtonMatchAllAuthors": "Alle auteurs matchen",
|
||||||
"ButtonMatchBooks": "Alle boeken matchen",
|
"ButtonMatchBooks": "Alle boeken matchen",
|
||||||
"ButtonNevermind": "Laat maar",
|
"ButtonNevermind": "Laat maar",
|
||||||
"ButtonNext": "Next",
|
"ButtonNext": "Volgende",
|
||||||
"ButtonNextChapter": "Next Chapter",
|
"ButtonNextChapter": "Volgend hoofdstuk",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Feed openen",
|
"ButtonOpenFeed": "Feed openen",
|
||||||
"ButtonOpenManager": "Manager openen",
|
"ButtonOpenManager": "Manager openen",
|
||||||
"ButtonPause": "Pause",
|
"ButtonPause": "Pauze",
|
||||||
"ButtonPlay": "Afspelen",
|
"ButtonPlay": "Afspelen",
|
||||||
"ButtonPlaying": "Speelt",
|
"ButtonPlaying": "Speelt",
|
||||||
"ButtonPlaylists": "Afspeellijsten",
|
"ButtonPlaylists": "Afspeellijsten",
|
||||||
"ButtonPrevious": "Previous",
|
"ButtonPrevious": "Vorige",
|
||||||
"ButtonPreviousChapter": "Previous Chapter",
|
"ButtonPreviousChapter": "Vorig hoofdstuk",
|
||||||
"ButtonPurgeAllCache": "Volledige cache legen",
|
"ButtonPurgeAllCache": "Volledige cache legen",
|
||||||
"ButtonPurgeItemsCache": "Onderdelen-cache legen",
|
"ButtonPurgeItemsCache": "Onderdelen-cache legen",
|
||||||
"ButtonQueueAddItem": "In wachtrij zetten",
|
"ButtonQueueAddItem": "In wachtrij zetten",
|
||||||
@ -62,14 +62,14 @@
|
|||||||
"ButtonQuickMatch": "Snelle match",
|
"ButtonQuickMatch": "Snelle match",
|
||||||
"ButtonReScan": "Nieuwe scan",
|
"ButtonReScan": "Nieuwe scan",
|
||||||
"ButtonRead": "Lees",
|
"ButtonRead": "Lees",
|
||||||
"ButtonReadLess": "Read less",
|
"ButtonReadLess": "Lees minder",
|
||||||
"ButtonReadMore": "Read more",
|
"ButtonReadMore": "Lees meer",
|
||||||
"ButtonRefresh": "Refresh",
|
"ButtonRefresh": "Verversen",
|
||||||
"ButtonRemove": "Verwijder",
|
"ButtonRemove": "Verwijder",
|
||||||
"ButtonRemoveAll": "Alles verwijderen",
|
"ButtonRemoveAll": "Alles verwijderen",
|
||||||
"ButtonRemoveAllLibraryItems": "Verwijder volledige bibliotheekinhoud",
|
"ButtonRemoveAllLibraryItems": "Verwijder volledige bibliotheekinhoud",
|
||||||
"ButtonRemoveFromContinueListening": "Vewijder uit Verder luisteren",
|
"ButtonRemoveFromContinueListening": "Vewijder uit Verder luisteren",
|
||||||
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
"ButtonRemoveFromContinueReading": "Verwijder van Verder luisteren",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Verwijder serie uit Serie vervolgen",
|
"ButtonRemoveSeriesFromContinueSeries": "Verwijder serie uit Serie vervolgen",
|
||||||
"ButtonReset": "Reset",
|
"ButtonReset": "Reset",
|
||||||
"ButtonResetToDefault": "Reset to default",
|
"ButtonResetToDefault": "Reset to default",
|
||||||
@ -83,7 +83,7 @@
|
|||||||
"ButtonSelectFolderPath": "Maplocatie selecteren",
|
"ButtonSelectFolderPath": "Maplocatie selecteren",
|
||||||
"ButtonSeries": "Series",
|
"ButtonSeries": "Series",
|
||||||
"ButtonSetChaptersFromTracks": "Maak hoofdstukken op basis van tracks",
|
"ButtonSetChaptersFromTracks": "Maak hoofdstukken op basis van tracks",
|
||||||
"ButtonShare": "Share",
|
"ButtonShare": "Deel",
|
||||||
"ButtonShiftTimes": "Tijden verschuiven",
|
"ButtonShiftTimes": "Tijden verschuiven",
|
||||||
"ButtonShow": "Toon",
|
"ButtonShow": "Toon",
|
||||||
"ButtonStartM4BEncode": "Start M4B-encoding",
|
"ButtonStartM4BEncode": "Start M4B-encoding",
|
||||||
@ -98,9 +98,9 @@
|
|||||||
"ButtonUserEdit": "Wijzig gebruiker {0}",
|
"ButtonUserEdit": "Wijzig gebruiker {0}",
|
||||||
"ButtonViewAll": "Toon alle",
|
"ButtonViewAll": "Toon alle",
|
||||||
"ButtonYes": "Ja",
|
"ButtonYes": "Ja",
|
||||||
"ErrorUploadFetchMetadataAPI": "Error fetching metadata",
|
"ErrorUploadFetchMetadataAPI": "Error metadata ophalen",
|
||||||
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
|
"ErrorUploadFetchMetadataNoResults": "Kan metadata niet ophalen - probeer de titel en/of auteur te updaten",
|
||||||
"ErrorUploadLacksTitle": "Must have a title",
|
"ErrorUploadLacksTitle": "Moet een titel hebben",
|
||||||
"HeaderAccount": "Account",
|
"HeaderAccount": "Account",
|
||||||
"HeaderAdvanced": "Geavanceerd",
|
"HeaderAdvanced": "Geavanceerd",
|
||||||
"HeaderAppriseNotificationSettings": "Apprise-notificatie instellingen",
|
"HeaderAppriseNotificationSettings": "Apprise-notificatie instellingen",
|
||||||
@ -113,13 +113,13 @@
|
|||||||
"HeaderChooseAFolder": "Map kiezen",
|
"HeaderChooseAFolder": "Map kiezen",
|
||||||
"HeaderCollection": "Collectie",
|
"HeaderCollection": "Collectie",
|
||||||
"HeaderCollectionItems": "Collectie-objecten",
|
"HeaderCollectionItems": "Collectie-objecten",
|
||||||
"HeaderCover": "Cover",
|
"HeaderCover": "Omslag",
|
||||||
"HeaderCurrentDownloads": "Huidige downloads",
|
"HeaderCurrentDownloads": "Huidige downloads",
|
||||||
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
"HeaderCustomMessageOnLogin": "Custom Message on Login",
|
||||||
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
|
||||||
"HeaderDetails": "Details",
|
"HeaderDetails": "Details",
|
||||||
"HeaderDownloadQueue": "Download-wachtrij",
|
"HeaderDownloadQueue": "Download-wachtrij",
|
||||||
"HeaderEbookFiles": "Ebook Files",
|
"HeaderEbookFiles": "Ebook bestanden",
|
||||||
"HeaderEmail": "E-mail",
|
"HeaderEmail": "E-mail",
|
||||||
"HeaderEmailSettings": "E-mail instellingen",
|
"HeaderEmailSettings": "E-mail instellingen",
|
||||||
"HeaderEpisodes": "Afleveringen",
|
"HeaderEpisodes": "Afleveringen",
|
||||||
@ -239,11 +239,11 @@
|
|||||||
"LabelChapterTitle": "Hoofdstuktitel",
|
"LabelChapterTitle": "Hoofdstuktitel",
|
||||||
"LabelChapters": "Hoofdstukken",
|
"LabelChapters": "Hoofdstukken",
|
||||||
"LabelChaptersFound": "Hoofdstukken gevonden",
|
"LabelChaptersFound": "Hoofdstukken gevonden",
|
||||||
"LabelClickForMoreInfo": "Click for more info",
|
"LabelClickForMoreInfo": "Klik voor meer informatie",
|
||||||
"LabelClosePlayer": "Sluit speler",
|
"LabelClosePlayer": "Sluit speler",
|
||||||
"LabelCodec": "Codec",
|
"LabelCodec": "Codec",
|
||||||
"LabelCollapseSeries": "Series inklappen",
|
"LabelCollapseSeries": "Series inklappen",
|
||||||
"LabelCollection": "Collection",
|
"LabelCollection": "Collectie",
|
||||||
"LabelCollections": "Collecties",
|
"LabelCollections": "Collecties",
|
||||||
"LabelComplete": "Compleet",
|
"LabelComplete": "Compleet",
|
||||||
"LabelConfirmPassword": "Bevestig wachtwoord",
|
"LabelConfirmPassword": "Bevestig wachtwoord",
|
||||||
@ -258,6 +258,7 @@
|
|||||||
"LabelCurrently": "Op dit moment:",
|
"LabelCurrently": "Op dit moment:",
|
||||||
"LabelCustomCronExpression": "Aangepaste Cron-uitdrukking:",
|
"LabelCustomCronExpression": "Aangepaste Cron-uitdrukking:",
|
||||||
"LabelDatetime": "Datum-tijd",
|
"LabelDatetime": "Datum-tijd",
|
||||||
|
"LabelDays": "Dagen",
|
||||||
"LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
|
"LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
|
||||||
"LabelDescription": "Beschrijving",
|
"LabelDescription": "Beschrijving",
|
||||||
"LabelDeselectAll": "Deselecteer alle",
|
"LabelDeselectAll": "Deselecteer alle",
|
||||||
@ -296,7 +297,7 @@
|
|||||||
"LabelExplicitChecked": "Explicit (checked)",
|
"LabelExplicitChecked": "Explicit (checked)",
|
||||||
"LabelExplicitUnchecked": "Not Explicit (unchecked)",
|
"LabelExplicitUnchecked": "Not Explicit (unchecked)",
|
||||||
"LabelFeedURL": "Feed URL",
|
"LabelFeedURL": "Feed URL",
|
||||||
"LabelFetchingMetadata": "Fetching Metadata",
|
"LabelFetchingMetadata": "Metadata ophalen",
|
||||||
"LabelFile": "Bestand",
|
"LabelFile": "Bestand",
|
||||||
"LabelFileBirthtime": "Aanmaaktijd bestand",
|
"LabelFileBirthtime": "Aanmaaktijd bestand",
|
||||||
"LabelFileModified": "Bestand gewijzigd",
|
"LabelFileModified": "Bestand gewijzigd",
|
||||||
@ -306,7 +307,7 @@
|
|||||||
"LabelFinished": "Voltooid",
|
"LabelFinished": "Voltooid",
|
||||||
"LabelFolder": "Map",
|
"LabelFolder": "Map",
|
||||||
"LabelFolders": "Mappen",
|
"LabelFolders": "Mappen",
|
||||||
"LabelFontBold": "Bold",
|
"LabelFontBold": "Vetgedrukt",
|
||||||
"LabelFontBoldness": "Font Boldness",
|
"LabelFontBoldness": "Font Boldness",
|
||||||
"LabelFontFamily": "Lettertypefamilie",
|
"LabelFontFamily": "Lettertypefamilie",
|
||||||
"LabelFontItalic": "Italic",
|
"LabelFontItalic": "Italic",
|
||||||
@ -321,6 +322,7 @@
|
|||||||
"LabelHighestPriority": "Highest priority",
|
"LabelHighestPriority": "Highest priority",
|
||||||
"LabelHost": "Host",
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Uur",
|
"LabelHour": "Uur",
|
||||||
|
"LabelHours": "Uren",
|
||||||
"LabelIcon": "Icoon",
|
"LabelIcon": "Icoon",
|
||||||
"LabelImageURLFromTheWeb": "Image URL from the web",
|
"LabelImageURLFromTheWeb": "Image URL from the web",
|
||||||
"LabelInProgress": "Bezig",
|
"LabelInProgress": "Bezig",
|
||||||
@ -567,7 +569,7 @@
|
|||||||
"LabelTracksSingleTrack": "Enkele track",
|
"LabelTracksSingleTrack": "Enkele track",
|
||||||
"LabelType": "Type",
|
"LabelType": "Type",
|
||||||
"LabelUnabridged": "Onverkort",
|
"LabelUnabridged": "Onverkort",
|
||||||
"LabelUndo": "Undo",
|
"LabelUndo": "Ongedaan maken",
|
||||||
"LabelUnknown": "Onbekend",
|
"LabelUnknown": "Onbekend",
|
||||||
"LabelUpdateCover": "Cover bijwerken",
|
"LabelUpdateCover": "Cover bijwerken",
|
||||||
"LabelUpdateCoverHelp": "Sta overschrijven van bestaande covers toe voor de geselecteerde boeken wanneer een match is gevonden",
|
"LabelUpdateCoverHelp": "Sta overschrijven van bestaande covers toe voor de geselecteerde boeken wanneer een match is gevonden",
|
||||||
@ -630,7 +632,7 @@
|
|||||||
"MessageConfirmRemoveCollection": "Weet je zeker dat je de collectie \"{0}\" wil verwijderen?",
|
"MessageConfirmRemoveCollection": "Weet je zeker dat je de collectie \"{0}\" wil verwijderen?",
|
||||||
"MessageConfirmRemoveEpisode": "Weet je zeker dat je de aflevering \"{0}\" wil verwijderen?",
|
"MessageConfirmRemoveEpisode": "Weet je zeker dat je de aflevering \"{0}\" wil verwijderen?",
|
||||||
"MessageConfirmRemoveEpisodes": "Weet je zeker dat je {0} afleveringen wil verwijderen?",
|
"MessageConfirmRemoveEpisodes": "Weet je zeker dat je {0} afleveringen wil verwijderen?",
|
||||||
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
|
"MessageConfirmRemoveListeningSessions": "Weet je zeker dat je {0} luistersessies wilt verwijderen?",
|
||||||
"MessageConfirmRemoveNarrator": "Weet je zeker dat je verteller \"{0}\" wil verwijderen?",
|
"MessageConfirmRemoveNarrator": "Weet je zeker dat je verteller \"{0}\" wil verwijderen?",
|
||||||
"MessageConfirmRemovePlaylist": "Weet je zeker dat je je afspeellijst \"{0}\" wil verwijderen?",
|
"MessageConfirmRemovePlaylist": "Weet je zeker dat je je afspeellijst \"{0}\" wil verwijderen?",
|
||||||
"MessageConfirmRenameGenre": "Weet je zeker dat je genre \"{0}\" wil hernoemen naar \"{1}\" voor alle onderdelen?",
|
"MessageConfirmRenameGenre": "Weet je zeker dat je genre \"{0}\" wil hernoemen naar \"{1}\" voor alle onderdelen?",
|
||||||
@ -714,6 +716,7 @@
|
|||||||
"MessageSelected": "{0} selected",
|
"MessageSelected": "{0} selected",
|
||||||
"MessageServerCouldNotBeReached": "Server niet bereikbaar",
|
"MessageServerCouldNotBeReached": "Server niet bereikbaar",
|
||||||
"MessageSetChaptersFromTracksDescription": "Stel hoofdstukken in met ieder audiobestand als een hoofdstuk en de audiobestandsnaam als hoofdstuktitel",
|
"MessageSetChaptersFromTracksDescription": "Stel hoofdstukken in met ieder audiobestand als een hoofdstuk en de audiobestandsnaam als hoofdstuktitel",
|
||||||
|
"MessageShareExpiresIn": "Vervalt in {0}",
|
||||||
"MessageStartPlaybackAtTime": "Afspelen van \"{0}\" beginnen op {1}?",
|
"MessageStartPlaybackAtTime": "Afspelen van \"{0}\" beginnen op {1}?",
|
||||||
"MessageThinking": "Aan het denken...",
|
"MessageThinking": "Aan het denken...",
|
||||||
"MessageUploaderItemFailed": "Uploaden mislukt",
|
"MessageUploaderItemFailed": "Uploaden mislukt",
|
||||||
|
@ -62,8 +62,8 @@
|
|||||||
"ButtonQuickMatch": "Szybkie dopasowanie",
|
"ButtonQuickMatch": "Szybkie dopasowanie",
|
||||||
"ButtonReScan": "Ponowne skanowanie",
|
"ButtonReScan": "Ponowne skanowanie",
|
||||||
"ButtonRead": "Czytaj",
|
"ButtonRead": "Czytaj",
|
||||||
"ButtonReadLess": "Read less",
|
"ButtonReadLess": "Pokaż mniej",
|
||||||
"ButtonReadMore": "Read more",
|
"ButtonReadMore": "Pokaż więcej",
|
||||||
"ButtonRefresh": "Odśwież",
|
"ButtonRefresh": "Odśwież",
|
||||||
"ButtonRemove": "Usuń",
|
"ButtonRemove": "Usuń",
|
||||||
"ButtonRemoveAll": "Usuń wszystko",
|
"ButtonRemoveAll": "Usuń wszystko",
|
||||||
@ -88,6 +88,7 @@
|
|||||||
"ButtonShow": "Pokaż",
|
"ButtonShow": "Pokaż",
|
||||||
"ButtonStartM4BEncode": "Eksportuj jako plik M4B",
|
"ButtonStartM4BEncode": "Eksportuj jako plik M4B",
|
||||||
"ButtonStartMetadataEmbed": "Osadź metadane",
|
"ButtonStartMetadataEmbed": "Osadź metadane",
|
||||||
|
"ButtonStats": "Statystyki",
|
||||||
"ButtonSubmit": "Zaloguj",
|
"ButtonSubmit": "Zaloguj",
|
||||||
"ButtonTest": "Test",
|
"ButtonTest": "Test",
|
||||||
"ButtonUpload": "Wgraj",
|
"ButtonUpload": "Wgraj",
|
||||||
@ -130,13 +131,13 @@
|
|||||||
"HeaderIgnoredFiles": "Zignoruj pliki",
|
"HeaderIgnoredFiles": "Zignoruj pliki",
|
||||||
"HeaderItemFiles": "Pliki",
|
"HeaderItemFiles": "Pliki",
|
||||||
"HeaderItemMetadataUtils": "Item Metadata Utils",
|
"HeaderItemMetadataUtils": "Item Metadata Utils",
|
||||||
"HeaderLastListeningSession": "Ostatnio odtwarzana sesja",
|
"HeaderLastListeningSession": "Ostatnia sesja słuchania",
|
||||||
"HeaderLatestEpisodes": "Najnowsze odcinki",
|
"HeaderLatestEpisodes": "Najnowsze odcinki",
|
||||||
"HeaderLibraries": "Biblioteki",
|
"HeaderLibraries": "Biblioteki",
|
||||||
"HeaderLibraryFiles": "Pliki w bibliotece",
|
"HeaderLibraryFiles": "Pliki w bibliotece",
|
||||||
"HeaderLibraryStats": "Statystyki biblioteki",
|
"HeaderLibraryStats": "Statystyki biblioteki",
|
||||||
"HeaderListeningSessions": "Sesje słuchania",
|
"HeaderListeningSessions": "Sesje słuchania",
|
||||||
"HeaderListeningStats": "Statystyki odtwarzania",
|
"HeaderListeningStats": "Statystyki słuchania",
|
||||||
"HeaderLogin": "Zaloguj się",
|
"HeaderLogin": "Zaloguj się",
|
||||||
"HeaderLogs": "Logi",
|
"HeaderLogs": "Logi",
|
||||||
"HeaderManageGenres": "Zarządzaj gatunkami",
|
"HeaderManageGenres": "Zarządzaj gatunkami",
|
||||||
@ -148,12 +149,13 @@
|
|||||||
"HeaderNewAccount": "Nowe konto",
|
"HeaderNewAccount": "Nowe konto",
|
||||||
"HeaderNewLibrary": "Nowa biblioteka",
|
"HeaderNewLibrary": "Nowa biblioteka",
|
||||||
"HeaderNotifications": "Powiadomienia",
|
"HeaderNotifications": "Powiadomienia",
|
||||||
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
|
"HeaderOpenIDConnectAuthentication": "Uwierzytelnianie OpenID Connect",
|
||||||
"HeaderOpenRSSFeed": "Utwórz kanał RSS",
|
"HeaderOpenRSSFeed": "Utwórz kanał RSS",
|
||||||
"HeaderOtherFiles": "Inne pliki",
|
"HeaderOtherFiles": "Inne pliki",
|
||||||
"HeaderPasswordAuthentication": "Uwierzytelnianie hasłem",
|
"HeaderPasswordAuthentication": "Uwierzytelnianie hasłem",
|
||||||
"HeaderPermissions": "Uprawnienia",
|
"HeaderPermissions": "Uprawnienia",
|
||||||
"HeaderPlayerQueue": "Kolejka odtwarzania",
|
"HeaderPlayerQueue": "Kolejka odtwarzania",
|
||||||
|
"HeaderPlayerSettings": "Ustawienia Odtwarzania",
|
||||||
"HeaderPlaylist": "Playlista",
|
"HeaderPlaylist": "Playlista",
|
||||||
"HeaderPlaylistItems": "Pozycje listy odtwarzania",
|
"HeaderPlaylistItems": "Pozycje listy odtwarzania",
|
||||||
"HeaderPodcastsToAdd": "Podcasty do dodania",
|
"HeaderPodcastsToAdd": "Podcasty do dodania",
|
||||||
@ -175,7 +177,7 @@
|
|||||||
"HeaderSettingsScanner": "Skanowanie",
|
"HeaderSettingsScanner": "Skanowanie",
|
||||||
"HeaderSleepTimer": "Wyłącznik czasowy",
|
"HeaderSleepTimer": "Wyłącznik czasowy",
|
||||||
"HeaderStatsLargestItems": "Największe pozycje",
|
"HeaderStatsLargestItems": "Największe pozycje",
|
||||||
"HeaderStatsLongestItems": "Najdłuższe pozycje (hrs)",
|
"HeaderStatsLongestItems": "Najdłuższe pozycje (godziny)",
|
||||||
"HeaderStatsMinutesListeningChart": "Czas słuchania w minutach (ostatnie 7 dni)",
|
"HeaderStatsMinutesListeningChart": "Czas słuchania w minutach (ostatnie 7 dni)",
|
||||||
"HeaderStatsRecentSessions": "Ostatnie sesje",
|
"HeaderStatsRecentSessions": "Ostatnie sesje",
|
||||||
"HeaderStatsTop10Authors": "Top 10 Autorów",
|
"HeaderStatsTop10Authors": "Top 10 Autorów",
|
||||||
@ -200,8 +202,8 @@
|
|||||||
"LabelActivity": "Aktywność",
|
"LabelActivity": "Aktywność",
|
||||||
"LabelAddToCollection": "Dodaj do kolekcji",
|
"LabelAddToCollection": "Dodaj do kolekcji",
|
||||||
"LabelAddToCollectionBatch": "Dodaj {0} książki do kolekcji",
|
"LabelAddToCollectionBatch": "Dodaj {0} książki do kolekcji",
|
||||||
"LabelAddToPlaylist": "Add to Playlist",
|
"LabelAddToPlaylist": "Dodaj do playlisty",
|
||||||
"LabelAddToPlaylistBatch": "Add {0} Items to Playlist",
|
"LabelAddToPlaylistBatch": "Dodaj {0} pozycji do playlisty",
|
||||||
"LabelAdded": "Dodane",
|
"LabelAdded": "Dodane",
|
||||||
"LabelAddedAt": "Dodano",
|
"LabelAddedAt": "Dodano",
|
||||||
"LabelAdminUsersOnly": "Tylko użytkownicy administracyjni",
|
"LabelAdminUsersOnly": "Tylko użytkownicy administracyjni",
|
||||||
@ -226,14 +228,14 @@
|
|||||||
"LabelBackupLocation": "Lokalizacja kopii zapasowej",
|
"LabelBackupLocation": "Lokalizacja kopii zapasowej",
|
||||||
"LabelBackupsEnableAutomaticBackups": "Włącz automatyczne kopie zapasowe",
|
"LabelBackupsEnableAutomaticBackups": "Włącz automatyczne kopie zapasowe",
|
||||||
"LabelBackupsEnableAutomaticBackupsHelp": "Kopie zapasowe są zapisywane w folderze /metadata/backups",
|
"LabelBackupsEnableAutomaticBackupsHelp": "Kopie zapasowe są zapisywane w folderze /metadata/backups",
|
||||||
"LabelBackupsMaxBackupSize": "Maksymalny łączny rozmiar backupów (w GB)",
|
"LabelBackupsMaxBackupSize": "Maksymalny rozmiar kopii zapasowej (w GB)",
|
||||||
"LabelBackupsMaxBackupSizeHelp": "Jako zabezpieczenie przed błędną konfiguracją, kopie zapasowe nie będą wykonywane, jeśli przekroczą skonfigurowany rozmiar.",
|
"LabelBackupsMaxBackupSizeHelp": "Jako zabezpieczenie przed błędną konfiguracją, kopie zapasowe nie będą wykonywane, jeśli przekroczą skonfigurowany rozmiar.",
|
||||||
"LabelBackupsNumberToKeep": "Liczba kopii zapasowych do przechowywania",
|
"LabelBackupsNumberToKeep": "Liczba kopii zapasowych do przechowywania",
|
||||||
"LabelBackupsNumberToKeepHelp": "Tylko 1 kopia zapasowa zostanie usunięta, więc jeśli masz już więcej kopii zapasowych, powinieneś je ręcznie usunąć.",
|
"LabelBackupsNumberToKeepHelp": "Tylko 1 kopia zapasowa zostanie usunięta, więc jeśli masz już więcej kopii zapasowych, powinieneś je ręcznie usunąć.",
|
||||||
"LabelBitrate": "Bitrate",
|
"LabelBitrate": "Bitrate",
|
||||||
"LabelBooks": "Książki",
|
"LabelBooks": "Książki",
|
||||||
"LabelButtonText": "Button Text",
|
"LabelButtonText": "Button Text",
|
||||||
"LabelByAuthor": "by {0}",
|
"LabelByAuthor": "autorstwa {0}",
|
||||||
"LabelChangePassword": "Zmień hasło",
|
"LabelChangePassword": "Zmień hasło",
|
||||||
"LabelChannels": "Kanały",
|
"LabelChannels": "Kanały",
|
||||||
"LabelChapterTitle": "Tytuł rozdziału",
|
"LabelChapterTitle": "Tytuł rozdziału",
|
||||||
@ -247,7 +249,7 @@
|
|||||||
"LabelCollections": "Kolekcje",
|
"LabelCollections": "Kolekcje",
|
||||||
"LabelComplete": "Ukończone",
|
"LabelComplete": "Ukończone",
|
||||||
"LabelConfirmPassword": "Potwierdź hasło",
|
"LabelConfirmPassword": "Potwierdź hasło",
|
||||||
"LabelContinueListening": "Kontynuuj odtwarzanie",
|
"LabelContinueListening": "Kontynuuj słuchanie",
|
||||||
"LabelContinueReading": "Kontynuuj czytanie",
|
"LabelContinueReading": "Kontynuuj czytanie",
|
||||||
"LabelContinueSeries": "Kontynuuj serię",
|
"LabelContinueSeries": "Kontynuuj serię",
|
||||||
"LabelCover": "Okładka",
|
"LabelCover": "Okładka",
|
||||||
@ -319,6 +321,7 @@
|
|||||||
"LabelHardDeleteFile": "Usuń trwale plik",
|
"LabelHardDeleteFile": "Usuń trwale plik",
|
||||||
"LabelHasEbook": "Ma ebooka",
|
"LabelHasEbook": "Ma ebooka",
|
||||||
"LabelHasSupplementaryEbook": "Posiada dodatkowy ebook",
|
"LabelHasSupplementaryEbook": "Posiada dodatkowy ebook",
|
||||||
|
"LabelHideSubtitles": "Ukryj napisy",
|
||||||
"LabelHighestPriority": "Najwyższy priorytet",
|
"LabelHighestPriority": "Najwyższy priorytet",
|
||||||
"LabelHost": "Host",
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Godzina",
|
"LabelHour": "Godzina",
|
||||||
@ -413,7 +416,7 @@
|
|||||||
"LabelOverwrite": "Nadpisz",
|
"LabelOverwrite": "Nadpisz",
|
||||||
"LabelPassword": "Hasło",
|
"LabelPassword": "Hasło",
|
||||||
"LabelPath": "Ścieżka",
|
"LabelPath": "Ścieżka",
|
||||||
"LabelPermanent": "Trwały",
|
"LabelPermanent": "Stałe",
|
||||||
"LabelPermissionsAccessAllLibraries": "Ma dostęp do wszystkich bibliotek",
|
"LabelPermissionsAccessAllLibraries": "Ma dostęp do wszystkich bibliotek",
|
||||||
"LabelPermissionsAccessAllTags": "Ma dostęp do wszystkich tagów",
|
"LabelPermissionsAccessAllTags": "Ma dostęp do wszystkich tagów",
|
||||||
"LabelPermissionsAccessExplicitContent": "Ma dostęp do treści oznacznych jako nieprzyzwoite",
|
"LabelPermissionsAccessExplicitContent": "Ma dostęp do treści oznacznych jako nieprzyzwoite",
|
||||||
@ -446,6 +449,7 @@
|
|||||||
"LabelRSSFeedPreventIndexing": "Zapobiegaj indeksowaniu",
|
"LabelRSSFeedPreventIndexing": "Zapobiegaj indeksowaniu",
|
||||||
"LabelRSSFeedSlug": "RSS Feed Slug",
|
"LabelRSSFeedSlug": "RSS Feed Slug",
|
||||||
"LabelRSSFeedURL": "URL kanały RSS",
|
"LabelRSSFeedURL": "URL kanały RSS",
|
||||||
|
"LabelReAddSeriesToContinueListening": "Ponownie Dodaj Serię do sekcji Kontunuuj Odtwarzanie",
|
||||||
"LabelRead": "Czytaj",
|
"LabelRead": "Czytaj",
|
||||||
"LabelReadAgain": "Czytaj ponownie",
|
"LabelReadAgain": "Czytaj ponownie",
|
||||||
"LabelReadEbookWithoutProgress": "Czytaj książkę bez zapamiętywania postępu",
|
"LabelReadEbookWithoutProgress": "Czytaj książkę bez zapamiętywania postępu",
|
||||||
@ -516,6 +520,7 @@
|
|||||||
"LabelShareURL": "Link do udziału",
|
"LabelShareURL": "Link do udziału",
|
||||||
"LabelShowAll": "Pokaż wszystko",
|
"LabelShowAll": "Pokaż wszystko",
|
||||||
"LabelShowSeconds": "Pokaż sekundy",
|
"LabelShowSeconds": "Pokaż sekundy",
|
||||||
|
"LabelShowSubtitles": "Pokaż Napisy",
|
||||||
"LabelSize": "Rozmiar",
|
"LabelSize": "Rozmiar",
|
||||||
"LabelSleepTimer": "Wyłącznik czasowy",
|
"LabelSleepTimer": "Wyłącznik czasowy",
|
||||||
"LabelSlug": "Slug",
|
"LabelSlug": "Slug",
|
||||||
@ -534,10 +539,10 @@
|
|||||||
"LabelStatsItemsFinished": "Pozycje zakończone",
|
"LabelStatsItemsFinished": "Pozycje zakończone",
|
||||||
"LabelStatsItemsInLibrary": "Pozycje w bibliotece",
|
"LabelStatsItemsInLibrary": "Pozycje w bibliotece",
|
||||||
"LabelStatsMinutes": "Minuty",
|
"LabelStatsMinutes": "Minuty",
|
||||||
"LabelStatsMinutesListening": "Minuty odtwarzania",
|
"LabelStatsMinutesListening": "Minuty słuchania",
|
||||||
"LabelStatsOverallDays": "Całkowity czas (dni)",
|
"LabelStatsOverallDays": "Całkowity czas (dni)",
|
||||||
"LabelStatsOverallHours": "Całkowity czas (godziny)",
|
"LabelStatsOverallHours": "Całkowity czas (godziny)",
|
||||||
"LabelStatsWeekListening": "Tydzień odtwarzania",
|
"LabelStatsWeekListening": "Tydzień słuchania",
|
||||||
"LabelSubtitle": "Podtytuł",
|
"LabelSubtitle": "Podtytuł",
|
||||||
"LabelSupportedFileTypes": "Obsługiwane typy plików",
|
"LabelSupportedFileTypes": "Obsługiwane typy plików",
|
||||||
"LabelTag": "Tag",
|
"LabelTag": "Tag",
|
||||||
@ -592,6 +597,7 @@
|
|||||||
"LabelVersion": "Wersja",
|
"LabelVersion": "Wersja",
|
||||||
"LabelViewBookmarks": "Wyświetlaj zakładki",
|
"LabelViewBookmarks": "Wyświetlaj zakładki",
|
||||||
"LabelViewChapters": "Wyświetlaj rozdziały",
|
"LabelViewChapters": "Wyświetlaj rozdziały",
|
||||||
|
"LabelViewPlayerSettings": "Zobacz ustawienia odtwarzacza",
|
||||||
"LabelViewQueue": "Wyświetlaj kolejkę odtwarzania",
|
"LabelViewQueue": "Wyświetlaj kolejkę odtwarzania",
|
||||||
"LabelVolume": "Głośność",
|
"LabelVolume": "Głośność",
|
||||||
"LabelWeekdaysToRun": "Dni tygodnia",
|
"LabelWeekdaysToRun": "Dni tygodnia",
|
||||||
@ -642,7 +648,7 @@
|
|||||||
"MessageConfirmRemoveEpisodes": "Czy na pewno chcesz usunąć {0} odcinki?",
|
"MessageConfirmRemoveEpisodes": "Czy na pewno chcesz usunąć {0} odcinki?",
|
||||||
"MessageConfirmRemoveListeningSessions": "Czy na pewno chcesz usunąć {0} sesji słuchania?",
|
"MessageConfirmRemoveListeningSessions": "Czy na pewno chcesz usunąć {0} sesji słuchania?",
|
||||||
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
||||||
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "Czy jesteś pewien, że chcesz usunąć twoją playlistę \"{0}\"?",
|
||||||
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
|
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
|
||||||
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
||||||
"MessageConfirmRenameGenreWarning": "Warning! A similar genre with a different casing already exists \"{0}\".",
|
"MessageConfirmRenameGenreWarning": "Warning! A similar genre with a different casing already exists \"{0}\".",
|
||||||
@ -663,7 +669,7 @@
|
|||||||
"MessageItemsSelected": "{0} zaznaczone elementy",
|
"MessageItemsSelected": "{0} zaznaczone elementy",
|
||||||
"MessageItemsUpdated": "{0} Items Updated",
|
"MessageItemsUpdated": "{0} Items Updated",
|
||||||
"MessageJoinUsOn": "Dołącz do nas na",
|
"MessageJoinUsOn": "Dołącz do nas na",
|
||||||
"MessageListeningSessionsInTheLastYear": "{0} sesje odsłuchowe w ostatnim roku",
|
"MessageListeningSessionsInTheLastYear": "Sesje słuchania w ostatnim roku: {0}",
|
||||||
"MessageLoading": "Ładowanie...",
|
"MessageLoading": "Ładowanie...",
|
||||||
"MessageLoadingFolders": "Ładowanie folderów...",
|
"MessageLoadingFolders": "Ładowanie folderów...",
|
||||||
"MessageLogsDescription": "Logi zapisane są w <code>/metadata/logs</code> jako pliki JSON. Logi awaryjne są zapisane w <code>/metadata/logs/crash_logs.txt</code>.",
|
"MessageLogsDescription": "Logi zapisane są w <code>/metadata/logs</code> jako pliki JSON. Logi awaryjne są zapisane w <code>/metadata/logs/crash_logs.txt</code>.",
|
||||||
@ -692,7 +698,7 @@
|
|||||||
"MessageNoIssues": "Brak problemów",
|
"MessageNoIssues": "Brak problemów",
|
||||||
"MessageNoItems": "Brak elementów",
|
"MessageNoItems": "Brak elementów",
|
||||||
"MessageNoItemsFound": "Nie znaleziono żadnych elementów",
|
"MessageNoItemsFound": "Nie znaleziono żadnych elementów",
|
||||||
"MessageNoListeningSessions": "Brak sesji odtwarzania",
|
"MessageNoListeningSessions": "Brak sesji słuchania",
|
||||||
"MessageNoLogs": "Brak logów",
|
"MessageNoLogs": "Brak logów",
|
||||||
"MessageNoMediaProgress": "Brak postępu",
|
"MessageNoMediaProgress": "Brak postępu",
|
||||||
"MessageNoNotifications": "Brak powiadomień",
|
"MessageNoNotifications": "Brak powiadomień",
|
||||||
@ -709,7 +715,7 @@
|
|||||||
"MessageOr": "lub",
|
"MessageOr": "lub",
|
||||||
"MessagePauseChapter": "Zatrzymaj odtwarzanie rozdziały",
|
"MessagePauseChapter": "Zatrzymaj odtwarzanie rozdziały",
|
||||||
"MessagePlayChapter": "Rozpocznij odtwarzanie od początku rozdziału",
|
"MessagePlayChapter": "Rozpocznij odtwarzanie od początku rozdziału",
|
||||||
"MessagePlaylistCreateFromCollection": "Utwórz listę odtwarznia na podstawie kolekcji",
|
"MessagePlaylistCreateFromCollection": "Utwórz listę odtwarzania na podstawie kolekcji",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nie ma adresu url kanału RSS, który mógłby zostać użyty do dopasowania",
|
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nie ma adresu url kanału RSS, który mógłby zostać użyty do dopasowania",
|
||||||
"MessageQuickMatchDescription": "Wypełnij puste informacje i okładkę pierwszym wynikiem dopasowania z '{0}'. Nie nadpisuje szczegółów, chyba że włączone jest ustawienie serwera 'Preferuj dopasowane metadane'.",
|
"MessageQuickMatchDescription": "Wypełnij puste informacje i okładkę pierwszym wynikiem dopasowania z '{0}'. Nie nadpisuje szczegółów, chyba że włączone jest ustawienie serwera 'Preferuj dopasowane metadane'.",
|
||||||
"MessageRemoveChapter": "Usuń rozdział",
|
"MessageRemoveChapter": "Usuń rozdział",
|
||||||
@ -724,8 +730,9 @@
|
|||||||
"MessageSelected": "{0} wybranych",
|
"MessageSelected": "{0} wybranych",
|
||||||
"MessageServerCouldNotBeReached": "Nie udało się uzyskać połączenia z serwerem",
|
"MessageServerCouldNotBeReached": "Nie udało się uzyskać połączenia z serwerem",
|
||||||
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
|
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
|
||||||
|
"MessageShareExpirationWillBe": "Czas udostępniania <strong>{0}</strong>",
|
||||||
"MessageShareExpiresIn": "Wygaśnie za {0}",
|
"MessageShareExpiresIn": "Wygaśnie za {0}",
|
||||||
"MessageShareURLWillBe": "URL udziału będzie <strong>{0}</strong>",
|
"MessageShareURLWillBe": "Udostępnione pod linkiem <strong>{0}</strong>",
|
||||||
"MessageStartPlaybackAtTime": "Rozpoczęcie odtwarzania \"{0}\" od {1}?",
|
"MessageStartPlaybackAtTime": "Rozpoczęcie odtwarzania \"{0}\" od {1}?",
|
||||||
"MessageThinking": "Myślę...",
|
"MessageThinking": "Myślę...",
|
||||||
"MessageUploaderItemFailed": "Nie udało się przesłać",
|
"MessageUploaderItemFailed": "Nie udało się przesłać",
|
||||||
@ -746,7 +753,7 @@
|
|||||||
"NoteUploaderUnsupportedFiles": "Nieobsługiwane pliki są ignorowane. Podczas dodawania folderu, inne pliki, które nie znajdują się w folderze elementu, są ignorowane.",
|
"NoteUploaderUnsupportedFiles": "Nieobsługiwane pliki są ignorowane. Podczas dodawania folderu, inne pliki, które nie znajdują się w folderze elementu, są ignorowane.",
|
||||||
"PlaceholderNewCollection": "Nowa nazwa kolekcji",
|
"PlaceholderNewCollection": "Nowa nazwa kolekcji",
|
||||||
"PlaceholderNewFolderPath": "Nowa ścieżka folderu",
|
"PlaceholderNewFolderPath": "Nowa ścieżka folderu",
|
||||||
"PlaceholderNewPlaylist": "New playlist name",
|
"PlaceholderNewPlaylist": "Nowa nazwa playlisty",
|
||||||
"PlaceholderSearch": "Szukanie..",
|
"PlaceholderSearch": "Szukanie..",
|
||||||
"PlaceholderSearchEpisode": "Szukanie odcinka..",
|
"PlaceholderSearchEpisode": "Szukanie odcinka..",
|
||||||
"ToastAccountUpdateFailed": "Nie udało się zaktualizować konta",
|
"ToastAccountUpdateFailed": "Nie udało się zaktualizować konta",
|
||||||
@ -802,12 +809,12 @@
|
|||||||
"ToastLibraryScanStarted": "Rozpoczęto skanowanie biblioteki",
|
"ToastLibraryScanStarted": "Rozpoczęto skanowanie biblioteki",
|
||||||
"ToastLibraryUpdateFailed": "Nie udało się zaktualizować biblioteki",
|
"ToastLibraryUpdateFailed": "Nie udało się zaktualizować biblioteki",
|
||||||
"ToastLibraryUpdateSuccess": "Zaktualizowano \"{0}\" pozycji",
|
"ToastLibraryUpdateSuccess": "Zaktualizowano \"{0}\" pozycji",
|
||||||
"ToastPlaylistCreateFailed": "Failed to create playlist",
|
"ToastPlaylistCreateFailed": "Nie udało się utworzyć playlisty",
|
||||||
"ToastPlaylistCreateSuccess": "Playlist created",
|
"ToastPlaylistCreateSuccess": "Playlista utworzona",
|
||||||
"ToastPlaylistRemoveFailed": "Failed to remove playlist",
|
"ToastPlaylistRemoveFailed": "Nie udało się usunąć playlisty",
|
||||||
"ToastPlaylistRemoveSuccess": "Playlist removed",
|
"ToastPlaylistRemoveSuccess": "Playlista usunięta",
|
||||||
"ToastPlaylistUpdateFailed": "Failed to update playlist",
|
"ToastPlaylistUpdateFailed": "Nie udało się zaktualizować playlisty",
|
||||||
"ToastPlaylistUpdateSuccess": "Playlist updated",
|
"ToastPlaylistUpdateSuccess": "Playlista zaktualizowana",
|
||||||
"ToastPodcastCreateFailed": "Nie udało się utworzyć podcastu",
|
"ToastPodcastCreateFailed": "Nie udało się utworzyć podcastu",
|
||||||
"ToastPodcastCreateSuccess": "Podcast został pomyślnie utworzony",
|
"ToastPodcastCreateSuccess": "Podcast został pomyślnie utworzony",
|
||||||
"ToastRSSFeedCloseFailed": "Zamknięcie kanału RSS nie powiodło się",
|
"ToastRSSFeedCloseFailed": "Zamknięcie kanału RSS nie powiodło się",
|
||||||
|
@ -285,6 +285,7 @@ class Server {
|
|||||||
'/library/:library/bookshelf/:id?',
|
'/library/:library/bookshelf/:id?',
|
||||||
'/library/:library/authors',
|
'/library/:library/authors',
|
||||||
'/library/:library/narrators',
|
'/library/:library/narrators',
|
||||||
|
'/library/:library/stats',
|
||||||
'/library/:library/series/:id?',
|
'/library/:library/series/:id?',
|
||||||
'/library/:library/podcast/search',
|
'/library/:library/podcast/search',
|
||||||
'/library/:library/podcast/latest',
|
'/library/:library/podcast/latest',
|
||||||
|
@ -14,6 +14,15 @@ const CoverManager = require('../managers/CoverManager')
|
|||||||
const LibraryItem = require('../objects/LibraryItem')
|
const LibraryItem = require('../objects/LibraryItem')
|
||||||
|
|
||||||
class PodcastController {
|
class PodcastController {
|
||||||
|
/**
|
||||||
|
* POST /api/podcasts
|
||||||
|
* Create podcast
|
||||||
|
*
|
||||||
|
* @this import('../routers/ApiRouter')
|
||||||
|
*
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
*/
|
||||||
async create(req, res) {
|
async create(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to create podcast`)
|
Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to create podcast`)
|
||||||
@ -133,6 +142,14 @@ class PodcastController {
|
|||||||
res.json({ podcast })
|
res.json({ podcast })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST: /api/podcasts/opml
|
||||||
|
*
|
||||||
|
* @this import('../routers/ApiRouter')
|
||||||
|
*
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
*/
|
||||||
async getFeedsFromOPMLText(req, res) {
|
async getFeedsFromOPMLText(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to get feeds from opml`)
|
Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to get feeds from opml`)
|
||||||
@ -143,8 +160,44 @@ class PodcastController {
|
|||||||
return res.sendStatus(400)
|
return res.sendStatus(400)
|
||||||
}
|
}
|
||||||
|
|
||||||
const rssFeedsData = await this.podcastManager.getOPMLFeeds(req.body.opmlText)
|
res.json({
|
||||||
res.json(rssFeedsData)
|
feeds: this.podcastManager.getParsedOPMLFileFeeds(req.body.opmlText)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST: /api/podcasts/opml/create
|
||||||
|
*
|
||||||
|
* @this import('../routers/ApiRouter')
|
||||||
|
*
|
||||||
|
* @param {import('express').Request} req
|
||||||
|
* @param {import('express').Response} res
|
||||||
|
*/
|
||||||
|
async bulkCreatePodcastsFromOpmlFeedUrls(req, res) {
|
||||||
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.error(`[PodcastController] Non-admin user "${req.user.username}" attempted to bulk create podcasts`)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rssFeeds = req.body.feeds
|
||||||
|
if (!Array.isArray(rssFeeds) || !rssFeeds.length || rssFeeds.some((feed) => !validateUrl(feed))) {
|
||||||
|
return res.status(400).send('Invalid request body. "feeds" must be an array of RSS feed URLs')
|
||||||
|
}
|
||||||
|
|
||||||
|
const libraryId = req.body.libraryId
|
||||||
|
const folderId = req.body.folderId
|
||||||
|
if (!libraryId || !folderId) {
|
||||||
|
return res.status(400).send('Invalid request body. "libraryId" and "folderId" are required')
|
||||||
|
}
|
||||||
|
|
||||||
|
const folder = await Database.libraryFolderModel.findByPk(folderId)
|
||||||
|
if (!folder || folder.libraryId !== libraryId) {
|
||||||
|
return res.status(404).send('Folder not found')
|
||||||
|
}
|
||||||
|
const autoDownloadEpisodes = !!req.body.autoDownloadEpisodes
|
||||||
|
this.podcastManager.createPodcastsFromFeedUrls(rssFeeds, folder, autoDownloadEpisodes, this.cronManager)
|
||||||
|
|
||||||
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkNewEpisodes(req, res) {
|
async checkNewEpisodes(req, res) {
|
||||||
|
@ -42,7 +42,7 @@ class BackupManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get maxBackupSize() {
|
get maxBackupSize() {
|
||||||
return global.ServerSettings.maxBackupSize || 1
|
return global.ServerSettings.maxBackupSize || Infinity
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
@ -419,14 +419,16 @@ class BackupManager {
|
|||||||
reject(err)
|
reject(err)
|
||||||
})
|
})
|
||||||
archive.on('progress', ({ fs: fsobj }) => {
|
archive.on('progress', ({ fs: fsobj }) => {
|
||||||
const maxBackupSizeInBytes = this.maxBackupSize * 1000 * 1000 * 1000
|
if (this.maxBackupSize !== Infinity) {
|
||||||
if (fsobj.processedBytes > maxBackupSizeInBytes) {
|
const maxBackupSizeInBytes = this.maxBackupSize * 1000 * 1000 * 1000
|
||||||
Logger.error(`[BackupManager] Archiver is too large - aborting to prevent endless loop, Bytes Processed: ${fsobj.processedBytes}`)
|
if (fsobj.processedBytes > maxBackupSizeInBytes) {
|
||||||
archive.abort()
|
Logger.error(`[BackupManager] Archiver is too large - aborting to prevent endless loop, Bytes Processed: ${fsobj.processedBytes}`)
|
||||||
setTimeout(() => {
|
archive.abort()
|
||||||
this.removeBackup(backup)
|
setTimeout(() => {
|
||||||
output.destroy('Backup too large') // Promise is reject in write stream error evt
|
this.removeBackup(backup)
|
||||||
}, 500)
|
output.destroy('Backup too large') // Promise is reject in write stream error evt
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ const Database = require('../Database')
|
|||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
|
|
||||||
const { getPodcastFeed } = require('../utils/podcastUtils')
|
const { getPodcastFeed } = require('../utils/podcastUtils')
|
||||||
const { removeFile, downloadFile } = require('../utils/fileUtils')
|
const { removeFile, downloadFile, sanitizeFilename, filePathToPOSIX, getFileTimestampsWithIno } = require('../utils/fileUtils')
|
||||||
const { levenshteinDistance } = require('../utils/index')
|
const { levenshteinDistance } = require('../utils/index')
|
||||||
const opmlParser = require('../utils/parsers/parseOPML')
|
const opmlParser = require('../utils/parsers/parseOPML')
|
||||||
const opmlGenerator = require('../utils/generators/opmlGenerator')
|
const opmlGenerator = require('../utils/generators/opmlGenerator')
|
||||||
@ -13,11 +13,13 @@ const prober = require('../utils/prober')
|
|||||||
const ffmpegHelpers = require('../utils/ffmpegHelpers')
|
const ffmpegHelpers = require('../utils/ffmpegHelpers')
|
||||||
|
|
||||||
const TaskManager = require('./TaskManager')
|
const TaskManager = require('./TaskManager')
|
||||||
|
const CoverManager = require('../managers/CoverManager')
|
||||||
|
|
||||||
const LibraryFile = require('../objects/files/LibraryFile')
|
const LibraryFile = require('../objects/files/LibraryFile')
|
||||||
const PodcastEpisodeDownload = require('../objects/PodcastEpisodeDownload')
|
const PodcastEpisodeDownload = require('../objects/PodcastEpisodeDownload')
|
||||||
const PodcastEpisode = require('../objects/entities/PodcastEpisode')
|
const PodcastEpisode = require('../objects/entities/PodcastEpisode')
|
||||||
const AudioFile = require('../objects/files/AudioFile')
|
const AudioFile = require('../objects/files/AudioFile')
|
||||||
|
const LibraryItem = require('../objects/LibraryItem')
|
||||||
|
|
||||||
class PodcastManager {
|
class PodcastManager {
|
||||||
constructor(watcher, notificationManager) {
|
constructor(watcher, notificationManager) {
|
||||||
@ -350,19 +352,23 @@ class PodcastManager {
|
|||||||
return matches.sort((a, b) => a.levenshtein - b.levenshtein)
|
return matches.sort((a, b) => a.levenshtein - b.levenshtein)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getParsedOPMLFileFeeds(opmlText) {
|
||||||
|
return opmlParser.parse(opmlText)
|
||||||
|
}
|
||||||
|
|
||||||
async getOPMLFeeds(opmlText) {
|
async getOPMLFeeds(opmlText) {
|
||||||
var extractedFeeds = opmlParser.parse(opmlText)
|
const extractedFeeds = opmlParser.parse(opmlText)
|
||||||
if (!extractedFeeds || !extractedFeeds.length) {
|
if (!extractedFeeds?.length) {
|
||||||
Logger.error('[PodcastManager] getOPMLFeeds: No RSS feeds found in OPML')
|
Logger.error('[PodcastManager] getOPMLFeeds: No RSS feeds found in OPML')
|
||||||
return {
|
return {
|
||||||
error: 'No RSS feeds found in OPML'
|
error: 'No RSS feeds found in OPML'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var rssFeedData = []
|
const rssFeedData = []
|
||||||
|
|
||||||
for (let feed of extractedFeeds) {
|
for (let feed of extractedFeeds) {
|
||||||
var feedData = await getPodcastFeed(feed.feedUrl, true)
|
const feedData = await getPodcastFeed(feed.feedUrl, true)
|
||||||
if (feedData) {
|
if (feedData) {
|
||||||
feedData.metadata.feedUrl = feed.feedUrl
|
feedData.metadata.feedUrl = feed.feedUrl
|
||||||
rssFeedData.push(feedData)
|
rssFeedData.push(feedData)
|
||||||
@ -392,5 +398,115 @@ class PodcastManager {
|
|||||||
queue: this.downloadQueue.filter((item) => !libraryId || item.libraryId === libraryId).map((item) => item.toJSONForClient())
|
queue: this.downloadQueue.filter((item) => !libraryId || item.libraryId === libraryId).map((item) => item.toJSONForClient())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string[]} rssFeedUrls
|
||||||
|
* @param {import('../models/LibraryFolder')} folder
|
||||||
|
* @param {boolean} autoDownloadEpisodes
|
||||||
|
* @param {import('../managers/CronManager')} cronManager
|
||||||
|
*/
|
||||||
|
async createPodcastsFromFeedUrls(rssFeedUrls, folder, autoDownloadEpisodes, cronManager) {
|
||||||
|
const task = TaskManager.createAndAddTask('opml-import', 'OPML import', `Creating podcasts from ${rssFeedUrls.length} RSS feeds`, true, null)
|
||||||
|
let numPodcastsAdded = 0
|
||||||
|
Logger.info(`[PodcastManager] createPodcastsFromFeedUrls: Importing ${rssFeedUrls.length} RSS feeds to folder "${folder.path}"`)
|
||||||
|
for (const feedUrl of rssFeedUrls) {
|
||||||
|
const feed = await getPodcastFeed(feedUrl).catch(() => null)
|
||||||
|
if (!feed?.episodes) {
|
||||||
|
TaskManager.createAndEmitFailedTask('opml-import-feed', 'OPML import feed', `Importing RSS feed "${feedUrl}"`, 'Failed to get podcast feed')
|
||||||
|
Logger.error(`[PodcastManager] createPodcastsFromFeedUrls: Failed to get podcast feed for "${feedUrl}"`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const podcastFilename = sanitizeFilename(feed.metadata.title)
|
||||||
|
const podcastPath = filePathToPOSIX(`${folder.path}/${podcastFilename}`)
|
||||||
|
// Check if a library item with this podcast folder exists already
|
||||||
|
const existingLibraryItem =
|
||||||
|
(await Database.libraryItemModel.count({
|
||||||
|
where: {
|
||||||
|
path: podcastPath
|
||||||
|
}
|
||||||
|
})) > 0
|
||||||
|
if (existingLibraryItem) {
|
||||||
|
Logger.error(`[PodcastManager] createPodcastsFromFeedUrls: Podcast already exists at path "${podcastPath}"`)
|
||||||
|
TaskManager.createAndEmitFailedTask('opml-import-feed', 'OPML import feed', `Creating podcast "${feed.metadata.title}"`, 'Podcast already exists at path')
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const successCreatingPath = await fs
|
||||||
|
.ensureDir(podcastPath)
|
||||||
|
.then(() => true)
|
||||||
|
.catch((error) => {
|
||||||
|
Logger.error(`[PodcastManager] Failed to ensure podcast dir "${podcastPath}"`, error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if (!successCreatingPath) {
|
||||||
|
Logger.error(`[PodcastManager] createPodcastsFromFeedUrls: Failed to create podcast folder at "${podcastPath}"`)
|
||||||
|
TaskManager.createAndEmitFailedTask('opml-import-feed', 'OPML import feed', `Creating podcast "${feed.metadata.title}"`, 'Failed to create podcast folder')
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPodcastMetadata = {
|
||||||
|
title: feed.metadata.title,
|
||||||
|
author: feed.metadata.author,
|
||||||
|
description: feed.metadata.description,
|
||||||
|
releaseDate: '',
|
||||||
|
genres: [...feed.metadata.categories],
|
||||||
|
feedUrl: feed.metadata.feedUrl,
|
||||||
|
imageUrl: feed.metadata.image,
|
||||||
|
itunesPageUrl: '',
|
||||||
|
itunesId: '',
|
||||||
|
itunesArtistId: '',
|
||||||
|
language: '',
|
||||||
|
numEpisodes: feed.numEpisodes
|
||||||
|
}
|
||||||
|
|
||||||
|
const libraryItemFolderStats = await getFileTimestampsWithIno(podcastPath)
|
||||||
|
const libraryItemPayload = {
|
||||||
|
path: podcastPath,
|
||||||
|
relPath: podcastFilename,
|
||||||
|
folderId: folder.id,
|
||||||
|
libraryId: folder.libraryId,
|
||||||
|
ino: libraryItemFolderStats.ino,
|
||||||
|
mtimeMs: libraryItemFolderStats.mtimeMs || 0,
|
||||||
|
ctimeMs: libraryItemFolderStats.ctimeMs || 0,
|
||||||
|
birthtimeMs: libraryItemFolderStats.birthtimeMs || 0,
|
||||||
|
media: {
|
||||||
|
metadata: newPodcastMetadata,
|
||||||
|
autoDownloadEpisodes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const libraryItem = new LibraryItem()
|
||||||
|
libraryItem.setData('podcast', libraryItemPayload)
|
||||||
|
|
||||||
|
// Download and save cover image
|
||||||
|
if (newPodcastMetadata.imageUrl) {
|
||||||
|
// TODO: Scan cover image to library files
|
||||||
|
// Podcast cover will always go into library item folder
|
||||||
|
const coverResponse = await CoverManager.downloadCoverFromUrl(libraryItem, newPodcastMetadata.imageUrl, true)
|
||||||
|
if (coverResponse) {
|
||||||
|
if (coverResponse.error) {
|
||||||
|
Logger.error(`[PodcastManager] createPodcastsFromFeedUrls: Download cover error from "${newPodcastMetadata.imageUrl}": ${coverResponse.error}`)
|
||||||
|
} else if (coverResponse.cover) {
|
||||||
|
libraryItem.media.coverPath = coverResponse.cover
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Database.createLibraryItem(libraryItem)
|
||||||
|
SocketAuthority.emitter('item_added', libraryItem.toJSONExpanded())
|
||||||
|
|
||||||
|
// Turn on podcast auto download cron if not already on
|
||||||
|
if (libraryItem.media.autoDownloadEpisodes) {
|
||||||
|
cronManager.checkUpdatePodcastCron(libraryItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
numPodcastsAdded++
|
||||||
|
}
|
||||||
|
task.setFinished(`Added ${numPodcastsAdded} podcasts`)
|
||||||
|
TaskManager.taskFinished(task)
|
||||||
|
Logger.info(`[PodcastManager] createPodcastsFromFeedUrls: Finished OPML import. Created ${numPodcastsAdded} podcasts out of ${rssFeedUrls.length} RSS feed URLs`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = PodcastManager
|
module.exports = PodcastManager
|
||||||
|
@ -9,8 +9,8 @@ class TaskManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Add task and emit socket task_started event
|
* Add task and emit socket task_started event
|
||||||
*
|
*
|
||||||
* @param {Task} task
|
* @param {Task} task
|
||||||
*/
|
*/
|
||||||
addTask(task) {
|
addTask(task) {
|
||||||
this.tasks.push(task)
|
this.tasks.push(task)
|
||||||
@ -19,24 +19,24 @@ class TaskManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove task and emit task_finished event
|
* Remove task and emit task_finished event
|
||||||
*
|
*
|
||||||
* @param {Task} task
|
* @param {Task} task
|
||||||
*/
|
*/
|
||||||
taskFinished(task) {
|
taskFinished(task) {
|
||||||
if (this.tasks.some(t => t.id === task.id)) {
|
if (this.tasks.some((t) => t.id === task.id)) {
|
||||||
this.tasks = this.tasks.filter(t => t.id !== task.id)
|
this.tasks = this.tasks.filter((t) => t.id !== task.id)
|
||||||
SocketAuthority.emitter('task_finished', task.toJSON())
|
SocketAuthority.emitter('task_finished', task.toJSON())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create new task and add
|
* Create new task and add
|
||||||
*
|
*
|
||||||
* @param {string} action
|
* @param {string} action
|
||||||
* @param {string} title
|
* @param {string} title
|
||||||
* @param {string} description
|
* @param {string} description
|
||||||
* @param {boolean} showSuccess
|
* @param {boolean} showSuccess
|
||||||
* @param {Object} [data]
|
* @param {Object} [data]
|
||||||
*/
|
*/
|
||||||
createAndAddTask(action, title, description, showSuccess, data = {}) {
|
createAndAddTask(action, title, description, showSuccess, data = {}) {
|
||||||
const task = new Task()
|
const task = new Task()
|
||||||
@ -44,5 +44,21 @@ class TaskManager {
|
|||||||
this.addTask(task)
|
this.addTask(task)
|
||||||
return task
|
return task
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new failed task and add
|
||||||
|
*
|
||||||
|
* @param {string} action
|
||||||
|
* @param {string} title
|
||||||
|
* @param {string} description
|
||||||
|
* @param {string} errorMessage
|
||||||
|
*/
|
||||||
|
createAndEmitFailedTask(action, title, description, errorMessage) {
|
||||||
|
const task = new Task()
|
||||||
|
task.setData(action, title, description, false)
|
||||||
|
task.setFailed(errorMessage)
|
||||||
|
SocketAuthority.emitter('task_started', task.toJSON())
|
||||||
|
return task
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = new TaskManager()
|
module.exports = new TaskManager()
|
||||||
|
@ -60,7 +60,7 @@ class Library extends Model {
|
|||||||
/**
|
/**
|
||||||
* Convert expanded Library to oldLibrary
|
* Convert expanded Library to oldLibrary
|
||||||
* @param {Library} libraryExpanded
|
* @param {Library} libraryExpanded
|
||||||
* @returns {Promise<oldLibrary>}
|
* @returns {oldLibrary}
|
||||||
*/
|
*/
|
||||||
static getOldLibrary(libraryExpanded) {
|
static getOldLibrary(libraryExpanded) {
|
||||||
const folders = libraryExpanded.libraryFolders.map((folder) => {
|
const folders = libraryExpanded.libraryFolders.map((folder) => {
|
||||||
|
@ -102,7 +102,7 @@ class ServerSettings {
|
|||||||
this.backupPath = settings.backupPath || Path.join(global.MetadataPath, 'backups')
|
this.backupPath = settings.backupPath || Path.join(global.MetadataPath, 'backups')
|
||||||
this.backupSchedule = settings.backupSchedule || false
|
this.backupSchedule = settings.backupSchedule || false
|
||||||
this.backupsToKeep = settings.backupsToKeep || 2
|
this.backupsToKeep = settings.backupsToKeep || 2
|
||||||
this.maxBackupSize = settings.maxBackupSize || 1
|
this.maxBackupSize = settings.maxBackupSize === 0 ? 0 : settings.maxBackupSize || 1
|
||||||
|
|
||||||
this.loggerDailyLogsToKeep = settings.loggerDailyLogsToKeep || 7
|
this.loggerDailyLogsToKeep = settings.loggerDailyLogsToKeep || 7
|
||||||
this.loggerScannerLogsToKeep = settings.loggerScannerLogsToKeep || 2
|
this.loggerScannerLogsToKeep = settings.loggerScannerLogsToKeep || 2
|
||||||
|
@ -45,6 +45,7 @@ class ApiRouter {
|
|||||||
this.backupManager = Server.backupManager
|
this.backupManager = Server.backupManager
|
||||||
/** @type {import('../Watcher')} */
|
/** @type {import('../Watcher')} */
|
||||||
this.watcher = Server.watcher
|
this.watcher = Server.watcher
|
||||||
|
/** @type {import('../managers/PodcastManager')} */
|
||||||
this.podcastManager = Server.podcastManager
|
this.podcastManager = Server.podcastManager
|
||||||
this.audioMetadataManager = Server.audioMetadataManager
|
this.audioMetadataManager = Server.audioMetadataManager
|
||||||
this.rssFeedManager = Server.rssFeedManager
|
this.rssFeedManager = Server.rssFeedManager
|
||||||
@ -239,7 +240,8 @@ class ApiRouter {
|
|||||||
//
|
//
|
||||||
this.router.post('/podcasts', PodcastController.create.bind(this))
|
this.router.post('/podcasts', PodcastController.create.bind(this))
|
||||||
this.router.post('/podcasts/feed', PodcastController.getPodcastFeed.bind(this))
|
this.router.post('/podcasts/feed', PodcastController.getPodcastFeed.bind(this))
|
||||||
this.router.post('/podcasts/opml', PodcastController.getFeedsFromOPMLText.bind(this))
|
this.router.post('/podcasts/opml/parse', PodcastController.getFeedsFromOPMLText.bind(this))
|
||||||
|
this.router.post('/podcasts/opml/create', PodcastController.bulkCreatePodcastsFromOpmlFeedUrls.bind(this))
|
||||||
this.router.get('/podcasts/:id/checknew', PodcastController.middleware.bind(this), PodcastController.checkNewEpisodes.bind(this))
|
this.router.get('/podcasts/:id/checknew', PodcastController.middleware.bind(this), PodcastController.checkNewEpisodes.bind(this))
|
||||||
this.router.get('/podcasts/:id/downloads', PodcastController.middleware.bind(this), PodcastController.getEpisodeDownloads.bind(this))
|
this.router.get('/podcasts/:id/downloads', PodcastController.middleware.bind(this), PodcastController.getEpisodeDownloads.bind(this))
|
||||||
this.router.get('/podcasts/:id/clear-queue', PodcastController.middleware.bind(this), PodcastController.clearEpisodeDownloadQueue.bind(this))
|
this.router.get('/podcasts/:id/clear-queue', PodcastController.middleware.bind(this), PodcastController.clearEpisodeDownloadQueue.bind(this))
|
||||||
|
@ -2,24 +2,26 @@ const { parseNfoMetadata } = require('../utils/parsers/parseNfoMetadata')
|
|||||||
const { readTextFile } = require('../utils/fileUtils')
|
const { readTextFile } = require('../utils/fileUtils')
|
||||||
|
|
||||||
class NfoFileScanner {
|
class NfoFileScanner {
|
||||||
constructor() { }
|
constructor() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse metadata from .nfo file found in library scan and update bookMetadata
|
* Parse metadata from .nfo file found in library scan and update bookMetadata
|
||||||
*
|
*
|
||||||
* @param {import('../models/LibraryItem').LibraryFileObject} nfoLibraryFileObj
|
* @param {import('../models/LibraryItem').LibraryFileObject} nfoLibraryFileObj
|
||||||
* @param {Object} bookMetadata
|
* @param {Object} bookMetadata
|
||||||
*/
|
*/
|
||||||
async scanBookNfoFile(nfoLibraryFileObj, bookMetadata) {
|
async scanBookNfoFile(nfoLibraryFileObj, bookMetadata) {
|
||||||
const nfoText = await readTextFile(nfoLibraryFileObj.metadata.path)
|
const nfoText = await readTextFile(nfoLibraryFileObj.metadata.path)
|
||||||
const nfoMetadata = nfoText ? await parseNfoMetadata(nfoText) : null
|
const nfoMetadata = nfoText ? await parseNfoMetadata(nfoText) : null
|
||||||
if (nfoMetadata) {
|
if (nfoMetadata) {
|
||||||
for (const key in nfoMetadata) {
|
for (const key in nfoMetadata) {
|
||||||
if (key === 'tags') { // Add tags only if tags are empty
|
if (key === 'tags') {
|
||||||
|
// Add tags only if tags are empty
|
||||||
if (nfoMetadata.tags.length) {
|
if (nfoMetadata.tags.length) {
|
||||||
bookMetadata.tags = nfoMetadata.tags
|
bookMetadata.tags = nfoMetadata.tags
|
||||||
}
|
}
|
||||||
} else if (key === 'genres') { // Add genres only if genres are empty
|
} else if (key === 'genres') {
|
||||||
|
// Add genres only if genres are empty
|
||||||
if (nfoMetadata.genres.length) {
|
if (nfoMetadata.genres.length) {
|
||||||
bookMetadata.genres = nfoMetadata.genres
|
bookMetadata.genres = nfoMetadata.genres
|
||||||
}
|
}
|
||||||
@ -33,10 +35,12 @@ class NfoFileScanner {
|
|||||||
}
|
}
|
||||||
} else if (key === 'series') {
|
} else if (key === 'series') {
|
||||||
if (nfoMetadata.series) {
|
if (nfoMetadata.series) {
|
||||||
bookMetadata.series = [{
|
bookMetadata.series = [
|
||||||
name: nfoMetadata.series,
|
{
|
||||||
sequence: nfoMetadata.sequence || null
|
name: nfoMetadata.series,
|
||||||
}]
|
sequence: nfoMetadata.sequence || null
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
} else if (nfoMetadata[key] && key !== 'sequence') {
|
} else if (nfoMetadata[key] && key !== 'sequence') {
|
||||||
bookMetadata[key] = nfoMetadata[key]
|
bookMetadata[key] = nfoMetadata[key]
|
||||||
@ -45,4 +49,4 @@ class NfoFileScanner {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = new NfoFileScanner()
|
module.exports = new NfoFileScanner()
|
||||||
|
@ -81,6 +81,10 @@ function parseNfoMetadata(nfoText) {
|
|||||||
case 'isbn-13':
|
case 'isbn-13':
|
||||||
metadata.isbn = value
|
metadata.isbn = value
|
||||||
break
|
break
|
||||||
|
case 'language':
|
||||||
|
case 'lang':
|
||||||
|
metadata.language = value
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,17 +1,21 @@
|
|||||||
const h = require('htmlparser2')
|
const h = require('htmlparser2')
|
||||||
const Logger = require('../../Logger')
|
const Logger = require('../../Logger')
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} opmlText
|
||||||
|
* @returns {Array<{title: string, feedUrl: string}>
|
||||||
|
*/
|
||||||
function parse(opmlText) {
|
function parse(opmlText) {
|
||||||
var feeds = []
|
var feeds = []
|
||||||
var parser = new h.Parser({
|
var parser = new h.Parser({
|
||||||
onopentag: (name, attribs) => {
|
onopentag: (name, attribs) => {
|
||||||
if (name === "outline" && attribs.type === 'rss') {
|
if (name === 'outline' && attribs.type === 'rss') {
|
||||||
if (!attribs.xmlurl) {
|
if (!attribs.xmlurl) {
|
||||||
Logger.error('[parseOPML] Invalid opml outline tag has no xmlurl attribute')
|
Logger.error('[parseOPML] Invalid opml outline tag has no xmlurl attribute')
|
||||||
} else {
|
} else {
|
||||||
feeds.push({
|
feeds.push({
|
||||||
title: attribs.title || 'No Title',
|
title: attribs.title || attribs.text || '',
|
||||||
text: attribs.text || '',
|
|
||||||
feedUrl: attribs.xmlurl
|
feedUrl: attribs.xmlurl
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -21,4 +25,4 @@ function parse(opmlText) {
|
|||||||
parser.write(opmlText)
|
parser.write(opmlText)
|
||||||
return feeds
|
return feeds
|
||||||
}
|
}
|
||||||
module.exports.parse = parse
|
module.exports.parse = parse
|
||||||
|
@ -289,7 +289,6 @@ module.exports.findMatchingEpisodesInFeed = (feed, searchTitle) => {
|
|||||||
const matches = []
|
const matches = []
|
||||||
feed.episodes.forEach((ep) => {
|
feed.episodes.forEach((ep) => {
|
||||||
if (!ep.title) return
|
if (!ep.title) return
|
||||||
|
|
||||||
const epTitle = ep.title.toLowerCase().trim()
|
const epTitle = ep.title.toLowerCase().trim()
|
||||||
if (epTitle === searchTitle) {
|
if (epTitle === searchTitle) {
|
||||||
matches.push({
|
matches.push({
|
||||||
|
@ -103,6 +103,16 @@ describe('parseNfoMetadata', () => {
|
|||||||
expect(result.asin).to.equal('B08X5JZJLH')
|
expect(result.asin).to.equal('B08X5JZJLH')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('parses language', () => {
|
||||||
|
const nfoText = 'Language: eng'
|
||||||
|
const result = parseNfoMetadata(nfoText)
|
||||||
|
expect(result.language).to.equal('eng')
|
||||||
|
|
||||||
|
const nfoText2 = 'lang: deu'
|
||||||
|
const result2 = parseNfoMetadata(nfoText2)
|
||||||
|
expect(result2.language).to.equal('deu')
|
||||||
|
})
|
||||||
|
|
||||||
it('parses description', () => {
|
it('parses description', () => {
|
||||||
const nfoText = 'Book Description\n=========\nThis is a book.\n It\'s good'
|
const nfoText = 'Book Description\n=========\nThis is a book.\n It\'s good'
|
||||||
const result = parseNfoMetadata(nfoText)
|
const result = parseNfoMetadata(nfoText)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user