mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-13 22:14:24 +02:00
Compare commits
66 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 32b886a0c3 | |||
| 2463c62bbf | |||
| 6057930507 | |||
| 9bbb23b853 | |||
| e865241258 | |||
| 1a67f57551 | |||
| 9b5bdc1fdb | |||
| acda776e3e | |||
| 8c4a9280ab | |||
| 1812282946 | |||
| 64e9ac9d8f | |||
| 0da9a04d8e | |||
| 11178f58bd | |||
| 08b2d07f65 | |||
| 3c210170b2 | |||
| 03d35421b4 | |||
| a176ba53e0 | |||
| e34dff8f30 | |||
| 0881ab4bfb | |||
| 20c32efd62 | |||
| e2b8127a5b | |||
| 90f32cefca | |||
| ab2e661e22 | |||
| a073aedca2 | |||
| b440a22ec9 | |||
| ec695e5f48 | |||
| 69ad0bf113 | |||
| 88f464398a | |||
| 6fce501389 | |||
| 559fab0d90 | |||
| 69c428802b | |||
| 6da631fa4f | |||
| f83b081791 | |||
| a6ce5fdd98 | |||
| 0a2e725bd3 | |||
| c07c4a3341 | |||
| 422773e745 | |||
| 7a298aa6f5 | |||
| 41daf557aa | |||
| de5bc63d88 | |||
| 5e2282ef76 | |||
| c819afc53b | |||
| 37221a0446 | |||
| 0f20ed101e | |||
| b0dbccd283 | |||
| 7001adb4dd | |||
| 02ecf7ccfe | |||
| 05ff5f1956 | |||
| 1649fb40db | |||
| 052e0059ff | |||
| 5edd799b3e | |||
| 1632d8edee | |||
| e6181196a7 | |||
| bea9d6aff4 | |||
| d410b13c9b | |||
| 8286aad7a4 | |||
| ed5960825b | |||
| 7fd8178dde | |||
| db17a5c88b | |||
| 2ec84edb5e | |||
| 0eed38b771 | |||
| 977bdbf0bb | |||
| a1ec10bd0d | |||
| 57d742b862 | |||
| e64302f1d4 | |||
| 0d8d0a650b |
@@ -46,5 +46,10 @@ RUN apk del make python3 g++
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
ENV PORT=80
|
||||
ENV CONFIG_PATH="/config"
|
||||
ENV METADATA_PATH="/metadata"
|
||||
ENV SOURCE="docker"
|
||||
|
||||
ENTRYPOINT ["tini", "--"]
|
||||
CMD ["node", "index.js"]
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
@showPlayerQueueItems="showPlayerQueueItemsModal = 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" :playback-rate="currentPlaybackRate" :library-item-id="libraryItemId" @select="selectBookmark" />
|
||||
|
||||
<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" />
|
||||
|
||||
|
||||
@@ -5,24 +5,26 @@
|
||||
<p class="text-3xl text-white truncate">{{ $strings.LabelYourBookmarks }}</p>
|
||||
</div>
|
||||
</template>
|
||||
<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="show" class="w-full h-full">
|
||||
<div v-if="show" class="w-full rounded-lg bg-bg box-shadow-md relative" style="max-height: 80vh">
|
||||
<div v-if="bookmarks.length" class="h-full max-h-[calc(80vh-60px)] w-full relative overflow-y-auto overflow-x-hidden">
|
||||
<template v-for="bookmark in bookmarks">
|
||||
<modals-bookmarks-bookmark-item :key="bookmark.id" :highlight="currentTime === bookmark.time" :bookmark="bookmark" @click="clickBookmark" @update="submitUpdateBookmark" @delete="deleteBookmark" />
|
||||
<modals-bookmarks-bookmark-item :key="bookmark.id" :highlight="currentTime === bookmark.time" :bookmark="bookmark" :playback-rate="playbackRate" @click="clickBookmark" @delete="deleteBookmark" />
|
||||
</template>
|
||||
<div v-if="!bookmarks.length" class="flex h-32 items-center justify-center">
|
||||
<p class="text-xl">{{ $strings.MessageNoBookmarks }}</p>
|
||||
</div>
|
||||
<div v-if="!hideCreate" class="w-full h-px bg-white bg-opacity-10" />
|
||||
<form v-if="!hideCreate" @submit.prevent="submitCreateBookmark">
|
||||
<div v-show="canCreateBookmark" class="flex px-4 py-2 items-center text-center border-b border-white border-opacity-10 text-white text-opacity-80">
|
||||
</div>
|
||||
<div v-else class="flex h-32 items-center justify-center">
|
||||
<p class="text-xl">{{ $strings.MessageNoBookmarks }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="canCreateBookmark && !hideCreate" class="w-full border-t border-white/10">
|
||||
<form @submit.prevent="submitCreateBookmark">
|
||||
<div class="flex px-4 py-2 items-center text-center border-b border-white border-opacity-10 text-white text-opacity-80">
|
||||
<div class="w-16 max-w-16 text-center">
|
||||
<p class="text-sm font-mono text-gray-400">
|
||||
{{ this.$secondsToTimestamp(currentTime) }}
|
||||
{{ this.$secondsToTimestamp(currentTime / playbackRate) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-grow px-2">
|
||||
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
|
||||
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full h-10" />
|
||||
</div>
|
||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-symbols text-2xl -mt-px">add</span></ui-btn>
|
||||
</div>
|
||||
@@ -45,6 +47,7 @@ export default {
|
||||
default: 0
|
||||
},
|
||||
libraryItemId: String,
|
||||
playbackRate: Number,
|
||||
hideCreate: Boolean
|
||||
},
|
||||
data() {
|
||||
@@ -57,6 +60,7 @@ export default {
|
||||
watch: {
|
||||
show(newVal) {
|
||||
if (newVal) {
|
||||
this.selectedBookmark = null
|
||||
this.showBookmarkTitleInput = false
|
||||
this.newBookmarkTitle = ''
|
||||
}
|
||||
@@ -72,7 +76,7 @@ export default {
|
||||
}
|
||||
},
|
||||
canCreateBookmark() {
|
||||
return !this.bookmarks.find((bm) => bm.time === this.currentTime)
|
||||
return !this.bookmarks.find((bm) => Math.abs(this.currentTime - bm.time) < 1)
|
||||
},
|
||||
dateFormat() {
|
||||
return this.$store.state.serverSettings.dateFormat
|
||||
@@ -102,19 +106,6 @@ export default {
|
||||
clickBookmark(bm) {
|
||||
this.$emit('select', bm)
|
||||
},
|
||||
submitUpdateBookmark(updatedBookmark) {
|
||||
var bookmark = { ...updatedBookmark }
|
||||
this.$axios
|
||||
.$patch(`/api/me/item/${this.libraryItemId}/bookmark`, bookmark)
|
||||
.then(() => {
|
||||
this.$toast.success(this.$strings.ToastBookmarkUpdateSuccess)
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
console.error(error)
|
||||
})
|
||||
this.show = false
|
||||
},
|
||||
submitCreateBookmark() {
|
||||
if (!this.newBookmarkTitle) {
|
||||
this.newBookmarkTitle = this.$formatDatetime(Date.now(), this.dateFormat, this.timeFormat)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="flex items-center px-4 py-4 justify-start relative bg-primary hover:bg-opacity-25" :class="wrapperClass" @click.stop="click" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||
<div class="flex items-center px-4 py-4 justify-start relative hover:bg-primary/10" :class="wrapperClass" @click.stop="click" @mouseover="mouseover" @mouseleave="mouseleave">
|
||||
<div class="w-16 max-w-16 text-center">
|
||||
<p class="text-sm font-mono text-gray-400">
|
||||
{{ this.$secondsToTimestamp(bookmark.time) }}
|
||||
{{ this.$secondsToTimestamp(bookmark.time / playbackRate) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex-grow overflow-hidden px-2">
|
||||
@@ -10,7 +10,7 @@
|
||||
<form @submit.prevent="submitUpdate">
|
||||
<div class="flex items-center">
|
||||
<div class="flex-grow pr-2">
|
||||
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full" />
|
||||
<ui-text-input v-model="newBookmarkTitle" placeholder="Note" class="w-full h-10" />
|
||||
</div>
|
||||
<ui-btn type="submit" color="success" :padding-x="4" class="h-10"><span class="material-symbols text-2xl -mt-px">forward</span></ui-btn>
|
||||
<div class="pl-2 flex items-center">
|
||||
@@ -35,7 +35,8 @@ export default {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
},
|
||||
highlight: Boolean
|
||||
highlight: Boolean,
|
||||
playbackRate: Number
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -83,11 +84,19 @@ export default {
|
||||
if (this.newBookmarkTitle === this.bookmark.title) {
|
||||
return this.cancelEditing()
|
||||
}
|
||||
var bookmark = { ...this.bookmark }
|
||||
const bookmark = { ...this.bookmark }
|
||||
bookmark.title = this.newBookmarkTitle
|
||||
this.$emit('update', bookmark)
|
||||
|
||||
this.$axios
|
||||
.$patch(`/api/me/item/${bookmark.libraryItemId}/bookmark`, bookmark)
|
||||
.then(() => {
|
||||
this.isEditing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
this.$toast.error(this.$strings.ToastFailedToUpdate)
|
||||
console.error(error)
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -113,6 +113,10 @@ export default {
|
||||
return false
|
||||
})
|
||||
console.log('updateResult', updateResult)
|
||||
} else if (!lastEpisodeCheck) {
|
||||
this.$toast.error(this.$strings.ToastDateTimeInvalidOrIncomplete)
|
||||
this.checkingNewEpisodes = false
|
||||
return false
|
||||
}
|
||||
|
||||
this.$axios
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
<ui-checkbox v-model="enableAutoScan" @input="toggleEnableAutoScan" :label="$strings.LabelEnable" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" />
|
||||
</div>
|
||||
<widgets-cron-expression-builder ref="cronExpressionBuilder" v-if="enableAutoScan" v-model="cronExpression" @input="updatedCron" />
|
||||
<div v-else>
|
||||
<p class="text-yellow-400 text-base">{{ $strings.MessageScheduleLibraryScanNote }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<ui-dropdown v-model="newEpisode.episodeType" :label="$strings.LabelEpisodeType" :items="episodeTypes" small />
|
||||
</div>
|
||||
<div class="w-2/5 p-1">
|
||||
<ui-text-input-with-label v-model="pubDateInput" @input="updatePubDate" type="datetime-local" :label="$strings.LabelPubDate" />
|
||||
<ui-text-input-with-label v-model="pubDateInput" ref="pubdate" type="datetime-local" :label="$strings.LabelPubDate" @input="updatePubDate" />
|
||||
</div>
|
||||
<div class="w-full p-1">
|
||||
<ui-text-input-with-label v-model="newEpisode.title" :label="$strings.LabelTitle" />
|
||||
@@ -145,11 +145,18 @@ export default {
|
||||
return null
|
||||
}
|
||||
|
||||
// Check pubdate is valid if it is being updated. Cannot be set to null in the web client
|
||||
if (this.newEpisode.pubDate === null && this.$refs.pubdate?.$refs?.input?.isInvalidDate) {
|
||||
this.$toast.error(this.$strings.ToastDateTimeInvalidOrIncomplete)
|
||||
return null
|
||||
}
|
||||
|
||||
const updatedDetails = this.getUpdatePayload()
|
||||
if (!Object.keys(updatedDetails).length) {
|
||||
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
|
||||
return false
|
||||
}
|
||||
|
||||
return this.updateDetails(updatedDetails)
|
||||
},
|
||||
async updateDetails(updatedDetails) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="absolute -top-10 lg:top-0 right-0 lg:right-2 flex items-center h-full">
|
||||
<controls-playback-speed-control v-model="playbackRate" @input="setPlaybackRate" @change="playbackRateChanged" class="mx-2 block" />
|
||||
|
||||
<ui-tooltip direction="left" :text="$strings.LabelVolume">
|
||||
<ui-tooltip direction="bottom" :text="$strings.LabelVolume">
|
||||
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden sm:block" />
|
||||
</ui-tooltip>
|
||||
|
||||
|
||||
@@ -63,9 +63,6 @@ export default {
|
||||
dayOfWeekToday() {
|
||||
return new Date().getDay()
|
||||
},
|
||||
firstWeekStart() {
|
||||
return this.$addDaysToToday(-this.daysToShow)
|
||||
},
|
||||
dayLabels() {
|
||||
return [
|
||||
{
|
||||
@@ -198,12 +195,25 @@ export default {
|
||||
let minValue = 0
|
||||
|
||||
const dates = []
|
||||
for (let i = 0; i < this.daysToShow + 1; i++) {
|
||||
const date = i === 0 ? this.firstWeekStart : this.$addDaysToDate(this.firstWeekStart, i)
|
||||
|
||||
const numDaysInTheLastYear = 52 * 7 + this.dayOfWeekToday
|
||||
const firstDay = this.$addDaysToToday(-numDaysInTheLastYear)
|
||||
for (let i = 0; i < numDaysInTheLastYear + 1; i++) {
|
||||
const date = i === 0 ? firstDay : this.$addDaysToDate(firstDay, i)
|
||||
const dateString = this.$formatJsDate(date, 'yyyy-MM-dd')
|
||||
|
||||
if (this.daysListening[dateString] > 0) {
|
||||
this.daysListenedInTheLastYear++
|
||||
}
|
||||
|
||||
const visibleDayIndex = i - (numDaysInTheLastYear - this.daysToShow)
|
||||
if (visibleDayIndex < 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const dateObj = {
|
||||
col: Math.floor(i / 7),
|
||||
row: i % 7,
|
||||
col: Math.floor(visibleDayIndex / 7),
|
||||
row: visibleDayIndex % 7,
|
||||
date,
|
||||
dateString,
|
||||
datePretty: this.$formatJsDate(date, 'MMM d, yyyy'),
|
||||
@@ -215,7 +225,6 @@ export default {
|
||||
dates.push(dateObj)
|
||||
|
||||
if (dateObj.value > 0) {
|
||||
this.daysListenedInTheLastYear++
|
||||
if (dateObj.value > maxValue) maxValue = dateObj.value
|
||||
if (!minValue || dateObj.value < minValue) minValue = dateObj.value
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ export default {
|
||||
return this.episode?.title || ''
|
||||
},
|
||||
episodeSubtitle() {
|
||||
return this.episode?.subtitle || ''
|
||||
return this.episode?.subtitle || this.episode?.description || ''
|
||||
},
|
||||
episodeType() {
|
||||
return this.episode?.episodeType || ''
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" />
|
||||
</form>
|
||||
</div>
|
||||
<div class="relative min-h-[176px]">
|
||||
<div class="relative min-h-44">
|
||||
<template v-for="episode in totalEpisodes">
|
||||
<div :key="episode" :id="`episode-${episode - 1}`" class="w-full h-44 px-2 py-3 overflow-hidden relative border-b border-white/10">
|
||||
<!-- episode is mounted here -->
|
||||
@@ -39,7 +39,7 @@
|
||||
<div v-if="isSearching" class="w-full h-full absolute inset-0 flex justify-center py-12" :class="{ 'bg-black/50': totalEpisodes }">
|
||||
<ui-loading-indicator />
|
||||
</div>
|
||||
<div v-else-if="!totalEpisodes" class="h-44 flex items-center justify-center">
|
||||
<div v-else-if="!totalEpisodes" id="no-episodes" class="h-44 flex items-center justify-center">
|
||||
<p class="text-lg">{{ $strings.MessageNoEpisodes }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -80,7 +80,8 @@ export default {
|
||||
episodeComponentRefs: {},
|
||||
windowHeight: 0,
|
||||
episodesTableOffsetTop: 0,
|
||||
episodeRowHeight: 176
|
||||
episodeRowHeight: 44 * 4, // h-44,
|
||||
currScrollTop: 0
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -484,9 +485,8 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
scroll(evt) {
|
||||
if (!evt?.target?.scrollTop) return
|
||||
const scrollTop = Math.max(evt.target.scrollTop - this.episodesTableOffsetTop, 0)
|
||||
handleScroll() {
|
||||
const scrollTop = this.currScrollTop
|
||||
let firstEpisodeIndex = Math.floor(scrollTop / this.episodeRowHeight)
|
||||
let lastEpisodeIndex = Math.ceil((scrollTop + this.windowHeight) / this.episodeRowHeight)
|
||||
lastEpisodeIndex = Math.min(this.totalEpisodes - 1, lastEpisodeIndex)
|
||||
@@ -501,6 +501,12 @@ export default {
|
||||
})
|
||||
this.mountEpisodes(firstEpisodeIndex, lastEpisodeIndex + 1)
|
||||
},
|
||||
scroll(evt) {
|
||||
if (!evt?.target?.scrollTop) return
|
||||
const scrollTop = Math.max(evt.target.scrollTop - this.episodesTableOffsetTop, 0)
|
||||
this.currScrollTop = scrollTop
|
||||
this.handleScroll()
|
||||
},
|
||||
initListeners() {
|
||||
const itemPageWrapper = document.getElementById('item-page-wrapper')
|
||||
if (itemPageWrapper) {
|
||||
@@ -532,11 +538,24 @@ export default {
|
||||
this.episodesTableOffsetTop = (lazyEpisodesTableEl?.offsetTop || 0) + 64
|
||||
|
||||
this.windowHeight = window.innerHeight
|
||||
this.episodesPerPage = Math.ceil(this.windowHeight / this.episodeRowHeight)
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.mountEpisodes(0, Math.min(this.episodesPerPage, this.totalEpisodes))
|
||||
this.recalcEpisodeRowHeight()
|
||||
this.episodesPerPage = Math.ceil(this.windowHeight / this.episodeRowHeight)
|
||||
// Maybe update currScrollTop if items were removed
|
||||
const itemPageWrapper = document.getElementById('item-page-wrapper')
|
||||
const { scrollHeight, clientHeight } = itemPageWrapper
|
||||
const maxScrollTop = scrollHeight - clientHeight
|
||||
this.currScrollTop = Math.min(this.currScrollTop, maxScrollTop)
|
||||
this.handleScroll()
|
||||
})
|
||||
},
|
||||
recalcEpisodeRowHeight() {
|
||||
const episodeRowEl = document.getElementById('episode-0') || document.getElementById('no-episodes')
|
||||
if (episodeRowEl) {
|
||||
const height = getComputedStyle(episodeRowEl).height
|
||||
this.episodeRowHeight = parseInt(height) || this.episodeRowHeight
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
|
||||
@@ -1,24 +1,6 @@
|
||||
<template>
|
||||
<div ref="wrapper" class="relative">
|
||||
<input
|
||||
:id="inputId"
|
||||
:name="inputName"
|
||||
ref="input"
|
||||
v-model="inputValue"
|
||||
:type="actualType"
|
||||
:step="step"
|
||||
:min="min"
|
||||
:readonly="readonly"
|
||||
:disabled="disabled"
|
||||
:placeholder="placeholder"
|
||||
dir="auto"
|
||||
class="rounded bg-primary text-gray-200 focus:border-gray-300 focus:bg-bg focus:outline-none border border-gray-600 h-full w-full"
|
||||
:class="classList"
|
||||
@keyup="keyup"
|
||||
@change="change"
|
||||
@focus="focused"
|
||||
@blur="blurred"
|
||||
/>
|
||||
<input :id="inputId" :name="inputName" ref="input" v-model="inputValue" :type="actualType" :step="step" :min="min" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" dir="auto" class="rounded bg-primary text-gray-200 focus:bg-bg focus:outline-none border h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
|
||||
<div v-if="clearable && inputValue" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center">
|
||||
<span class="material-symbols text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span>
|
||||
</div>
|
||||
@@ -65,7 +47,8 @@ export default {
|
||||
showPassword: false,
|
||||
isHovering: false,
|
||||
isFocused: false,
|
||||
hasCopied: false
|
||||
hasCopied: false,
|
||||
isInvalidDate: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -84,6 +67,10 @@ export default {
|
||||
if (this.noSpinner) _list.push('no-spinner')
|
||||
if (this.textCenter) _list.push('text-center')
|
||||
if (this.customInputClass) _list.push(this.customInputClass)
|
||||
|
||||
if (this.isInvalidDate) _list.push('border-error')
|
||||
else _list.push('focus:border-gray-300 border-gray-600')
|
||||
|
||||
return _list.join(' ')
|
||||
},
|
||||
actualType() {
|
||||
@@ -118,6 +105,14 @@ export default {
|
||||
},
|
||||
keyup(e) {
|
||||
this.$emit('keyup', e)
|
||||
|
||||
if (this.type === 'datetime-local') {
|
||||
if (e.target.validity?.badInput) {
|
||||
this.isInvalidDate = true
|
||||
} else {
|
||||
this.isInvalidDate = false
|
||||
}
|
||||
}
|
||||
},
|
||||
blur() {
|
||||
if (this.$refs.input) this.$refs.input.blur()
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<slot>
|
||||
<label :for="identifier" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }"
|
||||
>{{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em></label
|
||||
>
|
||||
<label :for="identifier" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }">
|
||||
{{ label }}
|
||||
<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
|
||||
</label>
|
||||
</slot>
|
||||
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" :class="inputClass" @blur="inputBlurred" />
|
||||
</div>
|
||||
@@ -57,4 +58,4 @@ export default {
|
||||
},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -249,11 +249,33 @@ export default {
|
||||
}
|
||||
}
|
||||
return target
|
||||
},
|
||||
enableBreakParagraphOnReturn() {
|
||||
// Trix works with divs by default, we want paragraphs instead
|
||||
Trix.config.blockAttributes.default.tagName = 'p'
|
||||
// Enable break paragraph on Enter (Shift + Enter will still create a line break)
|
||||
Trix.config.blockAttributes.default.breakOnReturn = true
|
||||
|
||||
// Hack to fix buggy paragraph breaks
|
||||
// Copied from https://github.com/basecamp/trix/issues/680#issuecomment-735742942
|
||||
Trix.Block.prototype.breaksOnReturn = function () {
|
||||
const attr = this.getLastAttribute()
|
||||
const config = Trix.getBlockConfig(attr ? attr : 'default')
|
||||
return config ? config.breakOnReturn : false
|
||||
}
|
||||
Trix.LineBreakInsertion.prototype.shouldInsertBlockBreak = function () {
|
||||
if (this.block.hasAttributes() && this.block.isListItem() && !this.block.isEmpty()) {
|
||||
return this.startLocation.offset > 0
|
||||
} else {
|
||||
return !this.shouldBreakFormattedBlock() ? this.breaksOnReturn : false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
/** Override editor configuration */
|
||||
this.overrideConfig(this.config)
|
||||
this.enableBreakParagraphOnReturn()
|
||||
/** Check if editor read-only mode is required */
|
||||
this.decorateDisabledEditor(this.disabledEditor)
|
||||
this.$nextTick(() => {
|
||||
@@ -283,4 +305,4 @@ export default {
|
||||
.trix_container .trix-content {
|
||||
background-color: white;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import Vue from 'vue'
|
||||
import '@/plugins/utils'
|
||||
|
||||
// This is the actual function that is being tested
|
||||
const elapsedPrettyExtended = Vue.prototype.$elapsedPrettyExtended
|
||||
|
||||
// Helper function to convert days, hours, minutes, seconds to total seconds
|
||||
function DHMStoSeconds(days, hours, minutes, seconds) {
|
||||
return seconds + minutes * 60 + hours * 3600 + days * 86400
|
||||
}
|
||||
|
||||
describe('$elapsedPrettyExtended', () => {
|
||||
describe('function is on the Vue Prototype', () => {
|
||||
it('exists as a function on Vue.prototype', () => {
|
||||
expect(Vue.prototype.$elapsedPrettyExtended).to.exist
|
||||
expect(Vue.prototype.$elapsedPrettyExtended).to.be.a('function')
|
||||
})
|
||||
})
|
||||
|
||||
describe('param default values', () => {
|
||||
const testSeconds = DHMStoSeconds(0, 25, 1, 5) // 25h 1m 5s = 90065 seconds
|
||||
|
||||
it('uses useDays=true showSeconds=true by default', () => {
|
||||
expect(elapsedPrettyExtended(testSeconds)).to.equal('1d 1h 1m 5s')
|
||||
})
|
||||
|
||||
it('only useDays=false overrides useDays but keeps showSeconds=true', () => {
|
||||
expect(elapsedPrettyExtended(testSeconds, false)).to.equal('25h 1m 5s')
|
||||
})
|
||||
|
||||
it('explicit useDays=false showSeconds=false overrides both', () => {
|
||||
expect(elapsedPrettyExtended(testSeconds, false, false)).to.equal('25h 1m')
|
||||
})
|
||||
})
|
||||
|
||||
describe('useDays=false showSeconds=true', () => {
|
||||
const useDaysFalse = false
|
||||
const showSecondsTrue = true
|
||||
const testCases = [
|
||||
[[0, 0, 0, 0], '', '0s -> ""'],
|
||||
[[0, 1, 0, 1], '1h 1s', '1h 1s -> 1h 1s'],
|
||||
[[0, 25, 0, 1], '25h 1s', '25h 1s -> 25h 1s']
|
||||
]
|
||||
|
||||
testCases.forEach(([dhms, expected, description]) => {
|
||||
it(description, () => {
|
||||
expect(elapsedPrettyExtended(DHMStoSeconds(...dhms), useDaysFalse, showSecondsTrue)).to.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('useDays=true showSeconds=true', () => {
|
||||
const useDaysTrue = true
|
||||
const showSecondsTrue = true
|
||||
const testCases = [
|
||||
[[0, 0, 0, 0], '', '0s -> ""'],
|
||||
[[0, 1, 0, 1], '1h 1s', '1h 1s -> 1h 1s'],
|
||||
[[0, 25, 0, 1], '1d 1h 1s', '25h 1s -> 1d 1h 1s']
|
||||
]
|
||||
|
||||
testCases.forEach(([dhms, expected, description]) => {
|
||||
it(description, () => {
|
||||
expect(elapsedPrettyExtended(DHMStoSeconds(...dhms), useDaysTrue, showSecondsTrue)).to.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('useDays=true showSeconds=false', () => {
|
||||
const useDaysTrue = true
|
||||
const showSecondsFalse = false
|
||||
const testCases = [
|
||||
[[0, 0, 0, 0], '', '0s -> ""'],
|
||||
[[0, 1, 0, 0], '1h', '1h -> 1h'],
|
||||
[[0, 1, 0, 1], '1h', '1h 1s -> 1h'],
|
||||
[[0, 1, 1, 0], '1h 1m', '1h 1m -> 1h 1m'],
|
||||
[[0, 25, 0, 0], '1d 1h', '25h -> 1d 1h'],
|
||||
[[0, 25, 0, 1], '1d 1h', '25h 1s -> 1d 1h'],
|
||||
[[2, 0, 0, 0], '2d', '2d -> 2d']
|
||||
]
|
||||
|
||||
testCases.forEach(([dhms, expected, description]) => {
|
||||
it(description, () => {
|
||||
expect(elapsedPrettyExtended(DHMStoSeconds(...dhms), useDaysTrue, showSecondsFalse)).to.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('rounding useDays=true showSeconds=true', () => {
|
||||
const useDaysTrue = true
|
||||
const showSecondsTrue = true
|
||||
const testCases = [
|
||||
// Seconds rounding
|
||||
[[0, 0, 0, 1], '1s', '1s -> 1s'],
|
||||
[[0, 0, 0, 29.9], '30s', '29.9s -> 30s'],
|
||||
[[0, 0, 0, 30], '30s', '30s -> 30s'],
|
||||
[[0, 0, 0, 30.1], '30s', '30.1s -> 30s'],
|
||||
[[0, 0, 0, 59.4], '59s', '59.4s -> 59s'],
|
||||
[[0, 0, 0, 59.5], '1m', '59.5s -> 1m'],
|
||||
|
||||
// Minutes rounding
|
||||
[[0, 0, 59, 29], '59m 29s', '59m 29s -> 59m 29s'],
|
||||
[[0, 0, 59, 30], '59m 30s', '59m 30s -> 59m 30s'],
|
||||
[[0, 0, 59, 59.5], '1h', '59m 59.5s -> 1h'],
|
||||
|
||||
// Hours rounding
|
||||
[[0, 23, 59, 29], '23h 59m 29s', '23h 59m 29s -> 23h 59m 29s'],
|
||||
[[0, 23, 59, 30], '23h 59m 30s', '23h 59m 30s -> 23h 59m 30s'],
|
||||
[[0, 23, 59, 59.5], '1d', '23h 59m 59.5s -> 1d'],
|
||||
|
||||
// The actual bug case
|
||||
[[44, 23, 59, 30], '44d 23h 59m 30s', '44d 23h 59m 30s -> 44d 23h 59m 30s']
|
||||
]
|
||||
|
||||
testCases.forEach(([dhms, expected, description]) => {
|
||||
it(description, () => {
|
||||
expect(elapsedPrettyExtended(DHMStoSeconds(...dhms), useDaysTrue, showSecondsTrue)).to.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('rounding useDays=true showSeconds=false', () => {
|
||||
const useDaysTrue = true
|
||||
const showSecondsFalse = false
|
||||
const testCases = [
|
||||
// Seconds rounding - these cases changed behavior from original
|
||||
[[0, 0, 0, 1], '', '1s -> ""'],
|
||||
[[0, 0, 0, 29.9], '', '29.9s -> ""'],
|
||||
[[0, 0, 0, 30], '', '30s -> ""'],
|
||||
[[0, 0, 0, 30.1], '', '30.1s -> ""'],
|
||||
[[0, 0, 0, 59.4], '', '59.4s -> ""'],
|
||||
[[0, 0, 0, 59.5], '1m', '59.5s -> 1m'],
|
||||
// This is unexpected behavior, but it's consistent with the original behavior
|
||||
// We preserved the test case, to document the current behavior
|
||||
// - with showSeconds=false,
|
||||
// one might expect: 1m 29.5s --round(1.4901m)-> 1m
|
||||
// actual implementation: 1h 29.5s --roundSeconds-> 1h 30s --roundMinutes-> 2m
|
||||
// So because of the separate rounding of seconds, and then minutes, it returns 2m
|
||||
[[0, 0, 1, 29.5], '2m', '1m 29.5s -> 2m'],
|
||||
|
||||
// Minutes carry - actual bug fixes below
|
||||
[[0, 0, 59, 29], '59m', '59m 29s -> 59m'],
|
||||
[[0, 0, 59, 30], '1h', '59m 30s -> 1h'], // This was an actual bug, used to return 60m
|
||||
[[0, 0, 59, 59.5], '1h', '59m 59.5s -> 1h'],
|
||||
|
||||
// Hours carry
|
||||
[[0, 23, 59, 29], '23h 59m', '23h 59m 29s -> 23h 59m'],
|
||||
[[0, 23, 59, 30], '1d', '23h 59m 30s -> 1d'], // This was an actual bug, used to return 23h 60m
|
||||
[[0, 23, 59, 59.5], '1d', '23h 59m 59.5s -> 1d'],
|
||||
|
||||
// The actual bug case
|
||||
[[44, 23, 59, 30], '45d', '44d 23h 59m 30s -> 45d'] // This was an actual bug, used to return 44d 23h 60m
|
||||
]
|
||||
|
||||
testCases.forEach(([dhms, expected, description]) => {
|
||||
it(description, () => {
|
||||
expect(elapsedPrettyExtended(DHMStoSeconds(...dhms), useDaysTrue, showSecondsFalse)).to.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('empty values', () => {
|
||||
const paramCombos = [
|
||||
// useDays, showSeconds, description
|
||||
[true, true, 'with days and seconds'],
|
||||
[true, false, 'with days, no seconds'],
|
||||
[false, true, 'no days, with seconds'],
|
||||
[false, false, 'no days, no seconds']
|
||||
]
|
||||
|
||||
const emptyInputs = [
|
||||
// input, description
|
||||
[null, 'null input'],
|
||||
[undefined, 'undefined input'],
|
||||
[0, 'zero'],
|
||||
[0.49, 'rounds to zero'] // Just under rounding threshold
|
||||
]
|
||||
|
||||
paramCombos.forEach(([useDays, showSeconds, paramDesc]) => {
|
||||
describe(paramDesc, () => {
|
||||
emptyInputs.forEach(([input, desc]) => {
|
||||
it(desc, () => {
|
||||
expect(elapsedPrettyExtended(input, useDays, showSeconds)).to.equal('')
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -141,7 +141,7 @@
|
||||
</div>
|
||||
|
||||
<modals-podcast-episode-feed v-model="showPodcastEpisodeFeed" :library-item="libraryItem" :episodes="podcastFeedEpisodes" />
|
||||
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :library-item-id="libraryItemId" hide-create @select="selectBookmark" />
|
||||
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :playback-rate="1" :library-item-id="libraryItemId" hide-create @select="selectBookmark" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
+10
-5
@@ -69,17 +69,22 @@ Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true, showSeconds = t
|
||||
let hours = Math.floor(minutes / 60)
|
||||
minutes -= hours * 60
|
||||
|
||||
// Handle rollovers before days calculation
|
||||
if (minutes && seconds && !showSeconds) {
|
||||
if (seconds >= 30) minutes++
|
||||
if (minutes >= 60) {
|
||||
hours++ // Increment hours if minutes roll over
|
||||
minutes -= 60 // adjust minutes
|
||||
}
|
||||
}
|
||||
|
||||
// Now calculate days with the final hours value
|
||||
let days = 0
|
||||
if (useDays || Math.floor(hours / 24) >= 100) {
|
||||
days = Math.floor(hours / 24)
|
||||
hours -= days * 24
|
||||
}
|
||||
|
||||
// If not showing seconds then round minutes up
|
||||
if (minutes && seconds && !showSeconds) {
|
||||
if (seconds >= 30) minutes++
|
||||
}
|
||||
|
||||
const strs = []
|
||||
if (days) strs.push(`${days}d`)
|
||||
if (hours) strs.push(`${hours}h`)
|
||||
|
||||
+117
-1
@@ -1 +1,117 @@
|
||||
{}
|
||||
{
|
||||
"ButtonAdd": "Дадаць",
|
||||
"ButtonAddChapters": "Дадаць раздзелы",
|
||||
"ButtonAddDevice": "Дадаць прыладу",
|
||||
"ButtonAddLibrary": "Дадаць бібліятэку",
|
||||
"ButtonAddPodcasts": "Дадаць падкасты",
|
||||
"ButtonAddUser": "Дадаць карыстальніка",
|
||||
"ButtonAddYourFirstLibrary": "Дадайце сваю першую бібліятэку",
|
||||
"ButtonApply": "Ужыць",
|
||||
"ButtonApplyChapters": "Ужыць раздзелы",
|
||||
"ButtonAuthors": "Аўтары",
|
||||
"ButtonBack": "Назад",
|
||||
"ButtonBrowseForFolder": "Знайсці тэчку",
|
||||
"ButtonCancel": "Адмяніць",
|
||||
"ButtonCancelEncode": "Адмяніць кадзіраванне",
|
||||
"ButtonChangeRootPassword": "Зменіце Root пароль",
|
||||
"ButtonCheckAndDownloadNewEpisodes": "Праверыць і спампаваць новыя эпізоды",
|
||||
"ButtonChooseAFolder": "Выбраць тэчку",
|
||||
"ButtonChooseFiles": "Выбраць файлы",
|
||||
"ButtonClearFilter": "Ачысціць фільтр",
|
||||
"ButtonCloseFeed": "Закрыць стужку",
|
||||
"ButtonCloseSession": "Закрыць адкрыты сеанс",
|
||||
"ButtonCollections": "Калекцыі",
|
||||
"ButtonConfigureScanner": "Наладзіць сканер",
|
||||
"ButtonCreate": "Ствараць",
|
||||
"ButtonCreateBackup": "Стварыць рэзервовую копію",
|
||||
"ButtonDelete": "Выдаліць",
|
||||
"ButtonDownloadQueue": "Чарга",
|
||||
"ButtonEdit": "Рэдагаваць",
|
||||
"ButtonEditChapters": "Рэдагаваць раздзелы",
|
||||
"ButtonEditPodcast": "Рэдагаваць падкаст",
|
||||
"ButtonEnable": "Уключыць",
|
||||
"ButtonFireAndFail": "Агонь і няўдача",
|
||||
"ButtonFireOnTest": "Тэст на вогнеўстойлівасць",
|
||||
"ButtonForceReScan": "Прымусовае паўторнае сканаванне",
|
||||
"ButtonFullPath": "Поўны шлях",
|
||||
"ButtonHide": "Схаваць",
|
||||
"ButtonIssues": "Праблемы",
|
||||
"ButtonJumpBackward": "Перайсці назад",
|
||||
"ButtonJumpForward": "Перайсці наперад",
|
||||
"ButtonLibrary": "Бібліятэка",
|
||||
"ButtonLogout": "Выйсці",
|
||||
"ButtonLookup": "",
|
||||
"ButtonMapChapterTitles": "Супаставіць назвы раздзелаў",
|
||||
"ButtonMatchAllAuthors": "Супадзенне ўсіх аўтараў",
|
||||
"ButtonNevermind": "Няважна",
|
||||
"ButtonNext": "Далей",
|
||||
"ButtonNextChapter": "Наступны раздзел",
|
||||
"ButtonNextItemInQueue": "Наступны элемент у чарзе",
|
||||
"ButtonOk": "Добра",
|
||||
"ButtonOpenFeed": "Адкрыць стужку",
|
||||
"ButtonOpenManager": "Адкрыць менеджар",
|
||||
"ButtonPause": "Паўза",
|
||||
"ButtonPlay": "Прайграць",
|
||||
"ButtonPlayAll": "Прайграць усё",
|
||||
"ButtonPlaying": "Прайграваецца",
|
||||
"ButtonPlaylists": "Плэйлісты",
|
||||
"ButtonPrevious": "Папярэдні",
|
||||
"ButtonPreviousChapter": "Папярэдні раздзел",
|
||||
"ButtonProbeAudioFile": "Праверыць аўдыяфайл",
|
||||
"ButtonPurgeAllCache": "Ачысціць увесь кэш",
|
||||
"ButtonPurgeItemsCache": "Ачысціць кэш элементаў",
|
||||
"ButtonQueueAddItem": "Дадаць у чаргу",
|
||||
"ButtonQueueRemoveItem": "Выдаліць з чаргі",
|
||||
"ButtonQuickEmbed": "Хуткае ўбудаванне",
|
||||
"ButtonQuickEmbedMetadata": "Хуткае ўбудаванне метаданых",
|
||||
"ButtonQuickMatch": "Хуткі пошук",
|
||||
"ButtonReScan": "Паўторнае сканаванне",
|
||||
"ButtonRead": "Чытаць",
|
||||
"ButtonRefresh": "Абнавіць",
|
||||
"ButtonRemove": "Выдаліць",
|
||||
"ButtonRemoveAll": "Выдаліць усе",
|
||||
"ButtonRemoveAllLibraryItems": "Выдаліць усе элементы бібліятэкі",
|
||||
"ButtonReset": "Скінуць",
|
||||
"ButtonResetToDefault": "Скінуць па змаўчанні",
|
||||
"ButtonRestore": "Аднавіць",
|
||||
"ButtonSave": "Захаваць",
|
||||
"ButtonSaveAndClose": "Захаваць і зачыніць",
|
||||
"ButtonSaveTracklist": "Захаваць спіс трэкаў",
|
||||
"ButtonScan": "Сканаваць",
|
||||
"ButtonScanLibrary": "Сканіраваць бібліятэку",
|
||||
"ButtonScrollLeft": "Пракруціць улева",
|
||||
"ButtonScrollRight": "Пракруціць направа",
|
||||
"ButtonSearch": "Пошук",
|
||||
"ButtonSelectFolderPath": "Выбраць шлях да тэчкі",
|
||||
"ButtonSeries": "Серыі",
|
||||
"ButtonSetChaptersFromTracks": "Усталяваць раздзелы з трэкаў",
|
||||
"ButtonShare": "Падзяліцца",
|
||||
"ButtonStartM4BEncode": "Пачаць кадзіраванне ў M4B",
|
||||
"ButtonStartMetadataEmbed": "Пачаць убудаванне метаданых",
|
||||
"ButtonStats": "Статыстыка",
|
||||
"ButtonSubmit": "Адправіць",
|
||||
"ButtonTest": "Тэст",
|
||||
"ButtonUnlinkOpenId": "Адвязаць OpenID",
|
||||
"ButtonUpload": "Загрузіць",
|
||||
"ButtonUploadBackup": "Загрузіць рэзервовую копію",
|
||||
"ButtonUploadCover": "Загрузіць вокладку",
|
||||
"ButtonUploadOPMLFile": "Загрузіць OPML файл",
|
||||
"ButtonUserDelete": "Выдаліць карыстальніка {0}",
|
||||
"ButtonUserEdit": "Рэдагаваць карыстальніка {0}",
|
||||
"ButtonViewAll": "Прагледзець усе",
|
||||
"ButtonYes": "Так",
|
||||
"HeaderAccount": "Уліковы запіс",
|
||||
"HeaderAddCustomMetadataProvider": "Дадаць карыстальніцкага пастаўшчыка метаданных",
|
||||
"HeaderAppriseNotificationSettings": "Налады апавяшчэнняў Apprise",
|
||||
"HeaderAudiobookTools": "Сродкі кіравання файламі аўдыякніг",
|
||||
"HeaderAuthentication": "Аўтэнтыфікацыя",
|
||||
"HeaderBackups": "Рэзервовыя копіі",
|
||||
"HeaderChangePassword": "Змяніць пароль",
|
||||
"HeaderChapters": "Раздзелы",
|
||||
"HeaderChooseAFolder": "Выбраць тэчку",
|
||||
"HeaderCollection": "Калекцыя",
|
||||
"HeaderCollectionItems": "Элементы калекцыі",
|
||||
"HeaderCover": "Вокладка",
|
||||
"HeaderCurrentDownloads": "Бягучыя загрузкі",
|
||||
"HeaderCustomMessageOnLogin": "Карыстальніцкае паведамленне пры ўваходзе"
|
||||
}
|
||||
|
||||
@@ -725,7 +725,6 @@
|
||||
"ToastBookmarkCreateFailed": "Неуспешно създаване на отметка",
|
||||
"ToastBookmarkCreateSuccess": "Отметката е създадена",
|
||||
"ToastBookmarkRemoveSuccess": "Отметката е премахната",
|
||||
"ToastBookmarkUpdateSuccess": "Отметката е обновена",
|
||||
"ToastChaptersHaveErrors": "Главите имат грешки",
|
||||
"ToastChaptersMustHaveTitles": "Главите трябва да имат заглавия",
|
||||
"ToastCollectionRemoveSuccess": "Колекцията е премахната",
|
||||
|
||||
@@ -952,7 +952,6 @@
|
||||
"ToastBookmarkCreateFailed": "বুকমার্ক তৈরি করতে ব্যর্থ",
|
||||
"ToastBookmarkCreateSuccess": "বুকমার্ক যোগ করা হয়েছে",
|
||||
"ToastBookmarkRemoveSuccess": "বুকমার্ক সরানো হয়েছে",
|
||||
"ToastBookmarkUpdateSuccess": "বুকমার্ক আপডেট করা হয়েছে",
|
||||
"ToastCachePurgeFailed": "ক্যাশে পরিষ্কার করতে ব্যর্থ হয়েছে",
|
||||
"ToastCachePurgeSuccess": "ক্যাশে সফলভাবে পরিষ্কার করা হয়েছে",
|
||||
"ToastChaptersHaveErrors": "অধ্যায়ে ত্রুটি আছে",
|
||||
|
||||
@@ -88,6 +88,8 @@
|
||||
"ButtonSaveTracklist": "Desa Pistes",
|
||||
"ButtonScan": "Escaneja",
|
||||
"ButtonScanLibrary": "Escaneja Biblioteca",
|
||||
"ButtonScrollLeft": "Mou a l'esquerra",
|
||||
"ButtonScrollRight": "Mou a la dreta",
|
||||
"ButtonSearch": "Cerca",
|
||||
"ButtonSelectFolderPath": "Selecciona Ruta de Carpeta",
|
||||
"ButtonSeries": "Sèries",
|
||||
@@ -896,7 +898,6 @@
|
||||
"ToastBookmarkCreateFailed": "Error en crear marcador",
|
||||
"ToastBookmarkCreateSuccess": "Marcador afegit",
|
||||
"ToastBookmarkRemoveSuccess": "Marcador eliminat",
|
||||
"ToastBookmarkUpdateSuccess": "Marcador actualitzat",
|
||||
"ToastCachePurgeFailed": "Error en purgar la memòria cau",
|
||||
"ToastCachePurgeSuccess": "Memòria cau purgada amb èxit",
|
||||
"ToastChaptersHaveErrors": "Els capítols tenen errors",
|
||||
|
||||
+12
-2
@@ -572,7 +572,7 @@
|
||||
"LabelSettingsLibraryMarkAsFinishedWhen": "Označit položku médií jako dokončenou, když",
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Přeskočit předchozí knihy v pokračování série",
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Polička Pokračovat v sérii na domovské stránce zobrazuje první nezačatou knihu v sériích, které mají alespoň jednu knihu dokončenou a žádnou rozečtenou. Povolením tohoto nastavení budou série pokračovat od poslední dokončené knihy namísto první nezačaté knihy.",
|
||||
"LabelSettingsParseSubtitles": "Analzyovat podtitul",
|
||||
"LabelSettingsParseSubtitles": "Analyzovat podtitul",
|
||||
"LabelSettingsParseSubtitlesHelp": "Rozparsovat podtitul z názvů složek audioknih.<br>Podtiul musí být oddělen znakem \" - \"<br>tj. \"Název knihy - Zde Podtitul\" má podtitul \"Zde podtitul\"",
|
||||
"LabelSettingsPreferMatchedMetadata": "Preferovat spárovaná metadata",
|
||||
"LabelSettingsPreferMatchedMetadataHelp": "Spárovaná data budou mít při použití funkce Rychlé párování přednost před údaji o položce. Ve výchozím nastavení funkce Rychlé párování pouze doplní chybějící údaje.",
|
||||
@@ -756,6 +756,7 @@
|
||||
"MessageConfirmResetProgress": "Opravdu chcete zahodit svůj pokrok?",
|
||||
"MessageConfirmSendEbookToDevice": "Opravdu chcete odeslat e-knihu {0} {1}\" do zařízení \"{2}\"?",
|
||||
"MessageConfirmUnlinkOpenId": "Opravdu chcete odpojit tohoto uživatele z OpenID?",
|
||||
"MessageDaysListenedInTheLastYear": "{0} poslechových dní v minulém roce",
|
||||
"MessageDownloadingEpisode": "Stahuji epizodu",
|
||||
"MessageDragFilesIntoTrackOrder": "Přetáhněte soubory do správného pořadí stop",
|
||||
"MessageEmbedFailed": "Vložení selhalo!",
|
||||
@@ -866,6 +867,8 @@
|
||||
"MessageTaskOpmlImportFeedPodcastExists": "Podcast se stejnou cestou již existuje",
|
||||
"MessageTaskOpmlImportFeedPodcastFailed": "Vytváření podcastu selhalo",
|
||||
"MessageTaskOpmlImportFinished": "Přidáno {0} podcastů",
|
||||
"MessageTaskOpmlParseFailed": "Selhalo parsování OPML souboru",
|
||||
"MessageTaskOpmlParseFastFail": "Neplatný OPML soubor <opml> tag nenalezen NEBO <outline> tag nenalezen",
|
||||
"MessageTaskScanItemsAdded": "{0} přidáno",
|
||||
"MessageTaskScanItemsMissing": "{0} chybí",
|
||||
"MessageTaskScanItemsUpdated": "{0} aktualizováno",
|
||||
@@ -890,6 +893,10 @@
|
||||
"NoteUploaderFoldersWithMediaFiles": "Se složkami s multimediálními soubory bude zacházeno jako se samostatnými položkami knihovny.",
|
||||
"NoteUploaderOnlyAudioFiles": "Pokud nahráváte pouze zvukové soubory, bude s každým zvukovým souborem zacházeno jako se samostatnou audioknihou.",
|
||||
"NoteUploaderUnsupportedFiles": "Nepodporované soubory jsou ignorovány. Při výběru nebo přetažení složky jsou ostatní soubory, které nejsou ve složce položek, ignorovány.",
|
||||
"NotificationOnBackupCompletedDescription": "Spuštěno po dokončení zálohování",
|
||||
"NotificationOnBackupFailedDescription": "Spuštěno pokud zálohování selže",
|
||||
"NotificationOnEpisodeDownloadedDescription": "Spuštěno při automatickém stažení epizody podcastu",
|
||||
"NotificationOnTestDescription": "Akce pro otestování upozorňovacího systému",
|
||||
"PlaceholderNewCollection": "Nový název kolekce",
|
||||
"PlaceholderNewFolderPath": "Nová cesta ke složce",
|
||||
"PlaceholderNewPlaylist": "Nový název seznamu přehrávání",
|
||||
@@ -900,6 +907,7 @@
|
||||
"StatsBooksAdditional": "Některé další zahrnují…",
|
||||
"StatsBooksFinished": "dokončené knihy",
|
||||
"StatsBooksFinishedThisYear": "Některé knihy dokončené tento rok…",
|
||||
"StatsBooksListenedTo": "knih poslechnuto",
|
||||
"StatsCollectionGrewTo": "Vaše kolekce knih se rozrostla na…",
|
||||
"StatsSessions": "sezení",
|
||||
"StatsSpentListening": "stráveno posloucháním",
|
||||
@@ -908,10 +916,13 @@
|
||||
"StatsTopGenre": "TOP ŽÁNR",
|
||||
"StatsTopGenres": "TOP ŽÁNRY",
|
||||
"StatsTopMonth": "TOP MĚSÍC",
|
||||
"StatsTopNarrator": "NEJLEPŠÍ VYPRAVĚČ",
|
||||
"StatsTopNarrators": "NEJLEPŠÍ VYPRAVĚČI",
|
||||
"StatsTotalDuration": "S celkovou dobou…",
|
||||
"StatsYearInReview": "ROK V PŘEHLEDU",
|
||||
"ToastAccountUpdateSuccess": "Účet aktualizován",
|
||||
"ToastAppriseUrlRequired": "Je nutné zadat Apprise URL",
|
||||
"ToastAsinRequired": "ASIN vyžadován",
|
||||
"ToastAuthorImageRemoveSuccess": "Obrázek autora odstraněn",
|
||||
"ToastAuthorNotFound": "Author \"{0}\" nenalezen",
|
||||
"ToastAuthorRemoveSuccess": "Autor odstraněn",
|
||||
@@ -936,7 +947,6 @@
|
||||
"ToastBookmarkCreateFailed": "Vytvoření záložky se nezdařilo",
|
||||
"ToastBookmarkCreateSuccess": "Přidána záložka",
|
||||
"ToastBookmarkRemoveSuccess": "Záložka odstraněna",
|
||||
"ToastBookmarkUpdateSuccess": "Záložka aktualizována",
|
||||
"ToastCachePurgeFailed": "Nepodařilo se vyčistit mezipaměť",
|
||||
"ToastCachePurgeSuccess": "Vyrovnávací paměť úspěšně vyčištěna",
|
||||
"ToastChaptersHaveErrors": "Kapitoly obsahují chyby",
|
||||
|
||||
@@ -636,7 +636,6 @@
|
||||
"ToastBookmarkCreateFailed": "Mislykkedes oprettelse af bogmærke",
|
||||
"ToastBookmarkCreateSuccess": "Bogmærke tilføjet",
|
||||
"ToastBookmarkRemoveSuccess": "Bogmærke fjernet",
|
||||
"ToastBookmarkUpdateSuccess": "Bogmærke opdateret",
|
||||
"ToastChaptersHaveErrors": "Kapitler har fejl",
|
||||
"ToastChaptersMustHaveTitles": "Kapitler skal have titler",
|
||||
"ToastCollectionRemoveSuccess": "Samling fjernet",
|
||||
|
||||
@@ -300,6 +300,7 @@
|
||||
"LabelDiscover": "Entdecken",
|
||||
"LabelDownload": "Herunterladen",
|
||||
"LabelDownloadNEpisodes": "Download {0} Episoden",
|
||||
"LabelDownloadable": "Herunterladbar",
|
||||
"LabelDuration": "Laufzeit",
|
||||
"LabelDurationComparisonExactMatch": "(genauer Treffer)",
|
||||
"LabelDurationComparisonLonger": "({0} länger)",
|
||||
@@ -588,6 +589,7 @@
|
||||
"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",
|
||||
"LabelShare": "Freigeben",
|
||||
"LabelShareDownloadableHelp": "Erlaubt es einem Nutzer, mit dem Link, die Dateien des Mediums als ZIP herunterzuladen.",
|
||||
"LabelShareOpen": "Freigeben",
|
||||
"LabelShareURL": "Freigabe URL",
|
||||
"LabelShowAll": "Alles anzeigen",
|
||||
@@ -756,6 +758,7 @@
|
||||
"MessageConfirmResetProgress": "Möchtest du Ihren Fortschritt wirklich zurücksetzen?",
|
||||
"MessageConfirmSendEbookToDevice": "{0} E-Buch „{1}“ wird auf das Gerät „{2}“ gesendet! Bist du dir sicher?",
|
||||
"MessageConfirmUnlinkOpenId": "Möchtest du die Verknüpfung dieses Benutzers mit OpenID wirklich löschen?",
|
||||
"MessageDaysListenedInTheLastYear": "{0} Tage in dem letzten Jahr gehört",
|
||||
"MessageDownloadingEpisode": "Episode wird heruntergeladen",
|
||||
"MessageDragFilesIntoTrackOrder": "Verschiebe die Dateien in die richtige Reihenfolge",
|
||||
"MessageEmbedFailed": "Einbetten fehlgeschlagen!",
|
||||
@@ -950,7 +953,6 @@
|
||||
"ToastBookmarkCreateFailed": "Lesezeichen konnte nicht erstellt werden",
|
||||
"ToastBookmarkCreateSuccess": "Lesezeichen hinzugefügt",
|
||||
"ToastBookmarkRemoveSuccess": "Lesezeichen entfernt",
|
||||
"ToastBookmarkUpdateSuccess": "Lesezeichen aktualisiert",
|
||||
"ToastCachePurgeFailed": "Cache leeren fehlgeschlagen",
|
||||
"ToastCachePurgeSuccess": "Cache geleert",
|
||||
"ToastChaptersHaveErrors": "Kapitel sind fehlerhaft",
|
||||
@@ -961,6 +963,7 @@
|
||||
"ToastCollectionRemoveSuccess": "Sammlung entfernt",
|
||||
"ToastCollectionUpdateSuccess": "Sammlung aktualisiert",
|
||||
"ToastCoverUpdateFailed": "Cover-Update fehlgeschlagen",
|
||||
"ToastDateTimeInvalidOrIncomplete": "Datum und Zeit ist ungültig oder unvollständig",
|
||||
"ToastDeleteFileFailed": "Die Datei konnte nicht gelöscht werden",
|
||||
"ToastDeleteFileSuccess": "Datei gelöscht",
|
||||
"ToastDeviceAddFailed": "Gerät konnte nicht hinzugefügt werden",
|
||||
|
||||
@@ -837,6 +837,7 @@
|
||||
"MessageResetChaptersConfirm": "Are you sure you want to reset chapters and undo the changes you made?",
|
||||
"MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on",
|
||||
"MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.",
|
||||
"MessageScheduleLibraryScanNote": "For most users, it is recommended to leave this feature disabled and keep the folder watcher setting enabled. The folder watcher will automatically detect changes in your library folders. The folder watcher doesn't work for every file system (like NFS) so scheduled library scans can be used instead.",
|
||||
"MessageSearchResultsFor": "Search results for",
|
||||
"MessageSelected": "{0} selected",
|
||||
"MessageServerCouldNotBeReached": "Server could not be reached",
|
||||
@@ -953,7 +954,6 @@
|
||||
"ToastBookmarkCreateFailed": "Failed to create bookmark",
|
||||
"ToastBookmarkCreateSuccess": "Bookmark added",
|
||||
"ToastBookmarkRemoveSuccess": "Bookmark removed",
|
||||
"ToastBookmarkUpdateSuccess": "Bookmark updated",
|
||||
"ToastCachePurgeFailed": "Failed to purge cache",
|
||||
"ToastCachePurgeSuccess": "Cache purged successfully",
|
||||
"ToastChaptersHaveErrors": "Chapters have errors",
|
||||
@@ -964,6 +964,7 @@
|
||||
"ToastCollectionRemoveSuccess": "Collection removed",
|
||||
"ToastCollectionUpdateSuccess": "Collection updated",
|
||||
"ToastCoverUpdateFailed": "Cover update failed",
|
||||
"ToastDateTimeInvalidOrIncomplete": "Date and time is invalid or incomplete",
|
||||
"ToastDeleteFileFailed": "Failed to delete file",
|
||||
"ToastDeleteFileSuccess": "File deleted",
|
||||
"ToastDeviceAddFailed": "Failed to add device",
|
||||
@@ -1016,6 +1017,7 @@
|
||||
"ToastNewUserTagError": "Must select at least one tag",
|
||||
"ToastNewUserUsernameError": "Enter a username",
|
||||
"ToastNoNewEpisodesFound": "No new episodes found",
|
||||
"ToastNoRSSFeed": "Podcast does not have an RSS Feed",
|
||||
"ToastNoUpdatesNecessary": "No updates necessary",
|
||||
"ToastNotificationCreateFailed": "Failed to create notification",
|
||||
"ToastNotificationDeleteFailed": "Failed to delete notification",
|
||||
|
||||
@@ -300,6 +300,7 @@
|
||||
"LabelDiscover": "Descubrir",
|
||||
"LabelDownload": "Descargar",
|
||||
"LabelDownloadNEpisodes": "Descargar {0} episodios",
|
||||
"LabelDownloadable": "Descarregable",
|
||||
"LabelDuration": "Duración",
|
||||
"LabelDurationComparisonExactMatch": "(coincidencia exacta)",
|
||||
"LabelDurationComparisonLonger": "({0} más largo)",
|
||||
@@ -588,6 +589,7 @@
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "Por defecto, los archivos de metadatos se almacenan en /metadata/items. Si habilita esta opción, los archivos de metadatos se guardarán en la carpeta de elementos de su biblioteca",
|
||||
"LabelSettingsTimeFormat": "Formato de Tiempo",
|
||||
"LabelShare": "Compartir",
|
||||
"LabelShareDownloadableHelp": "Permet als usuaris amb l'enllaç compartit descarregar un arxiu zip amb l'item de la llibreria.",
|
||||
"LabelShareOpen": "abrir un recurso compartido",
|
||||
"LabelShareURL": "Compartir la URL",
|
||||
"LabelShowAll": "Mostrar Todos",
|
||||
@@ -756,6 +758,7 @@
|
||||
"MessageConfirmResetProgress": "¿Estás seguro de que quieres reiniciar tu progreso?",
|
||||
"MessageConfirmSendEbookToDevice": "¿Está seguro de que enviar {0} ebook(s) \"{1}\" al dispositivo \"{2}\"?",
|
||||
"MessageConfirmUnlinkOpenId": "¿Estás seguro de que deseas desvincular este usuario de OpenID?",
|
||||
"MessageDaysListenedInTheLastYear": "{0} dies escoltats en l'últim any",
|
||||
"MessageDownloadingEpisode": "Descargando Capitulo",
|
||||
"MessageDragFilesIntoTrackOrder": "Arrastra los archivos al orden correcto de las pistas",
|
||||
"MessageEmbedFailed": "¡Error al insertar!",
|
||||
@@ -950,7 +953,6 @@
|
||||
"ToastBookmarkCreateFailed": "Error al crear marcador",
|
||||
"ToastBookmarkCreateSuccess": "Marcador Agregado",
|
||||
"ToastBookmarkRemoveSuccess": "Marcador eliminado",
|
||||
"ToastBookmarkUpdateSuccess": "Marcador actualizado",
|
||||
"ToastCachePurgeFailed": "Error al purgar el caché",
|
||||
"ToastCachePurgeSuccess": "Caché purgado de manera exitosa",
|
||||
"ToastChaptersHaveErrors": "Los capítulos tienen errores",
|
||||
@@ -997,7 +999,7 @@
|
||||
"ToastLibraryScanFailedToStart": "Error al iniciar el escaneo",
|
||||
"ToastLibraryScanStarted": "Se inició el escaneo de la biblioteca",
|
||||
"ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" actualizada",
|
||||
"ToastMatchAllAuthorsFailed": "No coincide con todos los autores",
|
||||
"ToastMatchAllAuthorsFailed": "No se pudo encontrar a todos los autores",
|
||||
"ToastMetadataFilesRemovedError": "Error al eliminar metadatos de {0} archivo(s)",
|
||||
"ToastMetadataFilesRemovedNoneFound": "No hay metadatos.{0} archivo(s) encontrado(s) en la biblioteca",
|
||||
"ToastMetadataFilesRemovedNoneRemoved": "Sin metadatos.{0} archivo(s) eliminado(s)",
|
||||
|
||||
@@ -709,7 +709,6 @@
|
||||
"ToastBookmarkCreateFailed": "Järjehoidja loomine ebaõnnestus",
|
||||
"ToastBookmarkCreateSuccess": "Järjehoidja lisatud",
|
||||
"ToastBookmarkRemoveSuccess": "Järjehoidja eemaldatud",
|
||||
"ToastBookmarkUpdateSuccess": "Järjehoidja värskendatud",
|
||||
"ToastChaptersHaveErrors": "Peatükkidel on vigu",
|
||||
"ToastChaptersMustHaveTitles": "Peatükkidel peab olema pealkiri",
|
||||
"ToastCollectionRemoveSuccess": "Kogum eemaldatud",
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"ButtonPurgeItemsCache": "Tyhjennä kohteiden välimuisti",
|
||||
"ButtonQueueAddItem": "Lisää jonoon",
|
||||
"ButtonQueueRemoveItem": "Poista jonosta",
|
||||
"ButtonQuickEmbed": "Pikaupota",
|
||||
"ButtonQuickMatch": "Pikatäsmää",
|
||||
"ButtonReScan": "Uudelleenskannaa",
|
||||
"ButtonRead": "Lue",
|
||||
@@ -85,6 +86,8 @@
|
||||
"ButtonSaveTracklist": "Tallenna raitalista",
|
||||
"ButtonScan": "Skannaa",
|
||||
"ButtonScanLibrary": "Skannaa kirjasto",
|
||||
"ButtonScrollLeft": "Vieritä vasemmalle",
|
||||
"ButtonScrollRight": "Vieritä oikealle",
|
||||
"ButtonSearch": "Etsi",
|
||||
"ButtonSelectFolderPath": "Valitse kansiopolku",
|
||||
"ButtonSeries": "Sarjat",
|
||||
@@ -148,6 +151,7 @@
|
||||
"HeaderLogs": "Lokit",
|
||||
"HeaderManageGenres": "Hallitse lajityyppejä",
|
||||
"HeaderManageTags": "Hallitse tageja",
|
||||
"HeaderMetadataOrderOfPrecedence": "Metadatan tärkeysjärjestys",
|
||||
"HeaderMetadataToEmbed": "Sisällytettävä metadata",
|
||||
"HeaderNewAccount": "Uusi tili",
|
||||
"HeaderNewLibrary": "Uusi kirjasto",
|
||||
@@ -156,6 +160,7 @@
|
||||
"HeaderNotifications": "Ilmoitukset",
|
||||
"HeaderOpenRSSFeed": "Avaa RSS-syöte",
|
||||
"HeaderOtherFiles": "Muut tiedostot",
|
||||
"HeaderPasswordAuthentication": "Salasanan todentaminen",
|
||||
"HeaderPermissions": "Käyttöoikeudet",
|
||||
"HeaderPlayerQueue": "Soittimen jono",
|
||||
"HeaderPlayerSettings": "Soittimen asetukset",
|
||||
@@ -169,22 +174,28 @@
|
||||
"HeaderRemoveEpisode": "Poista jakso",
|
||||
"HeaderRemoveEpisodes": "Poista {0} jaksoa",
|
||||
"HeaderSchedule": "Ajoita",
|
||||
"HeaderScheduleEpisodeDownloads": "Ajoita automaattiset jaksolataukset",
|
||||
"HeaderScheduleLibraryScans": "Ajoita automaattiset kirjastoskannaukset",
|
||||
"HeaderSession": "Istunto",
|
||||
"HeaderSetBackupSchedule": "Aseta varmuuskopiointiaikataulu",
|
||||
"HeaderSettings": "Asetukset",
|
||||
"HeaderSettingsDisplay": "Näyttö",
|
||||
"HeaderSettingsExperimental": "Kokeelliset ominaisuudet",
|
||||
"HeaderSettingsGeneral": "Yleiset",
|
||||
"HeaderSettingsScanner": "Skannaaja",
|
||||
"HeaderSleepTimer": "Uniajastin",
|
||||
"HeaderStatsMinutesListeningChart": "Kuunteluminuutit (viim. 7 pv)",
|
||||
"HeaderStatsRecentSessions": "Viimeaikaiset istunnot",
|
||||
"HeaderStatsTop10Authors": "Top 10 kirjailijat",
|
||||
"HeaderStatsTop5Genres": "Top 5 lajityypit",
|
||||
"HeaderTableOfContents": "Sisällysluettelo",
|
||||
"HeaderTools": "Työkalut",
|
||||
"HeaderUpdateAccount": "Päivitä tili",
|
||||
"HeaderUpdateAuthor": "Päivitä kirjailija",
|
||||
"HeaderUpdateDetails": "Päivitä yksityiskohdat",
|
||||
"HeaderUpdateLibrary": "Päivitä kirjasto",
|
||||
"HeaderUsers": "Käyttäjät",
|
||||
"HeaderYearReview": "Vuosi {0} tarkasteltuna",
|
||||
"HeaderYourStats": "Tilastosi",
|
||||
"LabelAbridged": "Lyhennetty",
|
||||
"LabelAccountType": "Tilin tyyppi",
|
||||
@@ -204,11 +215,15 @@
|
||||
"LabelAllUsersExcludingGuests": "Kaikki käyttäjät vieraita lukuun ottamatta",
|
||||
"LabelAllUsersIncludingGuests": "Kaikki käyttäjät mukaan lukien vieraat",
|
||||
"LabelAlreadyInYourLibrary": "Jo kirjastossasi",
|
||||
"LabelAudioBitrate": "Äänen bittinopeus (esim. 128k)",
|
||||
"LabelAudioChannels": "Äänikanavat (1 tai 2)",
|
||||
"LabelAudioCodec": "Äänikoodekki",
|
||||
"LabelAuthor": "Tekijä",
|
||||
"LabelAuthorFirstLast": "Tekijä (Etunimi Sukunimi)",
|
||||
"LabelAuthorLastFirst": "Tekijä (Sukunimi, Etunimi)",
|
||||
"LabelAuthors": "Tekijät",
|
||||
"LabelAutoDownloadEpisodes": "Lataa jaksot automaattisesti",
|
||||
"LabelAutoFetchMetadata": "Etsi metadata automaattisesti",
|
||||
"LabelBackToUser": "Takaisin käyttäjään",
|
||||
"LabelBackupLocation": "Varmuuskopiointipaikka",
|
||||
"LabelBackupsEnableAutomaticBackups": "Ota automaattinen varmuuskopiointi käyttöön",
|
||||
@@ -238,24 +253,41 @@
|
||||
"LabelCurrent": "Nykyinen",
|
||||
"LabelDays": "Päivää",
|
||||
"LabelDescription": "Kuvaus",
|
||||
"LabelDeselectAll": "Poista valinta kaikista",
|
||||
"LabelDevice": "Laite",
|
||||
"LabelDeviceInfo": "Laitteen tiedot",
|
||||
"LabelDeviceIsAvailableTo": "Laite on saatavilla...",
|
||||
"LabelDirectory": "Kansio",
|
||||
"LabelDiscover": "Löydä",
|
||||
"LabelDownload": "Lataa",
|
||||
"LabelDownloadNEpisodes": "Lataa {0} jaksoa",
|
||||
"LabelDownloadable": "Ladattavissa",
|
||||
"LabelDuration": "Kesto",
|
||||
"LabelDurationComparisonLonger": "({0} pidempi)",
|
||||
"LabelDurationComparisonShorter": "({0} lyhyempi)",
|
||||
"LabelDurationFound": "Kesto löydetty:",
|
||||
"LabelEbook": "E-kirja",
|
||||
"LabelEbooks": "E-kirjat",
|
||||
"LabelEdit": "Muokkaa",
|
||||
"LabelEmail": "Sähköposti",
|
||||
"LabelEmailSettingsFromAddress": "Osoitteesta",
|
||||
"LabelEmailSettingsRejectUnauthorizedHelp": "SSL-sertifikaatin varmentamisen käytöstä poistaminen saattaa vaarantaa yhteytesti turvallisuusriskeihin, kuten man-in-the-middle hyökkäyksiin. Poista käytöstä vain jos ymmärrät vaaran ja luotat yhdistämääsi sähköpostipalvelimeen.",
|
||||
"LabelEmailSettingsSecure": "Turvallinen",
|
||||
"LabelEmailSettingsTestAddress": "Testiosoite",
|
||||
"LabelEmbeddedCover": "Upotettu kansikuva",
|
||||
"LabelEnable": "Ota käyttöön",
|
||||
"LabelEncodingBackupLocation": "Alkuperäisistä audiotiedostoistasi tallennetaan varmuuskopio osoitteessa:",
|
||||
"LabelEncodingStartedNavigation": "Voit poistua sivulta kun tehtävä on aloitettu.",
|
||||
"LabelEncodingTimeWarning": "Koodaus saattaa kestää 30 minuuttiin asti.",
|
||||
"LabelEncodingWarningAdvancedSettings": "Varoitus: Älä päivitä näitä asetuksia ellet ymmärrä ffmpeg-koodausasetuksia.",
|
||||
"LabelEnd": "Loppu",
|
||||
"LabelEndOfChapter": "Luvun loppu",
|
||||
"LabelEpisode": "Jakso",
|
||||
"LabelEpisodeNotLinkedToRssFeed": "Jakso ei yhdistetty RSS-syötteeseen",
|
||||
"LabelEpisodeNumber": "Jakso #{0}",
|
||||
"LabelEpisodeTitle": "Jakson nimi",
|
||||
"LabelEpisodeType": "Jakson tyyppi",
|
||||
"LabelEpisodeUrlFromRssFeed": "Jakson URL RSS-syötteestä",
|
||||
"LabelEpisodes": "Jaksot",
|
||||
"LabelExample": "Esimerkki",
|
||||
"LabelFeedURL": "Syötteen URL",
|
||||
@@ -287,12 +319,20 @@
|
||||
"LabelLanguageDefaultServer": "Palvelimen oletuskieli",
|
||||
"LabelLanguages": "Kielet",
|
||||
"LabelLastBookAdded": "Viimeisin lisätty kirja",
|
||||
"LabelLastBookUpdated": "Viimeisin päivitetty kirja",
|
||||
"LabelLastUpdate": "Viimeisin päivitys",
|
||||
"LabelLibrariesAccessibleToUser": "Käyttäjälle saatavilla olevat kirjastot",
|
||||
"LabelLibrary": "Kirjasto",
|
||||
"LabelLibraryName": "Kirjaston nimi",
|
||||
"LabelLineSpacing": "Riviväli",
|
||||
"LabelListenAgain": "Kuuntele uudelleen",
|
||||
"LabelLookForNewEpisodesAfterDate": "Etsi uusia jaksoja tämän päivämäärän jälkeen",
|
||||
"LabelMaxEpisodesToDownload": "Jaksojen maksimilatausmäärä. 0 poistaa rajoituksen.",
|
||||
"LabelMediaPlayer": "Mediasoitin",
|
||||
"LabelMediaType": "Mediatyyppi",
|
||||
"LabelMinute": "Minuutti",
|
||||
"LabelMinutes": "Minuutit",
|
||||
"LabelMissingEbook": "Ei e-kirjaa",
|
||||
"LabelMore": "Lisää",
|
||||
"LabelMoreInfo": "Lisätietoja",
|
||||
"LabelName": "Nimi",
|
||||
@@ -302,6 +342,7 @@
|
||||
"LabelNewPassword": "Uusi salasana",
|
||||
"LabelNewestAuthors": "Uusimmat kirjailijat",
|
||||
"LabelNewestEpisodes": "Uusimmat jaksot",
|
||||
"LabelNextBackupDate": "Seuraava varmuuskopiointipäivämäärä",
|
||||
"LabelNotStarted": "Ei aloitettu",
|
||||
"LabelPassword": "Salasana",
|
||||
"LabelPath": "Polku",
|
||||
|
||||
@@ -944,7 +944,6 @@
|
||||
"ToastBookmarkCreateFailed": "Échec de la création de signet",
|
||||
"ToastBookmarkCreateSuccess": "Signet ajouté",
|
||||
"ToastBookmarkRemoveSuccess": "Signet supprimé",
|
||||
"ToastBookmarkUpdateSuccess": "Signet mis à jour",
|
||||
"ToastCachePurgeFailed": "Échec de la purge du cache",
|
||||
"ToastCachePurgeSuccess": "Cache purgé avec succès",
|
||||
"ToastChaptersHaveErrors": "Les chapitres contiennent des erreurs",
|
||||
|
||||
@@ -740,7 +740,6 @@
|
||||
"ToastBookmarkCreateFailed": "יצירת סימניה נכשלה",
|
||||
"ToastBookmarkCreateSuccess": "הסימניה נוספה בהצלחה",
|
||||
"ToastBookmarkRemoveSuccess": "הסימניה הוסרה בהצלחה",
|
||||
"ToastBookmarkUpdateSuccess": "הסימניה עודכנה בהצלחה",
|
||||
"ToastChaptersHaveErrors": "פרקים מכילים שגיאות",
|
||||
"ToastChaptersMustHaveTitles": "פרקים חייבים לכלול כותרות",
|
||||
"ToastCollectionRemoveSuccess": "האוסף הוסר בהצלחה",
|
||||
|
||||
@@ -300,6 +300,7 @@
|
||||
"LabelDiscover": "Otkrij",
|
||||
"LabelDownload": "Preuzmi",
|
||||
"LabelDownloadNEpisodes": "Preuzmi {0} nastavak/a",
|
||||
"LabelDownloadable": "Moguće preuzimanje",
|
||||
"LabelDuration": "Trajanje",
|
||||
"LabelDurationComparisonExactMatch": "(točno podudaranje)",
|
||||
"LabelDurationComparisonLonger": "({0} duže)",
|
||||
@@ -588,6 +589,7 @@
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "Meta-podatci se obično spremaju u /metadata/items; ako uključite ovu postavku meta-podatci će se čuvati u mapama knjižničkih stavki",
|
||||
"LabelSettingsTimeFormat": "Format vremena",
|
||||
"LabelShare": "Podijeli",
|
||||
"LabelShareDownloadableHelp": "Korisnicima s poveznicom za dijeljenje omogućuje preuzimanje stavke.",
|
||||
"LabelShareOpen": "Dijeljenje otvoreno",
|
||||
"LabelShareURL": "URL za dijeljenje",
|
||||
"LabelShowAll": "Prikaži sve",
|
||||
@@ -756,6 +758,7 @@
|
||||
"MessageConfirmResetProgress": "Sigurno želite resetirati napredak?",
|
||||
"MessageConfirmSendEbookToDevice": "Sigurno želite poslati {0} e-knjiga/u \"{1}\" na uređaj \"{2}\"?",
|
||||
"MessageConfirmUnlinkOpenId": "Sigurno želite odspojiti ovog korisnika s OpenID-ja?",
|
||||
"MessageDaysListenedInTheLastYear": "{0} dana slušanja u posljednjih godinu dana",
|
||||
"MessageDownloadingEpisode": "Preuzimam nastavak",
|
||||
"MessageDragFilesIntoTrackOrder": "Prevlačenjem datoteka složite pravilan redoslijed",
|
||||
"MessageEmbedFailed": "Ugrađivanje nije uspjelo!",
|
||||
@@ -950,7 +953,6 @@
|
||||
"ToastBookmarkCreateFailed": "Izrada knjižne oznake nije uspjela",
|
||||
"ToastBookmarkCreateSuccess": "Knjižna oznaka dodana",
|
||||
"ToastBookmarkRemoveSuccess": "Knjižna oznaka uklonjena",
|
||||
"ToastBookmarkUpdateSuccess": "Knjižna oznaka ažurirana",
|
||||
"ToastCachePurgeFailed": "Čišćenje predmemorije nije uspjelo",
|
||||
"ToastCachePurgeSuccess": "Predmemorija uspješno očišćena",
|
||||
"ToastChaptersHaveErrors": "Poglavlja imaju pogreške",
|
||||
|
||||
+47
-15
@@ -100,7 +100,7 @@
|
||||
"ButtonStartM4BEncode": "M4B kódolás indítása",
|
||||
"ButtonStartMetadataEmbed": "Metaadatok beágyazásának indítása",
|
||||
"ButtonStats": "Statisztikák",
|
||||
"ButtonSubmit": "Beküldés",
|
||||
"ButtonSubmit": "Küldés",
|
||||
"ButtonTest": "Teszt",
|
||||
"ButtonUnlinkOpenId": "OpenID szétkapcsolása",
|
||||
"ButtonUpload": "Feltöltés",
|
||||
@@ -143,7 +143,7 @@
|
||||
"HeaderFindChapters": "Fejezetek keresése",
|
||||
"HeaderIgnoredFiles": "Figyelmen kívül hagyott fájlok",
|
||||
"HeaderItemFiles": "Elemfájlok",
|
||||
"HeaderItemMetadataUtils": "Elem metaadat eszközök",
|
||||
"HeaderItemMetadataUtils": "Metaadatok eszközei",
|
||||
"HeaderLastListeningSession": "Utolsó hallgatási munkamenet",
|
||||
"HeaderLatestEpisodes": "Legújabb epizódok",
|
||||
"HeaderLibraries": "Könyvtárak",
|
||||
@@ -165,6 +165,7 @@
|
||||
"HeaderNotificationUpdate": "Értesítés frissítése",
|
||||
"HeaderNotifications": "Értesítések",
|
||||
"HeaderOpenIDConnectAuthentication": "OpenID Connect hitelesítés",
|
||||
"HeaderOpenListeningSessions": "Hallgatási menetek megnyitása",
|
||||
"HeaderOpenRSSFeed": "RSS hírcsatorna megnyitása",
|
||||
"HeaderOtherFiles": "Egyéb fájlok",
|
||||
"HeaderPasswordAuthentication": "Jelszó hitelesítés",
|
||||
@@ -194,7 +195,7 @@
|
||||
"HeaderSettingsWebClient": "Webkliens",
|
||||
"HeaderSleepTimer": "Alvásidőzítő",
|
||||
"HeaderStatsLargestItems": "Legnagyobb elemek",
|
||||
"HeaderStatsLongestItems": "Leghosszabb elemek (órákban)",
|
||||
"HeaderStatsLongestItems": "Leghosszabb elemek (órában)",
|
||||
"HeaderStatsMinutesListeningChart": "Hallgatási grafikon percekben (az elmúlt 7 napból)",
|
||||
"HeaderStatsRecentSessions": "Legutóbbi munkamenetek",
|
||||
"HeaderStatsTop10Authors": "Top 10 szerző",
|
||||
@@ -206,7 +207,7 @@
|
||||
"HeaderUpdateDetails": "Részletek frissítése",
|
||||
"HeaderUpdateLibrary": "Könyvtár frissítése",
|
||||
"HeaderUsers": "Felhasználók",
|
||||
"HeaderYearReview": "{0} év visszatekintése",
|
||||
"HeaderYearReview": "Visszatekintés {0} -ra/re",
|
||||
"HeaderYourStats": "Saját statisztikák",
|
||||
"LabelAbridged": "Tömörített",
|
||||
"LabelAbridgedChecked": "Rövidített (ellenőrizve)",
|
||||
@@ -237,7 +238,7 @@
|
||||
"LabelAuthor": "Szerző",
|
||||
"LabelAuthorFirstLast": "Szerző (Keresztnév Vezetéknév)",
|
||||
"LabelAuthorLastFirst": "Szerző (Vezetéknév, Keresztnév)",
|
||||
"LabelAuthors": "Szerzők",
|
||||
"LabelAuthors": "Szerző",
|
||||
"LabelAutoDownloadEpisodes": "Epizódok automatikus letöltése",
|
||||
"LabelAutoFetchMetadata": "Metaadatok automatikus lekérése",
|
||||
"LabelAutoFetchMetadataHelp": "Cím, szerző és sorozat metaadatok automatikus lekérése a feltöltés megkönnyítése érdekében. További metaadatok egyeztetése szükséges lehet a feltöltés után.",
|
||||
@@ -272,7 +273,7 @@
|
||||
"LabelCollapseSeries": "Sorozat összecsukása",
|
||||
"LabelCollapseSubSeries": "Alszéria összecsukása",
|
||||
"LabelCollection": "Gyűjtemény",
|
||||
"LabelCollections": "Gyűjtemények",
|
||||
"LabelCollections": "Gyűjtemény",
|
||||
"LabelComplete": "Kész",
|
||||
"LabelConfirmPassword": "Jelszó megerősítése",
|
||||
"LabelContinueListening": "Hallgatás folytatása",
|
||||
@@ -299,6 +300,7 @@
|
||||
"LabelDiscover": "Felfedezés",
|
||||
"LabelDownload": "Letöltés",
|
||||
"LabelDownloadNEpisodes": "{0} epizód letöltése",
|
||||
"LabelDownloadable": "Letölthető",
|
||||
"LabelDuration": "Időtartam",
|
||||
"LabelDurationComparisonExactMatch": "(pontos egyezés)",
|
||||
"LabelDurationComparisonLonger": "({0}-val hosszabb)",
|
||||
@@ -320,6 +322,7 @@
|
||||
"LabelEncodingChaptersNotEmbedded": "A fejezetek nincsenek beágyazva a többsávos hangoskönyvekbe.",
|
||||
"LabelEncodingClearItemCache": "Győződjön meg róla, hogy rendszeresen tisztítja az elemek gyorsítótárát.",
|
||||
"LabelEncodingFinishedM4B": "A kész M4B a hangoskönyv mappádba kerül:",
|
||||
"LabelEncodingInfoEmbedded": "A metaadatok beépülnek a hangsávokba a hangoskönyv mappáján belül.",
|
||||
"LabelEncodingStartedNavigation": "Ha a feladat elindult, el lehet navigálni erről az oldalról.",
|
||||
"LabelEncodingTimeWarning": "A kódolás akár 30 percet is igénybe vehet.",
|
||||
"LabelEncodingWarningAdvancedSettings": "Figyelmeztetés: Ne frissítse ezeket a beállításokat, hacsak nem ismeri az ffmpeg kódolási beállításait.",
|
||||
@@ -441,7 +444,7 @@
|
||||
"LabelNarrators": "Előadók",
|
||||
"LabelNew": "Új",
|
||||
"LabelNewPassword": "Új jelszó",
|
||||
"LabelNewestAuthors": "Legújabb szerzők",
|
||||
"LabelNewestAuthors": "A legújabb szerzők",
|
||||
"LabelNewestEpisodes": "Legújabb epizódok",
|
||||
"LabelNextBackupDate": "Következő biztonsági másolat dátuma",
|
||||
"LabelNextScheduledRun": "Következő ütemezett futtatás",
|
||||
@@ -478,7 +481,7 @@
|
||||
"LabelPermissionsDownload": "Letölthet",
|
||||
"LabelPermissionsUpdate": "Frissíthet",
|
||||
"LabelPermissionsUpload": "Feltölthet",
|
||||
"LabelPersonalYearReview": "Az évvisszatekintésed ({0})",
|
||||
"LabelPersonalYearReview": "Az éved összefoglalása ({0})",
|
||||
"LabelPhotoPathURL": "Fénykép útvonal/URL",
|
||||
"LabelPlayMethod": "Lejátszási módszer",
|
||||
"LabelPlayerChapterNumberMarker": "{0} a {1} -ből",
|
||||
@@ -535,11 +538,12 @@
|
||||
"LabelSelectUsers": "Felhasználók kiválasztása",
|
||||
"LabelSendEbookToDevice": "E-könyv küldése...",
|
||||
"LabelSequence": "Sorozat",
|
||||
"LabelSerial": "Sorozat",
|
||||
"LabelSeries": "Sorozat",
|
||||
"LabelSeriesName": "Sorozat neve",
|
||||
"LabelSeriesProgress": "Sorozat haladása",
|
||||
"LabelServerLogLevel": "Kiszolgáló naplózási szint",
|
||||
"LabelServerYearReview": "Szerver évvisszatekintés ({0})",
|
||||
"LabelServerYearReview": "Szerver éves visszatekintése ({0})",
|
||||
"LabelSetEbookAsPrimary": "Beállítás elsődlegesként",
|
||||
"LabelSetEbookAsSupplementary": "Beállítás kiegészítőként",
|
||||
"LabelSettingsAllowIframe": "A beágyazás engedélyezése egy iframe-be",
|
||||
@@ -585,7 +589,11 @@
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "Alapértelmezés szerint a metaadatfájlok a /metadata/items mappában vannak tárolva, ennek a beállításnak az engedélyezése a metaadatfájlokat a könyvtári elem mappáiban tárolja",
|
||||
"LabelSettingsTimeFormat": "Időformátum",
|
||||
"LabelShare": "Megosztás",
|
||||
"LabelShareDownloadableHelp": "Lehetővé teszi a megosztási linket birtokló felhasználók számára, hogy letöltsék a könyvtári elem zip-fájlját.",
|
||||
"LabelShareOpen": "Megosztás megnyitása",
|
||||
"LabelShareURL": "URL megosztása",
|
||||
"LabelShowAll": "Mindent mutat",
|
||||
"LabelShowSeconds": "Másodperc megjelenítése",
|
||||
"LabelShowSubtitles": "Felirat megjelenítése",
|
||||
"LabelSize": "Méret",
|
||||
"LabelSleepTimer": "Alvásidőzítő",
|
||||
@@ -596,8 +604,8 @@
|
||||
"LabelStartTime": "Kezdési idő",
|
||||
"LabelStarted": "Elkezdődött",
|
||||
"LabelStartedAt": "Kezdés ideje",
|
||||
"LabelStatsAudioTracks": "Audiósávok",
|
||||
"LabelStatsAuthors": "Szerzők",
|
||||
"LabelStatsAudioTracks": "Audiósáv",
|
||||
"LabelStatsAuthors": "Szerző",
|
||||
"LabelStatsBestDay": "Legjobb nap",
|
||||
"LabelStatsDailyAverage": "Napi átlag",
|
||||
"LabelStatsDays": "Napok",
|
||||
@@ -605,7 +613,7 @@
|
||||
"LabelStatsHours": "Órák",
|
||||
"LabelStatsInARow": "egymás után",
|
||||
"LabelStatsItemsFinished": "Befejezett elem",
|
||||
"LabelStatsItemsInLibrary": "Elemek a könyvtárban",
|
||||
"LabelStatsItemsInLibrary": "Elem a könyvtárban",
|
||||
"LabelStatsMinutes": "perc",
|
||||
"LabelStatsMinutesListening": "Hallgatási perc",
|
||||
"LabelStatsOverallDays": "Összes nap",
|
||||
@@ -684,8 +692,8 @@
|
||||
"LabelWeekdaysToRun": "Futás napjai",
|
||||
"LabelXBooks": "{0} könyv",
|
||||
"LabelXItems": "{0} elem",
|
||||
"LabelYearReviewHide": "Az évvisszatekintés elrejtése",
|
||||
"LabelYearReviewShow": "Évvisszatekintés megtekintése",
|
||||
"LabelYearReviewHide": "Visszatekintés az évre elrejtése",
|
||||
"LabelYearReviewShow": "Visszatekintés az évre megtekintése",
|
||||
"LabelYourAudiobookDuration": "Hangoskönyv időtartama",
|
||||
"LabelYourBookmarks": "Könyvjelzőid",
|
||||
"LabelYourPlaylists": "Lejátszási listáid",
|
||||
@@ -750,10 +758,12 @@
|
||||
"MessageConfirmResetProgress": "Biztos, hogy vissza akarja állítani a haladási folyamatát?",
|
||||
"MessageConfirmSendEbookToDevice": "Biztosan el szeretné küldeni a(z) {0} e-könyvet a(z) \"{1}\" eszközre?",
|
||||
"MessageConfirmUnlinkOpenId": "Biztos, hogy el akarja távolítani ezt a felhasználót az OpenID-ból?",
|
||||
"MessageDaysListenedInTheLastYear": "{0} napot hallgatott az elmúlt évben",
|
||||
"MessageDownloadingEpisode": "Epizód letöltése",
|
||||
"MessageDragFilesIntoTrackOrder": "Húzza a fájlokat a helyes sávrendbe",
|
||||
"MessageEmbedFailed": "A beágyazás sikertelen!",
|
||||
"MessageEmbedFinished": "Beágyazás befejeződött!",
|
||||
"MessageEmbedQueue": "Metaadatok beágyazására várakozik ({0} a sorban)",
|
||||
"MessageEpisodesQueuedForDownload": "{0} epizód letöltésre vár",
|
||||
"MessageEreaderDevices": "Az e-könyvek kézbesítésének biztosítása érdekében a fenti e-mail címet az alább felsorolt minden egyes eszközhöz, mint érvényes feladót kell hozzáadnia.",
|
||||
"MessageFeedURLWillBe": "A hírcsatorna URL-je {0} lesz",
|
||||
@@ -816,6 +826,7 @@
|
||||
"MessagePodcastHasNoRSSFeedForMatching": "A podcastnak nincs RSS hírcsatorna URL-je az egyeztetéshez",
|
||||
"MessagePodcastSearchField": "Adja meg a keresési kifejezést vagy az RSS hírcsatorna URL-címét",
|
||||
"MessageQuickEmbedInProgress": "Gyors beágyazás folyamatban",
|
||||
"MessageQuickEmbedQueue": "Gyors beágyazásra várakozik ({0} a sorban)",
|
||||
"MessageQuickMatchAllEpisodes": "Minden epizód gyors egyeztetése",
|
||||
"MessageQuickMatchDescription": "Üres elem részletek és borító feltöltése az első találati eredménnyel a(z) '{0}'-ból. Nem írja felül a részleteket, kivéve, ha a 'Preferált egyeztetett metaadatok' szerverbeállítás engedélyezve van.",
|
||||
"MessageRemoveChapter": "Fejezet eltávolítása",
|
||||
@@ -832,6 +843,7 @@
|
||||
"MessageSetChaptersFromTracksDescription": "Fejezetek beállítása minden egyes hangfájlt egy fejezetként használva, és a fejezet címét a hangfájl neveként",
|
||||
"MessageShareExpirationWillBe": "A lejárat: <strong>{0}</strong>",
|
||||
"MessageShareExpiresIn": "{0} múlva jár le",
|
||||
"MessageShareURLWillBe": "A megosztási URL <strong>{0}</strong> lesz",
|
||||
"MessageStartPlaybackAtTime": "\"{0}\" lejátszásának kezdése {1} -tól?",
|
||||
"MessageTaskAudioFileNotWritable": "A/Az „{0}” hangfájl nem írható",
|
||||
"MessageTaskCanceledByUser": "Felhasználó törölte a feladatot",
|
||||
@@ -937,7 +949,6 @@
|
||||
"ToastBookmarkCreateFailed": "Könyvjelző létrehozása sikertelen",
|
||||
"ToastBookmarkCreateSuccess": "Könyvjelző hozzáadva",
|
||||
"ToastBookmarkRemoveSuccess": "Könyvjelző eltávolítva",
|
||||
"ToastBookmarkUpdateSuccess": "Könyvjelző frissítve",
|
||||
"ToastCachePurgeFailed": "A gyorsítótár törlése sikertelen",
|
||||
"ToastCachePurgeSuccess": "A gyorsítótár sikeresen törölve",
|
||||
"ToastChaptersHaveErrors": "A fejezetek hibákat tartalmaznak",
|
||||
@@ -954,9 +965,11 @@
|
||||
"ToastDeviceTestEmailFailed": "Teszt email küldése sikertelen",
|
||||
"ToastDeviceTestEmailSuccess": "Teszt email elküldve",
|
||||
"ToastEmailSettingsUpdateSuccess": "Email beállítások frissítve",
|
||||
"ToastEncodeCancelFailed": "A kódolás törlése sikertelen volt",
|
||||
"ToastEncodeCancelSucces": "Kódolás törölve",
|
||||
"ToastEpisodeDownloadQueueClearFailed": "Nem sikerült törölni a várólistát",
|
||||
"ToastEpisodeUpdateSuccess": "{0} epizód frissítve",
|
||||
"ToastErrorCannotShare": "Ezen az eszközön nem lehet natívan megosztani",
|
||||
"ToastFailedToLoadData": "Sikertelen adatbetöltés",
|
||||
"ToastFailedToMatch": "Nem sikerült egyezőséget találni",
|
||||
"ToastFailedToShare": "Nem sikerült megosztani",
|
||||
@@ -992,10 +1005,14 @@
|
||||
"ToastNewUserCreatedSuccess": "Új fiók létrehozva",
|
||||
"ToastNewUserLibraryError": "Legalább egy könyvtárat ki kell választani",
|
||||
"ToastNewUserPasswordError": "Kötelező a jelszó, csak a root felhasználónak lehet üres jelszava",
|
||||
"ToastNewUserTagError": "Legalább egy címkét ki kell választania",
|
||||
"ToastNewUserUsernameError": "Adjon meg egy felhasználónevet",
|
||||
"ToastNoNewEpisodesFound": "Nincs új epizód",
|
||||
"ToastNoUpdatesNecessary": "Nincs szükség frissítésre",
|
||||
"ToastNotificationCreateFailed": "Értesítés létrehozása sikertelen",
|
||||
"ToastNotificationDeleteFailed": "Értesítés törlése sikertelen",
|
||||
"ToastNotificationSettingsUpdateSuccess": "Értesítési beállítások frissítve",
|
||||
"ToastNotificationTestTriggerFailed": "Nem sikerült a tesztértesítést elindítani",
|
||||
"ToastNotificationUpdateSuccess": "Értesítés frissítve",
|
||||
"ToastPlaylistCreateFailed": "Lejátszási lista létrehozása sikertelen",
|
||||
"ToastPlaylistCreateSuccess": "Lejátszási lista létrehozva",
|
||||
@@ -1005,22 +1022,37 @@
|
||||
"ToastPodcastCreateSuccess": "A podcast sikeresen létrehozva",
|
||||
"ToastPodcastNoEpisodesInFeed": "Nincsenek epizódok az RSS hírcsatornában",
|
||||
"ToastPodcastNoRssFeed": "A podcastnak nincs RSS-hírcsatornája",
|
||||
"ToastProgressIsNotBeingSynced": "Az előrehaladás nem szinkronizálódik, a lejátszás újraindul",
|
||||
"ToastProviderCreatedFailed": "Hiba a szolgáltató hozzáadásakor",
|
||||
"ToastProviderCreatedSuccess": "Új szolgáltató hozzáadva",
|
||||
"ToastProviderNameAndUrlRequired": "Név és Url kötelező",
|
||||
"ToastProviderRemoveSuccess": "Szolgáltató eltávolítva",
|
||||
"ToastRSSFeedCloseFailed": "Az RSS hírcsatorna bezárása sikertelen",
|
||||
"ToastRSSFeedCloseSuccess": "RSS hírfolyam leállítva",
|
||||
"ToastRemoveFailed": "Sikertelen eltávolítás",
|
||||
"ToastRemoveItemFromCollectionFailed": "Tétel eltávolítása a gyűjteményből sikertelen",
|
||||
"ToastRemoveItemFromCollectionSuccess": "Tétel eltávolítva a gyűjteményből",
|
||||
"ToastRenameFailed": "Sikertelen átnevezés",
|
||||
"ToastSelectAtLeastOneUser": "Válasszon legalább egy felhasználót",
|
||||
"ToastSendEbookToDeviceFailed": "E-könyv küldése az eszközre sikertelen",
|
||||
"ToastSendEbookToDeviceSuccess": "E-könyv elküldve az eszközre \"{0}\"",
|
||||
"ToastSeriesUpdateFailed": "Sorozat frissítése sikertelen",
|
||||
"ToastSeriesUpdateSuccess": "Sorozat frissítése sikeres",
|
||||
"ToastServerSettingsUpdateSuccess": "Szerver beállítások frissítve",
|
||||
"ToastSessionCloseFailed": "A munkamenet bezárása sikertelen",
|
||||
"ToastSessionDeleteFailed": "Munkamenet törlése sikertelen",
|
||||
"ToastSessionDeleteSuccess": "Munkamenet törölve",
|
||||
"ToastSleepTimerDone": "Alvásidőzítő kész... zZzzZZz",
|
||||
"ToastSocketConnected": "Socket csatlakoztatva",
|
||||
"ToastSocketDisconnected": "Socket lecsatlakoztatva",
|
||||
"ToastSocketFailedToConnect": "A Socket csatlakoztatása sikertelen",
|
||||
"ToastSortingPrefixesEmptyError": "Legalább 1 rendezési előtaggal kell rendelkeznie",
|
||||
"ToastSortingPrefixesUpdateSuccess": "Rendezési előtagok frissítése ({0} elem)",
|
||||
"ToastTitleRequired": "A cím kötelező",
|
||||
"ToastUnknownError": "Ismeretlen hiba",
|
||||
"ToastUserDeleteFailed": "Felhasználó törlése sikertelen",
|
||||
"ToastUserDeleteSuccess": "Felhasználó törölve",
|
||||
"ToastUserPasswordChangeSuccess": "Jelszó sikeresen megváltoztatva",
|
||||
"ToastUserPasswordMustChange": "Az új jelszó nem egyezik a régi jelszóval",
|
||||
"ToastUserRootRequireName": "Egy root felhasználónevet kell megadnia"
|
||||
}
|
||||
|
||||
@@ -941,7 +941,6 @@
|
||||
"ToastBookmarkCreateFailed": "Creazione segnalibro fallita",
|
||||
"ToastBookmarkCreateSuccess": "Segnalibro creato",
|
||||
"ToastBookmarkRemoveSuccess": "Segnalibro Rimosso",
|
||||
"ToastBookmarkUpdateSuccess": "Segnalibro aggiornato",
|
||||
"ToastCachePurgeFailed": "Impossibile eliminare la cache",
|
||||
"ToastCachePurgeSuccess": "Cache eliminata correttamente",
|
||||
"ToastChaptersHaveErrors": "I capitoli contengono errori",
|
||||
|
||||
@@ -660,7 +660,6 @@
|
||||
"ToastBookmarkCreateFailed": "Žymos sukurti nepavyko",
|
||||
"ToastBookmarkCreateSuccess": "Žyma pridėta",
|
||||
"ToastBookmarkRemoveSuccess": "Žyma pašalinta",
|
||||
"ToastBookmarkUpdateSuccess": "Žyma atnaujinta",
|
||||
"ToastChaptersHaveErrors": "Skyriai turi klaidų",
|
||||
"ToastChaptersMustHaveTitles": "Skyriai turi turėti pavadinimus",
|
||||
"ToastChaptersRemoved": "Skyriai pašalinti",
|
||||
|
||||
@@ -937,7 +937,6 @@
|
||||
"ToastBookmarkCreateFailed": "Aanmaken boekwijzer mislukt",
|
||||
"ToastBookmarkCreateSuccess": "boekwijzer toegevoegd",
|
||||
"ToastBookmarkRemoveSuccess": "Boekwijzer verwijderd",
|
||||
"ToastBookmarkUpdateSuccess": "Boekwijzer bijgewerkt",
|
||||
"ToastCachePurgeFailed": "Cache wissen is mislukt",
|
||||
"ToastCachePurgeSuccess": "Cache succesvol verwijderd",
|
||||
"ToastChaptersHaveErrors": "Hoofdstukken bevatten fouten",
|
||||
|
||||
+43
-13
@@ -12,7 +12,7 @@
|
||||
"ButtonBack": "Tilbake",
|
||||
"ButtonBrowseForFolder": "Bla gjennom mappe",
|
||||
"ButtonCancel": "Avbryt",
|
||||
"ButtonCancelEncode": "Avbryt Encode",
|
||||
"ButtonCancelEncode": "Avbryt konvertering",
|
||||
"ButtonChangeRootPassword": "Bytt Root-bruker passord",
|
||||
"ButtonCheckAndDownloadNewEpisodes": "Sjekk og last ned nye episoder",
|
||||
"ButtonChooseAFolder": "Velg mappe",
|
||||
@@ -97,10 +97,10 @@
|
||||
"ButtonShare": "Del",
|
||||
"ButtonShiftTimes": "Forskyv tider",
|
||||
"ButtonShow": "Vis",
|
||||
"ButtonStartM4BEncode": "Start M4B Koding",
|
||||
"ButtonStartM4BEncode": "Start konvertering til M4B",
|
||||
"ButtonStartMetadataEmbed": "Start Metadata innbaking",
|
||||
"ButtonStats": "Statistikk",
|
||||
"ButtonSubmit": "Send inn",
|
||||
"ButtonSubmit": "Lagre",
|
||||
"ButtonTest": "Test",
|
||||
"ButtonUnlinkOpenId": "Koble fra OpenID",
|
||||
"ButtonUpload": "Last opp",
|
||||
@@ -143,12 +143,12 @@
|
||||
"HeaderFindChapters": "Finn Kapittel",
|
||||
"HeaderIgnoredFiles": "Ignorerte filer",
|
||||
"HeaderItemFiles": "Elementfiler",
|
||||
"HeaderItemMetadataUtils": "Enhet Metadata verktøy",
|
||||
"HeaderItemMetadataUtils": "Element Metadata verktøy",
|
||||
"HeaderLastListeningSession": "Siste lyttesesjon",
|
||||
"HeaderLatestEpisodes": "Siste episoder",
|
||||
"HeaderLibraries": "Biblioteker",
|
||||
"HeaderLibraryFiles": "Bibliotek filer",
|
||||
"HeaderLibraryStats": "Bibliotek statistikk",
|
||||
"HeaderLibraryStats": "Bibliotekstatistikk",
|
||||
"HeaderListeningSessions": "Lyttesesjoner",
|
||||
"HeaderListeningStats": "Lyttestatistikk",
|
||||
"HeaderLogin": "Logg inn",
|
||||
@@ -300,6 +300,7 @@
|
||||
"LabelDiscover": "Oppdag",
|
||||
"LabelDownload": "Last ned",
|
||||
"LabelDownloadNEpisodes": "Last ned {0} episoder",
|
||||
"LabelDownloadable": "Nedlastbar",
|
||||
"LabelDuration": "Varighet",
|
||||
"LabelDurationComparisonExactMatch": "(nøyaktig treff)",
|
||||
"LabelDurationComparisonLonger": "({0} lenger)",
|
||||
@@ -365,11 +366,11 @@
|
||||
"LabelFormat": "Format",
|
||||
"LabelFull": "Full",
|
||||
"LabelGenre": "Sjanger",
|
||||
"LabelGenres": "Sjangers",
|
||||
"LabelGenres": "Sjangre",
|
||||
"LabelHardDeleteFile": "Tving sletting av fil",
|
||||
"LabelHasEbook": "Har e-bok",
|
||||
"LabelHasSupplementaryEbook": "Har komplimentær e-bok",
|
||||
"LabelHideSubtitles": "Skjul undertekster",
|
||||
"LabelHideSubtitles": "Skjul undertitler",
|
||||
"LabelHighestPriority": "Høyeste prioritet",
|
||||
"LabelHost": "Tjener",
|
||||
"LabelHour": "Time",
|
||||
@@ -406,7 +407,7 @@
|
||||
"LabelLess": "Mindre",
|
||||
"LabelLibrariesAccessibleToUser": "Biblioteker tilgjengelig for bruker",
|
||||
"LabelLibrary": "Bibliotek",
|
||||
"LabelLibraryFilterSublistEmpty": "",
|
||||
"LabelLibraryFilterSublistEmpty": "Ingen {0}",
|
||||
"LabelLibraryItem": "Bibliotek enhet",
|
||||
"LabelLibraryName": "Bibliotek navn",
|
||||
"LabelLimit": "Begrensning",
|
||||
@@ -570,7 +571,7 @@
|
||||
"LabelSettingsLibraryMarkAsFinishedWhen": "Marker som ferdig når",
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Hopp over tidligere bøker i \"Fortsett serien\"",
|
||||
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "\"Fortsett serie\"-siden viser første bok som ikke er påbegynt i serier der en bok er lest og ingen bøker leses nå. Ved å slå på denne innstillingen så vil man fortsette på serien etter siste leste bok, fremfor første bok som ikke er startet på i en serie.",
|
||||
"LabelSettingsParseSubtitles": "Analyser undertekster",
|
||||
"LabelSettingsParseSubtitles": "Analyser undertitler",
|
||||
"LabelSettingsParseSubtitlesHelp": "Hent undertittel fra lydbokens mappenavn.<br>Undertittel må være separert med \" - \"<br>f.eks. \"Boktittel - En lengre tittel\" har undertittel \"En lengre tittel\".",
|
||||
"LabelSettingsPreferMatchedMetadata": "Foretrekk funnet metadata",
|
||||
"LabelSettingsPreferMatchedMetadataHelp": "Funnet data vil overskrive enhetens detaljene når man bruker Kjapt søk. Som standard vil Kjapt søk kun fylle inn manglende detaljer.",
|
||||
@@ -586,6 +587,7 @@
|
||||
"LabelSettingsStoreMetadataWithItemHelp": "Som standard vil metadata bli lagret under /metadata/items, aktiveres dette valget vil metadata bli lagret i samme mappe som gjenstanden",
|
||||
"LabelSettingsTimeFormat": "Tid format",
|
||||
"LabelShare": "Dele",
|
||||
"LabelShareDownloadableHelp": "Tillat brukere med en delt link å laste ned en zip-fil av elementet.",
|
||||
"LabelShareOpen": "Åpne deling",
|
||||
"LabelShareURL": "Dele URL",
|
||||
"LabelShowAll": "Vis alle",
|
||||
@@ -615,7 +617,7 @@
|
||||
"LabelStatsOverallDays": "Totale dager",
|
||||
"LabelStatsOverallHours": "Totale timer",
|
||||
"LabelStatsWeekListening": "Uker lyttet",
|
||||
"LabelSubtitle": "undertekster",
|
||||
"LabelSubtitle": "Undertittel",
|
||||
"LabelSupportedFileTypes": "Støttede filtyper",
|
||||
"LabelTag": "Tag",
|
||||
"LabelTags": "Tagger",
|
||||
@@ -640,11 +642,11 @@
|
||||
"LabelTimeRemaining": "{0} gjennstående",
|
||||
"LabelTimeToShift": "Tid å forflytte i sekunder",
|
||||
"LabelTitle": "Tittel",
|
||||
"LabelToolsEmbedMetadata": "Bak inn metadata",
|
||||
"LabelToolsEmbedMetadata": "Bygg inn metadata",
|
||||
"LabelToolsEmbedMetadataDescription": "Bak inn metadata i lydfilen, inkludert omslagsbilde og kapitler.",
|
||||
"LabelToolsM4bEncoder": "M4B enkoder",
|
||||
"LabelToolsMakeM4b": "Lag M4B Lydbokfil",
|
||||
"LabelToolsMakeM4bDescription": "Lager en.M4B lydbokfil med innbakte omslagsbilde og kapitler.",
|
||||
"LabelToolsMakeM4bDescription": "Lager en M4B lydbokfil med innbakt omslagsbilde og kapitler.",
|
||||
"LabelToolsSplitM4b": "Del M4B inn i MP3er",
|
||||
"LabelToolsSplitM4bDescription": "Lager MP3er fra en M4B inndelt i kapitler med innbakt metadata, omslagsbilde og kapitler.",
|
||||
"LabelTotalDuration": "Total lengde",
|
||||
@@ -754,6 +756,7 @@
|
||||
"MessageConfirmResetProgress": "Er du sikkert på at du vil tilbakestille fremgangen?",
|
||||
"MessageConfirmSendEbookToDevice": "Er du sikker på at du vil sende {0} ebok \"{1}\" til enhet \"{2}\"?",
|
||||
"MessageConfirmUnlinkOpenId": "Er du sikker på at du vil koble denne brukeren fra OpenID?",
|
||||
"MessageDaysListenedInTheLastYear": "{0} dager med lytting siste året",
|
||||
"MessageDownloadingEpisode": "Laster ned episode",
|
||||
"MessageDragFilesIntoTrackOrder": "Dra filene i rett spor rekkefølge",
|
||||
"MessageEmbedFailed": "Innbygging feilet!",
|
||||
@@ -771,6 +774,7 @@
|
||||
"MessageJoinUsOn": "Følg oss nå",
|
||||
"MessageLoading": "Laster...",
|
||||
"MessageLoadingFolders": "Laster mapper...",
|
||||
"MessageLogsDescription": "Logger lagres i <code>/metadata/logs</code> som JSON-filer. Krasjlogger lagres i <code>/metadata/logs/crash_logs.txt</code>.",
|
||||
"MessageM4BFailed": "M4B mislykkes!",
|
||||
"MessageM4BFinished": "M4B fullført!",
|
||||
"MessageMapChapterTitles": "Bruk kapittel titler fra din eksisterende lydbok kapitler uten å justere tidsstempel",
|
||||
@@ -787,6 +791,7 @@
|
||||
"MessageNoCollections": "Ingen samlinger",
|
||||
"MessageNoCoversFound": "Ingen omslagsbilde funnet",
|
||||
"MessageNoDescription": "Ingen beskrivelse",
|
||||
"MessageNoDevices": "Ingen enheter",
|
||||
"MessageNoDownloadsInProgress": "Ingen aktive nedlastinger",
|
||||
"MessageNoDownloadsQueued": "Ingen nedlastinger i kø",
|
||||
"MessageNoEpisodeMatchesFound": "Ingen lik episode funnet",
|
||||
@@ -800,6 +805,7 @@
|
||||
"MessageNoLogs": "Ingen logger",
|
||||
"MessageNoMediaProgress": "Ingen mediefremgang",
|
||||
"MessageNoNotifications": "Ingen varslinger",
|
||||
"MessageNoPodcastFeed": "Ugyldig podcast: Ingen feed",
|
||||
"MessageNoPodcastsFound": "Ingen podcaster funnet",
|
||||
"MessageNoResults": "Ingen resultat",
|
||||
"MessageNoSearchResultsFor": "Ingen søkeresultat for \"{0}\"",
|
||||
@@ -809,11 +815,17 @@
|
||||
"MessageNoUpdatesWereNecessary": "Ingen oppdatering var nødvendig",
|
||||
"MessageNoUserPlaylists": "Du har ingen spillelister",
|
||||
"MessageNotYetImplemented": "Ikke implementert ennå",
|
||||
"MessageOpmlPreviewNote": "PS: Dette er en forhåndvisning av en OPML-fil. Den faktiske podcast-tittelen hentes direkte fra RSS-feeden.",
|
||||
"MessageOr": "eller",
|
||||
"MessagePauseChapter": "Pause avspilling av kapittel",
|
||||
"MessagePlayChapter": "Lytter på begynnelsen av kapittel",
|
||||
"MessagePlaylistCreateFromCollection": "Lag spilleliste fra samling",
|
||||
"MessagePleaseWait": "Vennligst vent...",
|
||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast har ingen RSS feed url til bruk av sammenligning",
|
||||
"MessagePodcastSearchField": "Skriv inn søkeord eller RSS-feed URL",
|
||||
"MessageQuickEmbedInProgress": "Hurtiginnbygging pågår",
|
||||
"MessageQuickEmbedQueue": "Kø for hurtiginnbygging ({0} i kø)",
|
||||
"MessageQuickMatchAllEpisodes": "Kjapp matching av alle episoder",
|
||||
"MessageQuickMatchDescription": "Fyll inn tomme gjenstandsdetaljer og omslagsbilde med første resultat fra '{0}'. Overskriver ikke detaljene utenom 'Foretrekk funnet metadata' tjenerinstilling er aktivert.",
|
||||
"MessageRemoveChapter": "fjerne kapittel",
|
||||
"MessageRemoveEpisodes": "fjerne {0} kapitler",
|
||||
@@ -823,10 +835,29 @@
|
||||
"MessageResetChaptersConfirm": "Er du sikker på at du vil nullstille kapitler og angre endringene du har gjort?",
|
||||
"MessageRestoreBackupConfirm": "Er du sikker på at du vil gjenopprette sikkerhetskopien som var laget",
|
||||
"MessageRestoreBackupWarning": "gjenoppretting av sikkerhetskopi vil overskrive hele databasen under /config og omslagsbilde under /metadata/items og /metadata/authors.<br /><br />Sikkerhetskopier endrer ikke noen filer under dine bibliotekmapper. Hvis du har aktivert tjenerinstillingen for å lagre omslagsbilder og metadata i bibliotekmapper så vil ikke de filene bli tatt sikkerhetskopi eller overskrevet.<br /><br />Alle klientene som bruker din tjener vil bli fornyet automatisk.",
|
||||
"MessageScheduleLibraryScanNote": "For de fleste brukere er det anbefalt å la denne funksjonen være slått av, og la mappeovervåkeren stå på. Mappeovervåkeren oppdager automatisk endringer i biblioteksmappene. Mappeovervåkeren fungerer ikke med alle filsystemer (f.eks. NFS) og da kan planlagt skanning av bibliotekene brukes i steden for.",
|
||||
"MessageSearchResultsFor": "Søk resultat for",
|
||||
"MessageSelected": "{0} valgt",
|
||||
"MessageServerCouldNotBeReached": "Tjener kunne ikke bli nådd",
|
||||
"MessageSetChaptersFromTracksDescription": "Sett kapitler ved å bruke hver lydfil som kapittel og kapitteltittel som lydfilnavnet",
|
||||
"MessageShareExpirationWillBe": "Utløp vil være <strong>{0}</strong>",
|
||||
"MessageShareExpiresIn": "Utløper om {0}",
|
||||
"MessageShareURLWillBe": "URL for deling blir <strong>{0}</strong>",
|
||||
"MessageStartPlaybackAtTime": "Start avspilling av \"{0}\" ved {1}?",
|
||||
"MessageTaskAudioFileNotWritable": "Lydfilen \"{0}\" kan ikke skrives til",
|
||||
"MessageTaskCanceledByUser": "Oppgave kansellert av bruker",
|
||||
"MessageTaskDownloadingEpisodeDescription": "Laster ned episode \"{0}\"",
|
||||
"MessageTaskEmbeddingMetadata": "Bygger inn metadata",
|
||||
"MessageTaskEmbeddingMetadataDescription": "Bygger inn metadata i lydboken \"{0}\"",
|
||||
"MessageTaskEncodingM4b": "Konverterer til M4B",
|
||||
"MessageTaskEncodingM4bDescription": "Konverterer lydboken \"{0}\" til én M4B-fil",
|
||||
"MessageTaskFailed": "Feilet",
|
||||
"MessageTaskFailedToBackupAudioFile": "Feil ved sikkerhetskopiering av lydfilen \"{0}\"",
|
||||
"MessageTaskFailedToCreateCacheDirectory": "Kunne ikke opprette mappe for mellomlagring (cache)",
|
||||
"MessageTaskFailedToEmbedMetadataInFile": "Kunne ikke bygge inn metadata i filen \"{0}\"",
|
||||
"MessageTaskFailedToMergeAudioFiles": "Kunne ikke slå sammen lydfiler",
|
||||
"MessageTaskFailedToMoveM4bFile": "Kunne ikke flytte M4B-fil",
|
||||
"MessageTaskFailedToWriteMetadataFile": "Kunne ikke lagre metadata-fil",
|
||||
"MessageThinking": "Tenker...",
|
||||
"MessageUploaderItemFailed": "Opplastning mislykkes",
|
||||
"MessageUploaderItemSuccess": "Opplastning fullført!",
|
||||
@@ -873,7 +904,6 @@
|
||||
"ToastBookmarkCreateFailed": "Misslykkes å opprette bokmerke",
|
||||
"ToastBookmarkCreateSuccess": "Bokmerke lagt til",
|
||||
"ToastBookmarkRemoveSuccess": "Bokmerke fjernet",
|
||||
"ToastBookmarkUpdateSuccess": "Bokmerke oppdatert",
|
||||
"ToastCachePurgeFailed": "Kunne ikke å slette mellomlager",
|
||||
"ToastCachePurgeSuccess": "Mellomlager slettet",
|
||||
"ToastChaptersHaveErrors": "Kapittel har feil",
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
"ButtonEditChapters": "Edytuj rozdziały",
|
||||
"ButtonEditPodcast": "Edytuj podcast",
|
||||
"ButtonEnable": "Włącz",
|
||||
"ButtonFireAndFail": "Fail start",
|
||||
"ButtonForceReScan": "Wymuś ponowne skanowanie",
|
||||
"ButtonFullPath": "Pełna ścieżka",
|
||||
"ButtonHide": "Ukryj",
|
||||
@@ -770,7 +771,6 @@
|
||||
"ToastBookmarkCreateFailed": "Nie udało się utworzyć zakładki",
|
||||
"ToastBookmarkCreateSuccess": "Dodano zakładkę",
|
||||
"ToastBookmarkRemoveSuccess": "Zakładka została usunięta",
|
||||
"ToastBookmarkUpdateSuccess": "Zaktualizowano zakładkę",
|
||||
"ToastCollectionRemoveSuccess": "Kolekcja usunięta",
|
||||
"ToastCollectionUpdateSuccess": "Zaktualizowano kolekcję",
|
||||
"ToastItemCoverUpdateSuccess": "Zaktualizowano okładkę",
|
||||
|
||||
@@ -729,7 +729,6 @@
|
||||
"ToastBookmarkCreateFailed": "Falha ao criar marcador",
|
||||
"ToastBookmarkCreateSuccess": "Marcador adicionado",
|
||||
"ToastBookmarkRemoveSuccess": "Marcador removido",
|
||||
"ToastBookmarkUpdateSuccess": "Marcador atualizado",
|
||||
"ToastCachePurgeFailed": "Falha ao apagar o cache",
|
||||
"ToastCachePurgeSuccess": "Cache apagado com sucesso",
|
||||
"ToastChaptersHaveErrors": "Capítulos com erro",
|
||||
|
||||
@@ -348,7 +348,7 @@
|
||||
"LabelFetchingMetadata": "Извлечение метаданных",
|
||||
"LabelFile": "Файл",
|
||||
"LabelFileBirthtime": "Дата создания",
|
||||
"LabelFileBornDate": "Родился {0}",
|
||||
"LabelFileBornDate": "Создан {0}",
|
||||
"LabelFileModified": "Дата модификации",
|
||||
"LabelFileModifiedDate": "Изменено {0}",
|
||||
"LabelFilename": "Имя файла",
|
||||
@@ -758,6 +758,7 @@
|
||||
"MessageConfirmResetProgress": "Вы уверены, что хотите сбросить свой прогресс?",
|
||||
"MessageConfirmSendEbookToDevice": "Вы уверены, что хотите отправить {0} e-книгу \"{1}\" на устройство \"{2}\"?",
|
||||
"MessageConfirmUnlinkOpenId": "Вы уверены, что хотите отвязать этого пользователя от OpenID?",
|
||||
"MessageDaysListenedInTheLastYear": "{0} дней прослушивания за последний год",
|
||||
"MessageDownloadingEpisode": "Эпизод скачивается",
|
||||
"MessageDragFilesIntoTrackOrder": "Перетащите файлы для исправления порядка треков",
|
||||
"MessageEmbedFailed": "Вставка не удалась!",
|
||||
@@ -836,6 +837,7 @@
|
||||
"MessageResetChaptersConfirm": "Вы уверены, что хотите сбросить главы и отменить внесенные изменения?",
|
||||
"MessageRestoreBackupConfirm": "Вы уверены, что хотите восстановить резервную копию, созданную",
|
||||
"MessageRestoreBackupWarning": "Восстановление резервной копии перезапишет всю базу данных, расположенную в /config, и обложки изображений в /metadata/items и /metadata/authors.<br/><br/>Бэкапы не изменяют файлы в папках библиотеки. Если вы включили параметры сервера для хранения обложек и метаданных в папках библиотеки, то они не резервируются и не перезаписываются.<br/><br/>Все клиенты, использующие ваш сервер, будут автоматически обновлены.",
|
||||
"MessageScheduleLibraryScanNote": "Большинству пользователей рекомендуется отключить эту функцию и включить функцию просмотра папок. Программа просмотра папок автоматически обнаружит изменения в папках вашей библиотеки. Программа просмотра папок работает не для каждой файловой системы (например, NFS), поэтому вместо этого можно использовать запланированные проверки библиотеки.",
|
||||
"MessageSearchResultsFor": "Результаты поиска для",
|
||||
"MessageSelected": "{0} выбрано",
|
||||
"MessageServerCouldNotBeReached": "Не удалось связаться с сервером",
|
||||
@@ -952,7 +954,6 @@
|
||||
"ToastBookmarkCreateFailed": "Не удалось создать закладку",
|
||||
"ToastBookmarkCreateSuccess": "Добавлена закладка",
|
||||
"ToastBookmarkRemoveSuccess": "Закладка удалена",
|
||||
"ToastBookmarkUpdateSuccess": "Закладка обновлена",
|
||||
"ToastCachePurgeFailed": "Не удалось очистить кэш",
|
||||
"ToastCachePurgeSuccess": "Кэш успешно очищен",
|
||||
"ToastChaptersHaveErrors": "Главы имеют ошибки",
|
||||
@@ -963,6 +964,7 @@
|
||||
"ToastCollectionRemoveSuccess": "Коллекция удалена",
|
||||
"ToastCollectionUpdateSuccess": "Коллекция обновлена",
|
||||
"ToastCoverUpdateFailed": "Не удалось обновить обложку",
|
||||
"ToastDateTimeInvalidOrIncomplete": "Дата и время указаны неверно или не до конца",
|
||||
"ToastDeleteFileFailed": "Не удалось удалить файл",
|
||||
"ToastDeleteFileSuccess": "Файл удален",
|
||||
"ToastDeviceAddFailed": "Не удалось добавить устройство",
|
||||
@@ -1015,6 +1017,7 @@
|
||||
"ToastNewUserTagError": "Необходимо выбрать хотя бы один тег",
|
||||
"ToastNewUserUsernameError": "Введите имя пользователя",
|
||||
"ToastNoNewEpisodesFound": "Новых эпизодов не найдено",
|
||||
"ToastNoRSSFeed": "У подкаста нет RSS-канала",
|
||||
"ToastNoUpdatesNecessary": "Обновления не требуются",
|
||||
"ToastNotificationCreateFailed": "Не удалось создать уведомление",
|
||||
"ToastNotificationDeleteFailed": "Не удалось удалить уведомление",
|
||||
|
||||
@@ -758,6 +758,7 @@
|
||||
"MessageConfirmResetProgress": "Ali ste prepričani, da želite ponastaviti svoj napredek?",
|
||||
"MessageConfirmSendEbookToDevice": "Ali ste prepričani, da želite poslati {0} e-knjigo \"{1}\" v napravo \"{2}\"?",
|
||||
"MessageConfirmUnlinkOpenId": "Ali ste prepričani, da želite prekiniti povezavo tega uporabnika z OpenID?",
|
||||
"MessageDaysListenedInTheLastYear": "{0} dni poslušanja v zadnjem letu",
|
||||
"MessageDownloadingEpisode": "Prenašam epizodo",
|
||||
"MessageDragFilesIntoTrackOrder": "Povlecite datoteke v pravilen vrstni red posnetkov",
|
||||
"MessageEmbedFailed": "Vdelava ni uspela!",
|
||||
@@ -836,6 +837,7 @@
|
||||
"MessageResetChaptersConfirm": "Ali ste prepričani, da želite ponastaviti poglavja in razveljaviti spremembe, ki ste jih naredili?",
|
||||
"MessageRestoreBackupConfirm": "Ali ste prepričani, da želite obnoviti varnostno kopijo, ustvarjeno ob",
|
||||
"MessageRestoreBackupWarning": "Obnovitev varnostne kopije bo prepisala celotno zbirko podatkov, ki se nahaja v /config, in zajema slike v /metadata/items in /metadata/authors.<br /><br />Varnostne kopije ne spreminjajo nobenih datotek v mapah vaše knjižnice. Če ste omogočili nastavitve strežnika za shranjevanje naslovnic in metapodatkov v mapah vaše knjižnice, potem ti niso varnostno kopirani ali prepisani.<br /><br />Vsi odjemalci, ki uporabljajo vaš strežnik, bodo samodejno osveženi.",
|
||||
"MessageScheduleLibraryScanNote": "Za večino uporabnikov je priporočljivo, da to funkcijo pustite onemogočeno in ohranite nastavitev pregledovalnika map omogočeno. Pregledovalnik map bo samodejno zaznal spremembe v mapah vaše knjižnice. Pregledovalnik map ne deluje za vse datotečne sisteme (na primer NFS), zato lahko namesto tega uporabite načrtovane preglede knjižnic.",
|
||||
"MessageSearchResultsFor": "Rezultati iskanja za",
|
||||
"MessageSelected": "{0} izbrano",
|
||||
"MessageServerCouldNotBeReached": "Strežnika ni bilo mogoče doseči",
|
||||
@@ -952,7 +954,6 @@
|
||||
"ToastBookmarkCreateFailed": "Zaznamka ni bilo mogoče ustvariti",
|
||||
"ToastBookmarkCreateSuccess": "Zaznamek dodan",
|
||||
"ToastBookmarkRemoveSuccess": "Zaznamek odstranjen",
|
||||
"ToastBookmarkUpdateSuccess": "Zaznamek posodobljen",
|
||||
"ToastCachePurgeFailed": "Čiščenje predpomnilnika ni uspelo",
|
||||
"ToastCachePurgeSuccess": "Predpomnilnik je bil uspešno očiščen",
|
||||
"ToastChaptersHaveErrors": "Poglavja imajo napake",
|
||||
@@ -963,6 +964,7 @@
|
||||
"ToastCollectionRemoveSuccess": "Zbirka je bila odstranjena",
|
||||
"ToastCollectionUpdateSuccess": "Zbirka je bila posodobljena",
|
||||
"ToastCoverUpdateFailed": "Posodobitev naslovnice ni uspela",
|
||||
"ToastDateTimeInvalidOrIncomplete": "Datum in čas sta neveljavna ali nepopolna",
|
||||
"ToastDeleteFileFailed": "Brisanje datoteke ni uspelo",
|
||||
"ToastDeleteFileSuccess": "Datoteka je bila izbrisana",
|
||||
"ToastDeviceAddFailed": "Naprave ni bilo mogoče dodati",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"ButtonChooseFiles": "Välj filer",
|
||||
"ButtonClearFilter": "Rensa filter",
|
||||
"ButtonCloseFeed": "Stäng flöde",
|
||||
"ButtonCloseSession": "Stäng öppen session",
|
||||
"ButtonCollections": "Samlingar",
|
||||
"ButtonConfigureScanner": "Konfigurera skanner",
|
||||
"ButtonCreate": "Skapa",
|
||||
@@ -28,11 +29,14 @@
|
||||
"ButtonEdit": "Redigera",
|
||||
"ButtonEditChapters": "Redigera kapitel",
|
||||
"ButtonEditPodcast": "Redigera podcast",
|
||||
"ButtonEnable": "Aktivera",
|
||||
"ButtonForceReScan": "Tvinga omstart",
|
||||
"ButtonFullPath": "Fullständig sökväg",
|
||||
"ButtonHide": "Dölj",
|
||||
"ButtonHome": "Hem",
|
||||
"ButtonIssues": "Problem",
|
||||
"ButtonJumpBackward": "Hoppa bakåt",
|
||||
"ButtonJumpForward": "Hoppa framåt",
|
||||
"ButtonLatest": "Senaste",
|
||||
"ButtonLibrary": "Bibliotek",
|
||||
"ButtonLogout": "Logga ut",
|
||||
@@ -44,6 +48,7 @@
|
||||
"ButtonNevermind": "Glöm det",
|
||||
"ButtonNext": "Nästa",
|
||||
"ButtonNextChapter": "Nästa kapitel",
|
||||
"ButtonNextItemInQueue": "Nästa objekt i Kö",
|
||||
"ButtonOk": "Ok",
|
||||
"ButtonOpenFeed": "Öppna flöde",
|
||||
"ButtonOpenManager": "Öppna Manager",
|
||||
@@ -54,6 +59,7 @@
|
||||
"ButtonPlaylists": "Spellistor",
|
||||
"ButtonPrevious": "Föregående",
|
||||
"ButtonPreviousChapter": "Föregående kapitel",
|
||||
"ButtonProbeAudioFile": "Analysera ljudfil",
|
||||
"ButtonPurgeAllCache": "Rensa all cache",
|
||||
"ButtonPurgeItemsCache": "Rensa föremåls-cache",
|
||||
"ButtonQueueAddItem": "Lägg till i kön",
|
||||
@@ -701,7 +707,6 @@
|
||||
"ToastBookmarkCreateFailed": "Det gick inte att skapa bokmärket",
|
||||
"ToastBookmarkCreateSuccess": "Bokmärket har adderats",
|
||||
"ToastBookmarkRemoveSuccess": "Bokmärket har raderats",
|
||||
"ToastBookmarkUpdateSuccess": "Bokmärket har uppdaterats",
|
||||
"ToastChaptersHaveErrors": "Kapitlen har fel",
|
||||
"ToastChaptersMustHaveTitles": "Kapitel måste ha titlar",
|
||||
"ToastCollectionRemoveSuccess": "Samlingen har raderats",
|
||||
|
||||
@@ -758,6 +758,7 @@
|
||||
"MessageConfirmResetProgress": "Ви впевнені, що хочете скинути свій прогрес?",
|
||||
"MessageConfirmSendEbookToDevice": "Ви дійсно хочете відправити на пристрій \"{2}\" електроні книги: {0}, \"{1}\"?",
|
||||
"MessageConfirmUnlinkOpenId": "Ви впевнені, що хочете відв'язати цього користувача від OpenID?",
|
||||
"MessageDaysListenedInTheLastYear": "{0} днів, прослуханих за останній рік",
|
||||
"MessageDownloadingEpisode": "Завантаження епізоду",
|
||||
"MessageDragFilesIntoTrackOrder": "Перетягніть файли до правильного порядку",
|
||||
"MessageEmbedFailed": "Не вдалося вбудувати!",
|
||||
@@ -836,6 +837,7 @@
|
||||
"MessageResetChaptersConfirm": "Ви дійсно бажаєте скинути глави та скасувати внесені зміни?",
|
||||
"MessageRestoreBackupConfirm": "Ви дійсно бажаєте відновити резервну копію від",
|
||||
"MessageRestoreBackupWarning": "Відновлення резервної копії перезапише всю базу даних, розташовану в /config, і зображення обкладинок в /metadata/items та /metadata/authors.<br /><br />Резервні копії не змінюють жодних файлів у теках бібліотеки. Якщо у налаштуваннях сервера увімкнено збереження обкладинок і метаданих у теках бібліотеки, вони не створюються під час резервного копіювання і не перезаписуються..<br /><br />Всі клієнти, що користуються вашим сервером, будуть автоматично оновлені.",
|
||||
"MessageScheduleLibraryScanNote": "Для більшості користувачів рекомендується залишити цю функцію вимкненою та залишити параметр перегляду папок увімкненим. Засіб спостереження за папками автоматично виявить зміни в папках вашої бібліотеки. Засіб спостереження за папками не працює для кожної файлової системи (наприклад, NFS), тому замість нього можна використовувати сканування бібліотек за розкладом.",
|
||||
"MessageSearchResultsFor": "Результати пошуку для",
|
||||
"MessageSelected": "Вибрано: {0}",
|
||||
"MessageServerCouldNotBeReached": "Не вдалося підключитися до сервера",
|
||||
@@ -952,7 +954,6 @@
|
||||
"ToastBookmarkCreateFailed": "Не вдалося створити закладку",
|
||||
"ToastBookmarkCreateSuccess": "Закладку додано",
|
||||
"ToastBookmarkRemoveSuccess": "Закладку видалено",
|
||||
"ToastBookmarkUpdateSuccess": "Закладку оновлено",
|
||||
"ToastCachePurgeFailed": "Не вдалося очистити кеш",
|
||||
"ToastCachePurgeSuccess": "Кеш очищено",
|
||||
"ToastChaptersHaveErrors": "Глави містять помилки",
|
||||
@@ -963,6 +964,7 @@
|
||||
"ToastCollectionRemoveSuccess": "Добірку видалено",
|
||||
"ToastCollectionUpdateSuccess": "Добірку оновлено",
|
||||
"ToastCoverUpdateFailed": "Не вдалося оновити обкладинку",
|
||||
"ToastDateTimeInvalidOrIncomplete": "Дата й час недійсні або неповні",
|
||||
"ToastDeleteFileFailed": "Не вдалося видалити файл",
|
||||
"ToastDeleteFileSuccess": "Файл видалено",
|
||||
"ToastDeviceAddFailed": "Не вдалося додати пристрій",
|
||||
@@ -1015,6 +1017,7 @@
|
||||
"ToastNewUserTagError": "Потрібно вибрати хоча б один тег",
|
||||
"ToastNewUserUsernameError": "Введіть ім'я користувача",
|
||||
"ToastNoNewEpisodesFound": "Нових епізодів не знайдено",
|
||||
"ToastNoRSSFeed": "Подкаст не має RSS-канал",
|
||||
"ToastNoUpdatesNecessary": "Оновлення не потрібні",
|
||||
"ToastNotificationCreateFailed": "Не вдалося створити сповіщення",
|
||||
"ToastNotificationDeleteFailed": "Не вдалося видалити сповіщення",
|
||||
|
||||
@@ -679,7 +679,6 @@
|
||||
"ToastBookmarkCreateFailed": "Tạo đánh dấu thất bại",
|
||||
"ToastBookmarkCreateSuccess": "Đã thêm đánh dấu",
|
||||
"ToastBookmarkRemoveSuccess": "Đánh dấu đã được xóa",
|
||||
"ToastBookmarkUpdateSuccess": "Đánh dấu đã được cập nhật",
|
||||
"ToastChaptersHaveErrors": "Các chương có lỗi",
|
||||
"ToastChaptersMustHaveTitles": "Các chương phải có tiêu đề",
|
||||
"ToastCollectionRemoveSuccess": "Bộ sưu tập đã được xóa",
|
||||
|
||||
@@ -950,7 +950,6 @@
|
||||
"ToastBookmarkCreateFailed": "创建书签失败",
|
||||
"ToastBookmarkCreateSuccess": "书签已添加",
|
||||
"ToastBookmarkRemoveSuccess": "书签已删除",
|
||||
"ToastBookmarkUpdateSuccess": "书签已更新",
|
||||
"ToastCachePurgeFailed": "清除缓存失败",
|
||||
"ToastCachePurgeSuccess": "缓存清除成功",
|
||||
"ToastChaptersHaveErrors": "章节有错误",
|
||||
|
||||
@@ -723,7 +723,6 @@
|
||||
"ToastBookmarkCreateFailed": "創建書簽失敗",
|
||||
"ToastBookmarkCreateSuccess": "書籤已新增",
|
||||
"ToastBookmarkRemoveSuccess": "書籤已刪除",
|
||||
"ToastBookmarkUpdateSuccess": "書籤已更新",
|
||||
"ToastChaptersHaveErrors": "章節有錯誤",
|
||||
"ToastChaptersMustHaveTitles": "章節必須有標題",
|
||||
"ToastCollectionRemoveSuccess": "收藏夾已刪除",
|
||||
|
||||
@@ -1,3 +1,18 @@
|
||||
const optionDefinitions = [
|
||||
{ name: 'config', alias: 'c', type: String },
|
||||
{ name: 'metadata', alias: 'm', type: String },
|
||||
{ name: 'port', alias: 'p', type: String },
|
||||
{ name: 'host', alias: 'h', type: String },
|
||||
{ name: 'source', alias: 's', type: String },
|
||||
{ name: 'dev', alias: 'd', type: Boolean }
|
||||
]
|
||||
|
||||
const commandLineArgs = require('./server/libs/commandLineArgs')
|
||||
const options = commandLineArgs(optionDefinitions)
|
||||
|
||||
const Path = require('path')
|
||||
process.env.NODE_ENV = options.dev ? 'development' : process.env.NODE_ENV || 'production'
|
||||
|
||||
const server = require('./server/Server')
|
||||
global.appRoot = __dirname
|
||||
|
||||
@@ -17,14 +32,19 @@ if (isDev) {
|
||||
process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath || ''
|
||||
}
|
||||
|
||||
const PORT = process.env.PORT || 80
|
||||
const HOST = process.env.HOST
|
||||
const CONFIG_PATH = process.env.CONFIG_PATH || '/config'
|
||||
const METADATA_PATH = process.env.METADATA_PATH || '/metadata'
|
||||
const SOURCE = process.env.SOURCE || 'docker'
|
||||
const inputConfig = options.config ? Path.resolve(options.config) : null
|
||||
const inputMetadata = options.metadata ? Path.resolve(options.metadata) : null
|
||||
|
||||
const PORT = options.port || process.env.PORT || 3333
|
||||
const HOST = options.host || process.env.HOST
|
||||
const CONFIG_PATH = inputConfig || process.env.CONFIG_PATH || Path.resolve('config')
|
||||
const METADATA_PATH = inputMetadata || process.env.METADATA_PATH || Path.resolve('metadata')
|
||||
const SOURCE = options.source || process.env.SOURCE || 'debian'
|
||||
|
||||
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || ''
|
||||
|
||||
console.log('Config', CONFIG_PATH, METADATA_PATH)
|
||||
console.log(`Running in ${process.env.NODE_ENV} mode.`)
|
||||
console.log(`Options: CONFIG_PATH=${CONFIG_PATH}, METADATA_PATH=${METADATA_PATH}, PORT=${PORT}, HOST=${HOST}, SOURCE=${SOURCE}, ROUTER_BASE_PATH=${ROUTER_BASE_PATH}`)
|
||||
|
||||
const Server = new server(SOURCE, PORT, HOST, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH)
|
||||
Server.start()
|
||||
|
||||
Generated
+1
-1
@@ -30,7 +30,7 @@
|
||||
"xml2js": "^0.5.0"
|
||||
},
|
||||
"bin": {
|
||||
"audiobookshelf": "prod.js"
|
||||
"audiobookshelf": "index.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"chai": "^4.3.10",
|
||||
|
||||
+4
-4
@@ -5,10 +5,10 @@
|
||||
"description": "Self-hosted audiobook and podcast server",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"dev": "nodemon --watch server index.js",
|
||||
"dev": "nodemon --watch server index.js -- --dev",
|
||||
"start": "node index.js",
|
||||
"client": "cd client && npm ci && npm run generate",
|
||||
"prod": "npm run client && npm ci && node prod.js",
|
||||
"prod": "npm run client && npm ci && node index.js",
|
||||
"build-win": "npm run client && pkg -t node20-win-x64 -o ./dist/win/audiobookshelf -C GZip .",
|
||||
"build-linux": "build/linuxpackager",
|
||||
"docker": "docker buildx build --platform linux/amd64,linux/arm64 --push . -t advplyr/audiobookshelf",
|
||||
@@ -18,7 +18,7 @@
|
||||
"test": "mocha",
|
||||
"coverage": "nyc mocha"
|
||||
},
|
||||
"bin": "prod.js",
|
||||
"bin": "index.js",
|
||||
"pkg": {
|
||||
"assets": [
|
||||
"client/dist/**/*",
|
||||
@@ -26,7 +26,7 @@
|
||||
"server/migrations/*.js"
|
||||
],
|
||||
"scripts": [
|
||||
"prod.js",
|
||||
"index.js",
|
||||
"server/**/*.js"
|
||||
]
|
||||
},
|
||||
|
||||
@@ -111,8 +111,8 @@ server {
|
||||
|
||||
location / {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
|
||||
|
||||
@@ -85,6 +85,12 @@ class Server {
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.PODCAST_DOWNLOAD_TIMEOUT) {
|
||||
global.PodcastDownloadTimeout = process.env.PODCAST_DOWNLOAD_TIMEOUT
|
||||
} else {
|
||||
global.PodcastDownloadTimeout = 30000
|
||||
}
|
||||
|
||||
if (!fs.pathExistsSync(global.ConfigPath)) {
|
||||
fs.mkdirSync(global.ConfigPath)
|
||||
}
|
||||
|
||||
@@ -170,21 +170,34 @@ class LibraryController {
|
||||
* GET: /api/libraries
|
||||
* Get all libraries
|
||||
*
|
||||
* ?include=stats to load library stats - used in android auto to filter out libraries with no audio
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async findAll(req, res) {
|
||||
const libraries = await Database.libraryModel.getAllWithFolders()
|
||||
let libraries = await Database.libraryModel.getAllWithFolders()
|
||||
|
||||
const librariesAccessible = req.user.permissions?.librariesAccessible || []
|
||||
if (librariesAccessible.length) {
|
||||
return res.json({
|
||||
libraries: libraries.filter((lib) => librariesAccessible.includes(lib.id)).map((lib) => lib.toOldJSON())
|
||||
})
|
||||
libraries = libraries.filter((lib) => librariesAccessible.includes(lib.id))
|
||||
}
|
||||
|
||||
libraries = libraries.map((lib) => lib.toOldJSON())
|
||||
|
||||
const includeArray = (req.query.include || '').split(',')
|
||||
if (includeArray.includes('stats')) {
|
||||
for (const library of libraries) {
|
||||
if (library.mediaType === 'book') {
|
||||
library.stats = await libraryItemsBookFilters.getBookLibraryStats(library.id)
|
||||
} else if (library.mediaType === 'podcast') {
|
||||
library.stats = await libraryItemsPodcastFilters.getPodcastLibraryStats(library.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
libraries: libraries.map((lib) => lib.toOldJSON())
|
||||
libraries
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -266,7 +266,7 @@ class LibraryItemController {
|
||||
*
|
||||
* @param {LibraryItemControllerRequest} req
|
||||
* @param {Response} res
|
||||
* @param {boolean} [updateAndReturnJson=true]
|
||||
* @param {boolean} [updateAndReturnJson=true] - Allows the function to be used for both direct API calls and internally
|
||||
*/
|
||||
async uploadCover(req, res, updateAndReturnJson = true) {
|
||||
if (!req.user.canUpload) {
|
||||
@@ -291,11 +291,11 @@ class LibraryItemController {
|
||||
return res.status(500).send('Unknown error occurred')
|
||||
}
|
||||
|
||||
if (updateAndReturnJson) {
|
||||
req.libraryItem.media.coverPath = result.cover
|
||||
req.libraryItem.media.changed('coverPath', true)
|
||||
await req.libraryItem.media.save()
|
||||
req.libraryItem.media.coverPath = result.cover
|
||||
req.libraryItem.media.changed('coverPath', true)
|
||||
await req.libraryItem.media.save()
|
||||
|
||||
if (updateAndReturnJson) {
|
||||
// client uses updatedAt timestamp in URL to force refresh cover
|
||||
req.libraryItem.changed('updatedAt', true)
|
||||
await req.libraryItem.save()
|
||||
|
||||
@@ -461,6 +461,9 @@ class PodcastController {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
// Remove it from the podcastEpisodes array
|
||||
req.libraryItem.media.podcastEpisodes = req.libraryItem.media.podcastEpisodes.filter((ep) => ep.id !== episodeId)
|
||||
|
||||
if (hardDelete) {
|
||||
const audioFile = episode.audioFile
|
||||
// TODO: this will trigger the watcher. should maybe handle this gracefully
|
||||
|
||||
@@ -343,20 +343,20 @@ class PlaybackSessionManager {
|
||||
* @param {import('../models/User')} user
|
||||
* @param {*} session
|
||||
* @param {*} syncData
|
||||
* @returns
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async syncSession(user, session, syncData) {
|
||||
// TODO: Combine libraryItem query with library query
|
||||
const libraryItem = await Database.libraryItemModel.getExpandedById(session.libraryItemId)
|
||||
if (!libraryItem) {
|
||||
Logger.error(`[PlaybackSessionManager] syncSession Library Item not found "${session.libraryItemId}"`)
|
||||
return null
|
||||
return false
|
||||
}
|
||||
|
||||
const library = await Database.libraryModel.findByPk(libraryItem.libraryId)
|
||||
if (!library) {
|
||||
Logger.error(`[PlaybackSessionManager] syncSession Library not found "${libraryItem.libraryId}"`)
|
||||
return null
|
||||
return false
|
||||
}
|
||||
|
||||
session.currentTime = syncData.currentTime
|
||||
@@ -382,6 +382,8 @@ class PlaybackSessionManager {
|
||||
})
|
||||
}
|
||||
this.saveSession(session)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -115,10 +115,24 @@ class PodcastManager {
|
||||
let success = false
|
||||
if (this.currentDownload.isMp3) {
|
||||
// Download episode and tag it
|
||||
success = await ffmpegHelpers.downloadPodcastEpisode(this.currentDownload).catch((error) => {
|
||||
const ffmpegDownloadResponse = await ffmpegHelpers.downloadPodcastEpisode(this.currentDownload).catch((error) => {
|
||||
Logger.error(`[PodcastManager] Podcast Episode download failed`, error)
|
||||
return false
|
||||
})
|
||||
success = !!ffmpegDownloadResponse?.success
|
||||
|
||||
// If failed due to ffmpeg error, retry without tagging
|
||||
// e.g. RSS feed may have incorrect file extension and file type
|
||||
// See https://github.com/advplyr/audiobookshelf/issues/3837
|
||||
if (!success && ffmpegDownloadResponse?.isFfmpegError) {
|
||||
Logger.info(`[PodcastManager] Retrying episode download without tagging`)
|
||||
// Download episode only
|
||||
success = await downloadFile(this.currentDownload.url, this.currentDownload.targetPath)
|
||||
.then(() => true)
|
||||
.catch((error) => {
|
||||
Logger.error(`[PodcastManager] Podcast Episode download failed`, error)
|
||||
return false
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Download episode only
|
||||
success = await downloadFile(this.currentDownload.url, this.currentDownload.targetPath)
|
||||
|
||||
@@ -337,7 +337,7 @@ class LibraryItem extends Model {
|
||||
const itemsInProgressPayload = await libraryFilters.getMediaItemsInProgress(library, user, include, limit, false)
|
||||
if (itemsInProgressPayload.items.length) {
|
||||
const ebookOnlyItemsInProgress = itemsInProgressPayload.items.filter((li) => li.media.ebookFormat && !li.media.numTracks)
|
||||
const audioItemsInProgress = itemsInProgressPayload.items.filter((li) => li.media.numTracks)
|
||||
const audioItemsInProgress = itemsInProgressPayload.items.filter((li) => li.media.numTracks || li.mediaType === 'podcast')
|
||||
|
||||
if (audioItemsInProgress.length) {
|
||||
shelves.push({
|
||||
@@ -448,7 +448,7 @@ class LibraryItem extends Model {
|
||||
const mediaFinishedPayload = await libraryFilters.getMediaFinished(library, user, include, limit)
|
||||
if (mediaFinishedPayload.items.length) {
|
||||
const ebookOnlyItemsInProgress = mediaFinishedPayload.items.filter((li) => li.media.ebookFormat && !li.media.numTracks)
|
||||
const audioItemsInProgress = mediaFinishedPayload.items.filter((li) => li.media.numTracks)
|
||||
const audioItemsInProgress = mediaFinishedPayload.items.filter((li) => li.media.numTracks || li.mediaType === 'podcast')
|
||||
|
||||
if (audioItemsInProgress.length) {
|
||||
shelves.push({
|
||||
|
||||
@@ -168,7 +168,7 @@ class PodcastEpisode extends Model {
|
||||
getAudioTrack(libraryItemId) {
|
||||
const track = structuredClone(this.audioFile)
|
||||
track.startOffset = 0
|
||||
track.title = this.audioFile.metadata.title
|
||||
track.title = this.audioFile.metadata.filename
|
||||
track.contentUrl = `${global.RouterBasePath}/api/items/${libraryItemId}/file/${track.ino}`
|
||||
return track
|
||||
}
|
||||
|
||||
@@ -499,16 +499,17 @@ class AudioFileScanner {
|
||||
// Filter these out and log a warning
|
||||
// See https://github.com/advplyr/audiobookshelf/issues/3361
|
||||
const afChaptersCleaned =
|
||||
file.chapters?.filter((c) => {
|
||||
file.chapters?.filter((c, i) => {
|
||||
if (c.end - c.start < 0.1) {
|
||||
libraryScan.addLog(LogLevel.WARN, `Chapter "${c.title}" has invalid duration of ${c.end - c.start} seconds. Skipping this chapter.`)
|
||||
libraryScan.addLog(LogLevel.WARN, `Audio file "${file.metadata.filename}" Chapter "${c.title}" (index ${i}) has invalid duration of ${c.end - c.start} seconds. Skipping this chapter.`)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}) || []
|
||||
const afChapters = afChaptersCleaned.map((c) => ({
|
||||
|
||||
const afChapters = afChaptersCleaned.map((c, i) => ({
|
||||
...c,
|
||||
id: c.id + currChapterId,
|
||||
id: currChapterId + i,
|
||||
start: c.start + currStartTime,
|
||||
end: c.end + currStartTime
|
||||
}))
|
||||
|
||||
@@ -189,8 +189,14 @@ class CbzStreamZipComicBookExtractor extends AbstractComicBookExtractor {
|
||||
}
|
||||
|
||||
close() {
|
||||
this.archive?.close()
|
||||
Logger.debug(`[CbzStreamZipComicBookExtractor] Closed comic book "${this.comicPath}"`)
|
||||
this.archive
|
||||
?.close()
|
||||
.then(() => {
|
||||
Logger.debug(`[CbzStreamZipComicBookExtractor] Closed comic book "${this.comicPath}"`)
|
||||
})
|
||||
.catch((error) => {
|
||||
Logger.error(`[CbzStreamZipComicBookExtractor] Failed to close comic book "${this.comicPath}"`, error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ module.exports.resizeImage = resizeImage
|
||||
/**
|
||||
*
|
||||
* @param {import('../objects/PodcastEpisodeDownload')} podcastEpisodeDownload
|
||||
* @returns
|
||||
* @returns {Promise<{success: boolean, isFfmpegError?: boolean}>}
|
||||
*/
|
||||
module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
|
||||
return new Promise(async (resolve) => {
|
||||
@@ -110,12 +110,16 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
|
||||
headers: {
|
||||
'User-Agent': 'audiobookshelf (+https://audiobookshelf.org)'
|
||||
},
|
||||
timeout: 30000
|
||||
timeout: global.PodcastDownloadTimeout
|
||||
}).catch((error) => {
|
||||
Logger.error(`[ffmpegHelpers] Failed to download podcast episode with url "${podcastEpisodeDownload.url}"`, error)
|
||||
return null
|
||||
})
|
||||
if (!response) return resolve(false)
|
||||
if (!response) {
|
||||
return resolve({
|
||||
success: false
|
||||
})
|
||||
}
|
||||
|
||||
/** @type {import('../libs/fluentFfmpeg/index').FfmpegCommand} */
|
||||
const ffmpeg = Ffmpeg(response.data)
|
||||
@@ -177,7 +181,10 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
|
||||
if (stderrLines.length) {
|
||||
Logger.error(`Full stderr dump for episode url "${podcastEpisodeDownload.url}": ${stderrLines.join('\n')}`)
|
||||
}
|
||||
resolve(false)
|
||||
resolve({
|
||||
success: false,
|
||||
isFfmpegError: true
|
||||
})
|
||||
})
|
||||
ffmpeg.on('progress', (progress) => {
|
||||
let progressPercent = 0
|
||||
@@ -189,7 +196,9 @@ module.exports.downloadPodcastEpisode = (podcastEpisodeDownload) => {
|
||||
})
|
||||
ffmpeg.on('end', () => {
|
||||
Logger.debug(`[FfmpegHelpers] downloadPodcastEpisode: Complete`)
|
||||
resolve(podcastEpisodeDownload.targetPath)
|
||||
resolve({
|
||||
success: true
|
||||
})
|
||||
})
|
||||
ffmpeg.run()
|
||||
})
|
||||
|
||||
@@ -43,7 +43,9 @@ async function parse(ebookFile) {
|
||||
archive = createComicBookExtractor(comicPath)
|
||||
await archive.open()
|
||||
|
||||
const filePaths = await archive.getFilePaths()
|
||||
const filePaths = await archive.getFilePaths().catch((error) => {
|
||||
Logger.error(`[parseComicMetadata] Failed to get file paths from comic at "${comicPath}"`, error)
|
||||
})
|
||||
|
||||
// Sort the file paths in a natural order to get the first image
|
||||
filePaths.sort((a, b) => {
|
||||
|
||||
@@ -52,6 +52,29 @@ function extractFirstArrayItem(json, key) {
|
||||
return json[key][0]
|
||||
}
|
||||
|
||||
function extractStringOrStringify(json) {
|
||||
try {
|
||||
if (typeof json[Object.keys(json)[0]]?.[0] === 'string') {
|
||||
return json[Object.keys(json)[0]][0]
|
||||
}
|
||||
// Handles case where html was included without being wrapped in CDATA
|
||||
return JSON.stringify(value)
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function extractFirstArrayItemString(json, key) {
|
||||
const item = extractFirstArrayItem(json, key)
|
||||
if (!item) return ''
|
||||
if (typeof item === 'object') {
|
||||
if (item?.['_'] && typeof item['_'] === 'string') return item['_']
|
||||
|
||||
return extractStringOrStringify(item)
|
||||
}
|
||||
return typeof item === 'string' ? item : ''
|
||||
}
|
||||
|
||||
function extractImage(channel) {
|
||||
if (!channel.image || !channel.image.url || !channel.image.url.length) {
|
||||
if (!channel['itunes:image'] || !channel['itunes:image'].length || !channel['itunes:image'][0]['$']) {
|
||||
@@ -101,7 +124,7 @@ function extractPodcastMetadata(channel) {
|
||||
}
|
||||
|
||||
if (channel['description']) {
|
||||
const rawDescription = extractFirstArrayItem(channel, 'description') || ''
|
||||
const rawDescription = extractFirstArrayItemString(channel, 'description')
|
||||
metadata.description = htmlSanitizer.sanitize(rawDescription.trim())
|
||||
metadata.descriptionPlain = htmlSanitizer.stripAllTags(rawDescription.trim())
|
||||
}
|
||||
@@ -145,7 +168,8 @@ function extractEpisodeData(item) {
|
||||
|
||||
// Supposed to be the plaintext description but not always followed
|
||||
if (item['description']) {
|
||||
const rawDescription = extractFirstArrayItem(item, 'description') || ''
|
||||
const rawDescription = extractFirstArrayItemString(item, 'description')
|
||||
|
||||
if (!episode.description) episode.description = htmlSanitizer.sanitize(rawDescription.trim())
|
||||
episode.descriptionPlain = htmlSanitizer.stripAllTags(rawDescription.trim())
|
||||
}
|
||||
@@ -175,9 +199,7 @@ function extractEpisodeData(item) {
|
||||
const arrayFields = ['title', 'itunes:episodeType', 'itunes:season', 'itunes:episode', 'itunes:author', 'itunes:duration', 'itunes:explicit', 'itunes:subtitle']
|
||||
arrayFields.forEach((key) => {
|
||||
const cleanKey = key.split(':').pop()
|
||||
let value = extractFirstArrayItem(item, key)
|
||||
if (value?.['_']) value = value['_']
|
||||
episode[cleanKey] = value
|
||||
episode[cleanKey] = extractFirstArrayItemString(item, key)
|
||||
})
|
||||
return episode
|
||||
}
|
||||
@@ -281,7 +303,7 @@ module.exports.getPodcastFeed = (feedUrl, excludeEpisodeMetadata = false) => {
|
||||
return axios({
|
||||
url: feedUrl,
|
||||
method: 'GET',
|
||||
timeout: 12000,
|
||||
timeout: global.PodcastDownloadTimeout,
|
||||
responseType: 'arraybuffer',
|
||||
headers: {
|
||||
Accept: 'application/rss+xml, application/xhtml+xml, application/xml, */*;q=0.8',
|
||||
|
||||
@@ -143,6 +143,7 @@ function parseChapters(_chapters) {
|
||||
.map((chap) => {
|
||||
let title = chap['TAG:title'] || chap.title || ''
|
||||
if (!title && chap.tags?.title) title = chap.tags.title
|
||||
title = title.trim()
|
||||
|
||||
const timebase = chap.time_base?.includes('/') ? Number(chap.time_base.split('/')[1]) : 1
|
||||
const start = !isNullOrNaN(chap.start_time) ? Number(chap.start_time) : !isNullOrNaN(chap.start) ? Number(chap.start) / timebase : 0
|
||||
|
||||
Reference in New Issue
Block a user