Merge branch 'master' into enhancement/title-field-visibility

This commit is contained in:
Greg Lorenzen 2024-11-25 18:57:00 -08:00
commit 9cd203d95b
185 changed files with 5867 additions and 1686 deletions

33
.github/pull_request_template.md vendored Normal file
View file

@ -0,0 +1,33 @@
<!--
For Work In Progress Pull Requests, please use the Draft PR feature,
see https://github.blog/2019-02-14-introducing-draft-pull-requests/ for further details.
If you do not follow this template, the PR may be closed without review.
Please ensure all checks pass.
If you are a new contributor, the workflows will need to be manually approved before they run.
-->
## Brief summary
<!-- Please provide a brief summary of what your PR attempts to achieve. -->
## Which issue is fixed?
<!-- Which issue number does this PR fix? Ex: "Fixes #1234" -->
## In-depth Description
<!--
Describe your solution in more depth.
How does it work? Why is this the best solution?
Does it solve a problem that affects multiple users or is this an edge case for your setup?
-->
## How have you tested this?
<!-- Please describe in detail with reproducible steps how you tested your changes. -->
## Screenshots
<!-- If your PR includes any changes to the web client, please include screenshots or a short video from before and after your changes. -->

View file

@ -1,11 +1,25 @@
name: "CodeQL" name: 'CodeQL'
on: on:
push: push:
branches: [ 'master' ] branches: ['master']
# Only build when files in these directories have been changed
paths:
- client/**
- server/**
- test/**
- index.js
- package.json
pull_request: pull_request:
# The branches below must be a subset of the branches above # The branches below must be a subset of the branches above
branches: [ 'master' ] branches: ['master']
# Only build when files in these directories have been changed
paths:
- client/**
- server/**
- test/**
- index.js
- package.json
schedule: schedule:
- cron: '16 5 * * 4' - cron: '16 5 * * 4'
@ -21,7 +35,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
language: [ 'javascript' ] language: ['javascript']
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Use only 'java' to analyze code written in Java, Kotlin or both # Use only 'java' to analyze code written in Java, Kotlin or both
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both # Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
@ -43,7 +57,6 @@ jobs:
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality # queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
@ -62,4 +75,4 @@ jobs:
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2 uses: github/codeql-action/analyze@v2
with: with:
category: "/language:${{matrix.language}}" category: '/language:${{matrix.language}}'

View file

@ -70,6 +70,7 @@ jobs:
uses: docker/build-push-action@v3 uses: docker/build-push-action@v3
with: with:
tags: ${{ github.event.inputs.tags || steps.meta.outputs.tags }} tags: ${{ github.event.inputs.tags || steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
context: . context: .
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true

View file

@ -5,6 +5,13 @@ on:
push: push:
branches-ignore: branches-ignore:
- 'dependabot/**' # Don't run dependabot branches, as they are already covered by pull requests - 'dependabot/**' # Don't run dependabot branches, as they are already covered by pull requests
# Only build when files in these directories have been changed
paths:
- client/**
- server/**
- test/**
- index.js
- package.json
jobs: jobs:
build: build:

1
.gitignore vendored
View file

@ -16,6 +16,7 @@
/ffmpeg* /ffmpeg*
/ffprobe* /ffprobe*
/unicode* /unicode*
/libnusqlite3*
sw.* sw.*
.DS_STORE .DS_STORE

View file

@ -16,15 +16,30 @@ RUN apk update && \
tzdata \ tzdata \
ffmpeg \ ffmpeg \
make \ make \
gcompat \
python3 \ python3 \
g++ \ g++ \
tini tini \
unzip
COPY --from=build /client/dist /client/dist COPY --from=build /client/dist /client/dist
COPY index.js package* / COPY index.js package* /
COPY server server COPY server server
ARG TARGETPLATFORM
ENV NUSQLITE3_DIR="/usr/local/lib/nusqlite3"
ENV NUSQLITE3_PATH="${NUSQLITE3_DIR}/libnusqlite3.so"
RUN case "$TARGETPLATFORM" in \
"linux/amd64") \
curl -L -o /tmp/library.zip "https://github.com/mikiher/nunicode-sqlite/releases/download/v1.2/libnusqlite3-linux-musl-x64.zip" ;; \
"linux/arm64") \
curl -L -o /tmp/library.zip "https://github.com/mikiher/nunicode-sqlite/releases/download/v1.2/libnusqlite3-linux-musl-arm64.zip" ;; \
*) echo "Unsupported platform: $TARGETPLATFORM" && exit 1 ;; \
esac && \
unzip /tmp/library.zip -d $NUSQLITE3_DIR && \
rm /tmp/library.zip
RUN npm ci --only=production RUN npm ci --only=production
RUN apk del make python3 g++ RUN apk del make python3 g++

View file

@ -24,7 +24,7 @@
</div> </div>
<div v-if="shelf.type === 'authors'" class="flex items-center"> <div v-if="shelf.type === 'authors'" class="flex items-center">
<template v-for="entity in shelf.entities"> <template v-for="entity in shelf.entities">
<cards-author-card :key="entity.id" :author="entity" @hook:updated="updatedBookCard" class="mx-2e" @edit="editAuthor" /> <cards-author-card :key="entity.id" :authorMount="entity" @hook:updated="updatedBookCard" class="mx-2e" @edit="editAuthor" />
</template> </template>
</div> </div>
<div v-if="shelf.type === 'narrators'" class="flex items-center"> <div v-if="shelf.type === 'narrators'" class="flex items-center">

View file

@ -30,7 +30,7 @@
<p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p> <p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
<span v-else class="material-symbols text-lg">&#xe431;</span> <span v-else class="material-symbols text-lg">&#xe431;</span>
</nuxt-link> </nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'"> <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p> <p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p>
<svg v-else class="w-5 h-5" viewBox="0 0 24 24"> <svg v-else class="w-5 h-5" viewBox="0 0 24 24">
<path <path
@ -62,7 +62,7 @@
<ui-context-menu-dropdown v-if="!isBatchSelecting && seriesContextMenuItems.length" :items="seriesContextMenuItems" class="mx-px" @action="seriesContextMenuAction" /> <ui-context-menu-dropdown v-if="!isBatchSelecting && seriesContextMenuItems.length" :items="seriesContextMenuItems" class="mx-px" @action="seriesContextMenuAction" />
</template> </template>
<!-- library & collections page --> <!-- library & collections page -->
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome"> <template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome && !isAuthorsPage">
<p class="hidden md:block">{{ $formatNumber(numShowing) }} {{ entityName }}</p> <p class="hidden md:block">{{ $formatNumber(numShowing) }} {{ entityName }}</p>
<div class="flex-grow hidden sm:inline-block" /> <div class="flex-grow hidden sm:inline-block" />
@ -92,12 +92,14 @@
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" /> <ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" :menu-width="110" class="ml-2" @action="contextMenuAction" />
</template> </template>
<!-- authors page --> <!-- authors page -->
<template v-else-if="page === 'authors'"> <template v-else-if="isAuthorsPage">
<div class="flex-grow" /> <p class="hidden md:block">{{ $formatNumber(numShowing) }} {{ entityName }}</p>
<ui-btn v-if="userCanUpdate && authors?.length && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
<div class="flex-grow hidden sm:inline-block" />
<ui-btn v-if="userCanUpdate && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
<!-- author sort select --> <!-- author sort select -->
<controls-sort-select v-if="authors?.length" v-model="settings.authorSortBy" :descending.sync="settings.authorSortDesc" :items="authorSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateAuthorSort" /> <controls-sort-select v-model="settings.authorSortBy" :descending.sync="settings.authorSortDesc" :items="authorSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateAuthorSort" />
</template> </template>
<!-- home page --> <!-- home page -->
<template v-else-if="isHome"> <template v-else-if="isHome">
@ -117,11 +119,7 @@ export default {
type: Object, type: Object,
default: () => null default: () => null
}, },
searchQuery: String, searchQuery: String
authors: {
type: Array,
default: () => []
}
}, },
data() { data() {
return { return {
@ -268,7 +266,7 @@ export default {
return this.$route.name === 'library-library-podcast-latest' return this.$route.name === 'library-library-podcast-latest'
}, },
isAuthorsPage() { isAuthorsPage() {
return this.$route.name === 'library-library-authors' return this.page === 'authors'
}, },
isAlbumsPage() { isAlbumsPage() {
return this.page === 'albums' return this.page === 'albums'
@ -284,6 +282,7 @@ export default {
if (this.isSeriesPage) return this.$strings.LabelSeries if (this.isSeriesPage) return this.$strings.LabelSeries
if (this.isCollectionsPage) return this.$strings.LabelCollections if (this.isCollectionsPage) return this.$strings.LabelCollections
if (this.isPlaylistsPage) return this.$strings.LabelPlaylists if (this.isPlaylistsPage) return this.$strings.LabelPlaylists
if (this.isAuthorsPage) return this.$strings.LabelAuthors
return '' return ''
}, },
seriesId() { seriesId() {
@ -473,16 +472,24 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to re-add series to continue listening', error) console.error('Failed to re-add series to continue listening', error)
this.$toast.error(this.$strings.ToastItemUpdateFailed) this.$toast.error(this.$strings.ToastFailedToUpdate)
}) })
.finally(() => { .finally(() => {
this.processingSeries = false this.processingSeries = false
}) })
}, },
async fetchAllAuthors() {
// fetch all authors from the server, in the order that they are currently displayed
const response = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/authors?sort=${this.settings.authorSortBy}&desc=${this.settings.authorSortDesc}`)
return response.authors
},
async matchAllAuthors() { async matchAllAuthors() {
this.processingAuthors = true this.processingAuthors = true
for (const author of this.authors) { try {
const authors = await this.fetchAllAuthors()
for (const author of authors) {
const payload = {} const payload = {}
if (author.asin) payload.asin = author.asin if (author.asin) payload.asin = author.asin
else payload.q = author.name else payload.q = author.name
@ -510,6 +517,10 @@ export default {
this.$eventBus.$emit(`searching-author-${author.id}`, false) this.$eventBus.$emit(`searching-author-${author.id}`, false)
} }
} catch (error) {
console.error('Failed to match all authors', error)
this.$toast.error(this.$strings.ToastMatchAllAuthorsFailed)
}
this.processingAuthors = false this.processingAuthors = false
}, },
removeAllIssues() { removeAllIssues() {

View file

@ -19,7 +19,7 @@
<p class="text-xs text-gray-300 italic">{{ Source }}</p> <p class="text-xs text-gray-300 italic">{{ Source }}</p>
</div> </div>
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ $config.version }}</a> <a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xs">Latest: {{ versionData.latestVersion }}</a>
</div> </div>
</div> </div>
</template> </template>

View file

@ -91,6 +91,7 @@ export default {
if (this.page === 'series') return this.$strings.MessageBookshelfNoSeries if (this.page === 'series') return this.$strings.MessageBookshelfNoSeries
if (this.page === 'collections') return this.$strings.MessageBookshelfNoCollections if (this.page === 'collections') return this.$strings.MessageBookshelfNoCollections
if (this.page === 'playlists') return this.$strings.MessageNoUserPlaylists if (this.page === 'playlists') return this.$strings.MessageNoUserPlaylists
if (this.page === 'authors') return this.$strings.MessageNoAuthors
if (this.hasFilter) { if (this.hasFilter) {
if (this.filterName === 'Issues') return this.$strings.MessageNoIssues if (this.filterName === 'Issues') return this.$strings.MessageNoIssues
else if (this.filterName === 'Feed-open') return this.$strings.MessageBookshelfNoRSSFeeds else if (this.filterName === 'Feed-open') return this.$strings.MessageBookshelfNoRSSFeeds
@ -111,6 +112,12 @@ export default {
seriesFilterBy() { seriesFilterBy() {
return this.$store.getters['user/getUserSetting']('seriesFilterBy') return this.$store.getters['user/getUserSetting']('seriesFilterBy')
}, },
authorSortBy() {
return this.$store.getters['user/getUserSetting']('authorSortBy')
},
authorSortDesc() {
return !!this.$store.getters['user/getUserSetting']('authorSortDesc')
},
orderBy() { orderBy() {
return this.$store.getters['user/getUserSetting']('orderBy') return this.$store.getters['user/getUserSetting']('orderBy')
}, },
@ -217,6 +224,8 @@ export default {
this.$store.commit('globals/setEditCollection', entity) this.$store.commit('globals/setEditCollection', entity)
} else if (this.entityName === 'playlists') { } else if (this.entityName === 'playlists') {
this.$store.commit('globals/setEditPlaylist', entity) this.$store.commit('globals/setEditPlaylist', entity)
} else if (this.entityName === 'authors') {
this.$store.commit('globals/showEditAuthorModal', entity)
} }
}, },
clearSelectedEntities() { clearSelectedEntities() {
@ -457,6 +466,9 @@ export default {
if (this.collapseBookSeries) { if (this.collapseBookSeries) {
searchParams.set('collapseseries', 1) searchParams.set('collapseseries', 1)
} }
} else if (this.page === 'authors') {
searchParams.set('sort', this.authorSortBy)
searchParams.set('desc', this.authorSortDesc ? 1 : 0)
} else { } else {
if (this.filterBy && this.filterBy !== 'all') { if (this.filterBy && this.filterBy !== 'all') {
searchParams.set('filter', this.filterBy) searchParams.set('filter', this.filterBy)
@ -601,6 +613,34 @@ export default {
this.executeRebuild() this.executeRebuild()
} }
}, },
authorAdded(author) {
if (this.entityName !== 'authors') return
console.log(`[LazyBookshelf] authorAdded ${author.id}`, author)
this.resetEntities()
},
authorUpdated(author) {
if (this.entityName !== 'authors') return
console.log(`[LazyBookshelf] authorUpdated ${author.id}`, author)
const indexOf = this.entities.findIndex((ent) => ent && ent.id === author.id)
if (indexOf >= 0) {
this.entities[indexOf] = author
if (this.entityComponentRefs[indexOf]) {
this.entityComponentRefs[indexOf].setEntity(author)
}
}
},
authorRemoved(author) {
if (this.entityName !== 'authors') return
console.log(`[LazyBookshelf] authorRemoved ${author.id}`, author)
const indexOf = this.entities.findIndex((ent) => ent && ent.id === author.id)
if (indexOf >= 0) {
this.entities = this.entities.filter((ent) => ent.id !== author.id)
this.totalEntities--
this.$eventBus.$emit('bookshelf-total-entities', this.totalEntities)
this.executeRebuild()
}
},
shareOpen(mediaItemShare) { shareOpen(mediaItemShare) {
if (this.entityName === 'items' || this.entityName === 'series-books') { if (this.entityName === 'items' || this.entityName === 'series-books') {
var indexOf = this.entities.findIndex((ent) => ent?.media?.id === mediaItemShare.mediaItemId) var indexOf = this.entities.findIndex((ent) => ent?.media?.id === mediaItemShare.mediaItemId)
@ -727,6 +767,9 @@ export default {
this.$root.socket.on('playlist_added', this.playlistAdded) this.$root.socket.on('playlist_added', this.playlistAdded)
this.$root.socket.on('playlist_updated', this.playlistUpdated) this.$root.socket.on('playlist_updated', this.playlistUpdated)
this.$root.socket.on('playlist_removed', this.playlistRemoved) this.$root.socket.on('playlist_removed', this.playlistRemoved)
this.$root.socket.on('author_added', this.authorAdded)
this.$root.socket.on('author_updated', this.authorUpdated)
this.$root.socket.on('author_removed', this.authorRemoved)
this.$root.socket.on('share_open', this.shareOpen) this.$root.socket.on('share_open', this.shareOpen)
this.$root.socket.on('share_closed', this.shareClosed) this.$root.socket.on('share_closed', this.shareClosed)
} else { } else {
@ -756,6 +799,9 @@ export default {
this.$root.socket.off('playlist_added', this.playlistAdded) this.$root.socket.off('playlist_added', this.playlistAdded)
this.$root.socket.off('playlist_updated', this.playlistUpdated) this.$root.socket.off('playlist_updated', this.playlistUpdated)
this.$root.socket.off('playlist_removed', this.playlistRemoved) this.$root.socket.off('playlist_removed', this.playlistRemoved)
this.$root.socket.off('author_added', this.authorAdded)
this.$root.socket.off('author_updated', this.authorUpdated)
this.$root.socket.off('author_removed', this.authorRemoved)
this.$root.socket.off('share_open', this.shareOpen) this.$root.socket.off('share_open', this.shareOpen)
this.$root.socket.off('share_closed', this.shareClosed) this.$root.socket.off('share_closed', this.shareClosed)
} else { } else {

View file

@ -167,7 +167,7 @@ export default {
}, },
podcastAuthor() { podcastAuthor() {
if (!this.isPodcast) return null if (!this.isPodcast) return null
return this.mediaMetadata.author || 'Unknown' return this.mediaMetadata.author || this.$strings.LabelUnknown
}, },
hasNextItemInQueue() { hasNextItemInQueue() {
return this.currentPlayerQueueIndex < this.playerQueueItems.length - 1 return this.currentPlayerQueueIndex < this.playerQueueItems.length - 1
@ -251,7 +251,7 @@ export default {
sleepTimerEnd() { sleepTimerEnd() {
this.clearSleepTimer() this.clearSleepTimer()
this.playerHandler.pause() this.playerHandler.pause()
this.$toast.info('Sleep Timer Done.. zZzzZz') this.$toast.info(this.$strings.ToastSleepTimerDone)
}, },
cancelSleepTimer() { cancelSleepTimer() {
this.showSleepTimerModal = false this.showSleepTimerModal = false
@ -525,7 +525,7 @@ export default {
}, },
showFailedProgressSyncs() { showFailedProgressSyncs() {
if (!isNaN(this.syncFailedToast)) this.$toast.dismiss(this.syncFailedToast) if (!isNaN(this.syncFailedToast)) this.$toast.dismiss(this.syncFailedToast)
this.syncFailedToast = this.$toast('Progress is not being synced. Restart playback', { timeout: false, type: 'error' }) this.syncFailedToast = this.$toast(this.$strings.ToastProgressIsNotBeingSynced, { timeout: false, type: 'error' })
}, },
sessionClosedEvent(sessionId) { sessionClosedEvent(sessionId) {
if (this.playerHandler.currentSessionId === sessionId) { if (this.playerHandler.currentSessionId === sessionId) {

View file

@ -58,7 +58,7 @@
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" /> <div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
</nuxt-link> </nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'"> <nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
<svg class="w-6 h-6" viewBox="0 0 24 24"> <svg class="w-6 h-6" viewBox="0 0 24 24">
<path <path
fill="currentColor" fill="currentColor"
@ -180,7 +180,7 @@ export default {
return this.$route.name === 'library-library-series-id' || this.paramId === 'series' return this.$route.name === 'library-library-series-id' || this.paramId === 'series'
}, },
isAuthorsPage() { isAuthorsPage() {
return this.$route.name === 'library-library-authors' return this.libraryBookshelfPage && this.paramId === 'authors'
}, },
isNarratorsPage() { isNarratorsPage() {
return this.$route.name === 'library-library-narrators' return this.$route.name === 'library-library-narrators'

View file

@ -1,6 +1,6 @@
<template> <template>
<div :style="{ minWidth: cardWidth + 'px', maxWidth: cardWidth + 'px' }"> <div class="pb-3e" :style="{ minWidth: cardWidth + 'px', maxWidth: cardWidth + 'px' }">
<nuxt-link :to="`/author/${author.id}`"> <nuxt-link :to="`/author/${author?.id}`">
<div cy-id="card" @mouseover="mouseover" @mouseleave="mouseleave"> <div cy-id="card" @mouseover="mouseover" @mouseleave="mouseleave">
<div cy-id="imageArea" :style="{ height: cardHeight + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden"> <div cy-id="imageArea" :style="{ height: cardHeight + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
<!-- Image or placeholder --> <!-- Image or placeholder -->
@ -40,7 +40,7 @@
<script> <script>
export default { export default {
props: { props: {
author: { authorMount: {
type: Object, type: Object,
default: () => {} default: () => {}
}, },
@ -57,7 +57,8 @@ export default {
data() { data() {
return { return {
searching: false, searching: false,
isHovering: false isHovering: false,
author: null
} }
}, },
computed: { computed: {
@ -68,34 +69,37 @@ export default {
return this.height * this.sizeMultiplier return this.height * this.sizeMultiplier
}, },
userToken() { userToken() {
return this.$store.getters['user/getToken'] return this.store.getters['user/getToken']
}, },
_author() { _author() {
return this.author || {} return this.author || {}
}, },
authorId() { authorId() {
return this._author.id return this._author?.id || ''
}, },
name() { name() {
return this._author.name || '' return this._author?.name || ''
}, },
asin() { asin() {
return this._author.asin || '' return this._author?.asin || ''
}, },
numBooks() { numBooks() {
return this._author.numBooks || 0 return this._author?.numBooks || 0
},
store() {
return this.$store || this.$nuxt.$store
}, },
userCanUpdate() { userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate'] return this.store.getters['user/getUserCanUpdate']
}, },
currentLibraryId() { currentLibraryId() {
return this.$store.state.libraries.currentLibraryId return this.store.state.libraries.currentLibraryId
}, },
libraryProvider() { libraryProvider() {
return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google' return this.store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google'
}, },
sizeMultiplier() { sizeMultiplier() {
return this.$store.getters['user/getSizeMultiplier'] return this.store.getters['user/getSizeMultiplier']
} }
}, },
methods: { methods: {
@ -121,24 +125,54 @@ export default {
return null return null
}) })
if (!response) { if (!response) {
this.$toast.error(`Author ${this.name} not found`) this.$toast.error(this.$getString('ToastAuthorNotFound', [this.name]))
} else if (response.updated) { } else if (response.updated) {
if (response.author.imagePath) this.$toast.success(`Author ${response.author.name} was updated`) if (response.author.imagePath) {
else this.$toast.success(`Author ${response.author.name} was updated (no image found)`) this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
} else { } else {
this.$toast.info(`No updates were made for Author ${response.author.name}`) this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
}
} else {
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
} }
this.searching = false this.searching = false
}, },
setSearching(isSearching) { setSearching(isSearching) {
this.searching = isSearching this.searching = isSearching
},
setEntity(author) {
this.removeListeners()
this.author = author
this.addListeners()
},
addListeners() {
if (this.author) {
this.$eventBus.$on(`searching-author-${this.authorId}`, this.setSearching)
} }
}, },
removeListeners() {
if (this.author) {
this.$eventBus.$off(`searching-author-${this.authorId}`, this.setSearching)
}
},
destroy() {
// destroy the vue listeners, etc
this.$destroy()
// remove the element from the DOM
if (this.$el && this.$el.parentNode) {
this.$el.parentNode.removeChild(this.$el)
} else if (this.$el && this.$el.remove) {
this.$el.remove()
}
},
setSelectionMode(val) {}
},
mounted() { mounted() {
this.$eventBus.$on(`searching-author-${this.authorId}`, this.setSearching) if (this.authorMount) this.setEntity(this.authorMount)
}, },
beforeDestroy() { beforeDestroy() {
this.$eventBus.$off(`searching-author-${this.authorId}`, this.setSearching) this.removeListeners()
} }
} }
</script> </script>

View file

@ -325,7 +325,7 @@ export default {
}, },
displaySubtitle() { displaySubtitle() {
if (!this.libraryItem) return '\u00A0' if (!this.libraryItem) return '\u00A0'
if (this.collapsedSeries) return this.collapsedSeries.numBooks === 1 ? '1 book' : `${this.collapsedSeries.numBooks} books` if (this.collapsedSeries) return `${this.collapsedSeries.numBooks} ${this.$strings.LabelBooks}`
if (this.mediaMetadata.subtitle) return this.mediaMetadata.subtitle if (this.mediaMetadata.subtitle) return this.mediaMetadata.subtitle
if (this.mediaMetadata.seriesName) return this.mediaMetadata.seriesName if (this.mediaMetadata.seriesName) return this.mediaMetadata.seriesName
return '' return ''
@ -816,7 +816,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove series from home', error) console.error('Failed to remove series from home', error)
this.$toast.error(this.$strings.ToastFailedToUpdateUser) this.$toast.error(this.$strings.ToastFailedToUpdate)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@ -834,7 +834,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to hide item from home', error) console.error('Failed to hide item from home', error)
this.$toast.error(this.$strings.ToastFailedToUpdateUser) this.$toast.error(this.$strings.ToastFailedToUpdate)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false

View file

@ -130,7 +130,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update notification', error) console.error('Failed to update notification', error)
this.$toast.error(this.$strings.ToastNotificationUpdateFailed) this.$toast.error(this.$strings.ToastFailedToUpdate)
}) })
.finally(() => { .finally(() => {
this.enabling = false this.enabling = false

View file

@ -9,7 +9,7 @@
<span v-else class="material-symbols" style="font-size: 1.2rem">close</span> <span v-else class="material-symbols" style="font-size: 1.2rem">close</span>
</div> </div>
</div> </div>
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-full max-w-64 sm:max-w-80 sm:w-80 bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu"> <div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-full max-w-64 sm:max-w-80 sm:w-80 bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu" @mousedown.stop.prevent>
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label"> <ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<li v-if="isTyping" class="py-2 px-2"> <li v-if="isTyping" class="py-2 px-2">
<p>{{ $strings.MessageThinking }}</p> <p>{{ $strings.MessageThinking }}</p>
@ -157,7 +157,7 @@ export default {
clearTimeout(this.focusTimeout) clearTimeout(this.focusTimeout)
this.focusTimeout = setTimeout(() => { this.focusTimeout = setTimeout(() => {
this.showMenu = false this.showMenu = false
}, 200) }, 100)
}, },
async runSearch(value) { async runSearch(value) {
this.lastSearch = value this.lastSearch = value

View file

@ -189,6 +189,12 @@ export default {
value: 'publishers', value: 'publishers',
sublist: true sublist: true
}, },
{
text: this.$strings.LabelPublishedDecade,
textPlural: this.$strings.LabelPublishedDecades,
value: 'publishedDecades',
sublist: true
},
{ {
text: this.$strings.LabelLanguage, text: this.$strings.LabelLanguage,
textPlural: this.$strings.LabelLanguages, textPlural: this.$strings.LabelLanguages,
@ -338,6 +344,9 @@ export default {
publishers() { publishers() {
return this.filterData.publishers || [] return this.filterData.publishers || []
}, },
publishedDecades() {
return this.filterData.publishedDecades || []
},
progress() { progress() {
return [ return [
{ {
@ -404,21 +413,17 @@ export default {
id: 'isbn', id: 'isbn',
name: 'ISBN' name: 'ISBN'
}, },
{
id: 'subtitle',
name: this.$strings.LabelSubtitle
},
{ {
id: 'authors', id: 'authors',
name: this.$strings.LabelAuthor name: this.$strings.LabelAuthor
}, },
{ {
id: 'publishedYear', id: 'chapters',
name: this.$strings.LabelPublishYear name: this.$strings.LabelChapters
}, },
{ {
id: 'series', id: 'cover',
name: this.$strings.LabelSeries name: this.$strings.LabelCover
}, },
{ {
id: 'description', id: 'description',
@ -429,24 +434,32 @@ export default {
name: this.$strings.LabelGenres name: this.$strings.LabelGenres
}, },
{ {
id: 'tags', id: 'language',
name: this.$strings.LabelTags name: this.$strings.LabelLanguage
}, },
{ {
id: 'narrators', id: 'narrators',
name: this.$strings.LabelNarrator name: this.$strings.LabelNarrator
}, },
{
id: 'publishedYear',
name: this.$strings.LabelPublishYear
},
{ {
id: 'publisher', id: 'publisher',
name: this.$strings.LabelPublisher name: this.$strings.LabelPublisher
}, },
{ {
id: 'language', id: 'series',
name: this.$strings.LabelLanguage name: this.$strings.LabelSeries
}, },
{ {
id: 'cover', id: 'subtitle',
name: this.$strings.LabelCover name: this.$strings.LabelSubtitle
},
{
id: 'tags',
name: this.$strings.LabelTags
} }
] ]
}, },

View file

@ -1,9 +1,9 @@
<template> <template>
<div ref="wrapper" class="relative ml-4 sm:ml-8" v-click-outside="clickOutside"> <div ref="wrapper" class="relative ml-4 sm:ml-8" v-click-outside="clickOutside">
<div class="flex items-center justify-center text-gray-300 cursor-pointer h-full" @mousedown.prevent @mouseup.prevent @click="setShowMenu(true)"> <div class="flex items-center justify-center text-gray-300 cursor-pointer h-full" @mousedown.prevent @mouseup.prevent @click="setShowMenu(true)">
<span class="font-mono uppercase text-gray-200 text-sm sm:text-base">{{ playbackRate.toFixed(1) }}<span class="text-base">x</span></span> <span class="text-gray-200 text-sm sm:text-base">{{ playbackRate.toFixed(1) }}<span class="text-base">x</span></span>
</div> </div>
<div v-show="showMenu" class="absolute -top-20 z-20 bg-bg border-black-200 border shadow-xl rounded-lg" :style="{ left: menuLeft + 'px' }"> <div v-show="showMenu" class="absolute -top-[5.5rem] z-20 bg-bg border-black-200 border shadow-xl rounded-lg" :style="{ left: menuLeft + 'px' }">
<div class="absolute -bottom-1.5 right-0 w-full flex justify-center" :style="{ left: arrowLeft + 'px' }"> <div class="absolute -bottom-1.5 right-0 w-full flex justify-center" :style="{ left: arrowLeft + 'px' }">
<div class="arrow-down" /> <div class="arrow-down" />
</div> </div>
@ -11,12 +11,12 @@
<template v-for="rate in rates"> <template v-for="rate in rates">
<div :key="rate" class="h-full border-black-300 w-11 cursor-pointer border rounded-sm" :class="value === rate ? 'bg-black-100' : 'hover:bg-black hover:bg-opacity-10'" style="min-width: 44px; max-width: 44px" @click="set(rate)"> <div :key="rate" class="h-full border-black-300 w-11 cursor-pointer border rounded-sm" :class="value === rate ? 'bg-black-100' : 'hover:bg-black hover:bg-opacity-10'" style="min-width: 44px; max-width: 44px" @click="set(rate)">
<div class="w-full h-full flex justify-center items-center"> <div class="w-full h-full flex justify-center items-center">
<p class="text-xs text-center font-mono">{{ rate }}<span class="text-sm">x</span></p> <p class="text-xs text-center">{{ rate }}<span class="text-sm">x</span></p>
</div> </div>
</div> </div>
</template> </template>
</div> </div>
<div class="w-full py-1 px-4"> <div class="w-full py-1 px-1">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<ui-icon-btn :disabled="!canDecrement" icon="remove" @click="decrement" /> <ui-icon-btn :disabled="!canDecrement" icon="remove" @click="decrement" />
<p class="px-2 text-2xl sm:text-3xl">{{ playbackRate }}<span class="text-2xl">x</span></p> <p class="px-2 text-2xl sm:text-3xl">{{ playbackRate }}<span class="text-2xl">x</span></p>
@ -41,7 +41,7 @@ export default {
currentPlaybackRate: 0, currentPlaybackRate: 0,
MIN_SPEED: 0.5, MIN_SPEED: 0.5,
MAX_SPEED: 10, MAX_SPEED: 10,
menuLeft: -92, menuLeft: -96,
arrowLeft: 0 arrowLeft: 0
} }
}, },
@ -89,9 +89,9 @@ export default {
if (boundingBox.left + 110 > window.innerWidth - 10) { if (boundingBox.left + 110 > window.innerWidth - 10) {
this.menuLeft = window.innerWidth - 230 - boundingBox.left this.menuLeft = window.innerWidth - 230 - boundingBox.left
this.arrowLeft = Math.abs(this.menuLeft) - 92 this.arrowLeft = Math.abs(this.menuLeft) - 96
} else { } else {
this.menuLeft = -92 this.menuLeft = -96
this.arrowLeft = 0 this.arrowLeft = 0
} }
}, },

View file

@ -4,10 +4,10 @@
<span class="material-symbols text-2xl sm:text-3xl">{{ volumeIcon }}</span> <span class="material-symbols text-2xl sm:text-3xl">{{ volumeIcon }}</span>
</button> </button>
<transition name="menux"> <transition name="menux">
<div v-show="isOpen" class="volumeMenu h-6 absolute bottom-2 w-28 px-2 bg-bg shadow-sm rounded-lg" style="left: -116px"> <div v-show="isOpen" class="volumeMenu h-28 absolute bottom-2 w-6 py-2 bg-bg shadow-sm rounded-lg" style="top: -116px">
<div ref="volumeTrack" class="h-1 w-full bg-gray-500 my-2.5 relative cursor-pointer rounded-full" @mousedown="mousedownTrack" @click="clickVolumeTrack"> <div ref="volumeTrack" class="w-1 h-full bg-gray-500 mx-2.5 relative cursor-pointer rounded-full" @mousedown="mousedownTrack" @click="clickVolumeTrack">
<div class="bg-gray-100 h-full absolute left-0 top-0 pointer-events-none rounded-full" :style="{ width: volume * trackWidth + 'px' }" /> <div class="bg-gray-100 w-full absolute left-0 bottom-0 pointer-events-none rounded-full" :style="{ height: volume * trackHeight + 'px' }" />
<div class="w-2.5 h-2.5 bg-white shadow-sm rounded-full absolute pointer-events-none" :class="isDragging ? 'transform scale-125 origin-center' : ''" :style="{ left: cursorLeft + 'px', top: '-3px' }" /> <div class="w-2.5 h-2.5 bg-white shadow-sm rounded-full absolute pointer-events-none" :class="isDragging ? 'transform scale-125 origin-center' : ''" :style="{ bottom: cursorBottom + 'px', left: '-3px' }" />
</div> </div>
</div> </div>
</transition> </transition>
@ -24,10 +24,10 @@ export default {
isOpen: false, isOpen: false,
isDragging: false, isDragging: false,
isHovering: false, isHovering: false,
posX: 0, posY: 0,
lastValue: 0.5, lastValue: 0.5,
isMute: false, isMute: false,
trackWidth: 112 - 20, trackHeight: 112 - 20,
openTimeout: null openTimeout: null
} }
}, },
@ -45,9 +45,9 @@ export default {
this.$emit('input', val) this.$emit('input', val)
} }
}, },
cursorLeft() { cursorBottom() {
var left = this.trackWidth * this.volume var bottom = this.trackHeight * this.volume
return left - 3 return bottom - 3
}, },
volumeIcon() { volumeIcon() {
if (this.volume <= 0) return 'volume_mute' if (this.volume <= 0) return 'volume_mute'
@ -89,17 +89,10 @@ export default {
}, 600) }, 600)
}, },
mousemove(e) { mousemove(e) {
var diff = this.posX - e.x var diff = this.posY - e.y
this.posX = e.x this.posY = e.y
var volShift = 0 var volShift = diff / this.trackHeight
if (diff < 0) { var newVol = this.volume + volShift
// Volume up
volShift = diff / this.trackWidth
} else {
// volume down
volShift = diff / this.trackWidth
}
var newVol = this.volume - volShift
newVol = Math.min(Math.max(0, newVol), 1) newVol = Math.min(Math.max(0, newVol), 1)
this.volume = newVol this.volume = newVol
e.preventDefault() e.preventDefault()
@ -113,8 +106,8 @@ export default {
}, },
mousedownTrack(e) { mousedownTrack(e) {
this.isDragging = true this.isDragging = true
this.posX = e.x this.posY = e.y
var vol = e.offsetX / this.trackWidth var vol = 1 - e.offsetY / this.trackHeight
vol = Math.min(Math.max(vol, 0), 1) vol = Math.min(Math.max(vol, 0), 1)
this.volume = vol this.volume = vol
document.body.addEventListener('mousemove', this.mousemove) document.body.addEventListener('mousemove', this.mousemove)
@ -137,7 +130,7 @@ export default {
this.clickVolumeIcon() this.clickVolumeIcon()
}, },
clickVolumeTrack(e) { clickVolumeTrack(e) {
var vol = e.offsetX / this.trackWidth var vol = 1 - e.offsetY / this.trackHeight
vol = Math.min(Math.max(vol, 0), 1) vol = Math.min(Math.max(vol, 0), 1)
this.volume = vol this.volume = vol
} }
@ -147,7 +140,7 @@ export default {
this.isMute = true this.isMute = true
} }
const storageVolume = localStorage.getItem('volume') const storageVolume = localStorage.getItem('volume')
if (storageVolume) { if (storageVolume && !isNaN(storageVolume)) {
this.volume = parseFloat(storageVolume) this.volume = parseFloat(storageVolume)
} }
}, },

View file

@ -56,24 +56,15 @@ export default {
}, },
imgSrc() { imgSrc() {
if (!this.imagePath) return null if (!this.imagePath) return null
if (process.env.NODE_ENV !== 'production') { return `${this.$config.routerBasePath}/api/authors/${this.authorId}/image?ts=${this.updatedAt}`
// Testing
return `http://localhost:3333${this.$config.routerBasePath}/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
}
return `/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
} }
}, },
methods: { methods: {
imageLoaded() { imageLoaded() {
var aspectRatio = 1.25
if (this.$refs.wrapper) {
aspectRatio = this.$refs.wrapper.clientHeight / this.$refs.wrapper.clientWidth
}
if (this.$refs.img) { if (this.$refs.img) {
var { naturalWidth, naturalHeight } = this.$refs.img var { naturalWidth, naturalHeight } = this.$refs.img
var imgAr = naturalHeight / naturalWidth var imgAr = naturalHeight / naturalWidth
var arDiff = Math.abs(imgAr - aspectRatio) if (imgAr < 0.5 || imgAr > 2) {
if (arDiff > 0.15) {
this.showCoverBg = true this.showCoverBg = true
} else { } else {
this.showCoverBg = false this.showCoverBg = false

View file

@ -69,6 +69,15 @@
</div> </div>
</div> </div>
<div class="flex items-center my-2 max-w-md">
<div class="w-1/2">
<p id="ereader-permissions-toggle">{{ $strings.LabelPermissionsCreateEreader }}</p>
</div>
<div class="w-1/2">
<ui-toggle-switch labeledBy="ereader-permissions-toggle" v-model="newUser.permissions.createEreader" />
</div>
</div>
<div class="flex items-center my-2 max-w-md"> <div class="flex items-center my-2 max-w-md">
<div class="w-1/2"> <div class="w-1/2">
<p id="explicit-content-permissions-toggle">{{ $strings.LabelPermissionsAccessExplicitContent }}</p> <p id="explicit-content-permissions-toggle">{{ $strings.LabelPermissionsAccessExplicitContent }}</p>
@ -296,7 +305,7 @@ export default {
.then((data) => { .then((data) => {
this.processing = false this.processing = false
if (data.error) { if (data.error) {
this.$toast.error(`${this.$strings.ToastAccountUpdateFailed}: ${data.error}`) this.$toast.error(`${this.$strings.ToastFailedToUpdate}: ${data.error}`)
} else { } else {
console.log('Account updated', data.user) console.log('Account updated', data.user)
@ -313,7 +322,7 @@ export default {
this.processing = false this.processing = false
console.error('Failed to update account', error) console.error('Failed to update account', error)
var errMsg = error.response ? error.response.data || '' : '' var errMsg = error.response ? error.response.data || '' : ''
this.$toast.error(errMsg || this.$strings.ToastFailedToUpdateAccount) this.$toast.error(errMsg || this.$strings.ToastFailedToUpdate)
}) })
}, },
submitCreateAccount() { submitCreateAccount() {
@ -354,7 +363,8 @@ export default {
accessExplicitContent: type === 'admin', accessExplicitContent: type === 'admin',
accessAllLibraries: true, accessAllLibraries: true,
accessAllTags: true, accessAllTags: true,
selectedTagsNotAccessible: false selectedTagsNotAccessible: false,
createEreader: type === 'admin'
} }
}, },
init() { init() {
@ -387,7 +397,8 @@ export default {
accessAllLibraries: true, accessAllLibraries: true,
accessAllTags: true, accessAllTags: true,
accessExplicitContent: false, accessExplicitContent: false,
selectedTagsNotAccessible: false selectedTagsNotAccessible: false,
createEreader: false
}, },
librariesAccessible: [], librariesAccessible: [],
itemTagsSelected: [] itemTagsSelected: []

View file

@ -116,10 +116,10 @@ export default {
libraryItemIds: this.selectedBookIds libraryItemIds: this.selectedBookIds
}) })
.then(() => { .then(() => {
this.$toast.info('Batch quick match of ' + this.selectedBookIds.length + ' books started!') this.$toast.info(this.$getString('ToastBatchQuickMatchStarted', [this.selectedBookIds.length]))
}) })
.catch((error) => { .catch((error) => {
this.$toast.error('Batch quick match failed') this.$toast.error(this.$strings.ToastBatchQuickMatchFailed)
console.error('Failed to batch quick match', error) console.error('Failed to batch quick match', error)
}) })
.finally(() => { .finally(() => {

View file

@ -110,7 +110,7 @@ export default {
this.$toast.success(this.$strings.ToastBookmarkUpdateSuccess) this.$toast.success(this.$strings.ToastBookmarkUpdateSuccess)
}) })
.catch((error) => { .catch((error) => {
this.$toast.error(this.$strings.ToastBookmarkUpdateFailed) this.$toast.error(this.$strings.ToastFailedToUpdate)
console.error(error) console.error(error)
}) })
this.show = false this.show = false

View file

@ -112,11 +112,11 @@ export default {
return this.$store.state.user.user return this.$store.state.user.user
}, },
demoShareUrl() { demoShareUrl() {
return `${window.origin}/share/${this.newShareSlug}` return `${window.origin}${this.$config.routerBasePath}/share/${this.newShareSlug}`
}, },
currentShareUrl() { currentShareUrl() {
if (!this.currentShare) return '' if (!this.currentShare) return ''
return `${window.origin}/share/${this.currentShare.slug}` return `${window.origin}${this.$config.routerBasePath}/share/${this.currentShare.slug}`
}, },
currentShareTimeRemaining() { currentShareTimeRemaining() {
if (!this.currentShare) return 'Error' if (!this.currentShare) return 'Error'

View file

@ -148,7 +148,7 @@ export default {
var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => { var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => {
console.error('Failed', error) console.error('Failed', error)
const errorMsg = error.response ? error.response.data : null const errorMsg = error.response ? error.response.data : null
this.$toast.error(errorMsg || this.$strings.ToastAuthorUpdateFailed) this.$toast.error(errorMsg || this.$strings.ToastFailedToUpdate)
return null return null
}) })
if (result) { if (result) {

View file

@ -135,7 +135,7 @@ export default {
.catch((error) => { .catch((error) => {
console.error('Failed to update collection', error) console.error('Failed to update collection', error)
this.processing = false this.processing = false
this.$toast.error(this.$strings.ToastCollectionUpdateFailed) this.$toast.error(this.$strings.ToastFailedToUpdate)
}) })
} }
}, },

View file

@ -178,7 +178,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update device', error) console.error('Failed to update device', error)
this.$toast.error(this.$strings.ToastDeviceUpdateFailed) this.$toast.error(this.$strings.ToastFailedToUpdate)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false

View file

@ -0,0 +1,188 @@
<template>
<modals-modal ref="modal" v-model="show" name="ereader-device-edit" :width="800" :height="'unset'" :processing="processing">
<template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
<p class="text-3xl text-white truncate">{{ title }}</p>
</div>
</template>
<form @submit.prevent="submitForm">
<div class="w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300">
<div class="w-full px-3 py-5 md:p-12">
<div class="flex items-center -mx-1 mb-4">
<div class="w-full md:w-1/2 px-1">
<ui-text-input-with-label ref="ereaderNameInput" v-model="newDevice.name" :disabled="processing" :label="$strings.LabelName" />
</div>
<div class="w-full md:w-1/2 px-1">
<ui-text-input-with-label ref="ereaderEmailInput" v-model="newDevice.email" :disabled="processing" :label="$strings.LabelEmail" />
</div>
</div>
<div class="flex items-center pt-4">
<div class="flex-grow" />
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
</div>
</div>
</div>
</form>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean,
existingDevices: {
type: Array,
default: () => []
},
ereaderDevice: {
type: Object,
default: () => null
}
},
data() {
return {
processing: false,
newDevice: {
name: '',
email: '',
availabilityOption: 'adminAndUp',
users: []
}
}
},
watch: {
show: {
handler(newVal) {
if (newVal) {
this.init()
}
}
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
user() {
return this.$store.state.user.user
},
title() {
return !this.ereaderDevice ? 'Create Device' : 'Update Device'
}
},
methods: {
submitForm() {
this.$refs.ereaderNameInput.blur()
this.$refs.ereaderEmailInput.blur()
if (!this.newDevice.name?.trim() || !this.newDevice.email?.trim()) {
this.$toast.error(this.$strings.ToastNameEmailRequired)
return
}
this.newDevice.name = this.newDevice.name.trim()
this.newDevice.email = this.newDevice.email.trim()
// Only catches duplicate names for the current user
// Duplicates with other users caught on server side
if (!this.ereaderDevice) {
if (this.existingDevices.some((d) => d.name === this.newDevice.name)) {
this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
return
}
this.submitCreate()
} else {
if (this.ereaderDevice.name !== this.newDevice.name && this.existingDevices.some((d) => d.name === this.newDevice.name)) {
this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
return
}
this.submitUpdate()
}
},
submitUpdate() {
this.processing = true
const existingDevicesWithoutThisOne = this.existingDevices.filter((d) => d.name !== this.ereaderDevice.name)
const payload = {
ereaderDevices: [
...existingDevicesWithoutThisOne,
{
...this.newDevice
}
]
}
this.$axios
.$post(`/api/me/ereader-devices`, payload)
.then((data) => {
this.$emit('update', data.ereaderDevices)
this.show = false
})
.catch((error) => {
console.error('Failed to update device', error)
if (error.response?.data?.toLowerCase().includes('duplicate')) {
this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
} else {
this.$toast.error(this.$strings.ToastDeviceAddFailed)
}
})
.finally(() => {
this.processing = false
})
},
submitCreate() {
this.processing = true
const payload = {
ereaderDevices: [
...this.existingDevices,
{
...this.newDevice
}
]
}
this.$axios
.$post('/api/me/ereader-devices', payload)
.then((data) => {
this.$emit('update', data.ereaderDevices || [])
this.show = false
})
.catch((error) => {
console.error('Failed to add device', error)
if (error.response?.data?.toLowerCase().includes('duplicate')) {
this.$toast.error(this.$strings.ToastDeviceNameAlreadyExists)
} else {
this.$toast.error(this.$strings.ToastDeviceAddFailed)
}
})
.finally(() => {
this.processing = false
})
},
init() {
if (this.ereaderDevice) {
this.newDevice.name = this.ereaderDevice.name
this.newDevice.email = this.ereaderDevice.email
this.newDevice.availabilityOption = this.ereaderDevice.availabilityOption || 'specificUsers'
this.newDevice.users = this.ereaderDevice.users || [this.user.id]
} else {
this.newDevice.name = ''
this.newDevice.email = ''
this.newDevice.availabilityOption = 'specificUsers'
this.newDevice.users = [this.user.id]
}
}
},
mounted() {}
}
</script>

View file

@ -6,7 +6,7 @@
<ui-text-input-with-label ref="maxEpisodesInput" v-model="maxEpisodesToDownload" :disabled="checkingNewEpisodes" type="number" :label="$strings.LabelLimit" class="w-16 mr-2" input-class="h-10"> <ui-text-input-with-label ref="maxEpisodesInput" v-model="maxEpisodesToDownload" :disabled="checkingNewEpisodes" type="number" :label="$strings.LabelLimit" class="w-16 mr-2" input-class="h-10">
<div class="flex -mb-0.5"> <div class="flex -mb-0.5">
<p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': checkingNewEpisodes }">{{ $strings.LabelLimit }}</p> <p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': checkingNewEpisodes }">{{ $strings.LabelLimit }}</p>
<ui-tooltip direction="top" text="Max # of episodes to download. Use 0 for unlimited."> <ui-tooltip direction="top" :text="$strings.LabelMaxEpisodesToDownload">
<span class="material-symbols text-base">info</span> <span class="material-symbols text-base">info</span>
</ui-tooltip> </ui-tooltip>
</div> </div>
@ -99,7 +99,7 @@ export default {
if (this.maxEpisodesToDownload < 0) { if (this.maxEpisodesToDownload < 0) {
this.maxEpisodesToDownload = 3 this.maxEpisodesToDownload = 3
this.$toast.error('Invalid max episodes to download') this.$toast.error(this.$strings.ToastInvalidMaxEpisodesToDownload)
return return
} }
@ -120,9 +120,9 @@ export default {
.then((response) => { .then((response) => {
if (response.episodes && response.episodes.length) { if (response.episodes && response.episodes.length) {
console.log('New episodes', response.episodes.length) console.log('New episodes', response.episodes.length)
this.$toast.success(`${response.episodes.length} new episodes found!`) this.$toast.success(this.$getString('ToastNewEpisodesFound', [response.episodes.length]))
} else { } else {
this.$toast.info('No new episodes found') this.$toast.info(this.$strings.ToastNoNewEpisodesFound)
} }
this.checkingNewEpisodes = false this.checkingNewEpisodes = false
}) })

View file

@ -60,7 +60,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.title" :disabled="!selectedMatchUsage.title" :label="$strings.LabelTitle" /> <ui-text-input-with-label v-model="selectedMatch.title" :disabled="!selectedMatchUsage.title" :label="$strings.LabelTitle" />
<p v-if="mediaMetadata.title" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.title" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('title', mediaMetadata.title)">{{ mediaMetadata.title || '' }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('title', mediaMetadata.title)">{{ mediaMetadata.title || '' }}</a>
</p> </p>
</div> </div>
</div> </div>
@ -69,7 +69,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.subtitle" :disabled="!selectedMatchUsage.subtitle" :label="$strings.LabelSubtitle" /> <ui-text-input-with-label v-model="selectedMatch.subtitle" :disabled="!selectedMatchUsage.subtitle" :label="$strings.LabelSubtitle" />
<p v-if="mediaMetadata.subtitle" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.subtitle" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('subtitle', mediaMetadata.subtitle)">{{ mediaMetadata.subtitle }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('subtitle', mediaMetadata.subtitle)">{{ mediaMetadata.subtitle }}</a>
</p> </p>
</div> </div>
</div> </div>
@ -78,7 +78,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" :label="$strings.LabelAuthor" /> <ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" :label="$strings.LabelAuthor" />
<p v-if="mediaMetadata.authorName" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.authorName" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('author', mediaMetadata.authorName)">{{ mediaMetadata.authorName }}</a> {{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('author', mediaMetadata.authorName)">{{ mediaMetadata.authorName }}</a>
</p> </p>
</div> </div>
</div> </div>
@ -87,7 +87,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-multi-select v-model="selectedMatch.narrator" :items="narrators" :disabled="!selectedMatchUsage.narrator" :label="$strings.LabelNarrators" /> <ui-multi-select v-model="selectedMatch.narrator" :items="narrators" :disabled="!selectedMatchUsage.narrator" :label="$strings.LabelNarrators" />
<p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('narrator', mediaMetadata.narrators)">{{ mediaMetadata.narratorName }}</a> {{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('narrator', mediaMetadata.narrators)">{{ mediaMetadata.narratorName }}</a>
</p> </p>
</div> </div>
</div> </div>
@ -96,7 +96,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" :label="$strings.LabelDescription" /> <ui-textarea-with-label v-model="selectedMatch.description" :rows="3" :disabled="!selectedMatchUsage.description" :label="$strings.LabelDescription" />
<p v-if="mediaMetadata.description" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.description" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('description', mediaMetadata.description)">{{ mediaMetadata.description.substr(0, 100) + (mediaMetadata.description.length > 100 ? '...' : '') }}</a> {{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('description', mediaMetadata.description)">{{ mediaMetadata.description.substr(0, 100) + (mediaMetadata.description.length > 100 ? '...' : '') }}</a>
</p> </p>
</div> </div>
</div> </div>
@ -105,7 +105,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" :label="$strings.LabelPublisher" /> <ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" :label="$strings.LabelPublisher" />
<p v-if="mediaMetadata.publisher" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.publisher" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publisher', mediaMetadata.publisher)">{{ mediaMetadata.publisher }}</a> {{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publisher', mediaMetadata.publisher)">{{ mediaMetadata.publisher }}</a>
</p> </p>
</div> </div>
</div> </div>
@ -114,7 +114,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.publishedYear" :disabled="!selectedMatchUsage.publishedYear" :label="$strings.LabelPublishYear" /> <ui-text-input-with-label v-model="selectedMatch.publishedYear" :disabled="!selectedMatchUsage.publishedYear" :label="$strings.LabelPublishYear" />
<p v-if="mediaMetadata.publishedYear" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.publishedYear" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publishedYear', mediaMetadata.publishedYear)">{{ mediaMetadata.publishedYear }}</a> {{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publishedYear', mediaMetadata.publishedYear)">{{ mediaMetadata.publishedYear }}</a>
</p> </p>
</div> </div>
</div> </div>
@ -124,7 +124,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<widgets-series-input-widget v-model="selectedMatch.series" :disabled="!selectedMatchUsage.series" /> <widgets-series-input-widget v-model="selectedMatch.series" :disabled="!selectedMatchUsage.series" />
<p v-if="mediaMetadata.seriesName" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.seriesName" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('series', mediaMetadata.series)">{{ mediaMetadata.seriesName }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('series', mediaMetadata.series)">{{ mediaMetadata.seriesName }}</a>
</p> </p>
</div> </div>
</div> </div>
@ -133,7 +133,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-multi-select v-model="selectedMatch.genres" :items="genres" :disabled="!selectedMatchUsage.genres" :label="$strings.LabelGenres" /> <ui-multi-select v-model="selectedMatch.genres" :items="genres" :disabled="!selectedMatchUsage.genres" :label="$strings.LabelGenres" />
<p v-if="mediaMetadata.genres?.length" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.genres?.length" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('genres', mediaMetadata.genres)">{{ mediaMetadata.genres.join(', ') }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('genres', mediaMetadata.genres)">{{ mediaMetadata.genres.join(', ') }}</a>
</p> </p>
</div> </div>
</div> </div>
@ -142,7 +142,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-multi-select v-model="selectedMatch.tags" :items="tags" :disabled="!selectedMatchUsage.tags" :label="$strings.LabelTags" /> <ui-multi-select v-model="selectedMatch.tags" :items="tags" :disabled="!selectedMatchUsage.tags" :label="$strings.LabelTags" />
<p v-if="media.tags?.length" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="media.tags?.length" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('tags', media.tags)">{{ media.tags.join(', ') }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('tags', media.tags)">{{ media.tags.join(', ') }}</a>
</p> </p>
</div> </div>
</div> </div>
@ -151,7 +151,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.language" :disabled="!selectedMatchUsage.language" :label="$strings.LabelLanguage" /> <ui-text-input-with-label v-model="selectedMatch.language" :disabled="!selectedMatchUsage.language" :label="$strings.LabelLanguage" />
<p v-if="mediaMetadata.language" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.language" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('language', mediaMetadata.language)">{{ mediaMetadata.language }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('language', mediaMetadata.language)">{{ mediaMetadata.language }}</a>
</p> </p>
</div> </div>
</div> </div>
@ -160,7 +160,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.isbn" :disabled="!selectedMatchUsage.isbn" label="ISBN" /> <ui-text-input-with-label v-model="selectedMatch.isbn" :disabled="!selectedMatchUsage.isbn" label="ISBN" />
<p v-if="mediaMetadata.isbn" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.isbn" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('isbn', mediaMetadata.isbn)">{{ mediaMetadata.isbn }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('isbn', mediaMetadata.isbn)">{{ mediaMetadata.isbn }}</a>
</p> </p>
</div> </div>
</div> </div>
@ -169,7 +169,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.asin" :disabled="!selectedMatchUsage.asin" label="ASIN" /> <ui-text-input-with-label v-model="selectedMatch.asin" :disabled="!selectedMatchUsage.asin" label="ASIN" />
<p v-if="mediaMetadata.asin" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.asin" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('asin', mediaMetadata.asin)">{{ mediaMetadata.asin }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('asin', mediaMetadata.asin)">{{ mediaMetadata.asin }}</a>
</p> </p>
</div> </div>
</div> </div>
@ -179,7 +179,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.itunesId" type="number" :disabled="!selectedMatchUsage.itunesId" label="iTunes ID" /> <ui-text-input-with-label v-model="selectedMatch.itunesId" type="number" :disabled="!selectedMatchUsage.itunesId" label="iTunes ID" />
<p v-if="mediaMetadata.itunesId" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.itunesId" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesId', mediaMetadata.itunesId)">{{ mediaMetadata.itunesId }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesId', mediaMetadata.itunesId)">{{ mediaMetadata.itunesId }}</a>
</p> </p>
</div> </div>
</div> </div>
@ -188,7 +188,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.feedUrl" :disabled="!selectedMatchUsage.feedUrl" label="RSS Feed URL" /> <ui-text-input-with-label v-model="selectedMatch.feedUrl" :disabled="!selectedMatchUsage.feedUrl" label="RSS Feed URL" />
<p v-if="mediaMetadata.feedUrl" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.feedUrl" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('feedUrl', mediaMetadata.feedUrl)">{{ mediaMetadata.feedUrl }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('feedUrl', mediaMetadata.feedUrl)">{{ mediaMetadata.feedUrl }}</a>
</p> </p>
</div> </div>
</div> </div>
@ -197,7 +197,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.itunesPageUrl" :disabled="!selectedMatchUsage.itunesPageUrl" label="iTunes Page URL" /> <ui-text-input-with-label v-model="selectedMatch.itunesPageUrl" :disabled="!selectedMatchUsage.itunesPageUrl" label="iTunes Page URL" />
<p v-if="mediaMetadata.itunesPageUrl" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.itunesPageUrl" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesPageUrl', mediaMetadata.itunesPageUrl)">{{ mediaMetadata.itunesPageUrl }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('itunesPageUrl', mediaMetadata.itunesPageUrl)">{{ mediaMetadata.itunesPageUrl }}</a>
</p> </p>
</div> </div>
</div> </div>
@ -206,7 +206,7 @@
<div class="flex-grow ml-4"> <div class="flex-grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.releaseDate" :disabled="!selectedMatchUsage.releaseDate" :label="$strings.LabelReleaseDate" /> <ui-text-input-with-label v-model="selectedMatch.releaseDate" :disabled="!selectedMatchUsage.releaseDate" :label="$strings.LabelReleaseDate" />
<p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60"> <p v-if="mediaMetadata.releaseDate" class="text-xs ml-1 text-white text-opacity-60">
{{ $strings.LabelCurrently }} <a title="Click to use current value" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('releaseDate', mediaMetadata.releaseDate)">{{ mediaMetadata.releaseDate }}</a> {{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('releaseDate', mediaMetadata.releaseDate)">{{ mediaMetadata.releaseDate }}</a>
</p> </p>
</div> </div>
</div> </div>
@ -623,7 +623,7 @@ export default {
this.clearSelectedMatch() this.clearSelectedMatch()
this.$emit('selectTab', 'details') this.$emit('selectTab', 'details')
} else { } else {
this.$toast.error(this.$strings.ToastItemDetailsUpdateFailed) this.$toast.error(this.$strings.ToastFailedToUpdate)
} }
} else { } else {
this.clearSelectedMatch() this.clearSelectedMatch()

View file

@ -2,28 +2,28 @@
<div class="w-full h-full relative"> <div class="w-full h-full relative">
<div id="scheduleWrapper" class="w-full overflow-y-auto px-2 py-4 md:px-6 md:py-6"> <div id="scheduleWrapper" class="w-full overflow-y-auto px-2 py-4 md:px-6 md:py-6">
<template v-if="!feedUrl"> <template v-if="!feedUrl">
<widgets-alert type="warning" class="text-base mb-4">No RSS feed URL is set for this podcast</widgets-alert> <widgets-alert type="warning" class="text-base mb-4">{{ $strings.ToastPodcastNoRssFeed }}</widgets-alert>
</template> </template>
<template v-if="feedUrl || autoDownloadEpisodes"> <template v-if="feedUrl || autoDownloadEpisodes">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<p class="text-base md:text-xl font-semibold">Schedule Automatic Episode Downloads</p> <p class="text-base md:text-xl font-semibold">{{ $strings.HeaderScheduleEpisodeDownloads }}</p>
<ui-checkbox v-model="enableAutoDownloadEpisodes" label="Enable" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" /> <ui-checkbox v-model="enableAutoDownloadEpisodes" :label="$strings.LabelEnable" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" />
</div> </div>
<div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2"> <div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2">
<ui-text-input ref="maxEpisodesInput" type="number" v-model="newMaxEpisodesToKeep" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updatedMaxEpisodesToKeep" /> <ui-text-input ref="maxEpisodesInput" type="number" v-model="newMaxEpisodesToKeep" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updatedMaxEpisodesToKeep" />
<ui-tooltip text="Value of 0 sets no max limit. After a new episode is auto-downloaded this will delete the oldest episode if you have more than X episodes. <br>This will only delete 1 episode per new download."> <ui-tooltip :text="$strings.LabelMaxEpisodesToKeepHelp">
<p class="pl-4 text-base"> <p class="pl-4 text-base">
Max episodes to keep {{ $strings.LabelMaxEpisodesToKeep }}
<span class="material-symbols icon-text">info</span> <span class="material-symbols icon-text">info</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2"> <div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2">
<ui-text-input ref="maxEpisodesToDownloadInput" type="number" v-model="newMaxNewEpisodesToDownload" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updateMaxNewEpisodesToDownload" /> <ui-text-input ref="maxEpisodesToDownloadInput" type="number" v-model="newMaxNewEpisodesToDownload" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updateMaxNewEpisodesToDownload" />
<ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded."> <ui-tooltip :text="$strings.LabelUseZeroForUnlimited">
<p class="pl-4 text-base"> <p class="pl-4 text-base">
Max new episodes to download per check {{ $strings.LabelMaxEpisodesToDownloadPerCheck }}
<span class="material-symbols icon-text">info</span> <span class="material-symbols icon-text">info</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
@ -36,7 +36,7 @@
<div v-if="feedUrl || autoDownloadEpisodes" class="absolute bottom-0 left-0 w-full py-2 md:py-4 bg-bg border-t border-white border-opacity-5"> <div v-if="feedUrl || autoDownloadEpisodes" class="absolute bottom-0 left-0 w-full py-2 md:py-4 bg-bg border-t border-white border-opacity-5">
<div class="flex items-center px-2 md:px-4"> <div class="flex items-center px-2 md:px-4">
<div class="flex-grow" /> <div class="flex-grow" />
<ui-btn @click="save" :disabled="!isUpdated" :color="isUpdated ? 'success' : 'primary'" class="mx-2">{{ isUpdated ? 'Save' : 'No update necessary' }}</ui-btn> <ui-btn @click="save" :disabled="!isUpdated" :color="isUpdated ? 'success' : 'primary'" class="mx-2">{{ isUpdated ? $strings.ButtonSave : $strings.MessageNoUpdatesWereNecessary }}</ui-btn>
</div> </div>
</div> </div>
</div> </div>

View file

@ -33,18 +33,18 @@
<span class="material-symbols text-lg ml-2">launch</span> <span class="material-symbols text-lg ml-2">launch</span>
</ui-btn> </ui-btn>
<ui-btn v-if="!isMetadataEmbedQueued && !isEmbedTaskRunning" class="w-full mt-4" small @click.stop="quickEmbed">Quick Embed</ui-btn> <ui-btn v-if="!isMetadataEmbedQueued && !isEmbedTaskRunning" class="w-full mt-4" small @click.stop="quickEmbed">{{ $strings.ButtonQuickEmbed }}</ui-btn>
</div> </div>
</div> </div>
<!-- queued alert --> <!-- queued alert -->
<widgets-alert v-if="isMetadataEmbedQueued" type="warning" class="mt-4"> <widgets-alert v-if="isMetadataEmbedQueued" type="warning" class="mt-4">
<p class="text-lg">Queued for metadata embed ({{ queuedEmbedLIds.length }} in queue)</p> <p class="text-lg">{{ $getString('MessageQuickEmbedQueue', [queuedEmbedLIds.length]) }}</p>
</widgets-alert> </widgets-alert>
<!-- processing alert --> <!-- processing alert -->
<widgets-alert v-if="isEmbedTaskRunning" type="warning" class="mt-4"> <widgets-alert v-if="isEmbedTaskRunning" type="warning" class="mt-4">
<p class="text-lg">Currently embedding metadata</p> <p class="text-lg">{{ $strings.MessageQuickEmbedInProgress }}</p>
</widgets-alert> </widgets-alert>
</div> </div>
@ -113,7 +113,7 @@ export default {
methods: { methods: {
quickEmbed() { quickEmbed() {
const payload = { const payload = {
message: 'Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?', message: this.$strings.MessageConfirmQuickEmbed,
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.$axios this.$axios

View file

@ -111,7 +111,6 @@ export default {
}, },
updateLibrary(library) { updateLibrary(library) {
this.mapLibraryToCopy(library) this.mapLibraryToCopy(library)
console.log('Updated library', this.libraryCopy)
}, },
getNewLibraryData() { getNewLibraryData() {
return { return {
@ -128,7 +127,9 @@ export default {
autoScanCronExpression: null, autoScanCronExpression: null,
hideSingleBookSeries: false, hideSingleBookSeries: false,
onlyShowLaterBooksInContinueSeries: false, onlyShowLaterBooksInContinueSeries: false,
metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'] metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata'],
markAsFinishedPercentComplete: null,
markAsFinishedTimeRemaining: 10
} }
} }
}, },
@ -160,7 +161,7 @@ export default {
return false return false
} }
if (!this.libraryCopy.folders.length) { if (!this.libraryCopy.folders.length) {
this.$toast.error('Library must have at least 1 path') this.$toast.error(this.$strings.ToastMustHaveAtLeastOnePath)
return false return false
} }
@ -222,7 +223,7 @@ export default {
if (error.response && error.response.data) { if (error.response && error.response.data) {
this.$toast.error(error.response.data) this.$toast.error(error.response.data)
} else { } else {
this.$toast.error(this.$strings.ToastLibraryUpdateFailed) this.$toast.error(this.$strings.ToastFailedToUpdate)
} }
this.processing = false this.processing = false
}) })
@ -236,7 +237,6 @@ export default {
this.show = false this.show = false
this.$toast.success(this.$getString('ToastLibraryCreateSuccess', [res.name])) this.$toast.success(this.$getString('ToastLibraryCreateSuccess', [res.name]))
if (!this.$store.state.libraries.currentLibraryId) { if (!this.$store.state.libraries.currentLibraryId) {
console.log('Setting initially library id', res.id)
// First library added // First library added
this.$store.dispatch('libraries/fetch', res.id) this.$store.dispatch('libraries/fetch', res.id)
} }

View file

@ -1,79 +1,95 @@
<template> <template>
<div class="w-full h-full px-1 md:px-4 py-1 mb-4"> <div class="w-full h-full px-1 md:px-4 py-1 mb-4">
<div class="flex items-center py-3"> <div class="flex flex-wrap">
<ui-toggle-switch v-model="useSquareBookCovers" @input="formUpdated" /> <div class="flex items-center p-2 w-full md:w-1/2">
<ui-toggle-switch v-model="useSquareBookCovers" size="sm" @input="formUpdated" />
<ui-tooltip :text="$strings.LabelSettingsSquareBookCoversHelp"> <ui-tooltip :text="$strings.LabelSettingsSquareBookCoversHelp">
<p class="pl-4 text-base"> <p class="pl-4 text-sm">
{{ $strings.LabelSettingsSquareBookCovers }} {{ $strings.LabelSettingsSquareBookCovers }}
<span class="material-symbols icon-text text-sm">info</span> <span class="material-symbols icon-text text-sm">info</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div class="py-3"> <div class="p-2 w-full md:w-1/2">
<div class="flex items-center"> <div class="flex items-center">
<ui-toggle-switch v-if="!globalWatcherDisabled" v-model="enableWatcher" @input="formUpdated" /> <ui-toggle-switch v-if="!globalWatcherDisabled" v-model="enableWatcher" size="sm" @input="formUpdated" />
<ui-toggle-switch v-else disabled :value="false" /> <ui-toggle-switch v-else disabled size="sm" :value="false" />
<p class="pl-4 text-base">{{ $strings.LabelSettingsEnableWatcherForLibrary }}</p> <p class="pl-4 text-sm">{{ $strings.LabelSettingsEnableWatcherForLibrary }}</p>
</div> </div>
<p v-if="globalWatcherDisabled" class="text-xs text-warning">*{{ $strings.MessageWatcherIsDisabledGlobally }}</p> <p v-if="globalWatcherDisabled" class="text-xs text-warning">*{{ $strings.MessageWatcherIsDisabledGlobally }}</p>
</div> </div>
<div v-if="isBookLibrary" class="flex items-center py-3"> <div v-if="isBookLibrary" class="flex items-center p-2 w-full md:w-1/2">
<ui-toggle-switch v-model="audiobooksOnly" @input="formUpdated" /> <ui-toggle-switch v-model="audiobooksOnly" size="sm" @input="formUpdated" />
<ui-tooltip :text="$strings.LabelSettingsAudiobooksOnlyHelp"> <ui-tooltip :text="$strings.LabelSettingsAudiobooksOnlyHelp">
<p class="pl-4 text-base"> <p class="pl-4 text-sm">
{{ $strings.LabelSettingsAudiobooksOnly }} {{ $strings.LabelSettingsAudiobooksOnly }}
<span class="material-symbols icon-text text-sm">info</span> <span class="material-symbols icon-text text-sm">info</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
<div v-if="isBookLibrary" class="py-3"> <div v-if="isBookLibrary" class="p-2 w-full md:w-1/2">
<div class="flex items-center"> <div class="flex items-center">
<ui-toggle-switch v-model="skipMatchingMediaWithAsin" @input="formUpdated" /> <ui-toggle-switch v-model="skipMatchingMediaWithAsin" size="sm" @input="formUpdated" />
<p class="pl-4 text-base">{{ $strings.LabelSettingsSkipMatchingBooksWithASIN }}</p> <p class="pl-4 text-sm">{{ $strings.LabelSettingsSkipMatchingBooksWithASIN }}</p>
</div> </div>
</div> </div>
<div v-if="isBookLibrary" class="py-3"> <div v-if="isBookLibrary" class="p-2 w-full md:w-1/2">
<div class="flex items-center"> <div class="flex items-center">
<ui-toggle-switch v-model="skipMatchingMediaWithIsbn" @input="formUpdated" /> <ui-toggle-switch v-model="skipMatchingMediaWithIsbn" size="sm" @input="formUpdated" />
<p class="pl-4 text-base">{{ $strings.LabelSettingsSkipMatchingBooksWithISBN }}</p> <p class="pl-4 text-sm">{{ $strings.LabelSettingsSkipMatchingBooksWithISBN }}</p>
</div> </div>
</div> </div>
<div v-if="isBookLibrary" class="py-3"> <div v-if="isBookLibrary" class="p-2 w-full md:w-1/2">
<div class="flex items-center"> <div class="flex items-center">
<ui-toggle-switch v-model="hideSingleBookSeries" @input="formUpdated" /> <ui-toggle-switch v-model="hideSingleBookSeries" size="sm" @input="formUpdated" />
<ui-tooltip :text="$strings.LabelSettingsHideSingleBookSeriesHelp"> <ui-tooltip :text="$strings.LabelSettingsHideSingleBookSeriesHelp">
<p class="pl-4 text-base"> <p class="pl-4 text-sm">
{{ $strings.LabelSettingsHideSingleBookSeries }} {{ $strings.LabelSettingsHideSingleBookSeries }}
<span class="material-symbols icon-text text-sm">info</span> <span class="material-symbols icon-text text-sm">info</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
</div> </div>
<div v-if="isBookLibrary" class="py-3"> <div v-if="isBookLibrary" class="p-2 w-full md:w-1/2">
<div class="flex items-center"> <div class="flex items-center">
<ui-toggle-switch v-model="onlyShowLaterBooksInContinueSeries" @input="formUpdated" /> <ui-toggle-switch v-model="onlyShowLaterBooksInContinueSeries" size="sm" @input="formUpdated" />
<ui-tooltip :text="$strings.LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp"> <ui-tooltip :text="$strings.LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp">
<p class="pl-4 text-base"> <p class="pl-4 text-sm">
{{ $strings.LabelSettingsOnlyShowLaterBooksInContinueSeries }} {{ $strings.LabelSettingsOnlyShowLaterBooksInContinueSeries }}
<span class="material-symbols icon-text text-sm">info</span> <span class="material-symbols icon-text text-sm">info</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
</div> </div>
<div v-if="isBookLibrary" class="py-3"> <div v-if="isBookLibrary" class="p-2 w-full md:w-1/2">
<div class="flex items-center"> <div class="flex items-center">
<ui-toggle-switch v-model="epubsAllowScriptedContent" @input="formUpdated" /> <ui-toggle-switch v-model="epubsAllowScriptedContent" size="sm" @input="formUpdated" />
<ui-tooltip :text="$strings.LabelSettingsEpubsAllowScriptedContentHelp"> <ui-tooltip :text="$strings.LabelSettingsEpubsAllowScriptedContentHelp">
<p class="pl-4 text-base"> <p class="pl-4 text-sm">
{{ $strings.LabelSettingsEpubsAllowScriptedContent }} {{ $strings.LabelSettingsEpubsAllowScriptedContent }}
<span class="material-symbols icon-text text-sm">info</span> <span class="material-symbols icon-text text-sm">info</span>
</p> </p>
</ui-tooltip> </ui-tooltip>
</div> </div>
</div> </div>
<div v-if="isPodcastLibrary" class="py-3"> <div v-if="isPodcastLibrary" class="p-2 w-full md:w-1/2">
<ui-dropdown :label="$strings.LabelPodcastSearchRegion" v-model="podcastSearchRegion" :items="$podcastSearchRegionOptions" small class="max-w-72" menu-max-height="200px" @input="formUpdated" /> <ui-dropdown :label="$strings.LabelPodcastSearchRegion" v-model="podcastSearchRegion" :items="$podcastSearchRegionOptions" small class="max-w-72" menu-max-height="200px" @input="formUpdated" />
</div> </div>
<div class="p-2 w-full flex items-center space-x-2 flex-wrap">
<div>
<ui-dropdown v-model="markAsFinishedWhen" :items="maskAsFinishedWhenItems" :label="$strings.LabelSettingsLibraryMarkAsFinishedWhen" small class="w-72 min-w-72 text-sm" menu-max-height="200px" @input="markAsFinishedWhenChanged" />
</div>
<div class="w-16">
<div>
<label class="px-1 text-sm font-semibold"></label>
<div class="relative">
<ui-text-input v-model="markAsFinishedValue" type="number" label="" no-spinner custom-input-class="pr-5" @input="markAsFinishedChanged" />
<div class="absolute top-0 bottom-0 right-4 flex items-center">{{ markAsFinishedWhen === 'timeRemaining' ? '' : '%' }}</div>
</div>
</div>
</div>
</div>
</div>
</div> </div>
</template> </template>
@ -97,7 +113,9 @@ export default {
epubsAllowScriptedContent: false, epubsAllowScriptedContent: false,
hideSingleBookSeries: false, hideSingleBookSeries: false,
onlyShowLaterBooksInContinueSeries: false, onlyShowLaterBooksInContinueSeries: false,
podcastSearchRegion: 'us' podcastSearchRegion: 'us',
markAsFinishedWhen: 'timeRemaining',
markAsFinishedValue: 10
} }
}, },
computed: { computed: {
@ -119,10 +137,34 @@ export default {
providers() { providers() {
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
return this.$store.state.scanners.providers return this.$store.state.scanners.providers
},
maskAsFinishedWhenItems() {
return [
{
text: this.$strings.LabelSettingsLibraryMarkAsFinishedTimeRemaining,
value: 'timeRemaining'
},
{
text: this.$strings.LabelSettingsLibraryMarkAsFinishedPercentComplete,
value: 'percentComplete'
}
]
} }
}, },
methods: { methods: {
markAsFinishedWhenChanged(val) {
if (val === 'percentComplete' && this.markAsFinishedValue > 100) {
this.markAsFinishedValue = 100
}
this.formUpdated()
},
markAsFinishedChanged(val) {
this.formUpdated()
},
getLibraryData() { getLibraryData() {
let markAsFinishedTimeRemaining = this.markAsFinishedWhen === 'timeRemaining' ? Number(this.markAsFinishedValue) : null
let markAsFinishedPercentComplete = this.markAsFinishedWhen === 'percentComplete' ? Number(this.markAsFinishedValue) : null
return { return {
settings: { settings: {
coverAspectRatio: this.useSquareBookCovers ? this.$constants.BookCoverAspectRatio.SQUARE : this.$constants.BookCoverAspectRatio.STANDARD, coverAspectRatio: this.useSquareBookCovers ? this.$constants.BookCoverAspectRatio.SQUARE : this.$constants.BookCoverAspectRatio.STANDARD,
@ -133,7 +175,9 @@ export default {
epubsAllowScriptedContent: !!this.epubsAllowScriptedContent, epubsAllowScriptedContent: !!this.epubsAllowScriptedContent,
hideSingleBookSeries: !!this.hideSingleBookSeries, hideSingleBookSeries: !!this.hideSingleBookSeries,
onlyShowLaterBooksInContinueSeries: !!this.onlyShowLaterBooksInContinueSeries, onlyShowLaterBooksInContinueSeries: !!this.onlyShowLaterBooksInContinueSeries,
podcastSearchRegion: this.podcastSearchRegion podcastSearchRegion: this.podcastSearchRegion,
markAsFinishedTimeRemaining: markAsFinishedTimeRemaining,
markAsFinishedPercentComplete: markAsFinishedPercentComplete
} }
} }
}, },
@ -150,6 +194,11 @@ export default {
this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries
this.onlyShowLaterBooksInContinueSeries = !!this.librarySettings.onlyShowLaterBooksInContinueSeries this.onlyShowLaterBooksInContinueSeries = !!this.librarySettings.onlyShowLaterBooksInContinueSeries
this.podcastSearchRegion = this.librarySettings.podcastSearchRegion || 'us' this.podcastSearchRegion = this.librarySettings.podcastSearchRegion || 'us'
this.markAsFinishedWhen = this.librarySettings.markAsFinishedTimeRemaining ? 'timeRemaining' : 'percentComplete'
if (!this.librarySettings.markAsFinishedTimeRemaining && !this.librarySettings.markAsFinishedPercentComplete) {
this.markAsFinishedWhen = 'timeRemaining'
}
this.markAsFinishedValue = this.librarySettings.markAsFinishedTimeRemaining || this.librarySettings.markAsFinishedPercentComplete || 10
} }
}, },
mounted() { mounted() {

View file

@ -3,13 +3,13 @@
<div class="w-full border border-black-200 p-4 my-8"> <div class="w-full border border-black-200 p-4 my-8">
<div class="flex flex-wrap items-center"> <div class="flex flex-wrap items-center">
<div> <div>
<p class="text-lg">Remove metadata files in library item folders</p> <p class="text-lg">{{ $strings.LabelRemoveMetadataFile }}</p>
<p class="max-w-sm text-sm pt-2 text-gray-300">Remove all metadata.json or metadata.abs files in your {{ mediaType }} folders</p> <p class="max-w-sm text-sm pt-2 text-gray-300">{{ $getString('LabelRemoveMetadataFileHelp', [mediaType]) }}</p>
</div> </div>
<div class="flex-grow" /> <div class="flex-grow" />
<div> <div>
<ui-btn class="mb-4 block" @click.stop="removeAllMetadataClick('json')">Remove all metadata.json</ui-btn> <ui-btn class="mb-4 block" @click.stop="removeAllMetadataClick('json')">{{ $strings.LabelRemoveAllMetadataJson }}</ui-btn>
<ui-btn @click.stop="removeAllMetadataClick('abs')">Remove all metadata.abs</ui-btn> <ui-btn @click.stop="removeAllMetadataClick('abs')">{{ $strings.LabelRemoveAllMetadataAbs }}</ui-btn>
</div> </div>
</div> </div>
</div> </div>
@ -43,7 +43,7 @@ export default {
methods: { methods: {
removeAllMetadataClick(ext) { removeAllMetadataClick(ext) {
const payload = { const payload = {
message: `Are you sure you want to remove all metadata.${ext} files in your library item folders?`, message: this.$getString('MessageConfirmRemoveMetadataFiles', [ext]),
persistent: true, persistent: true,
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
@ -60,16 +60,16 @@ export default {
.$post(`/api/libraries/${this.libraryId}/remove-metadata?ext=${ext}`) .$post(`/api/libraries/${this.libraryId}/remove-metadata?ext=${ext}`)
.then((data) => { .then((data) => {
if (!data.found) { if (!data.found) {
this.$toast.info(`No metadata.${ext} files were found in library`) this.$toast.info(this.$getString('ToastMetadataFilesRemovedNoneFound', [ext]))
} else if (!data.removed) { } else if (!data.removed) {
this.$toast.success(`No metadata.${ext} files removed`) this.$toast.success(this.$getString('ToastMetadataFilesRemovedNoneRemoved', [ext]))
} else { } else {
this.$toast.success(`Successfully removed ${data.removed} metadata.${ext} files`) this.$toast.success(this.$getString('ToastMetadataFilesRemovedSuccess', [data.removed, ext]))
} }
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove metadata files', error) console.error('Failed to remove metadata files', error)
this.$toast.error('Failed to remove metadata files') this.$toast.error(this.$getString('ToastMetadataFilesRemovedError', [ext]))
}) })
.finally(() => { .finally(() => {
this.$emit('update:processing', false) this.$emit('update:processing', false)

View file

@ -77,7 +77,13 @@ export default {
return this.notificationData.events || [] return this.notificationData.events || []
}, },
eventOptions() { eventOptions() {
return this.notificationEvents.map((e) => ({ value: e.name, text: e.name, subtext: e.description })) return this.notificationEvents.map((e) => {
return {
value: e.name,
text: e.name,
subtext: this.$strings[e.descriptionKey] || e.description
}
})
}, },
selectedEventData() { selectedEventData() {
return this.notificationEvents.find((e) => e.name === this.newNotification.eventName) return this.notificationEvents.find((e) => e.name === this.newNotification.eventName)
@ -132,7 +138,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update notification', error) console.error('Failed to update notification', error)
this.$toast.error(this.$strings.ToastNotificationUpdateFailed) this.$toast.error(this.$strings.ToastFailedToUpdate)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false

View file

@ -135,7 +135,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove items from playlist', error) console.error('Failed to remove items from playlist', error)
this.$toast.error(this.$strings.ToastPlaylistUpdateFailed) this.$toast.error(this.$strings.ToastFailedToUpdate)
this.processing = false this.processing = false
}) })
}, },
@ -153,7 +153,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to add items to playlist', error) console.error('Failed to add items to playlist', error)
this.$toast.error(this.$strings.ToastPlaylistUpdateFailed) this.$toast.error(this.$strings.ToastFailedToUpdate)
this.processing = false this.processing = false
}) })
}, },

View file

@ -115,7 +115,7 @@ export default {
.catch((error) => { .catch((error) => {
console.error('Failed to update playlist', error) console.error('Failed to update playlist', error)
this.processing = false this.processing = false
this.$toast.error(this.$strings.ToastPlaylistUpdateFailed) this.$toast.error(this.$strings.ToastFailedToUpdate)
}) })
} }
}, },

View file

@ -156,7 +156,12 @@ export default {
return this.selectedFolder.fullPath return this.selectedFolder.fullPath
}, },
podcastTypes() { podcastTypes() {
return this.$store.state.globals.podcastTypes || [] return this.$store.state.globals.podcastTypes.map((e) => {
return {
text: this.$strings[e.descriptionKey] || e.text,
value: e.value
}
})
} }
}, },
methods: { methods: {

View file

@ -18,6 +18,23 @@
<p dir="auto" class="text-lg font-semibold mb-6">{{ title }}</p> <p dir="auto" class="text-lg font-semibold mb-6">{{ title }}</p>
<div v-if="description" dir="auto" class="default-style" v-html="description" /> <div v-if="description" dir="auto" class="default-style" v-html="description" />
<p v-else class="mb-2">{{ $strings.MessageNoDescription }}</p> <p v-else class="mb-2">{{ $strings.MessageNoDescription }}</p>
<div class="w-full h-px bg-white/5 my-4" />
<div class="flex items-center">
<div class="flex-grow">
<p class="font-semibold text-xs mb-1">{{ $strings.LabelFilename }}</p>
<p class="mb-2 text-xs">
{{ audioFileFilename }}
</p>
</div>
<div class="flex-grow">
<p class="font-semibold text-xs mb-1">{{ $strings.LabelSize }}</p>
<p class="mb-2 text-xs">
{{ audioFileSize }}
</p>
</div>
</div>
</div> </div>
</modals-modal> </modals-modal>
</template> </template>
@ -54,7 +71,7 @@ export default {
return this.episode.description || '' return this.episode.description || ''
}, },
media() { media() {
return this.libraryItem ? this.libraryItem.media || {} : {} return this.libraryItem?.media || {}
}, },
mediaMetadata() { mediaMetadata() {
return this.media.metadata || {} return this.media.metadata || {}
@ -65,6 +82,14 @@ export default {
podcastAuthor() { podcastAuthor() {
return this.mediaMetadata.author return this.mediaMetadata.author
}, },
audioFileFilename() {
return this.episode.audioFile?.metadata?.filename || ''
},
audioFileSize() {
const size = this.episode.audioFile?.metadata?.size || 0
return this.$bytesPretty(size)
},
bookCoverAspectRatio() { bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio'] return this.$store.getters['libraries/getBookCoverAspectRatio']
} }

View file

@ -33,11 +33,11 @@
</div> </div>
<div v-if="enclosureUrl" class="pb-4 pt-6"> <div v-if="enclosureUrl" class="pb-4 pt-6">
<ui-text-input-with-label :value="enclosureUrl" readonly class="text-xs"> <ui-text-input-with-label :value="enclosureUrl" readonly class="text-xs">
<label class="px-1 text-xs text-gray-200 font-semibold">Episode URL from RSS feed</label> <label class="px-1 text-xs text-gray-200 font-semibold">{{ $strings.LabelEpisodeUrlFromRssFeed }}</label>
</ui-text-input-with-label> </ui-text-input-with-label>
</div> </div>
<div v-else class="py-4"> <div v-else class="py-4">
<p class="text-xs text-gray-300 font-semibold">Episode not linked to RSS feed episode</p> <p class="text-xs text-gray-300 font-semibold">{{ $strings.LabelEpisodeNotLinkedToRssFeed }}</p>
</div> </div>
</div> </div>
</template> </template>
@ -97,7 +97,12 @@ export default {
return this.enclosure.url return this.enclosure.url
}, },
episodeTypes() { episodeTypes() {
return this.$store.state.globals.episodeTypes || [] return this.$store.state.globals.episodeTypes.map((e) => {
return {
text: this.$strings[e.descriptionKey] || e.text,
value: e.value
}
})
} }
}, },
methods: { methods: {
@ -152,14 +157,14 @@ export default {
const updateResult = await this.$axios.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, updatedDetails).catch((error) => { const updateResult = await this.$axios.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, updatedDetails).catch((error) => {
console.error('Failed update episode', error) console.error('Failed update episode', error)
this.isProcessing = false this.isProcessing = false
this.$toast.error(error?.response?.data || 'Failed to update episode') this.$toast.error(error?.response?.data || this.$strings.ToastFailedToUpdate)
return false return false
}) })
this.isProcessing = false this.isProcessing = false
if (updateResult) { if (updateResult) {
if (updateResult) { if (updateResult) {
this.$toast.success('Podcast episode updated') this.$toast.success(this.$strings.ToastItemUpdateSuccess)
return true return true
} else { } else {
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary) this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)

View file

@ -139,7 +139,7 @@ export default {
slug: this.newFeedSlug, slug: this.newFeedSlug,
metadataDetails: this.metadataDetails metadataDetails: this.metadataDetails
} }
if (this.$isDev) payload.serverAddress = `http://localhost:3333${this.$config.routerBasePath}` if (this.$isDev) payload.serverAddress = process.env.serverUrl
console.log('Payload', payload) console.log('Payload', payload)
this.$axios this.$axios

View file

@ -1,6 +1,6 @@
<template> <template>
<div class="flex items-center pt-4 pb-2 lg:pt-0 lg:pb-2"> <div class="flex justify-center pt-4 pb-2 lg:pt-0 lg:pb-2">
<div class="flex-grow" /> <div class="flex items-center justify-center flex-grow">
<template v-if="!loading"> <template v-if="!loading">
<ui-tooltip direction="top" :text="$strings.ButtonPreviousChapter" class="mr-4 lg:mr-8"> <ui-tooltip direction="top" :text="$strings.ButtonPreviousChapter" class="mr-4 lg:mr-8">
<button :aria-label="$strings.ButtonPreviousChapter" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter"> <button :aria-label="$strings.ButtonPreviousChapter" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter">
@ -25,14 +25,13 @@
<span class="material-symbols text-2xl sm:text-3xl">last_page</span> <span class="material-symbols text-2xl sm:text-3xl">last_page</span>
</button> </button>
</ui-tooltip> </ui-tooltip>
<controls-playback-speed-control v-model="playbackRateInput" @input="playbackRateUpdated" @change="playbackRateChanged" />
</template> </template>
<template v-else> <template v-else>
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin"> <div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-8 animate-spin">
<span class="material-symbols text-2xl">autorenew</span> <span class="material-symbols text-2xl">autorenew</span>
</div> </div>
</template> </template>
<div class="flex-grow" /> </div>
</div> </div>
</template> </template>
@ -41,7 +40,6 @@ export default {
props: { props: {
loading: Boolean, loading: Boolean,
seekLoading: Boolean, seekLoading: Boolean,
playbackRate: Number,
paused: Boolean, paused: Boolean,
hasNextChapter: Boolean, hasNextChapter: Boolean,
hasNextItemInQueue: Boolean hasNextItemInQueue: Boolean
@ -50,14 +48,6 @@ export default {
return {} return {}
}, },
computed: { computed: {
playbackRateInput: {
get() {
return this.playbackRate
},
set(val) {
this.$emit('update:playbackRate', val)
}
},
jumpForwardText() { jumpForwardText() {
return this.getJumpText('jumpForwardAmount', this.$strings.ButtonJumpForward) return this.getJumpText('jumpForwardAmount', this.$strings.ButtonJumpForward)
}, },
@ -89,15 +79,6 @@ export default {
jumpForward() { jumpForward() {
this.$emit('jumpForward') this.$emit('jumpForward')
}, },
playbackRateUpdated(playbackRate) {
this.$emit('setPlaybackRate', playbackRate)
},
playbackRateChanged(playbackRate) {
this.$emit('setPlaybackRate', playbackRate)
this.$store.dispatch('user/updateUserSettings', { playbackRate }).catch((err) => {
console.error('Failed to update settings', err)
})
},
getJumpText(setting, prefix) { getJumpText(setting, prefix) {
const amount = this.$store.getters['user/getUserSetting'](setting) const amount = this.$store.getters['user/getUserSetting'](setting)
if (!amount) return prefix if (!amount) return prefix

View file

@ -2,9 +2,9 @@
<div class="w-full -mt-6"> <div class="w-full -mt-6">
<div class="w-full relative mb-1"> <div class="w-full relative mb-1">
<div class="absolute -top-10 lg:top-0 right-0 lg:right-2 flex items-center h-full"> <div class="absolute -top-10 lg:top-0 right-0 lg:right-2 flex items-center h-full">
<!-- <span class="material-symbols text-2xl cursor-pointer" @click="toggleFullscreen(true)">expand_less</span> --> <controls-playback-speed-control v-model="playbackRate" @input="setPlaybackRate" @change="playbackRateChanged" class="mx-2 block" />
<ui-tooltip direction="top" :text="$strings.LabelVolume"> <ui-tooltip direction="left" :text="$strings.LabelVolume">
<controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden sm:block" /> <controls-volume-control ref="volumeControl" v-model="volume" @input="setVolume" class="mx-2 hidden sm:block" />
</ui-tooltip> </ui-tooltip>
@ -13,7 +13,7 @@
<span v-if="!sleepTimerSet" class="material-symbols text-2xl">snooze</span> <span v-if="!sleepTimerSet" class="material-symbols text-2xl">snooze</span>
<div v-else class="flex items-center"> <div v-else class="flex items-center">
<span class="material-symbols text-lg text-warning">snooze</span> <span class="material-symbols text-lg text-warning">snooze</span>
<p class="text-sm sm:text-lg text-warning font-mono font-semibold text-center px-0.5 sm:pb-0.5 sm:min-w-8">{{ sleepTimerRemainingString }}</p> <p class="text-sm sm:text-lg text-warning font-semibold text-center px-0.5 sm:pb-0.5 sm:min-w-8">{{ sleepTimerRemainingString }}</p>
</div> </div>
</button> </button>
</ui-tooltip> </ui-tooltip>
@ -48,16 +48,20 @@
<player-track-bar ref="trackbar" :loading="loading" :chapters="chapters" :duration="duration" :current-chapter="currentChapter" :playback-rate="playbackRate" @seek="seek" /> <player-track-bar ref="trackbar" :loading="loading" :chapters="chapters" :duration="duration" :current-chapter="currentChapter" :playback-rate="playbackRate" @seek="seek" />
<div class="flex"> <div class="relative flex items-center justify-between">
<div class="flex-grow flex items-center">
<p ref="currentTimestamp" class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">00:00:00</p> <p ref="currentTimestamp" class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">00:00:00</p>
<p class="font-mono text-sm hidden sm:block text-gray-100 pointer-events-auto">&nbsp;/&nbsp;{{ progressPercent }}%</p> <p class="font-mono text-sm hidden sm:block text-gray-100 pointer-events-auto">&nbsp;/&nbsp;{{ progressPercent }}%</p>
<div class="flex-grow" /> </div>
<div class="absolute left-1/2 transform -translate-x-1/2">
<p class="text-xs sm:text-sm text-gray-300 pt-0.5 px-2 truncate"> <p class="text-xs sm:text-sm text-gray-300 pt-0.5 px-2 truncate">
{{ currentChapterName }} <span v-if="useChapterTrack" class="text-xs text-gray-400">&nbsp;({{ $getString('LabelPlayerChapterNumberMarker', [currentChapterIndex + 1, chapters.length]) }})</span> {{ currentChapterName }} <span v-if="useChapterTrack" class="text-xs text-gray-400">&nbsp;({{ $getString('LabelPlayerChapterNumberMarker', [currentChapterIndex + 1, chapters.length]) }})</span>
</p> </p>
<div class="flex-grow" /> </div>
<div class="flex-grow flex items-center justify-end">
<p class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p> <p class="font-mono text-xxs sm:text-sm text-gray-100 pointer-events-auto">{{ timeRemainingPretty }}</p>
</div> </div>
</div>
<modals-chapters-modal v-model="showChaptersModal" :current-chapter="currentChapter" :playback-rate="playbackRate" :chapters="chapters" @select="selectChapter" /> <modals-chapters-modal v-model="showChaptersModal" :current-chapter="currentChapter" :playback-rate="playbackRate" :chapters="chapters" @select="selectChapter" />
</div> </div>
@ -224,6 +228,12 @@ export default {
this.playbackRate = Number((this.playbackRate - 0.1).toFixed(1)) this.playbackRate = Number((this.playbackRate - 0.1).toFixed(1))
this.setPlaybackRate(this.playbackRate) this.setPlaybackRate(this.playbackRate)
}, },
playbackRateChanged(playbackRate) {
this.setPlaybackRate(playbackRate)
this.$store.dispatch('user/updateUserSettings', { playbackRate }).catch((err) => {
console.error('Failed to update settings', err)
})
},
setPlaybackRate(playbackRate) { setPlaybackRate(playbackRate) {
this.$emit('setPlaybackRate', playbackRate) this.$emit('setPlaybackRate', playbackRate)
}, },

View file

@ -78,7 +78,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update collection', error) console.error('Failed to update collection', error)
this.$toast.error(this.$strings.ToastCollectionUpdateFailed) this.$toast.error(this.$strings.ToastFailedToUpdate)
}) })
}, },
editBook(book) { editBook(book) {

View file

@ -92,7 +92,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update playlist', error) console.error('Failed to update playlist', error)
this.$toast.error(this.$strings.ToastPlaylistUpdateFailed) this.$toast.error(this.$strings.ToastFailedToUpdate)
}) })
}, },
init() { init() {

View file

@ -223,7 +223,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to remove item from playlist', error) console.error('Failed to remove item from playlist', error)
this.$toast.error(this.$strings.ToastPlaylistUpdateFailed) this.$toast.error(this.$strings.ToastFailedToUpdate)
}) })
.finally(() => { .finally(() => {
this.processingRemove = false this.processingRemove = false

View file

@ -12,10 +12,10 @@
</div> </div>
<div class="h-8 flex items-center"> <div class="h-8 flex items-center">
<div class="w-full inline-flex justify-between max-w-xl"> <div class="w-full inline-flex justify-between max-w-xl">
<p v-if="episode?.season" class="text-sm text-gray-300">Season #{{ episode.season }}</p> <p v-if="episode?.season" class="text-sm text-gray-300">{{ $getString('LabelSeasonNumber', [episode.season]) }}</p>
<p v-if="episode?.episode" class="text-sm text-gray-300">Episode #{{ episode.episode }}</p> <p v-if="episode?.episode" class="text-sm text-gray-300">{{ $getString('LabelEpisodeNumber', [episode.episode]) }}</p>
<p v-if="episode?.chapters?.length" class="text-sm text-gray-300">{{ episode.chapters.length }} Chapters</p> <p v-if="episode?.chapters?.length" class="text-sm text-gray-300">{{ $getString('LabelChapterCount', [episode.chapters.length]) }}</p>
<p v-if="publishedAt" class="text-sm text-gray-300">Published {{ $formatDate(publishedAt, dateFormat) }}</p> <p v-if="publishedAt" class="text-sm text-gray-300">{{ $getString('LabelPublishedDate', [$formatDate(publishedAt, dateFormat)]) }}</p>
</div> </div>
</div> </div>
@ -132,13 +132,13 @@ export default {
return this.store.state.streamIsPlaying && this.isStreaming return this.store.state.streamIsPlaying && this.isStreaming
}, },
timeRemaining() { timeRemaining() {
if (this.streamIsPlaying) return 'Playing' if (this.streamIsPlaying) return this.$strings.ButtonPlaying
if (!this.itemProgress) return this.$elapsedPretty(this.episode?.duration || 0) if (!this.itemProgress) return this.$elapsedPretty(this.episode?.duration || 0)
if (this.userIsFinished) return 'Finished' if (this.userIsFinished) return this.$strings.LabelFinished
const duration = this.itemProgress.duration || this.episode?.duration || 0 const duration = this.itemProgress.duration || this.episode?.duration || 0
const remaining = Math.floor(duration - this.itemProgress.currentTime) const remaining = Math.floor(duration - this.itemProgress.currentTime)
return `${this.$elapsedPretty(remaining)} left` return this.$getString('LabelTimeLeft', [this.$elapsedPretty(remaining)])
} }
}, },
methods: { methods: {
@ -182,7 +182,7 @@ export default {
toggleFinished(confirmed = false) { toggleFinished(confirmed = false) {
if (!this.userIsFinished && this.itemProgressPercent > 0 && !confirmed) { if (!this.userIsFinished && this.itemProgressPercent > 0 && !confirmed) {
const payload = { const payload = {
message: `Are you sure you want to mark "${this.episodeTitle}" as finished?`, message: this.$getString('MessageConfirmMarkItemFinished', [this.episodeTitle]),
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.toggleFinished(true) this.toggleFinished(true)

View file

@ -25,7 +25,6 @@
</template> </template>
</div> </div>
</div> </div>
<!-- <p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p> -->
<div v-if="episodes.length" class="w-full py-3 mx-auto flex"> <div v-if="episodes.length" class="w-full py-3 mx-auto flex">
<form @submit.prevent="submit" class="flex flex-grow"> <form @submit.prevent="submit" class="flex flex-grow">
<ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" /> <ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" />
@ -93,17 +92,18 @@ export default {
}, },
computed: { computed: {
contextMenuItems() { contextMenuItems() {
if (!this.userIsAdminOrUp) return [] const menuItems = []
return [ if (this.userIsAdminOrUp) {
{ menuItems.push({
text: 'Quick match all episodes', text: this.$strings.MessageQuickMatchAllEpisodes,
action: 'quick-match-episodes' action: 'quick-match-episodes'
}, })
{ }
menuItems.push({
text: this.allEpisodesFinished ? this.$strings.MessageMarkAllEpisodesNotFinished : this.$strings.MessageMarkAllEpisodesFinished, text: this.allEpisodesFinished ? this.$strings.MessageMarkAllEpisodesNotFinished : this.$strings.MessageMarkAllEpisodesFinished,
action: 'batch-mark-as-finished' action: 'batch-mark-as-finished'
} })
] return menuItems
}, },
sortItems() { sortItems() {
return [ return [
@ -261,21 +261,21 @@ export default {
this.processing = true this.processing = true
const payload = { const payload = {
message: 'Quick matching episodes will overwrite details if a match is found. Only unmatched episodes will be updated. Are you sure?', message: this.$strings.MessageConfirmQuickMatchEpisodes,
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.$axios this.$axios
.$post(`/api/podcasts/${this.libraryItem.id}/match-episodes?override=1`) .$post(`/api/podcasts/${this.libraryItem.id}/match-episodes?override=1`)
.then((data) => { .then((data) => {
if (data.numEpisodesUpdated) { if (data.numEpisodesUpdated) {
this.$toast.success(`${data.numEpisodesUpdated} episodes updated`) this.$toast.success(this.$getString('ToastEpisodeUpdateSuccess', [data.numEpisodesUpdated]))
} else { } else {
this.$toast.info(this.$strings.ToastNoUpdatesNecessary) this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
} }
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to request match episodes', error) console.error('Failed to request match episodes', error)
this.$toast.error('Failed to match episodes') this.$toast.error(this.$strings.ToastFailedToMatch)
}) })
} }
this.processing = false this.processing = false
@ -514,6 +514,10 @@ export default {
} }
}, },
filterSortChanged() { filterSortChanged() {
// Save filterKey and sortKey to local storage
localStorage.setItem('podcastEpisodesFilter', this.filterKey)
localStorage.setItem('podcastEpisodesSortBy', this.sortKey + (this.sortDesc ? '-desc' : ''))
this.init() this.init()
}, },
refresh() { refresh() {
@ -536,6 +540,11 @@ export default {
} }
}, },
mounted() { mounted() {
this.filterKey = localStorage.getItem('podcastEpisodesFilter') || 'incomplete'
const sortBy = localStorage.getItem('podcastEpisodesSortBy') || 'publishedAt-desc'
this.sortKey = sortBy.split('-')[0]
this.sortDesc = sortBy.split('-')[1] === 'desc'
this.episodesCopy = this.episodes.map((ep) => ({ ...ep })) this.episodesCopy = this.episodes.map((ep) => ({ ...ep }))
this.initListeners() this.initListeners()
this.init() this.init()

View file

@ -57,7 +57,8 @@ export default {
inputName: String, inputName: String,
showCopy: Boolean, showCopy: Boolean,
step: [String, Number], step: [String, Number],
min: [String, Number] min: [String, Number],
customInputClass: String
}, },
data() { data() {
return { return {
@ -82,6 +83,7 @@ export default {
_list.push(`py-${this.paddingY}`) _list.push(`py-${this.paddingY}`)
if (this.noSpinner) _list.push('no-spinner') if (this.noSpinner) _list.push('no-spinner')
if (this.textCenter) _list.push('text-center') if (this.textCenter) _list.push('text-center')
if (this.customInputClass) _list.push(this.customInputClass)
return _list.join(' ') return _list.join(' ')
}, },
actualType() { actualType() {

View file

@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<button :aria-labelledby="labeledBy" role="checkbox" type="button" class="border rounded-full border-black-100 flex items-center cursor-pointer w-10 justify-start" :aria-checked="toggleValue" :class="className" @click="clickToggle"> <button :aria-labelledby="labeledBy" role="checkbox" type="button" class="border rounded-full border-black-100 flex items-center cursor-pointer justify-start" :style="{ width: buttonWidth + 'px' }" :aria-checked="toggleValue" :class="className" @click="clickToggle">
<span class="rounded-full border w-5 h-5 border-black-50 shadow transform transition-transform duration-100" :class="switchClassName"></span> <span class="rounded-full border border-black-50 shadow transform transition-transform duration-100" :style="{ width: cursorHeightWidth + 'px', height: cursorHeightWidth + 'px' }" :class="switchClassName"></span>
</button> </button>
</div> </div>
</template> </template>
@ -19,7 +19,11 @@ export default {
default: 'primary' default: 'primary'
}, },
disabled: Boolean, disabled: Boolean,
labeledBy: String labeledBy: String,
size: {
type: String,
default: 'md'
}
}, },
computed: { computed: {
toggleValue: { toggleValue: {
@ -37,6 +41,13 @@ export default {
switchClassName() { switchClassName() {
var bgColor = this.disabled ? 'bg-gray-300' : 'bg-white' var bgColor = this.disabled ? 'bg-gray-300' : 'bg-white'
return this.toggleValue ? 'translate-x-5 ' + bgColor : bgColor return this.toggleValue ? 'translate-x-5 ' + bgColor : bgColor
},
cursorHeightWidth() {
if (this.size === 'sm') return 16
return 20
},
buttonWidth() {
return this.cursorHeightWidth * 2
} }
}, },
methods: { methods: {

View file

@ -1,14 +1,11 @@
<template> <template>
<ui-tooltip v-if="alreadyInLibrary" :text="$strings.LabelAlreadyInYourLibrary" direction="top"> <ui-tooltip :text="$strings.LabelAlreadyInYourLibrary" direction="top" class="inline-flex">
<span class="material-symbols ml-1 text-success" style="font-size: 0.8rem">check_circle</span> <span class="material-symbols ml-1 text-sm text-success">check_circle</span>
</ui-tooltip> </ui-tooltip>
</template> </template>
<script> <script>
export default { export default {
props: {
alreadyInLibrary: Boolean
},
data() { data() {
return {} return {}
}, },

View file

@ -65,7 +65,7 @@ export default {
}, },
authors: { authors: {
component: 'cards-author-card', component: 'cards-author-card',
itemPropName: 'author', itemPropName: 'author-mount',
itemIdFunc: (item) => item.id itemIdFunc: (item) => item.id
}, },
narrators: { narrators: {

View file

@ -101,7 +101,12 @@ export default {
return this.$store.state.libraries.filterData || {} return this.$store.state.libraries.filterData || {}
}, },
podcastTypes() { podcastTypes() {
return this.$store.state.globals.podcastTypes || [] return this.$store.state.globals.podcastTypes.map((e) => {
return {
text: this.$strings[e.descriptionKey] || e.text,
value: e.value
}
})
} }
}, },
methods: { methods: {

View file

@ -71,8 +71,6 @@ export default {
this.showSeriesForm = true this.showSeriesForm = true
}, },
submitSeriesForm() { submitSeriesForm() {
console.log('submit series form', this.value, this.selectedSeries)
if (!this.selectedSeries.name) { if (!this.selectedSeries.name) {
this.$toast.error('Must enter a series') this.$toast.error('Must enter a series')
return return

View file

@ -5,14 +5,14 @@ import Tooltip from '@/components/ui/Tooltip.vue'
import LoadingSpinner from '@/components/widgets/LoadingSpinner.vue' import LoadingSpinner from '@/components/widgets/LoadingSpinner.vue'
describe('AuthorCard', () => { describe('AuthorCard', () => {
const author = { const authorMount = {
id: 1, id: 1,
name: 'John Doe', name: 'John Doe',
numBooks: 5 numBooks: 5
} }
const propsData = { const propsData = {
author, authorMount,
nameBelow: false nameBelow: false
} }

View file

@ -357,7 +357,8 @@ export default {
teardown: false, teardown: false,
transports: ['websocket'], transports: ['websocket'],
upgrade: false, upgrade: false,
reconnection: true reconnection: true,
path: `${this.$config.routerBasePath}/socket.io`
}) })
this.$root.socket = this.socket this.$root.socket = this.socket
console.log('Socket initialized') console.log('Socket initialized')

View file

@ -4,6 +4,7 @@ import LazySeriesCard from '@/components/cards/LazySeriesCard'
import LazyCollectionCard from '@/components/cards/LazyCollectionCard' import LazyCollectionCard from '@/components/cards/LazyCollectionCard'
import LazyPlaylistCard from '@/components/cards/LazyPlaylistCard' import LazyPlaylistCard from '@/components/cards/LazyPlaylistCard'
import LazyAlbumCard from '@/components/cards/LazyAlbumCard' import LazyAlbumCard from '@/components/cards/LazyAlbumCard'
import AuthorCard from '@/components/cards/AuthorCard'
export default { export default {
data() { data() {
@ -20,6 +21,7 @@ export default {
if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard) if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard)
if (this.entityName === 'playlists') return Vue.extend(LazyPlaylistCard) if (this.entityName === 'playlists') return Vue.extend(LazyPlaylistCard)
if (this.entityName === 'albums') return Vue.extend(LazyAlbumCard) if (this.entityName === 'albums') return Vue.extend(LazyAlbumCard)
if (this.entityName === 'authors') return Vue.extend(AuthorCard)
return Vue.extend(LazyBookCard) return Vue.extend(LazyBookCard)
}, },
getComponentName() { getComponentName() {
@ -27,6 +29,7 @@ export default {
if (this.entityName === 'collections') return 'cards-lazy-collection-card' if (this.entityName === 'collections') return 'cards-lazy-collection-card'
if (this.entityName === 'playlists') return 'cards-lazy-playlist-card' if (this.entityName === 'playlists') return 'cards-lazy-playlist-card'
if (this.entityName === 'albums') return 'cards-lazy-album-card' if (this.entityName === 'albums') return 'cards-lazy-album-card'
if (this.entityName === 'authors') return 'cards-author-card'
return 'cards-lazy-book-card' return 'cards-lazy-book-card'
}, },
async setCardSize() { async setCardSize() {
@ -46,13 +49,14 @@ export default {
props.orderBy = this.seriesSortBy props.orderBy = this.seriesSortBy
} }
const instance = new ComponentClass({ const instance = new ComponentClass({
propsData: props propsData: props,
parent: this
}) })
instance.$mount() instance.$mount()
this.resizeObserver = new ResizeObserver((entries) => { this.resizeObserver = new ResizeObserver((entries) => {
for (let entry of entries) { for (let entry of entries) {
this.cardWidth = entry.contentRect.width this.cardWidth = entry.borderBoxSize[0].inlineSize
this.cardHeight = entry.contentRect.height this.cardHeight = entry.borderBoxSize[0].blockSize
this.resizeObserver.disconnect() this.resizeObserver.disconnect()
this.$refs.bookshelf.removeChild(instance.$el) this.$refs.bookshelf.removeChild(instance.$el)
} }
@ -72,7 +76,7 @@ export default {
}) })
const timeAfter = performance.now() const timeAfter = performance.now()
}, },
async mountEntityCard(index) { mountEntityCard(index) {
var shelf = Math.floor(index / this.entitiesPerShelf) var shelf = Math.floor(index / this.entitiesPerShelf)
var shelfEl = document.getElementById(`shelf-${shelf}`) var shelfEl = document.getElementById(`shelf-${shelf}`)
if (!shelfEl) { if (!shelfEl) {
@ -114,6 +118,7 @@ export default {
const _this = this const _this = this
const instance = new ComponentClass({ const instance = new ComponentClass({
propsData: props, propsData: props,
parent: this,
created() { created() {
this.$on('edit', (entity) => { this.$on('edit', (entity) => {
if (_this.editEntity) _this.editEntity(entity) if (_this.editEntity) _this.editEntity(entity)

View file

@ -28,10 +28,8 @@ export default {
var validOtherFiles = [] var validOtherFiles = []
var ignoredFiles = [] var ignoredFiles = []
files.forEach((file) => { files.forEach((file) => {
// var filetype = this.checkFileType(file.name)
if (!file.filetype) ignoredFiles.push(file) if (!file.filetype) ignoredFiles.push(file)
else { else {
// file.filetype = filetype
if (file.filetype === 'audio' || (file.filetype === 'ebook' && mediaType === 'book')) validItemFiles.push(file) if (file.filetype === 'audio' || (file.filetype === 'ebook' && mediaType === 'book')) validItemFiles.push(file)
else validOtherFiles.push(file) else validOtherFiles.push(file)
} }
@ -165,7 +163,7 @@ export default {
var firstBookPath = Path.dirname(firstBookFile.filepath) var firstBookPath = Path.dirname(firstBookFile.filepath)
var dirs = firstBookPath.split('/').filter(d => !!d && d !== '.') var dirs = firstBookPath.split('/').filter((d) => !!d && d !== '.')
if (dirs.length) { if (dirs.length) {
audiobook.title = dirs.pop() audiobook.title = dirs.pop()
if (dirs.length > 1) { if (dirs.length > 1) {
@ -189,7 +187,7 @@ export default {
var firstAudioFile = podcast.itemFiles[0] var firstAudioFile = podcast.itemFiles[0]
if (!firstAudioFile.filepath) return podcast // No path if (!firstAudioFile.filepath) return podcast // No path
var firstPath = Path.dirname(firstAudioFile.filepath) var firstPath = Path.dirname(firstAudioFile.filepath)
var dirs = firstPath.split('/').filter(d => !!d && d !== '.') var dirs = firstPath.split('/').filter((d) => !!d && d !== '.')
if (dirs.length) { if (dirs.length) {
podcast.title = dirs.length > 1 ? dirs[1] : dirs[0] podcast.title = dirs.length > 1 ? dirs[1] : dirs[0]
} else { } else {
@ -212,13 +210,15 @@ export default {
} }
var ignoredFiles = itemData.ignoredFiles var ignoredFiles = itemData.ignoredFiles
var index = 1 var index = 1
var items = itemData.items.filter((ab) => { var items = itemData.items
.filter((ab) => {
if (!ab.itemFiles.length) { if (!ab.itemFiles.length) {
if (ab.otherFiles.length) ignoredFiles = ignoredFiles.concat(ab.otherFiles) if (ab.otherFiles.length) ignoredFiles = ignoredFiles.concat(ab.otherFiles)
if (ab.ignoredFiles.length) ignoredFiles = ignoredFiles.concat(ab.ignoredFiles) if (ab.ignoredFiles.length) ignoredFiles = ignoredFiles.concat(ab.ignoredFiles)
} }
return ab.itemFiles.length return ab.itemFiles.length
}).map(ab => this.cleanItem(ab, mediaType, index++)) })
.map((ab) => this.cleanItem(ab, mediaType, index++))
return { return {
items, items,
ignoredFiles ignoredFiles
@ -259,7 +259,7 @@ export default {
otherFiles.forEach((file) => { otherFiles.forEach((file) => {
var dir = Path.dirname(file.filepath) var dir = Path.dirname(file.filepath)
var findItem = Object.values(itemMap).find(b => dir.startsWith(b.path)) var findItem = Object.values(itemMap).find((b) => dir.startsWith(b.path))
if (findItem) { if (findItem) {
findItem.otherFiles.push(file) findItem.otherFiles.push(file)
} else { } else {
@ -270,18 +270,18 @@ export default {
var items = [] var items = []
var index = 1 var index = 1
// If book media type and all files are audio files then treat each one as an audiobook // If book media type and all files are audio files then treat each one as an audiobook
if (itemMap[''] && !otherFiles.length && mediaType === 'book' && !itemMap[''].itemFiles.some(f => f.filetype !== 'audio')) { if (itemMap[''] && !otherFiles.length && mediaType === 'book' && !itemMap[''].itemFiles.some((f) => f.filetype !== 'audio')) {
items = itemMap[''].itemFiles.map((audioFile) => { items = itemMap[''].itemFiles.map((audioFile) => {
return this.cleanItem({ itemFiles: [audioFile], otherFiles: [], ignoredFiles: [] }, mediaType, index++) return this.cleanItem({ itemFiles: [audioFile], otherFiles: [], ignoredFiles: [] }, mediaType, index++)
}) })
} else { } else {
items = Object.values(itemMap).map(i => this.cleanItem(i, mediaType, index++)) items = Object.values(itemMap).map((i) => this.cleanItem(i, mediaType, index++))
} }
return { return {
items, items,
ignoredFiles: ignoredFiles ignoredFiles: ignoredFiles
} }
}, }
} }
} }

View file

@ -1,19 +1,24 @@
const pkg = require('./package.json') const pkg = require('./package.json')
const routerBasePath = process.env.ROUTER_BASE_PATH || ''
const serverHostUrl = process.env.NODE_ENV === 'production' ? '' : 'http://localhost:3333'
const serverPaths = ['api/', 'public/', 'hls/', 'auth/', 'feed/', 'status', 'login', 'logout', 'init']
const proxy = Object.fromEntries(serverPaths.map((path) => [`${routerBasePath}/${path}`, { target: process.env.NODE_ENV !== 'production' ? serverHostUrl : '/' }]))
module.exports = { module.exports = {
// Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode // Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode
ssr: false, ssr: false,
target: 'static', target: 'static',
dev: process.env.NODE_ENV !== 'production', dev: process.env.NODE_ENV !== 'production',
env: { env: {
serverUrl: process.env.NODE_ENV === 'production' ? process.env.ROUTER_BASE_PATH || '' : 'http://localhost:3333', serverUrl: serverHostUrl + routerBasePath,
chromecastReceiver: 'FD1F76C5' chromecastReceiver: 'FD1F76C5'
}, },
telemetry: false, telemetry: false,
publicRuntimeConfig: { publicRuntimeConfig: {
version: pkg.version, version: pkg.version,
routerBasePath: process.env.ROUTER_BASE_PATH || '' routerBasePath
}, },
// Global page headers: https://go.nuxtjs.dev/config-head // Global page headers: https://go.nuxtjs.dev/config-head
@ -22,38 +27,23 @@ module.exports = {
htmlAttrs: { htmlAttrs: {
lang: 'en' lang: 'en'
}, },
meta: [ meta: [{ charset: 'utf-8' }, { name: 'viewport', content: 'width=device-width, initial-scale=1' }, { hid: 'description', name: 'description', content: '' }, { hid: 'robots', name: 'robots', content: 'noindex' }],
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' },
{ hid: 'robots', name: 'robots', content: 'noindex' }
],
script: [], script: [],
link: [ link: [
{ rel: 'icon', type: 'image/x-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/favicon.ico' }, { rel: 'icon', type: 'image/x-icon', href: routerBasePath + '/favicon.ico' },
{ rel: 'apple-touch-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/ios_icon.png' } { rel: 'apple-touch-icon', href: routerBasePath + '/ios_icon.png' }
] ]
}, },
router: { router: {
base: process.env.ROUTER_BASE_PATH || '' base: routerBasePath
}, },
// Global CSS: https://go.nuxtjs.dev/config-css // Global CSS: https://go.nuxtjs.dev/config-css
css: [ css: ['@/assets/tailwind.css', '@/assets/app.css'],
'@/assets/tailwind.css',
'@/assets/app.css'
],
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins // Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: [ plugins: ['@/plugins/constants.js', '@/plugins/init.client.js', '@/plugins/axios.js', '@/plugins/toast.js', '@/plugins/utils.js', '@/plugins/i18n.js'],
'@/plugins/constants.js',
'@/plugins/init.client.js',
'@/plugins/axios.js',
'@/plugins/toast.js',
'@/plugins/utils.js',
'@/plugins/i18n.js'
],
// Auto import components: https://go.nuxtjs.dev/config-components // Auto import components: https://go.nuxtjs.dev/config-components
components: true, components: true,
@ -65,30 +55,25 @@ module.exports = {
], ],
// Modules: https://go.nuxtjs.dev/config-modules // Modules: https://go.nuxtjs.dev/config-modules
modules: [ modules: ['nuxt-socket-io', '@nuxtjs/axios', '@nuxtjs/proxy'],
'nuxt-socket-io',
'@nuxtjs/axios',
'@nuxtjs/proxy'
],
proxy: { proxy,
'/api/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
'/dev/': { target: 'http://localhost:3333', pathRewrite: { '^/dev/': '' } }
},
io: { io: {
sockets: [{ sockets: [
{
name: 'dev', name: 'dev',
url: 'http://localhost:3333' url: serverHostUrl
}, },
{ {
name: 'prod' name: 'prod'
}] }
]
}, },
// Axios module configuration: https://go.nuxtjs.dev/config-axios // Axios module configuration: https://go.nuxtjs.dev/config-axios
axios: { axios: {
baseURL: process.env.ROUTER_BASE_PATH || '' baseURL: routerBasePath
}, },
// nuxt/pwa https://pwa.nuxtjs.org // nuxt/pwa https://pwa.nuxtjs.org
@ -108,11 +93,11 @@ module.exports = {
background_color: '#232323', background_color: '#232323',
icons: [ icons: [
{ {
src: (process.env.ROUTER_BASE_PATH || '') + '/icon.svg', src: routerBasePath + '/icon.svg',
sizes: 'any' sizes: 'any'
}, },
{ {
src: (process.env.ROUTER_BASE_PATH || '') + '/icon192.png', src: routerBasePath + '/icon192.png',
type: 'image/png', type: 'image/png',
sizes: 'any' sizes: 'any'
} }
@ -132,7 +117,7 @@ module.exports = {
postcssOptions: { postcssOptions: {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {}, autoprefixer: {}
} }
} }
} }
@ -156,5 +141,5 @@ module.exports = {
*/ */
devServerHandlers: [], devServerHandlers: [],
ignore: ["**/*.test.*", "**/*.cy.*"] ignore: ['**/*.test.*', '**/*.cy.*']
} }

View file

@ -1,12 +1,12 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.13.4", "version": "2.17.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.13.4", "version": "2.17.2",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@nuxtjs/axios": "^5.13.6", "@nuxtjs/axios": "^5.13.6",

View file

@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-client", "name": "audiobookshelf-client",
"version": "2.13.4", "version": "2.17.2",
"buildNumber": 1, "buildNumber": 1,
"description": "Self-hosted audiobook and podcast client", "description": "Self-hosted audiobook and podcast client",
"main": "index.js", "main": "index.js",

View file

@ -32,9 +32,48 @@
</form> </form>
</div> </div>
<div v-if="showEreaderTable">
<div class="w-full h-px bg-white/10 my-4" />
<app-settings-content :header-text="$strings.HeaderEreaderDevices">
<template #header-items>
<div class="flex-grow" />
<ui-btn color="primary" small @click="addNewDeviceClick">{{ $strings.ButtonAddDevice }}</ui-btn>
</template>
<table v-if="ereaderDevices.length" class="tracksTable mt-4">
<tr>
<th class="text-left">{{ $strings.LabelName }}</th>
<th class="text-left">{{ $strings.LabelEmail }}</th>
<th class="w-40"></th>
</tr>
<tr v-for="device in ereaderDevices" :key="device.name">
<td>
<p class="text-sm md:text-base text-gray-100">{{ device.name }}</p>
</td>
<td class="text-left">
<p class="text-sm md:text-base text-gray-100">{{ device.email }}</p>
</td>
<td class="w-40">
<div class="flex justify-end items-center h-10">
<ui-icon-btn icon="edit" borderless :size="8" icon-font-size="1.1rem" :disabled="deletingDeviceName === device.name || device.users?.length !== 1" class="mx-1" @click="editDeviceClick(device)" />
<ui-icon-btn icon="delete" borderless :size="8" icon-font-size="1.1rem" :disabled="deletingDeviceName === device.name || device.users?.length !== 1" @click="deleteDeviceClick(device)" />
</div>
</td>
</tr>
</table>
<div v-else-if="!loading" class="text-center py-4">
<p class="text-lg text-gray-100">{{ $strings.MessageNoDevices }}</p>
</div>
</app-settings-content>
</div>
<div class="py-4 mt-8 flex"> <div class="py-4 mt-8 flex">
<ui-btn color="primary flex items-center text-lg" @click="logout"><span class="material-symbols mr-4 icon-text">logout</span>{{ $strings.ButtonLogout }}</ui-btn> <ui-btn color="primary flex items-center text-lg" @click="logout"><span class="material-symbols mr-4 icon-text">logout</span>{{ $strings.ButtonLogout }}</ui-btn>
</div> </div>
<modals-emails-user-e-reader-device-modal v-model="showEReaderDeviceModal" :existing-devices="revisedEreaderDevices" :ereader-device="selectedEReaderDevice" @update="ereaderDevicesUpdated" />
</div> </div>
</div> </div>
</template> </template>
@ -43,11 +82,20 @@
export default { export default {
data() { data() {
return { return {
loading: false,
password: null, password: null,
newPassword: null, newPassword: null,
confirmPassword: null, confirmPassword: null,
changingPassword: false, changingPassword: false,
selectedLanguage: '' selectedLanguage: '',
newEReaderDevice: {
name: '',
email: ''
},
ereaderDevices: [],
deletingDeviceName: null,
selectedEReaderDevice: null,
showEReaderDeviceModal: false
} }
}, },
computed: { computed: {
@ -75,6 +123,12 @@ export default {
}, },
showChangePasswordForm() { showChangePasswordForm() {
return !this.isGuest && this.isPasswordAuthEnabled return !this.isGuest && this.isPasswordAuthEnabled
},
showEreaderTable() {
return this.usertype !== 'root' && this.usertype !== 'admin' && this.user.permissions?.createEreader
},
revisedEreaderDevices() {
return this.ereaderDevices.filter((device) => device.users?.length === 1)
} }
}, },
methods: { methods: {
@ -142,10 +196,52 @@ export default {
this.$toast.error(this.$strings.ToastUnknownError) this.$toast.error(this.$strings.ToastUnknownError)
this.changingPassword = false this.changingPassword = false
}) })
},
addNewDeviceClick() {
this.selectedEReaderDevice = null
this.showEReaderDeviceModal = true
},
editDeviceClick(device) {
this.selectedEReaderDevice = device
this.showEReaderDeviceModal = true
},
deleteDeviceClick(device) {
const payload = {
message: this.$getString('MessageConfirmDeleteDevice', [device.name]),
callback: (confirmed) => {
if (confirmed) {
this.deleteDevice(device)
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
deleteDevice(device) {
const payload = {
ereaderDevices: this.revisedEreaderDevices.filter((d) => d.name !== device.name)
}
this.deletingDeviceName = device.name
this.$axios
.$post(`/api/me/ereader-devices`, payload)
.then((data) => {
this.ereaderDevicesUpdated(data.ereaderDevices)
})
.catch((error) => {
console.error('Failed to delete device', error)
this.$toast.error(this.$strings.ToastRemoveFailed)
})
.finally(() => {
this.deletingDeviceName = null
})
},
ereaderDevicesUpdated(ereaderDevices) {
this.ereaderDevices = ereaderDevices
} }
}, },
mounted() { mounted() {
this.selectedLanguage = this.$languageCodes.current this.selectedLanguage = this.$languageCodes.current
this.ereaderDevices = this.$store.state.libraries.ereaderDevices || []
} }
} }
</script> </script>

View file

@ -415,7 +415,7 @@ export default {
const audioEl = this.audioEl || document.createElement('audio') const audioEl = this.audioEl || document.createElement('audio')
var src = audioTrack.contentUrl + `?token=${this.userToken}` var src = audioTrack.contentUrl + `?token=${this.userToken}`
if (this.$isDev) { if (this.$isDev) {
src = `http://localhost:3333${this.$config.routerBasePath}${src}` src = `${process.env.serverUrl}${src}`
} }
audioEl.src = src audioEl.src = src
@ -486,7 +486,7 @@ export default {
.then((data) => { .then((data) => {
this.saving = false this.saving = false
if (data.updated) { if (data.updated) {
this.$toast.success('Chapters updated') this.$toast.success(this.$strings.ToastChaptersUpdated)
if (this.previousRoute) { if (this.previousRoute) {
this.$router.push(this.previousRoute) this.$router.push(this.previousRoute)
} else { } else {
@ -499,7 +499,7 @@ export default {
.catch((error) => { .catch((error) => {
this.saving = false this.saving = false
console.error('Failed to update chapters', error) console.error('Failed to update chapters', error)
this.$toast.error('Failed to update chapters') this.$toast.error(this.$strings.ToastFailedToUpdate)
}) })
}, },
applyChapterNamesOnly() { applyChapterNamesOnly() {
@ -533,7 +533,7 @@ export default {
}, },
findChapters() { findChapters() {
if (!this.asinInput) { if (!this.asinInput) {
this.$toast.error('Must input an ASIN') this.$toast.error(this.$strings.ToastAsinRequired)
return return
} }

View file

@ -63,11 +63,11 @@
<div class="w-full max-w-4xl mx-auto"> <div class="w-full max-w-4xl mx-auto">
<!-- queued alert --> <!-- queued alert -->
<widgets-alert v-if="isMetadataEmbedQueued" type="warning" class="mb-4"> <widgets-alert v-if="isMetadataEmbedQueued" type="warning" class="mb-4">
<p class="text-lg">Audiobook is queued for metadata embed ({{ queuedEmbedLIds.length }} in queue)</p> <p class="text-lg">{{ $getString('MessageEmbedQueue', [queuedEmbedLIds.length]) }}</p>
</widgets-alert> </widgets-alert>
<!-- metadata embed action buttons --> <!-- metadata embed action buttons -->
<div v-else-if="isEmbedTool" class="w-full flex justify-end items-center mb-4"> <div v-else-if="isEmbedTool" class="w-full flex justify-end items-center mb-4">
<ui-checkbox v-if="!isTaskFinished" v-model="shouldBackupAudioFiles" :disabled="processing" label="Backup audio files" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" @input="toggleBackupAudioFiles" /> <ui-checkbox v-if="!isTaskFinished" v-model="shouldBackupAudioFiles" :disabled="processing" :label="$strings.LabelBackupAudioFiles" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" @input="toggleBackupAudioFiles" />
<div class="flex-grow" /> <div class="flex-grow" />
@ -78,7 +78,7 @@
<!-- m4b embed action buttons --> <!-- m4b embed action buttons -->
<div v-else class="w-full flex items-center mb-4"> <div v-else class="w-full flex items-center mb-4">
<button :disabled="processing" class="text-sm uppercase text-gray-200 flex items-center pt-px pl-1 pr-2 hover:bg-white/5 rounded-md" @click="showEncodeOptions = !showEncodeOptions"> <button :disabled="processing" class="text-sm uppercase text-gray-200 flex items-center pt-px pl-1 pr-2 hover:bg-white/5 rounded-md" @click="showEncodeOptions = !showEncodeOptions">
<span class="material-symbols text-xl">{{ showEncodeOptions || usingCustomEncodeOptions ? 'check_box' : 'check_box_outline_blank' }}</span> <span class="pl-1">Use Advanced Options</span> <span class="material-symbols text-xl">{{ showEncodeOptions || usingCustomEncodeOptions ? 'check_box' : 'check_box_outline_blank' }}</span> <span class="pl-1">{{ $strings.LabelUseAdvancedOptions }}</span>
</button> </button>
<div class="flex-grow" /> <div class="flex-grow" />
@ -94,11 +94,11 @@
<transition name="slide"> <transition name="slide">
<div v-if="showEncodeOptions || usingCustomEncodeOptions" class="mb-4 pb-4 border-b border-white/10"> <div v-if="showEncodeOptions || usingCustomEncodeOptions" class="mb-4 pb-4 border-b border-white/10">
<div class="flex flex-wrap -mx-2"> <div class="flex flex-wrap -mx-2">
<ui-text-input-with-label ref="bitrateInput" v-model="encodingOptions.bitrate" :disabled="processing || isTaskFinished" :label="'Audio Bitrate (e.g. 128k)'" class="m-2 max-w-40" @input="bitrateChanged" /> <ui-text-input-with-label ref="bitrateInput" v-model="encodingOptions.bitrate" :disabled="processing || isTaskFinished" :label="$strings.LabelAudioBitrate" class="m-2 max-w-40" @input="bitrateChanged" />
<ui-text-input-with-label ref="channelsInput" v-model="encodingOptions.channels" :disabled="processing || isTaskFinished" :label="'Audio Channels (1 or 2)'" class="m-2 max-w-40" @input="channelsChanged" /> <ui-text-input-with-label ref="channelsInput" v-model="encodingOptions.channels" :disabled="processing || isTaskFinished" :label="$strings.LabelAudioChannels" class="m-2 max-w-40" @input="channelsChanged" />
<ui-text-input-with-label ref="codecInput" v-model="encodingOptions.codec" :disabled="processing || isTaskFinished" :label="'Audio Codec'" class="m-2 max-w-40" @input="codecChanged" /> <ui-text-input-with-label ref="codecInput" v-model="encodingOptions.codec" :disabled="processing || isTaskFinished" :label="$strings.LabelAudioCodec" class="m-2 max-w-40" @input="codecChanged" />
</div> </div>
<p class="text-sm text-warning">Warning: Do not update these settings unless you are familiar with ffmpeg encoding options.</p> <p class="text-sm text-warning">{{ $strings.LabelEncodingWarningAdvancedSettings }}</p>
</div> </div>
</transition> </transition>
</div> </div>
@ -106,36 +106,36 @@
<div class="mb-4"> <div class="mb-4">
<div v-if="isEmbedTool" class="flex items-start mb-2"> <div v-if="isEmbedTool" class="flex items-start mb-2">
<span class="material-symbols text-base text-warning pt-1">star</span> <span class="material-symbols text-base text-warning pt-1">star</span>
<p class="text-gray-200 ml-2">Metadata will be embedded in the audio tracks inside your audiobook folder.</p> <p class="text-gray-200 ml-2">{{ $strings.LabelEncodingInfoEmbedded }}</p>
</div> </div>
<div v-else class="flex items-start mb-2"> <div v-else class="flex items-start mb-2">
<span class="material-symbols text-base text-warning pt-1">star</span> <span class="material-symbols text-base text-warning pt-1">star</span>
<p class="text-gray-200 ml-2"> <p class="text-gray-200 ml-2">
Finished M4B will be put into your audiobook folder at <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">.../{{ libraryItemRelPath }}/</span>. {{ $strings.LabelEncodingFinishedM4B }} <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">.../{{ libraryItemRelPath }}/</span>.
</p> </p>
</div> </div>
<div v-if="shouldBackupAudioFiles || isM4BTool" class="flex items-start mb-2"> <div v-if="shouldBackupAudioFiles || isM4BTool" class="flex items-start mb-2">
<span class="material-symbols text-base text-warning pt-1">star</span> <span class="material-symbols text-base text-warning pt-1">star</span>
<p class="text-gray-200 ml-2"> <p class="text-gray-200 ml-2">
A backup of your original audio files will be stored in <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">/metadata/cache/items/{{ libraryItemId }}/</span>. Make sure to periodically purge items cache. {{ $strings.LabelEncodingBackupLocation }} <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">/metadata/cache/items/{{ libraryItemId }}/</span>. {{ $strings.LabelEncodingClearItemCache }}
</p> </p>
</div> </div>
<div v-if="isEmbedTool && audioFiles.length > 1" class="flex items-start mb-2"> <div v-if="isEmbedTool && audioFiles.length > 1" class="flex items-start mb-2">
<span class="material-symbols text-base text-warning pt-1">star</span> <span class="material-symbols text-base text-warning pt-1">star</span>
<p class="text-gray-200 ml-2">Chapters are not embedded in multi-track audiobooks.</p> <p class="text-gray-200 ml-2">{{ $strings.LabelEncodingChaptersNotEmbedded }}</p>
</div> </div>
<div v-if="isM4BTool" class="flex items-start mb-2"> <div v-if="isM4BTool" class="flex items-start mb-2">
<span class="material-symbols text-base text-warning pt-1">star</span> <span class="material-symbols text-base text-warning pt-1">star</span>
<p class="text-gray-200 ml-2">Encoding can take up to 30 minutes.</p> <p class="text-gray-200 ml-2">{{ $strings.LabelEncodingTimeWarning }}</p>
</div> </div>
<div v-if="isM4BTool" class="flex items-start mb-2"> <div v-if="isM4BTool" class="flex items-start mb-2">
<span class="material-symbols text-base text-warning pt-1">star</span> <span class="material-symbols text-base text-warning pt-1">star</span>
<p class="text-gray-200 ml-2">If you have the watcher disabled you will need to re-scan this audiobook afterwards.</p> <p class="text-gray-200 ml-2">{{ $strings.LabelEncodingWatcherDisabled }}</p>
</div> </div>
<div class="flex items-start mb-2"> <div class="flex items-start mb-2">
<span class="material-symbols text-base text-warning pt-1">star</span> <span class="material-symbols text-base text-warning pt-1">star</span>
<p class="text-gray-200 ml-2">Once the task is started you can navigate away from this page.</p> <p class="text-gray-200 ml-2">{{ $strings.LabelEncodingStartedNavigation }}</p>
</div> </div>
</div> </div>
</div> </div>
@ -269,11 +269,11 @@ export default {
}, },
availableTools() { availableTools() {
if (this.isSingleM4b) { if (this.isSingleM4b) {
return [{ value: 'embed', text: 'Embed Metadata' }] return [{ value: 'embed', text: this.$strings.LabelToolsEmbedMetadata }]
} else { } else {
return [ return [
{ value: 'embed', text: 'Embed Metadata' }, { value: 'embed', text: this.$strings.LabelToolsEmbedMetadata },
{ value: 'm4b', text: 'M4B Encoder' } { value: 'm4b', text: this.$strings.LabelToolsM4bEncoder }
] ]
} }
}, },
@ -370,7 +370,7 @@ export default {
}, },
embedClick() { embedClick() {
const payload = { const payload = {
message: `Are you sure you want to embed metadata in ${this.audioFiles.length} audio files?`, message: this.$getString('MessageConfirmEmbedMetadataInAudioFiles', [this.audioFiles.length]),
callback: (confirmed) => { callback: (confirmed) => {
if (confirmed) { if (confirmed) {
this.updateAudioFileMetadata() this.updateAudioFileMetadata()

View file

@ -53,7 +53,7 @@ export default {
}) })
if (!author) { if (!author) {
return redirect(`/library/${store.state.libraries.currentLibraryId}/authors`) return redirect(`/library/${store.state.libraries.currentLibraryId}/bookshelf/authors`)
} }
if (store.state.libraries.currentLibraryId !== author.libraryId || !store.state.libraries.filterData) { if (store.state.libraries.currentLibraryId !== author.libraryId || !store.state.libraries.filterData) {
@ -109,7 +109,7 @@ export default {
authorRemoved(author) { authorRemoved(author) {
if (author.id === this.author.id) { if (author.id === this.author.id) {
console.warn('Author was removed') console.warn('Author was removed')
this.$router.replace(`/library/${this.currentLibraryId}/authors`) this.$router.replace(`/library/${this.currentLibraryId}/bookshelf/authors`)
} }
} }
}, },

View file

@ -317,7 +317,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update server settings', error) console.error('Failed to update server settings', error)
this.$toast.error(this.$strings.ToastServerSettingsUpdateFailed) this.$toast.error(this.$strings.ToastFailedToUpdate)
}) })
.finally(() => { .finally(() => {
this.savingSettings = false this.savingSettings = false

View file

@ -162,7 +162,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to save backup path', error) console.error('Failed to save backup path', error)
const errorMsg = error.response?.data || this.$strings.ToastBackupPathUpdateFailed const errorMsg = error.response?.data || this.$strings.ToastFailedToUpdate
this.$toast.error(errorMsg) this.$toast.error(errorMsg)
}) })
.finally(() => { .finally(() => {

View file

@ -292,7 +292,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update email settings', error) console.error('Failed to update email settings', error)
this.$toast.error(this.$strings.ToastEmailSettingsUpdateFailed) this.$toast.error(this.$strings.ToastFailedToUpdate)
}) })
.finally(() => { .finally(() => {
this.savingSettings = false this.savingSettings = false

View file

@ -290,7 +290,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update prefixes', error) console.error('Failed to update prefixes', error)
this.$toast.error(this.$strings.ToastSortingPrefixesUpdateFailed) this.$toast.error(this.$strings.ToastFailedToUpdate)
}) })
.finally(() => { .finally(() => {
this.savingPrefixes = false this.savingPrefixes = false
@ -328,7 +328,6 @@ export default {
.dispatch('updateServerSettings', payload) .dispatch('updateServerSettings', payload)
.then(() => { .then(() => {
this.updatingServerSettings = false this.updatingServerSettings = false
this.$toast.success(this.$strings.ToastServerSettingsUpdateSuccess)
if (payload.language) { if (payload.language) {
// Updating language after save allows for re-rendering // Updating language after save allows for re-rendering
@ -338,7 +337,7 @@ export default {
.catch((error) => { .catch((error) => {
console.error('Failed to update server settings', error) console.error('Failed to update server settings', error)
this.updatingServerSettings = false this.updatingServerSettings = false
this.$toast.error(this.$strings.ToastServerSettingsUpdateFailed) this.$toast.error(this.$strings.ToastFailedToUpdate)
}) })
}, },
initServerSettings() { initServerSettings() {

View file

@ -10,9 +10,9 @@
</template> </template>
<div class="flex justify-between mb-2 place-items-end"> <div class="flex justify-between mb-2 place-items-end">
<ui-text-input ref="input" v-model="search" placeholder="Search filter.." @input="inputUpdate" clearable class="w-full sm:w-40 h-8 text-sm sm:mb-0" /> <ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" clearable class="w-full sm:w-40 h-8 text-sm sm:mb-0" />
<ui-dropdown v-model="newServerSettings.logLevel" label="Server Log Level" :items="logLevelItems" @input="logLevelUpdated" class="w-full sm:w-44" /> <ui-dropdown v-model="newServerSettings.logLevel" :label="$strings.LabelServerLogLevel" :items="logLevelItems" @input="logLevelUpdated" class="w-full sm:w-44" />
</div> </div>
<div class="relative"> <div class="relative">

View file

@ -132,7 +132,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to update notification settings', error) console.error('Failed to update notification settings', error)
this.$toast.error(this.$strings.ToastNotificationSettingsUpdateFailed) this.$toast.error(this.$strings.ToastFailedToUpdate)
}) })
.finally(() => { .finally(() => {
this.savingSettings = false this.savingSettings = false

View file

@ -88,7 +88,7 @@
<ui-dropdown v-model="itemsPerPage" :items="itemsPerPageOptions" small class="w-24 mx-2" @input="updatedItemsPerPage" /> <ui-dropdown v-model="itemsPerPage" :items="itemsPerPageOptions" small class="w-24 mx-2" @input="updatedItemsPerPage" />
</div> </div>
<div class="inline-flex items-center"> <div class="inline-flex items-center">
<p class="text-sm mx-2">Page {{ currentPage + 1 }} of {{ numPages }}</p> <p class="text-sm mx-2">{{ $getString('LabelPaginationPageXOfY', [currentPage + 1, numPages]) }}</p>
<ui-icon-btn icon="arrow_back_ios_new" :size="9" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" /> <ui-icon-btn icon="arrow_back_ios_new" :size="9" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" />
<ui-icon-btn icon="arrow_forward_ios" :size="9" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" /> <ui-icon-btn icon="arrow_forward_ios" :size="9" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" />
</div> </div>
@ -103,7 +103,7 @@
<div v-if="openListeningSessions.length" class="w-full my-8 h-px bg-white/10" /> <div v-if="openListeningSessions.length" class="w-full my-8 h-px bg-white/10" />
<!-- open listening sessions table --> <!-- open listening sessions table -->
<p v-if="openListeningSessions.length" class="text-lg my-4">Open Listening Sessions</p> <p v-if="openListeningSessions.length" class="text-lg my-4">{{ $strings.HeaderOpenListeningSessions }}</p>
<div v-if="openListeningSessions.length" class="block max-w-full"> <div v-if="openListeningSessions.length" class="block max-w-full">
<table class="userSessionsTable"> <table class="userSessionsTable">
<tr class="bg-primary bg-opacity-40"> <tr class="bg-primary bg-opacity-40">

View file

@ -14,7 +14,7 @@
<h1 class="text-xl pl-2">{{ username }}</h1> <h1 class="text-xl pl-2">{{ username }}</h1>
</div> </div>
<div v-if="userToken" class="flex text-xs mt-4"> <div v-if="userToken" class="flex text-xs mt-4">
<ui-text-input-with-label label="API Token" :value="userToken" readonly /> <ui-text-input-with-label :label="$strings.LabelApiToken" :value="userToken" readonly />
<div class="px-1 mt-8 cursor-pointer" @click="copyToClipboard(userToken)"> <div class="px-1 mt-8 cursor-pointer" @click="copyToClipboard(userToken)">
<span class="material-symbols pl-2 text-base">content_copy</span> <span class="material-symbols pl-2 text-base">content_copy</span>

View file

@ -54,7 +54,7 @@
</table> </table>
<div class="flex items-center justify-end py-1"> <div class="flex items-center justify-end py-1">
<ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" /> <ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" />
<p class="text-sm mx-1">Page {{ currentPage + 1 }} of {{ numPages }}</p> <p class="text-sm mx-1">{{ $getString('LabelPaginationPageXOfY', [currentPage + 1, numPages]) }}</p>
<ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" /> <ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" />
</div> </div>
</div> </div>

View file

@ -651,6 +651,11 @@ export default {
this.episodesDownloading = this.episodesDownloading.filter((d) => d.id !== episodeDownload.id) this.episodesDownloading = this.episodesDownloading.filter((d) => d.id !== episodeDownload.id)
} }
}, },
episodeDownloadQueueCleared(libraryItemId) {
if (libraryItemId === this.libraryItemId) {
this.episodeDownloadsQueued = []
}
},
rssFeedOpen(data) { rssFeedOpen(data) {
if (data.entityId === this.libraryItemId) { if (data.entityId === this.libraryItemId) {
console.log('RSS Feed Opened', data) console.log('RSS Feed Opened', data)
@ -794,6 +799,7 @@ export default {
this.$root.socket.on('episode_download_queued', this.episodeDownloadQueued) this.$root.socket.on('episode_download_queued', this.episodeDownloadQueued)
this.$root.socket.on('episode_download_started', this.episodeDownloadStarted) this.$root.socket.on('episode_download_started', this.episodeDownloadStarted)
this.$root.socket.on('episode_download_finished', this.episodeDownloadFinished) this.$root.socket.on('episode_download_finished', this.episodeDownloadFinished)
this.$root.socket.on('episode_download_queue_cleared', this.episodeDownloadQueueCleared)
}, },
beforeDestroy() { beforeDestroy() {
this.$eventBus.$off(`${this.libraryItem.id}_updated`, this.libraryItemUpdated) this.$eventBus.$off(`${this.libraryItem.id}_updated`, this.libraryItemUpdated)
@ -805,6 +811,7 @@ export default {
this.$root.socket.off('episode_download_queued', this.episodeDownloadQueued) this.$root.socket.off('episode_download_queued', this.episodeDownloadQueued)
this.$root.socket.off('episode_download_started', this.episodeDownloadStarted) this.$root.socket.off('episode_download_started', this.episodeDownloadStarted)
this.$root.socket.off('episode_download_finished', this.episodeDownloadFinished) this.$root.socket.off('episode_download_finished', this.episodeDownloadFinished)
this.$root.socket.off('episode_download_queue_cleared', this.episodeDownloadQueueCleared)
} }
} }
</script> </script>

View file

@ -1,115 +0,0 @@
<template>
<div class="page" :class="streamLibraryItem ? 'streaming' : ''">
<app-book-shelf-toolbar page="authors" is-home :authors="authors" />
<div id="bookshelf" class="w-full h-full p-8e overflow-y-auto" :style="{ fontSize: sizeMultiplier + 'rem' }">
<!-- Cover size widget -->
<widgets-cover-size-widget class="fixed right-4 z-50" :style="{ bottom: streamLibraryItem ? '181px' : '16px' }" />
<div class="flex flex-wrap justify-center">
<template v-for="author in authorsSorted">
<cards-author-card :key="author.id" :author="author" class="p-3e" @edit="editAuthor" />
</template>
</div>
</div>
</div>
</template>
<script>
export default {
async asyncData({ store, params, redirect, query, app }) {
var libraryId = params.library
var libraryData = await store.dispatch('libraries/fetch', libraryId)
if (!libraryData) {
return redirect('/oops?message=Library not found')
}
const library = libraryData.library
if (library.mediaType === 'podcast') {
return redirect(`/library/${libraryId}`)
}
return {
libraryId
}
},
data() {
return {
loading: true,
authors: []
}
},
computed: {
sizeMultiplier() {
return this.$store.getters['user/getSizeMultiplier']
},
streamLibraryItem() {
return this.$store.state.streamLibraryItem
},
currentLibraryId() {
return this.$store.state.libraries.currentLibraryId
},
selectedAuthor() {
return this.$store.state.globals.selectedAuthor
},
authorSortBy() {
return this.$store.getters['user/getUserSetting']('authorSortBy') || 'name'
},
authorSortDesc() {
return !!this.$store.getters['user/getUserSetting']('authorSortDesc')
},
authorsSorted() {
const sortProp = this.authorSortBy
const bDesc = this.authorSortDesc ? -1 : 1
return this.authors.sort((a, b) => {
if (typeof a[sortProp] === 'number' && typeof b[sortProp] === 'number') {
// Fallback to name sort if equal
if (a[sortProp] === b[sortProp]) return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }) * bDesc
return a[sortProp] > b[sortProp] ? bDesc : -bDesc
}
return a[sortProp]?.localeCompare(b[sortProp], undefined, { sensitivity: 'base' }) * bDesc
})
}
},
methods: {
async init() {
this.authors = await this.$axios
.$get(`/api/libraries/${this.currentLibraryId}/authors`)
.then((response) => response.authors)
.catch((error) => {
console.error('Failed to load authors', error)
return []
})
this.loading = false
},
authorAdded(author) {
if (!this.authors.some((au) => au.id === author.id)) {
this.authors.push(author)
}
},
authorUpdated(author) {
this.authors = this.authors.map((au) => {
if (au.id === author.id) {
return author
}
return au
})
},
authorRemoved(author) {
this.authors = this.authors.filter((au) => au.id !== author.id)
},
editAuthor(author) {
this.$store.commit('globals/showEditAuthorModal', author)
}
},
mounted() {
this.init()
this.$root.socket.on('author_added', this.authorAdded)
this.$root.socket.on('author_updated', this.authorUpdated)
this.$root.socket.on('author_removed', this.authorRemoved)
},
beforeDestroy() {
this.$root.socket.off('author_added', this.authorAdded)
this.$root.socket.off('author_updated', this.authorUpdated)
this.$root.socket.off('author_removed', this.authorRemoved)
}
}
</script>

View file

@ -27,7 +27,7 @@ export default {
// Redirect podcast libraries // Redirect podcast libraries
const library = libraryData.library const library = libraryData.library
if (library.mediaType === 'podcast' && (params.id === 'collections' || params.id === 'series')) { if (library.mediaType === 'podcast' && (params.id === 'collections' || params.id === 'series' || params.id === 'authors')) {
return redirect(`/library/${libraryId}`) return redirect(`/library/${libraryId}`)
} }

View file

@ -120,7 +120,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed to updated narrator', error) console.error('Failed to updated narrator', error)
this.$toast.error('Failed to update narrator') this.$toast.error(this.$strings.ToastFailedToUpdate)
this.loading = false this.loading = false
}) })
}, },

View file

@ -104,9 +104,6 @@ export default {
this.episodesDownloading = this.episodesDownloading.filter((d) => d.id !== episodeDownload.id) this.episodesDownloading = this.episodesDownloading.filter((d) => d.id !== episodeDownload.id)
} }
}, },
episodeDownloadQueueUpdated(downloadQueueDetails) {
this.episodeDownloadsQueued = downloadQueueDetails.queue.filter((q) => q.libraryId == this.libraryId)
},
async loadInitialDownloadQueue() { async loadInitialDownloadQueue() {
this.processing = true this.processing = true
const queuePayload = await this.$axios.$get(`/api/libraries/${this.libraryId}/episode-downloads`).catch((error) => { const queuePayload = await this.$axios.$get(`/api/libraries/${this.libraryId}/episode-downloads`).catch((error) => {
@ -128,7 +125,6 @@ export default {
this.$root.socket.on('episode_download_queued', this.episodeDownloadQueued) this.$root.socket.on('episode_download_queued', this.episodeDownloadQueued)
this.$root.socket.on('episode_download_started', this.episodeDownloadStarted) this.$root.socket.on('episode_download_started', this.episodeDownloadStarted)
this.$root.socket.on('episode_download_finished', this.episodeDownloadFinished) this.$root.socket.on('episode_download_finished', this.episodeDownloadFinished)
this.$root.socket.on('episode_download_queue_updated', this.episodeDownloadQueueUpdated)
} }
}, },
mounted() { mounted() {
@ -138,7 +134,6 @@ export default {
this.$root.socket.off('episode_download_queued', this.episodeDownloadQueued) this.$root.socket.off('episode_download_queued', this.episodeDownloadQueued)
this.$root.socket.off('episode_download_started', this.episodeDownloadStarted) this.$root.socket.off('episode_download_started', this.episodeDownloadStarted)
this.$root.socket.off('episode_download_finished', this.episodeDownloadFinished) this.$root.socket.off('episode_download_finished', this.episodeDownloadFinished)
this.$root.socket.off('episode_download_queue_updated', this.episodeDownloadQueueUpdated)
} }
} }
</script> </script>

View file

@ -5,7 +5,7 @@
<div id="bookshelf" class="w-full overflow-y-auto px-2 py-6 sm:px-4 md:p-12 relative"> <div id="bookshelf" class="w-full overflow-y-auto px-2 py-6 sm:px-4 md:p-12 relative">
<div class="w-full max-w-4xl mx-auto flex"> <div class="w-full max-w-4xl mx-auto flex">
<form @submit.prevent="submit" class="flex flex-grow"> <form @submit.prevent="submit" class="flex flex-grow">
<ui-text-input v-model="searchInput" type="search" :disabled="processing" placeholder="Enter search term or RSS feed URL" class="flex-grow mr-2 text-sm md:text-base" /> <ui-text-input v-model="searchInput" type="search" :disabled="processing" :placeholder="$strings.MessagePodcastSearchField" class="flex-grow mr-2 text-sm md:text-base" />
<ui-btn type="submit" :disabled="processing" class="hidden md:block">{{ $strings.ButtonSubmit }}</ui-btn> <ui-btn type="submit" :disabled="processing" class="hidden md:block">{{ $strings.ButtonSubmit }}</ui-btn>
<ui-btn type="submit" :disabled="processing" class="block md:hidden" small>{{ $strings.ButtonSubmit }}</ui-btn> <ui-btn type="submit" :disabled="processing" class="block md:hidden" small>{{ $strings.ButtonSubmit }}</ui-btn>
</form> </form>
@ -22,7 +22,7 @@
<div class="flex items-center"> <div class="flex items-center">
<a :href="podcast.pageUrl" class="text-base md:text-lg text-gray-200 hover:underline" target="_blank" @click.stop>{{ podcast.title }}</a> <a :href="podcast.pageUrl" class="text-base md:text-lg text-gray-200 hover:underline" target="_blank" @click.stop>{{ podcast.title }}</a>
<widgets-explicit-indicator v-if="podcast.explicit" /> <widgets-explicit-indicator v-if="podcast.explicit" />
<widgets-already-in-library-indicator :already-in-library="podcast.alreadyInLibrary" /> <widgets-already-in-library-indicator v-if="podcast.alreadyInLibrary" />
</div> </div>
<p class="text-sm md:text-base text-gray-300 whitespace-nowrap truncate">{{ $getString('LabelByAuthor', [podcast.artistName]) }}</p> <p class="text-sm md:text-base text-gray-300 whitespace-nowrap truncate">{{ $getString('LabelByAuthor', [podcast.artistName]) }}</p>
<p class="text-xs text-gray-400 leading-5">{{ podcast.genres.join(', ') }}</p> <p class="text-xs text-gray-400 leading-5">{{ podcast.genres.join(', ') }}</p>
@ -108,7 +108,7 @@ export default {
if (!txt || !txt.includes('<opml') || !txt.includes('<outline ')) { if (!txt || !txt.includes('<opml') || !txt.includes('<outline ')) {
// Quick lazy check for valid OPML // Quick lazy check for valid OPML
this.$toast.error('Invalid OPML file <opml> tag not found OR an <outline> tag was not found') this.$toast.error(this.$strings.MessageTaskOpmlParseFastFail)
this.processing = false this.processing = false
return return
} }
@ -117,7 +117,7 @@ export default {
.$post(`/api/podcasts/opml/parse`, { opmlText: txt }) .$post(`/api/podcasts/opml/parse`, { opmlText: txt })
.then((data) => { .then((data) => {
if (!data.feeds?.length) { if (!data.feeds?.length) {
this.$toast.error('No feeds found in OPML file') this.$toast.error(this.$strings.MessageTaskOpmlParseNoneFound)
} else { } else {
this.opmlFeeds = data.feeds || [] this.opmlFeeds = data.feeds || []
this.showOPMLFeedsModal = true this.showOPMLFeedsModal = true
@ -125,7 +125,7 @@ export default {
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error)
this.$toast.error('Failed to parse OPML file') this.$toast.error(this.$strings.MessageTaskOpmlParseFailed)
}) })
.finally(() => { .finally(() => {
this.processing = false this.processing = false
@ -191,7 +191,7 @@ export default {
return return
} }
if (!podcast.feedUrl) { if (!podcast.feedUrl) {
this.$toast.error('Invalid podcast - no feed') this.$toast.error(this.$strings.MessageNoPodcastFeed)
return return
} }
this.processing = true this.processing = true
@ -211,15 +211,15 @@ export default {
async fetchExistentPodcastsInYourLibrary() { async fetchExistentPodcastsInYourLibrary() {
this.processing = true this.processing = true
const podcasts = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/items?page=0&minified=1`).catch((error) => { const podcastsResponse = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/podcast-titles`).catch((error) => {
console.error('Failed to fetch podcasts', error) console.error('Failed to fetch podcasts', error)
return [] return []
}) })
this.existentPodcasts = podcasts.results.map((p) => { this.existentPodcasts = podcastsResponse.podcasts.map((p) => {
return { return {
title: p.media.metadata.title.toLowerCase(), title: p.title.toLowerCase(),
itunesId: p.media.metadata.itunesId, itunesId: p.itunesId,
id: p.id id: p.libraryItemId
} }
}) })
this.processing = false this.processing = false

View file

@ -10,7 +10,7 @@
<p v-if="mediaItemShare.playbackSession.displayAuthor" class="text-lg lg:text-xl text-slate-400 font-semibold text-center mb-1 truncate">{{ mediaItemShare.playbackSession.displayAuthor }}</p> <p v-if="mediaItemShare.playbackSession.displayAuthor" class="text-lg lg:text-xl text-slate-400 font-semibold text-center mb-1 truncate">{{ mediaItemShare.playbackSession.displayAuthor }}</p>
<div class="w-full pt-16"> <div class="w-full pt-16">
<player-ui ref="audioPlayer" :chapters="chapters" :paused="isPaused" :loading="!hasLoaded" :is-podcast="false" hide-bookmarks hide-sleep-timer @playPause="playPause" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setVolume="setVolume" @setPlaybackRate="setPlaybackRate" @seek="seek" /> <player-ui ref="audioPlayer" :chapters="chapters" :current-chapter="currentChapter" :paused="isPaused" :loading="!hasLoaded" :is-podcast="false" hide-bookmarks hide-sleep-timer @playPause="playPause" @jumpForward="jumpForward" @jumpBackward="jumpBackward" @setVolume="setVolume" @setPlaybackRate="setPlaybackRate" @seek="seek" />
</div> </div>
</div> </div>
</div> </div>
@ -51,7 +51,8 @@ export default {
windowHeight: 0, windowHeight: 0,
listeningTimeSinceSync: 0, listeningTimeSinceSync: 0,
coverRgb: null, coverRgb: null,
coverBgIsLight: false coverBgIsLight: false,
currentTime: 0
} }
}, },
computed: { computed: {
@ -60,16 +61,10 @@ export default {
}, },
coverUrl() { coverUrl() {
if (!this.playbackSession.coverPath) return `${this.$config.routerBasePath}/book_placeholder.jpg` if (!this.playbackSession.coverPath) return `${this.$config.routerBasePath}/book_placeholder.jpg`
if (process.env.NODE_ENV === 'development') { return `${this.$config.routerBasePath}/public/share/${this.mediaItemShare.slug}/cover`
return `http://localhost:3333/public/share/${this.mediaItemShare.slug}/cover`
}
return `/public/share/${this.mediaItemShare.slug}/cover`
}, },
audioTracks() { audioTracks() {
return (this.playbackSession.audioTracks || []).map((track) => { return (this.playbackSession.audioTracks || []).map((track) => {
if (process.env.NODE_ENV === 'development') {
track.contentUrl = `${process.env.serverUrl}${track.contentUrl}`
}
track.relativeContentUrl = track.contentUrl track.relativeContentUrl = track.contentUrl
return track return track
}) })
@ -83,6 +78,9 @@ export default {
chapters() { chapters() {
return this.playbackSession.chapters || [] return this.playbackSession.chapters || []
}, },
currentChapter() {
return this.chapters.find((chapter) => chapter.start <= this.currentTime && this.currentTime < chapter.end)
},
coverAspectRatio() { coverAspectRatio() {
const coverAspectRatio = this.playbackSession.coverAspectRatio const coverAspectRatio = this.playbackSession.coverAspectRatio
return coverAspectRatio === this.$constants.BookCoverAspectRatio.STANDARD ? 1.6 : 1 return coverAspectRatio === this.$constants.BookCoverAspectRatio.STANDARD ? 1.6 : 1
@ -154,6 +152,7 @@ export default {
// Update UI // Update UI
this.$refs.audioPlayer.setCurrentTime(time) this.$refs.audioPlayer.setCurrentTime(time)
this.currentTime = time
}, },
setDuration() { setDuration() {
if (!this.localAudioPlayer) return if (!this.localAudioPlayer) return

View file

@ -1,20 +1,20 @@
<template> <template>
<div id="page-wrapper" class="page p-0 sm:p-6 overflow-y-auto" :class="streamLibraryItem ? 'streaming' : ''"> <div id="page-wrapper" class="page p-1 sm:p-6 overflow-y-auto" :class="streamLibraryItem ? 'streaming' : ''">
<div class="w-full max-w-6xl mx-auto"> <div class="w-full max-w-6xl mx-auto">
<!-- Library & folder picker --> <!-- Library & folder picker -->
<div class="flex my-6 -mx-2"> <div class="flex flex-wrap my-6 md:-mx-2">
<div class="w-1/5 px-2"> <div class="w-full md:w-1/5 px-2">
<ui-dropdown v-model="selectedLibraryId" :items="libraryItems" :label="$strings.LabelLibrary" :disabled="!!items.length" @input="libraryChanged" /> <ui-dropdown v-model="selectedLibraryId" :items="libraryItems" :label="$strings.LabelLibrary" :disabled="!!items.length" @input="libraryChanged" />
</div> </div>
<div class="w-3/5 px-2"> <div class="w-full md:w-3/5 px-2">
<ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="!selectedLibraryId || !!items.length" :label="$strings.LabelFolder" /> <ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="!selectedLibraryId || !!items.length" :label="$strings.LabelFolder" />
</div> </div>
<div class="w-1/5 px-2"> <div class="w-full md:w-1/5 px-2">
<ui-text-input-with-label :value="selectedLibraryMediaType" readonly :label="$strings.LabelMediaType" /> <ui-text-input-with-label :value="selectedLibraryMediaType" readonly :label="$strings.LabelMediaType" />
</div> </div>
</div> </div>
<div v-if="!selectedLibraryIsPodcast" class="flex items-center mb-6"> <div v-if="!selectedLibraryIsPodcast" class="flex items-center mb-6 px-2 md:px-0">
<label class="flex cursor-pointer pt-4"> <label class="flex cursor-pointer pt-4">
<ui-toggle-switch v-model="fetchMetadata.enabled" class="inline-flex" /> <ui-toggle-switch v-model="fetchMetadata.enabled" class="inline-flex" />
<span class="pl-2 text-base">{{ $strings.LabelAutoFetchMetadata }}</span> <span class="pl-2 text-base">{{ $strings.LabelAutoFetchMetadata }}</span>
@ -33,13 +33,13 @@
</widgets-alert> </widgets-alert>
<!-- Picker display --> <!-- Picker display -->
<div v-if="!items.length && !ignoredFiles.length" class="w-full mx-auto border border-white border-opacity-20 px-12 pt-12 pb-4 my-12 relative" :class="isDragging ? 'bg-primary bg-opacity-40' : 'border-dashed'"> <div v-if="!items.length && !ignoredFiles.length" class="w-full mx-auto border border-white border-opacity-20 px-4 md:px-12 pt-12 pb-4 my-12 relative" :class="isDragging ? 'bg-primary bg-opacity-40' : 'border-dashed'">
<p class="text-2xl text-center">{{ isDragging ? $strings.LabelUploaderDropFiles : $strings.LabelUploaderDragAndDrop }}</p> <p class="text-2xl text-center">{{ isDragging ? $strings.LabelUploaderDropFiles : isIOS ? $strings.LabelUploaderDragAndDropFilesOnly : $strings.LabelUploaderDragAndDrop }}</p>
<p class="text-center text-sm my-5">{{ $strings.MessageOr }}</p> <p class="text-center text-sm my-5">{{ $strings.MessageOr }}</p>
<div class="w-full max-w-xl mx-auto"> <div class="w-full max-w-xl mx-auto">
<div class="flex"> <div class="flex">
<ui-btn class="w-full mx-1" @click="openFilePicker">{{ $strings.ButtonChooseFiles }}</ui-btn> <ui-btn class="w-full mx-1" @click="openFilePicker">{{ $strings.ButtonChooseFiles }}</ui-btn>
<ui-btn class="w-full mx-1" @click="openFolderPicker">{{ $strings.ButtonChooseAFolder }}</ui-btn> <ui-btn v-if="!isIOS" class="w-full mx-1" @click="openFolderPicker">{{ $strings.ButtonChooseAFolder }} </ui-btn>
</div> </div>
</div> </div>
<div class="pt-8 text-center"> <div class="pt-8 text-center">
@ -48,7 +48,7 @@
</p> </p>
<p class="text-sm text-white text-opacity-70"> <p class="text-sm text-white text-opacity-70">
{{ $strings.NoteUploaderFoldersWithMediaFiles }} <span v-if="selectedLibraryMediaType === 'book'">{{ $strings.NoteUploaderOnlyAudioFiles }}</span> <span v-if="!isIOS">{{ $strings.NoteUploaderFoldersWithMediaFiles }}</span> <span v-if="selectedLibraryMediaType === 'book'">{{ $strings.NoteUploaderOnlyAudioFiles }}</span>
</p> </p>
</div> </div>
</div> </div>
@ -84,8 +84,8 @@
</div> </div>
</div> </div>
<input ref="fileInput" type="file" multiple :accept="inputAccept" class="hidden" @change="inputChanged" /> <input ref="fileInput" type="file" multiple :accept="isIOS ? '' : inputAccept" class="hidden" @change="inputChanged" />
<input ref="fileFolderInput" type="file" webkitdirectory multiple :accept="inputAccept" class="hidden" @change="inputChanged" /> <input ref="fileFolderInput" type="file" webkitdirectory multiple :accept="inputAccept" class="hidden" @change="inputChanged" v-if="!isIOS" />
</div> </div>
</template> </template>
@ -127,6 +127,10 @@ export default {
}) })
return extensions return extensions
}, },
isIOS() {
const ua = window.navigator.userAgent
return /iPad|iPhone|iPod/.test(ua) && !window.MSStream
},
streamLibraryItem() { streamLibraryItem() {
return this.$store.state.streamLibraryItem return this.$store.state.streamLibraryItem
}, },
@ -384,12 +388,6 @@ export default {
else itemsFailed++ else itemsFailed++
this.updateItemCardStatus(item.index, result ? 'success' : 'failed') this.updateItemCardStatus(item.index, result ? 'success' : 'failed')
} }
if (itemsUploaded) {
this.$toast.success(`Successfully uploaded ${itemsUploaded} item${itemsUploaded > 1 ? 's' : ''}`)
}
if (itemsFailed) {
this.$toast.success(`Failed to upload ${itemsFailed} item${itemsFailed > 1 ? 's' : ''}`)
}
this.processing = false this.processing = false
this.uploadFinished = true this.uploadFinished = true
} }

View file

@ -23,10 +23,6 @@ export default class AudioTrack {
get relativeContentUrl() { get relativeContentUrl() {
if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl if (!this.contentUrl || this.contentUrl.startsWith('http')) return this.contentUrl
if (process.env.NODE_ENV === 'development') {
return `${process.env.serverUrl}${this.contentUrl}?token=${this.userToken}`
}
return this.contentUrl + `?token=${this.userToken}` return this.contentUrl + `?token=${this.userToken}`
} }
} }

View file

@ -147,7 +147,7 @@ export default class LocalAudioPlayer extends EventEmitter {
timeoutRetry: { timeoutRetry: {
maxNumRetry: 4, maxNumRetry: 4,
retryDelayMs: 0, retryDelayMs: 0,
maxRetryDelayMs: 0, maxRetryDelayMs: 0
}, },
errorRetry: { errorRetry: {
maxNumRetry: 8, maxNumRetry: 8,
@ -160,7 +160,7 @@ export default class LocalAudioPlayer extends EventEmitter {
} }
return retry return retry
} }
}, }
} }
} }
} }
@ -194,7 +194,7 @@ export default class LocalAudioPlayer extends EventEmitter {
setDirectPlay() { setDirectPlay() {
// Set initial track and track time offset // Set initial track and track time offset
var trackIndex = this.audioTracks.findIndex(t => this.startTime >= t.startOffset && this.startTime < (t.startOffset + t.duration)) var trackIndex = this.audioTracks.findIndex((t) => this.startTime >= t.startOffset && this.startTime < t.startOffset + t.duration)
this.currentTrackIndex = trackIndex >= 0 ? trackIndex : 0 this.currentTrackIndex = trackIndex >= 0 ? trackIndex : 0
this.loadCurrentTrack() this.loadCurrentTrack()
@ -270,7 +270,7 @@ export default class LocalAudioPlayer extends EventEmitter {
// Seeking Direct play // Seeking Direct play
if (time < this.currentTrack.startOffset || time > this.currentTrack.startOffset + this.currentTrack.duration) { if (time < this.currentTrack.startOffset || time > this.currentTrack.startOffset + this.currentTrack.duration) {
// Change Track // Change Track
var trackIndex = this.audioTracks.findIndex(t => time >= t.startOffset && time < (t.startOffset + t.duration)) var trackIndex = this.audioTracks.findIndex((t) => time >= t.startOffset && time < t.startOffset + t.duration)
if (trackIndex >= 0) { if (trackIndex >= 0) {
this.startTime = time this.startTime = time
this.currentTrackIndex = trackIndex this.currentTrackIndex = trackIndex
@ -293,7 +293,6 @@ export default class LocalAudioPlayer extends EventEmitter {
this.player.volume = volume this.player.volume = volume
} }
// Utils // Utils
isValidDuration(duration) { isValidDuration(duration) {
if (duration && !isNaN(duration) && duration !== Number.POSITIVE_INFINITY && duration !== Number.NEGATIVE_INFINITY) { if (duration && !isNaN(duration) && duration !== Number.POSITIVE_INFINITY && duration !== Number.NEGATIVE_INFINITY) {

View file

@ -297,7 +297,6 @@ export default class PlayerHandler {
if (listeningTimeToAdd > 20) { if (listeningTimeToAdd > 20) {
syncData = { syncData = {
timeListened: listeningTimeToAdd, timeListened: listeningTimeToAdd,
duration: this.getDuration(),
currentTime: this.getCurrentTime() currentTime: this.getCurrentTime()
} }
} }
@ -317,7 +316,6 @@ export default class PlayerHandler {
const listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync)) const listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
const syncData = { const syncData = {
timeListened: listeningTimeToAdd, timeListened: listeningTimeToAdd,
duration: this.getDuration(),
currentTime currentTime
} }

View file

@ -1,5 +1,5 @@
export default function ({ $axios, store, $config }) { export default function ({ $axios, store, $config }) {
$axios.onRequest(config => { $axios.onRequest((config) => {
if (!config.url) { if (!config.url) {
console.error('Axios request invalid config', config) console.error('Axios request invalid config', config)
return return
@ -13,12 +13,11 @@ export default function ({ $axios, store, $config }) {
} }
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
config.url = `/dev${config.url}`
console.log('Making request to ' + config.url) console.log('Making request to ' + config.url)
} }
}) })
$axios.onError(error => { $axios.onError((error) => {
const code = parseInt(error.response && error.response.status) const code = parseInt(error.response && error.response.status)
const message = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error' const message = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
console.error('Axios error', code, message) console.error('Axios error', code, message)

View file

@ -1,6 +1,6 @@
const SupportedFileTypes = { const SupportedFileTypes = {
image: ['png', 'jpg', 'jpeg', 'webp'], image: ['png', 'jpg', 'jpeg', 'webp'],
audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav', 'webm', 'webma', 'mka', 'awb', 'caf'], audio: ['m4b', 'mp3', 'm4a', 'flac', 'opus', 'ogg', 'oga', 'mp4', 'aac', 'wma', 'aiff', 'wav', 'webm', 'webma', 'mka', 'awb', 'caf', 'mpeg', 'mpg'],
ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'], ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
info: ['nfo'], info: ['nfo'],
text: ['txt'], text: ['txt'],
@ -81,9 +81,7 @@ const Hotkeys = {
} }
} }
export { export { Constants }
Constants
}
export default ({ app }, inject) => { export default ({ app }, inject) => {
inject('constants', Constants) inject('constants', Constants)
inject('keynames', KeyNames) inject('keynames', KeyNames)

View file

@ -72,13 +72,13 @@ export const state = () => ({
} }
], ],
podcastTypes: [ podcastTypes: [
{ text: 'Episodic', value: 'episodic' }, { text: 'Episodic', value: 'episodic', descriptionKey: 'LabelEpisodic' },
{ text: 'Serial', value: 'serial' } { text: 'Serial', value: 'serial', descriptionKey: 'LabelSerial' }
], ],
episodeTypes: [ episodeTypes: [
{ text: 'Full', value: 'full' }, { text: 'Full', value: 'full', descriptionKey: 'LabelFull' },
{ text: 'Trailer', value: 'trailer' }, { text: 'Trailer', value: 'trailer', descriptionKey: 'LabelTrailer' },
{ text: 'Bonus', value: 'bonus' } { text: 'Bonus', value: 'bonus', descriptionKey: 'LabelBonus' }
], ],
libraryIcons: ['database', 'audiobookshelf', 'books-1', 'books-2', 'book-1', 'microphone-1', 'microphone-3', 'radio', 'podcast', 'rss', 'headphones', 'music', 'file-picture', 'rocket', 'power', 'star', 'heart'] libraryIcons: ['database', 'audiobookshelf', 'books-1', 'books-2', 'book-1', 'microphone-1', 'microphone-3', 'radio', 'podcast', 'rss', 'headphones', 'music', 'file-picture', 'rocket', 'power', 'star', 'heart']
}) })
@ -98,13 +98,7 @@ export const getters = {
const userToken = rootGetters['user/getToken'] const userToken = rootGetters['user/getToken']
const lastUpdate = libraryItem.updatedAt || Date.now() const lastUpdate = libraryItem.updatedAt || Date.now()
const libraryItemId = libraryItem.libraryItemId || libraryItem.id // Workaround for /users/:id page showing media progress covers const libraryItemId = libraryItem.libraryItemId || libraryItem.id // Workaround for /users/:id page showing media progress covers
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?ts=${lastUpdate}${raw ? '&raw=1' : ''}`
if (process.env.NODE_ENV !== 'production') {
// Testing
return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}${raw ? '&raw=1' : ''}`
}
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}${raw ? '&raw=1' : ''}`
}, },
getLibraryItemCoverSrcById: getLibraryItemCoverSrcById:
(state, getters, rootState, rootGetters) => (state, getters, rootState, rootGetters) =>
@ -112,11 +106,7 @@ export const getters = {
const placeholder = `${rootState.routerBasePath}/book_placeholder.jpg` const placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
if (!libraryItemId) return placeholder if (!libraryItemId) return placeholder
const userToken = rootGetters['user/getToken'] const userToken = rootGetters['user/getToken']
if (process.env.NODE_ENV !== 'production') { return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?${raw ? '&raw=1' : ''}${timestamp ? `&ts=${timestamp}` : ''}`
// Testing
return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}${raw ? '&raw=1' : ''}${timestamp ? `&ts=${timestamp}` : ''}`
}
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}${raw ? '&raw=1' : ''}${timestamp ? `&ts=${timestamp}` : ''}`
}, },
getIsBatchSelectingMediaItems: (state) => { getIsBatchSelectingMediaItems: (state) => {
return state.selectedMediaItems.length return state.selectedMediaItems.length

View file

@ -240,7 +240,8 @@ export const mutations = {
series: [], series: [],
narrators: [], narrators: [],
languages: [], languages: [],
publishers: [] publishers: [],
publishedDecades: []
} }
*/ */
const mediaMetadata = libraryItem.media.metadata const mediaMetadata = libraryItem.media.metadata
@ -307,6 +308,16 @@ export const mutations = {
state.filterData.publishers.sort((a, b) => a.localeCompare(b)) state.filterData.publishers.sort((a, b) => a.localeCompare(b))
} }
// Add publishedDecades
if (mediaMetadata.publishedYear && !isNaN(mediaMetadata.publishedYear)) {
const publishedYear = parseInt(mediaMetadata.publishedYear, 10)
const decade = (Math.floor(publishedYear / 10) * 10).toString()
if (!state.filterData.publishedDecades.includes(decade)) {
state.filterData.publishedDecades.push(decade)
state.filterData.publishedDecades.sort((a, b) => a - b)
}
}
// Add language // Add language
if (mediaMetadata.language && !state.filterData.languages.includes(mediaMetadata.language)) { if (mediaMetadata.language && !state.filterData.languages.includes(mediaMetadata.language)) {
state.filterData.languages.push(mediaMetadata.language) state.filterData.languages.push(mediaMetadata.language)

View file

@ -101,7 +101,7 @@ export const actions = {
if (state.settings.orderBy == 'media.metadata.publishedYear') { if (state.settings.orderBy == 'media.metadata.publishedYear') {
settingsUpdate.orderBy = 'media.metadata.title' settingsUpdate.orderBy = 'media.metadata.title'
} }
const invalidFilters = ['series', 'authors', 'narrators', 'publishers', 'languages', 'progress', 'issues', 'ebooks', 'abridged'] const invalidFilters = ['series', 'authors', 'narrators', 'publishers', 'publishedDecades', 'languages', 'progress', 'issues', 'ebooks', 'abridged']
const filterByFirstPart = (state.settings.filterBy || '').split('.').shift() const filterByFirstPart = (state.settings.filterBy || '').split('.').shift()
if (invalidFilters.includes(filterByFirstPart)) { if (invalidFilters.includes(filterByFirstPart)) {
settingsUpdate.filterBy = 'all' settingsUpdate.filterBy = 'all'

156
client/strings/ar.json Normal file
View file

@ -0,0 +1,156 @@
{
"ButtonAdd": "إضافة",
"ButtonAddChapters": "إضافة الفصول",
"ButtonAddDevice": "إضافة جهاز",
"ButtonAddLibrary": "إضافة مكتبة",
"ButtonAddPodcasts": "إضافة بودكاست",
"ButtonAddUser": "إضافة مستخدم",
"ButtonAddYourFirstLibrary": "أضف مكتبتك الأولى",
"ButtonApply": "حفظ",
"ButtonApplyChapters": "حفظ الفصول",
"ButtonAuthors": "المؤلفون",
"ButtonBack": "الرجوع",
"ButtonBrowseForFolder": "البحث عن المجلد",
"ButtonCancel": "إلغاء",
"ButtonCancelEncode": "إلغاء الترميز",
"ButtonChangeRootPassword": "تغيير كلمة المرور الرئيسية",
"ButtonCheckAndDownloadNewEpisodes": "التحقق من الحلقات الجديدة وتنزيلها",
"ButtonChooseAFolder": "اختر المجلد",
"ButtonChooseFiles": "اختر الملفات",
"ButtonClearFilter": "تصفية الفرز",
"ButtonCloseFeed": "إغلاق",
"ButtonCloseSession": "إغلاق الجلسة المفتوحة",
"ButtonCollections": "المجموعات",
"ButtonConfigureScanner": "إعدادات الماسح الضوئي",
"ButtonCreate": "إنشاء",
"ButtonCreateBackup": "إنشاء نسخة احتياطية",
"ButtonDelete": "حذف",
"ButtonDownloadQueue": "قائمة",
"ButtonEdit": "تعديل",
"ButtonEditChapters": "تعديل الفصول",
"ButtonEditPodcast": "تعديل البودكاست",
"ButtonEnable": "تفعيل",
"ButtonFireAndFail": "النار والفشل",
"ButtonFireOnTest": "حادثة إطلاق النار",
"ButtonForceReScan": "فرض إعادة المسح",
"ButtonFullPath": "المسار الكامل",
"ButtonHide": "إخفاء",
"ButtonHome": "الرئيسية",
"ButtonIssues": "مشاكل",
"ButtonJumpBackward": "اقفز للخلف",
"ButtonJumpForward": "اقفز للأمام",
"ButtonLatest": "أحدث",
"ButtonLibrary": "المكتبة",
"ButtonLogout": "تسجيل الخروج",
"ButtonLookup": "البحث",
"ButtonManageTracks": "إدارة المقاطع",
"ButtonMapChapterTitles": "مطابقة عناوين الفصول",
"ButtonMatchAllAuthors": "مطابقة كل المؤلفون",
"ButtonMatchBooks": "مطابقة الكتب",
"ButtonNevermind": "لا تهتم",
"ButtonNext": "التالي",
"ButtonNextChapter": "الفصل التالي",
"ButtonNextItemInQueue": "العنصر التالي في قائمة الانتظار",
"ButtonOk": "نعم",
"ButtonOpenFeed": "فتح التغذية",
"ButtonOpenManager": "فتح الإدارة",
"ButtonPause": "تَوَقَّف",
"ButtonPlay": "تشغيل",
"ButtonPlayAll": "تشغيل الكل",
"ButtonPlaying": "مشغل الآن",
"ButtonPlaylists": "قوائم التشغيل",
"ButtonPrevious": "سابِق",
"ButtonPreviousChapter": "الفصل السابق",
"ButtonProbeAudioFile": "فحص ملف الصوت",
"ButtonPurgeAllCache": "مسح كافة ذاكرة التخزين المؤقتة",
"ButtonPurgeItemsCache": "مسح ذاكرة التخزين المؤقتة للعناصر",
"ButtonQueueAddItem": "أضف إلى قائمة الانتظار",
"ButtonQueueRemoveItem": "إزالة من قائمة الانتظار",
"ButtonQuickEmbed": "التضمين السريع",
"ButtonQuickEmbedMetadata": "إدراج سريع للبيانات الوصفية",
"ButtonQuickMatch": "مطابقة سريعة",
"ButtonReScan": "إعادة البحث",
"ButtonRead": "اقرأ",
"ButtonReadLess": "قلص",
"ButtonReadMore": "المزيد",
"ButtonRefresh": "تحديث",
"ButtonRemove": "إزالة",
"ButtonRemoveAll": "إزالة الكل",
"ButtonRemoveAllLibraryItems": "إزالة كافة عناصر المكتبة",
"ButtonRemoveFromContinueListening": "إزالة من متابعة الاستماع",
"ButtonRemoveFromContinueReading": "إزالة من متابعة القراءة",
"ButtonRemoveSeriesFromContinueSeries": "إزالة السلسلة من استمرار السلسلة",
"ButtonReset": "إعادة ضبط",
"ButtonResetToDefault": "إعادة ضبط إلى الوضع الافتراضي",
"ButtonRestore": "إستِعادة",
"ButtonSave": "حفظ",
"ButtonSaveAndClose": "حفظ و إغلاق",
"ButtonSaveTracklist": "حفظ قائمة التشغيل",
"ButtonScan": "تَحَقُق",
"ButtonScanLibrary": "تَحَقُق من المكتبة",
"ButtonSearch": "بحث",
"ButtonSelectFolderPath": "حدد مسار المجلد",
"ButtonSeries": "سلسلة",
"ButtonSetChaptersFromTracks": "تعيين الفصول من الملفات",
"ButtonShare": "نشر",
"ButtonShiftTimes": "أوقات العمل",
"ButtonShow": "عرض",
"ButtonStartM4BEncode": "ابدأ ترميز M4B",
"ButtonStartMetadataEmbed": "ابدأ تضمين البيانات الوصفية",
"ButtonStats": "الإحصائيات",
"ButtonSubmit": "تقديم",
"ButtonTest": "اختبار",
"ButtonUnlinkOpenId": "إلغاء ربط المعرف",
"ButtonUpload": "رفع",
"ButtonUploadBackup": "تحميل النسخة الاحتياطية",
"ButtonUploadCover": "ارفق الغلاف",
"ButtonUploadOPMLFile": "رفع ملف OPML",
"ButtonUserDelete": "حذف المستخدم {0}",
"ButtonUserEdit": "تعديل المستخدم {0}",
"ButtonViewAll": "عرض الكل",
"ButtonYes": "نعم",
"ErrorUploadFetchMetadataAPI": "خطأ في جلب البيانات الوصفية",
"ErrorUploadFetchMetadataNoResults": "لم يتم العثور على البيانات الوصفية - حاول تحديث العنوان و/أو المؤلف",
"ErrorUploadLacksTitle": "يجب أن يكون له عنوان",
"HeaderAccount": "الحساب",
"HeaderAddCustomMetadataProvider": "إضافة موفر بيانات تعريفية مخصص",
"HeaderAdvanced": "متقدم",
"HeaderAppriseNotificationSettings": "إعدادات الإشعارات",
"HeaderAudioTracks": "المسارات الصوتية",
"HeaderAudiobookTools": "أدوات إدارة ملفات الكتب الصوتية",
"HeaderAuthentication": "المصادقة",
"HeaderBackups": "النسخ الاحتياطية",
"HeaderChangePassword": "تغيير كلمة المرور",
"HeaderChapters": "الفصول",
"HeaderChooseAFolder": "اختيار المجلد",
"HeaderCollection": "مجموعة",
"HeaderCollectionItems": "عناصر المجموعة",
"HeaderCover": "الغلاف",
"HeaderCurrentDownloads": "التنزيلات الجارية",
"HeaderCustomMessageOnLogin": "رسالة مخصصة عند تسجيل الدخول",
"HeaderCustomMetadataProviders": "مقدمو البيانات الوصفية المخصصة",
"HeaderDetails": "التفاصيل",
"HeaderDownloadQueue": "تنزيل قائمة الانتظار",
"HeaderEbookFiles": "ملفات الكتب الإلكترونية",
"HeaderEmail": "البريد الإلكتروني",
"HeaderEmailSettings": "إعدادات البريد الإلكتروني",
"HeaderEpisodes": "الحلقات",
"HeaderEreaderDevices": "أجهزة قراءة الكتب الإلكترونية",
"HeaderEreaderSettings": "إعدادات القارئ الإلكتروني",
"HeaderFiles": "ملفات",
"HeaderFindChapters": "البحث عن الفصول",
"HeaderIgnoredFiles": "الملفات المتجاهلة",
"HeaderItemFiles": "ملفات العنصر",
"HeaderItemMetadataUtils": "بيانات تعريف العنصر",
"HeaderLastListeningSession": "آخر جلسة استماع",
"HeaderLatestEpisodes": "أحدث الحلقات",
"HeaderLibraries": "المكتبات",
"HeaderLibraryFiles": "ملفات المكتبة",
"HeaderLibraryStats": "إحصائيات المكتبة",
"HeaderListeningSessions": "جلسات الاستماع",
"HeaderListeningStats": "جلسات الاستماع",
"HeaderLogin": "تسجيل الدخول",
"HeaderLogs": "السجلات",
"HeaderManageGenres": "إدارة الانواع",
"HeaderManageTags": "إدارة العلامات"
}

View file

@ -711,10 +711,8 @@
"PlaceholderNewPlaylist": "Ново име на плейлиста", "PlaceholderNewPlaylist": "Ново име на плейлиста",
"PlaceholderSearch": "Търсене...", "PlaceholderSearch": "Търсене...",
"PlaceholderSearchEpisode": "Търсене на Епизоди...", "PlaceholderSearchEpisode": "Търсене на Епизоди...",
"ToastAccountUpdateFailed": "Неуспешно обновяване на акаунта",
"ToastAccountUpdateSuccess": "Успешно обновяване на акаунта", "ToastAccountUpdateSuccess": "Успешно обновяване на акаунта",
"ToastAuthorImageRemoveSuccess": "Авторската снимка е премахната", "ToastAuthorImageRemoveSuccess": "Авторската снимка е премахната",
"ToastAuthorUpdateFailed": "Неуспешно обновяване на автора",
"ToastAuthorUpdateMerged": "Обновяване на автора сливано", "ToastAuthorUpdateMerged": "Обновяване на автора сливано",
"ToastAuthorUpdateSuccess": "Автора обновен", "ToastAuthorUpdateSuccess": "Автора обновен",
"ToastAuthorUpdateSuccessNoImageFound": "Автор обновен (не е намерена снимка)", "ToastAuthorUpdateSuccessNoImageFound": "Автор обновен (не е намерена снимка)",
@ -728,17 +726,13 @@
"ToastBookmarkCreateFailed": "Неуспешно създаване на отметка", "ToastBookmarkCreateFailed": "Неуспешно създаване на отметка",
"ToastBookmarkCreateSuccess": "Отметката е създадена", "ToastBookmarkCreateSuccess": "Отметката е създадена",
"ToastBookmarkRemoveSuccess": "Отметката е премахната", "ToastBookmarkRemoveSuccess": "Отметката е премахната",
"ToastBookmarkUpdateFailed": "Неуспешно обновяване на отметка",
"ToastBookmarkUpdateSuccess": "Отметката е обновена", "ToastBookmarkUpdateSuccess": "Отметката е обновена",
"ToastChaptersHaveErrors": "Главите имат грешки", "ToastChaptersHaveErrors": "Главите имат грешки",
"ToastChaptersMustHaveTitles": "Главите трябва да имат заглавия", "ToastChaptersMustHaveTitles": "Главите трябва да имат заглавия",
"ToastCollectionItemsRemoveSuccess": "Елемент(и) премахнати от колекция", "ToastCollectionItemsRemoveSuccess": "Елемент(и) премахнати от колекция",
"ToastCollectionRemoveSuccess": "Колекцията е премахната", "ToastCollectionRemoveSuccess": "Колекцията е премахната",
"ToastCollectionUpdateFailed": "Неуспешно обновяване на колекция",
"ToastCollectionUpdateSuccess": "Колекцията е обновена", "ToastCollectionUpdateSuccess": "Колекцията е обновена",
"ToastItemCoverUpdateFailed": "Неуспешно обновяване на корица на елемент",
"ToastItemCoverUpdateSuccess": "Корицата на елемента е обновена", "ToastItemCoverUpdateSuccess": "Корицата на елемента е обновена",
"ToastItemDetailsUpdateFailed": "Неуспешно обновяване на детайли на елемент",
"ToastItemDetailsUpdateSuccess": "Детайлите на елемента са обновени", "ToastItemDetailsUpdateSuccess": "Детайлите на елемента са обновени",
"ToastItemMarkedAsFinishedFailed": "Неуспешно маркиране като завършено", "ToastItemMarkedAsFinishedFailed": "Неуспешно маркиране като завършено",
"ToastItemMarkedAsFinishedSuccess": "Елементът е маркиран като завършен", "ToastItemMarkedAsFinishedSuccess": "Елементът е маркиран като завършен",
@ -750,12 +744,10 @@
"ToastLibraryDeleteSuccess": "Библиотеката е изтрита", "ToastLibraryDeleteSuccess": "Библиотеката е изтрита",
"ToastLibraryScanFailedToStart": "Неуспешно стартиране на сканиране", "ToastLibraryScanFailedToStart": "Неуспешно стартиране на сканиране",
"ToastLibraryScanStarted": "Сканирането на библиотеката е стартирано", "ToastLibraryScanStarted": "Сканирането на библиотеката е стартирано",
"ToastLibraryUpdateFailed": "Неуспешно обновяване на библиотека",
"ToastLibraryUpdateSuccess": "Библиотеката \"{0}\" е обновена", "ToastLibraryUpdateSuccess": "Библиотеката \"{0}\" е обновена",
"ToastPlaylistCreateFailed": "Неуспешно създаване на плейлист", "ToastPlaylistCreateFailed": "Неуспешно създаване на плейлист",
"ToastPlaylistCreateSuccess": "Плейлистът е създаден", "ToastPlaylistCreateSuccess": "Плейлистът е създаден",
"ToastPlaylistRemoveSuccess": "Плейлистът е премахнат", "ToastPlaylistRemoveSuccess": "Плейлистът е премахнат",
"ToastPlaylistUpdateFailed": "Неуспешно обновяване на плейлист",
"ToastPlaylistUpdateSuccess": "Плейлистът е обновен", "ToastPlaylistUpdateSuccess": "Плейлистът е обновен",
"ToastPodcastCreateFailed": "Неуспешно създаване на подкаст", "ToastPodcastCreateFailed": "Неуспешно създаване на подкаст",
"ToastPodcastCreateSuccess": "Подкастът е създаден", "ToastPodcastCreateSuccess": "Подкастът е създаден",

View file

@ -8,7 +8,7 @@
"ButtonAddYourFirstLibrary": "আপনার প্রথম লাইব্রেরি যোগ করুন", "ButtonAddYourFirstLibrary": "আপনার প্রথম লাইব্রেরি যোগ করুন",
"ButtonApply": "প্রয়োগ করুন", "ButtonApply": "প্রয়োগ করুন",
"ButtonApplyChapters": "অধ্যায় প্রয়োগ করুন", "ButtonApplyChapters": "অধ্যায় প্রয়োগ করুন",
"ButtonAuthors": "লেখক", "ButtonAuthors": "লেখকগণ",
"ButtonBack": "পেছনে যান", "ButtonBack": "পেছনে যান",
"ButtonBrowseForFolder": "ফোল্ডারের জন্য ব্রাউজ করুন", "ButtonBrowseForFolder": "ফোল্ডারের জন্য ব্রাউজ করুন",
"ButtonCancel": "বাতিল করুন", "ButtonCancel": "বাতিল করুন",
@ -56,6 +56,7 @@
"ButtonOpenManager": "ম্যানেজার খুলুন", "ButtonOpenManager": "ম্যানেজার খুলুন",
"ButtonPause": "বিরতি", "ButtonPause": "বিরতি",
"ButtonPlay": "বাজান", "ButtonPlay": "বাজান",
"ButtonPlayAll": "সব চালান",
"ButtonPlaying": "বাজছে", "ButtonPlaying": "বাজছে",
"ButtonPlaylists": "প্লেলিস্ট", "ButtonPlaylists": "প্লেলিস্ট",
"ButtonPrevious": "পূর্ববর্তী", "ButtonPrevious": "পূর্ববর্তী",
@ -549,7 +550,7 @@
"LabelSleepTimer": "স্লিপ টাইমার", "LabelSleepTimer": "স্লিপ টাইমার",
"LabelSlug": "স্লাগ", "LabelSlug": "স্লাগ",
"LabelStart": "শুরু", "LabelStart": "শুরু",
"LabelStartTime": "শুরু করার সময়", "LabelStartTime": "শুরুর সময়",
"LabelStarted": "শুরু হয়েছে", "LabelStarted": "শুরু হয়েছে",
"LabelStartedAt": "এতে শুরু হয়েছে", "LabelStartedAt": "এতে শুরু হয়েছে",
"LabelStatsAudioTracks": "অডিও ট্র্যাক", "LabelStatsAudioTracks": "অডিও ট্র্যাক",
@ -702,7 +703,7 @@
"MessageEpisodesQueuedForDownload": "{0} পর্ব(গুলি) ডাউনলোডের জন্য সারিবদ্ধ", "MessageEpisodesQueuedForDownload": "{0} পর্ব(গুলি) ডাউনলোডের জন্য সারিবদ্ধ",
"MessageEreaderDevices": "ই-বুক সরবরাহ নিশ্চিত করতে, আপনাকে নীচে তালিকাভুক্ত প্রতিটি ডিভাইসের জন্য একটি বৈধ প্রেরক হিসাবে উপরের ইমেল ঠিকানাটি যুক্ত করতে হতে পারে।", "MessageEreaderDevices": "ই-বুক সরবরাহ নিশ্চিত করতে, আপনাকে নীচে তালিকাভুক্ত প্রতিটি ডিভাইসের জন্য একটি বৈধ প্রেরক হিসাবে উপরের ইমেল ঠিকানাটি যুক্ত করতে হতে পারে।",
"MessageFeedURLWillBe": "ফিড URL হবে {0}", "MessageFeedURLWillBe": "ফিড URL হবে {0}",
"MessageFetching": "আনয় হচ্ছে...", "MessageFetching": "আনয় হচ্ছে.",
"MessageForceReScanDescription": "সকল ফাইল আবার নতুন স্ক্যানের মত স্ক্যান করবে। অডিও ফাইল ID3 ট্যাগ, OPF ফাইল, এবং টেক্সট ফাইলগুলি নতুন হিসাবে স্ক্যান করা হবে।", "MessageForceReScanDescription": "সকল ফাইল আবার নতুন স্ক্যানের মত স্ক্যান করবে। অডিও ফাইল ID3 ট্যাগ, OPF ফাইল, এবং টেক্সট ফাইলগুলি নতুন হিসাবে স্ক্যান করা হবে।",
"MessageImportantNotice": "গুরুত্বপূর্ণ বিজ্ঞপ্তি!", "MessageImportantNotice": "গুরুত্বপূর্ণ বিজ্ঞপ্তি!",
"MessageInsertChapterBelow": "নীচে অধ্যায় ঢোকান", "MessageInsertChapterBelow": "নীচে অধ্যায় ঢোকান",
@ -710,7 +711,7 @@
"MessageItemsUpdated": "{0}টি আইটেম আপডেট করা হয়েছে", "MessageItemsUpdated": "{0}টি আইটেম আপডেট করা হয়েছে",
"MessageJoinUsOn": "আমাদের সাথে যোগ দিন", "MessageJoinUsOn": "আমাদের সাথে যোগ দিন",
"MessageListeningSessionsInTheLastYear": "গত বছরে {0}টি শোনার সেশন", "MessageListeningSessionsInTheLastYear": "গত বছরে {0}টি শোনার সেশন",
"MessageLoading": "লোড হচ্ছে...", "MessageLoading": "লোড হচ্ছে.",
"MessageLoadingFolders": "ফোল্ডার লোড হচ্ছে...", "MessageLoadingFolders": "ফোল্ডার লোড হচ্ছে...",
"MessageLogsDescription": "লগগুলি JSON ফাইল হিসাবে <code>/metadata/logs</code>-এ সংরক্ষণ করা হয়। ক্র্যাশ লগগুলি <code>/metadata/logs/crash_logs.txt</code>-এ সংরক্ষণ করা হয়।", "MessageLogsDescription": "লগগুলি JSON ফাইল হিসাবে <code>/metadata/logs</code>-এ সংরক্ষণ করা হয়। ক্র্যাশ লগগুলি <code>/metadata/logs/crash_logs.txt</code>-এ সংরক্ষণ করা হয়।",
"MessageM4BFailed": "M4B ব্যর্থ!", "MessageM4BFailed": "M4B ব্যর্থ!",
@ -776,6 +777,38 @@
"MessageShareExpiresIn": "মেয়াদ শেষ হবে {0}", "MessageShareExpiresIn": "মেয়াদ শেষ হবে {0}",
"MessageShareURLWillBe": "শেয়ার করা ইউআরএল হবে <strong>{0}</strong>", "MessageShareURLWillBe": "শেয়ার করা ইউআরএল হবে <strong>{0}</strong>",
"MessageStartPlaybackAtTime": "\"{0}\" এর জন্য {1} এ প্লেব্যাক শুরু করবেন?", "MessageStartPlaybackAtTime": "\"{0}\" এর জন্য {1} এ প্লেব্যাক শুরু করবেন?",
"MessageTaskAudioFileNotWritable": "অডিও ফাইল \"{0}\" লেখার যোগ্য নয়",
"MessageTaskCanceledByUser": "ব্যবহারকারী দ্বারা টাস্ক বাতিল করা হয়েছে",
"MessageTaskDownloadingEpisodeDescription": "\"{0}\" পর্ব ডাউনলোড করা হচ্ছে",
"MessageTaskEmbeddingMetadata": "মেটাডেটা এম্বেড করা হচ্ছে",
"MessageTaskEmbeddingMetadataDescription": "অডিওবুক \"{0}\" এ মেটাডেটা এম্বেড করা হচ্ছে",
"MessageTaskEncodingM4b": "এনকোডিং M4B",
"MessageTaskEncodingM4bDescription": "একটি একক m4b ফাইলে অডিওবুক \"{0}\" এনকোড করা হচ্ছে",
"MessageTaskFailed": "ব্যর্থ হয়েছে",
"MessageTaskFailedToBackupAudioFile": "অডিও ফাইল \"{0}\" ব্যাকআপ করতে ব্যর্থ হয়েছে",
"MessageTaskFailedToCreateCacheDirectory": "ক্যাশে ডিরেক্টরি তৈরি করতে ব্যর্থ হয়েছে",
"MessageTaskFailedToEmbedMetadataInFile": "\"{0}\" ফাইলে মেটাডেটা এম্বেড করতে ব্যর্থ হয়েছে",
"MessageTaskFailedToMergeAudioFiles": "অডিও ফাইল মার্জ করতে ব্যর্থ হয়েছে",
"MessageTaskFailedToMoveM4bFile": "m4b ফাইল সরাতে ব্যর্থ হয়েছে",
"MessageTaskFailedToWriteMetadataFile": "মেটাডেটা ফাইল লিখতে ব্যর্থ হয়েছে",
"MessageTaskMatchingBooksInLibrary": "লাইব্রেরি \"{0}\"-এ বই মিলানো হচ্ছে",
"MessageTaskNoFilesToScan": "স্ক্যান করার জন্য কোন ফাইল নেই",
"MessageTaskOpmlImport": "OPML আমদানি",
"MessageTaskOpmlImportDescription": "{0} RSS ফিড থেকে পডকাস্ট তৈরি করা হচ্ছে",
"MessageTaskOpmlImportFeed": "OPML ফিড আমদানি",
"MessageTaskOpmlImportFeedDescription": "RSS ফিড \"{0}\" আমদানি করা হচ্ছে",
"MessageTaskOpmlImportFeedFailed": "পডকাস্ট ফিড পেতে ব্যর্থ হয়েছে",
"MessageTaskOpmlImportFeedPodcastDescription": "পডকাস্ট তৈরি করা হচ্ছে \"{0}\"",
"MessageTaskOpmlImportFeedPodcastExists": "পডকাস্ট আগে থেকেই পাথে বিদ্যমান",
"MessageTaskOpmlImportFeedPodcastFailed": "পডকাস্ট তৈরি করতে ব্যর্থ",
"MessageTaskOpmlImportFinished": "{0}টি পডকাস্ট যোগ করা হয়েছে",
"MessageTaskScanItemsAdded": "{0}টি করা হয়েছে",
"MessageTaskScanItemsMissing": "{0}টি অনুপস্থিত",
"MessageTaskScanItemsUpdated": "{0} টি আপডেট করা হয়েছে",
"MessageTaskScanNoChangesNeeded": "কোন পরিবর্তন প্রয়োজন নেই",
"MessageTaskScanningFileChanges": "\"{0}\" এ ফাইলের পরিবর্তন স্ক্যান করা হচ্ছে",
"MessageTaskScanningLibrary": "\"{0}\" লাইব্রেরি স্ক্যান করা হচ্ছে",
"MessageTaskTargetDirectoryNotWritable": "টার্গেট ডিরেক্টরি লেখার যোগ্য নয়",
"MessageThinking": "চিন্তা করছি...", "MessageThinking": "চিন্তা করছি...",
"MessageUploaderItemFailed": "আপলোড করতে ব্যর্থ", "MessageUploaderItemFailed": "আপলোড করতে ব্যর্থ",
"MessageUploaderItemSuccess": "সফলভাবে আপলোড হয়েছে!", "MessageUploaderItemSuccess": "সফলভাবে আপলোড হয়েছে!",
@ -816,14 +849,12 @@
"StatsTopNarrators": "শীর্ষ কথকগণ", "StatsTopNarrators": "শীর্ষ কথকগণ",
"StatsTotalDuration": "মোট সময়কাল…", "StatsTotalDuration": "মোট সময়কাল…",
"StatsYearInReview": "বাৎসরিক পর্যালোচনা", "StatsYearInReview": "বাৎসরিক পর্যালোচনা",
"ToastAccountUpdateFailed": "অ্যাকাউন্ট আপডেট করতে ব্যর্থ",
"ToastAccountUpdateSuccess": "অ্যাকাউন্ট আপডেট করা হয়েছে", "ToastAccountUpdateSuccess": "অ্যাকাউন্ট আপডেট করা হয়েছে",
"ToastAppriseUrlRequired": "একটি Apprise ইউআরএল লিখতে হবে", "ToastAppriseUrlRequired": "একটি Apprise ইউআরএল লিখতে হবে",
"ToastAuthorImageRemoveSuccess": "লেখকের ছবি সরানো হয়েছে", "ToastAuthorImageRemoveSuccess": "লেখকের ছবি সরানো হয়েছে",
"ToastAuthorNotFound": "লেখক \"{0}\" খুঁজে পাওয়া যায়নি", "ToastAuthorNotFound": "লেখক \"{0}\" খুঁজে পাওয়া যায়নি",
"ToastAuthorRemoveSuccess": "লেখক সরানো হয়েছে", "ToastAuthorRemoveSuccess": "লেখক সরানো হয়েছে",
"ToastAuthorSearchNotFound": "লেখক পাওয়া যায়নি", "ToastAuthorSearchNotFound": "লেখক পাওয়া যায়নি",
"ToastAuthorUpdateFailed": "লেখক আপডেট করতে ব্যর্থ",
"ToastAuthorUpdateMerged": "লেখক একত্রিত হয়েছে", "ToastAuthorUpdateMerged": "লেখক একত্রিত হয়েছে",
"ToastAuthorUpdateSuccess": "লেখক আপডেট করেছেন", "ToastAuthorUpdateSuccess": "লেখক আপডেট করেছেন",
"ToastAuthorUpdateSuccessNoImageFound": "লেখক আপডেট করেছেন (কোন ছবি পাওয়া যায়নি)", "ToastAuthorUpdateSuccessNoImageFound": "লেখক আপডেট করেছেন (কোন ছবি পাওয়া যায়নি)",
@ -834,7 +865,6 @@
"ToastBackupDeleteSuccess": "ব্যাকআপ মুছে ফেলা হয়েছে", "ToastBackupDeleteSuccess": "ব্যাকআপ মুছে ফেলা হয়েছে",
"ToastBackupInvalidMaxKeep": "রাখার জন্য অকার্যকর ব্যাকআপের সংখ্যা", "ToastBackupInvalidMaxKeep": "রাখার জন্য অকার্যকর ব্যাকআপের সংখ্যা",
"ToastBackupInvalidMaxSize": "অকার্যকর সর্বোচ্চ ব্যাকআপ আকার", "ToastBackupInvalidMaxSize": "অকার্যকর সর্বোচ্চ ব্যাকআপ আকার",
"ToastBackupPathUpdateFailed": "ব্যাকআপ পথ আপডেট করতে ব্যর্থ হয়েছে",
"ToastBackupRestoreFailed": "ব্যাকআপ পুনরুদ্ধার করতে ব্যর্থ", "ToastBackupRestoreFailed": "ব্যাকআপ পুনরুদ্ধার করতে ব্যর্থ",
"ToastBackupUploadFailed": "ব্যাকআপ আপলোড করতে ব্যর্থ", "ToastBackupUploadFailed": "ব্যাকআপ আপলোড করতে ব্যর্থ",
"ToastBackupUploadSuccess": "ব্যাকআপ আপলোড হয়েছে", "ToastBackupUploadSuccess": "ব্যাকআপ আপলোড হয়েছে",
@ -845,7 +875,6 @@
"ToastBookmarkCreateFailed": "বুকমার্ক তৈরি করতে ব্যর্থ", "ToastBookmarkCreateFailed": "বুকমার্ক তৈরি করতে ব্যর্থ",
"ToastBookmarkCreateSuccess": "বুকমার্ক যোগ করা হয়েছে", "ToastBookmarkCreateSuccess": "বুকমার্ক যোগ করা হয়েছে",
"ToastBookmarkRemoveSuccess": "বুকমার্ক সরানো হয়েছে", "ToastBookmarkRemoveSuccess": "বুকমার্ক সরানো হয়েছে",
"ToastBookmarkUpdateFailed": "বুকমার্ক আপডেট করতে ব্যর্থ",
"ToastBookmarkUpdateSuccess": "বুকমার্ক আপডেট করা হয়েছে", "ToastBookmarkUpdateSuccess": "বুকমার্ক আপডেট করা হয়েছে",
"ToastCachePurgeFailed": "ক্যাশে পরিষ্কার করতে ব্যর্থ হয়েছে", "ToastCachePurgeFailed": "ক্যাশে পরিষ্কার করতে ব্যর্থ হয়েছে",
"ToastCachePurgeSuccess": "ক্যাশে সফলভাবে পরিষ্কার করা হয়েছে", "ToastCachePurgeSuccess": "ক্যাশে সফলভাবে পরিষ্কার করা হয়েছে",
@ -856,7 +885,6 @@
"ToastCollectionItemsAddSuccess": "আইটেম(গুলি) সংগ্রহে যোগ করা সফল হয়েছে", "ToastCollectionItemsAddSuccess": "আইটেম(গুলি) সংগ্রহে যোগ করা সফল হয়েছে",
"ToastCollectionItemsRemoveSuccess": "আইটেম(গুলি) সংগ্রহ থেকে সরানো হয়েছে", "ToastCollectionItemsRemoveSuccess": "আইটেম(গুলি) সংগ্রহ থেকে সরানো হয়েছে",
"ToastCollectionRemoveSuccess": "সংগ্রহ সরানো হয়েছে", "ToastCollectionRemoveSuccess": "সংগ্রহ সরানো হয়েছে",
"ToastCollectionUpdateFailed": "সংগ্রহ আপডেট করতে ব্যর্থ",
"ToastCollectionUpdateSuccess": "সংগ্রহ আপডেট করা হয়েছে", "ToastCollectionUpdateSuccess": "সংগ্রহ আপডেট করা হয়েছে",
"ToastCoverUpdateFailed": "কভার আপডেট ব্যর্থ হয়েছে", "ToastCoverUpdateFailed": "কভার আপডেট ব্যর্থ হয়েছে",
"ToastDeleteFileFailed": "ফাইল মুছে ফেলতে ব্যর্থ হয়েছে", "ToastDeleteFileFailed": "ফাইল মুছে ফেলতে ব্যর্থ হয়েছে",
@ -865,8 +893,6 @@
"ToastDeviceNameAlreadyExists": "এই নামের ইরিডার ডিভাইস ইতিমধ্যেই বিদ্যমান", "ToastDeviceNameAlreadyExists": "এই নামের ইরিডার ডিভাইস ইতিমধ্যেই বিদ্যমান",
"ToastDeviceTestEmailFailed": "পরীক্ষামূলক ইমেল পাঠাতে ব্যর্থ হয়েছে", "ToastDeviceTestEmailFailed": "পরীক্ষামূলক ইমেল পাঠাতে ব্যর্থ হয়েছে",
"ToastDeviceTestEmailSuccess": "পরীক্ষামূলক ইমেল পাঠানো হয়েছে", "ToastDeviceTestEmailSuccess": "পরীক্ষামূলক ইমেল পাঠানো হয়েছে",
"ToastDeviceUpdateFailed": "ডিভাইস আপডেট করতে ব্যর্থ হয়েছে",
"ToastEmailSettingsUpdateFailed": "ইমেল সেটিংস আপডেট করতে ব্যর্থ হয়েছে",
"ToastEmailSettingsUpdateSuccess": "ইমেল সেটিংস আপডেট করা হয়েছে", "ToastEmailSettingsUpdateSuccess": "ইমেল সেটিংস আপডেট করা হয়েছে",
"ToastEncodeCancelFailed": "এনকোড বাতিল করতে ব্যর্থ হয়েছে", "ToastEncodeCancelFailed": "এনকোড বাতিল করতে ব্যর্থ হয়েছে",
"ToastEncodeCancelSucces": "এনকোড বাতিল করা হয়েছে", "ToastEncodeCancelSucces": "এনকোড বাতিল করা হয়েছে",
@ -875,21 +901,17 @@
"ToastErrorCannotShare": "এই ডিভাইসে স্থানীয়ভাবে শেয়ার করা যাবে না", "ToastErrorCannotShare": "এই ডিভাইসে স্থানীয়ভাবে শেয়ার করা যাবে না",
"ToastFailedToLoadData": "ডেটা লোড করা যায়নি", "ToastFailedToLoadData": "ডেটা লোড করা যায়নি",
"ToastFailedToShare": "শেয়ার করতে ব্যর্থ", "ToastFailedToShare": "শেয়ার করতে ব্যর্থ",
"ToastFailedToUpdateAccount": "অ্যাকাউন্ট আপডেট করতে ব্যর্থ", "ToastFailedToUpdate": "আপডেট করতে ব্যর্থ হয়েছে",
"ToastFailedToUpdateUser": "ব্যবহারকারী আপডেট করতে ব্যর্থ",
"ToastInvalidImageUrl": "অকার্যকর ছবির ইউআরএল", "ToastInvalidImageUrl": "অকার্যকর ছবির ইউআরএল",
"ToastInvalidUrl": "অকার্যকর ইউআরএল", "ToastInvalidUrl": "অকার্যকর ইউআরএল",
"ToastItemCoverUpdateFailed": "আইটেম কভার আপডেট করতে ব্যর্থ হয়েছে",
"ToastItemCoverUpdateSuccess": "আইটেম কভার আপডেট করা হয়েছে", "ToastItemCoverUpdateSuccess": "আইটেম কভার আপডেট করা হয়েছে",
"ToastItemDeletedFailed": "আইটেম মুছে ফেলতে ব্যর্থ", "ToastItemDeletedFailed": "আইটেম মুছে ফেলতে ব্যর্থ",
"ToastItemDeletedSuccess": "মুছে ফেলা আইটেম", "ToastItemDeletedSuccess": "মুছে ফেলা আইটেম",
"ToastItemDetailsUpdateFailed": "আইটেমের বিবরণ আপডেট করতে ব্যর্থ",
"ToastItemDetailsUpdateSuccess": "আইটেমের বিবরণ আপডেট করা হয়েছে", "ToastItemDetailsUpdateSuccess": "আইটেমের বিবরণ আপডেট করা হয়েছে",
"ToastItemMarkedAsFinishedFailed": "সমাপ্ত হিসাবে চিহ্নিত করতে ব্যর্থ", "ToastItemMarkedAsFinishedFailed": "সমাপ্ত হিসাবে চিহ্নিত করতে ব্যর্থ",
"ToastItemMarkedAsFinishedSuccess": "আইটেম সমাপ্ত হিসাবে চিহ্নিত", "ToastItemMarkedAsFinishedSuccess": "আইটেম সমাপ্ত হিসাবে চিহ্নিত",
"ToastItemMarkedAsNotFinishedFailed": "সমাপ্ত হয়নি হিসাবে চিহ্নিত করতে ব্যর্থ", "ToastItemMarkedAsNotFinishedFailed": "সমাপ্ত হয়নি হিসাবে চিহ্নিত করতে ব্যর্থ",
"ToastItemMarkedAsNotFinishedSuccess": "আইটেম সমাপ্ত হয়নি বলে চিহ্নিত", "ToastItemMarkedAsNotFinishedSuccess": "আইটেম সমাপ্ত হয়নি বলে চিহ্নিত",
"ToastItemUpdateFailed": "আইটেম আপডেট করতে ব্যর্থ",
"ToastItemUpdateSuccess": "আইটেম আপডেট করা হয়েছে", "ToastItemUpdateSuccess": "আইটেম আপডেট করা হয়েছে",
"ToastLibraryCreateFailed": "লাইব্রেরি তৈরি করতে ব্যর্থ", "ToastLibraryCreateFailed": "লাইব্রেরি তৈরি করতে ব্যর্থ",
"ToastLibraryCreateSuccess": "লাইব্রেরি \"{0}\" তৈরি করা হয়েছে", "ToastLibraryCreateSuccess": "লাইব্রেরি \"{0}\" তৈরি করা হয়েছে",
@ -897,7 +919,6 @@
"ToastLibraryDeleteSuccess": "লাইব্রেরি মুছে ফেলা হয়েছে", "ToastLibraryDeleteSuccess": "লাইব্রেরি মুছে ফেলা হয়েছে",
"ToastLibraryScanFailedToStart": "স্ক্যান শুরু করতে ব্যর্থ", "ToastLibraryScanFailedToStart": "স্ক্যান শুরু করতে ব্যর্থ",
"ToastLibraryScanStarted": "লাইব্রেরি স্ক্যান শুরু হয়েছে", "ToastLibraryScanStarted": "লাইব্রেরি স্ক্যান শুরু হয়েছে",
"ToastLibraryUpdateFailed": "লাইব্রেরি আপডেট করতে ব্যর্থ",
"ToastLibraryUpdateSuccess": "লাইব্রেরি \"{0}\" আপডেট করা হয়েছে", "ToastLibraryUpdateSuccess": "লাইব্রেরি \"{0}\" আপডেট করা হয়েছে",
"ToastNameEmailRequired": "নাম এবং ইমেইল আবশ্যক", "ToastNameEmailRequired": "নাম এবং ইমেইল আবশ্যক",
"ToastNameRequired": "নাম আবশ্যক", "ToastNameRequired": "নাম আবশ্যক",
@ -912,16 +933,13 @@
"ToastNotificationDeleteFailed": "বিজ্ঞপ্তি মুছে ফেলতে ব্যর্থ", "ToastNotificationDeleteFailed": "বিজ্ঞপ্তি মুছে ফেলতে ব্যর্থ",
"ToastNotificationFailedMaximum": "সর্বাধিক ব্যর্থ প্রচেষ্টা >= 0 হতে হবে", "ToastNotificationFailedMaximum": "সর্বাধিক ব্যর্থ প্রচেষ্টা >= 0 হতে হবে",
"ToastNotificationQueueMaximum": "সর্বাধিক বিজ্ঞপ্তি সারি >= 0 হতে হবে", "ToastNotificationQueueMaximum": "সর্বাধিক বিজ্ঞপ্তি সারি >= 0 হতে হবে",
"ToastNotificationSettingsUpdateFailed": "বিজ্ঞপ্তি সেটিংস আপডেট করতে ব্যর্থ",
"ToastNotificationSettingsUpdateSuccess": "বিজ্ঞপ্তি সেটিংস আপডেট করা হয়েছে", "ToastNotificationSettingsUpdateSuccess": "বিজ্ঞপ্তি সেটিংস আপডেট করা হয়েছে",
"ToastNotificationTestTriggerFailed": "পরীক্ষামূলক বিজ্ঞপ্তি ট্রিগার করতে ব্যর্থ হয়েছে", "ToastNotificationTestTriggerFailed": "পরীক্ষামূলক বিজ্ঞপ্তি ট্রিগার করতে ব্যর্থ হয়েছে",
"ToastNotificationTestTriggerSuccess": "পরীক্ষামুলক বিজ্ঞপ্তি ট্রিগার হয়েছে", "ToastNotificationTestTriggerSuccess": "পরীক্ষামুলক বিজ্ঞপ্তি ট্রিগার হয়েছে",
"ToastNotificationUpdateFailed": "বিজ্ঞপ্তি আপডেট করতে ব্যর্থ",
"ToastNotificationUpdateSuccess": "বিজ্ঞপ্তি আপডেট হয়েছে", "ToastNotificationUpdateSuccess": "বিজ্ঞপ্তি আপডেট হয়েছে",
"ToastPlaylistCreateFailed": "প্লেলিস্ট তৈরি করতে ব্যর্থ", "ToastPlaylistCreateFailed": "প্লেলিস্ট তৈরি করতে ব্যর্থ",
"ToastPlaylistCreateSuccess": "প্লেলিস্ট তৈরি করা হয়েছে", "ToastPlaylistCreateSuccess": "প্লেলিস্ট তৈরি করা হয়েছে",
"ToastPlaylistRemoveSuccess": "প্লেলিস্ট সরানো হয়েছে", "ToastPlaylistRemoveSuccess": "প্লেলিস্ট সরানো হয়েছে",
"ToastPlaylistUpdateFailed": "প্লেলিস্ট আপডেট করতে ব্যর্থ",
"ToastPlaylistUpdateSuccess": "প্লেলিস্ট আপডেট করা হয়েছে", "ToastPlaylistUpdateSuccess": "প্লেলিস্ট আপডেট করা হয়েছে",
"ToastPodcastCreateFailed": "পডকাস্ট তৈরি করতে ব্যর্থ", "ToastPodcastCreateFailed": "পডকাস্ট তৈরি করতে ব্যর্থ",
"ToastPodcastCreateSuccess": "পডকাস্ট সফলভাবে তৈরি করা হয়েছে", "ToastPodcastCreateSuccess": "পডকাস্ট সফলভাবে তৈরি করা হয়েছে",
@ -950,7 +968,6 @@
"ToastSendEbookToDeviceSuccess": "ইবুক \"{0}\" ডিভাইসে পাঠানো হয়েছে", "ToastSendEbookToDeviceSuccess": "ইবুক \"{0}\" ডিভাইসে পাঠানো হয়েছে",
"ToastSeriesUpdateFailed": "সিরিজ আপডেট ব্যর্থ হয়েছে", "ToastSeriesUpdateFailed": "সিরিজ আপডেট ব্যর্থ হয়েছে",
"ToastSeriesUpdateSuccess": "সিরিজ আপডেট সাফল্য", "ToastSeriesUpdateSuccess": "সিরিজ আপডেট সাফল্য",
"ToastServerSettingsUpdateFailed": "সার্ভার সেটিংস আপডেট করতে ব্যর্থ হয়েছে",
"ToastServerSettingsUpdateSuccess": "সার্ভার সেটিংস আপডেট করা হয়েছে", "ToastServerSettingsUpdateSuccess": "সার্ভার সেটিংস আপডেট করা হয়েছে",
"ToastSessionCloseFailed": "অধিবেশন বন্ধ করতে ব্যর্থ হয়েছে", "ToastSessionCloseFailed": "অধিবেশন বন্ধ করতে ব্যর্থ হয়েছে",
"ToastSessionDeleteFailed": "সেশন মুছে ফেলতে ব্যর্থ", "ToastSessionDeleteFailed": "সেশন মুছে ফেলতে ব্যর্থ",
@ -961,7 +978,6 @@
"ToastSocketDisconnected": "সকেট সংযোগ বিচ্ছিন্ন", "ToastSocketDisconnected": "সকেট সংযোগ বিচ্ছিন্ন",
"ToastSocketFailedToConnect": "সকেট সংযোগ করতে ব্যর্থ হয়েছে", "ToastSocketFailedToConnect": "সকেট সংযোগ করতে ব্যর্থ হয়েছে",
"ToastSortingPrefixesEmptyError": "কমপক্ষে ১ টি সাজানোর উপসর্গ থাকতে হবে", "ToastSortingPrefixesEmptyError": "কমপক্ষে ১ টি সাজানোর উপসর্গ থাকতে হবে",
"ToastSortingPrefixesUpdateFailed": "বাছাই উপসর্গ আপডেট করতে ব্যর্থ হয়েছে",
"ToastSortingPrefixesUpdateSuccess": "বাছাই করা উপসর্গ আপডেট করা হয়েছে ({0}টি আইটেম)", "ToastSortingPrefixesUpdateSuccess": "বাছাই করা উপসর্গ আপডেট করা হয়েছে ({0}টি আইটেম)",
"ToastTitleRequired": "শিরোনাম আবশ্যক", "ToastTitleRequired": "শিরোনাম আবশ্যক",
"ToastUnknownError": "অজানা ত্রুটি", "ToastUnknownError": "অজানা ত্রুটি",

View file

@ -19,6 +19,7 @@
"ButtonChooseFiles": "Vybrat soubory", "ButtonChooseFiles": "Vybrat soubory",
"ButtonClearFilter": "Vymazat filtr", "ButtonClearFilter": "Vymazat filtr",
"ButtonCloseFeed": "Zavřít kanál", "ButtonCloseFeed": "Zavřít kanál",
"ButtonCloseSession": "Zavřít otevřenou relaci",
"ButtonCollections": "Kolekce", "ButtonCollections": "Kolekce",
"ButtonConfigureScanner": "Konfigurovat Prohledávání", "ButtonConfigureScanner": "Konfigurovat Prohledávání",
"ButtonCreate": "Vytvořit", "ButtonCreate": "Vytvořit",
@ -28,6 +29,9 @@
"ButtonEdit": "Upravit", "ButtonEdit": "Upravit",
"ButtonEditChapters": "Upravit kapitoly", "ButtonEditChapters": "Upravit kapitoly",
"ButtonEditPodcast": "Upravit podcast", "ButtonEditPodcast": "Upravit podcast",
"ButtonEnable": "Povolit",
"ButtonFireAndFail": "Spustit a selhat",
"ButtonFireOnTest": "Spustit událost onTest",
"ButtonForceReScan": "Vynutit opětovné prohledání", "ButtonForceReScan": "Vynutit opětovné prohledání",
"ButtonFullPath": "Úplná cesta", "ButtonFullPath": "Úplná cesta",
"ButtonHide": "Skrýt", "ButtonHide": "Skrýt",
@ -44,19 +48,26 @@
"ButtonMatchAllAuthors": "Spárovat všechny autory", "ButtonMatchAllAuthors": "Spárovat všechny autory",
"ButtonMatchBooks": "Spárovat Knihy", "ButtonMatchBooks": "Spárovat Knihy",
"ButtonNevermind": "Nevadí", "ButtonNevermind": "Nevadí",
"ButtonNext": "Další",
"ButtonNextChapter": "Další Kapitola", "ButtonNextChapter": "Další Kapitola",
"ButtonNextItemInQueue": "Žádná další položka ve frontě",
"ButtonOk": "Ok",
"ButtonOpenFeed": "Otevřít kanál", "ButtonOpenFeed": "Otevřít kanál",
"ButtonOpenManager": "Otevřít správce", "ButtonOpenManager": "Otevřít správce",
"ButtonPause": "Pozastavit",
"ButtonPlay": "Přehrát", "ButtonPlay": "Přehrát",
"ButtonPlayAll": "Přehrát vše",
"ButtonPlaying": "Hraje", "ButtonPlaying": "Hraje",
"ButtonPlaylists": "Seznamy skladeb", "ButtonPlaylists": "Seznamy skladeb",
"ButtonPrevious": "Předchozí", "ButtonPrevious": "Předchozí",
"ButtonPreviousChapter": "Předchozí Kapitola", "ButtonPreviousChapter": "Předchozí Kapitola",
"ButtonProbeAudioFile": "Prozkoumat audio soubor",
"ButtonPurgeAllCache": "Vyčistit veškerou mezipaměť", "ButtonPurgeAllCache": "Vyčistit veškerou mezipaměť",
"ButtonPurgeItemsCache": "Vyčistit mezipaměť položek", "ButtonPurgeItemsCache": "Vyčistit mezipaměť položek",
"ButtonQueueAddItem": "Přidat do fronty", "ButtonQueueAddItem": "Přidat do fronty",
"ButtonQueueRemoveItem": "Odstranit z fronty", "ButtonQueueRemoveItem": "Odstranit z fronty",
"ButtonQuickEmbedMetadata": "Rychle Zapsat Metadata", "ButtonQuickEmbed": "Rychle Zapsat",
"ButtonQuickEmbedMetadata": "Rychle zapsat Metadata",
"ButtonQuickMatch": "Rychlé přiřazení", "ButtonQuickMatch": "Rychlé přiřazení",
"ButtonReScan": "Znovu prohledat", "ButtonReScan": "Znovu prohledat",
"ButtonRead": "Číst", "ButtonRead": "Číst",
@ -88,6 +99,8 @@
"ButtonStartMetadataEmbed": "Spustit vkládání metadat", "ButtonStartMetadataEmbed": "Spustit vkládání metadat",
"ButtonStats": "Statistiky", "ButtonStats": "Statistiky",
"ButtonSubmit": "Odeslat", "ButtonSubmit": "Odeslat",
"ButtonTest": "Test",
"ButtonUnlinkOpenId": "Odpojit OpenID",
"ButtonUpload": "Nahrát", "ButtonUpload": "Nahrát",
"ButtonUploadBackup": "Nahrát zálohu", "ButtonUploadBackup": "Nahrát zálohu",
"ButtonUploadCover": "Nahrát obálku", "ButtonUploadCover": "Nahrát obálku",
@ -100,10 +113,12 @@
"ErrorUploadFetchMetadataNoResults": "Nepodařilo se načíst metadata - zkuste aktualizovat název a/nebo autora", "ErrorUploadFetchMetadataNoResults": "Nepodařilo se načíst metadata - zkuste aktualizovat název a/nebo autora",
"ErrorUploadLacksTitle": "Musí mít titul", "ErrorUploadLacksTitle": "Musí mít titul",
"HeaderAccount": "Účet", "HeaderAccount": "Účet",
"HeaderAddCustomMetadataProvider": "Přidat vlastního poskytovatele metadat",
"HeaderAdvanced": "Pokročilé", "HeaderAdvanced": "Pokročilé",
"HeaderAppriseNotificationSettings": "Nastavení oznámení Apprise", "HeaderAppriseNotificationSettings": "Nastavení oznámení Apprise",
"HeaderAudioTracks": "Zvukové stopy", "HeaderAudioTracks": "Zvukové stopy",
"HeaderAudiobookTools": "Nástroje pro správu souborů audioknih", "HeaderAudiobookTools": "Nástroje pro správu souborů audioknih",
"HeaderAuthentication": "Autentizace",
"HeaderBackups": "Zálohy", "HeaderBackups": "Zálohy",
"HeaderChangePassword": "Změnit heslo", "HeaderChangePassword": "Změnit heslo",
"HeaderChapters": "Kapitoly", "HeaderChapters": "Kapitoly",
@ -144,10 +159,14 @@
"HeaderMetadataToEmbed": "Metadata k vložení", "HeaderMetadataToEmbed": "Metadata k vložení",
"HeaderNewAccount": "Nový účet", "HeaderNewAccount": "Nový účet",
"HeaderNewLibrary": "Nová knihovna", "HeaderNewLibrary": "Nová knihovna",
"HeaderNotificationCreate": "Vytvořit notifikaci",
"HeaderNotificationUpdate": "Aktualizovat notifikaci",
"HeaderNotifications": "Oznámení", "HeaderNotifications": "Oznámení",
"HeaderOpenIDConnectAuthentication": "Ověřování pomocí OpenID Connect", "HeaderOpenIDConnectAuthentication": "Ověřování pomocí OpenID Connect",
"HeaderOpenListeningSessions": "Otevřené relace přehrávače",
"HeaderOpenRSSFeed": "Otevřít RSS kanál", "HeaderOpenRSSFeed": "Otevřít RSS kanál",
"HeaderOtherFiles": "Ostatní soubory", "HeaderOtherFiles": "Ostatní soubory",
"HeaderPasswordAuthentication": "Autentizace heslem",
"HeaderPermissions": "Oprávnění", "HeaderPermissions": "Oprávnění",
"HeaderPlayerQueue": "Fronta přehrávače", "HeaderPlayerQueue": "Fronta přehrávače",
"HeaderPlayerSettings": "Nastavení přehrávače", "HeaderPlayerSettings": "Nastavení přehrávače",
@ -162,6 +181,7 @@
"HeaderRemoveEpisodes": "Odstranit {0} epizody", "HeaderRemoveEpisodes": "Odstranit {0} epizody",
"HeaderSavedMediaProgress": "Průběh uložených médií", "HeaderSavedMediaProgress": "Průběh uložených médií",
"HeaderSchedule": "Plán", "HeaderSchedule": "Plán",
"HeaderScheduleEpisodeDownloads": "Naplánovat automatické stahování epizod",
"HeaderScheduleLibraryScans": "Naplánovat automatické prohledávání knihoven", "HeaderScheduleLibraryScans": "Naplánovat automatické prohledávání knihoven",
"HeaderSession": "Relace", "HeaderSession": "Relace",
"HeaderSetBackupSchedule": "Nastavit plán zálohování", "HeaderSetBackupSchedule": "Nastavit plán zálohování",
@ -200,13 +220,18 @@
"LabelAddToPlaylist": "Přidat do seznamu přehrávání", "LabelAddToPlaylist": "Přidat do seznamu přehrávání",
"LabelAddToPlaylistBatch": "Přidat {0} položky do seznamu přehrávání", "LabelAddToPlaylistBatch": "Přidat {0} položky do seznamu přehrávání",
"LabelAddedAt": "Přidáno v", "LabelAddedAt": "Přidáno v",
"LabelAddedDate": "Přidáno {0}",
"LabelAdminUsersOnly": "Pouze administrátoři", "LabelAdminUsersOnly": "Pouze administrátoři",
"LabelAll": "Vše", "LabelAll": "Vše",
"LabelAllUsers": "Všichni uživatelé", "LabelAllUsers": "Všichni uživatelé",
"LabelAllUsersExcludingGuests": "Všichni uživatelé kromě hostů", "LabelAllUsersExcludingGuests": "Všichni uživatelé kromě hostů",
"LabelAllUsersIncludingGuests": "Všichni uživatelé včetně hostů", "LabelAllUsersIncludingGuests": "Všichni uživatelé včetně hostů",
"LabelAlreadyInYourLibrary": "Již ve vaší knihovně", "LabelAlreadyInYourLibrary": "Již ve vaší knihovně",
"LabelApiToken": "API Token",
"LabelAppend": "Připojit", "LabelAppend": "Připojit",
"LabelAudioBitrate": "Bitový tok zvuku (např. 128k)",
"LabelAudioChannels": "Zvukové kanály (1 nebo 2)",
"LabelAudioCodec": "Kodek audia",
"LabelAuthor": "Autor", "LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (jméno a příjmení)", "LabelAuthorFirstLast": "Autor (jméno a příjmení)",
"LabelAuthorLastFirst": "Autor (příjmení a jméno)", "LabelAuthorLastFirst": "Autor (příjmení a jméno)",
@ -219,6 +244,7 @@
"LabelAutoRegister": "Automatická registrace", "LabelAutoRegister": "Automatická registrace",
"LabelAutoRegisterDescription": "Automaticky vytvářet nové uživatele po přihlášení", "LabelAutoRegisterDescription": "Automaticky vytvářet nové uživatele po přihlášení",
"LabelBackToUser": "Zpět k uživateli", "LabelBackToUser": "Zpět k uživateli",
"LabelBackupAudioFiles": "Zálohovat zvukové soubory",
"LabelBackupLocation": "Umístění zálohy", "LabelBackupLocation": "Umístění zálohy",
"LabelBackupsEnableAutomaticBackups": "Povolit automatické zálohování", "LabelBackupsEnableAutomaticBackups": "Povolit automatické zálohování",
"LabelBackupsEnableAutomaticBackupsHelp": "Zálohy uložené v /metadata/backups", "LabelBackupsEnableAutomaticBackupsHelp": "Zálohy uložené v /metadata/backups",
@ -227,10 +253,13 @@
"LabelBackupsNumberToKeep": "Počet záloh, které se mají uchovat", "LabelBackupsNumberToKeep": "Počet záloh, které se mají uchovat",
"LabelBackupsNumberToKeepHelp": "Najednou bude odstraněna pouze 1 záloha, takže pokud již máte více záloh, měli byste je odstranit ručně.", "LabelBackupsNumberToKeepHelp": "Najednou bude odstraněna pouze 1 záloha, takže pokud již máte více záloh, měli byste je odstranit ručně.",
"LabelBitrate": "Datový tok", "LabelBitrate": "Datový tok",
"LabelBonus": "Bonus",
"LabelBooks": "Knihy", "LabelBooks": "Knihy",
"LabelButtonText": "Text tlačítka", "LabelButtonText": "Text tlačítka",
"LabelByAuthor": "od {0}",
"LabelChangePassword": "Změnit heslo", "LabelChangePassword": "Změnit heslo",
"LabelChannels": "Kanály", "LabelChannels": "Kanály",
"LabelChapterCount": "{0} Kapitol",
"LabelChapterTitle": "Název kapitoly", "LabelChapterTitle": "Název kapitoly",
"LabelChapters": "Kapitoly", "LabelChapters": "Kapitoly",
"LabelChaptersFound": "Kapitoly nalezeny", "LabelChaptersFound": "Kapitoly nalezeny",
@ -238,6 +267,7 @@
"LabelClosePlayer": "Zavřít přehrávač", "LabelClosePlayer": "Zavřít přehrávač",
"LabelCodec": "Kodek", "LabelCodec": "Kodek",
"LabelCollapseSeries": "Sbalit sérii", "LabelCollapseSeries": "Sbalit sérii",
"LabelCollapseSubSeries": "Sbalit podsérie",
"LabelCollection": "Kolekce", "LabelCollection": "Kolekce",
"LabelCollections": "Kolekce", "LabelCollections": "Kolekce",
"LabelComplete": "Dokončeno", "LabelComplete": "Dokončeno",
@ -288,16 +318,21 @@
"LabelEpisode": "Epizoda", "LabelEpisode": "Epizoda",
"LabelEpisodeTitle": "Název epizody", "LabelEpisodeTitle": "Název epizody",
"LabelEpisodeType": "Typ epizody", "LabelEpisodeType": "Typ epizody",
"LabelEpisodes": "Epizody",
"LabelExample": "Příklad", "LabelExample": "Příklad",
"LabelExpandSeries": "Rozbalit série", "LabelExpandSeries": "Rozbalit série",
"LabelExpandSubSeries": "Rozbalit podsérie",
"LabelExplicit": "Explicitní", "LabelExplicit": "Explicitní",
"LabelExplicitChecked": "Explicitní (zaškrtnuto)", "LabelExplicitChecked": "Explicitní (zaškrtnuto)",
"LabelExplicitUnchecked": "Není explicitní (nezaškrtnuto)", "LabelExplicitUnchecked": "Není explicitní (nezaškrtnuto)",
"LabelExportOPML": "Export OPML",
"LabelFeedURL": "URL zdroje", "LabelFeedURL": "URL zdroje",
"LabelFetchingMetadata": "Získávání metadat", "LabelFetchingMetadata": "Získávání metadat",
"LabelFile": "Soubor", "LabelFile": "Soubor",
"LabelFileBirthtime": "Čas vzniku souboru", "LabelFileBirthtime": "Čas vzniku souboru",
"LabelFileBornDate": "Vytvořeno {0}",
"LabelFileModified": "Soubor změněn", "LabelFileModified": "Soubor změněn",
"LabelFileModifiedDate": "Změněno {0}",
"LabelFilename": "Název souboru", "LabelFilename": "Název souboru",
"LabelFilterByUser": "Filtrovat podle uživatele", "LabelFilterByUser": "Filtrovat podle uživatele",
"LabelFindEpisodes": "Najít epizody", "LabelFindEpisodes": "Najít epizody",
@ -307,6 +342,7 @@
"LabelFontBold": "Tučně", "LabelFontBold": "Tučně",
"LabelFontBoldness": "Výraznost písma", "LabelFontBoldness": "Výraznost písma",
"LabelFontFamily": "Rodina písem", "LabelFontFamily": "Rodina písem",
"LabelFontItalic": "Kurzíva",
"LabelFontScale": "Měřítko písma", "LabelFontScale": "Měřítko písma",
"LabelFontStrikethrough": "Přeškrtnutí", "LabelFontStrikethrough": "Přeškrtnutí",
"LabelFormat": "Formát", "LabelFormat": "Formát",
@ -325,6 +361,7 @@
"LabelInProgress": "Probíhá", "LabelInProgress": "Probíhá",
"LabelIncludeInTracklist": "Zahrnout do seznamu stop", "LabelIncludeInTracklist": "Zahrnout do seznamu stop",
"LabelIncomplete": "Neúplné", "LabelIncomplete": "Neúplné",
"LabelInterval": "Interval",
"LabelIntervalCustomDailyWeekly": "Vlastní denně/týdně", "LabelIntervalCustomDailyWeekly": "Vlastní denně/týdně",
"LabelIntervalEvery12Hours": "Každých 12 hodin", "LabelIntervalEvery12Hours": "Každých 12 hodin",
"LabelIntervalEvery15Minutes": "Každých 15 minut", "LabelIntervalEvery15Minutes": "Každých 15 minut",
@ -421,17 +458,22 @@
"LabelPersonalYearReview": "Váš přehled roku ({0})", "LabelPersonalYearReview": "Váš přehled roku ({0})",
"LabelPhotoPathURL": "Cesta k fotografii/URL", "LabelPhotoPathURL": "Cesta k fotografii/URL",
"LabelPlayMethod": "Metoda přehrávání", "LabelPlayMethod": "Metoda přehrávání",
"LabelPlayerChapterNumberMarker": "{0} z {1}",
"LabelPlaylists": "Seznamy skladeb", "LabelPlaylists": "Seznamy skladeb",
"LabelPodcast": "Podcast",
"LabelPodcastSearchRegion": "Oblast vyhledávání podcastu", "LabelPodcastSearchRegion": "Oblast vyhledávání podcastu",
"LabelPodcastType": "Typ podcastu", "LabelPodcastType": "Typ podcastu",
"LabelPodcasts": "Podcasty", "LabelPodcasts": "Podcasty",
"LabelPort": "Port",
"LabelPrefixesToIgnore": "Předpony, které se mají ignorovat (nerozlišují se malá a velká písmena)", "LabelPrefixesToIgnore": "Předpony, které se mají ignorovat (nerozlišují se malá a velká písmena)",
"LabelPreventIndexing": "Zabránit indexování vašeho kanálu v adresářích podcastů iTunes a Google", "LabelPreventIndexing": "Zabránit indexování vašeho kanálu v adresářích podcastů iTunes a Google",
"LabelPrimaryEbook": "Hlavní e-kniha", "LabelPrimaryEbook": "Hlavní e-kniha",
"LabelProgress": "Průběh", "LabelProgress": "Průběh",
"LabelProvider": "Poskytovatel", "LabelProvider": "Poskytovatel",
"LabelProviderAuthorizationValue": "Hodnota autorizačního headeru",
"LabelPubDate": "Datum vydání", "LabelPubDate": "Datum vydání",
"LabelPublishYear": "Rok vydání", "LabelPublishYear": "Rok vydání",
"LabelPublishedDate": "Vydáno {0}",
"LabelPublisher": "Vydavatel", "LabelPublisher": "Vydavatel",
"LabelPublishers": "Vydavatelé", "LabelPublishers": "Vydavatelé",
"LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka", "LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka",
@ -441,6 +483,7 @@
"LabelRSSFeedSlug": "RSS kanál Slug", "LabelRSSFeedSlug": "RSS kanál Slug",
"LabelRSSFeedURL": "URL RSS kanálu", "LabelRSSFeedURL": "URL RSS kanálu",
"LabelRandomly": "Náhodně", "LabelRandomly": "Náhodně",
"LabelReAddSeriesToContinueListening": "Znovu přidat sérii k pokračování poslechu",
"LabelRead": "Číst", "LabelRead": "Číst",
"LabelReadAgain": "Číst znovu", "LabelReadAgain": "Číst znovu",
"LabelReadEbookWithoutProgress": "Číst e-knihu bez zachování průběhu", "LabelReadEbookWithoutProgress": "Číst e-knihu bez zachování průběhu",
@ -448,6 +491,7 @@
"LabelRecentlyAdded": "Nedávno přidané", "LabelRecentlyAdded": "Nedávno přidané",
"LabelRecommended": "Doporučeno", "LabelRecommended": "Doporučeno",
"LabelRedo": "Přepracovat", "LabelRedo": "Přepracovat",
"LabelRegion": "Region",
"LabelReleaseDate": "Datum vydání", "LabelReleaseDate": "Datum vydání",
"LabelRemoveCover": "Odstranit obálku", "LabelRemoveCover": "Odstranit obálku",
"LabelRowsPerPage": "Řádky na stránku", "LabelRowsPerPage": "Řádky na stránku",
@ -539,6 +583,7 @@
"LabelTagsNotAccessibleToUser": "Značky nepřístupné uživateli", "LabelTagsNotAccessibleToUser": "Značky nepřístupné uživateli",
"LabelTasks": "Spuštěné Úlohy", "LabelTasks": "Spuštěné Úlohy",
"LabelTextEditorBulletedList": "Seznam s odrážkami", "LabelTextEditorBulletedList": "Seznam s odrážkami",
"LabelTextEditorLink": "Odkaz",
"LabelTextEditorNumberedList": "Seznam s čísly", "LabelTextEditorNumberedList": "Seznam s čísly",
"LabelTextEditorUnlink": "Zrušit odkaz", "LabelTextEditorUnlink": "Zrušit odkaz",
"LabelTheme": "Téma", "LabelTheme": "Téma",
@ -572,6 +617,7 @@
"LabelUnabridged": "Nezkráceno", "LabelUnabridged": "Nezkráceno",
"LabelUndo": "Zpět", "LabelUndo": "Zpět",
"LabelUnknown": "Neznámý", "LabelUnknown": "Neznámý",
"LabelUnknownPublishDate": "Neznámé datum vydání",
"LabelUpdateCover": "Aktualizovat obálku", "LabelUpdateCover": "Aktualizovat obálku",
"LabelUpdateCoverHelp": "Povolit přepsání existujících obálek pro vybrané knihy, pokud je nalezena shoda", "LabelUpdateCoverHelp": "Povolit přepsání existujících obálek pro vybrané knihy, pokud je nalezena shoda",
"LabelUpdateDetails": "Aktualizovat podrobnosti", "LabelUpdateDetails": "Aktualizovat podrobnosti",
@ -620,14 +666,19 @@
"MessageCheckingCron": "Kontrola cronu...", "MessageCheckingCron": "Kontrola cronu...",
"MessageConfirmCloseFeed": "Opravdu chcete zavřít tento kanál?", "MessageConfirmCloseFeed": "Opravdu chcete zavřít tento kanál?",
"MessageConfirmDeleteBackup": "Opravdu chcete smazat zálohu pro {0}?", "MessageConfirmDeleteBackup": "Opravdu chcete smazat zálohu pro {0}?",
"MessageConfirmDeleteDevice": "Opravdu chcete vymazat zařízení e-reader \"{0}\"?",
"MessageConfirmDeleteFile": "Tento krok smaže soubor ze souborového systému. Jsi si jisti?", "MessageConfirmDeleteFile": "Tento krok smaže soubor ze souborového systému. Jsi si jisti?",
"MessageConfirmDeleteLibrary": "Opravdu chcete trvale smazat knihovnu \"{0}\"?", "MessageConfirmDeleteLibrary": "Opravdu chcete trvale smazat knihovnu \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "Tento krok odstraní položku knihovny z databáze a vašeho souborového systému. Jste si jisti?", "MessageConfirmDeleteLibraryItem": "Tento krok odstraní položku knihovny z databáze a vašeho souborového systému. Jste si jisti?",
"MessageConfirmDeleteLibraryItems": "Tímto smažete {0} položkek knihovny z databáze a vašeho souborového systému. Jsi si jisti?", "MessageConfirmDeleteLibraryItems": "Tímto smažete {0} položkek knihovny z databáze a vašeho souborového systému. Jsi si jisti?",
"MessageConfirmDeleteMetadataProvider": "Opravdu chcete vymazat vlastního poskytovatele metadat \"{0}\"?",
"MessageConfirmDeleteNotification": "Opravdu chcete vymazat tuto notifikaci?",
"MessageConfirmDeleteSession": "Opravdu chcete smazat tuto relaci?", "MessageConfirmDeleteSession": "Opravdu chcete smazat tuto relaci?",
"MessageConfirmForceReScan": "Opravdu chcete vynutit opětovné prohledání?", "MessageConfirmForceReScan": "Opravdu chcete vynutit opětovné prohledání?",
"MessageConfirmMarkAllEpisodesFinished": "Opravdu chcete označit všechny epizody jako dokončené?", "MessageConfirmMarkAllEpisodesFinished": "Opravdu chcete označit všechny epizody jako dokončené?",
"MessageConfirmMarkAllEpisodesNotFinished": "Opravdu chcete označit všechny epizody jako nedokončené?", "MessageConfirmMarkAllEpisodesNotFinished": "Opravdu chcete označit všechny epizody jako nedokončené?",
"MessageConfirmMarkItemFinished": "Opravdu chcete označit \"{0}\" jako dokončené?",
"MessageConfirmMarkItemNotFinished": "Opravdu chcete označit \"{0}\" jako nedokončené?",
"MessageConfirmMarkSeriesFinished": "Opravdu chcete označit všechny knihy z této série jako dokončené?", "MessageConfirmMarkSeriesFinished": "Opravdu chcete označit všechny knihy z této série jako dokončené?",
"MessageConfirmMarkSeriesNotFinished": "Opravdu chcete označit všechny knihy z této série jako nedokončené?", "MessageConfirmMarkSeriesNotFinished": "Opravdu chcete označit všechny knihy z této série jako nedokončené?",
"MessageConfirmPurgeCache": "Vyčistit mezipaměť odstraní celý adresář na adrese <code>/metadata/cache</code>. <br /><br />Určitě chcete odstranit adresář mezipaměti?", "MessageConfirmPurgeCache": "Vyčistit mezipaměť odstraní celý adresář na adrese <code>/metadata/cache</code>. <br /><br />Určitě chcete odstranit adresář mezipaměti?",
@ -648,7 +699,9 @@
"MessageConfirmRenameTag": "Opravdu chcete přejmenovat tag \"{0}\" na \"{1}\" pro všechny položky?", "MessageConfirmRenameTag": "Opravdu chcete přejmenovat tag \"{0}\" na \"{1}\" pro všechny položky?",
"MessageConfirmRenameTagMergeNote": "Poznámka: Tato značka již existuje, takže budou sloučeny.", "MessageConfirmRenameTagMergeNote": "Poznámka: Tato značka již existuje, takže budou sloučeny.",
"MessageConfirmRenameTagWarning": "Varování! Podobná značka s jinými velkými a malými písmeny již existuje \"{0}\".", "MessageConfirmRenameTagWarning": "Varování! Podobná značka s jinými velkými a malými písmeny již existuje \"{0}\".",
"MessageConfirmResetProgress": "Opravdu chcete zahodit svůj pokrok?",
"MessageConfirmSendEbookToDevice": "Opravdu chcete odeslat e-knihu {0} {1}\" do zařízení \"{2}\"?", "MessageConfirmSendEbookToDevice": "Opravdu chcete odeslat e-knihu {0} {1}\" do zařízení \"{2}\"?",
"MessageConfirmUnlinkOpenId": "Opravdu chcete odpojit tohoto uživatele z OpenID?",
"MessageDownloadingEpisode": "Stahuji epizodu", "MessageDownloadingEpisode": "Stahuji epizodu",
"MessageDragFilesIntoTrackOrder": "Přetáhněte soubory do správného pořadí stop", "MessageDragFilesIntoTrackOrder": "Přetáhněte soubory do správného pořadí stop",
"MessageEmbedFailed": "Vložení selhalo!", "MessageEmbedFailed": "Vložení selhalo!",
@ -656,7 +709,7 @@
"MessageEpisodesQueuedForDownload": "{0} Epizody zařazené do fronty ke stažení", "MessageEpisodesQueuedForDownload": "{0} Epizody zařazené do fronty ke stažení",
"MessageEreaderDevices": "Aby bylo zajištěno doručení elektronických knih, může být nutné přidat výše uvedenou e-mailovou adresu jako platného odesílatele pro každé zařízení uvedené níže.", "MessageEreaderDevices": "Aby bylo zajištěno doručení elektronických knih, může být nutné přidat výše uvedenou e-mailovou adresu jako platného odesílatele pro každé zařízení uvedené níže.",
"MessageFeedURLWillBe": "URL zdroje bude {0}", "MessageFeedURLWillBe": "URL zdroje bude {0}",
"MessageFetching": "Stahování...", "MessageFetching": "Načítání...",
"MessageForceReScanDescription": "znovu prohledá všechny soubory jako při novém skenování. ID3 tagy zvukových souborů OPF soubory a textové soubory budou skenovány jako nové.", "MessageForceReScanDescription": "znovu prohledá všechny soubory jako při novém skenování. ID3 tagy zvukových souborů OPF soubory a textové soubory budou skenovány jako nové.",
"MessageImportantNotice": "Důležité upozornění!", "MessageImportantNotice": "Důležité upozornění!",
"MessageInsertChapterBelow": "Vložit kapitolu níže", "MessageInsertChapterBelow": "Vložit kapitolu níže",
@ -683,6 +736,7 @@
"MessageNoCollections": "Žádné kolekce", "MessageNoCollections": "Žádné kolekce",
"MessageNoCoversFound": "Nebyly nalezeny žádné obálky", "MessageNoCoversFound": "Nebyly nalezeny žádné obálky",
"MessageNoDescription": "Bez popisu", "MessageNoDescription": "Bez popisu",
"MessageNoDevices": "Žádná zařízení",
"MessageNoDownloadsInProgress": "Momentálně neprobíhá žádné stahování", "MessageNoDownloadsInProgress": "Momentálně neprobíhá žádné stahování",
"MessageNoDownloadsQueued": "Žádné stahování ve frontě", "MessageNoDownloadsQueued": "Žádné stahování ve frontě",
"MessageNoEpisodeMatchesFound": "Nebyly nalezeny žádné odpovídající epizody", "MessageNoEpisodeMatchesFound": "Nebyly nalezeny žádné odpovídající epizody",
@ -710,6 +764,7 @@
"MessagePauseChapter": "Pozastavit přehrávání kapitoly", "MessagePauseChapter": "Pozastavit přehrávání kapitoly",
"MessagePlayChapter": "Poslechnout si začátek kapitoly", "MessagePlayChapter": "Poslechnout si začátek kapitoly",
"MessagePlaylistCreateFromCollection": "Vytvořit seznam skladeb z kolekce", "MessagePlaylistCreateFromCollection": "Vytvořit seznam skladeb z kolekce",
"MessagePleaseWait": "Čekejte prosím...",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast nemá žádnou adresu URL kanálu RSS, kterou by mohl použít pro porovnávání", "MessagePodcastHasNoRSSFeedForMatching": "Podcast nemá žádnou adresu URL kanálu RSS, kterou by mohl použít pro porovnávání",
"MessageQuickMatchDescription": "Vyplňte prázdné detaily položky a obálku prvním výsledkem shody z '{0}'. Nepřepisuje podrobnosti, pokud není povoleno nastavení serveru \"Preferovat párování metadata\".", "MessageQuickMatchDescription": "Vyplňte prázdné detaily položky a obálku prvním výsledkem shody z '{0}'. Nepřepisuje podrobnosti, pokud není povoleno nastavení serveru \"Preferovat párování metadata\".",
"MessageRemoveChapter": "Odstranit kapitolu", "MessageRemoveChapter": "Odstranit kapitolu",
@ -728,17 +783,46 @@
"MessageShareExpiresIn": "Expiruje za {0}", "MessageShareExpiresIn": "Expiruje za {0}",
"MessageShareURLWillBe": "Sdílené URL bude <strong>{0}</strong>", "MessageShareURLWillBe": "Sdílené URL bude <strong>{0}</strong>",
"MessageStartPlaybackAtTime": "Spustit přehrávání pro \"{0}\" v {1}?", "MessageStartPlaybackAtTime": "Spustit přehrávání pro \"{0}\" v {1}?",
"MessageTaskAudioFileNotWritable": "Nelze zapisovat do audio souboru \"{0}\"",
"MessageTaskCanceledByUser": "Task zrušen uživatelem",
"MessageTaskDownloadingEpisodeDescription": "Stahování epizody \"{0}\"",
"MessageTaskEmbeddingMetadata": "Vkládání metadat",
"MessageTaskEmbeddingMetadataDescription": "Vkládání metadat do audioknihy \"{0}\"",
"MessageTaskEncodingM4b": "Kódování M4B",
"MessageTaskEncodingM4bDescription": "Kódování audioknihy \"{0}\" do jednoho m4b souboru",
"MessageTaskFailed": "Selhalo",
"MessageTaskFailedToBackupAudioFile": "Zálohování audio souboru \"{0}\" se selhalo",
"MessageTaskFailedToCreateCacheDirectory": "Vytvoření cache adresáře selhalo",
"MessageTaskFailedToEmbedMetadataInFile": "Vkládání metadat do souboru \"{0}\" selhalo",
"MessageTaskFailedToMergeAudioFiles": "Spojení audio souborů selhalo",
"MessageTaskFailedToMoveM4bFile": "Přesunutí m4b souboru selhalo",
"MessageTaskFailedToWriteMetadataFile": "Zápis souboru metadat selhal",
"MessageTaskNoFilesToScan": "Žádné soubory ke skenování",
"MessageTaskOpmlImport": "Import OPML",
"MessageTaskOpmlImportDescription": "Vytváření podcastů z {0} RSS feedů",
"MessageTaskOpmlImportFeedDescription": "Importování RSS feedu \"{0}\"",
"MessageTaskOpmlImportFeedPodcastDescription": "Vytváření podcastu \"{0}\"",
"MessageTaskOpmlImportFeedPodcastExists": "Podcast se stejnou cestou již existuje",
"MessageTaskOpmlImportFeedPodcastFailed": "Vytváření podcastu selhalo",
"MessageTaskOpmlImportFinished": "Přidáno {0} podcastů",
"MessageTaskScanItemsAdded": "{0} přidáno",
"MessageTaskScanItemsMissing": "{0} chybí",
"MessageTaskScanItemsUpdated": "{0} aktualizováno",
"MessageTaskScanNoChangesNeeded": "Žádné změny nejsou nutné",
"MessageTaskScanningFileChanges": "Skenování změn souborů v \"{0}\"",
"MessageTaskScanningLibrary": "Skenování \"{0}\" knihovny",
"MessageTaskTargetDirectoryNotWritable": "Do cílové složky nelze zapisovat",
"MessageThinking": "Přemýšlení...", "MessageThinking": "Přemýšlení...",
"MessageUploaderItemFailed": "Nahrávání se nezdařilo", "MessageUploaderItemFailed": "Nahrávání selhalo",
"MessageUploaderItemSuccess": "Nahráno bylo úspěšně!", "MessageUploaderItemSuccess": "Úspěšně nahráno!",
"MessageUploading": "Odesílám...", "MessageUploading": "Nahrávám...",
"MessageValidCronExpression": "Platný výraz cronu", "MessageValidCronExpression": "Platný výraz cronu",
"MessageWatcherIsDisabledGlobally": "Hlídač je globálně zakázán v nastavení serveru", "MessageWatcherIsDisabledGlobally": "Hlídač je globálně zakázán v nastavení serveru",
"MessageXLibraryIsEmpty": "{0} knihovna je prázdná!", "MessageXLibraryIsEmpty": "{0} knihovna je prázdná!",
"MessageYourAudiobookDurationIsLonger": "Doba trvání audioknihy je delší než nalezená délka", "MessageYourAudiobookDurationIsLonger": "Délka audioknihy je delší, než byla nalezena",
"MessageYourAudiobookDurationIsShorter": "Délka audioknihy je kratší, než byla nalezena", "MessageYourAudiobookDurationIsShorter": "Délka audioknihy je kratší, než byla nalezena",
"NoteChangeRootPassword": "Uživatel root je jediný uživatel, který může mít prázdné heslo", "NoteChangeRootPassword": "Uživatel root je jediný uživatel, který může mít prázdné heslo",
"NoteChapterEditorTimes": "Poznámka: Čas začátku první kapitoly musí zůstat v 0:00 a čas začátku poslední kapitoly nesmí překročit tuto dobu trvání audioknihy.", "NoteChapterEditorTimes": "Poznámka: Čas začátku první kapitoly musí zůstat na 0:00 a čas začátku poslední kapitoly nesmí překročit dobu trvání audioknihy.",
"NoteFolderPicker": "Poznámka: složky, které jsou již namapovány, nebudou zobrazeny", "NoteFolderPicker": "Poznámka: složky, které jsou již namapovány, nebudou zobrazeny",
"NoteRSSFeedPodcastAppsHttps": "Upozornění: Většina aplikací pro podcasty bude vyžadovat, aby adresa URL kanálu RSS používala protokol HTTPS", "NoteRSSFeedPodcastAppsHttps": "Upozornění: Většina aplikací pro podcasty bude vyžadovat, aby adresa URL kanálu RSS používala protokol HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Upozornění: 1 nebo více epizod nemá datum vydání. Některé podcastové aplikace to vyžadují.", "NoteRSSFeedPodcastAppsPubDate": "Upozornění: 1 nebo více epizod nemá datum vydání. Některé podcastové aplikace to vyžadují.",
@ -752,8 +836,10 @@
"PlaceholderSearchEpisode": "Hledat epizodu..", "PlaceholderSearchEpisode": "Hledat epizodu..",
"StatsAuthorsAdded": "autoři přidáni", "StatsAuthorsAdded": "autoři přidáni",
"StatsBooksAdded": "knihy přidány", "StatsBooksAdded": "knihy přidány",
"StatsBooksAdditional": "Některé další zahrnují…",
"StatsBooksFinished": "dokončené knihy", "StatsBooksFinished": "dokončené knihy",
"StatsBooksFinishedThisYear": "Některé knihy dokončené tento rok…", "StatsBooksFinishedThisYear": "Některé knihy dokončené tento rok…",
"StatsCollectionGrewTo": "Vaše kolekce knih se rozrostla na…",
"StatsSessions": "sezení", "StatsSessions": "sezení",
"StatsSpentListening": "stráveno posloucháním", "StatsSpentListening": "stráveno posloucháním",
"StatsTopAuthor": "TOP AUTOR", "StatsTopAuthor": "TOP AUTOR",
@ -763,59 +849,75 @@
"StatsTopMonth": "TOP MĚSÍC", "StatsTopMonth": "TOP MĚSÍC",
"StatsTotalDuration": "S celkovou dobou…", "StatsTotalDuration": "S celkovou dobou…",
"StatsYearInReview": "ROK V PŘEHLEDU", "StatsYearInReview": "ROK V PŘEHLEDU",
"ToastAccountUpdateFailed": "Aktualizace účtu se nezdařila",
"ToastAccountUpdateSuccess": "Účet aktualizován", "ToastAccountUpdateSuccess": "Účet aktualizován",
"ToastAppriseUrlRequired": "Je nutné zadat Apprise URL",
"ToastAuthorImageRemoveSuccess": "Obrázek autora odstraněn", "ToastAuthorImageRemoveSuccess": "Obrázek autora odstraněn",
"ToastAuthorUpdateFailed": "Aktualizace autora se nezdařila", "ToastAuthorNotFound": "Author \"{0}\" nenalezen",
"ToastAuthorRemoveSuccess": "Autor odstraněn",
"ToastAuthorSearchNotFound": "Autor nenalezen",
"ToastAuthorUpdateMerged": "Autor sloučen", "ToastAuthorUpdateMerged": "Autor sloučen",
"ToastAuthorUpdateSuccess": "Autor aktualizován", "ToastAuthorUpdateSuccess": "Autor aktualizován",
"ToastAuthorUpdateSuccessNoImageFound": "Autor aktualizován (nebyl nalezen žádný obrázek)", "ToastAuthorUpdateSuccessNoImageFound": "Autor aktualizován (nebyl nalezen žádný obrázek)",
"ToastBackupAppliedSuccess": "Záloha obnovena",
"ToastBackupCreateFailed": "Vytvoření zálohy se nezdařilo", "ToastBackupCreateFailed": "Vytvoření zálohy se nezdařilo",
"ToastBackupCreateSuccess": "Záloha vytvořena", "ToastBackupCreateSuccess": "Záloha vytvořena",
"ToastBackupDeleteFailed": "Nepodařilo se smazat zálohu", "ToastBackupDeleteFailed": "Nepodařilo se smazat zálohu",
"ToastBackupDeleteSuccess": "Záloha smazána", "ToastBackupDeleteSuccess": "Záloha smazána",
"ToastBackupInvalidMaxKeep": "Neplatný počet záloh k zachování",
"ToastBackupInvalidMaxSize": "Neplatná maximální velikost zálohy",
"ToastBackupRestoreFailed": "Nepodařilo se obnovit zálohu", "ToastBackupRestoreFailed": "Nepodařilo se obnovit zálohu",
"ToastBackupUploadFailed": "Nepodařilo se nahrát zálohu", "ToastBackupUploadFailed": "Nepodařilo se nahrát zálohu",
"ToastBackupUploadSuccess": "Záloha nahrána", "ToastBackupUploadSuccess": "Záloha nahrána",
"ToastBatchDeleteFailed": "Hromadné smazání selhalo",
"ToastBatchDeleteSuccess": "Hromadné smazání proběhlo úspěšně",
"ToastBatchUpdateFailed": "Dávková aktualizace se nezdařila", "ToastBatchUpdateFailed": "Dávková aktualizace se nezdařila",
"ToastBatchUpdateSuccess": "Dávková aktualizace proběhla úspěšně", "ToastBatchUpdateSuccess": "Dávková aktualizace proběhla úspěšně",
"ToastBookmarkCreateFailed": "Vytvoření záložky se nezdařilo", "ToastBookmarkCreateFailed": "Vytvoření záložky se nezdařilo",
"ToastBookmarkCreateSuccess": "Přidána záložka", "ToastBookmarkCreateSuccess": "Přidána záložka",
"ToastBookmarkRemoveSuccess": "Záložka odstraněna", "ToastBookmarkRemoveSuccess": "Záložka odstraněna",
"ToastBookmarkUpdateFailed": "Aktualizace záložky se nezdařila",
"ToastBookmarkUpdateSuccess": "Záložka aktualizována", "ToastBookmarkUpdateSuccess": "Záložka aktualizována",
"ToastCachePurgeFailed": "Nepodařilo se vyčistit mezipaměť", "ToastCachePurgeFailed": "Nepodařilo se vyčistit mezipaměť",
"ToastCachePurgeSuccess": "Vyrovnávací paměť úspěšně vyčištěna", "ToastCachePurgeSuccess": "Vyrovnávací paměť úspěšně vyčištěna",
"ToastChaptersHaveErrors": "Kapitoly obsahují chyby", "ToastChaptersHaveErrors": "Kapitoly obsahují chyby",
"ToastChaptersMustHaveTitles": "Kapitoly musí mít názvy", "ToastChaptersMustHaveTitles": "Kapitoly musí mít názvy",
"ToastChaptersRemoved": "Kapitoly odstraněny",
"ToastCollectionItemsRemoveSuccess": "Položky odstraněny z kolekce", "ToastCollectionItemsRemoveSuccess": "Položky odstraněny z kolekce",
"ToastCollectionRemoveSuccess": "Kolekce odstraněna", "ToastCollectionRemoveSuccess": "Kolekce odstraněna",
"ToastCollectionUpdateFailed": "Aktualizace kolekce se nezdařila",
"ToastCollectionUpdateSuccess": "Kolekce aktualizována", "ToastCollectionUpdateSuccess": "Kolekce aktualizována",
"ToastCoverUpdateFailed": "Aktualizace obálky selhala",
"ToastDeleteFileFailed": "Nepodařilo se smazat soubor", "ToastDeleteFileFailed": "Nepodařilo se smazat soubor",
"ToastDeleteFileSuccess": "Soubor smazán", "ToastDeleteFileSuccess": "Soubor smazán",
"ToastDeviceAddFailed": "Přidání zařízení selhalo",
"ToastDeviceNameAlreadyExists": "Zařízení se stejným jménem již existuje",
"ToastDeviceTestEmailFailed": "Odeslání testovacího emailu selhalo",
"ToastDeviceTestEmailSuccess": "Testovací email byl odeslán",
"ToastEmailSettingsUpdateSuccess": "Nastavení emailu aktualizována",
"ToastEpisodeDownloadQueueClearFailed": "Vyčištění fronty selhalo",
"ToastErrorCannotShare": "Na tomto zařízení nelze nativně sdílet", "ToastErrorCannotShare": "Na tomto zařízení nelze nativně sdílet",
"ToastFailedToLoadData": "Nepodařilo se načíst data", "ToastFailedToLoadData": "Nepodařilo se načíst data",
"ToastItemCoverUpdateFailed": "Aktualizace obálky se nezdařila", "ToastFailedToShare": "Sdílení selhalo",
"ToastFailedToUpdate": "Aktualizace selhala",
"ToastInvalidImageUrl": "Neplatná URL obrázku",
"ToastInvalidUrl": "Neplatná URL",
"ToastItemCoverUpdateSuccess": "Obálka předmětu byl aktualizována", "ToastItemCoverUpdateSuccess": "Obálka předmětu byl aktualizována",
"ToastItemDetailsUpdateFailed": "Nepodařilo se aktualizovat podrobnosti o položce", "ToastItemDeletedFailed": "Smazání položky selhalo",
"ToastItemDeletedSuccess": "Položka smazána",
"ToastItemDetailsUpdateSuccess": "Podrobnosti o položce byly aktualizovány", "ToastItemDetailsUpdateSuccess": "Podrobnosti o položce byly aktualizovány",
"ToastItemMarkedAsFinishedFailed": "Nepodařilo se označit jako dokončené", "ToastItemMarkedAsFinishedFailed": "Nepodařilo se označit jako dokončené",
"ToastItemMarkedAsFinishedSuccess": "Položka označena jako dokončená", "ToastItemMarkedAsFinishedSuccess": "Položka označena jako dokončená",
"ToastItemMarkedAsNotFinishedFailed": "Nepodařilo se označit jako nedokončené", "ToastItemMarkedAsNotFinishedFailed": "Nepodařilo se označit jako nedokončené",
"ToastItemMarkedAsNotFinishedSuccess": "Položka označena jako nedokončená", "ToastItemMarkedAsNotFinishedSuccess": "Položka označena jako nedokončená",
"ToastItemUpdateSuccess": "Položka aktualizována",
"ToastLibraryCreateFailed": "Vytvoření knihovny se nezdařilo", "ToastLibraryCreateFailed": "Vytvoření knihovny se nezdařilo",
"ToastLibraryCreateSuccess": "Knihovna \"{0}\" vytvořena", "ToastLibraryCreateSuccess": "Knihovna \"{0}\" vytvořena",
"ToastLibraryDeleteFailed": "Nepodařilo se smazat knihovnu", "ToastLibraryDeleteFailed": "Nepodařilo se smazat knihovnu",
"ToastLibraryDeleteSuccess": "Knihovna smazána", "ToastLibraryDeleteSuccess": "Knihovna smazána",
"ToastLibraryScanFailedToStart": "Nepodařilo se spustit kontrolu", "ToastLibraryScanFailedToStart": "Nepodařilo se spustit kontrolu",
"ToastLibraryScanStarted": "Kontrola knihovny spuštěna", "ToastLibraryScanStarted": "Kontrola knihovny spuštěna",
"ToastLibraryUpdateFailed": "Aktualizace knihovny se nezdařila",
"ToastLibraryUpdateSuccess": "Knihovna \"{0}\" aktualizována", "ToastLibraryUpdateSuccess": "Knihovna \"{0}\" aktualizována",
"ToastPlaylistCreateFailed": "Vytvoření seznamu přehrávání se nezdařilo", "ToastPlaylistCreateFailed": "Vytvoření seznamu přehrávání se nezdařilo",
"ToastPlaylistCreateSuccess": "Seznam přehrávání vytvořen", "ToastPlaylistCreateSuccess": "Seznam přehrávání vytvořen",
"ToastPlaylistRemoveSuccess": "Seznam přehrávání odstraněn", "ToastPlaylistRemoveSuccess": "Seznam přehrávání odstraněn",
"ToastPlaylistUpdateFailed": "Aktualizace seznamu přehrávání se nezdařila",
"ToastPlaylistUpdateSuccess": "Seznam přehrávání aktualizován", "ToastPlaylistUpdateSuccess": "Seznam přehrávání aktualizován",
"ToastPodcastCreateFailed": "Vytvoření podcastu se nezdařilo", "ToastPodcastCreateFailed": "Vytvoření podcastu se nezdařilo",
"ToastPodcastCreateSuccess": "Podcast byl úspěšně vytvořen", "ToastPodcastCreateSuccess": "Podcast byl úspěšně vytvořen",
@ -827,7 +929,6 @@
"ToastSendEbookToDeviceSuccess": "E-kniha odeslána do zařízení \"{0}\"", "ToastSendEbookToDeviceSuccess": "E-kniha odeslána do zařízení \"{0}\"",
"ToastSeriesUpdateFailed": "Aktualizace série se nezdařila", "ToastSeriesUpdateFailed": "Aktualizace série se nezdařila",
"ToastSeriesUpdateSuccess": "Aktualizace série byla úspěšná", "ToastSeriesUpdateSuccess": "Aktualizace série byla úspěšná",
"ToastServerSettingsUpdateFailed": "Nepodařilo se aktualizovat nastavení serveru",
"ToastServerSettingsUpdateSuccess": "Nastavení serveru aktualizováno", "ToastServerSettingsUpdateSuccess": "Nastavení serveru aktualizováno",
"ToastSessionDeleteFailed": "Nepodařilo se smazat relaci", "ToastSessionDeleteFailed": "Nepodařilo se smazat relaci",
"ToastSessionDeleteSuccess": "Relace smazána", "ToastSessionDeleteSuccess": "Relace smazána",
@ -835,7 +936,6 @@
"ToastSocketDisconnected": "Socket odpojen", "ToastSocketDisconnected": "Socket odpojen",
"ToastSocketFailedToConnect": "Socket se nepodařilo připojit", "ToastSocketFailedToConnect": "Socket se nepodařilo připojit",
"ToastSortingPrefixesEmptyError": "Musí mít alespoň 1 třídicí předponu", "ToastSortingPrefixesEmptyError": "Musí mít alespoň 1 třídicí předponu",
"ToastSortingPrefixesUpdateFailed": "Nepodařilo se aktualizovat třídicí předpony",
"ToastSortingPrefixesUpdateSuccess": "Aktualizovány předpony třídění ({0} položek)", "ToastSortingPrefixesUpdateSuccess": "Aktualizovány předpony třídění ({0} položek)",
"ToastUserDeleteFailed": "Nepodařilo se smazat uživatele", "ToastUserDeleteFailed": "Nepodařilo se smazat uživatele",
"ToastUserDeleteSuccess": "Uživatel smazán" "ToastUserDeleteSuccess": "Uživatel smazán"

View file

@ -1,7 +1,10 @@
{ {
"ButtonAdd": "Tilføj", "ButtonAdd": "Tilføj",
"ButtonAddChapters": "Tilføj kapitler", "ButtonAddChapters": "Tilføj kapitler",
"ButtonAddDevice": "Tilføj enhed",
"ButtonAddLibrary": "Tilføj Bibliotek",
"ButtonAddPodcasts": "Tilføj podcasts", "ButtonAddPodcasts": "Tilføj podcasts",
"ButtonAddUser": "Tilføj bruger",
"ButtonAddYourFirstLibrary": "Tilføj din første bibliotek", "ButtonAddYourFirstLibrary": "Tilføj din første bibliotek",
"ButtonApply": "Anvend", "ButtonApply": "Anvend",
"ButtonApplyChapters": "Anvend kapitler", "ButtonApplyChapters": "Anvend kapitler",
@ -25,6 +28,7 @@
"ButtonEdit": "Rediger", "ButtonEdit": "Rediger",
"ButtonEditChapters": "Rediger kapitler", "ButtonEditChapters": "Rediger kapitler",
"ButtonEditPodcast": "Rediger podcast", "ButtonEditPodcast": "Rediger podcast",
"ButtonEnable": "Aktiver",
"ButtonForceReScan": "Tvungen genindlæsning", "ButtonForceReScan": "Tvungen genindlæsning",
"ButtonFullPath": "Fuld sti", "ButtonFullPath": "Fuld sti",
"ButtonHide": "Skjul", "ButtonHide": "Skjul",
@ -42,6 +46,7 @@
"ButtonOk": "OK", "ButtonOk": "OK",
"ButtonOpenFeed": "Åbn feed", "ButtonOpenFeed": "Åbn feed",
"ButtonOpenManager": "Åbn manager", "ButtonOpenManager": "Åbn manager",
"ButtonPause": "Pause",
"ButtonPlay": "Afspil", "ButtonPlay": "Afspil",
"ButtonPlaying": "Afspiller", "ButtonPlaying": "Afspiller",
"ButtonPlaylists": "Afspilningslister", "ButtonPlaylists": "Afspilningslister",
@ -66,7 +71,7 @@
"ButtonScanLibrary": "Scan Bibliotek", "ButtonScanLibrary": "Scan Bibliotek",
"ButtonSearch": "Søg", "ButtonSearch": "Søg",
"ButtonSelectFolderPath": "Vælg Mappen Sti", "ButtonSelectFolderPath": "Vælg Mappen Sti",
"ButtonSeries": "Serie", "ButtonSeries": "Serier",
"ButtonSetChaptersFromTracks": "Sæt kapitler fra spor", "ButtonSetChaptersFromTracks": "Sæt kapitler fra spor",
"ButtonShiftTimes": "Skift Tider", "ButtonShiftTimes": "Skift Tider",
"ButtonShow": "Vis", "ButtonShow": "Vis",
@ -188,14 +193,14 @@
"LabelChapters": "Kapitler", "LabelChapters": "Kapitler",
"LabelChaptersFound": "fundne kapitler", "LabelChaptersFound": "fundne kapitler",
"LabelClosePlayer": "Luk afspiller", "LabelClosePlayer": "Luk afspiller",
"LabelCollapseSeries": "Fold Serie Sammen", "LabelCollapseSeries": "Fold Serier Sammen",
"LabelCollection": "Samling", "LabelCollection": "Samling",
"LabelCollections": "Samlinger", "LabelCollections": "Samlinger",
"LabelComplete": "Fuldfør", "LabelComplete": "Fuldfør",
"LabelConfirmPassword": "Bekræft Adgangskode", "LabelConfirmPassword": "Bekræft Adgangskode",
"LabelContinueListening": "Fortsæt Lytning", "LabelContinueListening": "Fortsæt med at lytte",
"LabelContinueReading": "Fortsæt Læsning", "LabelContinueReading": "Fortsæt med at læse",
"LabelContinueSeries": "Fortsæt Serie", "LabelContinueSeries": "Fortsæt Serien",
"LabelCover": "Omslag", "LabelCover": "Omslag",
"LabelCoverImageURL": "Omslagsbillede URL", "LabelCoverImageURL": "Omslagsbillede URL",
"LabelCreatedAt": "Oprettet Kl.", "LabelCreatedAt": "Oprettet Kl.",
@ -212,6 +217,7 @@
"LabelDiscFromFilename": "Disk fra Filnavn", "LabelDiscFromFilename": "Disk fra Filnavn",
"LabelDiscFromMetadata": "Disk fra Metadata", "LabelDiscFromMetadata": "Disk fra Metadata",
"LabelDiscover": "Opdag", "LabelDiscover": "Opdag",
"LabelDownload": "Download",
"LabelDownloadNEpisodes": "Download {0} episoder", "LabelDownloadNEpisodes": "Download {0} episoder",
"LabelDuration": "Varighed", "LabelDuration": "Varighed",
"LabelDurationFound": "Fundet varighed:", "LabelDurationFound": "Fundet varighed:",
@ -225,12 +231,15 @@
"LabelEmbeddedCover": "Indlejret Omslag", "LabelEmbeddedCover": "Indlejret Omslag",
"LabelEnable": "Aktivér", "LabelEnable": "Aktivér",
"LabelEnd": "Slut", "LabelEnd": "Slut",
"LabelEndOfChapter": "Slutningen af kapitel",
"LabelEpisode": "Episode",
"LabelEpisodeTitle": "Episodetitel", "LabelEpisodeTitle": "Episodetitel",
"LabelEpisodeType": "Episodetype", "LabelEpisodeType": "Episodetype",
"LabelExample": "Eksempel", "LabelExample": "Eksempel",
"LabelExplicit": "Eksplisit", "LabelExplicit": "Eksplisit",
"LabelFeedURL": "Feed URL",
"LabelFile": "Fil", "LabelFile": "Fil",
"LabelFileBirthtime": "Fødselstidspunkt for fil", "LabelFileBirthtime": "Oprettelsestidspunkt for fil",
"LabelFileModified": "Fil ændret", "LabelFileModified": "Fil ændret",
"LabelFilename": "Filnavn", "LabelFilename": "Filnavn",
"LabelFilterByUser": "Filtrér efter bruger", "LabelFilterByUser": "Filtrér efter bruger",
@ -238,8 +247,10 @@
"LabelFinished": "Færdig", "LabelFinished": "Færdig",
"LabelFolder": "Mappe", "LabelFolder": "Mappe",
"LabelFolders": "Mapper", "LabelFolders": "Mapper",
"LabelFontBoldness": "Skrift tykkelse",
"LabelFontFamily": "Fontfamilie", "LabelFontFamily": "Fontfamilie",
"LabelFontScale": "Skriftstørrelse", "LabelFontScale": "Skriftstørrelse",
"LabelGenre": "Genre",
"LabelGenres": "Genrer", "LabelGenres": "Genrer",
"LabelHardDeleteFile": "Permanent slet fil", "LabelHardDeleteFile": "Permanent slet fil",
"LabelHasEbook": "Har e-bog", "LabelHasEbook": "Har e-bog",
@ -267,6 +278,7 @@
"LabelLastSeen": "Sidst set", "LabelLastSeen": "Sidst set",
"LabelLastTime": "Sidste gang", "LabelLastTime": "Sidste gang",
"LabelLastUpdate": "Seneste opdatering", "LabelLastUpdate": "Seneste opdatering",
"LabelLayout": "Layout",
"LabelLayoutSinglePage": "Enkeltside", "LabelLayoutSinglePage": "Enkeltside",
"LabelLayoutSplitPage": "Opdelt side", "LabelLayoutSplitPage": "Opdelt side",
"LabelLess": "Mindre", "LabelLess": "Mindre",
@ -344,10 +356,11 @@
"LabelRSSFeedPreventIndexing": "Forhindrer indeksering", "LabelRSSFeedPreventIndexing": "Forhindrer indeksering",
"LabelRSSFeedSlug": "RSS-feed-slug", "LabelRSSFeedSlug": "RSS-feed-slug",
"LabelRSSFeedURL": "RSS-feed-URL", "LabelRSSFeedURL": "RSS-feed-URL",
"LabelRandomly": "Tilfældigt",
"LabelRead": "Læst", "LabelRead": "Læst",
"LabelReadAgain": "Læs igen", "LabelReadAgain": "Læs igen",
"LabelReadEbookWithoutProgress": "Læs e-bog uden at følge fremskridt", "LabelReadEbookWithoutProgress": "Læs e-bog uden at følge fremskridt",
"LabelRecentSeries": "Seneste serie", "LabelRecentSeries": "Seneste serier",
"LabelRecentlyAdded": "Senest tilføjet", "LabelRecentlyAdded": "Senest tilføjet",
"LabelRecommended": "Anbefalet", "LabelRecommended": "Anbefalet",
"LabelReleaseDate": "Udgivelsesdato", "LabelReleaseDate": "Udgivelsesdato",
@ -604,10 +617,8 @@
"PlaceholderNewPlaylist": "Nyt afspilningslistnavn", "PlaceholderNewPlaylist": "Nyt afspilningslistnavn",
"PlaceholderSearch": "Søg..", "PlaceholderSearch": "Søg..",
"PlaceholderSearchEpisode": "Søg efter episode..", "PlaceholderSearchEpisode": "Søg efter episode..",
"ToastAccountUpdateFailed": "Mislykkedes opdatering af konto",
"ToastAccountUpdateSuccess": "Konto opdateret", "ToastAccountUpdateSuccess": "Konto opdateret",
"ToastAuthorImageRemoveSuccess": "Forfatterbillede fjernet", "ToastAuthorImageRemoveSuccess": "Forfatterbillede fjernet",
"ToastAuthorUpdateFailed": "Mislykkedes opdatering af forfatter",
"ToastAuthorUpdateMerged": "Forfatter fusioneret", "ToastAuthorUpdateMerged": "Forfatter fusioneret",
"ToastAuthorUpdateSuccess": "Forfatter opdateret", "ToastAuthorUpdateSuccess": "Forfatter opdateret",
"ToastAuthorUpdateSuccessNoImageFound": "Forfatter opdateret (ingen billede fundet)", "ToastAuthorUpdateSuccessNoImageFound": "Forfatter opdateret (ingen billede fundet)",
@ -623,17 +634,13 @@
"ToastBookmarkCreateFailed": "Mislykkedes oprettelse af bogmærke", "ToastBookmarkCreateFailed": "Mislykkedes oprettelse af bogmærke",
"ToastBookmarkCreateSuccess": "Bogmærke tilføjet", "ToastBookmarkCreateSuccess": "Bogmærke tilføjet",
"ToastBookmarkRemoveSuccess": "Bogmærke fjernet", "ToastBookmarkRemoveSuccess": "Bogmærke fjernet",
"ToastBookmarkUpdateFailed": "Mislykkedes opdatering af bogmærke",
"ToastBookmarkUpdateSuccess": "Bogmærke opdateret", "ToastBookmarkUpdateSuccess": "Bogmærke opdateret",
"ToastChaptersHaveErrors": "Kapitler har fejl", "ToastChaptersHaveErrors": "Kapitler har fejl",
"ToastChaptersMustHaveTitles": "Kapitler skal have titler", "ToastChaptersMustHaveTitles": "Kapitler skal have titler",
"ToastCollectionItemsRemoveSuccess": "Element(er) fjernet fra samlingen", "ToastCollectionItemsRemoveSuccess": "Element(er) fjernet fra samlingen",
"ToastCollectionRemoveSuccess": "Samling fjernet", "ToastCollectionRemoveSuccess": "Samling fjernet",
"ToastCollectionUpdateFailed": "Mislykkedes opdatering af samling",
"ToastCollectionUpdateSuccess": "Samling opdateret", "ToastCollectionUpdateSuccess": "Samling opdateret",
"ToastItemCoverUpdateFailed": "Mislykkedes opdatering af varens omslag",
"ToastItemCoverUpdateSuccess": "Varens omslag opdateret", "ToastItemCoverUpdateSuccess": "Varens omslag opdateret",
"ToastItemDetailsUpdateFailed": "Mislykkedes opdatering af varedetaljer",
"ToastItemDetailsUpdateSuccess": "Varedetaljer opdateret", "ToastItemDetailsUpdateSuccess": "Varedetaljer opdateret",
"ToastItemMarkedAsFinishedFailed": "Mislykkedes markering som afsluttet", "ToastItemMarkedAsFinishedFailed": "Mislykkedes markering som afsluttet",
"ToastItemMarkedAsFinishedSuccess": "Vare markeret som afsluttet", "ToastItemMarkedAsFinishedSuccess": "Vare markeret som afsluttet",
@ -645,12 +652,10 @@
"ToastLibraryDeleteSuccess": "Bibliotek slettet", "ToastLibraryDeleteSuccess": "Bibliotek slettet",
"ToastLibraryScanFailedToStart": "Mislykkedes start af skanning", "ToastLibraryScanFailedToStart": "Mislykkedes start af skanning",
"ToastLibraryScanStarted": "Biblioteksskanning startet", "ToastLibraryScanStarted": "Biblioteksskanning startet",
"ToastLibraryUpdateFailed": "Mislykkedes opdatering af bibliotek",
"ToastLibraryUpdateSuccess": "Bibliotek \"{0}\" opdateret", "ToastLibraryUpdateSuccess": "Bibliotek \"{0}\" opdateret",
"ToastPlaylistCreateFailed": "Mislykkedes oprettelse af afspilningsliste", "ToastPlaylistCreateFailed": "Mislykkedes oprettelse af afspilningsliste",
"ToastPlaylistCreateSuccess": "Afspilningsliste oprettet", "ToastPlaylistCreateSuccess": "Afspilningsliste oprettet",
"ToastPlaylistRemoveSuccess": "Afspilningsliste fjernet", "ToastPlaylistRemoveSuccess": "Afspilningsliste fjernet",
"ToastPlaylistUpdateFailed": "Mislykkedes opdatering af afspilningsliste",
"ToastPlaylistUpdateSuccess": "Afspilningsliste opdateret", "ToastPlaylistUpdateSuccess": "Afspilningsliste opdateret",
"ToastPodcastCreateFailed": "Mislykkedes oprettelse af podcast", "ToastPodcastCreateFailed": "Mislykkedes oprettelse af podcast",
"ToastPodcastCreateSuccess": "Podcast oprettet med succes", "ToastPodcastCreateSuccess": "Podcast oprettet med succes",

View file

@ -56,6 +56,7 @@
"ButtonOpenManager": "Manager öffnen", "ButtonOpenManager": "Manager öffnen",
"ButtonPause": "Pausieren", "ButtonPause": "Pausieren",
"ButtonPlay": "Abspielen", "ButtonPlay": "Abspielen",
"ButtonPlayAll": "Alles abspielen",
"ButtonPlaying": "Spielt", "ButtonPlaying": "Spielt",
"ButtonPlaylists": "Wiedergabelisten", "ButtonPlaylists": "Wiedergabelisten",
"ButtonPrevious": "Zurück", "ButtonPrevious": "Zurück",
@ -65,12 +66,13 @@
"ButtonPurgeItemsCache": "Lösche Medien-Cache", "ButtonPurgeItemsCache": "Lösche Medien-Cache",
"ButtonQueueAddItem": "Zur Warteschlange hinzufügen", "ButtonQueueAddItem": "Zur Warteschlange hinzufügen",
"ButtonQueueRemoveItem": "Aus der Warteschlange entfernen", "ButtonQueueRemoveItem": "Aus der Warteschlange entfernen",
"ButtonQuickEmbed": "Schnelles Hinzufügen",
"ButtonQuickEmbedMetadata": "Schnelles Hinzufügen von Metadaten", "ButtonQuickEmbedMetadata": "Schnelles Hinzufügen von Metadaten",
"ButtonQuickMatch": "Schnellabgleich", "ButtonQuickMatch": "Schnellabgleich",
"ButtonReScan": "Neu scannen", "ButtonReScan": "Neu scannen",
"ButtonRead": "Lesen", "ButtonRead": "Lesen",
"ButtonReadLess": "Weniger anzeigen", "ButtonReadLess": "weniger Anzeigen",
"ButtonReadMore": "Mehr anzeigen", "ButtonReadMore": "Mehr Anzeigen",
"ButtonRefresh": "Neu Laden", "ButtonRefresh": "Neu Laden",
"ButtonRemove": "Entfernen", "ButtonRemove": "Entfernen",
"ButtonRemoveAll": "Alles entfernen", "ButtonRemoveAll": "Alles entfernen",
@ -161,6 +163,7 @@
"HeaderNotificationUpdate": "Benachrichtigung bearbeiten", "HeaderNotificationUpdate": "Benachrichtigung bearbeiten",
"HeaderNotifications": "Benachrichtigungen", "HeaderNotifications": "Benachrichtigungen",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentifizierung", "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentifizierung",
"HeaderOpenListeningSessions": "Aktive Hörbuch-Sitzungen",
"HeaderOpenRSSFeed": "RSS-Feed öffnen", "HeaderOpenRSSFeed": "RSS-Feed öffnen",
"HeaderOtherFiles": "Sonstige Dateien", "HeaderOtherFiles": "Sonstige Dateien",
"HeaderPasswordAuthentication": "Passwortauthentifizierung", "HeaderPasswordAuthentication": "Passwortauthentifizierung",
@ -178,6 +181,7 @@
"HeaderRemoveEpisodes": "Entferne {0} Episoden", "HeaderRemoveEpisodes": "Entferne {0} Episoden",
"HeaderSavedMediaProgress": "Gespeicherte Hörfortschritte", "HeaderSavedMediaProgress": "Gespeicherte Hörfortschritte",
"HeaderSchedule": "Zeitplan", "HeaderSchedule": "Zeitplan",
"HeaderScheduleEpisodeDownloads": "Automatische Episoden-Downloads planen",
"HeaderScheduleLibraryScans": "Automatische Bibliotheksscans", "HeaderScheduleLibraryScans": "Automatische Bibliotheksscans",
"HeaderSession": "Sitzung", "HeaderSession": "Sitzung",
"HeaderSetBackupSchedule": "Zeitplan für die Datensicherung festlegen", "HeaderSetBackupSchedule": "Zeitplan für die Datensicherung festlegen",
@ -216,14 +220,18 @@
"LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen", "LabelAddToPlaylist": "Zur Wiedergabeliste hinzufügen",
"LabelAddToPlaylistBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Wiedergabeliste hinzu", "LabelAddToPlaylistBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Wiedergabeliste hinzu",
"LabelAddedAt": "Hinzugefügt am", "LabelAddedAt": "Hinzugefügt am",
"LabelAddedDate": "Hinzugefügt {0}", "LabelAddedDate": "{0} Hinzugefügt",
"LabelAdminUsersOnly": "Nur Admin Benutzer", "LabelAdminUsersOnly": "Nur Admin Benutzer",
"LabelAll": "Alle", "LabelAll": "Alle",
"LabelAllUsers": "Alle Benutzer", "LabelAllUsers": "Alle Benutzer",
"LabelAllUsersExcludingGuests": "Alle Benutzer außer Gästen", "LabelAllUsersExcludingGuests": "Alle Benutzer außer Gästen",
"LabelAllUsersIncludingGuests": "Alle Benutzer und Gäste", "LabelAllUsersIncludingGuests": "Alle Benutzer und Gäste",
"LabelAlreadyInYourLibrary": "Bereits in der Bibliothek", "LabelAlreadyInYourLibrary": "Bereits in der Bibliothek",
"LabelApiToken": "API Schlüssel",
"LabelAppend": "Anhängen", "LabelAppend": "Anhängen",
"LabelAudioBitrate": "Audiobitrate (z. B. 128 kbit/s)",
"LabelAudioChannels": "Audiokanäle (1 oder 2)",
"LabelAudioCodec": "Audiocodec",
"LabelAuthor": "Autor", "LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (Vorname Nachname)", "LabelAuthorFirstLast": "Autor (Vorname Nachname)",
"LabelAuthorLastFirst": "Autor (Nachname, Vorname)", "LabelAuthorLastFirst": "Autor (Nachname, Vorname)",
@ -236,6 +244,7 @@
"LabelAutoRegister": "Automatische Registrierung", "LabelAutoRegister": "Automatische Registrierung",
"LabelAutoRegisterDescription": "Automatische neue Neutzer anlegen nach dem Registrieren", "LabelAutoRegisterDescription": "Automatische neue Neutzer anlegen nach dem Registrieren",
"LabelBackToUser": "Zurück zum Benutzer", "LabelBackToUser": "Zurück zum Benutzer",
"LabelBackupAudioFiles": "Audio-Dateien sichern",
"LabelBackupLocation": "Backup-Ort", "LabelBackupLocation": "Backup-Ort",
"LabelBackupsEnableAutomaticBackups": "Automatische Sicherung aktivieren", "LabelBackupsEnableAutomaticBackups": "Automatische Sicherung aktivieren",
"LabelBackupsEnableAutomaticBackupsHelp": "Backups werden in /metadata/backups gespeichert", "LabelBackupsEnableAutomaticBackupsHelp": "Backups werden in /metadata/backups gespeichert",
@ -244,15 +253,18 @@
"LabelBackupsNumberToKeep": "Anzahl der aufzubewahrenden Sicherungen", "LabelBackupsNumberToKeep": "Anzahl der aufzubewahrenden Sicherungen",
"LabelBackupsNumberToKeepHelp": "Es wird immer nur 1 Sicherung auf einmal entfernt. Wenn du bereits mehrere Sicherungen als die definierte max. Anzahl hast, solltest du diese manuell entfernen.", "LabelBackupsNumberToKeepHelp": "Es wird immer nur 1 Sicherung auf einmal entfernt. Wenn du bereits mehrere Sicherungen als die definierte max. Anzahl hast, solltest du diese manuell entfernen.",
"LabelBitrate": "Bitrate", "LabelBitrate": "Bitrate",
"LabelBonus": "Bonus",
"LabelBooks": "Bücher", "LabelBooks": "Bücher",
"LabelButtonText": "Knopftext", "LabelButtonText": "Knopftext",
"LabelByAuthor": "von {0}", "LabelByAuthor": "von {0}",
"LabelChangePassword": "Passwort ändern", "LabelChangePassword": "Passwort ändern",
"LabelChannels": "Kanäle", "LabelChannels": "Kanäle",
"LabelChapterCount": "{0} Kapitel",
"LabelChapterTitle": "Kapitelüberschrift", "LabelChapterTitle": "Kapitelüberschrift",
"LabelChapters": "Kapitel", "LabelChapters": "Kapitel",
"LabelChaptersFound": "Gefundene Kapitel", "LabelChaptersFound": "Gefundene Kapitel",
"LabelClickForMoreInfo": "Klicken für mehr Informationen", "LabelClickForMoreInfo": "Klicken für mehr Informationen",
"LabelClickToUseCurrentValue": "Anklicken um aktuellen Wert zu verwenden",
"LabelClosePlayer": "Player schließen", "LabelClosePlayer": "Player schließen",
"LabelCodec": "Codec", "LabelCodec": "Codec",
"LabelCollapseSeries": "Serien einklappen", "LabelCollapseSeries": "Serien einklappen",
@ -302,12 +314,25 @@
"LabelEmailSettingsTestAddress": "Test-Adresse", "LabelEmailSettingsTestAddress": "Test-Adresse",
"LabelEmbeddedCover": "Eingebettetes Cover", "LabelEmbeddedCover": "Eingebettetes Cover",
"LabelEnable": "Aktivieren", "LabelEnable": "Aktivieren",
"LabelEncodingBackupLocation": "Eine Sicherungskopie der originalen Audiodateien wird gespeichert in:",
"LabelEncodingChaptersNotEmbedded": "Kapitel sind in mehrspurigen Hörbüchern nicht eingebettet.",
"LabelEncodingClearItemCache": "Stelle sicher, dass der Cache regelmäßig geleert wird.",
"LabelEncodingFinishedM4B": "Die fertige M4B-Datei wird im Hörbuch-Ordner unter folgendem Pfad abgelegt:",
"LabelEncodingInfoEmbedded": "Metadaten werden in die Audiodateien innerhalb des Audiobook Ordners eingebunden.",
"LabelEncodingStartedNavigation": "Sobald die Aufgabe gestartet ist, kann die Seite verlassen werden.",
"LabelEncodingTimeWarning": "Kodierung kann bis zu 30 Minuten dauern.",
"LabelEncodingWarningAdvancedSettings": "Achtung: Ändere diese Einstellungen nur, wenn du dich mit ffmpeg Kodierung auskennst.",
"LabelEncodingWatcherDisabled": "Wenn der Watcher deaktiviert ist musst du das Hörbuch danach erneut scannen.",
"LabelEnd": "Ende", "LabelEnd": "Ende",
"LabelEndOfChapter": "Ende des Kapitels", "LabelEndOfChapter": "Ende des Kapitels",
"LabelEpisode": "Episode", "LabelEpisode": "Episode",
"LabelEpisodeNotLinkedToRssFeed": "Episode nicht mit RSS-Feed verknüpft",
"LabelEpisodeNumber": "Episode #{0}",
"LabelEpisodeTitle": "Episodentitel", "LabelEpisodeTitle": "Episodentitel",
"LabelEpisodeType": "Episodentyp", "LabelEpisodeType": "Episodentyp",
"LabelEpisodeUrlFromRssFeed": "Episoden URL vom RSS-Feed",
"LabelEpisodes": "Episoden", "LabelEpisodes": "Episoden",
"LabelEpisodic": "Episodisch",
"LabelExample": "Beispiel", "LabelExample": "Beispiel",
"LabelExpandSeries": "Serie ausklappen", "LabelExpandSeries": "Serie ausklappen",
"LabelExpandSubSeries": "Unterserie ausklappen", "LabelExpandSubSeries": "Unterserie ausklappen",
@ -335,6 +360,7 @@
"LabelFontScale": "Schriftgröße", "LabelFontScale": "Schriftgröße",
"LabelFontStrikethrough": "Durchgestrichen", "LabelFontStrikethrough": "Durchgestrichen",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelFull": "Voll",
"LabelGenre": "Kategorie", "LabelGenre": "Kategorie",
"LabelGenres": "Kategorien", "LabelGenres": "Kategorien",
"LabelHardDeleteFile": "Datei dauerhaft löschen", "LabelHardDeleteFile": "Datei dauerhaft löschen",
@ -390,6 +416,10 @@
"LabelLowestPriority": "Niedrigste Priorität", "LabelLowestPriority": "Niedrigste Priorität",
"LabelMatchExistingUsersBy": "Zuordnen existierender Benutzer mit", "LabelMatchExistingUsersBy": "Zuordnen existierender Benutzer mit",
"LabelMatchExistingUsersByDescription": "Wird zum Verbinden vorhandener Benutzer verwendet. Sobald die Verbindung hergestellt ist, wird den Benutzern eine eindeutige ID vom SSO-Anbieter zugeordnet", "LabelMatchExistingUsersByDescription": "Wird zum Verbinden vorhandener Benutzer verwendet. Sobald die Verbindung hergestellt ist, wird den Benutzern eine eindeutige ID vom SSO-Anbieter zugeordnet",
"LabelMaxEpisodesToDownload": "Max. Anzahl an Episoden zum Herunterladen, 0 für unbegrenzte Episoden.",
"LabelMaxEpisodesToDownloadPerCheck": "Max. Anzahl neuer Episoden zum Herunterladen pro Abfrage",
"LabelMaxEpisodesToKeep": "Max. Anzahl zu behaltender Episoden",
"LabelMaxEpisodesToKeepHelp": "0 setzt keine Begrenzung. Wenn eine neue Episode automatisch heruntergeladen wird, wird die älteste Episode gelöscht, wenn du mehr als X Episoden gespeichert hast. Es wird nur eine Episode pro neuem Download gelöscht.",
"LabelMediaPlayer": "Mediaplayer", "LabelMediaPlayer": "Mediaplayer",
"LabelMediaType": "Medientyp", "LabelMediaType": "Medientyp",
"LabelMetaTag": "Meta Schlagwort", "LabelMetaTag": "Meta Schlagwort",
@ -435,12 +465,14 @@
"LabelOpenIDGroupClaimDescription": "Name des OpenID-Claims, der eine Liste der Benutzergruppen enthält. Wird häufig als <code>groups</code> bezeichnet. <b>Wenn konfiguriert</b>, wird die Anwendung automatisch Rollen basierend auf den Gruppenmitgliedschaften des Benutzers zuweisen, vorausgesetzt, dass diese Gruppen im Claim als 'admin', 'user' oder 'guest' benannt sind (Groß/Kleinschreibung ist irrelevant). Der Claim eine Liste sein, und wenn ein Benutzer mehreren Gruppen angehört, wird die Anwendung die Rolle zuordnen, die dem höchsten Zugriffslevel entspricht. Wenn keine Gruppe übereinstimmt, wird der Zugang verweigert.", "LabelOpenIDGroupClaimDescription": "Name des OpenID-Claims, der eine Liste der Benutzergruppen enthält. Wird häufig als <code>groups</code> bezeichnet. <b>Wenn konfiguriert</b>, wird die Anwendung automatisch Rollen basierend auf den Gruppenmitgliedschaften des Benutzers zuweisen, vorausgesetzt, dass diese Gruppen im Claim als 'admin', 'user' oder 'guest' benannt sind (Groß/Kleinschreibung ist irrelevant). Der Claim eine Liste sein, und wenn ein Benutzer mehreren Gruppen angehört, wird die Anwendung die Rolle zuordnen, die dem höchsten Zugriffslevel entspricht. Wenn keine Gruppe übereinstimmt, wird der Zugang verweigert.",
"LabelOpenRSSFeed": "Öffne RSS-Feed", "LabelOpenRSSFeed": "Öffne RSS-Feed",
"LabelOverwrite": "Überschreiben", "LabelOverwrite": "Überschreiben",
"LabelPaginationPageXOfY": "Seite {0} von {1}",
"LabelPassword": "Passwort", "LabelPassword": "Passwort",
"LabelPath": "Pfad", "LabelPath": "Pfad",
"LabelPermanent": "Dauerhaft", "LabelPermanent": "Dauerhaft",
"LabelPermissionsAccessAllLibraries": "Zugriff auf alle Bibliotheken", "LabelPermissionsAccessAllLibraries": "Zugriff auf alle Bibliotheken",
"LabelPermissionsAccessAllTags": "Zugriff auf alle Schlagwörter", "LabelPermissionsAccessAllTags": "Zugriff auf alle Schlagwörter",
"LabelPermissionsAccessExplicitContent": "Zugriff auf explizite (alterbeschränkte) Inhalte", "LabelPermissionsAccessExplicitContent": "Zugriff auf explizite (alterbeschränkte) Inhalte",
"LabelPermissionsCreateEreader": "Kann E-Reader erstellen",
"LabelPermissionsDelete": "Darf Löschen", "LabelPermissionsDelete": "Darf Löschen",
"LabelPermissionsDownload": "Herunterladen", "LabelPermissionsDownload": "Herunterladen",
"LabelPermissionsUpdate": "Aktualisieren", "LabelPermissionsUpdate": "Aktualisieren",
@ -464,6 +496,8 @@
"LabelPubDate": "Veröffentlichungsdatum", "LabelPubDate": "Veröffentlichungsdatum",
"LabelPublishYear": "Jahr", "LabelPublishYear": "Jahr",
"LabelPublishedDate": "Veröffentlicht {0}", "LabelPublishedDate": "Veröffentlicht {0}",
"LabelPublishedDecade": "Jahrzehnt",
"LabelPublishedDecades": "Jahrzehnte",
"LabelPublisher": "Herausgeber", "LabelPublisher": "Herausgeber",
"LabelPublishers": "Herausgeber", "LabelPublishers": "Herausgeber",
"LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail", "LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail",
@ -483,21 +517,28 @@
"LabelRedo": "Wiederholen", "LabelRedo": "Wiederholen",
"LabelRegion": "Region", "LabelRegion": "Region",
"LabelReleaseDate": "Veröffentlichungsdatum", "LabelReleaseDate": "Veröffentlichungsdatum",
"LabelRemoveAllMetadataAbs": "Alle metadata.abs Dateien löschen",
"LabelRemoveAllMetadataJson": "Alle metadata.json Dateien löschen",
"LabelRemoveCover": "Entferne Titelbild", "LabelRemoveCover": "Entferne Titelbild",
"LabelRemoveMetadataFile": "Metadaten-Dateien in Bibliotheksordnern löschen",
"LabelRemoveMetadataFileHelp": "Alle metadata.json und metadata.abs Dateien aus den Ordnern {0} löschen.",
"LabelRowsPerPage": "Zeilen pro Seite", "LabelRowsPerPage": "Zeilen pro Seite",
"LabelSearchTerm": "Begriff suchen", "LabelSearchTerm": "Begriff suchen",
"LabelSearchTitle": "Titel suchen", "LabelSearchTitle": "Titel suchen",
"LabelSearchTitleOrASIN": "Titel oder ASIN suchen", "LabelSearchTitleOrASIN": "Titel oder ASIN suchen",
"LabelSeason": "Staffel", "LabelSeason": "Staffel",
"LabelSeasonNumber": "Staffel #{0}",
"LabelSelectAll": "Alles auswählen", "LabelSelectAll": "Alles auswählen",
"LabelSelectAllEpisodes": "Alle Episoden auswählen", "LabelSelectAllEpisodes": "Alle Episoden auswählen",
"LabelSelectEpisodesShowing": "{0} ausgewählte Episoden werden angezeigt", "LabelSelectEpisodesShowing": "{0} ausgewählte Episoden werden angezeigt",
"LabelSelectUsers": "Benutzer auswählen", "LabelSelectUsers": "Benutzer auswählen",
"LabelSendEbookToDevice": "E-Buch senden an …", "LabelSendEbookToDevice": "E-Buch senden an …",
"LabelSequence": "Reihenfolge", "LabelSequence": "Reihenfolge",
"LabelSerial": "fortlaufend",
"LabelSeries": "Serien", "LabelSeries": "Serien",
"LabelSeriesName": "Serienname", "LabelSeriesName": "Serienname",
"LabelSeriesProgress": "Serienfortschritt", "LabelSeriesProgress": "Serienfortschritt",
"LabelServerLogLevel": "Server Log Level",
"LabelServerYearReview": "Server Jahr in Übersicht ({0})", "LabelServerYearReview": "Server Jahr in Übersicht ({0})",
"LabelSetEbookAsPrimary": "Als Hauptbuch setzen", "LabelSetEbookAsPrimary": "Als Hauptbuch setzen",
"LabelSetEbookAsSupplementary": "Als Ergänzung setzen", "LabelSetEbookAsSupplementary": "Als Ergänzung setzen",
@ -522,6 +563,9 @@
"LabelSettingsHideSingleBookSeriesHelp": "Serien, die nur ein einzelnes Buch enthalten, werden auf der Startseite und in der Serienansicht ausgeblendet.", "LabelSettingsHideSingleBookSeriesHelp": "Serien, die nur ein einzelnes Buch enthalten, werden auf der Startseite und in der Serienansicht ausgeblendet.",
"LabelSettingsHomePageBookshelfView": "Startseite verwendet die Bücherregalansicht", "LabelSettingsHomePageBookshelfView": "Startseite verwendet die Bücherregalansicht",
"LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht", "LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht",
"LabelSettingsLibraryMarkAsFinishedPercentComplete": "In Prozent gehört größer als",
"LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Verbleibende Zeit ist weniger als (Sekunden)",
"LabelSettingsLibraryMarkAsFinishedWhen": "Markiere Mediendateien als fertig, wenn",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Überspringe vorherige Bücher in fortführender Serie", "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Überspringe vorherige Bücher in fortführender Serie",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Die Startseite von \"Fortführende Serien\" zeigt das erste noch nicht begonnene Buch in Serien an, die mindestens ein Buch abgeschlossen und keine Bücher begonnen haben. Wenn diese Einstellung aktiviert wird, werden Serien ab dem letzten abgeschlossenen Buch fortgesetzt und nicht ab dem ersten nicht begonnenen Buch.", "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "Die Startseite von \"Fortführende Serien\" zeigt das erste noch nicht begonnene Buch in Serien an, die mindestens ein Buch abgeschlossen und keine Bücher begonnen haben. Wenn diese Einstellung aktiviert wird, werden Serien ab dem letzten abgeschlossenen Buch fortgesetzt und nicht ab dem ersten nicht begonnenen Buch.",
"LabelSettingsParseSubtitles": "Analysiere Untertitel", "LabelSettingsParseSubtitles": "Analysiere Untertitel",
@ -566,7 +610,7 @@
"LabelStatsMinutesListening": "Gehörte Minuten", "LabelStatsMinutesListening": "Gehörte Minuten",
"LabelStatsOverallDays": "Gesamte Tage", "LabelStatsOverallDays": "Gesamte Tage",
"LabelStatsOverallHours": "Gesamte Stunden", "LabelStatsOverallHours": "Gesamte Stunden",
"LabelStatsWeekListening": "Gehörte Wochen", "LabelStatsWeekListening": "Wochenhördauer",
"LabelSubtitle": "Untertitel", "LabelSubtitle": "Untertitel",
"LabelSupportedFileTypes": "Unterstützte Dateitypen", "LabelSupportedFileTypes": "Unterstützte Dateitypen",
"LabelTag": "Schlagwort", "LabelTag": "Schlagwort",
@ -586,6 +630,7 @@
"LabelTimeDurationXMinutes": "{0} Minuten", "LabelTimeDurationXMinutes": "{0} Minuten",
"LabelTimeDurationXSeconds": "{0} Sekunden", "LabelTimeDurationXSeconds": "{0} Sekunden",
"LabelTimeInMinutes": "Zeit in Minuten", "LabelTimeInMinutes": "Zeit in Minuten",
"LabelTimeLeft": "{0} verbleibend",
"LabelTimeListened": "Gehörte Zeit", "LabelTimeListened": "Gehörte Zeit",
"LabelTimeListenedToday": "Heute gehörte Zeit", "LabelTimeListenedToday": "Heute gehörte Zeit",
"LabelTimeRemaining": "{0} verbleibend", "LabelTimeRemaining": "{0} verbleibend",
@ -593,6 +638,7 @@
"LabelTitle": "Titel", "LabelTitle": "Titel",
"LabelToolsEmbedMetadata": "Metadaten einbetten", "LabelToolsEmbedMetadata": "Metadaten einbetten",
"LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodatein ein.", "LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodatein ein.",
"LabelToolsM4bEncoder": "M4B Kodierer",
"LabelToolsMakeM4b": "M4B-Datei erstellen", "LabelToolsMakeM4b": "M4B-Datei erstellen",
"LabelToolsMakeM4bDescription": "Erstellt eine M4B-Datei (Endung \".m4b\") welche mehrere mp3-Dateien in einer einzigen Datei inkl. derer Metadaten (Beschreibung, Titelbild, Kapitel, ...) zusammenfasst. M4B-Datei können darüber hinaus Lesezeichen speichern und mit einem Abspielschutz (Passwort) versehen werden.", "LabelToolsMakeM4bDescription": "Erstellt eine M4B-Datei (Endung \".m4b\") welche mehrere mp3-Dateien in einer einzigen Datei inkl. derer Metadaten (Beschreibung, Titelbild, Kapitel, ...) zusammenfasst. M4B-Datei können darüber hinaus Lesezeichen speichern und mit einem Abspielschutz (Passwort) versehen werden.",
"LabelToolsSplitM4b": "M4B in MP3s aufteilen", "LabelToolsSplitM4b": "M4B in MP3s aufteilen",
@ -605,6 +651,7 @@
"LabelTracksMultiTrack": "Mehrfachdatei", "LabelTracksMultiTrack": "Mehrfachdatei",
"LabelTracksNone": "Keine Dateien", "LabelTracksNone": "Keine Dateien",
"LabelTracksSingleTrack": "Einzeldatei", "LabelTracksSingleTrack": "Einzeldatei",
"LabelTrailer": "Vorschau",
"LabelType": "Typ", "LabelType": "Typ",
"LabelUnabridged": "Ungekürzt", "LabelUnabridged": "Ungekürzt",
"LabelUndo": "Rückgängig machen", "LabelUndo": "Rückgängig machen",
@ -618,8 +665,10 @@
"LabelUploaderDragAndDrop": "Ziehen und Ablegen von Dateien oder Ordnern", "LabelUploaderDragAndDrop": "Ziehen und Ablegen von Dateien oder Ordnern",
"LabelUploaderDropFiles": "Dateien löschen", "LabelUploaderDropFiles": "Dateien löschen",
"LabelUploaderItemFetchMetadataHelp": "Automatisches Aktualisieren von Titel, Autor und Serie", "LabelUploaderItemFetchMetadataHelp": "Automatisches Aktualisieren von Titel, Autor und Serie",
"LabelUseAdvancedOptions": "Nutze Erweiterte Optionen",
"LabelUseChapterTrack": "Kapiteldatei verwenden", "LabelUseChapterTrack": "Kapiteldatei verwenden",
"LabelUseFullTrack": "Gesamte Datei verwenden", "LabelUseFullTrack": "Gesamte Datei verwenden",
"LabelUseZeroForUnlimited": "0 für unbegrenzt",
"LabelUser": "Benutzer", "LabelUser": "Benutzer",
"LabelUsername": "Benutzername", "LabelUsername": "Benutzername",
"LabelValue": "Wert", "LabelValue": "Wert",
@ -632,8 +681,8 @@
"LabelWeekdaysToRun": "Wochentage für die Ausführung", "LabelWeekdaysToRun": "Wochentage für die Ausführung",
"LabelXBooks": "{0} Bücher", "LabelXBooks": "{0} Bücher",
"LabelXItems": "{0} Medien", "LabelXItems": "{0} Medien",
"LabelYearReviewHide": "Verstecke Jahr in Übersicht", "LabelYearReviewHide": "Jahresrückblick verbergen",
"LabelYearReviewShow": "Zeige Jahr in Übersicht", "LabelYearReviewShow": "Jahresrückblick anzeigen",
"LabelYourAudiobookDuration": "Laufzeit deines Mediums", "LabelYourAudiobookDuration": "Laufzeit deines Mediums",
"LabelYourBookmarks": "Lesezeichen", "LabelYourBookmarks": "Lesezeichen",
"LabelYourPlaylists": "Eigene Wiedergabelisten", "LabelYourPlaylists": "Eigene Wiedergabelisten",
@ -666,6 +715,7 @@
"MessageConfirmDeleteMetadataProvider": "Möchtest du den benutzerdefinierten Metadatenanbieter \"{0}\" wirklich löschen?", "MessageConfirmDeleteMetadataProvider": "Möchtest du den benutzerdefinierten Metadatenanbieter \"{0}\" wirklich löschen?",
"MessageConfirmDeleteNotification": "Möchtest du diese Benachrichtigung wirklich löschen?", "MessageConfirmDeleteNotification": "Möchtest du diese Benachrichtigung wirklich löschen?",
"MessageConfirmDeleteSession": "Sitzung wird gelöscht! Bist du dir sicher?", "MessageConfirmDeleteSession": "Sitzung wird gelöscht! Bist du dir sicher?",
"MessageConfirmEmbedMetadataInAudioFiles": "Bist du dir sicher, dass die Metadaten in {0} Audiodateien eingebettet werden sollen?",
"MessageConfirmForceReScan": "Scanvorgang erzwingen! Bist du dir sicher?", "MessageConfirmForceReScan": "Scanvorgang erzwingen! Bist du dir sicher?",
"MessageConfirmMarkAllEpisodesFinished": "Alle Episoden werden als abgeschlossen markiert! Bist du dir sicher?", "MessageConfirmMarkAllEpisodesFinished": "Alle Episoden werden als abgeschlossen markiert! Bist du dir sicher?",
"MessageConfirmMarkAllEpisodesNotFinished": "Alle Episoden werden als nicht abgeschlossen markiert! Bist du dir sicher?", "MessageConfirmMarkAllEpisodesNotFinished": "Alle Episoden werden als nicht abgeschlossen markiert! Bist du dir sicher?",
@ -677,6 +727,7 @@
"MessageConfirmPurgeCache": "Cache leeren wird das ganze Verzeichnis <code>/metadata/cache</code> löschen. <br /><br />Bist du dir sicher, dass das Cache Verzeichnis gelöscht werden soll?", "MessageConfirmPurgeCache": "Cache leeren wird das ganze Verzeichnis <code>/metadata/cache</code> löschen. <br /><br />Bist du dir sicher, dass das Cache Verzeichnis gelöscht werden soll?",
"MessageConfirmPurgeItemsCache": "Durch Elementcache leeren wird das gesamte Verzeichnis unter <code>/metadata/cache/items</code> gelöscht.<br />Bist du dir sicher?", "MessageConfirmPurgeItemsCache": "Durch Elementcache leeren wird das gesamte Verzeichnis unter <code>/metadata/cache/items</code> gelöscht.<br />Bist du dir sicher?",
"MessageConfirmQuickEmbed": "Warnung! Audiodateien werden bei der Schnelleinbettung nicht gesichert! Achte darauf, dass du eine Sicherungskopie der Audiodateien besitzt. <br><br>Möchtest du fortfahren?", "MessageConfirmQuickEmbed": "Warnung! Audiodateien werden bei der Schnelleinbettung nicht gesichert! Achte darauf, dass du eine Sicherungskopie der Audiodateien besitzt. <br><br>Möchtest du fortfahren?",
"MessageConfirmQuickMatchEpisodes": "Schnelles Zuordnen von Episoden überschreibt die Details, wenn eine Übereinstimmung gefunden wird. Nur nicht zugeordnete Episoden werden aktualisiert. Bist du sicher?",
"MessageConfirmReScanLibraryItems": "{0} Elemente werden erneut gescannt! Bist du dir sicher?", "MessageConfirmReScanLibraryItems": "{0} Elemente werden erneut gescannt! Bist du dir sicher?",
"MessageConfirmRemoveAllChapters": "Alle Kapitel werden entfernt! Bist du dir sicher?", "MessageConfirmRemoveAllChapters": "Alle Kapitel werden entfernt! Bist du dir sicher?",
"MessageConfirmRemoveAuthor": "Autor \"{0}\" wird enfernt! Bist du dir sicher?", "MessageConfirmRemoveAuthor": "Autor \"{0}\" wird enfernt! Bist du dir sicher?",
@ -684,6 +735,7 @@
"MessageConfirmRemoveEpisode": "Episode \"{0}\" wird entfernt! Bist du dir sicher?", "MessageConfirmRemoveEpisode": "Episode \"{0}\" wird entfernt! Bist du dir sicher?",
"MessageConfirmRemoveEpisodes": "{0} Episoden werden entfernt! Bist du dir sicher?", "MessageConfirmRemoveEpisodes": "{0} Episoden werden entfernt! Bist du dir sicher?",
"MessageConfirmRemoveListeningSessions": "Bist du dir sicher, dass du {0} Hörsitzungen enfernen möchtest?", "MessageConfirmRemoveListeningSessions": "Bist du dir sicher, dass du {0} Hörsitzungen enfernen möchtest?",
"MessageConfirmRemoveMetadataFiles": "Bist du sicher, dass du alle metadata.{0} Dateien in deinen Bibliotheksordnern löschen willst?",
"MessageConfirmRemoveNarrator": "Erzähler \"{0}\" wird entfernt! Bist du dir sicher?", "MessageConfirmRemoveNarrator": "Erzähler \"{0}\" wird entfernt! Bist du dir sicher?",
"MessageConfirmRemovePlaylist": "Wiedergabeliste \"{0}\" wird entfernt! Bist du dir sicher?", "MessageConfirmRemovePlaylist": "Wiedergabeliste \"{0}\" wird entfernt! Bist du dir sicher?",
"MessageConfirmRenameGenre": "Kategorie \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Bist du dir sicher?", "MessageConfirmRenameGenre": "Kategorie \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Bist du dir sicher?",
@ -699,6 +751,7 @@
"MessageDragFilesIntoTrackOrder": "Verschiebe die Dateien in die richtige Reihenfolge", "MessageDragFilesIntoTrackOrder": "Verschiebe die Dateien in die richtige Reihenfolge",
"MessageEmbedFailed": "Einbetten fehlgeschlagen!", "MessageEmbedFailed": "Einbetten fehlgeschlagen!",
"MessageEmbedFinished": "Einbettung abgeschlossen!", "MessageEmbedFinished": "Einbettung abgeschlossen!",
"MessageEmbedQueue": "Eingereiht zum einbinden von Metadaten ({0} in Warteschlange)",
"MessageEpisodesQueuedForDownload": "{0} Episode(n) in der Warteschlange zum Herunterladen", "MessageEpisodesQueuedForDownload": "{0} Episode(n) in der Warteschlange zum Herunterladen",
"MessageEreaderDevices": "Um die Zustellung von E-Büchern sicherzustellen, musst du eventuell die oben genannte E-Mail-Adresse als gültigen Absender für jedes unten aufgeführte Gerät hinzufügen.", "MessageEreaderDevices": "Um die Zustellung von E-Büchern sicherzustellen, musst du eventuell die oben genannte E-Mail-Adresse als gültigen Absender für jedes unten aufgeführte Gerät hinzufügen.",
"MessageFeedURLWillBe": "Feed-URL wird {0} sein", "MessageFeedURLWillBe": "Feed-URL wird {0} sein",
@ -743,6 +796,7 @@
"MessageNoLogs": "Keine Protokolle", "MessageNoLogs": "Keine Protokolle",
"MessageNoMediaProgress": "Kein Medienfortschritt", "MessageNoMediaProgress": "Kein Medienfortschritt",
"MessageNoNotifications": "Keine Benachrichtigungen", "MessageNoNotifications": "Keine Benachrichtigungen",
"MessageNoPodcastFeed": "Ungültiger Podcast: Kein Feed",
"MessageNoPodcastsFound": "Keine Podcasts gefunden", "MessageNoPodcastsFound": "Keine Podcasts gefunden",
"MessageNoResults": "Keine Ergebnisse", "MessageNoResults": "Keine Ergebnisse",
"MessageNoSearchResultsFor": "Keine Suchergebnisse für \"{0}\"", "MessageNoSearchResultsFor": "Keine Suchergebnisse für \"{0}\"",
@ -759,6 +813,10 @@
"MessagePlaylistCreateFromCollection": "Erstelle eine Wiedergabeliste aus der Sammlung", "MessagePlaylistCreateFromCollection": "Erstelle eine Wiedergabeliste aus der Sammlung",
"MessagePleaseWait": "Bitte warten...", "MessagePleaseWait": "Bitte warten...",
"MessagePodcastHasNoRSSFeedForMatching": "Der Podcast hat keine RSS-Feed-Url welche für den Online-Abgleich verwendet werden kann", "MessagePodcastHasNoRSSFeedForMatching": "Der Podcast hat keine RSS-Feed-Url welche für den Online-Abgleich verwendet werden kann",
"MessagePodcastSearchField": "Suchbegriff oder RSS-Feed URL eingeben",
"MessageQuickEmbedInProgress": "Schnellabgleich läuft",
"MessageQuickEmbedQueue": "In Warteschlange für Schnelles einbinden ({0} eingereiht)",
"MessageQuickMatchAllEpisodes": "Quick Match aller Episoden",
"MessageQuickMatchDescription": "Füllt leere Details und Titelbilder mit dem ersten Treffer aus '{0}'. Überschreibt keine Details, es sei denn, die Server-Einstellung \"Passende Metadaten bevorzugen\" ist aktiviert.", "MessageQuickMatchDescription": "Füllt leere Details und Titelbilder mit dem ersten Treffer aus '{0}'. Überschreibt keine Details, es sei denn, die Server-Einstellung \"Passende Metadaten bevorzugen\" ist aktiviert.",
"MessageRemoveChapter": "Kapitel entfernen", "MessageRemoveChapter": "Kapitel entfernen",
"MessageRemoveEpisodes": "Entferne {0} Episode(n)", "MessageRemoveEpisodes": "Entferne {0} Episode(n)",
@ -776,6 +834,41 @@
"MessageShareExpiresIn": "Läuft in {0} ab", "MessageShareExpiresIn": "Läuft in {0} ab",
"MessageShareURLWillBe": "Der Freigabe Link wird <strong>{0}</strong> sein.", "MessageShareURLWillBe": "Der Freigabe Link wird <strong>{0}</strong> sein.",
"MessageStartPlaybackAtTime": "Start der Wiedergabe für \"{0}\" bei {1}?", "MessageStartPlaybackAtTime": "Start der Wiedergabe für \"{0}\" bei {1}?",
"MessageTaskAudioFileNotWritable": "Die Audiodatei \"{0}\" ist schreibgeschützt",
"MessageTaskCanceledByUser": "Aufgabe vom Benutzer abgebrochen",
"MessageTaskDownloadingEpisodeDescription": "Folge \"{0}\" wird heruntergeladen",
"MessageTaskEmbeddingMetadata": "Metadaten werden eingebettet",
"MessageTaskEmbeddingMetadataDescription": "Metadaten werden in Hörbuch \"{0}\" eingebettet",
"MessageTaskEncodingM4b": "M4B wird encodiert",
"MessageTaskEncodingM4bDescription": "Hörbuch \"{0}\" wird in eine einzelne m4b Datei encodiert",
"MessageTaskFailed": "Fehlgeschlagen",
"MessageTaskFailedToBackupAudioFile": "Sicherung der Audiodatei \"{0}\" fehlgeschlagen",
"MessageTaskFailedToCreateCacheDirectory": "Fehler beim erstellen des Cache-Verzeichnisses",
"MessageTaskFailedToEmbedMetadataInFile": "Einbetten der Metadaten in die Datei \"{0}\" fehlgeschlagen",
"MessageTaskFailedToMergeAudioFiles": "Fehler beim zusammenführen der Audiodateien",
"MessageTaskFailedToMoveM4bFile": "Fehler beim verschieben der m4b Datei",
"MessageTaskFailedToWriteMetadataFile": "Fehler beim schreiben der Metadaten-Datei",
"MessageTaskMatchingBooksInLibrary": "Vergleiche Bücher in Bibliothek \"{0}\"",
"MessageTaskNoFilesToScan": "Keine Dateien zum scannen",
"MessageTaskOpmlImport": "OPML-Import",
"MessageTaskOpmlImportDescription": "Podcasts von {0} RSS-Feeds werden ersrtellt",
"MessageTaskOpmlImportFeed": "OPML-Feed importieren",
"MessageTaskOpmlImportFeedDescription": "RSS-Feed \"{0}\" wird importiert",
"MessageTaskOpmlImportFeedFailed": "Podcast Feed konnte nicht geladen werden",
"MessageTaskOpmlImportFeedPodcastDescription": "Podcast \"{0}\" wird erstellt",
"MessageTaskOpmlImportFeedPodcastExists": "Der Podcast ist bereits im Pfad vorhanden",
"MessageTaskOpmlImportFeedPodcastFailed": "Erstellen des Podcasts fehlgeschlagen",
"MessageTaskOpmlImportFinished": "{0} Podcasts hinzugefügt",
"MessageTaskOpmlParseFailed": "Fehler beim lesen der OPML Datei",
"MessageTaskOpmlParseFastFail": "Ungültie OPML Datei: <opml> ODER <outline> tag wurde nicht gefunden",
"MessageTaskOpmlParseNoneFound": "Keine feeds in der OPML Datei gefunden",
"MessageTaskScanItemsAdded": "{0} hinzugefügt",
"MessageTaskScanItemsMissing": "{0} fehlend",
"MessageTaskScanItemsUpdated": "{0} aktualisiert",
"MessageTaskScanNoChangesNeeded": "Keine Änderungen nötig",
"MessageTaskScanningFileChanges": "Überprüfe \"{0}\" nach geänderten Dateien",
"MessageTaskScanningLibrary": "Bibliothek \"{0}\" wird durchsucht",
"MessageTaskTargetDirectoryNotWritable": "Das Zielverzeichnis ist schreibgeschützt",
"MessageThinking": "Nachdenken...", "MessageThinking": "Nachdenken...",
"MessageUploaderItemFailed": "Hochladen fehlgeschlagen", "MessageUploaderItemFailed": "Hochladen fehlgeschlagen",
"MessageUploaderItemSuccess": "Erfolgreich hochgeladen!", "MessageUploaderItemSuccess": "Erfolgreich hochgeladen!",
@ -793,6 +886,10 @@
"NoteUploaderFoldersWithMediaFiles": "Ordner mit Mediendateien werden als separate Bibliothekselemente behandelt.", "NoteUploaderFoldersWithMediaFiles": "Ordner mit Mediendateien werden als separate Bibliothekselemente behandelt.",
"NoteUploaderOnlyAudioFiles": "Wenn du nur Audiodateien hochlädst, wird jede Audiodatei als ein separates Medium behandelt.", "NoteUploaderOnlyAudioFiles": "Wenn du nur Audiodateien hochlädst, wird jede Audiodatei als ein separates Medium behandelt.",
"NoteUploaderUnsupportedFiles": "Nicht unterstützte Dateien werden ignoriert. Bei der Auswahl oder dem Löschen eines Ordners werden andere Dateien, die sich nicht in einem Elementordner befinden, ignoriert.", "NoteUploaderUnsupportedFiles": "Nicht unterstützte Dateien werden ignoriert. Bei der Auswahl oder dem Löschen eines Ordners werden andere Dateien, die sich nicht in einem Elementordner befinden, ignoriert.",
"NotificationOnBackupCompletedDescription": "Wird ausgeführt wenn ein Backup erstellt wurde",
"NotificationOnBackupFailedDescription": "Wird ausgeführt wenn ein Backup fehlgeschlagen ist",
"NotificationOnEpisodeDownloadedDescription": "Wird ausgeführt wenn eine Podcast Folge automatisch heruntergeladen wird",
"NotificationOnTestDescription": "Wird ausgeführt wenn das Benachrichtigungssystem getestet wird",
"PlaceholderNewCollection": "Neuer Sammlungsname", "PlaceholderNewCollection": "Neuer Sammlungsname",
"PlaceholderNewFolderPath": "Neuer Ordnerpfad", "PlaceholderNewFolderPath": "Neuer Ordnerpfad",
"PlaceholderNewPlaylist": "Neuer Wiedergabelistenname", "PlaceholderNewPlaylist": "Neuer Wiedergabelistenname",
@ -816,14 +913,13 @@
"StatsTopNarrators": "TOP SPRECHER", "StatsTopNarrators": "TOP SPRECHER",
"StatsTotalDuration": "Mit einer totalen Dauer von…", "StatsTotalDuration": "Mit einer totalen Dauer von…",
"StatsYearInReview": "DAS JAHR IM RÜCKBLICK", "StatsYearInReview": "DAS JAHR IM RÜCKBLICK",
"ToastAccountUpdateFailed": "Aktualisierung des Kontos fehlgeschlagen",
"ToastAccountUpdateSuccess": "Konto aktualisiert", "ToastAccountUpdateSuccess": "Konto aktualisiert",
"ToastAppriseUrlRequired": "Eine Apprise-URL ist notwendig", "ToastAppriseUrlRequired": "Eine Apprise-URL ist notwendig",
"ToastAsinRequired": "ASIN ist erforderlich",
"ToastAuthorImageRemoveSuccess": "Autorenbild entfernt", "ToastAuthorImageRemoveSuccess": "Autorenbild entfernt",
"ToastAuthorNotFound": "Autor \"{0}\" nicht gefunden", "ToastAuthorNotFound": "Autor \"{0}\" nicht gefunden",
"ToastAuthorRemoveSuccess": "Autor entfernt", "ToastAuthorRemoveSuccess": "Autor entfernt",
"ToastAuthorSearchNotFound": "Autor nicht gefunden", "ToastAuthorSearchNotFound": "Autor nicht gefunden",
"ToastAuthorUpdateFailed": "Aktualisierung des Autors fehlgeschlagen",
"ToastAuthorUpdateMerged": "Autor zusammengeführt", "ToastAuthorUpdateMerged": "Autor zusammengeführt",
"ToastAuthorUpdateSuccess": "Autor aktualisiert", "ToastAuthorUpdateSuccess": "Autor aktualisiert",
"ToastAuthorUpdateSuccessNoImageFound": "Autor aktualisiert (kein Bild gefunden)", "ToastAuthorUpdateSuccessNoImageFound": "Autor aktualisiert (kein Bild gefunden)",
@ -834,29 +930,29 @@
"ToastBackupDeleteSuccess": "Sicherung gelöscht", "ToastBackupDeleteSuccess": "Sicherung gelöscht",
"ToastBackupInvalidMaxKeep": "Ungültige Anzahl aufzubewahrender Backups", "ToastBackupInvalidMaxKeep": "Ungültige Anzahl aufzubewahrender Backups",
"ToastBackupInvalidMaxSize": "Ungültige maximale Backupgröße", "ToastBackupInvalidMaxSize": "Ungültige maximale Backupgröße",
"ToastBackupPathUpdateFailed": "Der Backuppfad konnte nicht aktualisiert werden",
"ToastBackupRestoreFailed": "Sicherung konnte nicht wiederhergestellt werden", "ToastBackupRestoreFailed": "Sicherung konnte nicht wiederhergestellt werden",
"ToastBackupUploadFailed": "Sicherung konnte nicht hochgeladen werden", "ToastBackupUploadFailed": "Sicherung konnte nicht hochgeladen werden",
"ToastBackupUploadSuccess": "Sicherung hochgeladen", "ToastBackupUploadSuccess": "Sicherung hochgeladen",
"ToastBatchDeleteFailed": "Batch-Löschen fehlgeschlagen", "ToastBatchDeleteFailed": "Batch-Löschen fehlgeschlagen",
"ToastBatchDeleteSuccess": "Batch-Löschung erfolgreich", "ToastBatchDeleteSuccess": "Batch-Löschung erfolgreich",
"ToastBatchQuickMatchFailed": "Batch-Schnellabgleich fehlgeschlagen!",
"ToastBatchQuickMatchStarted": "Batch-Schnellabgleich für {0} Bücher gestartet!",
"ToastBatchUpdateFailed": "Stapelaktualisierung fehlgeschlagen", "ToastBatchUpdateFailed": "Stapelaktualisierung fehlgeschlagen",
"ToastBatchUpdateSuccess": "Stapelaktualisierung erfolgreich", "ToastBatchUpdateSuccess": "Stapelaktualisierung erfolgreich",
"ToastBookmarkCreateFailed": "Lesezeichen konnte nicht erstellt werden", "ToastBookmarkCreateFailed": "Lesezeichen konnte nicht erstellt werden",
"ToastBookmarkCreateSuccess": "Lesezeichen hinzugefügt", "ToastBookmarkCreateSuccess": "Lesezeichen hinzugefügt",
"ToastBookmarkRemoveSuccess": "Lesezeichen entfernt", "ToastBookmarkRemoveSuccess": "Lesezeichen entfernt",
"ToastBookmarkUpdateFailed": "Lesezeichenaktualisierung fehlgeschlagen",
"ToastBookmarkUpdateSuccess": "Lesezeichen aktualisiert", "ToastBookmarkUpdateSuccess": "Lesezeichen aktualisiert",
"ToastCachePurgeFailed": "Cache leeren fehlgeschlagen", "ToastCachePurgeFailed": "Cache leeren fehlgeschlagen",
"ToastCachePurgeSuccess": "Cache geleert", "ToastCachePurgeSuccess": "Cache geleert",
"ToastChaptersHaveErrors": "Kapitel sind fehlerhaft", "ToastChaptersHaveErrors": "Kapitel sind fehlerhaft",
"ToastChaptersMustHaveTitles": "Kapitel benötigen eindeutige Namen", "ToastChaptersMustHaveTitles": "Kapitel benötigen eindeutige Namen",
"ToastChaptersRemoved": "Kapitel entfernt", "ToastChaptersRemoved": "Kapitel entfernt",
"ToastChaptersUpdated": "Kapitel aktualisiert",
"ToastCollectionItemsAddFailed": "Das Hinzufügen von Element(en) zur Sammlung ist fehlgeschlagen", "ToastCollectionItemsAddFailed": "Das Hinzufügen von Element(en) zur Sammlung ist fehlgeschlagen",
"ToastCollectionItemsAddSuccess": "Element(e) erfolgreich zur Sammlung hinzugefügt", "ToastCollectionItemsAddSuccess": "Element(e) erfolgreich zur Sammlung hinzugefügt",
"ToastCollectionItemsRemoveSuccess": "Medien aus der Sammlung entfernt", "ToastCollectionItemsRemoveSuccess": "Medien aus der Sammlung entfernt",
"ToastCollectionRemoveSuccess": "Sammlung entfernt", "ToastCollectionRemoveSuccess": "Sammlung entfernt",
"ToastCollectionUpdateFailed": "Sammlung konnte nicht aktualisiert werden",
"ToastCollectionUpdateSuccess": "Sammlung aktualisiert", "ToastCollectionUpdateSuccess": "Sammlung aktualisiert",
"ToastCoverUpdateFailed": "Cover-Update fehlgeschlagen", "ToastCoverUpdateFailed": "Cover-Update fehlgeschlagen",
"ToastDeleteFileFailed": "Die Datei konnte nicht gelöscht werden", "ToastDeleteFileFailed": "Die Datei konnte nicht gelöscht werden",
@ -865,31 +961,28 @@
"ToastDeviceNameAlreadyExists": "E-Reader-Gerät mit diesem Namen existiert bereits", "ToastDeviceNameAlreadyExists": "E-Reader-Gerät mit diesem Namen existiert bereits",
"ToastDeviceTestEmailFailed": "Senden der Test-E-Mail fehlgeschlagen", "ToastDeviceTestEmailFailed": "Senden der Test-E-Mail fehlgeschlagen",
"ToastDeviceTestEmailSuccess": "Test-E-Mail gesendet", "ToastDeviceTestEmailSuccess": "Test-E-Mail gesendet",
"ToastDeviceUpdateFailed": "Das Gerät konnte nicht aktualisiert werden",
"ToastEmailSettingsUpdateFailed": "E-Mail-Einstellungen konnten nicht aktualisiert werden",
"ToastEmailSettingsUpdateSuccess": "E-Mail-Einstellungen aktualisiert", "ToastEmailSettingsUpdateSuccess": "E-Mail-Einstellungen aktualisiert",
"ToastEncodeCancelFailed": "Das Encoding konnte nicht abgebrochen werden", "ToastEncodeCancelFailed": "Das Encoding konnte nicht abgebrochen werden",
"ToastEncodeCancelSucces": "Encoding abgebrochen", "ToastEncodeCancelSucces": "Encoding abgebrochen",
"ToastEpisodeDownloadQueueClearFailed": "Warteschlange konnte nicht gelöscht werden", "ToastEpisodeDownloadQueueClearFailed": "Warteschlange konnte nicht gelöscht werden",
"ToastEpisodeDownloadQueueClearSuccess": "Warteschlange für Episoden-Downloads gelöscht", "ToastEpisodeDownloadQueueClearSuccess": "Warteschlange für Episoden-Downloads gelöscht",
"ToastEpisodeUpdateSuccess": "{0} Episoden aktualisiert",
"ToastErrorCannotShare": "Das kann nicht nativ auf diesem Gerät freigegeben werden", "ToastErrorCannotShare": "Das kann nicht nativ auf diesem Gerät freigegeben werden",
"ToastFailedToLoadData": "Daten laden fehlgeschlagen", "ToastFailedToLoadData": "Daten laden fehlgeschlagen",
"ToastFailedToMatch": "Fehler beim Abgleich",
"ToastFailedToShare": "Fehler beim Teilen", "ToastFailedToShare": "Fehler beim Teilen",
"ToastFailedToUpdateAccount": "Fehler beim ändern des Accounts", "ToastFailedToUpdate": "Aktualisierung ist fehlgeschlagen",
"ToastFailedToUpdateUser": "Fehler beim ändern des Benutzers",
"ToastInvalidImageUrl": "Ungültiger Bild URL", "ToastInvalidImageUrl": "Ungültiger Bild URL",
"ToastInvalidMaxEpisodesToDownload": "Ungültige Max. Anzahl an Episoden zum Herunterladen",
"ToastInvalidUrl": "Ungültiger URL", "ToastInvalidUrl": "Ungültiger URL",
"ToastItemCoverUpdateFailed": "Fehler bei der Aktualisierung des Titelbildes",
"ToastItemCoverUpdateSuccess": "Titelbild aktualisiert", "ToastItemCoverUpdateSuccess": "Titelbild aktualisiert",
"ToastItemDeletedFailed": "Fehler beim löschen des Artikels", "ToastItemDeletedFailed": "Fehler beim löschen des Artikels",
"ToastItemDeletedSuccess": "Artikel gelöscht", "ToastItemDeletedSuccess": "Artikel gelöscht",
"ToastItemDetailsUpdateFailed": "Fehler bei der Aktualisierung der Artikeldetails",
"ToastItemDetailsUpdateSuccess": "Artikeldetails aktualisiert", "ToastItemDetailsUpdateSuccess": "Artikeldetails aktualisiert",
"ToastItemMarkedAsFinishedFailed": "Fehler bei der Markierung des Artikels als \"Beendet\"", "ToastItemMarkedAsFinishedFailed": "Fehler bei der Markierung des Artikels als \"Beendet\"",
"ToastItemMarkedAsFinishedSuccess": "Artikel als \"Beendet\" markiert", "ToastItemMarkedAsFinishedSuccess": "Artikel als \"Beendet\" markiert",
"ToastItemMarkedAsNotFinishedFailed": "Fehler bei der Markierung des Artikels als \"Nicht Beendet\"", "ToastItemMarkedAsNotFinishedFailed": "Fehler bei der Markierung des Artikels als \"Nicht Beendet\"",
"ToastItemMarkedAsNotFinishedSuccess": "Artikel als \"Nicht Beendet\" markiert", "ToastItemMarkedAsNotFinishedSuccess": "Artikel als \"Nicht Beendet\" markiert",
"ToastItemUpdateFailed": "Fehler beim ändern des Artikels",
"ToastItemUpdateSuccess": "Artikel wurde verändert", "ToastItemUpdateSuccess": "Artikel wurde verändert",
"ToastLibraryCreateFailed": "Bibliothek konnte nicht erstellt werden", "ToastLibraryCreateFailed": "Bibliothek konnte nicht erstellt werden",
"ToastLibraryCreateSuccess": "Bibliothek \"{0}\" erstellt", "ToastLibraryCreateSuccess": "Bibliothek \"{0}\" erstellt",
@ -897,37 +990,42 @@
"ToastLibraryDeleteSuccess": "Bibliothek gelöscht", "ToastLibraryDeleteSuccess": "Bibliothek gelöscht",
"ToastLibraryScanFailedToStart": "Scan konnte nicht gestartet werden", "ToastLibraryScanFailedToStart": "Scan konnte nicht gestartet werden",
"ToastLibraryScanStarted": "Bibliotheksscan gestartet", "ToastLibraryScanStarted": "Bibliotheksscan gestartet",
"ToastLibraryUpdateFailed": "Aktualisierung der Bibliothek fehlgeschlagen",
"ToastLibraryUpdateSuccess": "Bibliothek \"{0}\" aktualisiert", "ToastLibraryUpdateSuccess": "Bibliothek \"{0}\" aktualisiert",
"ToastMatchAllAuthorsFailed": "Nicht alle Autoren konnten zugeordnet werden",
"ToastMetadataFilesRemovedError": "Fehler beim löschen von metadata.{0} Dateien",
"ToastMetadataFilesRemovedNoneFound": "Keine metadata.{0} Dateien in Bibliothek gefunden",
"ToastMetadataFilesRemovedNoneRemoved": "Keine metadata.{0} Dateien gelöscht",
"ToastMetadataFilesRemovedSuccess": "{0} metadata.{1} Datei(en) gelöscht",
"ToastMustHaveAtLeastOnePath": "Es muss mindestens ein Pfad angegeben werden",
"ToastNameEmailRequired": "Name und E-Mail sind erforderlich", "ToastNameEmailRequired": "Name und E-Mail sind erforderlich",
"ToastNameRequired": "Name ist erforderlich", "ToastNameRequired": "Name ist erforderlich",
"ToastNewEpisodesFound": "{0} neue Episoden gefunden",
"ToastNewUserCreatedFailed": "Fehler beim erstellen des Accounts: \"{ 0}\"", "ToastNewUserCreatedFailed": "Fehler beim erstellen des Accounts: \"{ 0}\"",
"ToastNewUserCreatedSuccess": "Neuer Account erstellt", "ToastNewUserCreatedSuccess": "Neuer Account erstellt",
"ToastNewUserLibraryError": "Mindestens eine Bibliothek muss ausgewählt werden", "ToastNewUserLibraryError": "Mindestens eine Bibliothek muss ausgewählt werden",
"ToastNewUserPasswordError": "Passwort erforderlich, nur der root Benutzer darf ein leeres Passwort haben", "ToastNewUserPasswordError": "Passwort erforderlich, nur der root Benutzer darf ein leeres Passwort haben",
"ToastNewUserTagError": "Mindestens ein Tag muss ausgewählt sein", "ToastNewUserTagError": "Mindestens ein Tag muss ausgewählt sein",
"ToastNewUserUsernameError": "Nutzername eingeben", "ToastNewUserUsernameError": "Nutzername eingeben",
"ToastNoNewEpisodesFound": "Keine neuen Episoden gefunden",
"ToastNoUpdatesNecessary": "Keine Änderungen nötig", "ToastNoUpdatesNecessary": "Keine Änderungen nötig",
"ToastNotificationCreateFailed": "Fehler beim erstellen der Benachrichtig", "ToastNotificationCreateFailed": "Fehler beim erstellen der Benachrichtig",
"ToastNotificationDeleteFailed": "Fehler beim löschen der Benachrichtigung", "ToastNotificationDeleteFailed": "Fehler beim löschen der Benachrichtigung",
"ToastNotificationFailedMaximum": "Maximale Fehlversuche muss >= 0 sein", "ToastNotificationFailedMaximum": "Maximale Fehlversuche muss >= 0 sein",
"ToastNotificationQueueMaximum": "Maximale Benachrichtigungswarteschlange muss >= 0 sein", "ToastNotificationQueueMaximum": "Maximale Benachrichtigungswarteschlange muss >= 0 sein",
"ToastNotificationSettingsUpdateFailed": "Fehler beim ändern der Benachrichtigungseinstellungen",
"ToastNotificationSettingsUpdateSuccess": "Benachrichtigungseinstellungen geändert", "ToastNotificationSettingsUpdateSuccess": "Benachrichtigungseinstellungen geändert",
"ToastNotificationTestTriggerFailed": "Fehler beim Auslösen der Testbenachrichtigung", "ToastNotificationTestTriggerFailed": "Fehler beim Auslösen der Testbenachrichtigung",
"ToastNotificationTestTriggerSuccess": "Testbenachrichtigung ausgelöst", "ToastNotificationTestTriggerSuccess": "Testbenachrichtigung ausgelöst",
"ToastNotificationUpdateFailed": "Fehler bein ändern der Benachrichtigung",
"ToastNotificationUpdateSuccess": "Benachrichtigung geändert", "ToastNotificationUpdateSuccess": "Benachrichtigung geändert",
"ToastPlaylistCreateFailed": "Erstellen der Wiedergabeliste fehlgeschlagen", "ToastPlaylistCreateFailed": "Erstellen der Wiedergabeliste fehlgeschlagen",
"ToastPlaylistCreateSuccess": "Wiedergabeliste erstellt", "ToastPlaylistCreateSuccess": "Wiedergabeliste erstellt",
"ToastPlaylistRemoveSuccess": "Wiedergabeliste gelöscht", "ToastPlaylistRemoveSuccess": "Wiedergabeliste gelöscht",
"ToastPlaylistUpdateFailed": "Aktualisieren der Wiedergabeliste fehlgeschlagen",
"ToastPlaylistUpdateSuccess": "Wiedergabeliste aktualisiert", "ToastPlaylistUpdateSuccess": "Wiedergabeliste aktualisiert",
"ToastPodcastCreateFailed": "Podcast konnte nicht erstellt werden", "ToastPodcastCreateFailed": "Podcast konnte nicht erstellt werden",
"ToastPodcastCreateSuccess": "Podcast erstellt", "ToastPodcastCreateSuccess": "Podcast erstellt",
"ToastPodcastGetFeedFailed": "Fehler beim abrufen des Podcast Feeds", "ToastPodcastGetFeedFailed": "Fehler beim abrufen des Podcast Feeds",
"ToastPodcastNoEpisodesInFeed": "Keine Episoden in RSS Feed gefunden", "ToastPodcastNoEpisodesInFeed": "Keine Episoden in RSS Feed gefunden",
"ToastPodcastNoRssFeed": "Podcast enthält keinen RSS Feed", "ToastPodcastNoRssFeed": "Podcast enthält keinen RSS Feed",
"ToastProgressIsNotBeingSynced": "Fortschritt wird nicht synchronisiert, Wiedergabe wird neu gestartet",
"ToastProviderCreatedFailed": "Fehler beim hinzufügen des Anbieters", "ToastProviderCreatedFailed": "Fehler beim hinzufügen des Anbieters",
"ToastProviderCreatedSuccess": "Neuer Anbieter hinzugefügt", "ToastProviderCreatedSuccess": "Neuer Anbieter hinzugefügt",
"ToastProviderNameAndUrlRequired": "Name und URL notwendig", "ToastProviderNameAndUrlRequired": "Name und URL notwendig",
@ -950,18 +1048,17 @@
"ToastSendEbookToDeviceSuccess": "E-Buch an Gerät „{0}“ gesendet", "ToastSendEbookToDeviceSuccess": "E-Buch an Gerät „{0}“ gesendet",
"ToastSeriesUpdateFailed": "Aktualisierung der Serien fehlgeschlagen", "ToastSeriesUpdateFailed": "Aktualisierung der Serien fehlgeschlagen",
"ToastSeriesUpdateSuccess": "Serien aktualisiert", "ToastSeriesUpdateSuccess": "Serien aktualisiert",
"ToastServerSettingsUpdateFailed": "Die Server-Einstellungen wurden nicht gespeichert",
"ToastServerSettingsUpdateSuccess": "Die Server-Einstellungen wurden geupdated", "ToastServerSettingsUpdateSuccess": "Die Server-Einstellungen wurden geupdated",
"ToastSessionCloseFailed": "Fehler beim schließen der Sitzung", "ToastSessionCloseFailed": "Fehler beim schließen der Sitzung",
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden", "ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden",
"ToastSessionDeleteSuccess": "Sitzung gelöscht", "ToastSessionDeleteSuccess": "Sitzung gelöscht",
"ToastSleepTimerDone": "Einschlaf-Timer aktiviert... zZzzZz",
"ToastSlugMustChange": "URL-Schlüssel enthält ungültige Zeichen", "ToastSlugMustChange": "URL-Schlüssel enthält ungültige Zeichen",
"ToastSlugRequired": "URL-Schlüssel erforderlich", "ToastSlugRequired": "URL-Schlüssel erforderlich",
"ToastSocketConnected": "Verbindung zum WebSocket hergestellt", "ToastSocketConnected": "Verbindung zum WebSocket hergestellt",
"ToastSocketDisconnected": "Verbindung zum WebSocket verloren", "ToastSocketDisconnected": "Verbindung zum WebSocket verloren",
"ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen", "ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen",
"ToastSortingPrefixesEmptyError": "Es muss mindestens ein Sortier-Prefix vorhanden sein", "ToastSortingPrefixesEmptyError": "Es muss mindestens ein Sortier-Prefix vorhanden sein",
"ToastSortingPrefixesUpdateFailed": "Update der Sortier-Prefixe ist fehlgeschlagen",
"ToastSortingPrefixesUpdateSuccess": "Die Sortier-Prefixe wirden geupdated ({0} Einträge)", "ToastSortingPrefixesUpdateSuccess": "Die Sortier-Prefixe wirden geupdated ({0} Einträge)",
"ToastTitleRequired": "Titel erforderlich", "ToastTitleRequired": "Titel erforderlich",
"ToastUnknownError": "Unbekannter Fehler", "ToastUnknownError": "Unbekannter Fehler",

View file

@ -66,6 +66,7 @@
"ButtonPurgeItemsCache": "Purge Items Cache", "ButtonPurgeItemsCache": "Purge Items Cache",
"ButtonQueueAddItem": "Add to queue", "ButtonQueueAddItem": "Add to queue",
"ButtonQueueRemoveItem": "Remove from queue", "ButtonQueueRemoveItem": "Remove from queue",
"ButtonQuickEmbed": "Quick Embed",
"ButtonQuickEmbedMetadata": "Quick Embed Metadata", "ButtonQuickEmbedMetadata": "Quick Embed Metadata",
"ButtonQuickMatch": "Quick Match", "ButtonQuickMatch": "Quick Match",
"ButtonReScan": "Re-Scan", "ButtonReScan": "Re-Scan",
@ -163,6 +164,7 @@
"HeaderNotificationUpdate": "Update Notification", "HeaderNotificationUpdate": "Update Notification",
"HeaderNotifications": "Notifications", "HeaderNotifications": "Notifications",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication", "HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
"HeaderOpenListeningSessions": "Open Listening Sessions",
"HeaderOpenRSSFeed": "Open RSS Feed", "HeaderOpenRSSFeed": "Open RSS Feed",
"HeaderOtherFiles": "Other Files", "HeaderOtherFiles": "Other Files",
"HeaderPasswordAuthentication": "Password Authentication", "HeaderPasswordAuthentication": "Password Authentication",
@ -180,6 +182,7 @@
"HeaderRemoveEpisodes": "Remove {0} Episodes", "HeaderRemoveEpisodes": "Remove {0} Episodes",
"HeaderSavedMediaProgress": "Saved Media Progress", "HeaderSavedMediaProgress": "Saved Media Progress",
"HeaderSchedule": "Schedule", "HeaderSchedule": "Schedule",
"HeaderScheduleEpisodeDownloads": "Schedule Automatic Episode Downloads",
"HeaderScheduleLibraryScans": "Schedule Automatic Library Scans", "HeaderScheduleLibraryScans": "Schedule Automatic Library Scans",
"HeaderSession": "Session", "HeaderSession": "Session",
"HeaderSetBackupSchedule": "Set Backup Schedule", "HeaderSetBackupSchedule": "Set Backup Schedule",
@ -225,7 +228,11 @@
"LabelAllUsersExcludingGuests": "All users excluding guests", "LabelAllUsersExcludingGuests": "All users excluding guests",
"LabelAllUsersIncludingGuests": "All users including guests", "LabelAllUsersIncludingGuests": "All users including guests",
"LabelAlreadyInYourLibrary": "Already in your library", "LabelAlreadyInYourLibrary": "Already in your library",
"LabelApiToken": "API Token",
"LabelAppend": "Append", "LabelAppend": "Append",
"LabelAudioBitrate": "Audio Bitrate (e.g. 128k)",
"LabelAudioChannels": "Audio Channels (1 or 2)",
"LabelAudioCodec": "Audio Codec",
"LabelAuthor": "Author", "LabelAuthor": "Author",
"LabelAuthorFirstLast": "Author (First Last)", "LabelAuthorFirstLast": "Author (First Last)",
"LabelAuthorLastFirst": "Author (Last, First)", "LabelAuthorLastFirst": "Author (Last, First)",
@ -238,6 +245,7 @@
"LabelAutoRegister": "Auto Register", "LabelAutoRegister": "Auto Register",
"LabelAutoRegisterDescription": "Automatically create new users after logging in", "LabelAutoRegisterDescription": "Automatically create new users after logging in",
"LabelBackToUser": "Back to User", "LabelBackToUser": "Back to User",
"LabelBackupAudioFiles": "Backup Audio Files",
"LabelBackupLocation": "Backup Location", "LabelBackupLocation": "Backup Location",
"LabelBackupsEnableAutomaticBackups": "Enable automatic backups", "LabelBackupsEnableAutomaticBackups": "Enable automatic backups",
"LabelBackupsEnableAutomaticBackupsHelp": "Backups saved in /metadata/backups", "LabelBackupsEnableAutomaticBackupsHelp": "Backups saved in /metadata/backups",
@ -246,15 +254,18 @@
"LabelBackupsNumberToKeep": "Number of backups to keep", "LabelBackupsNumberToKeep": "Number of backups to keep",
"LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.", "LabelBackupsNumberToKeepHelp": "Only 1 backup will be removed at a time so if you already have more backups than this you should manually remove them.",
"LabelBitrate": "Bitrate", "LabelBitrate": "Bitrate",
"LabelBonus": "Bonus",
"LabelBooks": "Books", "LabelBooks": "Books",
"LabelButtonText": "Button Text", "LabelButtonText": "Button Text",
"LabelByAuthor": "by {0}", "LabelByAuthor": "by {0}",
"LabelChangePassword": "Change Password", "LabelChangePassword": "Change Password",
"LabelChannels": "Channels", "LabelChannels": "Channels",
"LabelChapterCount": "{0} Chapters",
"LabelChapterTitle": "Chapter Title", "LabelChapterTitle": "Chapter Title",
"LabelChapters": "Chapters", "LabelChapters": "Chapters",
"LabelChaptersFound": "chapters found", "LabelChaptersFound": "chapters found",
"LabelClickForMoreInfo": "Click for more info", "LabelClickForMoreInfo": "Click for more info",
"LabelClickToUseCurrentValue": "Click to use current value",
"LabelClosePlayer": "Close player", "LabelClosePlayer": "Close player",
"LabelCodec": "Codec", "LabelCodec": "Codec",
"LabelCollapseSeries": "Collapse Series", "LabelCollapseSeries": "Collapse Series",
@ -304,12 +315,25 @@
"LabelEmailSettingsTestAddress": "Test Address", "LabelEmailSettingsTestAddress": "Test Address",
"LabelEmbeddedCover": "Embedded Cover", "LabelEmbeddedCover": "Embedded Cover",
"LabelEnable": "Enable", "LabelEnable": "Enable",
"LabelEncodingBackupLocation": "A backup of your original audio files will be stored in:",
"LabelEncodingChaptersNotEmbedded": "Chapters are not embedded in multi-track audiobooks.",
"LabelEncodingClearItemCache": "Make sure to periodically purge items cache.",
"LabelEncodingFinishedM4B": "Finished M4B will be put into your audiobook folder at:",
"LabelEncodingInfoEmbedded": "Metadata will be embedded in the audio tracks inside your audiobook folder.",
"LabelEncodingStartedNavigation": "Once the task is started you can navigate away from this page.",
"LabelEncodingTimeWarning": "Encoding can take up to 30 minutes.",
"LabelEncodingWarningAdvancedSettings": "Warning: Do not update these settings unless you are familiar with ffmpeg encoding options.",
"LabelEncodingWatcherDisabled": "If you have the watcher disabled you will need to re-scan this audiobook afterwards.",
"LabelEnd": "End", "LabelEnd": "End",
"LabelEndOfChapter": "End of Chapter", "LabelEndOfChapter": "End of Chapter",
"LabelEpisode": "Episode", "LabelEpisode": "Episode",
"LabelEpisodeNotLinkedToRssFeed": "Episode not linked to RSS feed",
"LabelEpisodeNumber": "Episode #{0}",
"LabelEpisodeTitle": "Episode Title", "LabelEpisodeTitle": "Episode Title",
"LabelEpisodeType": "Episode Type", "LabelEpisodeType": "Episode Type",
"LabelEpisodeUrlFromRssFeed": "Episode URL from RSS feed",
"LabelEpisodes": "Episodes", "LabelEpisodes": "Episodes",
"LabelEpisodic": "Episodic",
"LabelExample": "Example", "LabelExample": "Example",
"LabelExpandSeries": "Expand Series", "LabelExpandSeries": "Expand Series",
"LabelExpandSubSeries": "Expand Sub Series", "LabelExpandSubSeries": "Expand Sub Series",
@ -338,6 +362,7 @@
"LabelFontScale": "Font scale", "LabelFontScale": "Font scale",
"LabelFontStrikethrough": "Strikethrough", "LabelFontStrikethrough": "Strikethrough",
"LabelFormat": "Format", "LabelFormat": "Format",
"LabelFull": "Full",
"LabelGenre": "Genre", "LabelGenre": "Genre",
"LabelGenres": "Genres", "LabelGenres": "Genres",
"LabelHardDeleteFile": "Hard delete file", "LabelHardDeleteFile": "Hard delete file",
@ -393,6 +418,10 @@
"LabelLowestPriority": "Lowest Priority", "LabelLowestPriority": "Lowest Priority",
"LabelMatchExistingUsersBy": "Match existing users by", "LabelMatchExistingUsersBy": "Match existing users by",
"LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider", "LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
"LabelMaxEpisodesToDownload": "Max # of episodes to download. Use 0 for unlimited.",
"LabelMaxEpisodesToDownloadPerCheck": "Max # of new episodes to download per check",
"LabelMaxEpisodesToKeep": "Max # of episodes to keep",
"LabelMaxEpisodesToKeepHelp": "Value of 0 sets no max limit. After a new episode is auto-downloaded this will delete the oldest episode if you have more than X episodes. This will only delete 1 episode per new download.",
"LabelMediaPlayer": "Media Player", "LabelMediaPlayer": "Media Player",
"LabelMediaType": "Media Type", "LabelMediaType": "Media Type",
"LabelMetaTag": "Meta Tag", "LabelMetaTag": "Meta Tag",
@ -437,12 +466,14 @@
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.", "LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "Open RSS Feed", "LabelOpenRSSFeed": "Open RSS Feed",
"LabelOverwrite": "Overwrite", "LabelOverwrite": "Overwrite",
"LabelPaginationPageXOfY": "Page {0} of {1}",
"LabelPassword": "Password", "LabelPassword": "Password",
"LabelPath": "Path", "LabelPath": "Path",
"LabelPermanent": "Permanent", "LabelPermanent": "Permanent",
"LabelPermissionsAccessAllLibraries": "Can Access All Libraries", "LabelPermissionsAccessAllLibraries": "Can Access All Libraries",
"LabelPermissionsAccessAllTags": "Can Access All Tags", "LabelPermissionsAccessAllTags": "Can Access All Tags",
"LabelPermissionsAccessExplicitContent": "Can Access Explicit Content", "LabelPermissionsAccessExplicitContent": "Can Access Explicit Content",
"LabelPermissionsCreateEreader": "Can Create Ereader",
"LabelPermissionsDelete": "Can Delete", "LabelPermissionsDelete": "Can Delete",
"LabelPermissionsDownload": "Can Download", "LabelPermissionsDownload": "Can Download",
"LabelPermissionsUpdate": "Can Update", "LabelPermissionsUpdate": "Can Update",
@ -466,6 +497,8 @@
"LabelPubDate": "Pub Date", "LabelPubDate": "Pub Date",
"LabelPublishYear": "Publish Year", "LabelPublishYear": "Publish Year",
"LabelPublishedDate": "Published {0}", "LabelPublishedDate": "Published {0}",
"LabelPublishedDecade": "Published Decade",
"LabelPublishedDecades": "Published Decades",
"LabelPublisher": "Publisher", "LabelPublisher": "Publisher",
"LabelPublishers": "Publishers", "LabelPublishers": "Publishers",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email", "LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
@ -485,21 +518,28 @@
"LabelRedo": "Redo", "LabelRedo": "Redo",
"LabelRegion": "Region", "LabelRegion": "Region",
"LabelReleaseDate": "Release Date", "LabelReleaseDate": "Release Date",
"LabelRemoveAllMetadataAbs": "Remove all metadata.abs files",
"LabelRemoveAllMetadataJson": "Remove all metadata.json files",
"LabelRemoveCover": "Remove cover", "LabelRemoveCover": "Remove cover",
"LabelRemoveMetadataFile": "Remove metadata files in library item folders",
"LabelRemoveMetadataFileHelp": "Remove all metadata.json and metadata.abs files in your {0} folders.",
"LabelRowsPerPage": "Rows per page", "LabelRowsPerPage": "Rows per page",
"LabelSearchTerm": "Search Term", "LabelSearchTerm": "Search Term",
"LabelSearchTitle": "Search Title", "LabelSearchTitle": "Search Title",
"LabelSearchTitleOrASIN": "Search Title or ASIN", "LabelSearchTitleOrASIN": "Search Title or ASIN",
"LabelSeason": "Season", "LabelSeason": "Season",
"LabelSeasonNumber": "Season #{0}",
"LabelSelectAll": "Select all", "LabelSelectAll": "Select all",
"LabelSelectAllEpisodes": "Select all episodes", "LabelSelectAllEpisodes": "Select all episodes",
"LabelSelectEpisodesShowing": "Select {0} episodes showing", "LabelSelectEpisodesShowing": "Select {0} episodes showing",
"LabelSelectUsers": "Select users", "LabelSelectUsers": "Select users",
"LabelSendEbookToDevice": "Send Ebook to...", "LabelSendEbookToDevice": "Send Ebook to...",
"LabelSequence": "Sequence", "LabelSequence": "Sequence",
"LabelSerial": "Serial",
"LabelSeries": "Series", "LabelSeries": "Series",
"LabelSeriesName": "Series Name", "LabelSeriesName": "Series Name",
"LabelSeriesProgress": "Series Progress", "LabelSeriesProgress": "Series Progress",
"LabelServerLogLevel": "Server Log Level",
"LabelServerYearReview": "Server Year in Review ({0})", "LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Set as primary", "LabelSetEbookAsPrimary": "Set as primary",
"LabelSetEbookAsSupplementary": "Set as supplementary", "LabelSetEbookAsSupplementary": "Set as supplementary",
@ -524,6 +564,9 @@
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.", "LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
"LabelSettingsHomePageBookshelfView": "Home page use bookshelf view", "LabelSettingsHomePageBookshelfView": "Home page use bookshelf view",
"LabelSettingsLibraryBookshelfView": "Library use bookshelf view", "LabelSettingsLibraryBookshelfView": "Library use bookshelf view",
"LabelSettingsLibraryMarkAsFinishedPercentComplete": "Percent complete is greater than",
"LabelSettingsLibraryMarkAsFinishedTimeRemaining": "Time remaining is less than (seconds)",
"LabelSettingsLibraryMarkAsFinishedWhen": "Mark media item as finished when",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series", "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.", "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Parse subtitles", "LabelSettingsParseSubtitles": "Parse subtitles",
@ -588,6 +631,7 @@
"LabelTimeDurationXMinutes": "{0} minutes", "LabelTimeDurationXMinutes": "{0} minutes",
"LabelTimeDurationXSeconds": "{0} seconds", "LabelTimeDurationXSeconds": "{0} seconds",
"LabelTimeInMinutes": "Time in minutes", "LabelTimeInMinutes": "Time in minutes",
"LabelTimeLeft": "{0} left",
"LabelTimeListened": "Time Listened", "LabelTimeListened": "Time Listened",
"LabelTimeListenedToday": "Time Listened Today", "LabelTimeListenedToday": "Time Listened Today",
"LabelTimeRemaining": "{0} remaining", "LabelTimeRemaining": "{0} remaining",
@ -595,6 +639,7 @@
"LabelTitle": "Title", "LabelTitle": "Title",
"LabelToolsEmbedMetadata": "Embed Metadata", "LabelToolsEmbedMetadata": "Embed Metadata",
"LabelToolsEmbedMetadataDescription": "Embed metadata into audio files including cover image and chapters.", "LabelToolsEmbedMetadataDescription": "Embed metadata into audio files including cover image and chapters.",
"LabelToolsM4bEncoder": "M4B Encoder",
"LabelToolsMakeM4b": "Make M4B Audiobook File", "LabelToolsMakeM4b": "Make M4B Audiobook File",
"LabelToolsMakeM4bDescription": "Generate a .M4B audiobook file with embedded metadata, cover image, and chapters.", "LabelToolsMakeM4bDescription": "Generate a .M4B audiobook file with embedded metadata, cover image, and chapters.",
"LabelToolsSplitM4b": "Split M4B to MP3's", "LabelToolsSplitM4b": "Split M4B to MP3's",
@ -607,6 +652,7 @@
"LabelTracksMultiTrack": "Multi-track", "LabelTracksMultiTrack": "Multi-track",
"LabelTracksNone": "No tracks", "LabelTracksNone": "No tracks",
"LabelTracksSingleTrack": "Single-track", "LabelTracksSingleTrack": "Single-track",
"LabelTrailer": "Trailer",
"LabelType": "Type", "LabelType": "Type",
"LabelUnabridged": "Unabridged", "LabelUnabridged": "Unabridged",
"LabelUndo": "Undo", "LabelUndo": "Undo",
@ -618,10 +664,13 @@
"LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located", "LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located",
"LabelUpdatedAt": "Updated At", "LabelUpdatedAt": "Updated At",
"LabelUploaderDragAndDrop": "Drag & drop files or folders", "LabelUploaderDragAndDrop": "Drag & drop files or folders",
"LabelUploaderDragAndDropFilesOnly": "Drag & drop files",
"LabelUploaderDropFiles": "Drop files", "LabelUploaderDropFiles": "Drop files",
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series", "LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
"LabelUseAdvancedOptions": "Use Advanced Options",
"LabelUseChapterTrack": "Use chapter track", "LabelUseChapterTrack": "Use chapter track",
"LabelUseFullTrack": "Use full track", "LabelUseFullTrack": "Use full track",
"LabelUseZeroForUnlimited": "Use 0 for unlimited",
"LabelUser": "User", "LabelUser": "User",
"LabelUsername": "Username", "LabelUsername": "Username",
"LabelValue": "Value", "LabelValue": "Value",
@ -668,6 +717,7 @@
"MessageConfirmDeleteMetadataProvider": "Are you sure you want to delete custom metadata provider \"{0}\"?", "MessageConfirmDeleteMetadataProvider": "Are you sure you want to delete custom metadata provider \"{0}\"?",
"MessageConfirmDeleteNotification": "Are you sure you want to delete this notification?", "MessageConfirmDeleteNotification": "Are you sure you want to delete this notification?",
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?", "MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
"MessageConfirmEmbedMetadataInAudioFiles": "Are you sure you want to embed metadata in {0} audio files?",
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?", "MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?", "MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?", "MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
@ -679,6 +729,7 @@
"MessageConfirmPurgeCache": "Purge cache will delete the entire directory at <code>/metadata/cache</code>. <br /><br />Are you sure you want to remove the cache directory?", "MessageConfirmPurgeCache": "Purge cache will delete the entire directory at <code>/metadata/cache</code>. <br /><br />Are you sure you want to remove the cache directory?",
"MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?", "MessageConfirmPurgeItemsCache": "Purge items cache will delete the entire directory at <code>/metadata/cache/items</code>.<br />Are you sure?",
"MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?", "MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?",
"MessageConfirmQuickMatchEpisodes": "Quick matching episodes will overwrite details if a match is found. Only unmatched episodes will be updated. Are you sure?",
"MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?", "MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?",
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?", "MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?", "MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
@ -686,6 +737,7 @@
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?", "MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?", "MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?", "MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveMetadataFiles": "Are you sure you want to remove all metadata.{0} files in your library item folders?",
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?", "MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?", "MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?", "MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
@ -701,6 +753,7 @@
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order", "MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
"MessageEmbedFailed": "Embed Failed!", "MessageEmbedFailed": "Embed Failed!",
"MessageEmbedFinished": "Embed Finished!", "MessageEmbedFinished": "Embed Finished!",
"MessageEmbedQueue": "Queued for metadata embed ({0} in queue)",
"MessageEpisodesQueuedForDownload": "{0} Episode(s) queued for download", "MessageEpisodesQueuedForDownload": "{0} Episode(s) queued for download",
"MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.", "MessageEreaderDevices": "To ensure delivery of ebooks, you may need to add the above email address as a valid sender for each device listed below.",
"MessageFeedURLWillBe": "Feed URL will be {0}", "MessageFeedURLWillBe": "Feed URL will be {0}",
@ -745,6 +798,7 @@
"MessageNoLogs": "No Logs", "MessageNoLogs": "No Logs",
"MessageNoMediaProgress": "No Media Progress", "MessageNoMediaProgress": "No Media Progress",
"MessageNoNotifications": "No Notifications", "MessageNoNotifications": "No Notifications",
"MessageNoPodcastFeed": "Invalid podcast: No Feed",
"MessageNoPodcastsFound": "No podcasts found", "MessageNoPodcastsFound": "No podcasts found",
"MessageNoResults": "No Results", "MessageNoResults": "No Results",
"MessageNoSearchResultsFor": "No search results for \"{0}\"", "MessageNoSearchResultsFor": "No search results for \"{0}\"",
@ -761,6 +815,10 @@
"MessagePlaylistCreateFromCollection": "Create playlist from collection", "MessagePlaylistCreateFromCollection": "Create playlist from collection",
"MessagePleaseWait": "Please wait...", "MessagePleaseWait": "Please wait...",
"MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching", "MessagePodcastHasNoRSSFeedForMatching": "Podcast has no RSS feed url to use for matching",
"MessagePodcastSearchField": "Enter search term or RSS feed URL",
"MessageQuickEmbedInProgress": "Quick embed in progress",
"MessageQuickEmbedQueue": "Queued for quick embed ({0} in queue)",
"MessageQuickMatchAllEpisodes": "Quick Match All Episodes",
"MessageQuickMatchDescription": "Populate empty item details & cover with first match result from '{0}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.", "MessageQuickMatchDescription": "Populate empty item details & cover with first match result from '{0}'. Does not overwrite details unless 'Prefer matched metadata' server setting is enabled.",
"MessageRemoveChapter": "Remove chapter", "MessageRemoveChapter": "Remove chapter",
"MessageRemoveEpisodes": "Remove {0} episode(s)", "MessageRemoveEpisodes": "Remove {0} episode(s)",
@ -803,6 +861,9 @@
"MessageTaskOpmlImportFeedPodcastExists": "Podcast already exists at path", "MessageTaskOpmlImportFeedPodcastExists": "Podcast already exists at path",
"MessageTaskOpmlImportFeedPodcastFailed": "Failed to create podcast", "MessageTaskOpmlImportFeedPodcastFailed": "Failed to create podcast",
"MessageTaskOpmlImportFinished": "Added {0} podcasts", "MessageTaskOpmlImportFinished": "Added {0} podcasts",
"MessageTaskOpmlParseFailed": "Failed to parse OPML file",
"MessageTaskOpmlParseFastFail": "Invalid OPML file <opml> tag not found OR an <outline> tag was not found",
"MessageTaskOpmlParseNoneFound": "No feeds found in OPML file",
"MessageTaskScanItemsAdded": "{0} added", "MessageTaskScanItemsAdded": "{0} added",
"MessageTaskScanItemsMissing": "{0} missing", "MessageTaskScanItemsMissing": "{0} missing",
"MessageTaskScanItemsUpdated": "{0} updated", "MessageTaskScanItemsUpdated": "{0} updated",
@ -827,6 +888,10 @@
"NoteUploaderFoldersWithMediaFiles": "Folders with media files will be handled as separate library items.", "NoteUploaderFoldersWithMediaFiles": "Folders with media files will be handled as separate library items.",
"NoteUploaderOnlyAudioFiles": "If uploading only audio files then each audio file will be handled as a separate audiobook.", "NoteUploaderOnlyAudioFiles": "If uploading only audio files then each audio file will be handled as a separate audiobook.",
"NoteUploaderUnsupportedFiles": "Unsupported files are ignored. When choosing or dropping a folder, other files that are not in an item folder are ignored.", "NoteUploaderUnsupportedFiles": "Unsupported files are ignored. When choosing or dropping a folder, other files that are not in an item folder are ignored.",
"NotificationOnBackupCompletedDescription": "Triggered when a backup is completed",
"NotificationOnBackupFailedDescription": "Triggered when a backup fails",
"NotificationOnEpisodeDownloadedDescription": "Triggered when a podcast episode is auto-downloaded",
"NotificationOnTestDescription": "Event for testing the notification system",
"PlaceholderNewCollection": "New collection name", "PlaceholderNewCollection": "New collection name",
"PlaceholderNewFolderPath": "New folder path", "PlaceholderNewFolderPath": "New folder path",
"PlaceholderNewPlaylist": "New playlist name", "PlaceholderNewPlaylist": "New playlist name",
@ -850,14 +915,13 @@
"StatsTopNarrators": "TOP NARRATORS", "StatsTopNarrators": "TOP NARRATORS",
"StatsTotalDuration": "With a total duration of…", "StatsTotalDuration": "With a total duration of…",
"StatsYearInReview": "YEAR IN REVIEW", "StatsYearInReview": "YEAR IN REVIEW",
"ToastAccountUpdateFailed": "Failed to update account",
"ToastAccountUpdateSuccess": "Account updated", "ToastAccountUpdateSuccess": "Account updated",
"ToastAppriseUrlRequired": "Must enter an Apprise URL", "ToastAppriseUrlRequired": "Must enter an Apprise URL",
"ToastAsinRequired": "ASIN is required",
"ToastAuthorImageRemoveSuccess": "Author image removed", "ToastAuthorImageRemoveSuccess": "Author image removed",
"ToastAuthorNotFound": "Author \"{0}\" not found", "ToastAuthorNotFound": "Author \"{0}\" not found",
"ToastAuthorRemoveSuccess": "Author removed", "ToastAuthorRemoveSuccess": "Author removed",
"ToastAuthorSearchNotFound": "Author not found", "ToastAuthorSearchNotFound": "Author not found",
"ToastAuthorUpdateFailed": "Failed to update author",
"ToastAuthorUpdateMerged": "Author merged", "ToastAuthorUpdateMerged": "Author merged",
"ToastAuthorUpdateSuccess": "Author updated", "ToastAuthorUpdateSuccess": "Author updated",
"ToastAuthorUpdateSuccessNoImageFound": "Author updated (no image found)", "ToastAuthorUpdateSuccessNoImageFound": "Author updated (no image found)",
@ -868,29 +932,29 @@
"ToastBackupDeleteSuccess": "Backup deleted", "ToastBackupDeleteSuccess": "Backup deleted",
"ToastBackupInvalidMaxKeep": "Invalid number of backups to keep", "ToastBackupInvalidMaxKeep": "Invalid number of backups to keep",
"ToastBackupInvalidMaxSize": "Invalid maximum backup size", "ToastBackupInvalidMaxSize": "Invalid maximum backup size",
"ToastBackupPathUpdateFailed": "Failed to update backup path",
"ToastBackupRestoreFailed": "Failed to restore backup", "ToastBackupRestoreFailed": "Failed to restore backup",
"ToastBackupUploadFailed": "Failed to upload backup", "ToastBackupUploadFailed": "Failed to upload backup",
"ToastBackupUploadSuccess": "Backup uploaded", "ToastBackupUploadSuccess": "Backup uploaded",
"ToastBatchDeleteFailed": "Batch delete failed", "ToastBatchDeleteFailed": "Batch delete failed",
"ToastBatchDeleteSuccess": "Batch delete success", "ToastBatchDeleteSuccess": "Batch delete success",
"ToastBatchQuickMatchFailed": "Batch Quick Match failed!",
"ToastBatchQuickMatchStarted": "Batch Quick Match of {0} books started!",
"ToastBatchUpdateFailed": "Batch update failed", "ToastBatchUpdateFailed": "Batch update failed",
"ToastBatchUpdateSuccess": "Batch update success", "ToastBatchUpdateSuccess": "Batch update success",
"ToastBookmarkCreateFailed": "Failed to create bookmark", "ToastBookmarkCreateFailed": "Failed to create bookmark",
"ToastBookmarkCreateSuccess": "Bookmark added", "ToastBookmarkCreateSuccess": "Bookmark added",
"ToastBookmarkRemoveSuccess": "Bookmark removed", "ToastBookmarkRemoveSuccess": "Bookmark removed",
"ToastBookmarkUpdateFailed": "Failed to update bookmark",
"ToastBookmarkUpdateSuccess": "Bookmark updated", "ToastBookmarkUpdateSuccess": "Bookmark updated",
"ToastCachePurgeFailed": "Failed to purge cache", "ToastCachePurgeFailed": "Failed to purge cache",
"ToastCachePurgeSuccess": "Cache purged successfully", "ToastCachePurgeSuccess": "Cache purged successfully",
"ToastChaptersHaveErrors": "Chapters have errors", "ToastChaptersHaveErrors": "Chapters have errors",
"ToastChaptersMustHaveTitles": "Chapters must have titles", "ToastChaptersMustHaveTitles": "Chapters must have titles",
"ToastChaptersRemoved": "Chapters removed", "ToastChaptersRemoved": "Chapters removed",
"ToastChaptersUpdated": "Chapters updated",
"ToastCollectionItemsAddFailed": "Item(s) added to collection failed", "ToastCollectionItemsAddFailed": "Item(s) added to collection failed",
"ToastCollectionItemsAddSuccess": "Item(s) added to collection success", "ToastCollectionItemsAddSuccess": "Item(s) added to collection success",
"ToastCollectionItemsRemoveSuccess": "Item(s) removed from collection", "ToastCollectionItemsRemoveSuccess": "Item(s) removed from collection",
"ToastCollectionRemoveSuccess": "Collection removed", "ToastCollectionRemoveSuccess": "Collection removed",
"ToastCollectionUpdateFailed": "Failed to update collection",
"ToastCollectionUpdateSuccess": "Collection updated", "ToastCollectionUpdateSuccess": "Collection updated",
"ToastCoverUpdateFailed": "Cover update failed", "ToastCoverUpdateFailed": "Cover update failed",
"ToastDeleteFileFailed": "Failed to delete file", "ToastDeleteFileFailed": "Failed to delete file",
@ -899,31 +963,28 @@
"ToastDeviceNameAlreadyExists": "Ereader device with that name already exists", "ToastDeviceNameAlreadyExists": "Ereader device with that name already exists",
"ToastDeviceTestEmailFailed": "Failed to send test email", "ToastDeviceTestEmailFailed": "Failed to send test email",
"ToastDeviceTestEmailSuccess": "Test email sent", "ToastDeviceTestEmailSuccess": "Test email sent",
"ToastDeviceUpdateFailed": "Failed to update device",
"ToastEmailSettingsUpdateFailed": "Failed to update email settings",
"ToastEmailSettingsUpdateSuccess": "Email settings updated", "ToastEmailSettingsUpdateSuccess": "Email settings updated",
"ToastEncodeCancelFailed": "Failed to cancel encode", "ToastEncodeCancelFailed": "Failed to cancel encode",
"ToastEncodeCancelSucces": "Encode canceled", "ToastEncodeCancelSucces": "Encode canceled",
"ToastEpisodeDownloadQueueClearFailed": "Failed to clear queue", "ToastEpisodeDownloadQueueClearFailed": "Failed to clear queue",
"ToastEpisodeDownloadQueueClearSuccess": "Episode download queue cleared", "ToastEpisodeDownloadQueueClearSuccess": "Episode download queue cleared",
"ToastEpisodeUpdateSuccess": "{0} episodes updated",
"ToastErrorCannotShare": "Cannot share natively on this device", "ToastErrorCannotShare": "Cannot share natively on this device",
"ToastFailedToLoadData": "Failed to load data", "ToastFailedToLoadData": "Failed to load data",
"ToastFailedToMatch": "Failed to match",
"ToastFailedToShare": "Failed to share", "ToastFailedToShare": "Failed to share",
"ToastFailedToUpdateAccount": "Failed to update account", "ToastFailedToUpdate": "Failed to update",
"ToastFailedToUpdateUser": "Failed to update user",
"ToastInvalidImageUrl": "Invalid image URL", "ToastInvalidImageUrl": "Invalid image URL",
"ToastInvalidMaxEpisodesToDownload": "Invalid max episodes to download",
"ToastInvalidUrl": "Invalid URL", "ToastInvalidUrl": "Invalid URL",
"ToastItemCoverUpdateFailed": "Failed to update item cover",
"ToastItemCoverUpdateSuccess": "Item cover updated", "ToastItemCoverUpdateSuccess": "Item cover updated",
"ToastItemDeletedFailed": "Failed to delete item", "ToastItemDeletedFailed": "Failed to delete item",
"ToastItemDeletedSuccess": "Deleted item", "ToastItemDeletedSuccess": "Deleted item",
"ToastItemDetailsUpdateFailed": "Failed to update item details",
"ToastItemDetailsUpdateSuccess": "Item details updated", "ToastItemDetailsUpdateSuccess": "Item details updated",
"ToastItemMarkedAsFinishedFailed": "Failed to mark as Finished", "ToastItemMarkedAsFinishedFailed": "Failed to mark as Finished",
"ToastItemMarkedAsFinishedSuccess": "Item marked as Finished", "ToastItemMarkedAsFinishedSuccess": "Item marked as Finished",
"ToastItemMarkedAsNotFinishedFailed": "Failed to mark as Not Finished", "ToastItemMarkedAsNotFinishedFailed": "Failed to mark as Not Finished",
"ToastItemMarkedAsNotFinishedSuccess": "Item marked as Not Finished", "ToastItemMarkedAsNotFinishedSuccess": "Item marked as Not Finished",
"ToastItemUpdateFailed": "Failed to update item",
"ToastItemUpdateSuccess": "Item updated", "ToastItemUpdateSuccess": "Item updated",
"ToastLibraryCreateFailed": "Failed to create library", "ToastLibraryCreateFailed": "Failed to create library",
"ToastLibraryCreateSuccess": "Library \"{0}\" created", "ToastLibraryCreateSuccess": "Library \"{0}\" created",
@ -931,37 +992,42 @@
"ToastLibraryDeleteSuccess": "Library deleted", "ToastLibraryDeleteSuccess": "Library deleted",
"ToastLibraryScanFailedToStart": "Failed to start scan", "ToastLibraryScanFailedToStart": "Failed to start scan",
"ToastLibraryScanStarted": "Library scan started", "ToastLibraryScanStarted": "Library scan started",
"ToastLibraryUpdateFailed": "Failed to update library",
"ToastLibraryUpdateSuccess": "Library \"{0}\" updated", "ToastLibraryUpdateSuccess": "Library \"{0}\" updated",
"ToastMatchAllAuthorsFailed": "Failed to match all authors",
"ToastMetadataFilesRemovedError": "Error removing metadata.{0} files",
"ToastMetadataFilesRemovedNoneFound": "No metadata.{0} files found in library",
"ToastMetadataFilesRemovedNoneRemoved": "No metadata.{0} files removed",
"ToastMetadataFilesRemovedSuccess": "{0} metadata.{1} files removed",
"ToastMustHaveAtLeastOnePath": "Must have at least one path",
"ToastNameEmailRequired": "Name and email are required", "ToastNameEmailRequired": "Name and email are required",
"ToastNameRequired": "Name is required", "ToastNameRequired": "Name is required",
"ToastNewEpisodesFound": "{0} new episodes found",
"ToastNewUserCreatedFailed": "Failed to create account: \"{0}\"", "ToastNewUserCreatedFailed": "Failed to create account: \"{0}\"",
"ToastNewUserCreatedSuccess": "New account created", "ToastNewUserCreatedSuccess": "New account created",
"ToastNewUserLibraryError": "Must select at least one library", "ToastNewUserLibraryError": "Must select at least one library",
"ToastNewUserPasswordError": "Must have a password, only root user can have an empty password", "ToastNewUserPasswordError": "Must have a password, only root user can have an empty password",
"ToastNewUserTagError": "Must select at least one tag", "ToastNewUserTagError": "Must select at least one tag",
"ToastNewUserUsernameError": "Enter a username", "ToastNewUserUsernameError": "Enter a username",
"ToastNoNewEpisodesFound": "No new episodes found",
"ToastNoUpdatesNecessary": "No updates necessary", "ToastNoUpdatesNecessary": "No updates necessary",
"ToastNotificationCreateFailed": "Failed to create notification", "ToastNotificationCreateFailed": "Failed to create notification",
"ToastNotificationDeleteFailed": "Failed to delete notification", "ToastNotificationDeleteFailed": "Failed to delete notification",
"ToastNotificationFailedMaximum": "Max failed attempts must be >= 0", "ToastNotificationFailedMaximum": "Max failed attempts must be >= 0",
"ToastNotificationQueueMaximum": "Max notification queue must be >= 0", "ToastNotificationQueueMaximum": "Max notification queue must be >= 0",
"ToastNotificationSettingsUpdateFailed": "Failed to update notification settings",
"ToastNotificationSettingsUpdateSuccess": "Notification settings updated", "ToastNotificationSettingsUpdateSuccess": "Notification settings updated",
"ToastNotificationTestTriggerFailed": "Failed to trigger test notification", "ToastNotificationTestTriggerFailed": "Failed to trigger test notification",
"ToastNotificationTestTriggerSuccess": "Triggered test notification", "ToastNotificationTestTriggerSuccess": "Triggered test notification",
"ToastNotificationUpdateFailed": "Failed to update notification",
"ToastNotificationUpdateSuccess": "Notification updated", "ToastNotificationUpdateSuccess": "Notification updated",
"ToastPlaylistCreateFailed": "Failed to create playlist", "ToastPlaylistCreateFailed": "Failed to create playlist",
"ToastPlaylistCreateSuccess": "Playlist created", "ToastPlaylistCreateSuccess": "Playlist created",
"ToastPlaylistRemoveSuccess": "Playlist removed", "ToastPlaylistRemoveSuccess": "Playlist removed",
"ToastPlaylistUpdateFailed": "Failed to update playlist",
"ToastPlaylistUpdateSuccess": "Playlist updated", "ToastPlaylistUpdateSuccess": "Playlist updated",
"ToastPodcastCreateFailed": "Failed to create podcast", "ToastPodcastCreateFailed": "Failed to create podcast",
"ToastPodcastCreateSuccess": "Podcast created successfully", "ToastPodcastCreateSuccess": "Podcast created successfully",
"ToastPodcastGetFeedFailed": "Failed to get podcast feed", "ToastPodcastGetFeedFailed": "Failed to get podcast feed",
"ToastPodcastNoEpisodesInFeed": "No episodes found in RSS feed", "ToastPodcastNoEpisodesInFeed": "No episodes found in RSS feed",
"ToastPodcastNoRssFeed": "Podcast does not have an RSS feed", "ToastPodcastNoRssFeed": "Podcast does not have an RSS feed",
"ToastProgressIsNotBeingSynced": "Progress is not being synced, restart playback",
"ToastProviderCreatedFailed": "Failed to add provider", "ToastProviderCreatedFailed": "Failed to add provider",
"ToastProviderCreatedSuccess": "New provider added", "ToastProviderCreatedSuccess": "New provider added",
"ToastProviderNameAndUrlRequired": "Name and Url required", "ToastProviderNameAndUrlRequired": "Name and Url required",
@ -984,18 +1050,17 @@
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"", "ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
"ToastSeriesUpdateFailed": "Series update failed", "ToastSeriesUpdateFailed": "Series update failed",
"ToastSeriesUpdateSuccess": "Series update success", "ToastSeriesUpdateSuccess": "Series update success",
"ToastServerSettingsUpdateFailed": "Failed to update server settings",
"ToastServerSettingsUpdateSuccess": "Server settings updated", "ToastServerSettingsUpdateSuccess": "Server settings updated",
"ToastSessionCloseFailed": "Failed to close session", "ToastSessionCloseFailed": "Failed to close session",
"ToastSessionDeleteFailed": "Failed to delete session", "ToastSessionDeleteFailed": "Failed to delete session",
"ToastSessionDeleteSuccess": "Session deleted", "ToastSessionDeleteSuccess": "Session deleted",
"ToastSleepTimerDone": "Sleep timer done... zZzzZz",
"ToastSlugMustChange": "Slug contains invalid characters", "ToastSlugMustChange": "Slug contains invalid characters",
"ToastSlugRequired": "Slug is required", "ToastSlugRequired": "Slug is required",
"ToastSocketConnected": "Socket connected", "ToastSocketConnected": "Socket connected",
"ToastSocketDisconnected": "Socket disconnected", "ToastSocketDisconnected": "Socket disconnected",
"ToastSocketFailedToConnect": "Socket failed to connect", "ToastSocketFailedToConnect": "Socket failed to connect",
"ToastSortingPrefixesEmptyError": "Must have at least 1 sorting prefix", "ToastSortingPrefixesEmptyError": "Must have at least 1 sorting prefix",
"ToastSortingPrefixesUpdateFailed": "Failed to update sorting prefixes",
"ToastSortingPrefixesUpdateSuccess": "Sorting prefixes updated ({0} items)", "ToastSortingPrefixesUpdateSuccess": "Sorting prefixes updated ({0} items)",
"ToastTitleRequired": "Title is required", "ToastTitleRequired": "Title is required",
"ToastUnknownError": "Unknown error", "ToastUnknownError": "Unknown error",

View file

@ -1,6 +1,6 @@
{ {
"ButtonAdd": "Agregar", "ButtonAdd": "Agregaro",
"ButtonAddChapters": "Agregar Capitulo", "ButtonAddChapters": "Agregar",
"ButtonAddDevice": "Agregar Dispositivo", "ButtonAddDevice": "Agregar Dispositivo",
"ButtonAddLibrary": "Crear Biblioteca", "ButtonAddLibrary": "Crear Biblioteca",
"ButtonAddPodcasts": "Agregar Podcasts", "ButtonAddPodcasts": "Agregar Podcasts",
@ -30,6 +30,7 @@
"ButtonEditChapters": "Editar Capítulo", "ButtonEditChapters": "Editar Capítulo",
"ButtonEditPodcast": "Editar Podcast", "ButtonEditPodcast": "Editar Podcast",
"ButtonEnable": "Permitir", "ButtonEnable": "Permitir",
"ButtonFireAndFail": "Ejecutado y fallido",
"ButtonFireOnTest": "Activar evento de prueba", "ButtonFireOnTest": "Activar evento de prueba",
"ButtonForceReScan": "Forzar Re-Escaneo", "ButtonForceReScan": "Forzar Re-Escaneo",
"ButtonFullPath": "Ruta de Acceso Completa", "ButtonFullPath": "Ruta de Acceso Completa",
@ -55,6 +56,7 @@
"ButtonOpenManager": "Abrir Editor", "ButtonOpenManager": "Abrir Editor",
"ButtonPause": "Pausar", "ButtonPause": "Pausar",
"ButtonPlay": "Reproducir", "ButtonPlay": "Reproducir",
"ButtonPlayAll": "Reproducir todo",
"ButtonPlaying": "Reproduciendo", "ButtonPlaying": "Reproduciendo",
"ButtonPlaylists": "Listas de reproducción", "ButtonPlaylists": "Listas de reproducción",
"ButtonPrevious": "Anterior", "ButtonPrevious": "Anterior",
@ -64,12 +66,13 @@
"ButtonPurgeItemsCache": "Purgar Elementos de Cache", "ButtonPurgeItemsCache": "Purgar Elementos de Cache",
"ButtonQueueAddItem": "Agregar a la Fila", "ButtonQueueAddItem": "Agregar a la Fila",
"ButtonQueueRemoveItem": "Remover de la Fila", "ButtonQueueRemoveItem": "Remover de la Fila",
"ButtonQuickEmbed": "Inserción rápida",
"ButtonQuickEmbedMetadata": "Agregue metadatos rápidamente", "ButtonQuickEmbedMetadata": "Agregue metadatos rápidamente",
"ButtonQuickMatch": "Encontrar Rápido", "ButtonQuickMatch": "Encontrar Rápido",
"ButtonReScan": "Re-Escanear", "ButtonReScan": "Re-Escanear",
"ButtonRead": "Leer", "ButtonRead": "Leer",
"ButtonReadLess": "Lea menos", "ButtonReadLess": "Leer menos",
"ButtonReadMore": "Lea mas", "ButtonReadMore": "Leer más",
"ButtonRefresh": "Refrecar", "ButtonRefresh": "Refrecar",
"ButtonRemove": "Remover", "ButtonRemove": "Remover",
"ButtonRemoveAll": "Remover Todos", "ButtonRemoveAll": "Remover Todos",
@ -160,6 +163,7 @@
"HeaderNotificationUpdate": "Notificación de actualización", "HeaderNotificationUpdate": "Notificación de actualización",
"HeaderNotifications": "Notificaciones", "HeaderNotifications": "Notificaciones",
"HeaderOpenIDConnectAuthentication": "Autenticación OpenID Connect", "HeaderOpenIDConnectAuthentication": "Autenticación OpenID Connect",
"HeaderOpenListeningSessions": "Sesiones públicas de escucha",
"HeaderOpenRSSFeed": "Abrir fuente RSS", "HeaderOpenRSSFeed": "Abrir fuente RSS",
"HeaderOtherFiles": "Otros Archivos", "HeaderOtherFiles": "Otros Archivos",
"HeaderPasswordAuthentication": "Autenticación por contraseña", "HeaderPasswordAuthentication": "Autenticación por contraseña",
@ -177,6 +181,7 @@
"HeaderRemoveEpisodes": "Remover {0} Episodios", "HeaderRemoveEpisodes": "Remover {0} Episodios",
"HeaderSavedMediaProgress": "Guardar Progreso de Multimedia", "HeaderSavedMediaProgress": "Guardar Progreso de Multimedia",
"HeaderSchedule": "Horario", "HeaderSchedule": "Horario",
"HeaderScheduleEpisodeDownloads": "Programar descargas automáticas de episodios",
"HeaderScheduleLibraryScans": "Programar Escaneo Automático de Biblioteca", "HeaderScheduleLibraryScans": "Programar Escaneo Automático de Biblioteca",
"HeaderSession": "Sesión", "HeaderSession": "Sesión",
"HeaderSetBackupSchedule": "Programar Respaldo", "HeaderSetBackupSchedule": "Programar Respaldo",
@ -215,14 +220,18 @@
"LabelAddToPlaylist": "Añadido a la lista de reproducción", "LabelAddToPlaylist": "Añadido a la lista de reproducción",
"LabelAddToPlaylistBatch": "Se Añadieron {0} Artículos a la Lista de Reproducción", "LabelAddToPlaylistBatch": "Se Añadieron {0} Artículos a la Lista de Reproducción",
"LabelAddedAt": "Añadido", "LabelAddedAt": "Añadido",
"LabelAddedDate": "Añadido {0}", "LabelAddedDate": "{0} Añadido",
"LabelAdminUsersOnly": "Solamente usuarios administradores", "LabelAdminUsersOnly": "Solamente usuarios administradores",
"LabelAll": "Todos", "LabelAll": "Todos",
"LabelAllUsers": "Todos los Usuarios", "LabelAllUsers": "Todos los Usuarios",
"LabelAllUsersExcludingGuests": "Todos los usuarios excepto invitados", "LabelAllUsersExcludingGuests": "Todos los usuarios excepto invitados",
"LabelAllUsersIncludingGuests": "Todos los usuarios e invitados", "LabelAllUsersIncludingGuests": "Todos los usuarios e invitados",
"LabelAlreadyInYourLibrary": "Ya existe en la Biblioteca", "LabelAlreadyInYourLibrary": "Ya existe en la Biblioteca",
"LabelApiToken": "Token de la API",
"LabelAppend": "Adjuntar", "LabelAppend": "Adjuntar",
"LabelAudioBitrate": "Tasa de bits del audio (por ejemplo, 128k)",
"LabelAudioChannels": "Canales de audio (1 o 2)",
"LabelAudioCodec": "Códec de audio",
"LabelAuthor": "Autor", "LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (Nombre Apellido)", "LabelAuthorFirstLast": "Autor (Nombre Apellido)",
"LabelAuthorLastFirst": "Autor (Apellido, Nombre)", "LabelAuthorLastFirst": "Autor (Apellido, Nombre)",
@ -235,6 +244,7 @@
"LabelAutoRegister": "Registro automático", "LabelAutoRegister": "Registro automático",
"LabelAutoRegisterDescription": "Crear usuarios automáticamente tras iniciar sesión", "LabelAutoRegisterDescription": "Crear usuarios automáticamente tras iniciar sesión",
"LabelBackToUser": "Regresar a Usuario", "LabelBackToUser": "Regresar a Usuario",
"LabelBackupAudioFiles": "Copia de seguridad de archivos de audio",
"LabelBackupLocation": "Ubicación del Respaldo", "LabelBackupLocation": "Ubicación del Respaldo",
"LabelBackupsEnableAutomaticBackups": "Habilitar Respaldo Automático", "LabelBackupsEnableAutomaticBackups": "Habilitar Respaldo Automático",
"LabelBackupsEnableAutomaticBackupsHelp": "Respaldo Guardado en /metadata/backups", "LabelBackupsEnableAutomaticBackupsHelp": "Respaldo Guardado en /metadata/backups",
@ -243,15 +253,18 @@
"LabelBackupsNumberToKeep": "Numero de respaldos para conservar", "LabelBackupsNumberToKeep": "Numero de respaldos para conservar",
"LabelBackupsNumberToKeepHelp": "Solamente 1 respaldo se removerá a la vez. Si tiene mas respaldos guardados, debe removerlos manualmente.", "LabelBackupsNumberToKeepHelp": "Solamente 1 respaldo se removerá a la vez. Si tiene mas respaldos guardados, debe removerlos manualmente.",
"LabelBitrate": "Tasa de bits", "LabelBitrate": "Tasa de bits",
"LabelBonus": "Bonus",
"LabelBooks": "Libros", "LabelBooks": "Libros",
"LabelButtonText": "Texto del botón", "LabelButtonText": "Texto del botón",
"LabelByAuthor": "por {0}", "LabelByAuthor": "por {0}",
"LabelChangePassword": "Cambiar Contraseña", "LabelChangePassword": "Cambiar Contraseña",
"LabelChannels": "Canales", "LabelChannels": "Canales",
"LabelChapterCount": "{0} capítulos",
"LabelChapterTitle": "Titulo del Capítulo", "LabelChapterTitle": "Titulo del Capítulo",
"LabelChapters": "Capítulos", "LabelChapters": "Capítulos",
"LabelChaptersFound": "Capítulo Encontrado", "LabelChaptersFound": "Capítulo Encontrado",
"LabelClickForMoreInfo": "Click para más información", "LabelClickForMoreInfo": "Click para más información",
"LabelClickToUseCurrentValue": "Haz clic para utilizar el valor actual",
"LabelClosePlayer": "Cerrar reproductor", "LabelClosePlayer": "Cerrar reproductor",
"LabelCodec": "Codec", "LabelCodec": "Codec",
"LabelCollapseSeries": "Colapsar serie", "LabelCollapseSeries": "Colapsar serie",
@ -301,12 +314,25 @@
"LabelEmailSettingsTestAddress": "Probar Dirección", "LabelEmailSettingsTestAddress": "Probar Dirección",
"LabelEmbeddedCover": "Portada Integrada", "LabelEmbeddedCover": "Portada Integrada",
"LabelEnable": "Habilitar", "LabelEnable": "Habilitar",
"LabelEncodingBackupLocation": "Se guardará una copia de seguridad de tus archivos de audio originales en:",
"LabelEncodingChaptersNotEmbedded": "Los capítulos no se incrustan en los audiolibros multipista.",
"LabelEncodingClearItemCache": "Asegúrese de purgar periódicamente la caché.",
"LabelEncodingFinishedM4B": "El M4B terminado se colocará en su carpeta de audiolibros en:",
"LabelEncodingInfoEmbedded": "Los metadatos se integrarán en las pistas de audio dentro de la carpeta de audiolibros.",
"LabelEncodingStartedNavigation": "Una vez iniciada la tarea, puedes salir de esta página.",
"LabelEncodingTimeWarning": "La codificación puede tardar hasta 30 minutos.",
"LabelEncodingWarningAdvancedSettings": "Advertencia: No actualice esta configuración a menos que esté familiarizado con las opciones de codificación de ffmpeg.",
"LabelEncodingWatcherDisabled": "Si ha desactivado la supervisión de los archivos, deberá volver a escanear este audiolibro más adelante.",
"LabelEnd": "Fin", "LabelEnd": "Fin",
"LabelEndOfChapter": "Fin del capítulo", "LabelEndOfChapter": "Fin del capítulo",
"LabelEpisode": "Episodio", "LabelEpisode": "Episodio",
"LabelEpisodeNotLinkedToRssFeed": "Episodio no enlazado al feed RSS",
"LabelEpisodeNumber": "Episodio #{0}",
"LabelEpisodeTitle": "Titulo de Episodio", "LabelEpisodeTitle": "Titulo de Episodio",
"LabelEpisodeType": "Tipo de Episodio", "LabelEpisodeType": "Tipo de Episodio",
"LabelEpisodeUrlFromRssFeed": "URL del episodio del feed RSS",
"LabelEpisodes": "Episodios", "LabelEpisodes": "Episodios",
"LabelEpisodic": "Episodios",
"LabelExample": "Ejemplo", "LabelExample": "Ejemplo",
"LabelExpandSeries": "Ampliar serie", "LabelExpandSeries": "Ampliar serie",
"LabelExpandSubSeries": "Expandir la subserie", "LabelExpandSubSeries": "Expandir la subserie",
@ -334,6 +360,7 @@
"LabelFontScale": "Tamaño de fuente", "LabelFontScale": "Tamaño de fuente",
"LabelFontStrikethrough": "Tachado", "LabelFontStrikethrough": "Tachado",
"LabelFormat": "Formato", "LabelFormat": "Formato",
"LabelFull": "Completo",
"LabelGenre": "Genero", "LabelGenre": "Genero",
"LabelGenres": "Géneros", "LabelGenres": "Géneros",
"LabelHardDeleteFile": "Eliminar Definitivamente", "LabelHardDeleteFile": "Eliminar Definitivamente",
@ -389,6 +416,10 @@
"LabelLowestPriority": "Menor prioridad", "LabelLowestPriority": "Menor prioridad",
"LabelMatchExistingUsersBy": "Emparejar a los usuarios existentes por", "LabelMatchExistingUsersBy": "Emparejar a los usuarios existentes por",
"LabelMatchExistingUsersByDescription": "Se utiliza para conectar usuarios existentes. Una vez conectados, los usuarios serán emparejados por un identificador único de su proveedor de SSO", "LabelMatchExistingUsersByDescription": "Se utiliza para conectar usuarios existentes. Una vez conectados, los usuarios serán emparejados por un identificador único de su proveedor de SSO",
"LabelMaxEpisodesToDownload": "Número máximo # de episodios para descargar. Usa 0 para descargar una cantidad ilimitada.",
"LabelMaxEpisodesToDownloadPerCheck": "Número máximo de episodios nuevos que se descargarán por comprobación",
"LabelMaxEpisodesToKeep": "Número máximo de episodios que se mantendrán",
"LabelMaxEpisodesToKeepHelp": "El valor 0 no establece un límite máximo. Después de que se descargue automáticamente un nuevo episodio, esto eliminará el episodio más antiguo si tiene más de X episodios. Esto solo eliminará 1 episodio por nueva descarga.",
"LabelMediaPlayer": "Reproductor de Medios", "LabelMediaPlayer": "Reproductor de Medios",
"LabelMediaType": "Tipo de multimedia", "LabelMediaType": "Tipo de multimedia",
"LabelMetaTag": "Metaetiqueta", "LabelMetaTag": "Metaetiqueta",
@ -434,12 +465,14 @@
"LabelOpenIDGroupClaimDescription": "Nombre de la declaración OpenID que contiene una lista de grupos del usuario. Comúnmente conocidos como <code>grupos</code>. <b>Si se configura</b>, la aplicación asignará automáticamente roles en función de la pertenencia a grupos del usuario, siempre que estos grupos se denominen \"admin\", \"user\" o \"guest\" en la notificación. La solicitud debe contener una lista, y si un usuario pertenece a varios grupos, la aplicación asignará el rol correspondiente al mayor nivel de acceso. Si ningún grupo coincide, se denegará el acceso.", "LabelOpenIDGroupClaimDescription": "Nombre de la declaración OpenID que contiene una lista de grupos del usuario. Comúnmente conocidos como <code>grupos</code>. <b>Si se configura</b>, la aplicación asignará automáticamente roles en función de la pertenencia a grupos del usuario, siempre que estos grupos se denominen \"admin\", \"user\" o \"guest\" en la notificación. La solicitud debe contener una lista, y si un usuario pertenece a varios grupos, la aplicación asignará el rol correspondiente al mayor nivel de acceso. Si ningún grupo coincide, se denegará el acceso.",
"LabelOpenRSSFeed": "Abrir Fuente RSS", "LabelOpenRSSFeed": "Abrir Fuente RSS",
"LabelOverwrite": "Sobrescribir", "LabelOverwrite": "Sobrescribir",
"LabelPaginationPageXOfY": "Página {0} de {1}",
"LabelPassword": "Contraseña", "LabelPassword": "Contraseña",
"LabelPath": "Ruta de carpeta", "LabelPath": "Ruta de carpeta",
"LabelPermanent": "Permanente", "LabelPermanent": "Permanente",
"LabelPermissionsAccessAllLibraries": "Puede Accesar a Todas las bibliotecas", "LabelPermissionsAccessAllLibraries": "Puede Accesar a Todas las bibliotecas",
"LabelPermissionsAccessAllTags": "Pueda Accesar a Todas las Etiquetas", "LabelPermissionsAccessAllTags": "Pueda Accesar a Todas las Etiquetas",
"LabelPermissionsAccessExplicitContent": "Puede Accesar a Contenido Explicito", "LabelPermissionsAccessExplicitContent": "Puede Accesar a Contenido Explicito",
"LabelPermissionsCreateEreader": "Puede crear un gestor de proyectos",
"LabelPermissionsDelete": "Puede Eliminar", "LabelPermissionsDelete": "Puede Eliminar",
"LabelPermissionsDownload": "Puede Descargar", "LabelPermissionsDownload": "Puede Descargar",
"LabelPermissionsUpdate": "Puede Actualizar", "LabelPermissionsUpdate": "Puede Actualizar",
@ -459,9 +492,12 @@
"LabelPrimaryEbook": "Ebook principal", "LabelPrimaryEbook": "Ebook principal",
"LabelProgress": "Progreso", "LabelProgress": "Progreso",
"LabelProvider": "Proveedor", "LabelProvider": "Proveedor",
"LabelProviderAuthorizationValue": "Valor del encabezado de autorización",
"LabelPubDate": "Fecha de publicación", "LabelPubDate": "Fecha de publicación",
"LabelPublishYear": "Año de publicación", "LabelPublishYear": "Año de publicación",
"LabelPublishedDate": "Publicado {0}", "LabelPublishedDate": "Publicado {0}",
"LabelPublishedDecade": "Década de publicación",
"LabelPublishedDecades": "Décadas publicadas",
"LabelPublisher": "Editor", "LabelPublisher": "Editor",
"LabelPublishers": "Editores", "LabelPublishers": "Editores",
"LabelRSSFeedCustomOwnerEmail": "Correo electrónico de dueño personalizado", "LabelRSSFeedCustomOwnerEmail": "Correo electrónico de dueño personalizado",
@ -481,21 +517,28 @@
"LabelRedo": "Rehacer", "LabelRedo": "Rehacer",
"LabelRegion": "Región", "LabelRegion": "Región",
"LabelReleaseDate": "Fecha de Estreno", "LabelReleaseDate": "Fecha de Estreno",
"LabelRemoveAllMetadataAbs": "Eliminar todos los archivos metadata.abs",
"LabelRemoveAllMetadataJson": "Eliminar todos los archivos metadata.json",
"LabelRemoveCover": "Remover Portada", "LabelRemoveCover": "Remover Portada",
"LabelRemoveMetadataFile": "Eliminar archivos de metadatos en carpetas de elementos de biblioteca",
"LabelRemoveMetadataFileHelp": "Elimine todos los archivos metadata.json y metadata.abs de sus carpetas {0}.",
"LabelRowsPerPage": "Filas por página", "LabelRowsPerPage": "Filas por página",
"LabelSearchTerm": "Buscar Termino", "LabelSearchTerm": "Buscar Termino",
"LabelSearchTitle": "Buscar Titulo", "LabelSearchTitle": "Buscar Titulo",
"LabelSearchTitleOrASIN": "Buscar Título o ASIN", "LabelSearchTitleOrASIN": "Buscar Título o ASIN",
"LabelSeason": "Temporada", "LabelSeason": "Temporada",
"LabelSeasonNumber": "Sesión #{0}",
"LabelSelectAll": "Seleccionar todo", "LabelSelectAll": "Seleccionar todo",
"LabelSelectAllEpisodes": "Seleccionar todos los episodios", "LabelSelectAllEpisodes": "Seleccionar todos los episodios",
"LabelSelectEpisodesShowing": "Seleccionar los {0} episodios visibles", "LabelSelectEpisodesShowing": "Seleccionar los {0} episodios visibles",
"LabelSelectUsers": "Seleccionar usuarios", "LabelSelectUsers": "Seleccionar usuarios",
"LabelSendEbookToDevice": "Enviar Ebook a...", "LabelSendEbookToDevice": "Enviar Ebook a...",
"LabelSequence": "Secuencia", "LabelSequence": "Secuencia",
"LabelSerial": "Serial",
"LabelSeries": "Series", "LabelSeries": "Series",
"LabelSeriesName": "Nombre de la Serie", "LabelSeriesName": "Nombre de la Serie",
"LabelSeriesProgress": "Progreso de la Serie", "LabelSeriesProgress": "Progreso de la Serie",
"LabelServerLogLevel": "Nivel de registro del servidor",
"LabelServerYearReview": "Resumen del año del servidor ({0})", "LabelServerYearReview": "Resumen del año del servidor ({0})",
"LabelSetEbookAsPrimary": "Establecer como primario", "LabelSetEbookAsPrimary": "Establecer como primario",
"LabelSetEbookAsSupplementary": "Establecer como suplementario", "LabelSetEbookAsSupplementary": "Establecer como suplementario",
@ -520,6 +563,9 @@
"LabelSettingsHideSingleBookSeriesHelp": "Las series con un solo libro no aparecerán en la página de series ni la repisa para series de la página principal.", "LabelSettingsHideSingleBookSeriesHelp": "Las series con un solo libro no aparecerán en la página de series ni la repisa para series de la página principal.",
"LabelSettingsHomePageBookshelfView": "Usar la vista de librero en la página principal", "LabelSettingsHomePageBookshelfView": "Usar la vista de librero en la página principal",
"LabelSettingsLibraryBookshelfView": "Usar la vista de librero en la biblioteca", "LabelSettingsLibraryBookshelfView": "Usar la vista de librero en la biblioteca",
"LabelSettingsLibraryMarkAsFinishedPercentComplete": "El porcentaje completado es mayor que",
"LabelSettingsLibraryMarkAsFinishedTimeRemaining": "El tiempo restante es menor a (segundos)",
"LabelSettingsLibraryMarkAsFinishedWhen": "Marcar el archivo multimedia como terminado cuando",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Saltar libros anteriores de la serie Continuada", "LabelSettingsOnlyShowLaterBooksInContinueSeries": "Saltar libros anteriores de la serie Continuada",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "El estante de la página de inicio de Continuar Serie muestra el primer libro no iniciado de una serie que tenga por lo menos un libro finalizado y no tenga libros en progreso. Habilitar esta opción le permitirá continuar series desde el último libro que ha completado en vez del primer libro que no ha empezado.", "LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "El estante de la página de inicio de Continuar Serie muestra el primer libro no iniciado de una serie que tenga por lo menos un libro finalizado y no tenga libros en progreso. Habilitar esta opción le permitirá continuar series desde el último libro que ha completado en vez del primer libro que no ha empezado.",
"LabelSettingsParseSubtitles": "Extraer Subtítulos", "LabelSettingsParseSubtitles": "Extraer Subtítulos",
@ -584,6 +630,7 @@
"LabelTimeDurationXMinutes": "{0} minutos", "LabelTimeDurationXMinutes": "{0} minutos",
"LabelTimeDurationXSeconds": "{0} segundos", "LabelTimeDurationXSeconds": "{0} segundos",
"LabelTimeInMinutes": "Tiempo en minutos", "LabelTimeInMinutes": "Tiempo en minutos",
"LabelTimeLeft": "Quedan {0}",
"LabelTimeListened": "Tiempo Escuchando", "LabelTimeListened": "Tiempo Escuchando",
"LabelTimeListenedToday": "Tiempo Escuchando Hoy", "LabelTimeListenedToday": "Tiempo Escuchando Hoy",
"LabelTimeRemaining": "{0} restante", "LabelTimeRemaining": "{0} restante",
@ -591,6 +638,7 @@
"LabelTitle": "Título", "LabelTitle": "Título",
"LabelToolsEmbedMetadata": "Incrustar Metadatos", "LabelToolsEmbedMetadata": "Incrustar Metadatos",
"LabelToolsEmbedMetadataDescription": "Incrusta metadatos en los archivos de audio, incluyendo la portada y capítulos.", "LabelToolsEmbedMetadataDescription": "Incrusta metadatos en los archivos de audio, incluyendo la portada y capítulos.",
"LabelToolsM4bEncoder": "Codificador M4B",
"LabelToolsMakeM4b": "Hacer Archivo de Audiolibro M4B", "LabelToolsMakeM4b": "Hacer Archivo de Audiolibro M4B",
"LabelToolsMakeM4bDescription": "Generar archivo de audiolibro .M4B con metadatos, imágenes de portada y capítulos incorporados.", "LabelToolsMakeM4bDescription": "Generar archivo de audiolibro .M4B con metadatos, imágenes de portada y capítulos incorporados.",
"LabelToolsSplitM4b": "Dividir M4B en Archivos MP3", "LabelToolsSplitM4b": "Dividir M4B en Archivos MP3",
@ -603,6 +651,7 @@
"LabelTracksMultiTrack": "Varias pistas", "LabelTracksMultiTrack": "Varias pistas",
"LabelTracksNone": "Ninguna pista", "LabelTracksNone": "Ninguna pista",
"LabelTracksSingleTrack": "Una pista", "LabelTracksSingleTrack": "Una pista",
"LabelTrailer": "Tráiler",
"LabelType": "Tipo", "LabelType": "Tipo",
"LabelUnabridged": "No Abreviado", "LabelUnabridged": "No Abreviado",
"LabelUndo": "Deshacer", "LabelUndo": "Deshacer",
@ -616,8 +665,10 @@
"LabelUploaderDragAndDrop": "Arrastre y suelte archivos o carpetas", "LabelUploaderDragAndDrop": "Arrastre y suelte archivos o carpetas",
"LabelUploaderDropFiles": "Suelte los Archivos", "LabelUploaderDropFiles": "Suelte los Archivos",
"LabelUploaderItemFetchMetadataHelp": "Buscar título, autor y series automáticamente", "LabelUploaderItemFetchMetadataHelp": "Buscar título, autor y series automáticamente",
"LabelUseAdvancedOptions": "Usar opciones avanzadas",
"LabelUseChapterTrack": "Usar pista por capitulo", "LabelUseChapterTrack": "Usar pista por capitulo",
"LabelUseFullTrack": "Usar pista completa", "LabelUseFullTrack": "Usar pista completa",
"LabelUseZeroForUnlimited": "Utilice 0 para ilimitado",
"LabelUser": "Usuario", "LabelUser": "Usuario",
"LabelUsername": "Nombre de Usuario", "LabelUsername": "Nombre de Usuario",
"LabelValue": "Valor", "LabelValue": "Valor",
@ -630,8 +681,8 @@
"LabelWeekdaysToRun": "Correr en Días de la Semana", "LabelWeekdaysToRun": "Correr en Días de la Semana",
"LabelXBooks": "{0} libros", "LabelXBooks": "{0} libros",
"LabelXItems": "{0} elementos", "LabelXItems": "{0} elementos",
"LabelYearReviewHide": "Ocultar Year in Review", "LabelYearReviewHide": "Ocultar Resumen del año",
"LabelYearReviewShow": "Ver Year in Review", "LabelYearReviewShow": "Resumen del año",
"LabelYourAudiobookDuration": "Duración de tu Audiolibro", "LabelYourAudiobookDuration": "Duración de tu Audiolibro",
"LabelYourBookmarks": "Tus Marcadores", "LabelYourBookmarks": "Tus Marcadores",
"LabelYourPlaylists": "Tus Listas", "LabelYourPlaylists": "Tus Listas",
@ -664,6 +715,7 @@
"MessageConfirmDeleteMetadataProvider": "¿Estás seguro de que deseas eliminar el proveedor de metadatos personalizado \"{0}\"?", "MessageConfirmDeleteMetadataProvider": "¿Estás seguro de que deseas eliminar el proveedor de metadatos personalizado \"{0}\"?",
"MessageConfirmDeleteNotification": "¿Estás seguro de que deseas eliminar esta notificación?", "MessageConfirmDeleteNotification": "¿Estás seguro de que deseas eliminar esta notificación?",
"MessageConfirmDeleteSession": "¿Está seguro de que desea eliminar esta sesión?", "MessageConfirmDeleteSession": "¿Está seguro de que desea eliminar esta sesión?",
"MessageConfirmEmbedMetadataInAudioFiles": "¿Está seguro de que desea incrustar metadatos en {0} archivos de audio?",
"MessageConfirmForceReScan": "¿Está seguro de que desea forzar un re-escaneo?", "MessageConfirmForceReScan": "¿Está seguro de que desea forzar un re-escaneo?",
"MessageConfirmMarkAllEpisodesFinished": "¿Está seguro de que desea marcar todos los episodios como terminados?", "MessageConfirmMarkAllEpisodesFinished": "¿Está seguro de que desea marcar todos los episodios como terminados?",
"MessageConfirmMarkAllEpisodesNotFinished": "¿Está seguro de que desea marcar todos los episodios como no terminados?", "MessageConfirmMarkAllEpisodesNotFinished": "¿Está seguro de que desea marcar todos los episodios como no terminados?",
@ -675,6 +727,7 @@
"MessageConfirmPurgeCache": "Purgar el caché eliminará el directorio completo ubicado en <code>/metadata/cache</code>. <br /><br />¿Está seguro que desea eliminar el directorio del caché?", "MessageConfirmPurgeCache": "Purgar el caché eliminará el directorio completo ubicado en <code>/metadata/cache</code>. <br /><br />¿Está seguro que desea eliminar el directorio del caché?",
"MessageConfirmPurgeItemsCache": "Purgar la caché de los elementos eliminará todo el directorio <code>/metadata/cache/items</code>.<br />¿Estás seguro?", "MessageConfirmPurgeItemsCache": "Purgar la caché de los elementos eliminará todo el directorio <code>/metadata/cache/items</code>.<br />¿Estás seguro?",
"MessageConfirmQuickEmbed": "¡Advertencia! La integración rápida no realiza copias de seguridad a ninguno de tus archivos de audio. Asegúrate de haber realizado una copia de los mismos previamente. <br><br>¿Deseas continuar?", "MessageConfirmQuickEmbed": "¡Advertencia! La integración rápida no realiza copias de seguridad a ninguno de tus archivos de audio. Asegúrate de haber realizado una copia de los mismos previamente. <br><br>¿Deseas continuar?",
"MessageConfirmQuickMatchEpisodes": "El reconocimiento rápido de extensiones sobrescribirá los detalles si se encuentra una coincidencia. Se actualizarán las extensiones no reconocidas. ¿Está seguro?",
"MessageConfirmReScanLibraryItems": "¿Estás seguro de querer re escanear {0} elemento(s)?", "MessageConfirmReScanLibraryItems": "¿Estás seguro de querer re escanear {0} elemento(s)?",
"MessageConfirmRemoveAllChapters": "¿Está seguro de que desea remover todos los capitulos?", "MessageConfirmRemoveAllChapters": "¿Está seguro de que desea remover todos los capitulos?",
"MessageConfirmRemoveAuthor": "¿Está seguro de que desea remover el autor \"{0}\"?", "MessageConfirmRemoveAuthor": "¿Está seguro de que desea remover el autor \"{0}\"?",
@ -682,6 +735,7 @@
"MessageConfirmRemoveEpisode": "¿Está seguro de que desea remover el episodio \"{0}\"?", "MessageConfirmRemoveEpisode": "¿Está seguro de que desea remover el episodio \"{0}\"?",
"MessageConfirmRemoveEpisodes": "¿Está seguro de que desea remover {0} episodios?", "MessageConfirmRemoveEpisodes": "¿Está seguro de que desea remover {0} episodios?",
"MessageConfirmRemoveListeningSessions": "¿Está seguro que desea remover {0} sesiones de escuchar?", "MessageConfirmRemoveListeningSessions": "¿Está seguro que desea remover {0} sesiones de escuchar?",
"MessageConfirmRemoveMetadataFiles": "¿Está seguro de que desea eliminar todos los archivos de metadatos.{0} en las carpetas de elementos de su biblioteca?",
"MessageConfirmRemoveNarrator": "¿Está seguro de que desea remover el narrador \"{0}\"?", "MessageConfirmRemoveNarrator": "¿Está seguro de que desea remover el narrador \"{0}\"?",
"MessageConfirmRemovePlaylist": "¿Está seguro de que desea remover la lista de reproducción \"{0}\"?", "MessageConfirmRemovePlaylist": "¿Está seguro de que desea remover la lista de reproducción \"{0}\"?",
"MessageConfirmRenameGenre": "¿Está seguro de que desea renombrar el genero \"{0}\" a \"{1}\" de todos los elementos?", "MessageConfirmRenameGenre": "¿Está seguro de que desea renombrar el genero \"{0}\" a \"{1}\" de todos los elementos?",
@ -697,6 +751,7 @@
"MessageDragFilesIntoTrackOrder": "Arrastra los archivos al orden correcto de las pistas", "MessageDragFilesIntoTrackOrder": "Arrastra los archivos al orden correcto de las pistas",
"MessageEmbedFailed": "¡Error al insertar!", "MessageEmbedFailed": "¡Error al insertar!",
"MessageEmbedFinished": "Incrustación Terminada!", "MessageEmbedFinished": "Incrustación Terminada!",
"MessageEmbedQueue": "En cola para incrustar metadatos ({0} en cola)",
"MessageEpisodesQueuedForDownload": "{0} Episodio(s) en cola para descargar", "MessageEpisodesQueuedForDownload": "{0} Episodio(s) en cola para descargar",
"MessageEreaderDevices": "Para garantizar la entrega de libros electrónicos, es posible que tenga que agregar la dirección de correo electrónico anterior como remitente válido para cada dispositivo enumerado a continuación.", "MessageEreaderDevices": "Para garantizar la entrega de libros electrónicos, es posible que tenga que agregar la dirección de correo electrónico anterior como remitente válido para cada dispositivo enumerado a continuación.",
"MessageFeedURLWillBe": "URL de la fuente será {0}", "MessageFeedURLWillBe": "URL de la fuente será {0}",
@ -724,7 +779,7 @@
"MessageNoBackups": "Sin Respaldos", "MessageNoBackups": "Sin Respaldos",
"MessageNoBookmarks": "Sin marcadores", "MessageNoBookmarks": "Sin marcadores",
"MessageNoChapters": "Sin capítulos", "MessageNoChapters": "Sin capítulos",
"MessageNoCollections": "Sin Colecciones", "MessageNoCollections": "Sin colecciones",
"MessageNoCoversFound": "Ninguna Portada Encontrada", "MessageNoCoversFound": "Ninguna Portada Encontrada",
"MessageNoDescription": "Sin Descripción", "MessageNoDescription": "Sin Descripción",
"MessageNoDevices": "Sin dispositivos", "MessageNoDevices": "Sin dispositivos",
@ -741,6 +796,7 @@
"MessageNoLogs": "No hay logs", "MessageNoLogs": "No hay logs",
"MessageNoMediaProgress": "Multimedia sin Progreso", "MessageNoMediaProgress": "Multimedia sin Progreso",
"MessageNoNotifications": "Ninguna Notificación", "MessageNoNotifications": "Ninguna Notificación",
"MessageNoPodcastFeed": "Podcast no válido: Sin feed",
"MessageNoPodcastsFound": "Ningún podcast encontrado", "MessageNoPodcastsFound": "Ningún podcast encontrado",
"MessageNoResults": "Sin Resultados", "MessageNoResults": "Sin Resultados",
"MessageNoSearchResultsFor": "No hay resultados para la búsqueda \"{0}\"", "MessageNoSearchResultsFor": "No hay resultados para la búsqueda \"{0}\"",
@ -757,6 +813,10 @@
"MessagePlaylistCreateFromCollection": "Crear una lista de reproducción a partir de una colección", "MessagePlaylistCreateFromCollection": "Crear una lista de reproducción a partir de una colección",
"MessagePleaseWait": "Por favor, espera...", "MessagePleaseWait": "Por favor, espera...",
"MessagePodcastHasNoRSSFeedForMatching": "El podcast no tiene una URL de fuente RSS que pueda usar", "MessagePodcastHasNoRSSFeedForMatching": "El podcast no tiene una URL de fuente RSS que pueda usar",
"MessagePodcastSearchField": "Introduzca el término de búsqueda o la URL de la fuente RSS",
"MessageQuickEmbedInProgress": "Integración rápida en proceso",
"MessageQuickEmbedQueue": "En cola para inserción rápida ({0} en cola)",
"MessageQuickMatchAllEpisodes": "Combina rápidamente todos los episodios",
"MessageQuickMatchDescription": "Rellenar detalles de elementos vacíos y portada con los primeros resultados de '{0}'. No sobrescribe los detalles a menos que la opción \"Preferir Metadatos Encontrados\" del servidor esté habilitada.", "MessageQuickMatchDescription": "Rellenar detalles de elementos vacíos y portada con los primeros resultados de '{0}'. No sobrescribe los detalles a menos que la opción \"Preferir Metadatos Encontrados\" del servidor esté habilitada.",
"MessageRemoveChapter": "Remover capítulos", "MessageRemoveChapter": "Remover capítulos",
"MessageRemoveEpisodes": "Remover {0} episodio(s)", "MessageRemoveEpisodes": "Remover {0} episodio(s)",
@ -774,6 +834,41 @@
"MessageShareExpiresIn": "Caduduca en {0}", "MessageShareExpiresIn": "Caduduca en {0}",
"MessageShareURLWillBe": "La URL para compartir será <strong> {0} </strong>", "MessageShareURLWillBe": "La URL para compartir será <strong> {0} </strong>",
"MessageStartPlaybackAtTime": "Iniciar reproducción para \"{0}\" en {1}?", "MessageStartPlaybackAtTime": "Iniciar reproducción para \"{0}\" en {1}?",
"MessageTaskAudioFileNotWritable": "El archivo de audio \"{0}\" no se puede grabar",
"MessageTaskCanceledByUser": "Tarea cancelada por el usuario",
"MessageTaskDownloadingEpisodeDescription": "Descargando el episodio \"{0}\"",
"MessageTaskEmbeddingMetadata": "Inserción de metadatos",
"MessageTaskEmbeddingMetadataDescription": "Inserción de metadatos en el audiolibro \"{0}\"",
"MessageTaskEncodingM4b": "Codificación M4B",
"MessageTaskEncodingM4bDescription": "Codificación del audiolibro \"{0}\" en un único archivo m4b",
"MessageTaskFailed": "Fallida",
"MessageTaskFailedToBackupAudioFile": "Error en la copia de seguridad del archivo de audio \"{0}\"",
"MessageTaskFailedToCreateCacheDirectory": "Error al crear el directorio de la caché",
"MessageTaskFailedToEmbedMetadataInFile": "Error al incrustar metadatos en el archivo \"{0}\"",
"MessageTaskFailedToMergeAudioFiles": "Error al fusionar archivos de audio",
"MessageTaskFailedToMoveM4bFile": "Error al mover el archivo m4b",
"MessageTaskFailedToWriteMetadataFile": "Error al escribir el archivo de metadatos",
"MessageTaskMatchingBooksInLibrary": "Libros coincidentes en la biblioteca \"{0}\"",
"MessageTaskNoFilesToScan": "Sin archivos para escanear",
"MessageTaskOpmlImport": "Importar OPML",
"MessageTaskOpmlImportDescription": "Creando podcasts a partir de {0} fuentes RSS",
"MessageTaskOpmlImportFeed": "Feed de importación OPML",
"MessageTaskOpmlImportFeedDescription": "Importando el feed RSS \"{0}\"",
"MessageTaskOpmlImportFeedFailed": "No se puede obtener el podcast",
"MessageTaskOpmlImportFeedPodcastDescription": "Creando podcast \"{0}\"",
"MessageTaskOpmlImportFeedPodcastExists": "Podcast ya existe en la ruta",
"MessageTaskOpmlImportFeedPodcastFailed": "Error al crear podcast",
"MessageTaskOpmlImportFinished": "Añadido {0} podcasts",
"MessageTaskOpmlParseFailed": "No se pudo analizar el archivo OPML",
"MessageTaskOpmlParseFastFail": "No se encontró la etiqueta <opml> del archivo OPML no válido O no se encontró la etiqueta <outline>",
"MessageTaskOpmlParseNoneFound": "No se encontraron fuentes en el archivo OPML",
"MessageTaskScanItemsAdded": "{0} añadido",
"MessageTaskScanItemsMissing": "Falta {0}",
"MessageTaskScanItemsUpdated": "{0} actualizado",
"MessageTaskScanNoChangesNeeded": "No se necesitan cambios",
"MessageTaskScanningFileChanges": "Escaneando cambios en el archivo en \"{0}\"",
"MessageTaskScanningLibrary": "Escaneando la biblioteca \"{0}\"",
"MessageTaskTargetDirectoryNotWritable": "El directorio de destino no se puede escribir",
"MessageThinking": "Pensando...", "MessageThinking": "Pensando...",
"MessageUploaderItemFailed": "Error al Subir", "MessageUploaderItemFailed": "Error al Subir",
"MessageUploaderItemSuccess": "¡Éxito al Subir!", "MessageUploaderItemSuccess": "¡Éxito al Subir!",
@ -791,6 +886,10 @@
"NoteUploaderFoldersWithMediaFiles": "Las carpetas con archivos multimedia se manejarán como elementos separados en la biblioteca.", "NoteUploaderFoldersWithMediaFiles": "Las carpetas con archivos multimedia se manejarán como elementos separados en la biblioteca.",
"NoteUploaderOnlyAudioFiles": "Si sube solamente archivos de audio, cada archivo se manejará como un audiolibro por separado.", "NoteUploaderOnlyAudioFiles": "Si sube solamente archivos de audio, cada archivo se manejará como un audiolibro por separado.",
"NoteUploaderUnsupportedFiles": "Se ignorarán los archivos no soportados. Al elegir o arrastrar una carpeta, los archivos que no estén dentro de una subcarpeta serán ignorados.", "NoteUploaderUnsupportedFiles": "Se ignorarán los archivos no soportados. Al elegir o arrastrar una carpeta, los archivos que no estén dentro de una subcarpeta serán ignorados.",
"NotificationOnBackupCompletedDescription": "Se activa cuando se completa una copia de seguridad",
"NotificationOnBackupFailedDescription": "Se activa cuando falla una copia de seguridad",
"NotificationOnEpisodeDownloadedDescription": "Se activa cuando se descarga automáticamente un episodio de un podcast",
"NotificationOnTestDescription": "Evento para probar el sistema de notificaciones",
"PlaceholderNewCollection": "Nuevo nombre de la colección", "PlaceholderNewCollection": "Nuevo nombre de la colección",
"PlaceholderNewFolderPath": "Nueva ruta de carpeta", "PlaceholderNewFolderPath": "Nueva ruta de carpeta",
"PlaceholderNewPlaylist": "Nuevo nombre de la lista de reproducción", "PlaceholderNewPlaylist": "Nuevo nombre de la lista de reproducción",
@ -814,14 +913,13 @@
"StatsTopNarrators": "NARRADORES DESTACADOS", "StatsTopNarrators": "NARRADORES DESTACADOS",
"StatsTotalDuration": "Con una duración total de…", "StatsTotalDuration": "Con una duración total de…",
"StatsYearInReview": "RESEÑA DEL AÑO", "StatsYearInReview": "RESEÑA DEL AÑO",
"ToastAccountUpdateFailed": "Error al actualizar cuenta",
"ToastAccountUpdateSuccess": "Cuenta actualizada", "ToastAccountUpdateSuccess": "Cuenta actualizada",
"ToastAppriseUrlRequired": "Debes ingresar una URL de Apprise", "ToastAppriseUrlRequired": "Debes ingresar una URL de Apprise",
"ToastAsinRequired": "Se requiere ASIN",
"ToastAuthorImageRemoveSuccess": "Se eliminó la imagen del autor", "ToastAuthorImageRemoveSuccess": "Se eliminó la imagen del autor",
"ToastAuthorNotFound": "No se encontró el autor \"{0}\"", "ToastAuthorNotFound": "No se encontró el autor \"{0}\"",
"ToastAuthorRemoveSuccess": "Autor eliminado", "ToastAuthorRemoveSuccess": "Autor eliminado",
"ToastAuthorSearchNotFound": "No se encontró al autor", "ToastAuthorSearchNotFound": "No se encontró al autor",
"ToastAuthorUpdateFailed": "Error al actualizar el autor",
"ToastAuthorUpdateMerged": "Autor combinado", "ToastAuthorUpdateMerged": "Autor combinado",
"ToastAuthorUpdateSuccess": "Autor actualizado", "ToastAuthorUpdateSuccess": "Autor actualizado",
"ToastAuthorUpdateSuccessNoImageFound": "Autor actualizado (Imagen no encontrada)", "ToastAuthorUpdateSuccessNoImageFound": "Autor actualizado (Imagen no encontrada)",
@ -832,29 +930,29 @@
"ToastBackupDeleteSuccess": "Respaldo eliminado", "ToastBackupDeleteSuccess": "Respaldo eliminado",
"ToastBackupInvalidMaxKeep": "Número no válido de copias de seguridad a conservar", "ToastBackupInvalidMaxKeep": "Número no válido de copias de seguridad a conservar",
"ToastBackupInvalidMaxSize": "Tamaño máximo de copia de seguridad no válido", "ToastBackupInvalidMaxSize": "Tamaño máximo de copia de seguridad no válido",
"ToastBackupPathUpdateFailed": "Error al actualizar la ruta de la copia de seguridad",
"ToastBackupRestoreFailed": "Error al restaurar el respaldo", "ToastBackupRestoreFailed": "Error al restaurar el respaldo",
"ToastBackupUploadFailed": "Error al subir el respaldo", "ToastBackupUploadFailed": "Error al subir el respaldo",
"ToastBackupUploadSuccess": "Respaldo cargado", "ToastBackupUploadSuccess": "Respaldo cargado",
"ToastBatchDeleteFailed": "Error al eliminar por lotes", "ToastBatchDeleteFailed": "Error al eliminar por lotes",
"ToastBatchDeleteSuccess": "Borrado por lotes correcto", "ToastBatchDeleteSuccess": "Borrado por lotes correcto",
"ToastBatchQuickMatchFailed": "¡Error en la sincronización rápida por lotes!",
"ToastBatchQuickMatchStarted": "¡Se inició el lote de búsqueda rápida de {0} libros!",
"ToastBatchUpdateFailed": "Subida masiva fallida", "ToastBatchUpdateFailed": "Subida masiva fallida",
"ToastBatchUpdateSuccess": "Subida masiva exitosa", "ToastBatchUpdateSuccess": "Subida masiva exitosa",
"ToastBookmarkCreateFailed": "Error al crear marcador", "ToastBookmarkCreateFailed": "Error al crear marcador",
"ToastBookmarkCreateSuccess": "Marcador Agregado", "ToastBookmarkCreateSuccess": "Marcador Agregado",
"ToastBookmarkRemoveSuccess": "Marcador eliminado", "ToastBookmarkRemoveSuccess": "Marcador eliminado",
"ToastBookmarkUpdateFailed": "Error al actualizar el marcador",
"ToastBookmarkUpdateSuccess": "Marcador actualizado", "ToastBookmarkUpdateSuccess": "Marcador actualizado",
"ToastCachePurgeFailed": "Error al purgar el caché", "ToastCachePurgeFailed": "Error al purgar el caché",
"ToastCachePurgeSuccess": "Caché purgado de manera exitosa", "ToastCachePurgeSuccess": "Caché purgado de manera exitosa",
"ToastChaptersHaveErrors": "Los capítulos tienen errores", "ToastChaptersHaveErrors": "Los capítulos tienen errores",
"ToastChaptersMustHaveTitles": "Los capítulos tienen que tener un título", "ToastChaptersMustHaveTitles": "Los capítulos tienen que tener un título",
"ToastChaptersRemoved": "Capítulos eliminados", "ToastChaptersRemoved": "Capítulos eliminados",
"ToastChaptersUpdated": "Capítulos actualizados",
"ToastCollectionItemsAddFailed": "Artículo(s) añadido(s) a la colección fallido(s)", "ToastCollectionItemsAddFailed": "Artículo(s) añadido(s) a la colección fallido(s)",
"ToastCollectionItemsAddSuccess": "Artículo(s) añadido(s) a la colección correctamente", "ToastCollectionItemsAddSuccess": "Artículo(s) añadido(s) a la colección correctamente",
"ToastCollectionItemsRemoveSuccess": "Elementos(s) removidos de la colección", "ToastCollectionItemsRemoveSuccess": "Elementos(s) removidos de la colección",
"ToastCollectionRemoveSuccess": "Colección removida", "ToastCollectionRemoveSuccess": "Colección removida",
"ToastCollectionUpdateFailed": "Error al actualizar la colección",
"ToastCollectionUpdateSuccess": "Colección actualizada", "ToastCollectionUpdateSuccess": "Colección actualizada",
"ToastCoverUpdateFailed": "Error al actualizar la cubierta", "ToastCoverUpdateFailed": "Error al actualizar la cubierta",
"ToastDeleteFileFailed": "Error el eliminar archivo", "ToastDeleteFileFailed": "Error el eliminar archivo",
@ -863,31 +961,28 @@
"ToastDeviceNameAlreadyExists": "Un libro electrónico ya existe con ese nombre", "ToastDeviceNameAlreadyExists": "Un libro electrónico ya existe con ese nombre",
"ToastDeviceTestEmailFailed": "Error al enviar correo de prueba", "ToastDeviceTestEmailFailed": "Error al enviar correo de prueba",
"ToastDeviceTestEmailSuccess": "Correo electrónico de prueba enviado", "ToastDeviceTestEmailSuccess": "Correo electrónico de prueba enviado",
"ToastDeviceUpdateFailed": "Error al actualizar el dispositivo",
"ToastEmailSettingsUpdateFailed": "Error al actualizar la configuración del correo electrónico",
"ToastEmailSettingsUpdateSuccess": "Configuración del correo electrónico actualizada", "ToastEmailSettingsUpdateSuccess": "Configuración del correo electrónico actualizada",
"ToastEncodeCancelFailed": "No se pudo cancelar la codificación", "ToastEncodeCancelFailed": "No se pudo cancelar la codificación",
"ToastEncodeCancelSucces": "Codificación cancelada", "ToastEncodeCancelSucces": "Codificación cancelada",
"ToastEpisodeDownloadQueueClearFailed": "No se pudo borrar la cola", "ToastEpisodeDownloadQueueClearFailed": "No se pudo borrar la cola",
"ToastEpisodeDownloadQueueClearSuccess": "Se borró la cola de descargas de los episodios", "ToastEpisodeDownloadQueueClearSuccess": "Se borró la cola de descargas de los episodios",
"ToastEpisodeUpdateSuccess": "{0} episodio(s) actualizado(s)",
"ToastErrorCannotShare": "No se puede compartir de forma nativa en este dispositivo", "ToastErrorCannotShare": "No se puede compartir de forma nativa en este dispositivo",
"ToastFailedToLoadData": "Error al cargar data", "ToastFailedToLoadData": "Error al cargar data",
"ToastFailedToMatch": "Error al emparejar",
"ToastFailedToShare": "Error al compartir", "ToastFailedToShare": "Error al compartir",
"ToastFailedToUpdateAccount": "Error al actualizar la cuenta", "ToastFailedToUpdate": "Error al actualizar",
"ToastFailedToUpdateUser": "Error al actualizar el usuario",
"ToastInvalidImageUrl": "URL de la imagen no válida", "ToastInvalidImageUrl": "URL de la imagen no válida",
"ToastInvalidMaxEpisodesToDownload": "Número máximo de episodios para descargar no válidos",
"ToastInvalidUrl": "URL no válida", "ToastInvalidUrl": "URL no válida",
"ToastItemCoverUpdateFailed": "Error al actualizar la portada del elemento",
"ToastItemCoverUpdateSuccess": "Portada del elemento actualizada", "ToastItemCoverUpdateSuccess": "Portada del elemento actualizada",
"ToastItemDeletedFailed": "Error al eliminar el elemento", "ToastItemDeletedFailed": "Error al eliminar el elemento",
"ToastItemDeletedSuccess": "Elemento borrado", "ToastItemDeletedSuccess": "Elemento borrado",
"ToastItemDetailsUpdateFailed": "Error al actualizar los detalles del elemento",
"ToastItemDetailsUpdateSuccess": "Detalles del Elemento Actualizados", "ToastItemDetailsUpdateSuccess": "Detalles del Elemento Actualizados",
"ToastItemMarkedAsFinishedFailed": "Error al marcar como terminado", "ToastItemMarkedAsFinishedFailed": "Error al marcar como terminado",
"ToastItemMarkedAsFinishedSuccess": "Elemento marcado como terminado", "ToastItemMarkedAsFinishedSuccess": "Elemento marcado como terminado",
"ToastItemMarkedAsNotFinishedFailed": "No se ha podido marcar como no finalizado", "ToastItemMarkedAsNotFinishedFailed": "No se ha podido marcar como no finalizado",
"ToastItemMarkedAsNotFinishedSuccess": "Elemento marcado como No Terminado", "ToastItemMarkedAsNotFinishedSuccess": "Elemento marcado como No Terminado",
"ToastItemUpdateFailed": "Error al actualizar el elemento",
"ToastItemUpdateSuccess": "Elemento actualizado", "ToastItemUpdateSuccess": "Elemento actualizado",
"ToastLibraryCreateFailed": "Error al crear biblioteca", "ToastLibraryCreateFailed": "Error al crear biblioteca",
"ToastLibraryCreateSuccess": "Biblioteca \"{0}\" creada", "ToastLibraryCreateSuccess": "Biblioteca \"{0}\" creada",
@ -895,37 +990,42 @@
"ToastLibraryDeleteSuccess": "Biblioteca eliminada", "ToastLibraryDeleteSuccess": "Biblioteca eliminada",
"ToastLibraryScanFailedToStart": "Error al iniciar el escaneo", "ToastLibraryScanFailedToStart": "Error al iniciar el escaneo",
"ToastLibraryScanStarted": "Se inició el escaneo de la biblioteca", "ToastLibraryScanStarted": "Se inició el escaneo de la biblioteca",
"ToastLibraryUpdateFailed": "Error al actualizar la biblioteca",
"ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" actualizada", "ToastLibraryUpdateSuccess": "Biblioteca \"{0}\" actualizada",
"ToastNameEmailRequired": "Nombre y correo electrónico obligatorios", "ToastMatchAllAuthorsFailed": "No coincide con 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)",
"ToastMetadataFilesRemovedSuccess": "{0} metadatos.{1} archivos eliminados",
"ToastMustHaveAtLeastOnePath": "Debe tener al menos una ruta",
"ToastNameEmailRequired": "Son obligatorios el nombre y el correo electrónico",
"ToastNameRequired": "Nombre obligatorio", "ToastNameRequired": "Nombre obligatorio",
"ToastNewEpisodesFound": "{0} nuevo(s) episodio(s) encontrado(s)",
"ToastNewUserCreatedFailed": "Error al crear la cuenta: \"{0}\"", "ToastNewUserCreatedFailed": "Error al crear la cuenta: \"{0}\"",
"ToastNewUserCreatedSuccess": "Nueva cuenta creada", "ToastNewUserCreatedSuccess": "Nueva cuenta creada",
"ToastNewUserLibraryError": "Debes seleccionar al menos una biblioteca", "ToastNewUserLibraryError": "Debes seleccionar al menos una biblioteca",
"ToastNewUserPasswordError": "Debes tener una contraseña, solo el usuario root puede estar sin contraseña", "ToastNewUserPasswordError": "Debes tener una contraseña, solo el usuario root puede estar sin contraseña",
"ToastNewUserTagError": "Debes seleccionar al menos una etiqueta", "ToastNewUserTagError": "Debes seleccionar al menos una etiqueta",
"ToastNewUserUsernameError": "Introduce un nombre de usuario", "ToastNewUserUsernameError": "Introduce un nombre de usuario",
"ToastNoNewEpisodesFound": "No se encontraron nuevos episodios",
"ToastNoUpdatesNecessary": "No es necesario actualizar", "ToastNoUpdatesNecessary": "No es necesario actualizar",
"ToastNotificationCreateFailed": "Error al crear notificación", "ToastNotificationCreateFailed": "Error al crear notificación",
"ToastNotificationDeleteFailed": "Error al borrar la notificación", "ToastNotificationDeleteFailed": "Error al borrar la notificación",
"ToastNotificationFailedMaximum": "El número máximo de intentos fallidos debe ser ≥ 0", "ToastNotificationFailedMaximum": "El número máximo de intentos fallidos debe ser ≥ 0",
"ToastNotificationQueueMaximum": "La cola de notificación máxima debe ser ≥ 0", "ToastNotificationQueueMaximum": "La cola de notificación máxima debe ser ≥ 0",
"ToastNotificationSettingsUpdateFailed": "Error al actualizar los ajustes de la notificación",
"ToastNotificationSettingsUpdateSuccess": "Ajustes de la notificación actualizados", "ToastNotificationSettingsUpdateSuccess": "Ajustes de la notificación actualizados",
"ToastNotificationTestTriggerFailed": "No se ha podido activar la notificación de prueba", "ToastNotificationTestTriggerFailed": "No se ha podido activar la notificación de prueba",
"ToastNotificationTestTriggerSuccess": "Notificación de prueba activada", "ToastNotificationTestTriggerSuccess": "Notificación de prueba activada",
"ToastNotificationUpdateFailed": "No se ha podido actualizar la notificación",
"ToastNotificationUpdateSuccess": "Notificación actualizada", "ToastNotificationUpdateSuccess": "Notificación actualizada",
"ToastPlaylistCreateFailed": "Error al crear la lista de reproducción", "ToastPlaylistCreateFailed": "Error al crear la lista de reproducción",
"ToastPlaylistCreateSuccess": "Lista de reproducción creada", "ToastPlaylistCreateSuccess": "Lista de reproducción creada",
"ToastPlaylistRemoveSuccess": "Lista de reproducción eliminada", "ToastPlaylistRemoveSuccess": "Lista de reproducción eliminada",
"ToastPlaylistUpdateFailed": "Error al actualizar la lista de reproducción",
"ToastPlaylistUpdateSuccess": "Lista de reproducción actualizada", "ToastPlaylistUpdateSuccess": "Lista de reproducción actualizada",
"ToastPodcastCreateFailed": "Error al crear podcast", "ToastPodcastCreateFailed": "Error al crear podcast",
"ToastPodcastCreateSuccess": "Podcast creado", "ToastPodcastCreateSuccess": "Podcast creado",
"ToastPodcastGetFeedFailed": "No se puede obtener el podcast", "ToastPodcastGetFeedFailed": "No se puede obtener el podcast",
"ToastPodcastNoEpisodesInFeed": "No se han encontrado episodios en el feed del RSS", "ToastPodcastNoEpisodesInFeed": "No se han encontrado episodios en el feed del RSS",
"ToastPodcastNoRssFeed": "El podcast no tiene feed RSS", "ToastPodcastNoRssFeed": "El podcast no tiene feed RSS",
"ToastProgressIsNotBeingSynced": "El progreso no se sincroniza, reinicia la reproducción",
"ToastProviderCreatedFailed": "Error al añadir el proveedor", "ToastProviderCreatedFailed": "Error al añadir el proveedor",
"ToastProviderCreatedSuccess": "Nuevo proveedor añadido", "ToastProviderCreatedSuccess": "Nuevo proveedor añadido",
"ToastProviderNameAndUrlRequired": "Nombre y Url obligatorios", "ToastProviderNameAndUrlRequired": "Nombre y Url obligatorios",
@ -948,18 +1048,17 @@
"ToastSendEbookToDeviceSuccess": "Ebook enviado al dispositivo \"{0}\"", "ToastSendEbookToDeviceSuccess": "Ebook enviado al dispositivo \"{0}\"",
"ToastSeriesUpdateFailed": "Error al actualizar la serie", "ToastSeriesUpdateFailed": "Error al actualizar la serie",
"ToastSeriesUpdateSuccess": "Serie actualizada", "ToastSeriesUpdateSuccess": "Serie actualizada",
"ToastServerSettingsUpdateFailed": "Error al actualizar configuración del servidor",
"ToastServerSettingsUpdateSuccess": "Configuración del servidor actualizada", "ToastServerSettingsUpdateSuccess": "Configuración del servidor actualizada",
"ToastSessionCloseFailed": "Error al cerrar la sesión", "ToastSessionCloseFailed": "Error al cerrar la sesión",
"ToastSessionDeleteFailed": "Error al eliminar sesión", "ToastSessionDeleteFailed": "Error al eliminar sesión",
"ToastSessionDeleteSuccess": "Sesión eliminada", "ToastSessionDeleteSuccess": "Sesión eliminada",
"ToastSleepTimerDone": "Temporizador de apagado automático activado... zZzzZz",
"ToastSlugMustChange": "El slug contiene caracteres no válidos", "ToastSlugMustChange": "El slug contiene caracteres no válidos",
"ToastSlugRequired": "Slug obligatorio", "ToastSlugRequired": "Slug obligatorio",
"ToastSocketConnected": "Socket conectado", "ToastSocketConnected": "Socket conectado",
"ToastSocketDisconnected": "Socket desconectado", "ToastSocketDisconnected": "Socket desconectado",
"ToastSocketFailedToConnect": "Error al conectar al Socket", "ToastSocketFailedToConnect": "Error al conectar al Socket",
"ToastSortingPrefixesEmptyError": "Debe tener por lo menos 1 prefijo para ordenar", "ToastSortingPrefixesEmptyError": "Debe tener por lo menos 1 prefijo para ordenar",
"ToastSortingPrefixesUpdateFailed": "Error al actualizar los prefijos de ordenar",
"ToastSortingPrefixesUpdateSuccess": "Prefijos de ordenar actualizaron ({0} items)", "ToastSortingPrefixesUpdateSuccess": "Prefijos de ordenar actualizaron ({0} items)",
"ToastTitleRequired": "Título obligatorio", "ToastTitleRequired": "Título obligatorio",
"ToastUnknownError": "Error desconocido", "ToastUnknownError": "Error desconocido",

View file

@ -9,6 +9,7 @@
"ButtonApply": "Rakenda", "ButtonApply": "Rakenda",
"ButtonApplyChapters": "Rakenda peatükid", "ButtonApplyChapters": "Rakenda peatükid",
"ButtonAuthors": "Autorid", "ButtonAuthors": "Autorid",
"ButtonBack": "Tagasi",
"ButtonBrowseForFolder": "Sirvi kausta", "ButtonBrowseForFolder": "Sirvi kausta",
"ButtonCancel": "Tühista", "ButtonCancel": "Tühista",
"ButtonCancelEncode": "Tühista kodeerimine", "ButtonCancelEncode": "Tühista kodeerimine",
@ -18,6 +19,7 @@
"ButtonChooseFiles": "Vali failid", "ButtonChooseFiles": "Vali failid",
"ButtonClearFilter": "Tühista filter", "ButtonClearFilter": "Tühista filter",
"ButtonCloseFeed": "Sulge voog", "ButtonCloseFeed": "Sulge voog",
"ButtonCloseSession": "Sulge avatud sessioon",
"ButtonCollections": "Kogud", "ButtonCollections": "Kogud",
"ButtonConfigureScanner": "Konfigureeri skanner", "ButtonConfigureScanner": "Konfigureeri skanner",
"ButtonCreate": "Loo", "ButtonCreate": "Loo",
@ -27,6 +29,7 @@
"ButtonEdit": "Muuda", "ButtonEdit": "Muuda",
"ButtonEditChapters": "Muuda peatükke", "ButtonEditChapters": "Muuda peatükke",
"ButtonEditPodcast": "Muuda podcasti", "ButtonEditPodcast": "Muuda podcasti",
"ButtonEnable": "Aktiveeri",
"ButtonForceReScan": "Sunnitud uuestiskaneerimine", "ButtonForceReScan": "Sunnitud uuestiskaneerimine",
"ButtonFullPath": "Täielik asukoht", "ButtonFullPath": "Täielik asukoht",
"ButtonHide": "Peida", "ButtonHide": "Peida",
@ -43,13 +46,18 @@
"ButtonMatchAllAuthors": "Sobita kõik autorid", "ButtonMatchAllAuthors": "Sobita kõik autorid",
"ButtonMatchBooks": "Sobita raamatud", "ButtonMatchBooks": "Sobita raamatud",
"ButtonNevermind": "Pole tähtis", "ButtonNevermind": "Pole tähtis",
"ButtonNext": "Järgmine",
"ButtonNextChapter": "Järgmine peatükk", "ButtonNextChapter": "Järgmine peatükk",
"ButtonNextItemInQueue": "Järgmine kirje järjekorras",
"ButtonOk": "Ok",
"ButtonOpenFeed": "Ava voog", "ButtonOpenFeed": "Ava voog",
"ButtonOpenManager": "Ava haldur", "ButtonOpenManager": "Ava haldur",
"ButtonPause": "Peata", "ButtonPause": "Peata",
"ButtonPlay": "Mängi", "ButtonPlay": "Mängi",
"ButtonPlayAll": "Mängi kõik",
"ButtonPlaying": "Mängib", "ButtonPlaying": "Mängib",
"ButtonPlaylists": "Esitusloendid", "ButtonPlaylists": "Esitusloendid",
"ButtonPrevious": "Eelmine",
"ButtonPreviousChapter": "Eelmine peatükk", "ButtonPreviousChapter": "Eelmine peatükk",
"ButtonPurgeAllCache": "Tühjenda kogu vahemälu", "ButtonPurgeAllCache": "Tühjenda kogu vahemälu",
"ButtonPurgeItemsCache": "Tühjenda esemete vahemälu", "ButtonPurgeItemsCache": "Tühjenda esemete vahemälu",
@ -58,6 +66,9 @@
"ButtonQuickMatch": "Kiire sobitamine", "ButtonQuickMatch": "Kiire sobitamine",
"ButtonReScan": "Uuestiskaneeri", "ButtonReScan": "Uuestiskaneeri",
"ButtonRead": "Loe", "ButtonRead": "Loe",
"ButtonReadLess": "Loe vähem",
"ButtonReadMore": "Loe rohkem",
"ButtonRefresh": "Värskenda",
"ButtonRemove": "Eemalda", "ButtonRemove": "Eemalda",
"ButtonRemoveAll": "Eemalda kõik", "ButtonRemoveAll": "Eemalda kõik",
"ButtonRemoveAllLibraryItems": "Eemalda kõik raamatukogu esemed", "ButtonRemoveAllLibraryItems": "Eemalda kõik raamatukogu esemed",
@ -211,7 +222,7 @@
"LabelBackupLocation": "Varukoopia asukoht", "LabelBackupLocation": "Varukoopia asukoht",
"LabelBackupsEnableAutomaticBackups": "Luba automaatsed varukoopiad", "LabelBackupsEnableAutomaticBackups": "Luba automaatsed varukoopiad",
"LabelBackupsEnableAutomaticBackupsHelp": "Varukoopiad salvestatakse /metadata/backups kausta", "LabelBackupsEnableAutomaticBackupsHelp": "Varukoopiad salvestatakse /metadata/backups kausta",
"LabelBackupsMaxBackupSize": "Maksimaalne varukoopia suurus (GB-des)", "LabelBackupsMaxBackupSize": "Maksimaalne varukoopia suurus (GB-des) (0 lõpmatu suuruse jaoks)",
"LabelBackupsMaxBackupSizeHelp": "Kaitsena valesti seadistamise vastu ebaõnnestuvad varukoopiad, kui need ületavad seadistatud suuruse.", "LabelBackupsMaxBackupSizeHelp": "Kaitsena valesti seadistamise vastu ebaõnnestuvad varukoopiad, kui need ületavad seadistatud suuruse.",
"LabelBackupsNumberToKeep": "Varukoopiate arv, mida hoida", "LabelBackupsNumberToKeep": "Varukoopiate arv, mida hoida",
"LabelBackupsNumberToKeepHelp": "Ühel ajal eemaldatakse ainult 1 varukoopia, seega kui teil on juba rohkem varukoopiaid kui siin määratud, peaksite need käsitsi eemaldama.", "LabelBackupsNumberToKeepHelp": "Ühel ajal eemaldatakse ainult 1 varukoopia, seega kui teil on juba rohkem varukoopiaid kui siin määratud, peaksite need käsitsi eemaldama.",
@ -449,7 +460,7 @@
"LabelSettingsHomePageBookshelfView": "Avaleht kasutage raamatukoguvaadet", "LabelSettingsHomePageBookshelfView": "Avaleht kasutage raamatukoguvaadet",
"LabelSettingsLibraryBookshelfView": "Raamatukogu kasutamiseks kasutage raamatukoguvaadet", "LabelSettingsLibraryBookshelfView": "Raamatukogu kasutamiseks kasutage raamatukoguvaadet",
"LabelSettingsParseSubtitles": "Lugege subtiitreid", "LabelSettingsParseSubtitles": "Lugege subtiitreid",
"LabelSettingsParseSubtitlesHelp": "Eraldage subtiitrid heliraamatu kaustade nimedest.<br>Subtiitrid peavad olema eraldatud \" - \".<br>Näiteks: \"Raamatu pealkiri - Siin on alapealkiri\" alapealkiri on \"Siin on alapealkiri\"", "LabelSettingsParseSubtitlesHelp": "Eraldage subtiitrid heliraamatu kaustade nimedest.<br>Subtiitrid peavad olema eraldatud kasutades \" - \".<br>Näiteks: \"Raamatu pealkiri - Siin on alapealkiri\" alapealkiri on \"Siin on alapealkiri\"",
"LabelSettingsPreferMatchedMetadata": "Eelista sobitatud metaandmeid", "LabelSettingsPreferMatchedMetadata": "Eelista sobitatud metaandmeid",
"LabelSettingsPreferMatchedMetadataHelp": "Sobitatud andmed kirjutavad Kiir Sobitamise kasutamisel üle üksikasjad.", "LabelSettingsPreferMatchedMetadataHelp": "Sobitatud andmed kirjutavad Kiir Sobitamise kasutamisel üle üksikasjad.",
"LabelSettingsSkipMatchingBooksWithASIN": "Jätke ASIN-iga sobituvad raamatud vahele", "LabelSettingsSkipMatchingBooksWithASIN": "Jätke ASIN-iga sobituvad raamatud vahele",
@ -682,10 +693,8 @@
"PlaceholderNewPlaylist": "Uue esitusloendi nimi", "PlaceholderNewPlaylist": "Uue esitusloendi nimi",
"PlaceholderSearch": "Otsi...", "PlaceholderSearch": "Otsi...",
"PlaceholderSearchEpisode": "Otsi episoodi...", "PlaceholderSearchEpisode": "Otsi episoodi...",
"ToastAccountUpdateFailed": "Konto värskendamine ebaõnnestus",
"ToastAccountUpdateSuccess": "Konto on värskendatud", "ToastAccountUpdateSuccess": "Konto on värskendatud",
"ToastAuthorImageRemoveSuccess": "Autori pilt on eemaldatud", "ToastAuthorImageRemoveSuccess": "Autori pilt on eemaldatud",
"ToastAuthorUpdateFailed": "Autori värskendamine ebaõnnestus",
"ToastAuthorUpdateMerged": "Autor liidetud", "ToastAuthorUpdateMerged": "Autor liidetud",
"ToastAuthorUpdateSuccess": "Autor värskendatud", "ToastAuthorUpdateSuccess": "Autor värskendatud",
"ToastAuthorUpdateSuccessNoImageFound": "Autor värskendatud (pilti ei leitud)", "ToastAuthorUpdateSuccessNoImageFound": "Autor värskendatud (pilti ei leitud)",
@ -701,17 +710,13 @@
"ToastBookmarkCreateFailed": "Järjehoidja loomine ebaõnnestus", "ToastBookmarkCreateFailed": "Järjehoidja loomine ebaõnnestus",
"ToastBookmarkCreateSuccess": "Järjehoidja lisatud", "ToastBookmarkCreateSuccess": "Järjehoidja lisatud",
"ToastBookmarkRemoveSuccess": "Järjehoidja eemaldatud", "ToastBookmarkRemoveSuccess": "Järjehoidja eemaldatud",
"ToastBookmarkUpdateFailed": "Järjehoidja värskendamine ebaõnnestus",
"ToastBookmarkUpdateSuccess": "Järjehoidja värskendatud", "ToastBookmarkUpdateSuccess": "Järjehoidja värskendatud",
"ToastChaptersHaveErrors": "Peatükkidel on vigu", "ToastChaptersHaveErrors": "Peatükkidel on vigu",
"ToastChaptersMustHaveTitles": "Peatükkidel peab olema pealkiri", "ToastChaptersMustHaveTitles": "Peatükkidel peab olema pealkiri",
"ToastCollectionItemsRemoveSuccess": "Üksus(ed) eemaldatud kogumist", "ToastCollectionItemsRemoveSuccess": "Üksus(ed) eemaldatud kogumist",
"ToastCollectionRemoveSuccess": "Kogum eemaldatud", "ToastCollectionRemoveSuccess": "Kogum eemaldatud",
"ToastCollectionUpdateFailed": "Kogumi värskendamine ebaõnnestus",
"ToastCollectionUpdateSuccess": "Kogum värskendatud", "ToastCollectionUpdateSuccess": "Kogum värskendatud",
"ToastItemCoverUpdateFailed": "Üksuse kaane värskendamine ebaõnnestus",
"ToastItemCoverUpdateSuccess": "Üksuse kaas värskendatud", "ToastItemCoverUpdateSuccess": "Üksuse kaas värskendatud",
"ToastItemDetailsUpdateFailed": "Üksuse üksikasjade värskendamine ebaõnnestus",
"ToastItemDetailsUpdateSuccess": "Üksuse üksikasjad värskendatud", "ToastItemDetailsUpdateSuccess": "Üksuse üksikasjad värskendatud",
"ToastItemMarkedAsFinishedFailed": "Märgistamine kui lõpetatud ebaõnnestus", "ToastItemMarkedAsFinishedFailed": "Märgistamine kui lõpetatud ebaõnnestus",
"ToastItemMarkedAsFinishedSuccess": "Üksus märgitud kui lõpetatud", "ToastItemMarkedAsFinishedSuccess": "Üksus märgitud kui lõpetatud",
@ -723,12 +728,10 @@
"ToastLibraryDeleteSuccess": "Raamatukogu kustutatud", "ToastLibraryDeleteSuccess": "Raamatukogu kustutatud",
"ToastLibraryScanFailedToStart": "Skanneerimine ei käivitunud", "ToastLibraryScanFailedToStart": "Skanneerimine ei käivitunud",
"ToastLibraryScanStarted": "Raamatukogu skaneerimine alustatud", "ToastLibraryScanStarted": "Raamatukogu skaneerimine alustatud",
"ToastLibraryUpdateFailed": "Raamatukogu värskendamine ebaõnnestus",
"ToastLibraryUpdateSuccess": "Raamatukogu \"{0}\" värskendatud", "ToastLibraryUpdateSuccess": "Raamatukogu \"{0}\" värskendatud",
"ToastPlaylistCreateFailed": "Esitusloendi loomine ebaõnnestus", "ToastPlaylistCreateFailed": "Esitusloendi loomine ebaõnnestus",
"ToastPlaylistCreateSuccess": "Esitusloend loodud", "ToastPlaylistCreateSuccess": "Esitusloend loodud",
"ToastPlaylistRemoveSuccess": "Esitusloend eemaldatud", "ToastPlaylistRemoveSuccess": "Esitusloend eemaldatud",
"ToastPlaylistUpdateFailed": "Esitusloendi värskendamine ebaõnnestus",
"ToastPlaylistUpdateSuccess": "Esitusloend värskendatud", "ToastPlaylistUpdateSuccess": "Esitusloend värskendatud",
"ToastPodcastCreateFailed": "Podcasti loomine ebaõnnestus", "ToastPodcastCreateFailed": "Podcasti loomine ebaõnnestus",
"ToastPodcastCreateSuccess": "Podcast loodud edukalt", "ToastPodcastCreateSuccess": "Podcast loodud edukalt",

Some files were not shown because too many files have changed in this diff Show more