mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-04 01:40:40 +02:00
Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ca5f781531 | |||
| 53c96b2540 | |||
| 9712bdf5f0 | |||
| 0678c26627 | |||
| b52e240025 | |||
| 2fa73f7a8d | |||
| 2cc23b6d6b | |||
| 9a617226b3 | |||
| fbfc015d92 | |||
| 3e4c94e2b4 | |||
| 1da471e136 | |||
| 4dba95c000 | |||
| 36477a832c | |||
| b4aa8f0c9a | |||
| 6a974d5ef0 | |||
| 304eda9f8c | |||
| 581f2e3d15 | |||
| be2d317325 | |||
| 9f6bfeb839 | |||
| f4f5f79af7 | |||
| 92bb2fb23d | |||
| 3c406c12b4 | |||
| 81d4ac3ed2 | |||
| 32bdae31a8 | |||
| 84c16c4a39 | |||
| b8b3d05f5e | |||
| bac09de23d | |||
| b0bf9604bb | |||
| 688531f0a7 | |||
| dfc7877f69 | |||
| e00116a0e3 | |||
| 2ab287e2a9 | |||
| 1e0da09b2f | |||
| 0e7a5649cc | |||
| 30009e45da | |||
| f9a668cb41 | |||
| c848f366de | |||
| 25daab2f34 | |||
| 7170ab7239 | |||
| 063b3bb8db | |||
| 6eb6a7b115 | |||
| d0972348b9 | |||
| 0e70af77c6 | |||
| 4efca78602 | |||
| 87d10bd6f5 | |||
| 0f82aed4ce | |||
| 58f10ad7af | |||
| 68dcf87aea | |||
| c2f85deb11 | |||
| 0dd3a52cc8 | |||
| c07c73c649 | |||
| dbde5f773c | |||
| 68bf038205 | |||
| eb7f66c89e | |||
| 58ebde2982 | |||
| 604a671549 | |||
| 5286b53334 | |||
| 4db26f9f79 | |||
| ff8a58c7bc | |||
| 6f67c7bfa2 | |||
| e9f5bd9bfe | |||
| 56e213d654 | |||
| 98323de64c | |||
| 4a13712b1c | |||
| 0387436111 | |||
| 7685ead000 | |||
| 8665d66923 | |||
| 9a808602c4 | |||
| 813e553dbb | |||
| be050a7d57 | |||
| 065675697d | |||
| 8f6832fc2e | |||
| bdb154a6e5 | |||
| f557289274 | |||
| a5627a1b52 | |||
| 33f20d54cc | |||
| dadd41cb5c | |||
| 35e27e4f61 | |||
| 84839bea44 | |||
| 1342897858 | |||
| c32efb8db8 | |||
| f9ed412e4e | |||
| 6ae3ad508e | |||
| 24af702b41 | |||
| a57ff20f35 | |||
| 39e710deb1 | |||
| 3b6fa73ac0 | |||
| e2dd66d450 | |||
| b1b53a1eae | |||
| 6f73345f39 | |||
| c7b4b3bd3e | |||
| 98d543e3e5 | |||
| 4de4e958a0 | |||
| cc5e92ec8e | |||
| 6cb9dfaa85 | |||
| 8790166ac1 | |||
| 3b97e2146d | |||
| 0bb1cf002d | |||
| 307c7ebc9d | |||
| cc1b41995d | |||
| 730d60575e | |||
| 1b96297cc7 |
@@ -12,4 +12,4 @@ RUN apt-get update && \
|
|||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Move tone executable to appropriate directory
|
# Move tone executable to appropriate directory
|
||||||
COPY --from=sandreas/tone:v0.1.2 /usr/local/bin/tone /usr/local/bin/
|
COPY --from=sandreas/tone:v0.1.5 /usr/local/bin/tone /usr/local/bin/
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ RUN npm ci && npm cache clean --force
|
|||||||
RUN npm run generate
|
RUN npm run generate
|
||||||
|
|
||||||
### STAGE 1: Build server ###
|
### STAGE 1: Build server ###
|
||||||
FROM sandreas/tone:v0.1.2 AS tone
|
FROM sandreas/tone:v0.1.5 AS tone
|
||||||
FROM node:16-alpine
|
FROM node:16-alpine
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ install_ffmpeg() {
|
|||||||
echo "Starting FFMPEG Install"
|
echo "Starting FFMPEG Install"
|
||||||
|
|
||||||
WGET="wget https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-amd64-static.tar.xz --output-document=ffmpeg-git-amd64-static.tar.xz"
|
WGET="wget https://johnvansickle.com/ffmpeg/builds/ffmpeg-git-amd64-static.tar.xz --output-document=ffmpeg-git-amd64-static.tar.xz"
|
||||||
WGET_TONE="wget https://github.com/sandreas/tone/releases/download/v0.1.2/tone-0.1.2-linux-x64.tar.gz --output-document=tone-0.1.2-linux-x64.tar.gz"
|
WGET_TONE="wget https://github.com/sandreas/tone/releases/download/v0.1.5/tone-0.1.5-linux-x64.tar.gz --output-document=tone-0.1.5-linux-x64.tar.gz"
|
||||||
|
|
||||||
if ! cd "$FFMPEG_INSTALL_DIR"; then
|
if ! cd "$FFMPEG_INSTALL_DIR"; then
|
||||||
echo "Creating ffmpeg install dir at $FFMPEG_INSTALL_DIR"
|
echo "Creating ffmpeg install dir at $FFMPEG_INSTALL_DIR"
|
||||||
@@ -66,8 +66,8 @@ install_ffmpeg() {
|
|||||||
# Temp downloading tone library to the ffmpeg dir
|
# Temp downloading tone library to the ffmpeg dir
|
||||||
echo "Getting tone.."
|
echo "Getting tone.."
|
||||||
$WGET_TONE
|
$WGET_TONE
|
||||||
tar xvf tone-0.1.2-linux-x64.tar.gz --strip-components=1
|
tar xvf tone-0.1.5-linux-x64.tar.gz --strip-components=1
|
||||||
rm tone-0.1.2-linux-x64.tar.gz
|
rm tone-0.1.5-linux-x64.tar.gz
|
||||||
|
|
||||||
echo "Good to go on Ffmpeg (& tone)... hopefully"
|
echo "Good to go on Ffmpeg (& tone)... hopefully"
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-1
@@ -112,7 +112,7 @@ input[type=number] {
|
|||||||
background-color: #373838;
|
background-color: #373838;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tracksTable tr:hover {
|
.tracksTable tr:hover:not(:has(th)) {
|
||||||
background-color: #474747;
|
background-color: #474747;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,6 +232,20 @@ Bookshelf Label
|
|||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.episode-subtitle-long {
|
||||||
|
word-break: break-word;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
line-height: 16px;
|
||||||
|
/* fallback */
|
||||||
|
max-height: 72px;
|
||||||
|
/* fallback */
|
||||||
|
-webkit-line-clamp: 6;
|
||||||
|
/* number of lines to show */
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Padding for toastification toasts in the top right to not cover appbar/toolbar */
|
/* Padding for toastification toasts in the top right to not cover appbar/toolbar */
|
||||||
.app-bar-and-toolbar .Vue-Toastification__container.top-right {
|
.app-bar-and-toolbar .Vue-Toastification__container.top-right {
|
||||||
|
|||||||
@@ -15,8 +15,6 @@
|
|||||||
<controls-global-search v-if="currentLibrary" class="mr-1 sm:mr-0" />
|
<controls-global-search v-if="currentLibrary" class="mr-1 sm:mr-0" />
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<widgets-notification-widget class="hidden md:block" />
|
|
||||||
|
|
||||||
<ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center">
|
<ui-tooltip v-if="isChromecastInitialized && !isHttps" direction="bottom" text="Casting requires a secure connection" class="flex items-center">
|
||||||
<span class="material-icons-outlined text-2xl text-warning text-opacity-50"> cast </span>
|
<span class="material-icons-outlined text-2xl text-warning text-opacity-50"> cast </span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@@ -24,6 +22,8 @@
|
|||||||
<google-cast-launcher></google-cast-launcher>
|
<google-cast-launcher></google-cast-launcher>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<widgets-notification-widget class="hidden md:block" />
|
||||||
|
|
||||||
<nuxt-link v-if="currentLibrary" to="/config/stats" class="hover:text-gray-200 cursor-pointer w-8 h-8 hidden sm:flex items-center justify-center mx-1">
|
<nuxt-link v-if="currentLibrary" to="/config/stats" class="hover:text-gray-200 cursor-pointer w-8 h-8 hidden sm:flex items-center justify-center mx-1">
|
||||||
<ui-tooltip :text="$strings.HeaderYourStats" direction="bottom" class="flex items-center">
|
<ui-tooltip :text="$strings.HeaderYourStats" direction="bottom" class="flex items-center">
|
||||||
<span class="material-icons text-2xl" aria-label="User Stats" role="button">equalizer</span>
|
<span class="material-icons text-2xl" aria-label="User Stats" role="button">equalizer</span>
|
||||||
@@ -178,6 +178,11 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options.push({
|
||||||
|
text: 'Re-Scan',
|
||||||
|
action: 'rescan'
|
||||||
|
})
|
||||||
|
|
||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -211,8 +216,34 @@ export default {
|
|||||||
this.requestBatchQuickEmbed()
|
this.requestBatchQuickEmbed()
|
||||||
} else if (action === 'quick-match') {
|
} else if (action === 'quick-match') {
|
||||||
this.batchAutoMatchClick()
|
this.batchAutoMatchClick()
|
||||||
|
} else if (action === 'rescan') {
|
||||||
|
this.batchRescan()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async batchRescan() {
|
||||||
|
const payload = {
|
||||||
|
message: `Are you sure you want to re-scan ${this.selectedMediaItems.length} items?`,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.$axios
|
||||||
|
.$post(`/api/items/batch/scan`, {
|
||||||
|
libraryItemIds: this.selectedMediaItems.map((i) => i.id)
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
console.log('Batch Re-Scan started')
|
||||||
|
this.cancelSelectionMode()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Batch Re-Scan failed', error)
|
||||||
|
const errorMsg = error.response.data || 'Failed to batch re-scan'
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
async playSelectedItems() {
|
async playSelectedItems() {
|
||||||
this.$store.commit('setProcessingBatch', true)
|
this.$store.commit('setProcessingBatch', true)
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<!-- Alternate plain view -->
|
<!-- Alternate plain view -->
|
||||||
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24">
|
<div v-else-if="isAlternativeBookshelfView" class="w-full mb-24">
|
||||||
<template v-for="(shelf, index) in shelves">
|
<template v-for="(shelf, index) in shelves">
|
||||||
<widgets-item-slider v-if="shelf.type === 'book' || shelf.type === 'podcast'" :shelf-id="shelf.id" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6" @selectEntity="(payload) => selectEntity(payload, index)">
|
<widgets-item-slider v-if="shelf.type === 'book' || shelf.type === 'podcast'" :shelf-id="shelf.id" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening' || shelf.id === 'continue-reading'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6" @selectEntity="(payload) => selectEntity(payload, index)">
|
||||||
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
|
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
|
||||||
</widgets-item-slider>
|
</widgets-item-slider>
|
||||||
<widgets-episode-slider v-else-if="shelf.type === 'episode'" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6" @selectEntity="(payload) => selectEntity(payload, index)">
|
<widgets-episode-slider v-else-if="shelf.type === 'episode'" :key="index + '.'" :items="shelf.entities" :continue-listening-shelf="shelf.id === 'continue-listening'" :height="232 * sizeMultiplier" class="bookshelf-row pl-8 my-6" @selectEntity="(payload) => selectEntity(payload, index)">
|
||||||
@@ -28,12 +28,15 @@
|
|||||||
<widgets-authors-slider v-else-if="shelf.type === 'authors'" :key="index + '.'" :items="shelf.entities" :height="192 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
|
<widgets-authors-slider v-else-if="shelf.type === 'authors'" :key="index + '.'" :items="shelf.entities" :height="192 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
|
||||||
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
|
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
|
||||||
</widgets-authors-slider>
|
</widgets-authors-slider>
|
||||||
|
<widgets-narrators-slider v-else-if="shelf.type === 'narrators'" :key="index + '.'" :items="shelf.entities" :height="100 * sizeMultiplier" class="bookshelf-row pl-8 my-6">
|
||||||
|
<p class="font-semibold text-gray-100" :style="{ fontSize: sizeMultiplier + 'rem' }">{{ $strings[shelf.labelStringKey] }}</p>
|
||||||
|
</widgets-narrators-slider>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<!-- Regular bookshelf view -->
|
<!-- Regular bookshelf view -->
|
||||||
<div v-else class="w-full">
|
<div v-else class="w-full">
|
||||||
<template v-for="(shelf, index) in shelves">
|
<template v-for="(shelf, index) in shelves">
|
||||||
<app-book-shelf-row :key="index" :index="index" :shelf="shelf" :size-multiplier="sizeMultiplier" :book-cover-width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" :continue-listening-shelf="shelf.id === 'continue-listening'" @selectEntity="(payload) => selectEntity(payload, index)" />
|
<app-book-shelf-row :key="index" :index="index" :shelf="shelf" :size-multiplier="sizeMultiplier" :book-cover-width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" :continue-listening-shelf="shelf.id === 'continue-listening' || shelf.id === 'continue-reading'" @selectEntity="(payload) => selectEntity(payload, index)" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -185,8 +188,8 @@ export default {
|
|||||||
this.shelves = categories
|
this.shelves = categories
|
||||||
},
|
},
|
||||||
async setShelvesFromSearch() {
|
async setShelvesFromSearch() {
|
||||||
var shelves = []
|
const shelves = []
|
||||||
if (this.results.books && this.results.books.length) {
|
if (this.results.books?.length) {
|
||||||
shelves.push({
|
shelves.push({
|
||||||
id: 'books',
|
id: 'books',
|
||||||
label: 'Books',
|
label: 'Books',
|
||||||
@@ -196,7 +199,7 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.results.podcasts && this.results.podcasts.length) {
|
if (this.results.podcasts?.length) {
|
||||||
shelves.push({
|
shelves.push({
|
||||||
id: 'podcasts',
|
id: 'podcasts',
|
||||||
label: 'Podcasts',
|
label: 'Podcasts',
|
||||||
@@ -206,7 +209,7 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.results.series && this.results.series.length) {
|
if (this.results.series?.length) {
|
||||||
shelves.push({
|
shelves.push({
|
||||||
id: 'series',
|
id: 'series',
|
||||||
label: 'Series',
|
label: 'Series',
|
||||||
@@ -221,7 +224,7 @@ export default {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (this.results.tags && this.results.tags.length) {
|
if (this.results.tags?.length) {
|
||||||
shelves.push({
|
shelves.push({
|
||||||
id: 'tags',
|
id: 'tags',
|
||||||
label: 'Tags',
|
label: 'Tags',
|
||||||
@@ -236,7 +239,7 @@ export default {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (this.results.authors && this.results.authors.length) {
|
if (this.results.authors?.length) {
|
||||||
shelves.push({
|
shelves.push({
|
||||||
id: 'authors',
|
id: 'authors',
|
||||||
label: 'Authors',
|
label: 'Authors',
|
||||||
@@ -250,6 +253,20 @@ export default {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (this.results.narrators?.length) {
|
||||||
|
shelves.push({
|
||||||
|
id: 'narrators',
|
||||||
|
label: 'Narrators',
|
||||||
|
labelStringKey: 'LabelNarrators',
|
||||||
|
type: 'narrators',
|
||||||
|
entities: this.results.narrators.map((n) => {
|
||||||
|
return {
|
||||||
|
...n,
|
||||||
|
type: 'narrator'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
this.shelves = shelves
|
this.shelves = shelves
|
||||||
},
|
},
|
||||||
scan() {
|
scan() {
|
||||||
@@ -269,7 +286,8 @@ export default {
|
|||||||
}
|
}
|
||||||
if (user.mediaProgress.length) {
|
if (user.mediaProgress.length) {
|
||||||
const mediaProgressToHide = user.mediaProgress.filter((mp) => mp.hideFromContinueListening)
|
const mediaProgressToHide = user.mediaProgress.filter((mp) => mp.hideFromContinueListening)
|
||||||
this.removeItemsFromContinueListening(mediaProgressToHide)
|
this.removeItemsFromContinueListeningReading(mediaProgressToHide, 'continue-listening')
|
||||||
|
this.removeItemsFromContinueListeningReading(mediaProgressToHide, 'continue-reading')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
libraryItemAdded(libraryItem) {
|
libraryItemAdded(libraryItem) {
|
||||||
@@ -319,8 +337,9 @@ export default {
|
|||||||
},
|
},
|
||||||
libraryItemsAdded(libraryItems) {
|
libraryItemsAdded(libraryItems) {
|
||||||
console.log('libraryItems added', libraryItems)
|
console.log('libraryItems added', libraryItems)
|
||||||
// TODO: Check if audiobook would be on this shelf
|
|
||||||
if (!this.search) {
|
const isThisLibrary = !libraryItems.some((li) => li.libraryId !== this.currentLibraryId)
|
||||||
|
if (!this.search && isThisLibrary) {
|
||||||
this.fetchCategories()
|
this.fetchCategories()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -329,6 +348,14 @@ export default {
|
|||||||
this.libraryItemUpdated(li)
|
this.libraryItemUpdated(li)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
episodeAdded(episodeWithLibraryItem) {
|
||||||
|
console.log('Podcast episode added', episodeWithLibraryItem)
|
||||||
|
|
||||||
|
const isThisLibrary = episodeWithLibraryItem.libraryItem?.libraryId === this.currentLibraryId
|
||||||
|
if (!this.search && isThisLibrary) {
|
||||||
|
this.fetchCategories()
|
||||||
|
}
|
||||||
|
},
|
||||||
removeAllSeriesFromContinueSeries(seriesIds) {
|
removeAllSeriesFromContinueSeries(seriesIds) {
|
||||||
this.shelves.forEach((shelf) => {
|
this.shelves.forEach((shelf) => {
|
||||||
if (shelf.type == 'book' && shelf.id == 'continue-series') {
|
if (shelf.type == 'book' && shelf.id == 'continue-series') {
|
||||||
@@ -340,8 +367,8 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
removeItemsFromContinueListening(mediaProgressItems) {
|
removeItemsFromContinueListeningReading(mediaProgressItems, categoryId) {
|
||||||
const continueListeningShelf = this.shelves.find((s) => s.id === 'continue-listening')
|
const continueListeningShelf = this.shelves.find((s) => s.id === categoryId)
|
||||||
if (continueListeningShelf) {
|
if (continueListeningShelf) {
|
||||||
if (continueListeningShelf.type === 'book') {
|
if (continueListeningShelf.type === 'book') {
|
||||||
continueListeningShelf.entities = continueListeningShelf.entities.filter((ent) => {
|
continueListeningShelf.entities = continueListeningShelf.entities.filter((ent) => {
|
||||||
@@ -356,17 +383,6 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// this.shelves.forEach((shelf) => {
|
|
||||||
// if (shelf.id == 'continue-listening') {
|
|
||||||
// if (shelf.type == 'book') {
|
|
||||||
// // Filter out books from continue listening shelf
|
|
||||||
// shelf.entities = shelf.entities.filter((ent) => {
|
|
||||||
// if (mediaProgressItems.some(mp => mp.libraryItemId === ent.id)) return false
|
|
||||||
// return true
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
},
|
},
|
||||||
authorUpdated(author) {
|
authorUpdated(author) {
|
||||||
this.shelves.forEach((shelf) => {
|
this.shelves.forEach((shelf) => {
|
||||||
@@ -400,6 +416,7 @@ export default {
|
|||||||
this.$root.socket.on('item_removed', this.libraryItemRemoved)
|
this.$root.socket.on('item_removed', this.libraryItemRemoved)
|
||||||
this.$root.socket.on('items_updated', this.libraryItemsUpdated)
|
this.$root.socket.on('items_updated', this.libraryItemsUpdated)
|
||||||
this.$root.socket.on('items_added', this.libraryItemsAdded)
|
this.$root.socket.on('items_added', this.libraryItemsAdded)
|
||||||
|
this.$root.socket.on('episode_added', this.episodeAdded)
|
||||||
} else {
|
} else {
|
||||||
console.error('Error socket not initialized')
|
console.error('Error socket not initialized')
|
||||||
}
|
}
|
||||||
@@ -414,6 +431,7 @@ export default {
|
|||||||
this.$root.socket.off('item_removed', this.libraryItemRemoved)
|
this.$root.socket.off('item_removed', this.libraryItemRemoved)
|
||||||
this.$root.socket.off('items_updated', this.libraryItemsUpdated)
|
this.$root.socket.off('items_updated', this.libraryItemsUpdated)
|
||||||
this.$root.socket.off('items_added', this.libraryItemsAdded)
|
this.$root.socket.off('items_added', this.libraryItemsAdded)
|
||||||
|
this.$root.socket.off('episode_added', this.episodeAdded)
|
||||||
} else {
|
} else {
|
||||||
console.error('Error socket not initialized')
|
console.error('Error socket not initialized')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,11 @@
|
|||||||
<cards-author-card :key="entity.id" :width="bookCoverWidth / 1.25" :height="bookCoverWidth" :author="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" @edit="editAuthor" />
|
<cards-author-card :key="entity.id" :width="bookCoverWidth / 1.25" :height="bookCoverWidth" :author="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" @edit="editAuthor" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="shelf.type === 'narrators'" class="flex items-center">
|
||||||
|
<template v-for="entity in shelf.entities">
|
||||||
|
<cards-narrator-card :key="entity.name" :width="150" :height="100" :narrator="entity" :size-multiplier="sizeMultiplier" @hook:updated="updatedBookCard" class="pb-6 mx-2" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -88,6 +93,7 @@ export default {
|
|||||||
return this.bookCoverWidth * this.bookCoverAspectRatio
|
return this.bookCoverWidth * this.bookCoverAspectRatio
|
||||||
},
|
},
|
||||||
shelfHeight() {
|
shelfHeight() {
|
||||||
|
if (this.shelf.type === 'narrators') return 148
|
||||||
return this.bookCoverHeight + 48
|
return this.bookCoverHeight + 48
|
||||||
},
|
},
|
||||||
paddingLeft() {
|
paddingLeft() {
|
||||||
|
|||||||
@@ -49,6 +49,14 @@
|
|||||||
<div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="paramId === 'collections'" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
|
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" 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="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
|
<span class="material-icons text-2.5xl">queue_music</span>
|
||||||
|
|
||||||
|
<p class="pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p>
|
||||||
|
|
||||||
|
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
|
</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}/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
|
||||||
@@ -62,6 +70,14 @@
|
|||||||
<div v-show="isAuthorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isAuthorsPage" 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}/narrators`" 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="isNarratorsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
|
<span class="material-icons text-2xl">record_voice_over</span>
|
||||||
|
|
||||||
|
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.LabelNarrators }}</p>
|
||||||
|
|
||||||
|
<div v-show="isNarratorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="abs-icons icon-podcast text-xl"></span>
|
<span class="abs-icons icon-podcast text-xl"></span>
|
||||||
|
|
||||||
@@ -78,14 +94,6 @@
|
|||||||
<div v-show="isMusicAlbumsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isMusicAlbumsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" 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="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
|
||||||
<span class="material-icons text-2.5xl">queue_music</span>
|
|
||||||
|
|
||||||
<p class="pt-0.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonPlaylists }}</p>
|
|
||||||
|
|
||||||
<div v-show="isPlaylistsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
|
||||||
</nuxt-link>
|
|
||||||
|
|
||||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" 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="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/download-queue`" 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="isPodcastDownloadQueuePage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="material-icons text-2xl">file_download</span>
|
<span class="material-icons text-2xl">file_download</span>
|
||||||
|
|
||||||
@@ -178,6 +186,9 @@ export default {
|
|||||||
isAuthorsPage() {
|
isAuthorsPage() {
|
||||||
return this.$route.name === 'library-library-authors'
|
return this.$route.name === 'library-library-authors'
|
||||||
},
|
},
|
||||||
|
isNarratorsPage() {
|
||||||
|
return this.$route.name === 'library-library-narrators'
|
||||||
|
},
|
||||||
isPlaylistsPage() {
|
isPlaylistsPage() {
|
||||||
return this.paramId === 'playlists'
|
return this.paramId === 'playlists'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div>
|
<div v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</div>
|
||||||
<div v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</div>
|
<div v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</div>
|
||||||
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base">
|
<div v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base">
|
||||||
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
<nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}?library=${libraryId}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">{{ $strings.LabelUnknown }}</div>
|
<div v-else class="text-xs sm:text-base cursor-pointer pl-1 sm:pl-1.5">{{ $strings.LabelUnknown }}</div>
|
||||||
<widgets-explicit-indicator :explicit="isExplicit"></widgets-explicit-indicator>
|
<widgets-explicit-indicator :explicit="isExplicit"></widgets-explicit-indicator>
|
||||||
@@ -268,6 +268,10 @@ export default {
|
|||||||
seek(time) {
|
seek(time) {
|
||||||
this.playerHandler.seek(time)
|
this.playerHandler.seek(time)
|
||||||
},
|
},
|
||||||
|
playbackTimeUpdate(time) {
|
||||||
|
// When updating progress from another session
|
||||||
|
this.playerHandler.seek(time, false)
|
||||||
|
},
|
||||||
setCurrentTime(time) {
|
setCurrentTime(time) {
|
||||||
this.currentTime = time
|
this.currentTime = time
|
||||||
if (this.$refs.audioPlayer) {
|
if (this.$refs.audioPlayer) {
|
||||||
@@ -366,9 +370,8 @@ export default {
|
|||||||
navigator.mediaSession.setActionHandler('seekbackward', this.mediaSessionSeekBackward)
|
navigator.mediaSession.setActionHandler('seekbackward', this.mediaSessionSeekBackward)
|
||||||
navigator.mediaSession.setActionHandler('seekforward', this.mediaSessionSeekForward)
|
navigator.mediaSession.setActionHandler('seekforward', this.mediaSessionSeekForward)
|
||||||
navigator.mediaSession.setActionHandler('seekto', this.mediaSessionSeekTo)
|
navigator.mediaSession.setActionHandler('seekto', this.mediaSessionSeekTo)
|
||||||
navigator.mediaSession.setActionHandler('previoustrack', this.mediaSessionPreviousTrack)
|
navigator.mediaSession.setActionHandler('previoustrack', this.mediaSessionSeekBackward)
|
||||||
const hasNextChapter = this.$refs.audioPlayer && this.$refs.audioPlayer.hasNextChapter
|
navigator.mediaSession.setActionHandler('nexttrack', this.mediaSessionSeekForward)
|
||||||
navigator.mediaSession.setActionHandler('nexttrack', hasNextChapter ? this.mediaSessionNextTrack : null)
|
|
||||||
} else {
|
} else {
|
||||||
console.warn('Media session not available')
|
console.warn('Media session not available')
|
||||||
}
|
}
|
||||||
@@ -478,12 +481,14 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
this.$eventBus.$on('cast-session-active', this.castSessionActive)
|
this.$eventBus.$on('cast-session-active', this.castSessionActive)
|
||||||
this.$eventBus.$on('playback-seek', this.seek)
|
this.$eventBus.$on('playback-seek', this.seek)
|
||||||
|
this.$eventBus.$on('playback-time-update', this.playbackTimeUpdate)
|
||||||
this.$eventBus.$on('play-item', this.playLibraryItem)
|
this.$eventBus.$on('play-item', this.playLibraryItem)
|
||||||
this.$eventBus.$on('pause-item', this.pauseItem)
|
this.$eventBus.$on('pause-item', this.pauseItem)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$eventBus.$off('cast-session-active', this.castSessionActive)
|
this.$eventBus.$off('cast-session-active', this.castSessionActive)
|
||||||
this.$eventBus.$off('playback-seek', this.seek)
|
this.$eventBus.$off('playback-seek', this.seek)
|
||||||
|
this.$eventBus.$off('playback-time-update', this.playbackTimeUpdate)
|
||||||
this.$eventBus.$off('play-item', this.playLibraryItem)
|
this.$eventBus.$off('play-item', this.playLibraryItem)
|
||||||
this.$eventBus.$off('pause-item', this.pauseItem)
|
this.$eventBus.$off('pause-item', this.pauseItem)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<nuxt-link :to="`/author/${author.id}`">
|
<nuxt-link :to="`/author/${author.id}?library=${currentLibraryId}`">
|
||||||
<div @mouseover="mouseover" @mouseleave="mouseleave">
|
<div @mouseover="mouseover" @mouseleave="mouseleave">
|
||||||
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
|
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
|
||||||
<!-- Image or placeholder -->
|
<!-- Image or placeholder -->
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
<p v-if="matchKey !== 'title'" class="truncate text-sm">{{ title }}</p>
|
<p v-if="matchKey !== 'title'" class="truncate text-sm">{{ title }}</p>
|
||||||
<p v-else class="truncate text-sm" v-html="matchHtml" />
|
<p v-else class="truncate text-sm" v-html="matchHtml" />
|
||||||
|
|
||||||
<p v-if="matchKey === 'subtitle'" class="truncate text-xs text-gray-300">{{ matchHtml }}</p>
|
<p v-if="matchKey === 'subtitle'" class="truncate text-xs text-gray-300" v-html="matchHtml" />
|
||||||
|
|
||||||
<p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">by {{ authorName }}</p>
|
<p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">by {{ authorName }}</p>
|
||||||
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
|
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
|
||||||
|
|
||||||
<div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin' || matchKey === 'episode'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
|
<div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin' || matchKey === 'episode' || matchKey === 'narrators'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -61,18 +61,19 @@ export default {
|
|||||||
},
|
},
|
||||||
matchHtml() {
|
matchHtml() {
|
||||||
if (!this.matchText || !this.search) return ''
|
if (!this.matchText || !this.search) return ''
|
||||||
if (this.matchKey === 'subtitle') return ''
|
|
||||||
|
|
||||||
// This used to highlight the part of the search found
|
// This used to highlight the part of the search found
|
||||||
// but with removing commas periods etc this is no longer plausible
|
// but with removing commas periods etc this is no longer plausible
|
||||||
const html = this.matchText
|
const html = this.matchText
|
||||||
|
|
||||||
if (this.matchKey === 'episode') return `<p class="truncate">Episode: ${html}</p>`
|
if (this.matchKey === 'episode') return `<p class="truncate">${this.$strings.LabelEpisode}: ${html}</p>`
|
||||||
if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>`
|
if (this.matchKey === 'tags') return `<p class="truncate">${this.$strings.LabelTags}: ${html}</p>`
|
||||||
|
if (this.matchKey === 'subtitle') return `<p class="truncate">${html}</p>`
|
||||||
if (this.matchKey === 'authors') return `by ${html}`
|
if (this.matchKey === 'authors') return `by ${html}`
|
||||||
if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>`
|
if (this.matchKey === 'isbn') return `<p class="truncate">ISBN: ${html}</p>`
|
||||||
if (this.matchKey === 'asin') return `<p class="truncate">ASIN: ${html}</p>`
|
if (this.matchKey === 'asin') return `<p class="truncate">ASIN: ${html}</p>`
|
||||||
if (this.matchKey === 'series') return `<p class="truncate">Series: ${html}</p>`
|
if (this.matchKey === 'series') return `<p class="truncate">${this.$strings.LabelSeries}: ${html}</p>`
|
||||||
|
if (this.matchKey === 'narrators') return `<p class="truncate">${this.$strings.LabelNarrator}: ${html}</p>`
|
||||||
return `${html}`
|
return `${html}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex items-center h-full px-1 overflow-hidden">
|
<div class="flex items-center px-1 overflow-hidden">
|
||||||
<div class="h-5 w-5 min-w-5 text-lg mr-1.5 flex items-center justify-center">
|
<div class="w-8 flex items-center justify-center">
|
||||||
<span v-if="isFinished" :class="taskIconStatus" class="material-icons text-base">{{actionIcon}}</span>
|
<!-- <div class="text-lg"> -->
|
||||||
|
<span v-if="isFinished" :class="taskIconStatus" class="material-icons text-base">{{ actionIcon }}</span>
|
||||||
<widgets-loading-spinner v-else />
|
<widgets-loading-spinner v-else />
|
||||||
|
<!-- </div> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-2 taskRunningCardContent">
|
<div class="flex-grow px-2 taskRunningCardContent">
|
||||||
<p class="truncate text-sm">{{ title }}</p>
|
<p class="truncate text-sm">{{ title }}</p>
|
||||||
@@ -36,10 +38,13 @@ export default {
|
|||||||
return this.task.details || 'Unknown'
|
return this.task.details || 'Unknown'
|
||||||
},
|
},
|
||||||
isFinished() {
|
isFinished() {
|
||||||
return this.task.isFinished || false
|
return !!this.task.isFinished
|
||||||
},
|
},
|
||||||
isFailed() {
|
isFailed() {
|
||||||
return this.task.isFailed || false
|
return !!this.task.isFailed
|
||||||
|
},
|
||||||
|
isSuccess() {
|
||||||
|
return this.isFinished && !this.isFailed
|
||||||
},
|
},
|
||||||
failedMessage() {
|
failedMessage() {
|
||||||
return this.task.error || ''
|
return this.task.error || ''
|
||||||
@@ -48,6 +53,11 @@ export default {
|
|||||||
return this.task.action || ''
|
return this.task.action || ''
|
||||||
},
|
},
|
||||||
actionIcon() {
|
actionIcon() {
|
||||||
|
if (this.isFailed) {
|
||||||
|
return 'error'
|
||||||
|
} else if (this.isSuccess) {
|
||||||
|
return 'done'
|
||||||
|
}
|
||||||
switch (this.action) {
|
switch (this.action) {
|
||||||
case 'download-podcast-episode':
|
case 'download-podcast-episode':
|
||||||
return 'cloud_download'
|
return 'cloud_download'
|
||||||
@@ -68,16 +78,15 @@ export default {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {},
|
||||||
},
|
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.taskRunningCardContent {
|
.taskRunningCardContent {
|
||||||
width: calc(100% - 80px);
|
width: calc(100% - 84px);
|
||||||
height: 75px;
|
height: 60px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ export default {
|
|||||||
var files = this.item.itemFiles.concat(this.item.otherFiles)
|
var files = this.item.itemFiles.concat(this.item.otherFiles)
|
||||||
return {
|
return {
|
||||||
index: this.item.index,
|
index: this.item.index,
|
||||||
|
directory: this.directory,
|
||||||
...this.itemData,
|
...this.itemData,
|
||||||
files
|
files
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,10 @@
|
|||||||
<div ref="moreIcon" v-show="!isSelectionMode && moreMenuItems.length" class="hidden md:block absolute cursor-pointer hover:text-yellow-300 300 hover:scale-125 transform duration-150" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
|
<div ref="moreIcon" v-show="!isSelectionMode && moreMenuItems.length" class="hidden md:block absolute cursor-pointer hover:text-yellow-300 300 hover:scale-125 transform duration-150" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem' }" @click.stop.prevent="clickShowMore">
|
||||||
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
|
<span class="material-icons" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">more_vert</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="ebookFormat" class="absolute" :style="{ bottom: 0.375 * sizeMultiplier + 'rem', left: 0.375 * sizeMultiplier + 'rem' }">
|
||||||
|
<span class="text-white/80" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ ebookFormat }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Processing/loading spinner overlay -->
|
<!-- Processing/loading spinner overlay -->
|
||||||
@@ -221,7 +225,7 @@ export default {
|
|||||||
libraryId() {
|
libraryId() {
|
||||||
return this._libraryItem.libraryId
|
return this._libraryItem.libraryId
|
||||||
},
|
},
|
||||||
hasEbook() {
|
ebookFormat() {
|
||||||
return this.media.ebookFormat
|
return this.media.ebookFormat
|
||||||
},
|
},
|
||||||
numTracks() {
|
numTracks() {
|
||||||
@@ -252,14 +256,14 @@ export default {
|
|||||||
},
|
},
|
||||||
booksInSeries() {
|
booksInSeries() {
|
||||||
// Only added to item object when collapseSeries is enabled
|
// Only added to item object when collapseSeries is enabled
|
||||||
return this.collapsedSeries ? this.collapsedSeries.numBooks : 0
|
return this.collapsedSeries?.numBooks || 0
|
||||||
},
|
},
|
||||||
seriesSequenceList() {
|
seriesSequenceList() {
|
||||||
return this.collapsedSeries ? this.collapsedSeries.seriesSequenceList : null
|
return this.collapsedSeries?.seriesSequenceList || null
|
||||||
},
|
},
|
||||||
libraryItemIdsInSeries() {
|
libraryItemIdsInSeries() {
|
||||||
// Only added to item object when collapseSeries is enabled
|
// Only added to item object when collapseSeries is enabled
|
||||||
return this.collapsedSeries ? this.collapsedSeries.libraryItemIds || [] : []
|
return this.collapsedSeries?.libraryItemIds || []
|
||||||
},
|
},
|
||||||
hasCover() {
|
hasCover() {
|
||||||
return !!this.media.coverPath
|
return !!this.media.coverPath
|
||||||
@@ -325,6 +329,9 @@ export default {
|
|||||||
if (this.episodeProgress) return this.episodeProgress
|
if (this.episodeProgress) return this.episodeProgress
|
||||||
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
|
isEBookOnly() {
|
||||||
|
return !this.numTracks && this.ebookFormat
|
||||||
|
},
|
||||||
useEBookProgress() {
|
useEBookProgress() {
|
||||||
if (!this.userProgress || this.userProgress.progress) return false
|
if (!this.userProgress || this.userProgress.progress) return false
|
||||||
return this.userProgress.ebookProgress > 0
|
return this.userProgress.ebookProgress > 0
|
||||||
@@ -360,13 +367,13 @@ export default {
|
|||||||
return this.store.getters['getIsStreamingFromDifferentLibrary']
|
return this.store.getters['getIsStreamingFromDifferentLibrary']
|
||||||
},
|
},
|
||||||
showReadButton() {
|
showReadButton() {
|
||||||
return !this.isSelectionMode && !this.showPlayButton && this.hasEbook && (this.showExperimentalFeatures || this.enableEReader)
|
return !this.isSelectionMode && !this.showPlayButton && this.ebookFormat && (this.showExperimentalFeatures || this.enableEReader)
|
||||||
},
|
},
|
||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode || this.isMusic)
|
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode || this.isMusic)
|
||||||
},
|
},
|
||||||
showSmallEBookIcon() {
|
showSmallEBookIcon() {
|
||||||
return !this.isSelectionMode && this.hasEbook && (this.showExperimentalFeatures || this.enableEReader)
|
return !this.isSelectionMode && this.ebookFormat && (this.showExperimentalFeatures || this.enableEReader)
|
||||||
},
|
},
|
||||||
isMissing() {
|
isMissing() {
|
||||||
return this._libraryItem.isMissing
|
return this._libraryItem.isMissing
|
||||||
@@ -441,6 +448,7 @@ export default {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
if (this.continueListeningShelf) {
|
if (this.continueListeningShelf) {
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
func: 'removeFromContinueListening',
|
func: 'removeFromContinueListening',
|
||||||
text: this.$strings.ButtonRemoveFromContinueListening
|
text: this.$strings.ButtonRemoveFromContinueListening
|
||||||
@@ -508,7 +516,7 @@ export default {
|
|||||||
if (this.continueListeningShelf) {
|
if (this.continueListeningShelf) {
|
||||||
items.push({
|
items.push({
|
||||||
func: 'removeFromContinueListening',
|
func: 'removeFromContinueListening',
|
||||||
text: this.$strings.ButtonRemoveFromContinueListening
|
text: this.isEBookOnly ? this.$strings.ButtonRemoveFromContinueReading : this.$strings.ButtonRemoveFromContinueListening
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (!this.isPodcast) {
|
if (!this.isPodcast) {
|
||||||
@@ -865,7 +873,8 @@ export default {
|
|||||||
this.createMoreMenu()
|
this.createMoreMenu()
|
||||||
},
|
},
|
||||||
async clickReadEBook() {
|
async clickReadEBook() {
|
||||||
var libraryItem = await this.$axios.$get(`/api/items/${this.libraryItemId}?expanded=1`).catch((error) => {
|
const axios = this.$axios || this.$nuxt.$axios
|
||||||
|
var libraryItem = await axios.$get(`/api/items/${this.libraryItemId}?expanded=1`).catch((error) => {
|
||||||
console.error('Failed to get lirbary item', this.libraryItemId)
|
console.error('Failed to get lirbary item', this.libraryItemId)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
<div class="absolute z-10 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ books.length }}</div>
|
<div class="absolute z-10 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ books.length }}</div>
|
||||||
|
|
||||||
<div v-if="isSeriesFinished" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b bg-success w-full" />
|
<div v-if="seriesPercentInProgress > 0" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b w-full" :class="isSeriesFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: seriesPercentInProgress * 100 + '%' }" />
|
||||||
|
|
||||||
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
<div v-if="hasValidCovers" class="bg-black bg-opacity-60 absolute top-0 left-0 w-full h-full flex items-center justify-center text-center transition-opacity" :class="isHovering ? '' : 'opacity-0'" :style="{ padding: `${sizeMultiplier}rem` }">
|
||||||
<p :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
<p :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
||||||
@@ -85,13 +85,13 @@ export default {
|
|||||||
case 'addedAt':
|
case 'addedAt':
|
||||||
return `${this.$strings.LabelAdded} ${this.$formatDate(this.addedAt, this.dateFormat)}`
|
return `${this.$strings.LabelAdded} ${this.$formatDate(this.addedAt, this.dateFormat)}`
|
||||||
case 'totalDuration':
|
case 'totalDuration':
|
||||||
return `${this.$strings.LabelDuration} ${this.$elapsedPrettyExtended(this.totalDuration, false)}`
|
return `${this.$strings.LabelDuration} ${this.$elapsedPrettyExtended(this.totalDuration, false)}`
|
||||||
case 'lastBookUpdated':
|
case 'lastBookUpdated':
|
||||||
const lastUpdated = Math.max(...(this.books).map(x => x.updatedAt), 0)
|
const lastUpdated = Math.max(...this.books.map((x) => x.updatedAt), 0)
|
||||||
return `${this.$strings.LabelLastBookUpdated} ${this.$formatDate(lastUpdated, this.dateFormat)}`
|
return `${this.$strings.LabelLastBookUpdated} ${this.$formatDate(lastUpdated, this.dateFormat)}`
|
||||||
case 'lastBookAdded':
|
case 'lastBookAdded':
|
||||||
const lastBookAdded = Math.max(...(this.books).map(x => x.addedAt), 0)
|
const lastBookAdded = Math.max(...this.books.map((x) => x.addedAt), 0)
|
||||||
return `${this.$strings.LabelLastBookAdded} ${this.$formatDate(lastBookAdded, this.dateFormat)}`
|
return `${this.$strings.LabelLastBookAdded} ${this.$formatDate(lastBookAdded, this.dateFormat)}`
|
||||||
default:
|
default:
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -115,6 +115,14 @@ export default {
|
|||||||
seriesBooksFinished() {
|
seriesBooksFinished() {
|
||||||
return this.seriesBookProgress.filter((p) => p.isFinished)
|
return this.seriesBookProgress.filter((p) => p.isFinished)
|
||||||
},
|
},
|
||||||
|
hasSeriesBookInProgress() {
|
||||||
|
return this.seriesBookProgress.some((p) => !p.isFinished && p.progress > 0)
|
||||||
|
},
|
||||||
|
seriesPercentInProgress() {
|
||||||
|
let totalFinishedAndInProgress = this.seriesBooksFinished.length
|
||||||
|
if (this.hasSeriesBookInProgress) totalFinishedAndInProgress += 1
|
||||||
|
return Math.min(1, Math.max(0, totalFinishedAndInProgress / this.books.length))
|
||||||
|
},
|
||||||
isSeriesFinished() {
|
isSeriesFinished() {
|
||||||
return this.books.length === this.seriesBooksFinished.length
|
return this.books.length === this.seriesBooksFinished.length
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(narrator.name)}`">
|
||||||
|
<div :style="{ width: width + 'px', height: height + 'px' }" class="bg-primary box-shadow-book rounded-md relative overflow-hidden">
|
||||||
|
<div class="absolute inset-0 w-full h-full flex items-center justify-center pointer-events-none opacity-40">
|
||||||
|
<span class="material-icons-outlined text-[10rem]">record_voice_over</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Narrator name & num books overlay -->
|
||||||
|
<div class="absolute bottom-0 left-0 w-full py-1 bg-black bg-opacity-60 px-2">
|
||||||
|
<p class="text-center font-semibold truncate" :style="{ fontSize: sizeMultiplier * 0.75 + 'rem' }">{{ name }}</p>
|
||||||
|
<p class="text-center text-gray-200" :style="{ fontSize: sizeMultiplier * 0.65 + 'rem' }">{{ numBooks }} Book{{ numBooks === 1 ? '' : 's' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nuxt-link>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
narrator: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
width: Number,
|
||||||
|
height: Number,
|
||||||
|
sizeMultiplier: {
|
||||||
|
type: Number,
|
||||||
|
default: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
name() {
|
||||||
|
return this.narrator?.name || ''
|
||||||
|
},
|
||||||
|
numBooks() {
|
||||||
|
return this.narrator?.books?.length || 0
|
||||||
|
},
|
||||||
|
userCanUpdate() {
|
||||||
|
return this.$store.getters['user/getUserCanUpdate']
|
||||||
|
},
|
||||||
|
currentLibraryId() {
|
||||||
|
return this.$store.state.libraries.currentLibraryId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex h-full px-1 overflow-hidden">
|
||||||
|
<div class="w-10 h-10 flex items-center justify-center">
|
||||||
|
<span class="material-icons text-2xl text-gray-200">record_voice_over</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow px-2 narratorSearchCardContent h-full">
|
||||||
|
<p class="truncate text-sm">{{ narrator }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
narrator: String
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.narratorSearchCardContent {
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
height: 40px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="narrators?.length" class="flex py-0.5 mt-4">
|
||||||
|
<div class="w-32">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelNarrators }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||||
|
<template v-for="(narrator, index) in narrators">
|
||||||
|
<nuxt-link :key="narrator" :to="`/library/${libraryId}/bookshelf?filter=narrators.${$encode(narrator)}`" class="hover:underline">{{ narrator }}</nuxt-link
|
||||||
|
><span :key="index" v-if="index < narrators.length - 1">, </span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="publishedYear" class="flex py-0.5">
|
||||||
|
<div class="w-32">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublishYear }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ publishedYear }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="publisher" class="flex py-0.5">
|
||||||
|
<div class="w-32">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublisher }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ publisher }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="musicAlbum" class="flex py-0.5">
|
||||||
|
<div class="w-32">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">Album</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ musicAlbum }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="musicAlbumArtist" class="flex py-0.5">
|
||||||
|
<div class="w-32">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">Album Artist</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ musicAlbumArtist }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="musicTrackPretty" class="flex py-0.5">
|
||||||
|
<div class="w-32">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">Track</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ musicTrackPretty }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="musicDiscPretty" class="flex py-0.5">
|
||||||
|
<div class="w-32">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">Disc</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ musicDiscPretty }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="podcastType" class="flex py-0.5">
|
||||||
|
<div class="w-32">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPodcastType }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="capitalize">
|
||||||
|
{{ podcastType }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex py-0.5" v-if="genres.length">
|
||||||
|
<div class="w-32">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelGenres }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||||
|
<template v-for="(genre, index) in genres">
|
||||||
|
<nuxt-link :key="genre" :to="`/library/${libraryId}/bookshelf?filter=genres.${$encode(genre)}`" class="hover:underline">{{ genre }}</nuxt-link
|
||||||
|
><span :key="index" v-if="index < genres.length - 1">, </span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex py-0.5" v-if="tags.length">
|
||||||
|
<div class="w-32">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelTags }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||||
|
<template v-for="(tag, index) in tags">
|
||||||
|
<nuxt-link :key="tag" :to="`/library/${libraryId}/bookshelf?filter=tags.${$encode(tag)}`" class="hover:underline">{{ tag }}</nuxt-link
|
||||||
|
><span :key="index" v-if="index < tags.length - 1">, </span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="tracks.length || audioFile || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
|
||||||
|
<div class="w-32">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ durationPretty }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex py-0.5">
|
||||||
|
<div class="w-32">
|
||||||
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelSize }}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ sizePretty }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
libraryItem: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
libraryId() {
|
||||||
|
return this.libraryItem.libraryId
|
||||||
|
},
|
||||||
|
isPodcast() {
|
||||||
|
return this.libraryItem.mediaType === 'podcast'
|
||||||
|
},
|
||||||
|
audioFile() {
|
||||||
|
// Music track
|
||||||
|
return this.media.audioFile
|
||||||
|
},
|
||||||
|
media() {
|
||||||
|
return this.libraryItem.media || {}
|
||||||
|
},
|
||||||
|
tracks() {
|
||||||
|
return this.media.tracks || []
|
||||||
|
},
|
||||||
|
podcastEpisodes() {
|
||||||
|
return this.media.episodes || []
|
||||||
|
},
|
||||||
|
mediaMetadata() {
|
||||||
|
return this.media.metadata || {}
|
||||||
|
},
|
||||||
|
publishedYear() {
|
||||||
|
return this.mediaMetadata.publishedYear
|
||||||
|
},
|
||||||
|
genres() {
|
||||||
|
return this.mediaMetadata.genres || []
|
||||||
|
},
|
||||||
|
tags() {
|
||||||
|
return this.media.tags || []
|
||||||
|
},
|
||||||
|
podcastAuthor() {
|
||||||
|
return this.mediaMetadata.author || ''
|
||||||
|
},
|
||||||
|
authors() {
|
||||||
|
return this.mediaMetadata.authors || []
|
||||||
|
},
|
||||||
|
publisher() {
|
||||||
|
return this.mediaMetadata.publisher || ''
|
||||||
|
},
|
||||||
|
musicArtists() {
|
||||||
|
return this.mediaMetadata.artists || []
|
||||||
|
},
|
||||||
|
musicAlbum() {
|
||||||
|
return this.mediaMetadata.album || ''
|
||||||
|
},
|
||||||
|
musicAlbumArtist() {
|
||||||
|
return this.mediaMetadata.albumArtist || ''
|
||||||
|
},
|
||||||
|
musicTrackPretty() {
|
||||||
|
if (!this.mediaMetadata.trackNumber) return null
|
||||||
|
if (!this.mediaMetadata.trackTotal) return this.mediaMetadata.trackNumber
|
||||||
|
return `${this.mediaMetadata.trackNumber} / ${this.mediaMetadata.trackTotal}`
|
||||||
|
},
|
||||||
|
musicDiscPretty() {
|
||||||
|
if (!this.mediaMetadata.discNumber) return null
|
||||||
|
if (!this.mediaMetadata.discTotal) return this.mediaMetadata.discNumber
|
||||||
|
return `${this.mediaMetadata.discNumber} / ${this.mediaMetadata.discTotal}`
|
||||||
|
},
|
||||||
|
narrators() {
|
||||||
|
return this.mediaMetadata.narrators || []
|
||||||
|
},
|
||||||
|
durationPretty() {
|
||||||
|
if (this.isPodcast) return this.$elapsedPrettyExtended(this.totalPodcastDuration)
|
||||||
|
|
||||||
|
if (!this.tracks.length && !this.audioFile) return 'N/A'
|
||||||
|
if (this.audioFile) return this.$elapsedPrettyExtended(this.duration)
|
||||||
|
return this.$elapsedPretty(this.duration)
|
||||||
|
},
|
||||||
|
duration() {
|
||||||
|
if (!this.tracks.length && !this.audioFile) return 0
|
||||||
|
return this.media.duration
|
||||||
|
},
|
||||||
|
totalPodcastDuration() {
|
||||||
|
if (!this.podcastEpisodes.length) return 0
|
||||||
|
let totalDuration = 0
|
||||||
|
this.podcastEpisodes.forEach((ep) => (totalDuration += ep.duration || 0))
|
||||||
|
return totalDuration
|
||||||
|
},
|
||||||
|
sizePretty() {
|
||||||
|
return this.$bytesPretty(this.media.size)
|
||||||
|
},
|
||||||
|
podcastType() {
|
||||||
|
return this.mediaMetadata.type
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -63,6 +63,15 @@
|
|||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<p v-if="narratorResults.length" class="uppercase text-xs text-gray-400 mb-1 mt-3 px-1 font-semibold">{{ $strings.LabelNarrators }}</p>
|
||||||
|
<template v-for="narrator in narratorResults">
|
||||||
|
<li :key="narrator.name" class="text-gray-50 select-none relative cursor-pointer hover:bg-black-400 py-1" role="option" @click="clickOption">
|
||||||
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(narrator.name)}`">
|
||||||
|
<cards-narrator-search-card :narrator="narrator.name" />
|
||||||
|
</nuxt-link>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -84,6 +93,7 @@ export default {
|
|||||||
authorResults: [],
|
authorResults: [],
|
||||||
seriesResults: [],
|
seriesResults: [],
|
||||||
tagResults: [],
|
tagResults: [],
|
||||||
|
narratorResults: [],
|
||||||
searchTimeout: null,
|
searchTimeout: null,
|
||||||
lastSearch: null
|
lastSearch: null
|
||||||
}
|
}
|
||||||
@@ -114,6 +124,7 @@ export default {
|
|||||||
this.authorResults = []
|
this.authorResults = []
|
||||||
this.seriesResults = []
|
this.seriesResults = []
|
||||||
this.tagResults = []
|
this.tagResults = []
|
||||||
|
this.narratorResults = []
|
||||||
this.showMenu = false
|
this.showMenu = false
|
||||||
this.isFetching = false
|
this.isFetching = false
|
||||||
this.isTyping = false
|
this.isTyping = false
|
||||||
@@ -142,7 +153,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.isFetching = true
|
this.isFetching = true
|
||||||
|
|
||||||
var searchResults = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/search?q=${value}&limit=3`).catch((error) => {
|
const searchResults = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/search?q=${value}&limit=3`).catch((error) => {
|
||||||
console.error('Search error', error)
|
console.error('Search error', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
@@ -155,6 +166,7 @@ export default {
|
|||||||
this.authorResults = searchResults.authors || []
|
this.authorResults = searchResults.authors || []
|
||||||
this.seriesResults = searchResults.series || []
|
this.seriesResults = searchResults.series || []
|
||||||
this.tagResults = searchResults.tags || []
|
this.tagResults = searchResults.tags || []
|
||||||
|
this.narratorResults = searchResults.narrators || []
|
||||||
|
|
||||||
this.isFetching = false
|
this.isFetching = false
|
||||||
if (!this.showMenu) {
|
if (!this.showMenu) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
|
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 overflow-y-auto overflow-x-hidden" style="min-height: 400px; max-height: 80vh">
|
||||||
<div class="w-full p-8">
|
<div class="w-full p-8">
|
||||||
<div class="flex py-2">
|
<div class="flex py-2">
|
||||||
<div class="w-1/2 px-2">
|
<div class="w-1/2 px-2">
|
||||||
@@ -96,7 +96,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!newUser.permissions.accessAllTags" class="my-4">
|
<div v-if="!newUser.permissions.accessAllTags" class="my-4">
|
||||||
<ui-multi-select-dropdown v-model="newUser.itemTagsAccessible" :items="itemTags" :label="$strings.LabelTagsAccessibleToUser" />
|
<div class="flex items-center">
|
||||||
|
<ui-multi-select-dropdown v-model="newUser.itemTagsSelected" :items="itemTags" :label="tagsSelectionText" />
|
||||||
|
<div class="flex items-center pt-4 px-2">
|
||||||
|
<p class="px-3 font-semibold" id="selected-tags-not-accessible--permissions-toggle">{{ $strings.LabelInvert }}</p>
|
||||||
|
<ui-toggle-switch labeledBy="selected-tags-not-accessible--permissions-toggle" v-model="newUser.permissions.selectedTagsNotAccessible" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -185,6 +192,9 @@ export default {
|
|||||||
value: t
|
value: t
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
tagsSelectionText() {
|
||||||
|
return this.newUser.permissions.selectedTagsNotAccessible ? this.$strings.LabelTagsNotAccessibleToUser : this.$strings.LabelTagsAccessibleToUser
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -193,8 +203,11 @@ export default {
|
|||||||
if (this.$refs.modal) this.$refs.modal.setHide()
|
if (this.$refs.modal) this.$refs.modal.setHide()
|
||||||
},
|
},
|
||||||
accessAllTagsToggled(val) {
|
accessAllTagsToggled(val) {
|
||||||
if (val && this.newUser.itemTagsAccessible.length) {
|
if (val) {
|
||||||
this.newUser.itemTagsAccessible = []
|
if (this.newUser.itemTagsSelected?.length) {
|
||||||
|
this.newUser.itemTagsSelected = []
|
||||||
|
}
|
||||||
|
this.newUser.permissions.selectedTagsNotAccessible = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fetchAllTags() {
|
fetchAllTags() {
|
||||||
@@ -226,7 +239,7 @@ export default {
|
|||||||
this.$toast.error('Must select at least one library')
|
this.$toast.error('Must select at least one library')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (!this.newUser.permissions.accessAllTags && !this.newUser.itemTagsAccessible.length) {
|
if (!this.newUser.permissions.accessAllTags && !this.newUser.itemTagsSelected.length) {
|
||||||
this.$toast.error('Must select at least one tag')
|
this.$toast.error('Must select at least one tag')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -307,12 +320,12 @@ export default {
|
|||||||
delete: type === 'admin',
|
delete: type === 'admin',
|
||||||
upload: type === 'admin',
|
upload: type === 'admin',
|
||||||
accessAllLibraries: true,
|
accessAllLibraries: true,
|
||||||
accessAllTags: true
|
accessAllTags: true,
|
||||||
|
selectedTagsNotAccessible: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
this.fetchAllTags()
|
this.fetchAllTags()
|
||||||
|
|
||||||
this.isNew = !this.account
|
this.isNew = !this.account
|
||||||
if (this.account) {
|
if (this.account) {
|
||||||
this.newUser = {
|
this.newUser = {
|
||||||
@@ -322,9 +335,10 @@ export default {
|
|||||||
isActive: this.account.isActive,
|
isActive: this.account.isActive,
|
||||||
permissions: { ...this.account.permissions },
|
permissions: { ...this.account.permissions },
|
||||||
librariesAccessible: [...(this.account.librariesAccessible || [])],
|
librariesAccessible: [...(this.account.librariesAccessible || [])],
|
||||||
itemTagsAccessible: [...(this.account.itemTagsAccessible || [])]
|
itemTagsSelected: [...(this.account.itemTagsSelected || [])]
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
this.fetchAllTags()
|
||||||
this.newUser = {
|
this.newUser = {
|
||||||
username: null,
|
username: null,
|
||||||
password: null,
|
password: null,
|
||||||
@@ -336,7 +350,8 @@ export default {
|
|||||||
delete: false,
|
delete: false,
|
||||||
upload: false,
|
upload: false,
|
||||||
accessAllLibraries: true,
|
accessAllLibraries: true,
|
||||||
accessAllTags: true
|
accessAllTags: true,
|
||||||
|
selectedTagsNotAccessible: false
|
||||||
},
|
},
|
||||||
librariesAccessible: []
|
librariesAccessible: []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<form class="flex items-center justify-center px-6 py-3" @submit.prevent="submitCustomTime">
|
<form class="flex items-center justify-center px-6 py-3" @submit.prevent="submitCustomTime">
|
||||||
<ui-text-input v-model="customTime" type="number" step="any" min="0.1" placeholder="Time in minutes" class="w-48" />
|
<ui-text-input v-model="customTime" type="number" step="any" min="0.1" placeholder="Time in minutes" class="w-48" />
|
||||||
<ui-btn color="success" type="submit" padding-x="0" class="h-9 w-12 flex items-center justify-center ml-1">Set</ui-btn>
|
<ui-btn color="success" type="submit" :padding-x="0" class="h-9 w-12 flex items-center justify-center ml-1">Set</ui-btn>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-full p-4">
|
<div v-else class="w-full p-4">
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ export default {
|
|||||||
this.$store.commit('setEditModalTab', val)
|
this.$store.commit('setEditModalTab', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
height() {
|
||||||
|
return Math.min(this.availableHeight, 650)
|
||||||
|
},
|
||||||
tabs() {
|
tabs() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -136,6 +139,18 @@ export default {
|
|||||||
userIsAdminOrUp() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsAdminOrUp']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
|
selectedLibraryItem() {
|
||||||
|
return this.$store.state.selectedLibraryItem || {}
|
||||||
|
},
|
||||||
|
selectedLibraryItemId() {
|
||||||
|
return this.selectedLibraryItem.id
|
||||||
|
},
|
||||||
|
media() {
|
||||||
|
return this.libraryItem?.media || {}
|
||||||
|
},
|
||||||
|
mediaMetadata() {
|
||||||
|
return this.media.metadata || {}
|
||||||
|
},
|
||||||
availableTabs() {
|
availableTabs() {
|
||||||
if (!this.userCanUpdate && !this.userCanDownload) return []
|
if (!this.userCanUpdate && !this.userCanDownload) return []
|
||||||
return this.tabs.filter((tab) => {
|
return this.tabs.filter((tab) => {
|
||||||
@@ -144,6 +159,7 @@ export default {
|
|||||||
if (tab.admin && !this.userIsAdminOrUp) return false
|
if (tab.admin && !this.userIsAdminOrUp) return false
|
||||||
|
|
||||||
if (tab.id === 'tools' && this.isMissing) return false
|
if (tab.id === 'tools' && this.isMissing) return false
|
||||||
|
if (tab.id === 'chapters' && this.isEBookOnly) return false
|
||||||
|
|
||||||
if ((tab.id === 'tools' || tab.id === 'files') && this.userCanDownload) return true
|
if ((tab.id === 'tools' || tab.id === 'files') && this.userCanDownload) return true
|
||||||
if (tab.id !== 'tools' && tab.id !== 'files' && this.userCanUpdate) return true
|
if (tab.id !== 'tools' && tab.id !== 'files' && this.userCanUpdate) return true
|
||||||
@@ -151,9 +167,6 @@ export default {
|
|||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
height() {
|
|
||||||
return Math.min(this.availableHeight, 650)
|
|
||||||
},
|
|
||||||
tabName() {
|
tabName() {
|
||||||
var _tab = this.tabs.find((t) => t.id === this.selectedTab)
|
var _tab = this.tabs.find((t) => t.id === this.selectedTab)
|
||||||
return _tab ? _tab.component : ''
|
return _tab ? _tab.component : ''
|
||||||
@@ -161,20 +174,11 @@ export default {
|
|||||||
isMissing() {
|
isMissing() {
|
||||||
return this.selectedLibraryItem.isMissing
|
return this.selectedLibraryItem.isMissing
|
||||||
},
|
},
|
||||||
selectedLibraryItem() {
|
isEBookOnly() {
|
||||||
return this.$store.state.selectedLibraryItem || {}
|
return this.media.ebookFile && !this.media.tracks?.length
|
||||||
},
|
|
||||||
selectedLibraryItemId() {
|
|
||||||
return this.selectedLibraryItem.id
|
|
||||||
},
|
|
||||||
media() {
|
|
||||||
return this.libraryItem ? this.libraryItem.media || {} : {}
|
|
||||||
},
|
|
||||||
mediaMetadata() {
|
|
||||||
return this.media.metadata || {}
|
|
||||||
},
|
},
|
||||||
mediaType() {
|
mediaType() {
|
||||||
return this.libraryItem ? this.libraryItem.mediaType : null
|
return this.libraryItem?.mediaType || null
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.mediaMetadata.title || 'No Title'
|
return this.mediaMetadata.title || 'No Title'
|
||||||
|
|||||||
@@ -17,10 +17,10 @@
|
|||||||
<div class="flex-grow sm:pl-2 md:pl-6 sm:pr-2 mt-2 md:mt-0">
|
<div class="flex-grow sm:pl-2 md:pl-6 sm:pr-2 mt-2 md:mt-0">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div v-if="userCanUpload" class="w-10 md:w-40 pr-2 pt-4 md:min-w-32">
|
<div v-if="userCanUpload" class="w-10 md:w-40 pr-2 pt-4 md:min-w-32">
|
||||||
<ui-file-input ref="fileInput" @change="fileUploadSelected"
|
<ui-file-input ref="fileInput" @change="fileUploadSelected">
|
||||||
><span class="hidden md:inline-block">{{ $strings.ButtonUploadCover }}</span
|
<span class="hidden md:inline-block">{{ $strings.ButtonUploadCover }}</span>
|
||||||
><span class="material-icons text-2xl inline-block md:!hidden">upload</span></ui-file-input
|
<span class="material-icons text-2xl inline-block md:!hidden">upload</span>
|
||||||
>
|
</ui-file-input>
|
||||||
</div>
|
</div>
|
||||||
<form @submit.prevent="submitForm" class="flex flex-grow">
|
<form @submit.prevent="submitForm" class="flex flex-grow">
|
||||||
<ui-text-input-with-label v-model="imageUrl" :label="$strings.LabelCoverImageURL" />
|
<ui-text-input-with-label v-model="imageUrl" :label="$strings.LabelCoverImageURL" />
|
||||||
@@ -49,13 +49,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<form @submit.prevent="submitSearchForm">
|
<form @submit.prevent="submitSearchForm">
|
||||||
<div class="flex items-center justify-start -mx-1 h-20">
|
<div class="flex items-center justify-start -mx-1 h-20">
|
||||||
<div class="w-40 px-1">
|
<div class="w-48 px-1">
|
||||||
<ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelProvider" small />
|
<ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelProvider" small />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-72 px-1">
|
<div class="w-72 px-1">
|
||||||
<ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" :placeholder="$strings.PlaceholderSearch" />
|
<ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" :placeholder="$strings.PlaceholderSearch" />
|
||||||
</div>
|
</div>
|
||||||
<div v-show="provider != 'itunes'" class="w-72 px-1">
|
<div v-show="provider != 'itunes' && provider != 'audiobookcovers'" class="w-72 px-1">
|
||||||
<ui-text-input-with-label v-model="searchAuthor" :label="$strings.LabelAuthor" />
|
<ui-text-input-with-label v-model="searchAuthor" :label="$strings.LabelAuthor" />
|
||||||
</div>
|
</div>
|
||||||
<ui-btn class="mt-5 ml-1" type="submit">{{ $strings.ButtonSearch }}</ui-btn>
|
<ui-btn class="mt-5 ml-1" type="submit">{{ $strings.ButtonSearch }}</ui-btn>
|
||||||
@@ -128,7 +128,7 @@ export default {
|
|||||||
},
|
},
|
||||||
providers() {
|
providers() {
|
||||||
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
|
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
|
||||||
return this.$store.state.scanners.providers
|
return [{ text: 'All', value: 'all' }, ...this.$store.state.scanners.providers, ...this.$store.state.scanners.coverOnlyProviders]
|
||||||
},
|
},
|
||||||
searchTitleLabel() {
|
searchTitleLabel() {
|
||||||
if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN
|
if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN
|
||||||
@@ -223,7 +223,7 @@ export default {
|
|||||||
this.searchTitle = this.mediaMetadata.title || ''
|
this.searchTitle = this.mediaMetadata.title || ''
|
||||||
this.searchAuthor = this.mediaMetadata.authorName || ''
|
this.searchAuthor = this.mediaMetadata.authorName || ''
|
||||||
if (this.isPodcast) this.provider = 'itunes'
|
if (this.isPodcast) this.provider = 'itunes'
|
||||||
else this.provider = localStorage.getItem('book-provider') || 'google'
|
else this.provider = localStorage.getItem('book-cover-provider') || localStorage.getItem('book-provider') || 'google'
|
||||||
},
|
},
|
||||||
removeCover() {
|
removeCover() {
|
||||||
if (!this.media.coverPath) {
|
if (!this.media.coverPath) {
|
||||||
@@ -288,13 +288,13 @@ export default {
|
|||||||
},
|
},
|
||||||
getSearchQuery() {
|
getSearchQuery() {
|
||||||
var searchQuery = `provider=${this.provider}&title=${this.searchTitle}`
|
var searchQuery = `provider=${this.provider}&title=${this.searchTitle}`
|
||||||
if (this.searchAuthor) searchQuery += `&author=${this.searchAuthor}`
|
if (this.searchAuthor) searchQuery += `&author=${this.searchAuthor || ''}`
|
||||||
if (this.isPodcast) searchQuery += '&podcast=1'
|
if (this.isPodcast) searchQuery += '&podcast=1'
|
||||||
return searchQuery
|
return searchQuery
|
||||||
},
|
},
|
||||||
persistProvider() {
|
persistProvider() {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('book-provider', this.provider)
|
localStorage.setItem('book-cover-provider', this.provider)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('PersistProvider', error)
|
console.error('PersistProvider', error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,7 +115,7 @@
|
|||||||
<div v-if="selectedMatchOrig.genres && selectedMatchOrig.genres.length" class="flex items-center py-2">
|
<div v-if="selectedMatchOrig.genres && selectedMatchOrig.genres.length" class="flex items-center py-2">
|
||||||
<ui-checkbox v-model="selectedMatchUsage.genres" checkbox-bg="bg" @input="checkboxToggled" />
|
<ui-checkbox v-model="selectedMatchUsage.genres" checkbox-bg="bg" @input="checkboxToggled" />
|
||||||
<div class="flex-grow ml-4">
|
<div class="flex-grow ml-4">
|
||||||
<ui-multi-select v-model="selectedMatch.genres" :items="selectedMatch.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" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.genres.join(', ') }}</p>
|
<p v-if="mediaMetadata.genres" class="text-xs ml-1 text-white text-opacity-60">{{ $strings.LabelCurrently }} {{ mediaMetadata.genres.join(', ') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -300,6 +300,12 @@ export default {
|
|||||||
},
|
},
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.mediaType == 'podcast'
|
return this.mediaType == 'podcast'
|
||||||
|
},
|
||||||
|
genres() {
|
||||||
|
const filterData = this.$store.state.libraries.filterData || {}
|
||||||
|
const currentGenres = filterData.genres || []
|
||||||
|
const selectedMatchGenres = this.selectedMatch.genres || []
|
||||||
|
return [...new Set([...currentGenres ,...selectedMatchGenres])]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<div class="w-full px-3 py-5 md:p-12">
|
<div class="w-full px-3 py-5 md:p-12">
|
||||||
<ui-dropdown v-model="newNotification.eventName" :label="$strings.LabelNotificationEvent" :items="eventOptions" class="mb-4" @input="eventOptionUpdated" />
|
<ui-dropdown v-model="newNotification.eventName" :label="$strings.LabelNotificationEvent" :items="eventOptions" class="mb-4" @input="eventOptionUpdated" />
|
||||||
|
|
||||||
<ui-multi-select v-model="newNotification.urls" :label="$strings.LabelNotificationAppriseURL" class="mb-2" />
|
<ui-multi-select ref="urlsInput" v-model="newNotification.urls" :label="$strings.LabelNotificationAppriseURL" class="mb-2" />
|
||||||
|
|
||||||
<ui-text-input-with-label v-model="newNotification.titleTemplate" :label="$strings.LabelNotificationTitleTemplate" class="mb-2" />
|
<ui-text-input-with-label v-model="newNotification.titleTemplate" :label="$strings.LabelNotificationTitleTemplate" class="mb-2" />
|
||||||
|
|
||||||
@@ -103,6 +103,8 @@ export default {
|
|||||||
if (this.$refs.modal) this.$refs.modal.setHide()
|
if (this.$refs.modal) this.$refs.modal.setHide()
|
||||||
},
|
},
|
||||||
submitForm() {
|
submitForm() {
|
||||||
|
this.$refs.urlsInput?.forceBlur()
|
||||||
|
|
||||||
if (!this.newNotification.urls.length) {
|
if (!this.newNotification.urls.length) {
|
||||||
this.$toast.error('Must enter an Apprise URL')
|
this.$toast.error('Must enter an Apprise URL')
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="wrapper" id="podcast-wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
<div ref="wrapper" id="podcast-wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
||||||
<div v-if="episodes.length" class="w-full py-3 mx-auto flex">
|
<div v-if="episodesCleaned.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" />
|
||||||
</form>
|
</form>
|
||||||
@@ -16,12 +16,12 @@
|
|||||||
v-for="(episode, index) in episodesList"
|
v-for="(episode, index) in episodesList"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="relative"
|
class="relative"
|
||||||
:class="itemEpisodeMap[episode.enclosure.url?.split('?')[0]] ? 'bg-primary bg-opacity-40' : selectedEpisodes[String(index)] ? 'cursor-pointer bg-success bg-opacity-10' : index % 2 == 0 ? 'cursor-pointer bg-primary bg-opacity-25 hover:bg-opacity-40' : 'cursor-pointer bg-primary bg-opacity-5 hover:bg-opacity-25'"
|
:class="itemEpisodeMap[episode.cleanUrl] ? 'bg-primary bg-opacity-40' : selectedEpisodes[episode.cleanUrl] ? 'cursor-pointer bg-success bg-opacity-10' : index % 2 == 0 ? 'cursor-pointer bg-primary bg-opacity-25 hover:bg-opacity-40' : 'cursor-pointer bg-primary bg-opacity-5 hover:bg-opacity-25'"
|
||||||
@click="toggleSelectEpisode(index, episode)"
|
@click="toggleSelectEpisode(episode)"
|
||||||
>
|
>
|
||||||
<div class="absolute top-0 left-0 h-full flex items-center p-2">
|
<div class="absolute top-0 left-0 h-full flex items-center p-2">
|
||||||
<span v-if="itemEpisodeMap[episode.enclosure.url?.split('?')[0]]" class="material-icons text-success text-xl">download_done</span>
|
<span v-if="itemEpisodeMap[episode.cleanUrl]" class="material-icons text-success text-xl">download_done</span>
|
||||||
<ui-checkbox v-else v-model="selectedEpisodes[String(index)]" small checkbox-bg="primary" border-color="gray-600" />
|
<ui-checkbox v-else v-model="selectedEpisodes[episode.cleanUrl]" small checkbox-bg="primary" border-color="gray-600" />
|
||||||
</div>
|
</div>
|
||||||
<div class="px-8 py-2">
|
<div class="px-8 py-2">
|
||||||
<div class="flex items-center font-semibold text-gray-200">
|
<div class="flex items-center font-semibold text-gray-200">
|
||||||
@@ -63,6 +63,7 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
processing: false,
|
processing: false,
|
||||||
|
episodesCleaned: [],
|
||||||
selectedEpisodes: {},
|
selectedEpisodes: {},
|
||||||
selectAll: false,
|
selectAll: false,
|
||||||
search: null,
|
search: null,
|
||||||
@@ -92,7 +93,7 @@ export default {
|
|||||||
return this.libraryItem.media.metadata.title || 'Unknown'
|
return this.libraryItem.media.metadata.title || 'Unknown'
|
||||||
},
|
},
|
||||||
allDownloaded() {
|
allDownloaded() {
|
||||||
return !this.episodes.some((episode) => !this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]])
|
return !this.episodesCleaned.some((episode) => !this.itemEpisodeMap[episode.cleanUrl])
|
||||||
},
|
},
|
||||||
episodesSelected() {
|
episodesSelected() {
|
||||||
return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key])
|
return Object.keys(this.selectedEpisodes).filter((key) => !!this.selectedEpisodes[key])
|
||||||
@@ -113,7 +114,7 @@ export default {
|
|||||||
return map
|
return map
|
||||||
},
|
},
|
||||||
episodesList() {
|
episodesList() {
|
||||||
return this.episodes.filter((episode) => {
|
return this.episodesCleaned.filter((episode) => {
|
||||||
if (!this.searchText) return true
|
if (!this.searchText) return true
|
||||||
return (episode.title && episode.title.toLowerCase().includes(this.searchText)) || (episode.subtitle && episode.subtitle.toLowerCase().includes(this.searchText))
|
return (episode.title && episode.title.toLowerCase().includes(this.searchText)) || (episode.subtitle && episode.subtitle.toLowerCase().includes(this.searchText))
|
||||||
})
|
})
|
||||||
@@ -131,31 +132,29 @@ export default {
|
|||||||
}, 500)
|
}, 500)
|
||||||
},
|
},
|
||||||
toggleSelectAll(val) {
|
toggleSelectAll(val) {
|
||||||
for (let i = 0; i < this.episodes.length; i++) {
|
for (const episode of this.episodesCleaned) {
|
||||||
const episode = this.episodes[i]
|
if (this.itemEpisodeMap[episode.cleanUrl]) this.selectedEpisodes[episode.cleanUrl] = false
|
||||||
if (this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]]) this.selectedEpisodes[String(i)] = false
|
else this.$set(this.selectedEpisodes, episode.cleanUrl, val)
|
||||||
else this.$set(this.selectedEpisodes, String(i), val)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
checkSetIsSelectedAll() {
|
checkSetIsSelectedAll() {
|
||||||
for (let i = 0; i < this.episodes.length; i++) {
|
for (const episode of this.episodesCleaned) {
|
||||||
const episode = this.episodes[i]
|
if (!this.itemEpisodeMap[episode.cleanUrl] && !this.selectedEpisodes[episode.cleanUrl]) {
|
||||||
if (!this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]] && !this.selectedEpisodes[String(i)]) {
|
|
||||||
this.selectAll = false
|
this.selectAll = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.selectAll = true
|
this.selectAll = true
|
||||||
},
|
},
|
||||||
toggleSelectEpisode(index, episode) {
|
toggleSelectEpisode(episode) {
|
||||||
if (this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]]) return
|
if (this.itemEpisodeMap[episode.enclosure.url?.split('?')[0]]) return
|
||||||
this.$set(this.selectedEpisodes, String(index), !this.selectedEpisodes[String(index)])
|
this.$set(this.selectedEpisodes, episode.cleanUrl, !this.selectedEpisodes[episode.cleanUrl])
|
||||||
this.checkSetIsSelectedAll()
|
this.checkSetIsSelectedAll()
|
||||||
},
|
},
|
||||||
submit() {
|
submit() {
|
||||||
var episodesToDownload = []
|
var episodesToDownload = []
|
||||||
if (this.episodesSelected.length) {
|
if (this.episodesSelected.length) {
|
||||||
episodesToDownload = this.episodesSelected.map((episodeIndex) => this.episodes[Number(episodeIndex)])
|
episodesToDownload = this.episodesSelected.map((cleanUrl) => this.episodesCleaned.find((ep) => ep.cleanUrl == cleanUrl))
|
||||||
}
|
}
|
||||||
|
|
||||||
var payloadSize = JSON.stringify(episodesToDownload).length
|
var payloadSize = JSON.stringify(episodesToDownload).length
|
||||||
@@ -185,7 +184,15 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
this.episodes.sort((a, b) => (a.publishedAt < b.publishedAt ? 1 : -1))
|
this.episodesCleaned = this.episodes
|
||||||
|
.filter((ep) => ep.enclosure?.url)
|
||||||
|
.map((_ep) => {
|
||||||
|
return {
|
||||||
|
..._ep,
|
||||||
|
cleanUrl: _ep.enclosure.url.split('?')[0]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.episodesCleaned.sort((a, b) => (a.publishedAt < b.publishedAt ? 1 : -1))
|
||||||
this.selectAll = false
|
this.selectAll = false
|
||||||
this.selectedEpisodes = {}
|
this.selectedEpisodes = {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,10 @@
|
|||||||
<!-- mobile -->
|
<!-- mobile -->
|
||||||
<ui-btn @click="saveAndClose" class="mx-2 md:hidden">{{ $strings.ButtonSave }}</ui-btn>
|
<ui-btn @click="saveAndClose" class="mx-2 md:hidden">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="enclosureUrl" class="py-4">
|
<div v-if="enclosureUrl" class="pb-4 pt-6">
|
||||||
<p class="text-xs text-gray-300 font-semibold">Episode URL from RSS feed</p>
|
<ui-text-input-with-label :value="enclosureUrl" readonly class="text-xs">
|
||||||
<a :href="enclosureUrl" target="_blank" class="text-xs text-blue-400 hover:text-blue-500 hover:underline">{{ enclosureUrl }}</a>
|
<label class="px-1 text-xs text-gray-200 font-semibold">Episode URL from RSS feed</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">Episode not linked to RSS feed episode</p>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full">
|
<div class="w-full h-full">
|
||||||
<div v-show="showPageMenu" v-click-outside="clickOutside" class="pagemenu absolute top-9 right-20 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400 w-52">
|
<div v-show="showPageMenu" v-click-outside="clickOutside" class="pagemenu absolute top-9 left-8 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400 w-52">
|
||||||
<div v-for="(file, index) in pages" :key="file" class="w-full cursor-pointer hover:bg-black-200 px-2 py-1" :class="page === index ? 'bg-black-200' : ''" @click="setPage(index)">
|
<div v-for="(file, index) in pages" :key="file" class="w-full cursor-pointer hover:bg-black-200 px-2 py-1" :class="page === index ? 'bg-black-200' : ''" @click="setPage(index)">
|
||||||
<p class="text-sm truncate">{{ file }}</p>
|
<p class="text-sm truncate">{{ file }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="showInfoMenu" v-click-outside="clickOutside" class="pagemenu absolute top-9 right-10 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400 w-96">
|
<div v-show="showInfoMenu" v-click-outside="clickOutside" class="pagemenu absolute top-9 left-20 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400 w-96">
|
||||||
<div v-for="key in comicMetadataKeys" :key="key" class="w-full px-2 py-1">
|
<div v-for="key in comicMetadataKeys" :key="key" class="w-full px-2 py-1">
|
||||||
<p class="text-xs">
|
<p class="text-xs">
|
||||||
<strong>{{ key }}</strong
|
<strong>{{ key }}</strong
|
||||||
@@ -14,17 +14,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="comicMetadata" class="absolute top-0 right-52 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="showInfoMenu = !showInfoMenu">
|
<div v-if="comicMetadata" class="absolute top-0 left-20 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="showInfoMenu = !showInfoMenu">
|
||||||
<span class="material-icons text-xl">more</span>
|
<span class="material-icons text-xl">more</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute top-0 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" style="right: 156px" @mousedown.prevent @click.stop.prevent="showPageMenu = !showPageMenu">
|
<div class="absolute top-0 left-8 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="showPageMenu = !showPageMenu">
|
||||||
<span class="material-icons text-xl">menu</span>
|
<span class="material-icons text-xl">menu</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute top-0 right-20 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20">
|
<div class="absolute top-0 right-16 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20">
|
||||||
<p class="font-mono">{{ page + 1 }} / {{ numPages }}</p>
|
<p class="font-mono">{{ page + 1 }} / {{ numPages }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="overflow-hidden m-auto comicwrapper relative">
|
<div class="overflow-hidden w-full h-full relative">
|
||||||
<div v-show="canGoPrev" class="absolute top-0 left-0 h-full w-1/2 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent>
|
<div v-show="canGoPrev" class="absolute top-0 left-0 h-full w-1/2 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent>
|
||||||
<div class="flex items-center justify-center h-full w-1/2">
|
<div class="flex items-center justify-center h-full w-1/2">
|
||||||
<span v-show="loadedFirstPage" class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_back_ios</span>
|
<span v-show="loadedFirstPage" class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_back_ios</span>
|
||||||
@@ -36,17 +36,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-full flex justify-center">
|
<div class="h-full flex justify-center">
|
||||||
<img v-if="mainImg" :src="mainImg" class="object-contain comicimg" />
|
<img v-if="mainImg" :src="mainImg" class="object-contain h-full m-auto" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="loading" class="w-full h-full absolute top-0 left-0 flex items-center justify-center z-10">
|
<div v-show="loading" class="w-full h-full absolute top-0 left-0 flex items-center justify-center z-10">
|
||||||
<ui-loading-indicator />
|
<ui-loading-indicator />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <div v-show="loading" class="w-screen h-screen absolute top-0 left-0 bg-black bg-opacity-20 flex items-center justify-center">
|
|
||||||
<ui-loading-indicator />
|
|
||||||
</div> -->
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -61,7 +57,12 @@ Archive.init({
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
url: String
|
url: String,
|
||||||
|
libraryItem: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
playerOpen: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -249,15 +250,6 @@ export default {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.pagemenu {
|
.pagemenu {
|
||||||
max-height: calc(100vh - 60px);
|
max-height: calc(100% - 48px);
|
||||||
}
|
|
||||||
.comicimg {
|
|
||||||
height: calc(100vh - 40px);
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
.comicwrapper {
|
|
||||||
width: 100vw;
|
|
||||||
height: calc(100vh - 40px);
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h-full w-full">
|
<div id="epub-reader" class="h-full w-full">
|
||||||
<div class="h-full flex items-center justify-center">
|
<div class="h-full flex items-center justify-center">
|
||||||
<div style="width: 100px; max-width: 100px" class="h-full hidden sm:flex items-center overflow-x-hidden justify-center">
|
<div style="width: 100px; max-width: 100px" class="h-full hidden sm:flex items-center overflow-x-hidden justify-center">
|
||||||
<span v-if="hasPrev" class="material-icons text-white text-opacity-50 hover:text-opacity-80 cursor-pointer text-6xl" @mousedown.prevent @click="prev">chevron_left</span>
|
<span v-if="hasPrev" class="material-icons text-white text-opacity-50 hover:text-opacity-80 cursor-pointer text-6xl" @mousedown.prevent @click="prev">chevron_left</span>
|
||||||
@@ -28,17 +28,24 @@ export default {
|
|||||||
libraryItem: {
|
libraryItem: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {}
|
default: () => {}
|
||||||
}
|
},
|
||||||
|
playerOpen: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
windowWidth: 0,
|
windowWidth: 0,
|
||||||
|
windowHeight: 0,
|
||||||
/** @type {ePub.Book} */
|
/** @type {ePub.Book} */
|
||||||
book: null,
|
book: null,
|
||||||
/** @type {ePub.Rendition} */
|
/** @type {ePub.Rendition} */
|
||||||
rendition: null
|
rendition: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
playerOpen() {
|
||||||
|
this.resize()
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
/** @returns {string} */
|
/** @returns {string} */
|
||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
@@ -64,6 +71,10 @@ export default {
|
|||||||
readerWidth() {
|
readerWidth() {
|
||||||
if (this.windowWidth < 640) return this.windowWidth
|
if (this.windowWidth < 640) return this.windowWidth
|
||||||
return this.windowWidth - 200
|
return this.windowWidth - 200
|
||||||
|
},
|
||||||
|
readerHeight() {
|
||||||
|
if (this.windowHeight < 400 || !this.playerOpen) return this.windowHeight
|
||||||
|
return this.windowHeight - 164
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -203,13 +214,13 @@ export default {
|
|||||||
/** @type {ePub.Book} */
|
/** @type {ePub.Book} */
|
||||||
reader.book = new ePub(reader.url, {
|
reader.book = new ePub(reader.url, {
|
||||||
width: this.readerWidth,
|
width: this.readerWidth,
|
||||||
height: window.innerHeight - 50
|
height: this.readerHeight - 50
|
||||||
})
|
})
|
||||||
|
|
||||||
/** @type {ePub.Rendition} */
|
/** @type {ePub.Rendition} */
|
||||||
reader.rendition = reader.book.renderTo('viewer', {
|
reader.rendition = reader.book.renderTo('viewer', {
|
||||||
width: this.readerWidth,
|
width: this.readerWidth,
|
||||||
height: window.innerHeight * 0.8
|
height: this.readerHeight * 0.8
|
||||||
})
|
})
|
||||||
|
|
||||||
// load saved progress
|
// load saved progress
|
||||||
@@ -253,17 +264,19 @@ export default {
|
|||||||
},
|
},
|
||||||
resize() {
|
resize() {
|
||||||
this.windowWidth = window.innerWidth
|
this.windowWidth = window.innerWidth
|
||||||
this.rendition?.resize(this.readerWidth, window.innerHeight * 0.8)
|
this.windowHeight = window.innerHeight
|
||||||
|
this.rendition?.resize(this.readerWidth, this.readerHeight * 0.8)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
this.windowWidth = window.innerWidth
|
||||||
|
this.windowHeight = window.innerHeight
|
||||||
|
window.addEventListener('resize', this.resize)
|
||||||
|
this.initEpub()
|
||||||
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
window.removeEventListener('resize', this.resize)
|
window.removeEventListener('resize', this.resize)
|
||||||
this.book?.destroy()
|
this.book?.destroy()
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.windowWidth = window.innerWidth
|
|
||||||
window.addEventListener('resize', this.resize)
|
|
||||||
this.initEpub()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full">
|
<div class="w-full h-full">
|
||||||
<div class="h-full max-h-full w-full">
|
<div class="h-full max-h-full w-full">
|
||||||
<div class="ebook-viewer absolute overflow-y-scroll left-0 right-0 top-12 w-full max-w-4xl m-auto z-10 border border-black border-opacity-20 shadow-md bg-white">
|
<div class="ebook-viewer absolute overflow-y-scroll left-0 right-0 top-16 w-full max-w-4xl m-auto z-10 border border-black border-opacity-20 shadow-md bg-white">
|
||||||
<iframe title="html-viewer" width="100%"> Loading </iframe>
|
<iframe title="html-viewer" width="100%"> Loading </iframe>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -15,7 +15,12 @@ import defaultCss from '@/assets/ebooks/basic.js'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
url: String
|
url: String,
|
||||||
|
libraryItem: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
playerOpen: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@@ -11,15 +11,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="absolute top-0 right-20 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center">
|
<div class="absolute top-0 right-20 bg-bg text-gray-100 border-b border-l border-r border-gray-400 z-20 rounded-b-md px-2 h-9 flex items-center text-center">
|
||||||
<p class="font-mono">{{ page }} / {{ numPages }}</p>
|
<p class="font-mono">{{ page }} / {{ numPages }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="absolute top-0 right-40 bg-bg text-gray-100 border-b border-l border-r border-gray-400 z-20 rounded-b-md px-2 h-9 flex items-center text-center">
|
||||||
|
<ui-icon-btn icon="zoom_out" :size="8" :disabled="!canScaleDown" borderless class="mr-px" @click="zoomOut" />
|
||||||
|
<ui-icon-btn icon="zoom_in" :size="8" :disabled="!canScaleUp" borderless class="ml-px" @click="zoomIn" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div :style="{ height: pdfHeight + 'px' }" class="overflow-hidden m-auto">
|
<div :style="{ height: pdfHeight + 'px' }" class="overflow-hidden m-auto">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<div :style="{ width: pdfWidth + 'px', height: pdfHeight + 'px' }" class="w-full h-full overflow-auto">
|
<div :style="{ width: pdfWidth + 'px', height: pdfHeight + 'px' }" class="overflow-auto">
|
||||||
<div v-if="loadedRatio > 0 && loadedRatio < 1" style="background-color: green; color: white; text-align: center" :style="{ width: loadedRatio * 100 + '%' }">{{ Math.floor(loadedRatio * 100) }}%</div>
|
<div v-if="loadedRatio > 0 && loadedRatio < 1" style="background-color: green; color: white; text-align: center" :style="{ width: loadedRatio * 100 + '%' }">{{ Math.floor(loadedRatio * 100) }}%</div>
|
||||||
<pdf ref="pdf" class="m-auto z-10 border border-black border-opacity-20 shadow-md" :src="url" :page="page" :rotate="rotate" @progress="loadedRatio = $event" @error="error" @num-pages="numPagesLoaded" @link-clicked="page = $event"></pdf>
|
<pdf ref="pdf" class="m-auto z-10 border border-black border-opacity-20 shadow-md" :src="url" :page="page" :rotate="rotate" @progress="progressEvt" @error="error" @num-pages="numPagesLoaded" @link-clicked="page = $event" @loaded="loadedEvt"></pdf>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,17 +34,25 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import pdf from 'vue-pdf'
|
import pdf from '@teckel/vue-pdf'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
pdf
|
pdf
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
url: String
|
url: String,
|
||||||
|
libraryItem: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
playerOpen: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
windowWidth: 0,
|
||||||
|
windowHeight: 0,
|
||||||
|
scale: 1,
|
||||||
rotate: 0,
|
rotate: 0,
|
||||||
loadedRatio: 0,
|
loadedRatio: 0,
|
||||||
page: 1,
|
page: 1,
|
||||||
@@ -48,35 +60,99 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
libraryItemId() {
|
||||||
|
return this.libraryItem?.id
|
||||||
|
},
|
||||||
|
fitToPageWidth() {
|
||||||
|
return this.pdfHeight * 0.6
|
||||||
|
},
|
||||||
pdfWidth() {
|
pdfWidth() {
|
||||||
return this.pdfHeight * 0.6667
|
return this.fitToPageWidth * this.scale
|
||||||
},
|
},
|
||||||
pdfHeight() {
|
pdfHeight() {
|
||||||
return window.innerHeight - 120
|
if (this.windowHeight < 400 || !this.playerOpen) return this.windowHeight - 120
|
||||||
|
return this.windowHeight - 284
|
||||||
|
},
|
||||||
|
maxScale() {
|
||||||
|
return Math.floor((this.windowWidth * 10) / this.fitToPageWidth) / 10
|
||||||
},
|
},
|
||||||
canGoNext() {
|
canGoNext() {
|
||||||
return this.page < this.numPages
|
return this.page < this.numPages
|
||||||
},
|
},
|
||||||
canGoPrev() {
|
canGoPrev() {
|
||||||
return this.page > 1
|
return this.page > 1
|
||||||
|
},
|
||||||
|
canScaleUp() {
|
||||||
|
return this.scale < this.maxScale
|
||||||
|
},
|
||||||
|
canScaleDown() {
|
||||||
|
return this.scale > 1
|
||||||
|
},
|
||||||
|
userMediaProgress() {
|
||||||
|
if (!this.libraryItemId) return
|
||||||
|
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||||
|
},
|
||||||
|
savedPage() {
|
||||||
|
return Number(this.userMediaProgress?.ebookLocation || 0)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
zoomIn() {
|
||||||
|
this.scale += 0.1
|
||||||
|
},
|
||||||
|
zoomOut() {
|
||||||
|
this.scale -= 0.1
|
||||||
|
},
|
||||||
|
updateProgress() {
|
||||||
|
if (!this.numPages) {
|
||||||
|
console.error('Num pages not loaded')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
ebookLocation: this.page,
|
||||||
|
ebookProgress: Math.max(0, Math.min(1, (Number(this.page) - 1) / Number(this.numPages)))
|
||||||
|
}
|
||||||
|
this.$axios.$patch(`/api/me/progress/${this.libraryItemId}`, payload).catch((error) => {
|
||||||
|
console.error('EpubReader.updateProgress failed:', error)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
loadedEvt() {
|
||||||
|
if (this.savedPage && this.savedPage > 0 && this.savedPage <= this.numPages) {
|
||||||
|
this.page = this.savedPage
|
||||||
|
}
|
||||||
|
},
|
||||||
|
progressEvt(progress) {
|
||||||
|
this.loadedRatio = progress
|
||||||
|
},
|
||||||
numPagesLoaded(e) {
|
numPagesLoaded(e) {
|
||||||
this.numPages = e
|
this.numPages = e
|
||||||
},
|
},
|
||||||
prev() {
|
prev() {
|
||||||
if (this.page <= 1) return
|
if (this.page <= 1) return
|
||||||
this.page--
|
this.page--
|
||||||
|
this.updateProgress()
|
||||||
},
|
},
|
||||||
next() {
|
next() {
|
||||||
if (this.page >= this.numPages) return
|
if (this.page >= this.numPages) return
|
||||||
this.page++
|
this.page++
|
||||||
|
this.updateProgress()
|
||||||
},
|
},
|
||||||
error(err) {
|
error(err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
},
|
||||||
|
resize() {
|
||||||
|
this.windowWidth = window.innerWidth
|
||||||
|
this.windowHeight = window.innerHeight
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {
|
||||||
|
this.windowWidth = window.innerWidth
|
||||||
|
this.windowHeight = window.innerHeight
|
||||||
|
window.addEventListener('resize', this.resize)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.resize)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="show" class="w-screen h-screen fixed top-0 left-0 z-60 bg-primary text-white">
|
<div v-if="show" id="reader" class="absolute top-0 left-0 w-full z-60 bg-primary text-white" :class="{ 'reader-player-open': !!streamLibraryItem }">
|
||||||
<div class="absolute top-4 left-4 z-20">
|
<div class="absolute top-4 left-4 z-20">
|
||||||
<span v-if="hasToC && !tocOpen" ref="tocButton" class="material-icons cursor-pointer text-2xl" @click="toggleToC">menu</span>
|
<span v-if="hasToC && !tocOpen" ref="tocButton" class="material-icons cursor-pointer text-2xl" @click="toggleToC">menu</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -17,17 +17,22 @@
|
|||||||
<span class="material-icons cursor-pointer text-2xl" @click="close">close</span>
|
<span class="material-icons cursor-pointer text-2xl" @click="close">close</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<component v-if="componentName" ref="readerComponent" :is="componentName" :url="ebookUrl" :library-item="selectedLibraryItem" />
|
<component v-if="componentName" ref="readerComponent" :is="componentName" :url="ebookUrl" :library-item="selectedLibraryItem" :player-open="!!streamLibraryItem" />
|
||||||
|
|
||||||
<!-- TOC side nav -->
|
<!-- TOC side nav -->
|
||||||
<div v-if="tocOpen" class="w-full h-full fixed inset-0 bg-black/20 z-20" @click.stop.prevent="toggleToC"></div>
|
<div v-if="tocOpen" class="w-full h-full fixed inset-0 bg-black/20 z-20" @click.stop.prevent="toggleToC"></div>
|
||||||
<div v-if="hasToC" class="w-72 h-full max-h-full absolute top-0 left-0 bg-bg shadow-xl transition-transform z-30" :class="tocOpen ? 'translate-x-0' : '-translate-x-72'" @click.stop.prevent="toggleToC">
|
<div v-if="hasToC" class="w-96 h-full max-h-full absolute top-0 left-0 bg-bg shadow-xl transition-transform z-30" :class="tocOpen ? 'translate-x-0' : '-translate-x-96'" @click.stop.prevent="toggleToC">
|
||||||
<div class="p-4 h-full overflow-hidden">
|
<div class="p-4 h-full">
|
||||||
<p class="text-lg font-semibold mb-2">Table of Contents</p>
|
<p class="text-lg font-semibold mb-2">Table of Contents</p>
|
||||||
<div class="tocContent">
|
<div class="tocContent">
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="chapter in chapters" :key="chapter.id" class="py-1">
|
<li v-for="chapter in chapters" :key="chapter.id" class="py-1">
|
||||||
<a :href="chapter.href" @click.prevent="$refs.readerComponent.goToChapter(chapter.href)">{{ chapter.label }}</a>
|
<a :href="chapter.href" class="text-white/70 hover:text-white" @click.prevent="$refs.readerComponent.goToChapter(chapter.href)">{{ chapter.label }}</a>
|
||||||
|
<ul v-if="chapter.subitems.length">
|
||||||
|
<li v-for="subchapter in chapter.subitems" :key="subchapter.id" class="py-1 pl-4">
|
||||||
|
<a :href="subchapter.href" class="text-white/70 hover:text-white" @click.prevent="$refs.readerComponent.goToChapter(subchapter.href)">{{ subchapter.label }}</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,6 +72,9 @@ export default {
|
|||||||
else if (this.ebookType === 'comic') return 'readers-comic-reader'
|
else if (this.ebookType === 'comic') return 'readers-comic-reader'
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
|
streamLibraryItem() {
|
||||||
|
return this.$store.state.streamLibraryItem
|
||||||
|
},
|
||||||
hasToC() {
|
hasToC() {
|
||||||
return this.isEpub
|
return this.isEpub
|
||||||
},
|
},
|
||||||
@@ -146,7 +154,6 @@ export default {
|
|||||||
},
|
},
|
||||||
openSettings() {},
|
openSettings() {},
|
||||||
hotkey(action) {
|
hotkey(action) {
|
||||||
console.log('Reader hotkey', action)
|
|
||||||
if (!this.$refs.readerComponent) return
|
if (!this.$refs.readerComponent) return
|
||||||
|
|
||||||
if (action === this.$hotkeys.EReader.NEXT_PAGE) {
|
if (action === this.$hotkeys.EReader.NEXT_PAGE) {
|
||||||
@@ -187,12 +194,19 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* @import url(@/assets/calibre/basic.css); */
|
|
||||||
.ebook-viewer {
|
|
||||||
height: calc(100% - 96px);
|
|
||||||
}
|
|
||||||
.tocContent {
|
.tocContent {
|
||||||
height: calc(100% - 36px);
|
height: calc(100% - 36px);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
#reader {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
#reader.reader-player-open {
|
||||||
|
height: calc(100% - 164px);
|
||||||
|
}
|
||||||
|
@media (max-height: 400px) {
|
||||||
|
#reader.reader-player-open {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -70,6 +70,9 @@ export default {
|
|||||||
return this.libraryItem.libraryFiles || []
|
return this.libraryItem.libraryFiles || []
|
||||||
},
|
},
|
||||||
audioFiles() {
|
audioFiles() {
|
||||||
|
if (this.libraryItem.mediaType === 'podcast') {
|
||||||
|
return this.libraryItem.media?.episodes.map((ep) => ep.audioFile) || []
|
||||||
|
}
|
||||||
return this.libraryItem.media?.audioFiles || []
|
return this.libraryItem.media?.audioFiles || []
|
||||||
},
|
},
|
||||||
filesWithAudioFile() {
|
filesWithAudioFile() {
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full px-1 md:px-2 py-2 overflow-hidden relative" @mouseover="mouseover" @mouseleave="mouseleave" :class="isHovering ? 'bg-white bg-opacity-5' : ''">
|
<div class="w-full px-1 md:px-2 py-2 overflow-hidden relative" @mouseover="mouseover" @mouseleave="mouseleave" :class="isHovering ? 'bg-white bg-opacity-5' : ''">
|
||||||
<div v-if="book" class="flex h-16 md:h-20">
|
<div v-if="book" class="flex h-18 md:h-[5.5rem]">
|
||||||
<div class="w-10 min-w-10 md:w-16 md:max-w-16 h-full">
|
<div class="w-10 min-w-10 md:w-16 md:max-w-16 h-full">
|
||||||
<div class="flex h-full items-center justify-center">
|
<div class="flex h-full items-center justify-center">
|
||||||
<span class="material-icons drag-handle text-lg md:text-xl">menu</span>
|
<span class="material-icons drag-handle text-lg md:text-xl">menu</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-full relative flex items-center" :style="{ width: coverWidth + 'px', minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }">
|
<div class="h-full flex items-center" :style="{ width: coverWidth + 'px', minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }">
|
||||||
<covers-book-cover :library-item="book" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<div class="relative" :style="{ height: coverHeight + 'px', minHeight: coverHeight + 'px', maxHeight: coverHeight + 'px' }">
|
||||||
<div class="absolute top-0 left-0 bg-black bg-opacity-50 flex items-center justify-center h-full w-full z-10" v-show="isHovering && showPlayBtn">
|
<covers-book-cover :library-item="book" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
<div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick">
|
<div class="absolute top-0 left-0 flex items-center justify-center bg-black bg-opacity-50 h-full w-full z-10" v-show="isHovering && showPlayBtn">
|
||||||
<span class="material-icons text-2xl">play_arrow</span>
|
<div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick">
|
||||||
|
<span class="material-icons text-2xl">play_arrow</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -19,9 +21,12 @@
|
|||||||
<div class="truncate max-w-48 md:max-w-md">
|
<div class="truncate max-w-48 md:max-w-md">
|
||||||
<nuxt-link :to="`/item/${book.id}`" class="truncate hover:underline text-sm md:text-base">{{ bookTitle }}</nuxt-link>
|
<nuxt-link :to="`/item/${book.id}`" class="truncate hover:underline text-sm md:text-base">{{ bookTitle }}</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="truncate max-w-48 md:max-w-md text-xs md:text-sm text-gray-300">
|
||||||
|
<nuxt-link v-for="_series in seriesList" :key="_series.id" :to="`/library/${book.libraryId}/series/${_series.id}`" class="hover:underline font-sans text-gray-300"> {{ _series.text }}</nuxt-link>
|
||||||
|
</div>
|
||||||
<div class="truncate max-w-48 md:max-w-md text-xs md:text-sm text-gray-300">
|
<div class="truncate max-w-48 md:max-w-md text-xs md:text-sm text-gray-300">
|
||||||
<template v-for="(author, index) in bookAuthors">
|
<template v-for="(author, index) in bookAuthors">
|
||||||
<nuxt-link :key="author.id" :to="`/author/${author.id}`" class="truncate hover:underline">{{ author.name }}</nuxt-link
|
<nuxt-link :key="author.id" :to="`/author/${author.id}?library=${book.libraryId}`" class="truncate hover:underline">{{ author.name }}</nuxt-link
|
||||||
><span :key="author.id + '-comma'" v-if="index < bookAuthors.length - 1">, </span>
|
><span :key="author.id + '-comma'" v-if="index < bookAuthors.length - 1">, </span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,6 +101,19 @@ export default {
|
|||||||
bookDuration() {
|
bookDuration() {
|
||||||
return this.$elapsedPretty(this.media.duration)
|
return this.$elapsedPretty(this.media.duration)
|
||||||
},
|
},
|
||||||
|
series() {
|
||||||
|
return this.mediaMetadata.series || []
|
||||||
|
},
|
||||||
|
seriesList() {
|
||||||
|
return this.series.map((se) => {
|
||||||
|
let text = se.name
|
||||||
|
if (se.sequence) text += ` #${se.sequence}`
|
||||||
|
return {
|
||||||
|
...se,
|
||||||
|
text
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
isMissing() {
|
isMissing() {
|
||||||
return this.book.isMissing
|
return this.book.isMissing
|
||||||
},
|
},
|
||||||
@@ -117,6 +135,9 @@ export default {
|
|||||||
coverSize() {
|
coverSize() {
|
||||||
return this.$store.state.globals.isMobile ? 30 : 50
|
return this.$store.state.globals.isMobile ? 30 : 50
|
||||||
},
|
},
|
||||||
|
coverHeight() {
|
||||||
|
return this.coverSize * 1.6
|
||||||
|
},
|
||||||
coverWidth() {
|
coverWidth() {
|
||||||
if (this.bookCoverAspectRatio === 1) return this.coverSize * 1.6
|
if (this.bookCoverAspectRatio === 1) return this.coverSize * 1.6
|
||||||
return this.coverSize
|
return this.coverSize
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="truncate max-w-48 md:max-w-md text-xs md:text-sm text-gray-300">
|
<div class="truncate max-w-48 md:max-w-md text-xs md:text-sm text-gray-300">
|
||||||
<template v-for="(author, index) in bookAuthors">
|
<template v-for="(author, index) in bookAuthors">
|
||||||
<nuxt-link :key="author.id" :to="`/author/${author.id}`" class="truncate hover:underline">{{ author.name }}</nuxt-link
|
<nuxt-link :key="author.id" :to="`/author/${author.id}?library=${libraryItem.libraryId}`" class="truncate hover:underline">{{ author.name }}</nuxt-link
|
||||||
><span :key="author.id + '-comma'" v-if="index < bookAuthors.length - 1">, </span>
|
><span :key="author.id + '-comma'" v-if="index < bookAuthors.length - 1">, </span>
|
||||||
</template>
|
</template>
|
||||||
<nuxt-link v-if="episode" :to="`/item/${libraryItem.id}`" class="truncate hover:underline">{{ mediaMetadata.title }}</nuxt-link>
|
<nuxt-link v-if="episode" :to="`/item/${libraryItem.id}`" class="truncate hover:underline">{{ mediaMetadata.title }}</nuxt-link>
|
||||||
|
|||||||
@@ -7,8 +7,7 @@
|
|||||||
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5">{{ subtitle }}</p>
|
<p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5" v-html="subtitle"></p>
|
||||||
|
|
||||||
<div class="flex justify-between pt-2 max-w-xl">
|
<div class="flex justify-between pt-2 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">Season #{{ 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">Episode #{{ episode.episode }}</p>
|
||||||
@@ -22,10 +21,6 @@
|
|||||||
<p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p>
|
<p class="pl-2 pr-1 text-sm font-semibold">{{ timeRemaining }}</p>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- <button v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" class="h-8 w-8 flex justify-center items-center mx-2" :class="isQueued ? 'text-success' : ''" @click.stop="queueBtnClick">
|
|
||||||
<span class="material-icons-outlined">{{ isQueued ? 'playlist_add_check' : 'queue' }}</span>
|
|
||||||
</button> -->
|
|
||||||
|
|
||||||
<ui-tooltip v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" :text="isQueued ? $strings.MessageRemoveFromPlayerQueue : $strings.MessageAddToPlayerQueue" :class="isQueued ? 'text-success' : ''" direction="top">
|
<ui-tooltip v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" :text="isQueued ? $strings.MessageRemoveFromPlayerQueue : $strings.MessageAddToPlayerQueue" :class="isQueued ? 'text-success' : ''" direction="top">
|
||||||
<ui-icon-btn :icon="isQueued ? 'playlist_add_check' : 'playlist_play'" borderless @click="queueBtnClick" />
|
<ui-icon-btn :icon="isQueued ? 'playlist_add_check' : 'playlist_play'" borderless @click="queueBtnClick" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@@ -89,7 +84,7 @@ export default {
|
|||||||
return this.episode.title || ''
|
return this.episode.title || ''
|
||||||
},
|
},
|
||||||
subtitle() {
|
subtitle() {
|
||||||
return this.episode.subtitle || ''
|
return this.episode.subtitle || this.description
|
||||||
},
|
},
|
||||||
description() {
|
description() {
|
||||||
return this.episode.description || ''
|
return this.episode.description || ''
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="flex items-center py-3">
|
||||||
|
<slot />
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollLeft ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollLeft">
|
||||||
|
<span class="material-icons text-2xl">chevron_left</span>
|
||||||
|
</button>
|
||||||
|
<button v-if="isScrollable" class="w-8 h-8 mx-1 flex items-center justify-center rounded-full" :class="canScrollRight ? 'hover:bg-white hover:bg-opacity-5 text-gray-300 hover:text-white' : 'text-white text-opacity-40 cursor-text'" @click="scrollRight">
|
||||||
|
<span class="material-icons text-2xl">chevron_right</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div ref="slider" class="w-full overflow-y-hidden overflow-x-auto no-scroll -mx-2" style="scroll-behavior: smooth" @scroll="scrolled">
|
||||||
|
<div class="flex" :style="{ height: height + 'px' }">
|
||||||
|
<template v-for="item in items">
|
||||||
|
<cards-narrator-card :key="item.name" :ref="`slider-item-${item.name}`" :narrator="item" :height="cardHeight" :width="cardWidth" class="relative mx-2" @hook:updated="setScrollVars" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
default: 192
|
||||||
|
},
|
||||||
|
bookshelfView: {
|
||||||
|
type: Number,
|
||||||
|
default: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isScrollable: false,
|
||||||
|
canScrollLeft: false,
|
||||||
|
canScrollRight: false,
|
||||||
|
clientWidth: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
cardHeight() {
|
||||||
|
return this.height
|
||||||
|
},
|
||||||
|
cardWidth() {
|
||||||
|
return this.cardHeight * 1.5
|
||||||
|
},
|
||||||
|
booksPerPage() {
|
||||||
|
return Math.floor(this.clientWidth / (this.cardWidth + 16))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
scrolled() {
|
||||||
|
this.setScrollVars()
|
||||||
|
},
|
||||||
|
scrollRight() {
|
||||||
|
if (!this.canScrollRight) return
|
||||||
|
const slider = this.$refs.slider
|
||||||
|
if (!slider) return
|
||||||
|
const scrollAmount = this.booksPerPage * this.cardWidth
|
||||||
|
const maxScrollLeft = slider.scrollWidth - slider.clientWidth
|
||||||
|
|
||||||
|
const newScrollLeft = Math.min(maxScrollLeft, slider.scrollLeft + scrollAmount)
|
||||||
|
slider.scrollLeft = newScrollLeft
|
||||||
|
},
|
||||||
|
scrollLeft() {
|
||||||
|
if (!this.canScrollLeft) return
|
||||||
|
const slider = this.$refs.slider
|
||||||
|
if (!slider) return
|
||||||
|
|
||||||
|
const scrollAmount = this.booksPerPage * this.cardWidth
|
||||||
|
|
||||||
|
const newScrollLeft = Math.max(0, slider.scrollLeft - scrollAmount)
|
||||||
|
slider.scrollLeft = newScrollLeft
|
||||||
|
},
|
||||||
|
setScrollVars() {
|
||||||
|
const slider = this.$refs.slider
|
||||||
|
if (!slider) return
|
||||||
|
const { scrollLeft, scrollWidth, clientWidth } = slider
|
||||||
|
const scrollPercent = (scrollLeft + clientWidth) / scrollWidth
|
||||||
|
|
||||||
|
this.clientWidth = clientWidth
|
||||||
|
this.isScrollable = scrollWidth > clientWidth
|
||||||
|
this.canScrollRight = scrollPercent < 1
|
||||||
|
this.canScrollLeft = scrollLeft > 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updated() {
|
||||||
|
this.setScrollVars()
|
||||||
|
},
|
||||||
|
mounted() {},
|
||||||
|
beforeDestroy() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,19 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="tasksRunning" class="w-4 h-4 mx-3 relative" v-click-outside="clickOutsideObj">
|
<div v-if="tasksToShow.length" class="w-4 h-4 mx-3 relative" v-click-outside="clickOutsideObj">
|
||||||
<button type="button" :disabled="disabled" class="w-10 sm:w-full relative h-full cursor-pointer" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
<button type="button" :disabled="disabled" class="w-10 sm:w-full relative h-full cursor-pointer" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||||
<div class="flex h-full items-center justify-center">
|
<div class="flex h-full items-center justify-center">
|
||||||
<ui-tooltip text="Tasks running" direction="bottom" class="flex items-center">
|
<ui-tooltip v-if="tasksRunning" :text="$strings.LabelTasks" direction="bottom" class="flex items-center">
|
||||||
<widgets-loading-spinner />
|
<widgets-loading-spinner />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
<ui-tooltip v-else text="Activities" direction="bottom" class="flex items-center">
|
||||||
|
<span class="material-icons text-1.5xl" aria-label="Activities" role="button">notifications</span>
|
||||||
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<transition name="menu">
|
<transition name="menu">
|
||||||
<div class="sm:w-80 w-full relative">
|
<div class="sm:w-80 w-full relative">
|
||||||
<div v-show="showMenu" class="absolute z-40 -mt-px w-40 sm:w-full 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 globalTaskRunningMenu">
|
<div v-show="showMenu" class="absolute z-40 -mt-px w-40 sm:w-full bg-bg border border-black-200 shadow-lg rounded-md text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalTaskRunningMenu">
|
||||||
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-if="tasksRunningOrFailed.length">
|
<template v-if="tasksToShow.length">
|
||||||
<p class="uppercase text-xs text-gray-400 my-1 px-1 font-semibold">{{ $strings.LabelTasks }}</p>
|
<template v-for="task in tasksToShow">
|
||||||
<template v-for="task in tasksRunningOrFailed">
|
|
||||||
<nuxt-link :key="task.id" v-if="actionLink(task)" :to="actionLink(task)">
|
<nuxt-link :key="task.id" v-if="actionLink(task)" :to="actionLink(task)">
|
||||||
<li class="text-gray-50 select-none relative hover:bg-black-400 py-1 cursor-pointer">
|
<li class="text-gray-50 select-none relative hover:bg-black-400 py-1 cursor-pointer">
|
||||||
<cards-item-task-running-card :task="task" />
|
<cards-item-task-running-card :task="task" />
|
||||||
@@ -54,9 +56,10 @@ export default {
|
|||||||
tasksRunning() {
|
tasksRunning() {
|
||||||
return this.tasks.some((t) => !t.isFinished)
|
return this.tasks.some((t) => !t.isFinished)
|
||||||
},
|
},
|
||||||
tasksRunningOrFailed() {
|
tasksToShow() {
|
||||||
// return just the tasks that are running or failed in the last 1 minute
|
// return just the tasks that are running or failed (or show success) in the last 1 minute
|
||||||
return this.tasks.filter((t) => !t.isFinished || (t.isFailed && t.finishedAt > new Date().getTime() - 1000 * 60)) || []
|
const tasks = this.tasks.filter((t) => !t.isFinished || ((t.isFailed || t.showSuccess) && t.finishedAt > new Date().getTime() - 1000 * 60)) || []
|
||||||
|
return tasks.sort((a, b) => b.startedAt - a.startedAt)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -75,6 +78,8 @@ export default {
|
|||||||
return `/audiobook/${task.data.libraryItemId}/manage?tool=m4b`
|
return `/audiobook/${task.data.libraryItemId}/manage?tool=m4b`
|
||||||
case 'embed-metadata':
|
case 'embed-metadata':
|
||||||
return `/audiobook/${task.data.libraryItemId}/manage?tool=embed`
|
return `/audiobook/${task.data.libraryItemId}/manage?tool=embed`
|
||||||
|
case 'scan-item':
|
||||||
|
return `/item/${task.data.libraryItemId}`
|
||||||
default:
|
default:
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ export default {
|
|||||||
isSocketConnected: false,
|
isSocketConnected: false,
|
||||||
isFirstSocketConnection: true,
|
isFirstSocketConnection: true,
|
||||||
socketConnectionToastId: null,
|
socketConnectionToastId: null,
|
||||||
currentLang: null
|
currentLang: null,
|
||||||
|
multiSessionOtherSessionId: null, // Used for multiple sessions open warning toast
|
||||||
|
multiSessionCurrentSessionId: null // Used for multiple sessions open warning toast
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -300,14 +302,27 @@ export default {
|
|||||||
this.$store.commit('users/updateUserOnline', user)
|
this.$store.commit('users/updateUserOnline', user)
|
||||||
},
|
},
|
||||||
userSessionClosed(sessionId) {
|
userSessionClosed(sessionId) {
|
||||||
|
// If this session or other session is closed then dismiss multiple sessions warning toast
|
||||||
|
if (sessionId === this.multiSessionOtherSessionId || this.multiSessionCurrentSessionId === sessionId) {
|
||||||
|
this.multiSessionOtherSessionId = null
|
||||||
|
this.multiSessionCurrentSessionId = null
|
||||||
|
this.$toast.dismiss('multiple-sessions')
|
||||||
|
}
|
||||||
if (this.$refs.streamContainer) this.$refs.streamContainer.sessionClosedEvent(sessionId)
|
if (this.$refs.streamContainer) this.$refs.streamContainer.sessionClosedEvent(sessionId)
|
||||||
},
|
},
|
||||||
userMediaProgressUpdate(payload) {
|
userMediaProgressUpdate(payload) {
|
||||||
this.$store.commit('user/updateMediaProgress', payload)
|
this.$store.commit('user/updateMediaProgress', payload)
|
||||||
|
|
||||||
if (payload.data) {
|
if (payload.data) {
|
||||||
if (this.$store.getters['getIsMediaStreaming'](payload.data.libraryItemId, payload.data.episodeId)) {
|
if (this.$store.getters['getIsMediaStreaming'](payload.data.libraryItemId, payload.data.episodeId) && this.$store.state.playbackSessionId !== payload.sessionId) {
|
||||||
// TODO: Update currently open session if being played from another device
|
this.multiSessionOtherSessionId = payload.sessionId
|
||||||
|
this.multiSessionCurrentSessionId = this.$store.state.playbackSessionId
|
||||||
|
console.log(`Media progress was updated from another session (${this.multiSessionOtherSessionId}) for currently open media. Device description=${payload.deviceDescription}. Current session id=${this.multiSessionCurrentSessionId}`)
|
||||||
|
if (this.$store.state.streamIsPlaying) {
|
||||||
|
this.$toast.update('multiple-sessions', { content: `Another session is open for this item on device ${payload.deviceDescription}`, options: { timeout: 20000, type: 'warning', pauseOnFocusLoss: false } }, true)
|
||||||
|
} else {
|
||||||
|
this.$eventBus.$emit('playback-time-update', payload.data.currentTime)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -569,6 +584,7 @@ export default {
|
|||||||
changeLanguage(code) {
|
changeLanguage(code) {
|
||||||
console.log('Changed lang', code)
|
console.log('Changed lang', code)
|
||||||
this.currentLang = code
|
this.currentLang = code
|
||||||
|
document.documentElement.lang = code
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
beforeMount() {
|
beforeMount() {
|
||||||
@@ -593,6 +609,11 @@ export default {
|
|||||||
this.$toast.error(this.$route.query.error)
|
this.$toast.error(this.$route.query.error)
|
||||||
this.$router.replace(this.$route.path)
|
this.$router.replace(this.$route.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set lang on HTML tag
|
||||||
|
if (this.$languageCodes?.current) {
|
||||||
|
document.documentElement.lang = this.$languageCodes.current
|
||||||
|
}
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$eventBus.$off('change-lang', this.changeLanguage)
|
this.$eventBus.$off('change-lang', this.changeLanguage)
|
||||||
|
|||||||
@@ -27,11 +27,7 @@ module.exports = {
|
|||||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
|
||||||
{ hid: 'description', name: 'description', content: '' }
|
{ hid: 'description', name: 'description', content: '' }
|
||||||
],
|
],
|
||||||
script: [
|
script: [],
|
||||||
{
|
|
||||||
src: (process.env.ROUTER_BASE_PATH || '') + '/libs/sortable.js'
|
|
||||||
}
|
|
||||||
],
|
|
||||||
link: [
|
link: [
|
||||||
{ rel: 'icon', type: 'image/x-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/favicon.ico' }
|
{ rel: 'icon', type: 'image/x-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/favicon.ico' }
|
||||||
]
|
]
|
||||||
|
|||||||
Generated
+73
-73
@@ -1,16 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.19",
|
"version": "2.2.21",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.19",
|
"version": "2.2.21",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
"@nuxtjs/proxy": "^2.1.0",
|
"@nuxtjs/proxy": "^2.1.0",
|
||||||
|
"@teckel/vue-pdf": "^4.3.5",
|
||||||
"core-js": "^3.16.0",
|
"core-js": "^3.16.0",
|
||||||
"cron-parser": "^4.7.1",
|
"cron-parser": "^4.7.1",
|
||||||
"date-fns": "^2.25.0",
|
"date-fns": "^2.25.0",
|
||||||
@@ -21,7 +22,6 @@
|
|||||||
"nuxt-socket-io": "^1.1.18",
|
"nuxt-socket-io": "^1.1.18",
|
||||||
"trix": "^1.3.1",
|
"trix": "^1.3.1",
|
||||||
"v-click-outside": "^3.1.2",
|
"v-click-outside": "^3.1.2",
|
||||||
"vue-pdf": "^4.2.0",
|
|
||||||
"vue-toastification": "^1.7.11",
|
"vue-toastification": "^1.7.11",
|
||||||
"vuedraggable": "^2.24.3"
|
"vuedraggable": "^2.24.3"
|
||||||
},
|
},
|
||||||
@@ -2983,6 +2983,43 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||||
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@teckel/vue-pdf": {
|
||||||
|
"version": "4.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@teckel/vue-pdf/-/vue-pdf-4.3.5.tgz",
|
||||||
|
"integrity": "sha512-g2DAbZMPbPc7NPFImOsU/e7rt7wfdmBkmFa2kPsB4x+k+Bs8yC5Icmq/VnTSEq/Y8bNvEY7i6+JoicGnlfQL7Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||||
|
"loader-utils": "^1.4.0",
|
||||||
|
"pdfjs-dist": "^2.5.207 <2.8.0",
|
||||||
|
"raw-loader": "^4.0.1",
|
||||||
|
"vue-resize-sensor": "^2.0.0",
|
||||||
|
"worker-loader": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@teckel/vue-pdf/node_modules/json5": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||||
|
"dependencies": {
|
||||||
|
"minimist": "^1.2.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"json5": "lib/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@teckel/vue-pdf/node_modules/loader-utils": {
|
||||||
|
"version": "1.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
|
||||||
|
"integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
|
||||||
|
"dependencies": {
|
||||||
|
"big.js": "^5.2.2",
|
||||||
|
"emojis-list": "^3.0.0",
|
||||||
|
"json5": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/anymatch": {
|
"node_modules/@types/anymatch": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-3.0.0.tgz",
|
||||||
@@ -15921,43 +15958,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue-no-ssr/-/vue-no-ssr-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/vue-no-ssr/-/vue-no-ssr-1.1.1.tgz",
|
||||||
"integrity": "sha512-ZMjqRpWabMPqPc7gIrG0Nw6vRf1+itwf0Itft7LbMXs2g3Zs/NFmevjZGN1x7K3Q95GmIjWbQZTVerxiBxI+0g=="
|
"integrity": "sha512-ZMjqRpWabMPqPc7gIrG0Nw6vRf1+itwf0Itft7LbMXs2g3Zs/NFmevjZGN1x7K3Q95GmIjWbQZTVerxiBxI+0g=="
|
||||||
},
|
},
|
||||||
"node_modules/vue-pdf": {
|
|
||||||
"version": "4.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/vue-pdf/-/vue-pdf-4.3.0.tgz",
|
|
||||||
"integrity": "sha512-zd3lJj6CbtrawgaaDDciTDjkJMUKiLWtbEmBg5CvFn9Noe9oAO/GNy/fc5c59qGuFCJ14ibIV1baw4S07e5bSQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
|
||||||
"loader-utils": "^1.4.0",
|
|
||||||
"pdfjs-dist": "2.6.347",
|
|
||||||
"raw-loader": "^4.0.2",
|
|
||||||
"vue-resize-sensor": "^2.0.0",
|
|
||||||
"worker-loader": "^2.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vue-pdf/node_modules/json5": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
|
||||||
"dependencies": {
|
|
||||||
"minimist": "^1.2.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"json5": "lib/cli.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vue-pdf/node_modules/loader-utils": {
|
|
||||||
"version": "1.4.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
|
|
||||||
"integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
|
|
||||||
"dependencies": {
|
|
||||||
"big.js": "^5.2.2",
|
|
||||||
"emojis-list": "^3.0.0",
|
|
||||||
"json5": "^1.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=4.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vue-resize-sensor": {
|
"node_modules/vue-resize-sensor": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-resize-sensor/-/vue-resize-sensor-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-resize-sensor/-/vue-resize-sensor-2.0.0.tgz",
|
||||||
@@ -19591,6 +19591,39 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||||
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
||||||
},
|
},
|
||||||
|
"@teckel/vue-pdf": {
|
||||||
|
"version": "4.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@teckel/vue-pdf/-/vue-pdf-4.3.5.tgz",
|
||||||
|
"integrity": "sha512-g2DAbZMPbPc7NPFImOsU/e7rt7wfdmBkmFa2kPsB4x+k+Bs8yC5Icmq/VnTSEq/Y8bNvEY7i6+JoicGnlfQL7Q==",
|
||||||
|
"requires": {
|
||||||
|
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||||
|
"loader-utils": "^1.4.0",
|
||||||
|
"pdfjs-dist": "^2.5.207 <2.8.0",
|
||||||
|
"raw-loader": "^4.0.1",
|
||||||
|
"vue-resize-sensor": "^2.0.0",
|
||||||
|
"worker-loader": "^2.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"json5": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
|
||||||
|
"requires": {
|
||||||
|
"minimist": "^1.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"loader-utils": {
|
||||||
|
"version": "1.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
|
||||||
|
"integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
|
||||||
|
"requires": {
|
||||||
|
"big.js": "^5.2.2",
|
||||||
|
"emojis-list": "^3.0.0",
|
||||||
|
"json5": "^1.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/anymatch": {
|
"@types/anymatch": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/anymatch/-/anymatch-3.0.0.tgz",
|
||||||
@@ -29618,39 +29651,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vue-no-ssr/-/vue-no-ssr-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/vue-no-ssr/-/vue-no-ssr-1.1.1.tgz",
|
||||||
"integrity": "sha512-ZMjqRpWabMPqPc7gIrG0Nw6vRf1+itwf0Itft7LbMXs2g3Zs/NFmevjZGN1x7K3Q95GmIjWbQZTVerxiBxI+0g=="
|
"integrity": "sha512-ZMjqRpWabMPqPc7gIrG0Nw6vRf1+itwf0Itft7LbMXs2g3Zs/NFmevjZGN1x7K3Q95GmIjWbQZTVerxiBxI+0g=="
|
||||||
},
|
},
|
||||||
"vue-pdf": {
|
|
||||||
"version": "4.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/vue-pdf/-/vue-pdf-4.3.0.tgz",
|
|
||||||
"integrity": "sha512-zd3lJj6CbtrawgaaDDciTDjkJMUKiLWtbEmBg5CvFn9Noe9oAO/GNy/fc5c59qGuFCJ14ibIV1baw4S07e5bSQ==",
|
|
||||||
"requires": {
|
|
||||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
|
||||||
"loader-utils": "^1.4.0",
|
|
||||||
"pdfjs-dist": "2.6.347",
|
|
||||||
"raw-loader": "^4.0.2",
|
|
||||||
"vue-resize-sensor": "^2.0.0",
|
|
||||||
"worker-loader": "^2.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"json5": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==",
|
|
||||||
"requires": {
|
|
||||||
"minimist": "^1.2.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"loader-utils": {
|
|
||||||
"version": "1.4.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz",
|
|
||||||
"integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==",
|
|
||||||
"requires": {
|
|
||||||
"big.js": "^5.2.2",
|
|
||||||
"emojis-list": "^3.0.0",
|
|
||||||
"json5": "^1.0.1"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"vue-resize-sensor": {
|
"vue-resize-sensor": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/vue-resize-sensor/-/vue-resize-sensor-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/vue-resize-sensor/-/vue-resize-sensor-2.0.0.tgz",
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.19",
|
"version": "2.2.21",
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
"@nuxtjs/proxy": "^2.1.0",
|
"@nuxtjs/proxy": "^2.1.0",
|
||||||
|
"@teckel/vue-pdf": "^4.3.5",
|
||||||
"core-js": "^3.16.0",
|
"core-js": "^3.16.0",
|
||||||
"cron-parser": "^4.7.1",
|
"cron-parser": "^4.7.1",
|
||||||
"date-fns": "^2.25.0",
|
"date-fns": "^2.25.0",
|
||||||
@@ -25,7 +26,6 @@
|
|||||||
"nuxt-socket-io": "^1.1.18",
|
"nuxt-socket-io": "^1.1.18",
|
||||||
"trix": "^1.3.1",
|
"trix": "^1.3.1",
|
||||||
"v-click-outside": "^3.1.2",
|
"v-click-outside": "^3.1.2",
|
||||||
"vue-pdf": "^4.2.0",
|
|
||||||
"vue-toastification": "^1.7.11",
|
"vue-toastification": "^1.7.11",
|
||||||
"vuedraggable": "^2.24.3"
|
"vuedraggable": "^2.24.3"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -43,8 +43,8 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ store, app, params, redirect }) {
|
async asyncData({ store, app, params, redirect, query }) {
|
||||||
const author = await app.$axios.$get(`/api/authors/${params.id}?library=${store.state.libraries.currentLibraryId}&include=items,series`).catch((error) => {
|
const author = await app.$axios.$get(`/api/authors/${params.id}?library=${query.library || store.state.libraries.currentLibraryId}&include=items,series`).catch((error) => {
|
||||||
console.error('Failed to get author', error)
|
console.error('Failed to get author', error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@@ -53,6 +53,10 @@ export default {
|
|||||||
return redirect(`/library/${store.state.libraries.currentLibraryId}/authors`)
|
return redirect(`/library/${store.state.libraries.currentLibraryId}/authors`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (query.library) {
|
||||||
|
store.commit('libraries/setCurrentLibrary', query.library)
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
author
|
author
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,11 +39,15 @@
|
|||||||
<ui-multi-select v-model="newServerSettings.sortingPrefixes" small :items="newServerSettings.sortingPrefixes" :label="$strings.LabelPrefixesToIgnore" @input="updateSortingPrefixes" :disabled="updatingServerSettings" />
|
<ui-multi-select v-model="newServerSettings.sortingPrefixes" small :items="newServerSettings.sortingPrefixes" :label="$strings.LabelPrefixesToIgnore" @input="updateSortingPrefixes" :disabled="updatingServerSettings" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2 mb-2">
|
||||||
<ui-toggle-switch labeledBy="settings-chromecast-support" v-model="newServerSettings.chromecastEnabled" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" />
|
<ui-toggle-switch labeledBy="settings-chromecast-support" v-model="newServerSettings.chromecastEnabled" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('chromecastEnabled', val)" />
|
||||||
<p class="pl-4" id="settings-chromecast-support">{{ $strings.LabelSettingsChromecastSupport }}</p>
|
<p class="pl-4" id="settings-chromecast-support">{{ $strings.LabelSettingsChromecastSupport }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="w-44 mb-2">
|
||||||
|
<ui-dropdown v-model="newServerSettings.metadataFileFormat" small :items="metadataFileFormats" label="Metadata File Format" @input="updateMetadataFileFormat" :disabled="updatingServerSettings" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="pt-4">
|
<div class="pt-4">
|
||||||
<h2 class="font-semibold">{{ $strings.HeaderSettingsDisplay }}</h2>
|
<h2 class="font-semibold">{{ $strings.HeaderSettingsDisplay }}</h2>
|
||||||
</div>
|
</div>
|
||||||
@@ -272,7 +276,17 @@ export default {
|
|||||||
useBookshelfView: false,
|
useBookshelfView: false,
|
||||||
isPurgingCache: false,
|
isPurgingCache: false,
|
||||||
newServerSettings: {},
|
newServerSettings: {},
|
||||||
showConfirmPurgeCache: false
|
showConfirmPurgeCache: false,
|
||||||
|
metadataFileFormats: [
|
||||||
|
{
|
||||||
|
text: '.json',
|
||||||
|
value: 'json'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: '.abs',
|
||||||
|
value: 'abs'
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -341,6 +355,10 @@ export default {
|
|||||||
updateServerLanguage(val) {
|
updateServerLanguage(val) {
|
||||||
this.updateSettingsKey('language', val)
|
this.updateSettingsKey('language', val)
|
||||||
},
|
},
|
||||||
|
updateMetadataFileFormat(val) {
|
||||||
|
if (this.serverSettings.metadataFileFormat === val) return
|
||||||
|
this.updateSettingsKey('metadataFileFormat', val)
|
||||||
|
},
|
||||||
updateSettingsKey(key, val) {
|
updateSettingsKey(key, val) {
|
||||||
this.updateServerSettings({
|
this.updateServerSettings({
|
||||||
[key]: val
|
[key]: val
|
||||||
@@ -350,8 +368,7 @@ export default {
|
|||||||
this.updatingServerSettings = true
|
this.updatingServerSettings = true
|
||||||
this.$store
|
this.$store
|
||||||
.dispatch('updateServerSettings', payload)
|
.dispatch('updateServerSettings', payload)
|
||||||
.then((success) => {
|
.then(() => {
|
||||||
console.log('Updated Server Settings', success)
|
|
||||||
this.updatingServerSettings = false
|
this.updatingServerSettings = false
|
||||||
this.$toast.success('Server settings updated')
|
this.$toast.success('Server settings updated')
|
||||||
|
|
||||||
|
|||||||
@@ -17,8 +17,8 @@
|
|||||||
<ui-text-input v-else v-model="newTagName" />
|
<ui-text-input v-else v-model="newTagName" />
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<template v-if="editingTag !== tag">
|
<template v-if="editingTag !== tag">
|
||||||
<ui-icon-btn v-if="editingTag !== tag" icon="edit" borderless :size="8" icon-font-size="1.1rem" class="mx-1" @click="editTagClick(tag)" />
|
<ui-icon-btn icon="edit" borderless :size="8" icon-font-size="1.1rem" class="mx-1" @click="editTagClick(tag)" />
|
||||||
<ui-icon-btn v-if="editingTag !== tag" icon="delete" borderless :size="8" icon-font-size="1.1rem" @click="removeTagClick(tag)" />
|
<ui-icon-btn icon="delete" borderless :size="8" icon-font-size="1.1rem" @click="removeTagClick(tag)" />
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<ui-btn color="success" small class="mx-2" @click.stop="saveTagClick">{{ $strings.ButtonSave }}</ui-btn>
|
<ui-btn color="success" small class="mx-2" @click.stop="saveTagClick">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
|
|||||||
@@ -42,89 +42,12 @@
|
|||||||
<nuxt-link v-for="(artist, index) in musicArtists" :key="index" :to="`/artist/${$encode(artist)}`" class="hover:underline">{{ artist }}<span v-if="index < musicArtists.length - 1">, </span></nuxt-link>
|
<nuxt-link v-for="(artist, index) in musicArtists" :key="index" :to="`/artist/${$encode(artist)}`" class="hover:underline">{{ artist }}<span v-if="index < musicArtists.length - 1">, </span></nuxt-link>
|
||||||
</p>
|
</p>
|
||||||
<p v-else-if="authors.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl max-w-[calc(100vw-2rem)] overflow-hidden overflow-ellipsis">
|
<p v-else-if="authors.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl max-w-[calc(100vw-2rem)] overflow-hidden overflow-ellipsis">
|
||||||
by <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
by <nuxt-link v-for="(author, index) in authors" :key="index" :to="`/author/${author.id}?library=${libraryItem.libraryId}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
||||||
</p>
|
</p>
|
||||||
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
|
<p v-else class="mb-2 mt-0.5 text-gray-200 text-xl">by Unknown</p>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="narrator" class="flex py-0.5 mt-4">
|
<content-library-item-details :library-item="libraryItem" />
|
||||||
<div class="w-32">
|
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelNarrators }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
|
||||||
<template v-for="(narrator, index) in narrators">
|
|
||||||
<nuxt-link :key="narrator" :to="`/library/${libraryId}/bookshelf?filter=narrators.${$encode(narrator)}`" class="hover:underline">{{ narrator }}</nuxt-link
|
|
||||||
><span :key="index" v-if="index < narrators.length - 1">, </span>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="publishedYear" class="flex py-0.5">
|
|
||||||
<div class="w-32">
|
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublishYear }}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ publishedYear }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="musicAlbum" class="flex py-0.5">
|
|
||||||
<div class="w-32">
|
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">Album</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ musicAlbum }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="musicAlbumArtist" class="flex py-0.5">
|
|
||||||
<div class="w-32">
|
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">Album Artist</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ musicAlbumArtist }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="musicTrackPretty" class="flex py-0.5">
|
|
||||||
<div class="w-32">
|
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">Track</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ musicTrackPretty }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="musicDiscPretty" class="flex py-0.5">
|
|
||||||
<div class="w-32">
|
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">Disc</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ musicDiscPretty }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex py-0.5" v-if="genres.length">
|
|
||||||
<div class="w-32">
|
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelGenres }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
|
||||||
<template v-for="(genre, index) in genres">
|
|
||||||
<nuxt-link :key="genre" :to="`/library/${libraryId}/bookshelf?filter=genres.${$encode(genre)}`" class="hover:underline">{{ genre }}</nuxt-link
|
|
||||||
><span :key="index" v-if="index < genres.length - 1">, </span>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="tracks.length || audioFile || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
|
|
||||||
<div class="w-32">
|
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ durationPretty }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex py-0.5">
|
|
||||||
<div class="w-32">
|
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelSize }}</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{{ sizePretty }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="hidden md:block flex-grow" />
|
<div class="hidden md:block flex-grow" />
|
||||||
</div>
|
</div>
|
||||||
@@ -339,9 +262,6 @@ export default {
|
|||||||
libraryId() {
|
libraryId() {
|
||||||
return this.libraryItem.libraryId
|
return this.libraryItem.libraryId
|
||||||
},
|
},
|
||||||
folderId() {
|
|
||||||
return this.libraryItem.folderId
|
|
||||||
},
|
|
||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
return this.libraryItem.id
|
return this.libraryItem.id
|
||||||
},
|
},
|
||||||
@@ -367,19 +287,10 @@ export default {
|
|||||||
title() {
|
title() {
|
||||||
return this.mediaMetadata.title || 'No Title'
|
return this.mediaMetadata.title || 'No Title'
|
||||||
},
|
},
|
||||||
publishedYear() {
|
|
||||||
return this.mediaMetadata.publishedYear
|
|
||||||
},
|
|
||||||
narrator() {
|
|
||||||
return this.mediaMetadata.narratorName
|
|
||||||
},
|
|
||||||
bookSubtitle() {
|
bookSubtitle() {
|
||||||
if (this.isPodcast) return null
|
if (this.isPodcast) return null
|
||||||
return this.mediaMetadata.subtitle
|
return this.mediaMetadata.subtitle
|
||||||
},
|
},
|
||||||
genres() {
|
|
||||||
return this.mediaMetadata.genres || []
|
|
||||||
},
|
|
||||||
podcastAuthor() {
|
podcastAuthor() {
|
||||||
return this.mediaMetadata.author || ''
|
return this.mediaMetadata.author || ''
|
||||||
},
|
},
|
||||||
@@ -389,25 +300,6 @@ export default {
|
|||||||
musicArtists() {
|
musicArtists() {
|
||||||
return this.mediaMetadata.artists || []
|
return this.mediaMetadata.artists || []
|
||||||
},
|
},
|
||||||
musicAlbum() {
|
|
||||||
return this.mediaMetadata.album || ''
|
|
||||||
},
|
|
||||||
musicAlbumArtist() {
|
|
||||||
return this.mediaMetadata.albumArtist || ''
|
|
||||||
},
|
|
||||||
musicTrackPretty() {
|
|
||||||
if (!this.mediaMetadata.trackNumber) return null
|
|
||||||
if (!this.mediaMetadata.trackTotal) return this.mediaMetadata.trackNumber
|
|
||||||
return `${this.mediaMetadata.trackNumber} / ${this.mediaMetadata.trackTotal}`
|
|
||||||
},
|
|
||||||
musicDiscPretty() {
|
|
||||||
if (!this.mediaMetadata.discNumber) return null
|
|
||||||
if (!this.mediaMetadata.discTotal) return this.mediaMetadata.discNumber
|
|
||||||
return `${this.mediaMetadata.discNumber} / ${this.mediaMetadata.discTotal}`
|
|
||||||
},
|
|
||||||
narrators() {
|
|
||||||
return this.mediaMetadata.narrators || []
|
|
||||||
},
|
|
||||||
series() {
|
series() {
|
||||||
return this.mediaMetadata.series || []
|
return this.mediaMetadata.series || []
|
||||||
},
|
},
|
||||||
@@ -421,26 +313,10 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
durationPretty() {
|
|
||||||
if (this.isPodcast) return this.$elapsedPrettyExtended(this.totalPodcastDuration)
|
|
||||||
|
|
||||||
if (!this.tracks.length && !this.audioFile) return 'N/A'
|
|
||||||
if (this.audioFile) return this.$elapsedPrettyExtended(this.duration)
|
|
||||||
return this.$elapsedPretty(this.duration)
|
|
||||||
},
|
|
||||||
duration() {
|
duration() {
|
||||||
if (!this.tracks.length && !this.audioFile) return 0
|
if (!this.tracks.length && !this.audioFile) return 0
|
||||||
return this.media.duration
|
return this.media.duration
|
||||||
},
|
},
|
||||||
totalPodcastDuration() {
|
|
||||||
if (!this.podcastEpisodes.length) return 0
|
|
||||||
let totalDuration = 0
|
|
||||||
this.podcastEpisodes.forEach((ep) => (totalDuration += ep.duration || 0))
|
|
||||||
return totalDuration
|
|
||||||
},
|
|
||||||
sizePretty() {
|
|
||||||
return this.$bytesPretty(this.media.size)
|
|
||||||
},
|
|
||||||
libraryFiles() {
|
libraryFiles() {
|
||||||
return this.libraryItem.libraryFiles || []
|
return this.libraryItem.libraryFiles || []
|
||||||
},
|
},
|
||||||
@@ -726,10 +602,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
clearProgressClick() {
|
clearProgressClick() {
|
||||||
|
if (!this.userMediaProgress) return
|
||||||
if (confirm(`Are you sure you want to reset your progress?`)) {
|
if (confirm(`Are you sure you want to reset your progress?`)) {
|
||||||
this.resettingProgress = true
|
this.resettingProgress = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$delete(`/api/me/progress/${this.libraryItemId}`)
|
.$delete(`/api/me/progress/${this.userMediaProgress.id}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('Progress reset complete')
|
console.log('Progress reset complete')
|
||||||
this.$toast.success(`Your progress was reset`)
|
this.$toast.success(`Your progress was reset`)
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
<template>
|
||||||
|
<div class="page relative" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
|
<app-book-shelf-toolbar page="narrators" is-home />
|
||||||
|
<div id="bookshelf" class="w-full h-full px-1 py-4 md:p-8 relative overflow-y-auto">
|
||||||
|
<table class="tracksTable max-w-2xl mx-auto">
|
||||||
|
<tr>
|
||||||
|
<th class="text-left">{{ $strings.LabelName }}</th>
|
||||||
|
<th class="text-center w-24">{{ $strings.LabelBooks }}</th>
|
||||||
|
<th v-if="userCanUpdate" class="w-40"></th>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="narrator in narrators" :key="narrator.id">
|
||||||
|
<td>
|
||||||
|
<p v-if="selectedNarrator?.id !== narrator.id" class="text-sm md:text-base text-gray-100">{{ narrator.name }}</p>
|
||||||
|
<form v-else @submit.prevent="saveClick">
|
||||||
|
<ui-text-input v-model="newNarratorName" />
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
<td class="text-center w-24">
|
||||||
|
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${narrator.id}`" class="hover:underline">{{ narrator.numBooks }}</nuxt-link>
|
||||||
|
</td>
|
||||||
|
<td v-if="userCanUpdate" class="w-40">
|
||||||
|
<div class="flex justify-end items-center h-10">
|
||||||
|
<template v-if="selectedNarrator?.id !== narrator.id">
|
||||||
|
<ui-icon-btn icon="edit" borderless :size="8" icon-font-size="1.1rem" class="mx-1" @click="editClick(narrator)" />
|
||||||
|
<ui-icon-btn icon="delete" borderless :size="8" icon-font-size="1.1rem" @click="removeClick(narrator)" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<ui-btn color="success" small class="mr-2" @click.stop="saveClick">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
|
<ui-btn small @click.stop="cancelEditClick">{{ $strings.ButtonCancel }}</ui-btn>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="absolute top-0 left-0 w-full h-[calc(100%-40px)] mt-10 flex items-center justify-center bg-black/25">
|
||||||
|
<ui-loading-indicator />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
async asyncData({ store, params, redirect, query, app }) {
|
||||||
|
const libraryId = params.library
|
||||||
|
const 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,
|
||||||
|
narrators: [],
|
||||||
|
selectedNarrator: null,
|
||||||
|
newNarratorName: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
streamLibraryItem() {
|
||||||
|
return this.$store.state.streamLibraryItem
|
||||||
|
},
|
||||||
|
currentLibraryId() {
|
||||||
|
return this.$store.state.libraries.currentLibraryId
|
||||||
|
},
|
||||||
|
userCanUpdate() {
|
||||||
|
return this.$store.getters['user/getUserCanUpdate']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
removeClick(narrator) {
|
||||||
|
const payload = {
|
||||||
|
message: this.$getString('MessageConfirmRemoveNarrator', [narrator.name]),
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.removeNarrator(narrator.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
|
editClick(narrator) {
|
||||||
|
this.selectedNarrator = narrator
|
||||||
|
this.newNarratorName = narrator.name
|
||||||
|
},
|
||||||
|
cancelEditClick() {
|
||||||
|
this.selectedNarrator = null
|
||||||
|
this.newNarratorName = null
|
||||||
|
},
|
||||||
|
saveClick() {
|
||||||
|
if (!this.selectedNarrator) return
|
||||||
|
this.newNarratorName = this.newNarratorName?.trim() || ''
|
||||||
|
if (!this.newNarratorName || this.newNarratorName === this.selectedNarrator.name) {
|
||||||
|
this.cancelEditClick()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.loading = true
|
||||||
|
this.$axios
|
||||||
|
.$patch(`/api/libraries/${this.currentLibraryId}/narrators/${this.selectedNarrator.id}`, { name: this.newNarratorName })
|
||||||
|
.then((data) => {
|
||||||
|
if (data.updated) {
|
||||||
|
this.$toast.success(this.$getString('MessageItemsUpdated', [data.updated]))
|
||||||
|
} else {
|
||||||
|
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||||
|
}
|
||||||
|
this.cancelEditClick()
|
||||||
|
this.init()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to updated narrator', error)
|
||||||
|
this.$toast.error('Failed to update narrator')
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
removeNarrator(id) {
|
||||||
|
this.loading = true
|
||||||
|
this.$axios
|
||||||
|
.$delete(`/api/libraries/${this.currentLibraryId}/narrators/${id}`)
|
||||||
|
.then((data) => {
|
||||||
|
if (data.updated) {
|
||||||
|
this.$toast.success(this.$getString('MessageItemsUpdated', [data.updated]))
|
||||||
|
} else {
|
||||||
|
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
|
||||||
|
}
|
||||||
|
this.init()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to remove narrator', error)
|
||||||
|
this.$toast.error('Failed to remove narrator')
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async init() {
|
||||||
|
this.narrators = await this.$axios
|
||||||
|
.$get(`/api/libraries/${this.currentLibraryId}/narrators`)
|
||||||
|
.then((response) => response.narrators)
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to load narrators', error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
beforeDestroy() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
<widgets-podcast-type-indicator :type="episode.episodeType" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-sm text-gray-200 mb-4">{{ episode.subtitle }}</p>
|
<p class="text-sm text-gray-200 mb-4 episode-subtitle-long" v-html="episode.subtitle || episode.description" />
|
||||||
|
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="episode.progress && episode.progress.isFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick(episode)">
|
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="episode.progress && episode.progress.isFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick(episode)">
|
||||||
|
|||||||
@@ -11,27 +11,27 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ store, params, redirect, query, app }) {
|
async asyncData({ store, params, redirect, query, app }) {
|
||||||
var libraryId = params.library
|
const libraryId = params.library
|
||||||
var library = await store.dispatch('libraries/fetch', libraryId)
|
const library = await store.dispatch('libraries/fetch', libraryId)
|
||||||
if (!library) {
|
if (!library) {
|
||||||
return redirect('/oops?message=Library not found')
|
return redirect('/oops?message=Library not found')
|
||||||
}
|
}
|
||||||
var query = query.q
|
let results = await app.$axios.$get(`/api/libraries/${libraryId}/search?q=${query.q}`).catch((error) => {
|
||||||
var results = await app.$axios.$get(`/api/libraries/${libraryId}/search?q=${query}`).catch((error) => {
|
|
||||||
console.error('Failed to search library', error)
|
console.error('Failed to search library', error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
results = {
|
results = {
|
||||||
podcasts: results && results.podcast ? results.podcast : null,
|
podcasts: results?.podcast || [],
|
||||||
books: results && results.book ? results.book : null,
|
books: results?.book || [],
|
||||||
authors: results && results.authors.length ? results.authors : null,
|
authors: results?.authors || [],
|
||||||
series: results && results.series.length ? results.series : null,
|
series: results?.series || [],
|
||||||
tags: results && results.tags.length ? results.tags : null
|
tags: results?.tags || [],
|
||||||
|
narrators: results?.narrators || []
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
libraryId,
|
libraryId,
|
||||||
results,
|
results,
|
||||||
query
|
query: query.q
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -55,16 +55,17 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async search() {
|
async search() {
|
||||||
var results = await this.$axios.$get(`/api/libraries/${this.libraryId}/search?q=${this.query}`).catch((error) => {
|
const results = await this.$axios.$get(`/api/libraries/${this.libraryId}/search?q=${this.query}`).catch((error) => {
|
||||||
console.error('Failed to search library', error)
|
console.error('Failed to search library', error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
this.results = {
|
this.results = {
|
||||||
podcasts: results && results.podcast ? results.podcast : null,
|
podcasts: results?.podcast || [],
|
||||||
books: results && results.book ? results.book : null,
|
books: results?.book || [],
|
||||||
authors: results && results.authors.length ? results.authors : null,
|
authors: results?.authors || [],
|
||||||
series: results && results.series.length ? results.series : null,
|
series: results?.series || [],
|
||||||
tags: results && results.tags.length ? results.tags : null
|
tags: results?.tags || [],
|
||||||
|
narrators: results?.narrators || []
|
||||||
}
|
}
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (this.$refs.bookshelf) {
|
if (this.$refs.bookshelf) {
|
||||||
|
|||||||
@@ -78,6 +78,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Path from 'path'
|
||||||
import uploadHelpers from '@/mixins/uploadHelpers'
|
import uploadHelpers from '@/mixins/uploadHelpers'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -243,7 +244,7 @@ export default {
|
|||||||
ref.setUploadStatus(status)
|
ref.setUploadStatus(status)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
uploadItem(item) {
|
async uploadItem(item) {
|
||||||
var form = new FormData()
|
var form = new FormData()
|
||||||
form.set('title', item.title)
|
form.set('title', item.title)
|
||||||
if (!this.selectedLibraryIsPodcast) {
|
if (!this.selectedLibraryIsPodcast) {
|
||||||
@@ -294,18 +295,41 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var items = this.validateItems()
|
const items = this.validateItems()
|
||||||
if (!items) {
|
if (!items) {
|
||||||
this.$toast.error('Some invalid items')
|
this.$toast.error('Some invalid items')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.processing = true
|
this.processing = true
|
||||||
var itemsUploaded = 0
|
|
||||||
var itemsFailed = 0
|
const itemsToUpload = []
|
||||||
for (let i = 0; i < items.length; i++) {
|
|
||||||
var item = items[i]
|
// Check if path already exists before starting upload
|
||||||
|
// uploading fails if path already exists
|
||||||
|
for (const item of items) {
|
||||||
|
const filepath = Path.join(this.selectedFolder.fullPath, item.directory)
|
||||||
|
const exists = await this.$axios
|
||||||
|
.$post(`/api/filesystem/pathexists`, { filepath })
|
||||||
|
.then((data) => {
|
||||||
|
if (data.exists) {
|
||||||
|
this.$toast.error(`Filepath "${filepath}" already exists on server`)
|
||||||
|
}
|
||||||
|
return data.exists
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to check if filepath exists', error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if (!exists) {
|
||||||
|
itemsToUpload.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemsUploaded = 0
|
||||||
|
let itemsFailed = 0
|
||||||
|
for (const item of itemsToUpload) {
|
||||||
this.updateItemCardStatus(item.index, 'uploading')
|
this.updateItemCardStatus(item.index, 'uploading')
|
||||||
var result = await this.uploadItem(item)
|
const result = await this.uploadItem(item)
|
||||||
if (result) itemsUploaded++
|
if (result) itemsUploaded++
|
||||||
else itemsFailed++
|
else itemsFailed++
|
||||||
this.updateItemCardStatus(item.index, result ? 'success' : 'failed')
|
this.updateItemCardStatus(item.index, result ? 'success' : 'failed')
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ export default class PlayerHandler {
|
|||||||
|
|
||||||
this.failedProgressSyncs = 0
|
this.failedProgressSyncs = 0
|
||||||
this.lastSyncTime = 0
|
this.lastSyncTime = 0
|
||||||
this.lastSyncedAt = 0
|
|
||||||
this.listeningTimeSinceSync = 0
|
this.listeningTimeSinceSync = 0
|
||||||
|
|
||||||
this.playInterval = null
|
this.playInterval = null
|
||||||
@@ -53,6 +52,11 @@ export default class PlayerHandler {
|
|||||||
return this.libraryItem.media.episodes.find(ep => ep.id === this.episodeId)
|
return this.libraryItem.media.episodes.find(ep => ep.id === this.episodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setSessionId(sessionId) {
|
||||||
|
this.currentSessionId = sessionId
|
||||||
|
this.ctx.$store.commit('setPlaybackSessionId', sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
load(libraryItem, episodeId, playWhenReady, playbackRate, startTimeOverride = undefined) {
|
load(libraryItem, episodeId, playWhenReady, playbackRate, startTimeOverride = undefined) {
|
||||||
this.libraryItem = libraryItem
|
this.libraryItem = libraryItem
|
||||||
this.isVideo = libraryItem.mediaType === 'video'
|
this.isVideo = libraryItem.mediaType === 'video'
|
||||||
@@ -183,7 +187,7 @@ export default class PlayerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async prepare(forceTranscode = false) {
|
async prepare(forceTranscode = false) {
|
||||||
this.currentSessionId = null // Reset session
|
this.setSessionId(null) // Reset session
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
deviceInfo: {
|
deviceInfo: {
|
||||||
@@ -210,6 +214,8 @@ export default class PlayerHandler {
|
|||||||
this.playWhenReady = false
|
this.playWhenReady = false
|
||||||
this.initialPlaybackRate = playbackRate
|
this.initialPlaybackRate = playbackRate
|
||||||
this.startTimeOverride = undefined
|
this.startTimeOverride = undefined
|
||||||
|
this.lastSyncTime = 0
|
||||||
|
this.listeningTimeSinceSync = 0
|
||||||
|
|
||||||
this.prepareSession(session)
|
this.prepareSession(session)
|
||||||
}
|
}
|
||||||
@@ -217,7 +223,7 @@ export default class PlayerHandler {
|
|||||||
prepareSession(session) {
|
prepareSession(session) {
|
||||||
this.failedProgressSyncs = 0
|
this.failedProgressSyncs = 0
|
||||||
this.startTime = this.startTimeOverride !== undefined ? this.startTimeOverride : session.currentTime
|
this.startTime = this.startTimeOverride !== undefined ? this.startTimeOverride : session.currentTime
|
||||||
this.currentSessionId = session.id
|
this.setSessionId(session.id)
|
||||||
this.displayTitle = session.displayTitle
|
this.displayTitle = session.displayTitle
|
||||||
this.displayAuthor = session.displayAuthor
|
this.displayAuthor = session.displayAuthor
|
||||||
|
|
||||||
@@ -262,7 +268,7 @@ export default class PlayerHandler {
|
|||||||
this.player = null
|
this.player = null
|
||||||
this.playerState = 'IDLE'
|
this.playerState = 'IDLE'
|
||||||
this.libraryItem = null
|
this.libraryItem = null
|
||||||
this.currentSessionId = null
|
this.setSessionId(null)
|
||||||
this.startTime = 0
|
this.startTime = 0
|
||||||
this.stopPlayInterval()
|
this.stopPlayInterval()
|
||||||
}
|
}
|
||||||
@@ -287,7 +293,8 @@ export default class PlayerHandler {
|
|||||||
const exactTimeElapsed = ((Date.now() - lastTick) / 1000)
|
const exactTimeElapsed = ((Date.now() - lastTick) / 1000)
|
||||||
lastTick = Date.now()
|
lastTick = Date.now()
|
||||||
this.listeningTimeSinceSync += exactTimeElapsed
|
this.listeningTimeSinceSync += exactTimeElapsed
|
||||||
if (this.listeningTimeSinceSync >= 5) {
|
const TimeToWaitBeforeSync = this.lastSyncTime > 0 ? 5 : 20
|
||||||
|
if (this.listeningTimeSinceSync >= TimeToWaitBeforeSync) {
|
||||||
this.sendProgressSync(currentTime)
|
this.sendProgressSync(currentTime)
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
@@ -297,13 +304,17 @@ export default class PlayerHandler {
|
|||||||
let syncData = null
|
let syncData = null
|
||||||
if (this.player) {
|
if (this.player) {
|
||||||
const listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
|
const listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
|
||||||
syncData = {
|
// When opening player and quickly closing dont save progress
|
||||||
timeListened: listeningTimeToAdd,
|
if (listeningTimeToAdd > 20) {
|
||||||
duration: this.getDuration(),
|
syncData = {
|
||||||
currentTime: this.getCurrentTime()
|
timeListened: listeningTimeToAdd,
|
||||||
|
duration: this.getDuration(),
|
||||||
|
currentTime: this.getCurrentTime()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.listeningTimeSinceSync = 0
|
this.listeningTimeSinceSync = 0
|
||||||
|
this.lastSyncTime = 0
|
||||||
return this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/close`, syncData, { timeout: 1000 }).catch((error) => {
|
return this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/close`, syncData, { timeout: 1000 }).catch((error) => {
|
||||||
console.error('Failed to close session', error)
|
console.error('Failed to close session', error)
|
||||||
})
|
})
|
||||||
@@ -322,6 +333,7 @@ export default class PlayerHandler {
|
|||||||
duration: this.getDuration(),
|
duration: this.getDuration(),
|
||||||
currentTime
|
currentTime
|
||||||
}
|
}
|
||||||
|
|
||||||
this.listeningTimeSinceSync = 0
|
this.listeningTimeSinceSync = 0
|
||||||
this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/sync`, syncData, { timeout: 3000 }).then(() => {
|
this.ctx.$axios.$post(`/api/session/${this.currentSessionId}/sync`, syncData, { timeout: 3000 }).then(() => {
|
||||||
this.failedProgressSyncs = 0
|
this.failedProgressSyncs = 0
|
||||||
@@ -383,13 +395,13 @@ export default class PlayerHandler {
|
|||||||
this.player.setPlaybackRate(playbackRate)
|
this.player.setPlaybackRate(playbackRate)
|
||||||
}
|
}
|
||||||
|
|
||||||
seek(time) {
|
seek(time, shouldSync = true) {
|
||||||
if (!this.player) return
|
if (!this.player) return
|
||||||
this.player.seek(time, this.playerPlaying)
|
this.player.seek(time, this.playerPlaying)
|
||||||
this.ctx.setCurrentTime(time)
|
this.ctx.setCurrentTime(time)
|
||||||
|
|
||||||
// Update progress if paused
|
// Update progress if paused
|
||||||
if (!this.playerPlaying) {
|
if (!this.playerPlaying && shouldSync) {
|
||||||
this.sendProgressSync(time)
|
this.sendProgressSync(time)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const SupportedFileTypes = {
|
|||||||
ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
ebook: ['epub', 'pdf', 'mobi', 'azw3', 'cbr', 'cbz'],
|
||||||
info: ['nfo'],
|
info: ['nfo'],
|
||||||
text: ['txt'],
|
text: ['txt'],
|
||||||
metadata: ['opf', 'abs']
|
metadata: ['opf', 'abs', 'xml', 'json']
|
||||||
}
|
}
|
||||||
|
|
||||||
const DownloadStatus = {
|
const DownloadStatus = {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const languageCodeMap = {
|
|||||||
'fr': { label: 'Français', dateFnsLocale: 'fr' },
|
'fr': { label: 'Français', dateFnsLocale: 'fr' },
|
||||||
'hr': { label: 'Hrvatski', dateFnsLocale: 'hr' },
|
'hr': { label: 'Hrvatski', dateFnsLocale: 'hr' },
|
||||||
'it': { label: 'Italiano', dateFnsLocale: 'it' },
|
'it': { label: 'Italiano', dateFnsLocale: 'it' },
|
||||||
|
'nl': { label: 'Nederlands', dateFnsLocale: 'nl' },
|
||||||
'pl': { label: 'Polski', dateFnsLocale: 'pl' },
|
'pl': { label: 'Polski', dateFnsLocale: 'pl' },
|
||||||
'ru': { label: 'Русский', dateFnsLocale: 'ru' },
|
'ru': { label: 'Русский', dateFnsLocale: 'ru' },
|
||||||
'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' },
|
'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' },
|
||||||
@@ -74,10 +75,9 @@ async function loadi18n(code) {
|
|||||||
for (const key in Vue.prototype.$strings) {
|
for (const key in Vue.prototype.$strings) {
|
||||||
Vue.prototype.$strings[key] = strings[key] || translations[defaultCode][key]
|
Vue.prototype.$strings[key] = strings[key] || translations[defaultCode][key]
|
||||||
}
|
}
|
||||||
console.log(`dateFnsLocale = ${languageCodeMap[code].dateFnsLocale}`)
|
|
||||||
Vue.prototype.$setDateFnsLocale(languageCodeMap[code].dateFnsLocale)
|
Vue.prototype.$setDateFnsLocale(languageCodeMap[code].dateFnsLocale)
|
||||||
|
|
||||||
console.log('i18n strings=', Vue.prototype.$strings)
|
|
||||||
this.$eventBus.$emit('change-lang', code)
|
this.$eventBus.$emit('change-lang', code)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -6,6 +6,7 @@ export const state = () => ({
|
|||||||
Source: null,
|
Source: null,
|
||||||
versionData: null,
|
versionData: null,
|
||||||
serverSettings: null,
|
serverSettings: null,
|
||||||
|
playbackSessionId: null,
|
||||||
streamLibraryItem: null,
|
streamLibraryItem: null,
|
||||||
streamEpisodeId: null,
|
streamEpisodeId: null,
|
||||||
streamIsPlaying: false,
|
streamIsPlaying: false,
|
||||||
@@ -35,7 +36,7 @@ export const getters = {
|
|||||||
return state.serverSettings[key]
|
return state.serverSettings[key]
|
||||||
},
|
},
|
||||||
getLibraryItemIdStreaming: state => {
|
getLibraryItemIdStreaming: state => {
|
||||||
return state.streamLibraryItem ? state.streamLibraryItem.id : null
|
return state.streamLibraryItem?.id || null
|
||||||
},
|
},
|
||||||
getIsStreamingFromDifferentLibrary: (state, getters, rootState) => {
|
getIsStreamingFromDifferentLibrary: (state, getters, rootState) => {
|
||||||
if (!state.streamLibraryItem) return false
|
if (!state.streamLibraryItem) return false
|
||||||
@@ -150,6 +151,9 @@ export const mutations = {
|
|||||||
if (!settings) return
|
if (!settings) return
|
||||||
state.serverSettings = settings
|
state.serverSettings = settings
|
||||||
},
|
},
|
||||||
|
setPlaybackSessionId(state, playbackSessionId) {
|
||||||
|
state.playbackSessionId = playbackSessionId
|
||||||
|
},
|
||||||
setMediaPlaying(state, payload) {
|
setMediaPlaying(state, payload) {
|
||||||
if (!payload) {
|
if (!payload) {
|
||||||
state.streamLibraryItem = null
|
state.streamLibraryItem = null
|
||||||
|
|||||||
@@ -63,6 +63,12 @@ export const state = () => ({
|
|||||||
text: 'iTunes',
|
text: 'iTunes',
|
||||||
value: 'itunes'
|
value: 'itunes'
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
coverOnlyProviders: [
|
||||||
|
{
|
||||||
|
text: 'AudiobookCovers.com',
|
||||||
|
value: 'audiobookcovers'
|
||||||
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+41
-35
@@ -20,7 +20,7 @@
|
|||||||
"ButtonCreate": "Erstellen",
|
"ButtonCreate": "Erstellen",
|
||||||
"ButtonCreateBackup": "Sicherung erstellen",
|
"ButtonCreateBackup": "Sicherung erstellen",
|
||||||
"ButtonDelete": "Löschen",
|
"ButtonDelete": "Löschen",
|
||||||
"ButtonDownloadQueue": "Queue",
|
"ButtonDownloadQueue": "Warteschlange",
|
||||||
"ButtonEdit": "Bearbeiten",
|
"ButtonEdit": "Bearbeiten",
|
||||||
"ButtonEditChapters": "Kapitel bearbeiten",
|
"ButtonEditChapters": "Kapitel bearbeiten",
|
||||||
"ButtonEditPodcast": "Podcast bearbeiten",
|
"ButtonEditPodcast": "Podcast bearbeiten",
|
||||||
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "Alles löschen",
|
"ButtonRemoveAll": "Alles löschen",
|
||||||
"ButtonRemoveAllLibraryItems": "Lösche alle Bibliothekseinträge",
|
"ButtonRemoveAllLibraryItems": "Lösche alle Bibliothekseinträge",
|
||||||
"ButtonRemoveFromContinueListening": "Lösche den Eintrag aus der Fortsetzungsliste",
|
"ButtonRemoveFromContinueListening": "Lösche den Eintrag aus der Fortsetzungsliste",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Lösche die Serie aus der Serienfortsetzungsliste",
|
"ButtonRemoveSeriesFromContinueSeries": "Lösche die Serie aus der Serienfortsetzungsliste",
|
||||||
"ButtonReScan": "Neu scannen",
|
"ButtonReScan": "Neu scannen",
|
||||||
"ButtonReset": "Zurücksetzen",
|
"ButtonReset": "Zurücksetzen",
|
||||||
@@ -93,9 +94,9 @@
|
|||||||
"HeaderCollection": "Sammlungen",
|
"HeaderCollection": "Sammlungen",
|
||||||
"HeaderCollectionItems": "Sammlungseinträge",
|
"HeaderCollectionItems": "Sammlungseinträge",
|
||||||
"HeaderCover": "Titelbild",
|
"HeaderCover": "Titelbild",
|
||||||
"HeaderCurrentDownloads": "Current Downloads",
|
"HeaderCurrentDownloads": "Aktuelle Downloads",
|
||||||
"HeaderDetails": "Details",
|
"HeaderDetails": "Details",
|
||||||
"HeaderDownloadQueue": "Download Queue",
|
"HeaderDownloadQueue": "Download Warteschlange",
|
||||||
"HeaderEpisodes": "Episoden",
|
"HeaderEpisodes": "Episoden",
|
||||||
"HeaderFiles": "Dateien",
|
"HeaderFiles": "Dateien",
|
||||||
"HeaderFindChapters": "Kapitel suchen",
|
"HeaderFindChapters": "Kapitel suchen",
|
||||||
@@ -142,8 +143,8 @@
|
|||||||
"HeaderSettingsGeneral": "Allgemein",
|
"HeaderSettingsGeneral": "Allgemein",
|
||||||
"HeaderSettingsScanner": "Scanner",
|
"HeaderSettingsScanner": "Scanner",
|
||||||
"HeaderSleepTimer": "Einschlaf-Timer",
|
"HeaderSleepTimer": "Einschlaf-Timer",
|
||||||
"HeaderStatsLargestItems": "Largest Items",
|
"HeaderStatsLargestItems": "Größte Medien",
|
||||||
"HeaderStatsLongestItems": "Längste Einträge (h)",
|
"HeaderStatsLongestItems": "Längste Medien (h)",
|
||||||
"HeaderStatsMinutesListeningChart": "Hörminuten (letzte 7 Tage)",
|
"HeaderStatsMinutesListeningChart": "Hörminuten (letzte 7 Tage)",
|
||||||
"HeaderStatsRecentSessions": "Neueste Ereignisse",
|
"HeaderStatsRecentSessions": "Neueste Ereignisse",
|
||||||
"HeaderStatsTop10Authors": "Top 10 Autoren",
|
"HeaderStatsTop10Authors": "Top 10 Autoren",
|
||||||
@@ -161,7 +162,7 @@
|
|||||||
"LabelAccountTypeGuest": "Gast",
|
"LabelAccountTypeGuest": "Gast",
|
||||||
"LabelAccountTypeUser": "Benutzer",
|
"LabelAccountTypeUser": "Benutzer",
|
||||||
"LabelActivity": "Aktivitäten",
|
"LabelActivity": "Aktivitäten",
|
||||||
"LabelAdded": "Added",
|
"LabelAdded": "Hinzugefügt",
|
||||||
"LabelAddedAt": "Hinzugefügt am",
|
"LabelAddedAt": "Hinzugefügt am",
|
||||||
"LabelAddToCollection": "Zur Sammlung hinzufügen",
|
"LabelAddToCollection": "Zur Sammlung hinzufügen",
|
||||||
"LabelAddToCollectionBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Sammlung hinzu",
|
"LabelAddToCollectionBatch": "Füge {0} Hörbüch(er)/Podcast(s) der Sammlung hinzu",
|
||||||
@@ -169,7 +170,7 @@
|
|||||||
"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",
|
||||||
"LabelAll": "Alle",
|
"LabelAll": "Alle",
|
||||||
"LabelAllUsers": "Alle Benutzer",
|
"LabelAllUsers": "Alle Benutzer",
|
||||||
"LabelAlreadyInYourLibrary": "Already in your library",
|
"LabelAlreadyInYourLibrary": "In der Bibliothek vorhanden",
|
||||||
"LabelAppend": "Anhängen",
|
"LabelAppend": "Anhängen",
|
||||||
"LabelAuthor": "Autor",
|
"LabelAuthor": "Autor",
|
||||||
"LabelAuthorFirstLast": "Autor (Vorname Nachname)",
|
"LabelAuthorFirstLast": "Autor (Vorname Nachname)",
|
||||||
@@ -186,7 +187,7 @@
|
|||||||
"LabelBitrate": "Bitrate",
|
"LabelBitrate": "Bitrate",
|
||||||
"LabelBooks": "Bücher",
|
"LabelBooks": "Bücher",
|
||||||
"LabelChangePassword": "Passwort ändern",
|
"LabelChangePassword": "Passwort ändern",
|
||||||
"LabelChannels": "Channels",
|
"LabelChannels": "Kanäle",
|
||||||
"LabelChapters": "Chapters",
|
"LabelChapters": "Chapters",
|
||||||
"LabelChaptersFound": "gefundene Kapitel",
|
"LabelChaptersFound": "gefundene Kapitel",
|
||||||
"LabelChapterTitle": "Kapitelüberschrift",
|
"LabelChapterTitle": "Kapitelüberschrift",
|
||||||
@@ -197,14 +198,15 @@
|
|||||||
"LabelComplete": "Vollständig",
|
"LabelComplete": "Vollständig",
|
||||||
"LabelConfirmPassword": "Passwort bestätigen",
|
"LabelConfirmPassword": "Passwort bestätigen",
|
||||||
"LabelContinueListening": "Weiterhören",
|
"LabelContinueListening": "Weiterhören",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Serien fortsetzen",
|
"LabelContinueSeries": "Serien fortsetzen",
|
||||||
"LabelCover": "Titelbild",
|
"LabelCover": "Titelbild",
|
||||||
"LabelCoverImageURL": "URL des Titelbildes",
|
"LabelCoverImageURL": "URL des Titelbildes",
|
||||||
"LabelCreatedAt": "Erstellt am",
|
"LabelCreatedAt": "Erstellt am",
|
||||||
"LabelCronExpression": "Cron Ausdruck",
|
"LabelCronExpression": "Cron-Ausdruck",
|
||||||
"LabelCurrent": "Aktuell",
|
"LabelCurrent": "Aktuell",
|
||||||
"LabelCurrently": "Aktuell:",
|
"LabelCurrently": "Aktuell:",
|
||||||
"LabelCustomCronExpression": "Custom Cron Expression:",
|
"LabelCustomCronExpression": "Benutzerdefinierter Cron-Ausdruck",
|
||||||
"LabelDatetime": "Datum & Uhrzeit",
|
"LabelDatetime": "Datum & Uhrzeit",
|
||||||
"LabelDescription": "Beschreibung",
|
"LabelDescription": "Beschreibung",
|
||||||
"LabelDeselectAll": "Alles abwählen",
|
"LabelDeselectAll": "Alles abwählen",
|
||||||
@@ -217,13 +219,13 @@
|
|||||||
"LabelDuration": "Laufzeit",
|
"LabelDuration": "Laufzeit",
|
||||||
"LabelDurationFound": "Gefundene Laufzeit:",
|
"LabelDurationFound": "Gefundene Laufzeit:",
|
||||||
"LabelEdit": "Bearbeiten",
|
"LabelEdit": "Bearbeiten",
|
||||||
"LabelEmbeddedCover": "Embedded Cover",
|
"LabelEmbeddedCover": "Eingebettetes Cover",
|
||||||
"LabelEnable": "Aktivieren",
|
"LabelEnable": "Aktivieren",
|
||||||
"LabelEnd": "Ende",
|
"LabelEnd": "Ende",
|
||||||
"LabelEpisode": "Episode",
|
"LabelEpisode": "Episode",
|
||||||
"LabelEpisodeTitle": "Episodentitel",
|
"LabelEpisodeTitle": "Episodentitel",
|
||||||
"LabelEpisodeType": "Episodentyp",
|
"LabelEpisodeType": "Episodentyp",
|
||||||
"LabelExample": "Example",
|
"LabelExample": "Beispiel",
|
||||||
"LabelExplicit": "Explizit (Altersbeschränkung)",
|
"LabelExplicit": "Explizit (Altersbeschränkung)",
|
||||||
"LabelFeedURL": "Feed URL",
|
"LabelFeedURL": "Feed URL",
|
||||||
"LabelFile": "Datei",
|
"LabelFile": "Datei",
|
||||||
@@ -254,11 +256,12 @@
|
|||||||
"LabelIntervalEveryDay": "Jeden Tag",
|
"LabelIntervalEveryDay": "Jeden Tag",
|
||||||
"LabelIntervalEveryHour": "Jede Stunde",
|
"LabelIntervalEveryHour": "Jede Stunde",
|
||||||
"LabelInvalidParts": "Ungültige Teile",
|
"LabelInvalidParts": "Ungültige Teile",
|
||||||
|
"LabelInvert": "Invert",
|
||||||
"LabelItem": "Medium",
|
"LabelItem": "Medium",
|
||||||
"LabelLanguage": "Sprache",
|
"LabelLanguage": "Sprache",
|
||||||
"LabelLanguageDefaultServer": "Standard-Server-Sprache",
|
"LabelLanguageDefaultServer": "Standard-Server-Sprache",
|
||||||
"LabelLastBookAdded": "Last Book Added",
|
"LabelLastBookAdded": "Zuletzt hinzugefügtes Medium",
|
||||||
"LabelLastBookUpdated": "Last Book Updated",
|
"LabelLastBookUpdated": "Zuletzt aktualisiertes Medium",
|
||||||
"LabelLastSeen": "Zuletzt angesehen",
|
"LabelLastSeen": "Zuletzt angesehen",
|
||||||
"LabelLastTime": "Letztes Mal",
|
"LabelLastTime": "Letztes Mal",
|
||||||
"LabelLastUpdate": "Letzte Aktualisierung",
|
"LabelLastUpdate": "Letzte Aktualisierung",
|
||||||
@@ -290,8 +293,8 @@
|
|||||||
"LabelNewestAuthors": "Neuste Autoren",
|
"LabelNewestAuthors": "Neuste Autoren",
|
||||||
"LabelNewestEpisodes": "Neueste Episoden",
|
"LabelNewestEpisodes": "Neueste Episoden",
|
||||||
"LabelNewPassword": "Neues Passwort",
|
"LabelNewPassword": "Neues Passwort",
|
||||||
"LabelNextBackupDate": "Next backup date",
|
"LabelNextBackupDate": "Nächstes Sicherungsdatum",
|
||||||
"LabelNextScheduledRun": "Next scheduled run",
|
"LabelNextScheduledRun": "Nächster planmäßiger Durchlauf",
|
||||||
"LabelNotes": "Hinweise",
|
"LabelNotes": "Hinweise",
|
||||||
"LabelNotFinished": "nicht beendet",
|
"LabelNotFinished": "nicht beendet",
|
||||||
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
||||||
@@ -322,24 +325,25 @@
|
|||||||
"LabelPlayMethod": "Abspielmethode",
|
"LabelPlayMethod": "Abspielmethode",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
"LabelPodcastType": "Podcast Type",
|
"LabelPodcastType": "Podcast Typ",
|
||||||
"LabelPrefixesToIgnore": "Zu ignorierende(s) Vorwort(e) (Groß- und Kleinschreibung wird nicht berücksichtigt)",
|
"LabelPrefixesToIgnore": "Zu ignorierende(s) Vorwort(e) (Groß- und Kleinschreibung wird nicht berücksichtigt)",
|
||||||
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
"LabelPreventIndexing": "Verhindere, dass dein Feed von iTunes- und Google-Podcast-Verzeichnissen indiziert wird",
|
||||||
"LabelProgress": "Fortschritt",
|
"LabelProgress": "Fortschritt",
|
||||||
"LabelProvider": "Anbieter",
|
"LabelProvider": "Anbieter",
|
||||||
"LabelPubDate": "Veröffentlichungsdatum",
|
"LabelPubDate": "Veröffentlichungsdatum",
|
||||||
"LabelPublisher": "Herausgeber",
|
"LabelPublisher": "Herausgeber",
|
||||||
"LabelPublishYear": "Jahr",
|
"LabelPublishYear": "Jahr",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
||||||
"LabelRecentSeries": "Aktuelle Serien",
|
"LabelRecentSeries": "Aktuelle Serien",
|
||||||
"LabelRecommended": "Recommended",
|
"LabelRecommended": "Empfohlen",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Veröffentlichungsdatum",
|
"LabelReleaseDate": "Veröffentlichungsdatum",
|
||||||
"LabelRemoveCover": "Lösche Titelbild",
|
"LabelRemoveCover": "Lösche Titelbild",
|
||||||
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
|
"LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail",
|
||||||
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
|
"LabelRSSFeedCustomOwnerName": "Benutzerdefinierter Name des Eigentümers",
|
||||||
"LabelRSSFeedOpen": "RSS Feed Offen",
|
"LabelRSSFeedOpen": "RSS Feed Offen",
|
||||||
"LabelRSSFeedPreventIndexing": "Prevent Indexing",
|
"LabelRSSFeedPreventIndexing": "Indizierung verhindern",
|
||||||
"LabelRSSFeedSlug": "RSS Feed Schlagwort",
|
"LabelRSSFeedSlug": "RSS Feed Schlagwort",
|
||||||
"LabelRSSFeedURL": "RSS Feed URL",
|
"LabelRSSFeedURL": "RSS Feed URL",
|
||||||
"LabelSearchTerm": "Begriff suchen",
|
"LabelSearchTerm": "Begriff suchen",
|
||||||
@@ -384,7 +388,7 @@
|
|||||||
"LabelSettingsStoreCoversWithItemHelp": "Standardmäßig werden die Titelbilder in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Titelbilder als jpg Datei in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"cover.jpg\" gespeichert.",
|
"LabelSettingsStoreCoversWithItemHelp": "Standardmäßig werden die Titelbilder in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Titelbilder als jpg Datei in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"cover.jpg\" gespeichert.",
|
||||||
"LabelSettingsStoreMetadataWithItem": "Metadaten als OPF-Datei im Medienordner speichern",
|
"LabelSettingsStoreMetadataWithItem": "Metadaten als OPF-Datei im Medienordner speichern",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"matadata.abs\" gespeichert.",
|
"LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"matadata.abs\" gespeichert.",
|
||||||
"LabelSettingsTimeFormat": "Time Format",
|
"LabelSettingsTimeFormat": "Zeitformat",
|
||||||
"LabelShowAll": "Alles anzeigen",
|
"LabelShowAll": "Alles anzeigen",
|
||||||
"LabelSize": "Größe",
|
"LabelSize": "Größe",
|
||||||
"LabelSleepTimer": "Einschlaf-Timer",
|
"LabelSleepTimer": "Einschlaf-Timer",
|
||||||
@@ -412,8 +416,9 @@
|
|||||||
"LabelTag": "Schlagwort",
|
"LabelTag": "Schlagwort",
|
||||||
"LabelTags": "Schlagwörter",
|
"LabelTags": "Schlagwörter",
|
||||||
"LabelTagsAccessibleToUser": "Für Benutzer zugängliche Schlagwörter",
|
"LabelTagsAccessibleToUser": "Für Benutzer zugängliche Schlagwörter",
|
||||||
"LabelTasks": "Tasks Running",
|
"LabelTagsNotAccessibleToUser": "Für Benutzer nicht zugängliche Schlagwörter",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTasks": "Laufende Aufgaben",
|
||||||
|
"LabelTimeBase": "Basiszeit",
|
||||||
"LabelTimeListened": "Gehörte Zeit",
|
"LabelTimeListened": "Gehörte Zeit",
|
||||||
"LabelTimeListenedToday": "Heute gehörte Zeit",
|
"LabelTimeListenedToday": "Heute gehörte Zeit",
|
||||||
"LabelTimeRemaining": "{0} verbleibend",
|
"LabelTimeRemaining": "{0} verbleibend",
|
||||||
@@ -462,25 +467,26 @@
|
|||||||
"MessageBackupsDescription": "In einer Sicherung werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder welche in <code>/metadata/items</code> & <code>/metadata/authors</code> gespeichert sind gespeichert. Sicherungen enthalten keine Dateien welche in den einzelnen Bibliotheksordnern (Medien-Ordnern) gespeichert sind.",
|
"MessageBackupsDescription": "In einer Sicherung werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder welche in <code>/metadata/items</code> & <code>/metadata/authors</code> gespeichert sind gespeichert. Sicherungen enthalten keine Dateien welche in den einzelnen Bibliotheksordnern (Medien-Ordnern) gespeichert sind.",
|
||||||
"MessageBatchQuickMatchDescription": "Der Schnellabgleich versucht, fehlende Titelbilder und Metadaten für die ausgewählten Artikel hinzuzufügen. Aktivieren Sie die nachstehenden Optionen, damit der Schnellabgleich vorhandene Titelbilder und/oder Metadaten überschreiben kann.",
|
"MessageBatchQuickMatchDescription": "Der Schnellabgleich versucht, fehlende Titelbilder und Metadaten für die ausgewählten Artikel hinzuzufügen. Aktivieren Sie die nachstehenden Optionen, damit der Schnellabgleich vorhandene Titelbilder und/oder Metadaten überschreiben kann.",
|
||||||
"MessageBookshelfNoCollections": "Es wurden noch keine Sammlungen erstellt",
|
"MessageBookshelfNoCollections": "Es wurden noch keine Sammlungen erstellt",
|
||||||
"MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für filter \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für Filter \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoRSSFeeds": "Keine RSS-Feeds geöffnet",
|
"MessageBookshelfNoRSSFeeds": "Keine RSS-Feeds geöffnet",
|
||||||
"MessageBookshelfNoSeries": "Keine Serien vorhanden",
|
"MessageBookshelfNoSeries": "Keine Serien vorhanden",
|
||||||
"MessageChapterEndIsAfter": "Ungültige Kapitelendzeit: Kapitelende > Mediumende (Kapitelende liegt nach dem Ende des Mediums)",
|
"MessageChapterEndIsAfter": "Ungültige Kapitelendzeit: Kapitelende > Mediumende (Kapitelende liegt nach dem Ende des Mediums)",
|
||||||
"MessageChapterErrorFirstNotZero": "Ungültige Kapitelstartzeit: Das erste Kapitel muss bei 0 beginnen",
|
"MessageChapterErrorFirstNotZero": "Ungültige Kapitelstartzeit: Das erste Kapitel muss bei 0 beginnen",
|
||||||
"MessageChapterErrorStartGteDuration": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumlänge (Kapitelanfang liegt zeitlich nach dem Ende des Mediums -> Lösung: Kapitelanfang < Mediumlänge)",
|
"MessageChapterErrorStartGteDuration": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumlänge (Kapitelanfang liegt zeitlich nach dem Ende des Mediums -> Lösung: Kapitelanfang < Mediumlänge)",
|
||||||
"MessageChapterErrorStartLtPrev": "Ungültige Kapitelstartzeit: Kapitelanfang < Kapitelanfang vorheriges Kapitel (Kapitelanfang liegt zeitlich vor dem Beginn des vorherigen Kapitels -> Lösung: Kapitelanfang >= Startzeit des vorherigen Kapitels)",
|
"MessageChapterErrorStartLtPrev": "Ungültige Kapitelstartzeit: Kapitelanfang < Kapitelanfang vorheriges Kapitel (Kapitelanfang liegt zeitlich vor dem Beginn des vorherigen Kapitels -> Lösung: Kapitelanfang >= Startzeit des vorherigen Kapitels)",
|
||||||
"MessageChapterStartIsAfter": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumende (Kapitelanfang liegt nach dem Ende des Mediums)",
|
"MessageChapterStartIsAfter": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumende (Kapitelanfang liegt nach dem Ende des Mediums)",
|
||||||
"MessageCheckingCron": "Überprüfe cron...",
|
"MessageCheckingCron": "Überprüfe Cron...",
|
||||||
"MessageConfirmDeleteBackup": "Sind Sie sicher, dass Sie die Sicherung für {0} löschen wollen?",
|
"MessageConfirmDeleteBackup": "Sind Sie sicher, dass Sie die Sicherung für {0} löschen wollen?",
|
||||||
"MessageConfirmDeleteLibrary": "Sind Sie sicher, dass Sie die Bibliothek \"{0}\" dauerhaft löschen wollen?",
|
"MessageConfirmDeleteLibrary": "Sind Sie sicher, dass Sie die Bibliothek \"{0}\" dauerhaft löschen wollen?",
|
||||||
"MessageConfirmDeleteSession": "Sind Sie sicher, dass Sie diese Sitzung löschen möchten?",
|
"MessageConfirmDeleteSession": "Sind Sie sicher, dass Sie diese Sitzung löschen möchten?",
|
||||||
"MessageConfirmForceReScan": "Sind Sie sicher, dass Sie einen erneuten Scanvorgang erzwingen wollen?",
|
"MessageConfirmForceReScan": "Sind Sie sicher, dass Sie einen erneuten Scanvorgang erzwingen wollen?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Sind Sie sicher, dass Sie alle Medien dieser Reihe als abgeschlossen markieren wollen?",
|
"MessageConfirmMarkSeriesFinished": "Sind Sie sicher, dass Sie alle Medien dieser Reihe als abgeschlossen markieren wollen?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Sind Sie sicher, dass Sie alle Medien dieser Reihe als nicht abgeschlossen markieren wollen?",
|
"MessageConfirmMarkSeriesNotFinished": "Sind Sie sicher, dass Sie alle Medien dieser Reihe als nicht abgeschlossen markieren wollen?",
|
||||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
"MessageConfirmRemoveAllChapters": "Sind Sie sicher, dass Sie alle Kapitel entfernen möchten?",
|
||||||
"MessageConfirmRemoveCollection": "Sind Sie sicher, dass Sie die Sammlung \"{0}\" löschen wollen?",
|
"MessageConfirmRemoveCollection": "Sind Sie sicher, dass Sie die Sammlung \"{0}\" löschen wollen?",
|
||||||
"MessageConfirmRemoveEpisode": "Sind Sie sicher, dass Sie die Episode \"{0}\" löschen möchten?",
|
"MessageConfirmRemoveEpisode": "Sind Sie sicher, dass Sie die Episode \"{0}\" löschen möchten?",
|
||||||
"MessageConfirmRemoveEpisodes": "Sind Sie sicher, dass Sie {0} Episoden löschen wollen?",
|
"MessageConfirmRemoveEpisodes": "Sind Sie sicher, dass Sie {0} Episoden löschen wollen?",
|
||||||
|
"MessageConfirmRemoveNarrator": "Sind Sie sicher, dass Sie den Erzähler \"{0}\" löschen möchten?",
|
||||||
"MessageConfirmRemovePlaylist": "Sind Sie sicher, dass Sie die Wiedergabeliste \"{0}\" entfernen möchten?",
|
"MessageConfirmRemovePlaylist": "Sind Sie sicher, dass Sie die Wiedergabeliste \"{0}\" entfernen möchten?",
|
||||||
"MessageConfirmRenameGenre": "Sind Sie sicher, dass Sie die Kategorie \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts umbenennen wollen?",
|
"MessageConfirmRenameGenre": "Sind Sie sicher, dass Sie die Kategorie \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts umbenennen wollen?",
|
||||||
"MessageConfirmRenameGenreMergeNote": "Hinweis: Kategorie existiert bereits -> Kategorien werden zusammengelegt.",
|
"MessageConfirmRenameGenreMergeNote": "Hinweis: Kategorie existiert bereits -> Kategorien werden zusammengelegt.",
|
||||||
@@ -517,8 +523,8 @@
|
|||||||
"MessageNoCollections": "Keine Sammlungen",
|
"MessageNoCollections": "Keine Sammlungen",
|
||||||
"MessageNoCoversFound": "Keine Titelbilder gefunden",
|
"MessageNoCoversFound": "Keine Titelbilder gefunden",
|
||||||
"MessageNoDescription": "Keine Beschreibung",
|
"MessageNoDescription": "Keine Beschreibung",
|
||||||
"MessageNoDownloadsInProgress": "No downloads currently in progress",
|
"MessageNoDownloadsInProgress": "Derzeit keine Downloads in Arbeit",
|
||||||
"MessageNoDownloadsQueued": "No downloads queued",
|
"MessageNoDownloadsQueued": "Keine Downloads in der Warteschlange",
|
||||||
"MessageNoEpisodeMatchesFound": "Keine Episodenübereinstimmungen gefunden",
|
"MessageNoEpisodeMatchesFound": "Keine Episodenübereinstimmungen gefunden",
|
||||||
"MessageNoEpisodes": "Keine Episoden",
|
"MessageNoEpisodes": "Keine Episoden",
|
||||||
"MessageNoFoldersAvailable": "Keine Ordner verfügbar",
|
"MessageNoFoldersAvailable": "Keine Ordner verfügbar",
|
||||||
@@ -535,7 +541,7 @@
|
|||||||
"MessageNoSearchResultsFor": "Keine Suchergebnisse für \"{0}\"",
|
"MessageNoSearchResultsFor": "Keine Suchergebnisse für \"{0}\"",
|
||||||
"MessageNoSeries": "Keine Serien",
|
"MessageNoSeries": "Keine Serien",
|
||||||
"MessageNoTags": "Keine Tags",
|
"MessageNoTags": "Keine Tags",
|
||||||
"MessageNoTasksRunning": "No Tasks Running",
|
"MessageNoTasksRunning": "Keine laufenden Aufgaben",
|
||||||
"MessageNotYetImplemented": "Noch nicht implementiert",
|
"MessageNotYetImplemented": "Noch nicht implementiert",
|
||||||
"MessageNoUpdateNecessary": "Keine Aktualisierung erforderlich",
|
"MessageNoUpdateNecessary": "Keine Aktualisierung erforderlich",
|
||||||
"MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig",
|
"MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig",
|
||||||
@@ -549,7 +555,7 @@
|
|||||||
"MessageRemoveAllItemsWarning": "WARNUNG! Bei dieser Aktion werden alle Bibliotheksobjekte aus der Datenbank entfernt, einschließlich aller Aktualisierungen oder Online-Abgleichs, die Sie vorgenommen haben. Ihre eigentlichen Dateien bleiben davon unberührt. Sind Sie sicher?",
|
"MessageRemoveAllItemsWarning": "WARNUNG! Bei dieser Aktion werden alle Bibliotheksobjekte aus der Datenbank entfernt, einschließlich aller Aktualisierungen oder Online-Abgleichs, die Sie vorgenommen haben. Ihre eigentlichen Dateien bleiben davon unberührt. Sind Sie sicher?",
|
||||||
"MessageRemoveChapter": "Kapitel löschen",
|
"MessageRemoveChapter": "Kapitel löschen",
|
||||||
"MessageRemoveEpisodes": "Entferne {0} Episode(n)",
|
"MessageRemoveEpisodes": "Entferne {0} Episode(n)",
|
||||||
"MessageRemoveFromPlayerQueue": "Aus der Abspielwarteliste löschen Remove from player queue",
|
"MessageRemoveFromPlayerQueue": "Aus der Abspielwarteliste löschen",
|
||||||
"MessageRemoveUserWarning": "Sind Sie sicher, dass Sie den Benutzer \"{0}\" dauerhaft löschen wollen?",
|
"MessageRemoveUserWarning": "Sind Sie sicher, dass Sie den Benutzer \"{0}\" dauerhaft löschen wollen?",
|
||||||
"MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und Beiträge leisten auf",
|
"MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und Beiträge leisten auf",
|
||||||
"MessageResetChaptersConfirm": "Sind Sie sicher, dass Sie die Kapitel zurücksetzen und die vorgenommenen Änderungen rückgängig machen wollen?",
|
"MessageResetChaptersConfirm": "Sind Sie sicher, dass Sie die Kapitel zurücksetzen und die vorgenommenen Änderungen rückgängig machen wollen?",
|
||||||
@@ -563,7 +569,7 @@
|
|||||||
"MessageUploaderItemFailed": "Hochladen fehlgeschlagen",
|
"MessageUploaderItemFailed": "Hochladen fehlgeschlagen",
|
||||||
"MessageUploaderItemSuccess": "Erfolgreich hochgeladen!",
|
"MessageUploaderItemSuccess": "Erfolgreich hochgeladen!",
|
||||||
"MessageUploading": "Hochladen...",
|
"MessageUploading": "Hochladen...",
|
||||||
"MessageValidCronExpression": "Gültiger cron-ausdruck",
|
"MessageValidCronExpression": "Gültiger Cron-Ausdruck",
|
||||||
"MessageWatcherIsDisabledGlobally": "Überwachung ist in den Servereinstellungen global deaktiviert",
|
"MessageWatcherIsDisabledGlobally": "Überwachung ist in den Servereinstellungen global deaktiviert",
|
||||||
"MessageXLibraryIsEmpty": "{0} Bibliothek ist leer!",
|
"MessageXLibraryIsEmpty": "{0} Bibliothek ist leer!",
|
||||||
"MessageYourAudiobookDurationIsLonger": "Die Dauer Ihres Mediums ist länger als die gefundene Dauer",
|
"MessageYourAudiobookDurationIsLonger": "Die Dauer Ihres Mediums ist länger als die gefundene Dauer",
|
||||||
@@ -581,7 +587,7 @@
|
|||||||
"PlaceholderNewFolderPath": "Neuer Ordnerpfad",
|
"PlaceholderNewFolderPath": "Neuer Ordnerpfad",
|
||||||
"PlaceholderNewPlaylist": "Neuer Wiedergabelistenname",
|
"PlaceholderNewPlaylist": "Neuer Wiedergabelistenname",
|
||||||
"PlaceholderSearch": "Suche...",
|
"PlaceholderSearch": "Suche...",
|
||||||
"PlaceholderSearchEpisode": "Search episode...",
|
"PlaceholderSearchEpisode": "Suche Episode...",
|
||||||
"ToastAccountUpdateFailed": "Aktualisierung des Kontos fehlgeschlagen",
|
"ToastAccountUpdateFailed": "Aktualisierung des Kontos fehlgeschlagen",
|
||||||
"ToastAccountUpdateSuccess": "Konto aktualisiert",
|
"ToastAccountUpdateSuccess": "Konto aktualisiert",
|
||||||
"ToastAuthorImageRemoveFailed": "Bild konnte nicht entfernt werden",
|
"ToastAuthorImageRemoveFailed": "Bild konnte nicht entfernt werden",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "Remove All",
|
"ButtonRemoveAll": "Remove All",
|
||||||
"ButtonRemoveAllLibraryItems": "Remove All Library Items",
|
"ButtonRemoveAllLibraryItems": "Remove All Library Items",
|
||||||
"ButtonRemoveFromContinueListening": "Remove from Continue Listening",
|
"ButtonRemoveFromContinueListening": "Remove from Continue Listening",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Remove Series from Continue Series",
|
"ButtonRemoveSeriesFromContinueSeries": "Remove Series from Continue Series",
|
||||||
"ButtonReScan": "Re-Scan",
|
"ButtonReScan": "Re-Scan",
|
||||||
"ButtonReset": "Reset",
|
"ButtonReset": "Reset",
|
||||||
@@ -197,6 +198,7 @@
|
|||||||
"LabelComplete": "Complete",
|
"LabelComplete": "Complete",
|
||||||
"LabelConfirmPassword": "Confirm Password",
|
"LabelConfirmPassword": "Confirm Password",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCover": "Cover",
|
"LabelCover": "Cover",
|
||||||
"LabelCoverImageURL": "Cover Image URL",
|
"LabelCoverImageURL": "Cover Image URL",
|
||||||
@@ -254,6 +256,7 @@
|
|||||||
"LabelIntervalEveryDay": "Every day",
|
"LabelIntervalEveryDay": "Every day",
|
||||||
"LabelIntervalEveryHour": "Every hour",
|
"LabelIntervalEveryHour": "Every hour",
|
||||||
"LabelInvalidParts": "Invalid Parts",
|
"LabelInvalidParts": "Invalid Parts",
|
||||||
|
"LabelInvert": "Invert",
|
||||||
"LabelItem": "Item",
|
"LabelItem": "Item",
|
||||||
"LabelLanguage": "Language",
|
"LabelLanguage": "Language",
|
||||||
"LabelLanguageDefaultServer": "Default Server Language",
|
"LabelLanguageDefaultServer": "Default Server Language",
|
||||||
@@ -330,6 +333,7 @@
|
|||||||
"LabelPubDate": "Pub Date",
|
"LabelPubDate": "Pub Date",
|
||||||
"LabelPublisher": "Publisher",
|
"LabelPublisher": "Publisher",
|
||||||
"LabelPublishYear": "Publish Year",
|
"LabelPublishYear": "Publish Year",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRecommended": "Recommended",
|
"LabelRecommended": "Recommended",
|
||||||
@@ -412,6 +416,7 @@
|
|||||||
"LabelTag": "Tag",
|
"LabelTag": "Tag",
|
||||||
"LabelTags": "Tags",
|
"LabelTags": "Tags",
|
||||||
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
||||||
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "Tasks Running",
|
"LabelTasks": "Tasks Running",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "Time Base",
|
||||||
"LabelTimeListened": "Time Listened",
|
"LabelTimeListened": "Time Listened",
|
||||||
@@ -481,6 +486,7 @@
|
|||||||
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
|
||||||
"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?",
|
||||||
|
"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?",
|
||||||
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
||||||
|
|||||||
+13
-7
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "Remover Todos",
|
"ButtonRemoveAll": "Remover Todos",
|
||||||
"ButtonRemoveAllLibraryItems": "Remover Todos los Elementos de la Biblioteca",
|
"ButtonRemoveAllLibraryItems": "Remover Todos los Elementos de la Biblioteca",
|
||||||
"ButtonRemoveFromContinueListening": "Remover de Continuar Escuchando",
|
"ButtonRemoveFromContinueListening": "Remover de Continuar Escuchando",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Remover Serie de Continuar Series",
|
"ButtonRemoveSeriesFromContinueSeries": "Remover Serie de Continuar Series",
|
||||||
"ButtonReScan": "Re-Escanear",
|
"ButtonReScan": "Re-Escanear",
|
||||||
"ButtonReset": "Reiniciar",
|
"ButtonReset": "Reiniciar",
|
||||||
@@ -186,8 +187,8 @@
|
|||||||
"LabelBitrate": "Bitrate",
|
"LabelBitrate": "Bitrate",
|
||||||
"LabelBooks": "Libros",
|
"LabelBooks": "Libros",
|
||||||
"LabelChangePassword": "Cambiar Contraseña",
|
"LabelChangePassword": "Cambiar Contraseña",
|
||||||
"LabelChannels": "Channels",
|
"LabelChannels": "Canales",
|
||||||
"LabelChapters": "Chapters",
|
"LabelChapters": "Capitulos",
|
||||||
"LabelChaptersFound": "Capitulo Encontrado",
|
"LabelChaptersFound": "Capitulo Encontrado",
|
||||||
"LabelChapterTitle": "Titulo del Capitulo",
|
"LabelChapterTitle": "Titulo del Capitulo",
|
||||||
"LabelClosePlayer": "Close player",
|
"LabelClosePlayer": "Close player",
|
||||||
@@ -197,6 +198,7 @@
|
|||||||
"LabelComplete": "Completo",
|
"LabelComplete": "Completo",
|
||||||
"LabelConfirmPassword": "Confirmar Contraseña",
|
"LabelConfirmPassword": "Confirmar Contraseña",
|
||||||
"LabelContinueListening": "Continuar Escuchando",
|
"LabelContinueListening": "Continuar Escuchando",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continuar Series",
|
"LabelContinueSeries": "Continuar Series",
|
||||||
"LabelCover": "Portada",
|
"LabelCover": "Portada",
|
||||||
"LabelCoverImageURL": "URL de Imagen de Portada",
|
"LabelCoverImageURL": "URL de Imagen de Portada",
|
||||||
@@ -217,7 +219,7 @@
|
|||||||
"LabelDuration": "Duración",
|
"LabelDuration": "Duración",
|
||||||
"LabelDurationFound": "Duración Comprobada:",
|
"LabelDurationFound": "Duración Comprobada:",
|
||||||
"LabelEdit": "Editar",
|
"LabelEdit": "Editar",
|
||||||
"LabelEmbeddedCover": "Embedded Cover",
|
"LabelEmbeddedCover": "Portada Integrada",
|
||||||
"LabelEnable": "Habilitar",
|
"LabelEnable": "Habilitar",
|
||||||
"LabelEnd": "Fin",
|
"LabelEnd": "Fin",
|
||||||
"LabelEpisode": "Episodio",
|
"LabelEpisode": "Episodio",
|
||||||
@@ -235,7 +237,7 @@
|
|||||||
"LabelFinished": "Terminado",
|
"LabelFinished": "Terminado",
|
||||||
"LabelFolder": "Carpeta",
|
"LabelFolder": "Carpeta",
|
||||||
"LabelFolders": "Carpetas",
|
"LabelFolders": "Carpetas",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "Formato",
|
||||||
"LabelGenre": "Genero",
|
"LabelGenre": "Genero",
|
||||||
"LabelGenres": "Géneros",
|
"LabelGenres": "Géneros",
|
||||||
"LabelHardDeleteFile": "Eliminar Definitivamente",
|
"LabelHardDeleteFile": "Eliminar Definitivamente",
|
||||||
@@ -254,6 +256,7 @@
|
|||||||
"LabelIntervalEveryDay": "Cada Dia",
|
"LabelIntervalEveryDay": "Cada Dia",
|
||||||
"LabelIntervalEveryHour": "Cada Hora",
|
"LabelIntervalEveryHour": "Cada Hora",
|
||||||
"LabelInvalidParts": "Partes Invalidas",
|
"LabelInvalidParts": "Partes Invalidas",
|
||||||
|
"LabelInvert": "Invert",
|
||||||
"LabelItem": "Elemento",
|
"LabelItem": "Elemento",
|
||||||
"LabelLanguage": "Lenguaje",
|
"LabelLanguage": "Lenguaje",
|
||||||
"LabelLanguageDefaultServer": "Lenguaje Predeterminado del Servidor",
|
"LabelLanguageDefaultServer": "Lenguaje Predeterminado del Servidor",
|
||||||
@@ -282,7 +285,7 @@
|
|||||||
"LabelMissing": "Ausente",
|
"LabelMissing": "Ausente",
|
||||||
"LabelMissingParts": "Partes Ausentes",
|
"LabelMissingParts": "Partes Ausentes",
|
||||||
"LabelMore": "Mas",
|
"LabelMore": "Mas",
|
||||||
"LabelMoreInfo": "More Info",
|
"LabelMoreInfo": "Mas Información",
|
||||||
"LabelName": "Nombre",
|
"LabelName": "Nombre",
|
||||||
"LabelNarrator": "Narrador",
|
"LabelNarrator": "Narrador",
|
||||||
"LabelNarrators": "Narradores",
|
"LabelNarrators": "Narradores",
|
||||||
@@ -330,6 +333,7 @@
|
|||||||
"LabelPubDate": "Fecha de Publicación",
|
"LabelPubDate": "Fecha de Publicación",
|
||||||
"LabelPublisher": "Editor",
|
"LabelPublisher": "Editor",
|
||||||
"LabelPublishYear": "Año de Publicación",
|
"LabelPublishYear": "Año de Publicación",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Agregado Reciente",
|
"LabelRecentlyAdded": "Agregado Reciente",
|
||||||
"LabelRecentSeries": "Series Recientes",
|
"LabelRecentSeries": "Series Recientes",
|
||||||
"LabelRecommended": "Recomendados",
|
"LabelRecommended": "Recomendados",
|
||||||
@@ -412,6 +416,7 @@
|
|||||||
"LabelTag": "Etiqueta",
|
"LabelTag": "Etiqueta",
|
||||||
"LabelTags": "Etiquetas",
|
"LabelTags": "Etiquetas",
|
||||||
"LabelTagsAccessibleToUser": "Etiquetas Accessible para el Usuario",
|
"LabelTagsAccessibleToUser": "Etiquetas Accessible para el Usuario",
|
||||||
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "Tareas Corriendo",
|
"LabelTasks": "Tareas Corriendo",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "Time Base",
|
||||||
"LabelTimeListened": "Tiempo Escuchando",
|
"LabelTimeListened": "Tiempo Escuchando",
|
||||||
@@ -477,10 +482,11 @@
|
|||||||
"MessageConfirmForceReScan": "Esta seguro que desea forzar re-escanear?",
|
"MessageConfirmForceReScan": "Esta seguro que desea forzar re-escanear?",
|
||||||
"MessageConfirmMarkSeriesFinished": "Esta seguro que desea marcar todos los libros en esta serie como terminados?",
|
"MessageConfirmMarkSeriesFinished": "Esta seguro que desea marcar todos los libros en esta serie como terminados?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "Esta seguro que desea marcar todos los libros en esta serie como no terminados?",
|
"MessageConfirmMarkSeriesNotFinished": "Esta seguro que desea marcar todos los libros en esta serie como no terminados?",
|
||||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
"MessageConfirmRemoveAllChapters": "Esta seguro que desea remover todos los capitulos?",
|
||||||
"MessageConfirmRemoveCollection": "Esta seguro que desea remover la colección \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Esta seguro que desea remover la colección \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "Esta seguro que desea remover el episodio \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Esta seguro que desea remover el episodio \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "Esta seguro que desea remover {0} episodios?",
|
"MessageConfirmRemoveEpisodes": "Esta seguro que desea remover {0} episodios?",
|
||||||
|
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
||||||
"MessageConfirmRemovePlaylist": "Esta seguro que desea remover su lista de reproducción \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "Esta seguro que desea remover su lista de reproducción \"{0}\"?",
|
||||||
"MessageConfirmRenameGenre": "Esta seguro que desea renombrar el genero \"{0}\" a \"{1}\" de todos los elementos?",
|
"MessageConfirmRenameGenre": "Esta seguro que desea renombrar el genero \"{0}\" a \"{1}\" de todos los elementos?",
|
||||||
"MessageConfirmRenameGenreMergeNote": "Nota: Este genero ya existe por lo que se fusionarán.",
|
"MessageConfirmRenameGenreMergeNote": "Nota: Este genero ya existe por lo que se fusionarán.",
|
||||||
@@ -549,7 +555,7 @@
|
|||||||
"MessageRemoveAllItemsWarning": "ADVERTENCIA! Esta acción eliminará todos los elementos de la biblioteca de la base de datos incluyendo cualquier actualización o match. Esto no hace nada a sus archivos reales. Esta seguro que desea continuar?",
|
"MessageRemoveAllItemsWarning": "ADVERTENCIA! Esta acción eliminará todos los elementos de la biblioteca de la base de datos incluyendo cualquier actualización o match. Esto no hace nada a sus archivos reales. Esta seguro que desea continuar?",
|
||||||
"MessageRemoveChapter": "Remover capítulos",
|
"MessageRemoveChapter": "Remover capítulos",
|
||||||
"MessageRemoveEpisodes": "Remover {0} episodio(s)",
|
"MessageRemoveEpisodes": "Remover {0} episodio(s)",
|
||||||
"MessageRemoveFromPlayerQueue": "Remover de player queue",
|
"MessageRemoveFromPlayerQueue": "Romover la cola de reporduccion",
|
||||||
"MessageRemoveUserWarning": "Esta seguro que desea eliminar el usuario \"{0}\"?",
|
"MessageRemoveUserWarning": "Esta seguro que desea eliminar el usuario \"{0}\"?",
|
||||||
"MessageReportBugsAndContribute": "Reporte erres, solicite funciones y contribuye en",
|
"MessageReportBugsAndContribute": "Reporte erres, solicite funciones y contribuye en",
|
||||||
"MessageResetChaptersConfirm": "Esta seguro que desea reiniciar el capitulo y deshacer los cambios que hiciste?",
|
"MessageResetChaptersConfirm": "Esta seguro que desea reiniciar el capitulo y deshacer los cambios que hiciste?",
|
||||||
|
|||||||
+24
-18
@@ -20,7 +20,7 @@
|
|||||||
"ButtonCreate": "Créer",
|
"ButtonCreate": "Créer",
|
||||||
"ButtonCreateBackup": "Créer une sauvegarde",
|
"ButtonCreateBackup": "Créer une sauvegarde",
|
||||||
"ButtonDelete": "Effacer",
|
"ButtonDelete": "Effacer",
|
||||||
"ButtonDownloadQueue": "Queue de téléchargement",
|
"ButtonDownloadQueue": "File d’attente de téléchargement",
|
||||||
"ButtonEdit": "Modifier",
|
"ButtonEdit": "Modifier",
|
||||||
"ButtonEditChapters": "Modifier les chapitres",
|
"ButtonEditChapters": "Modifier les chapitres",
|
||||||
"ButtonEditPodcast": "Modifier les podcasts",
|
"ButtonEditPodcast": "Modifier les podcasts",
|
||||||
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "Supprimer tout",
|
"ButtonRemoveAll": "Supprimer tout",
|
||||||
"ButtonRemoveAllLibraryItems": "Supprimer tous les articles de la bibliothèque",
|
"ButtonRemoveAllLibraryItems": "Supprimer tous les articles de la bibliothèque",
|
||||||
"ButtonRemoveFromContinueListening": "Ne plus continuer à écouter",
|
"ButtonRemoveFromContinueListening": "Ne plus continuer à écouter",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Ne plus continuer à écouter la série",
|
"ButtonRemoveSeriesFromContinueSeries": "Ne plus continuer à écouter la série",
|
||||||
"ButtonReScan": "Nouvelle analyse",
|
"ButtonReScan": "Nouvelle analyse",
|
||||||
"ButtonReset": "Réinitialiser",
|
"ButtonReset": "Réinitialiser",
|
||||||
@@ -93,7 +94,7 @@
|
|||||||
"HeaderCollection": "Collection",
|
"HeaderCollection": "Collection",
|
||||||
"HeaderCollectionItems": "Entrées de la Collection",
|
"HeaderCollectionItems": "Entrées de la Collection",
|
||||||
"HeaderCover": "Couverture",
|
"HeaderCover": "Couverture",
|
||||||
"HeaderCurrentDownloads": "Current Downloads",
|
"HeaderCurrentDownloads": "File d’attente de téléchargement",
|
||||||
"HeaderDetails": "Détails",
|
"HeaderDetails": "Détails",
|
||||||
"HeaderDownloadQueue": "Queue de téléchargement",
|
"HeaderDownloadQueue": "Queue de téléchargement",
|
||||||
"HeaderEpisodes": "Épisodes",
|
"HeaderEpisodes": "Épisodes",
|
||||||
@@ -161,7 +162,7 @@
|
|||||||
"LabelAccountTypeGuest": "Invité",
|
"LabelAccountTypeGuest": "Invité",
|
||||||
"LabelAccountTypeUser": "Utilisateur",
|
"LabelAccountTypeUser": "Utilisateur",
|
||||||
"LabelActivity": "Activité",
|
"LabelActivity": "Activité",
|
||||||
"LabelAdded": "Added",
|
"LabelAdded": "Ajouté",
|
||||||
"LabelAddedAt": "Date d’ajout",
|
"LabelAddedAt": "Date d’ajout",
|
||||||
"LabelAddToCollection": "Ajouter à la collection",
|
"LabelAddToCollection": "Ajouter à la collection",
|
||||||
"LabelAddToCollectionBatch": "Ajout de {0} livres à la lollection",
|
"LabelAddToCollectionBatch": "Ajout de {0} livres à la lollection",
|
||||||
@@ -186,8 +187,8 @@
|
|||||||
"LabelBitrate": "Bitrate",
|
"LabelBitrate": "Bitrate",
|
||||||
"LabelBooks": "Livres",
|
"LabelBooks": "Livres",
|
||||||
"LabelChangePassword": "Modifier le mot de passe",
|
"LabelChangePassword": "Modifier le mot de passe",
|
||||||
"LabelChannels": "Channels",
|
"LabelChannels": "Canaux",
|
||||||
"LabelChapters": "Chapters",
|
"LabelChapters": "Chapitres",
|
||||||
"LabelChaptersFound": "Chapitres trouvés",
|
"LabelChaptersFound": "Chapitres trouvés",
|
||||||
"LabelChapterTitle": "Titres du chapitre",
|
"LabelChapterTitle": "Titres du chapitre",
|
||||||
"LabelClosePlayer": "Fermer le lecteur",
|
"LabelClosePlayer": "Fermer le lecteur",
|
||||||
@@ -197,6 +198,7 @@
|
|||||||
"LabelComplete": "Complet",
|
"LabelComplete": "Complet",
|
||||||
"LabelConfirmPassword": "Confirmer le mot de passe",
|
"LabelConfirmPassword": "Confirmer le mot de passe",
|
||||||
"LabelContinueListening": "Continuer la lecture",
|
"LabelContinueListening": "Continuer la lecture",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continuer la série",
|
"LabelContinueSeries": "Continuer la série",
|
||||||
"LabelCover": "Couverture",
|
"LabelCover": "Couverture",
|
||||||
"LabelCoverImageURL": "URL vers l’image de couverture",
|
"LabelCoverImageURL": "URL vers l’image de couverture",
|
||||||
@@ -217,7 +219,7 @@
|
|||||||
"LabelDuration": "Durée",
|
"LabelDuration": "Durée",
|
||||||
"LabelDurationFound": "Durée trouvée :",
|
"LabelDurationFound": "Durée trouvée :",
|
||||||
"LabelEdit": "Modifier",
|
"LabelEdit": "Modifier",
|
||||||
"LabelEmbeddedCover": "Embedded Cover",
|
"LabelEmbeddedCover": "Couverture du livre intégrée",
|
||||||
"LabelEnable": "Activer",
|
"LabelEnable": "Activer",
|
||||||
"LabelEnd": "Fin",
|
"LabelEnd": "Fin",
|
||||||
"LabelEpisode": "Épisode",
|
"LabelEpisode": "Épisode",
|
||||||
@@ -254,11 +256,12 @@
|
|||||||
"LabelIntervalEveryDay": "Tous les jours",
|
"LabelIntervalEveryDay": "Tous les jours",
|
||||||
"LabelIntervalEveryHour": "Toutes les heures",
|
"LabelIntervalEveryHour": "Toutes les heures",
|
||||||
"LabelInvalidParts": "Parties invalides",
|
"LabelInvalidParts": "Parties invalides",
|
||||||
|
"LabelInvert": "Invert",
|
||||||
"LabelItem": "Article",
|
"LabelItem": "Article",
|
||||||
"LabelLanguage": "Langue",
|
"LabelLanguage": "Langue",
|
||||||
"LabelLanguageDefaultServer": "Langue par défaut",
|
"LabelLanguageDefaultServer": "Langue par défaut",
|
||||||
"LabelLastBookAdded": "Last Book Added",
|
"LabelLastBookAdded": "Dernier livre ajouté",
|
||||||
"LabelLastBookUpdated": "Last Book Updated",
|
"LabelLastBookUpdated": "Dernier livre mis à jour",
|
||||||
"LabelLastSeen": "Vu dernièrement",
|
"LabelLastSeen": "Vu dernièrement",
|
||||||
"LabelLastTime": "Progression",
|
"LabelLastTime": "Progression",
|
||||||
"LabelLastUpdate": "Dernière mise à jour",
|
"LabelLastUpdate": "Dernière mise à jour",
|
||||||
@@ -277,12 +280,12 @@
|
|||||||
"LabelMediaType": "Type de média",
|
"LabelMediaType": "Type de média",
|
||||||
"LabelMetadataProvider": "Fournisseur de métadonnées",
|
"LabelMetadataProvider": "Fournisseur de métadonnées",
|
||||||
"LabelMetaTag": "Etiquette de métadonnée",
|
"LabelMetaTag": "Etiquette de métadonnée",
|
||||||
"LabelMetaTags": "Meta Tags",
|
"LabelMetaTags": "Etiquettes de métadonnée",
|
||||||
"LabelMinute": "Minute",
|
"LabelMinute": "Minute",
|
||||||
"LabelMissing": "Manquant",
|
"LabelMissing": "Manquant",
|
||||||
"LabelMissingParts": "Parties manquantes",
|
"LabelMissingParts": "Parties manquantes",
|
||||||
"LabelMore": "Plus",
|
"LabelMore": "Plus",
|
||||||
"LabelMoreInfo": "More Info",
|
"LabelMoreInfo": "Plus d’info",
|
||||||
"LabelName": "Nom",
|
"LabelName": "Nom",
|
||||||
"LabelNarrator": "Narrateur",
|
"LabelNarrator": "Narrateur",
|
||||||
"LabelNarrators": "Narrateurs",
|
"LabelNarrators": "Narrateurs",
|
||||||
@@ -290,7 +293,7 @@
|
|||||||
"LabelNewestAuthors": "Nouveaux auteurs",
|
"LabelNewestAuthors": "Nouveaux auteurs",
|
||||||
"LabelNewestEpisodes": "Derniers épisodes",
|
"LabelNewestEpisodes": "Derniers épisodes",
|
||||||
"LabelNewPassword": "Nouveau mot de passe",
|
"LabelNewPassword": "Nouveau mot de passe",
|
||||||
"LabelNextBackupDate": "Prochaine date de sauvegarde",
|
"LabelNextBackupDate": "Date de la prochaine sauvegarde",
|
||||||
"LabelNextScheduledRun": "Prochain lancement prévu",
|
"LabelNextScheduledRun": "Prochain lancement prévu",
|
||||||
"LabelNotes": "Notes",
|
"LabelNotes": "Notes",
|
||||||
"LabelNotFinished": "Non terminé(e)",
|
"LabelNotFinished": "Non terminé(e)",
|
||||||
@@ -324,12 +327,13 @@
|
|||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
"LabelPodcastType": "Type de Podcast",
|
"LabelPodcastType": "Type de Podcast",
|
||||||
"LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)",
|
"LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)",
|
||||||
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
"LabelPreventIndexing": "Empêcher l’indexation de votre flux par les bases de donénes iTunes et Google podcast",
|
||||||
"LabelProgress": "Progression",
|
"LabelProgress": "Progression",
|
||||||
"LabelProvider": "Fournisseur",
|
"LabelProvider": "Fournisseur",
|
||||||
"LabelPubDate": "Date de publication",
|
"LabelPubDate": "Date de publication",
|
||||||
"LabelPublisher": "Éditeur",
|
"LabelPublisher": "Éditeur",
|
||||||
"LabelPublishYear": "Année d’édition",
|
"LabelPublishYear": "Année d’édition",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Derniers ajouts",
|
"LabelRecentlyAdded": "Derniers ajouts",
|
||||||
"LabelRecentSeries": "Séries récentes",
|
"LabelRecentSeries": "Séries récentes",
|
||||||
"LabelRecommended": "Recommandé",
|
"LabelRecommended": "Recommandé",
|
||||||
@@ -339,7 +343,7 @@
|
|||||||
"LabelRSSFeedCustomOwnerEmail": "Email propriétaire personnalisé",
|
"LabelRSSFeedCustomOwnerEmail": "Email propriétaire personnalisé",
|
||||||
"LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé",
|
"LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé",
|
||||||
"LabelRSSFeedOpen": "Flux RSS ouvert",
|
"LabelRSSFeedOpen": "Flux RSS ouvert",
|
||||||
"LabelRSSFeedPreventIndexing": "Empêcher l'indexation",
|
"LabelRSSFeedPreventIndexing": "Empêcher l’indexation",
|
||||||
"LabelRSSFeedSlug": "Identificateur d’adresse du Flux RSS ",
|
"LabelRSSFeedSlug": "Identificateur d’adresse du Flux RSS ",
|
||||||
"LabelRSSFeedURL": "Adresse du flux RSS",
|
"LabelRSSFeedURL": "Adresse du flux RSS",
|
||||||
"LabelSearchTerm": "Terme de recherche",
|
"LabelSearchTerm": "Terme de recherche",
|
||||||
@@ -384,7 +388,7 @@
|
|||||||
"LabelSettingsStoreCoversWithItemHelp": "Par défaut, les couvertures sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les couvertures dans le dossier avec les fichiers de l’article. Seul un fichier nommé « cover » sera conservé.",
|
"LabelSettingsStoreCoversWithItemHelp": "Par défaut, les couvertures sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les couvertures dans le dossier avec les fichiers de l’article. Seul un fichier nommé « cover » sera conservé.",
|
||||||
"LabelSettingsStoreMetadataWithItem": "Enregistrer les Métadonnées avec les articles",
|
"LabelSettingsStoreMetadataWithItem": "Enregistrer les Métadonnées avec les articles",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les métadonnées sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les métadonnées dans le dossier de l’article avec une extension « .abs ».",
|
"LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les métadonnées sont enregistrées dans /metadata/items. Activer ce paramètre enregistrera les métadonnées dans le dossier de l’article avec une extension « .abs ».",
|
||||||
"LabelSettingsTimeFormat": "Time Format",
|
"LabelSettingsTimeFormat": "Format d’heure",
|
||||||
"LabelShowAll": "Afficher Tout",
|
"LabelShowAll": "Afficher Tout",
|
||||||
"LabelSize": "Taille",
|
"LabelSize": "Taille",
|
||||||
"LabelSleepTimer": "Minuterie",
|
"LabelSleepTimer": "Minuterie",
|
||||||
@@ -412,8 +416,9 @@
|
|||||||
"LabelTag": "Étiquette",
|
"LabelTag": "Étiquette",
|
||||||
"LabelTags": "Étiquettes",
|
"LabelTags": "Étiquettes",
|
||||||
"LabelTagsAccessibleToUser": "Étiquettes accessibles à l’utilisateur",
|
"LabelTagsAccessibleToUser": "Étiquettes accessibles à l’utilisateur",
|
||||||
"LabelTasks": "Tasks Running",
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTasks": "Tâches en cours",
|
||||||
|
"LabelTimeBase": "Base de temps",
|
||||||
"LabelTimeListened": "Temps d’écoute",
|
"LabelTimeListened": "Temps d’écoute",
|
||||||
"LabelTimeListenedToday": "Nombres d’écoutes Aujourd’hui",
|
"LabelTimeListenedToday": "Nombres d’écoutes Aujourd’hui",
|
||||||
"LabelTimeRemaining": "{0} restantes",
|
"LabelTimeRemaining": "{0} restantes",
|
||||||
@@ -481,6 +486,7 @@
|
|||||||
"MessageConfirmRemoveCollection": "Êtes-vous sûr de vouloir supprimer la collection « {0} » ?",
|
"MessageConfirmRemoveCollection": "Êtes-vous sûr de vouloir supprimer la collection « {0} » ?",
|
||||||
"MessageConfirmRemoveEpisode": "Êtes-vous sûr de vouloir supprimer l’épisode « {0} » ?",
|
"MessageConfirmRemoveEpisode": "Êtes-vous sûr de vouloir supprimer l’épisode « {0} » ?",
|
||||||
"MessageConfirmRemoveEpisodes": "Êtes-vous sûr de vouloir supprimer {0} épisodes ?",
|
"MessageConfirmRemoveEpisodes": "Êtes-vous sûr de vouloir supprimer {0} épisodes ?",
|
||||||
|
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
||||||
"MessageConfirmRemovePlaylist": "Êtes-vous sûr de vouloir supprimer la liste de lecture « {0} » ?",
|
"MessageConfirmRemovePlaylist": "Êtes-vous sûr de vouloir supprimer la liste de lecture « {0} » ?",
|
||||||
"MessageConfirmRenameGenre": "Êtes-vous sûr de vouloir renommer le genre « {0} » vers « {1} » pour tous les articles ?",
|
"MessageConfirmRenameGenre": "Êtes-vous sûr de vouloir renommer le genre « {0} » vers « {1} » pour tous les articles ?",
|
||||||
"MessageConfirmRenameGenreMergeNote": "Information: Ce genre existe déjà et sera fusionné.",
|
"MessageConfirmRenameGenreMergeNote": "Information: Ce genre existe déjà et sera fusionné.",
|
||||||
@@ -535,7 +541,7 @@
|
|||||||
"MessageNoSearchResultsFor": "Aucun résultat pour la recherche « {0} »",
|
"MessageNoSearchResultsFor": "Aucun résultat pour la recherche « {0} »",
|
||||||
"MessageNoSeries": "Aucune série",
|
"MessageNoSeries": "Aucune série",
|
||||||
"MessageNoTags": "Aucune d’étiquettes",
|
"MessageNoTags": "Aucune d’étiquettes",
|
||||||
"MessageNoTasksRunning": "No Tasks Running",
|
"MessageNoTasksRunning": "Aucune tâche en cours",
|
||||||
"MessageNotYetImplemented": "Non implémenté",
|
"MessageNotYetImplemented": "Non implémenté",
|
||||||
"MessageNoUpdateNecessary": "Aucune mise à jour nécessaire",
|
"MessageNoUpdateNecessary": "Aucune mise à jour nécessaire",
|
||||||
"MessageNoUpdatesWereNecessary": "Aucune mise à jour n’était nécessaire",
|
"MessageNoUpdatesWereNecessary": "Aucune mise à jour n’était nécessaire",
|
||||||
@@ -581,7 +587,7 @@
|
|||||||
"PlaceholderNewFolderPath": "Nouveau chemin de dossier",
|
"PlaceholderNewFolderPath": "Nouveau chemin de dossier",
|
||||||
"PlaceholderNewPlaylist": "Nouveau nom de liste de lecture",
|
"PlaceholderNewPlaylist": "Nouveau nom de liste de lecture",
|
||||||
"PlaceholderSearch": "Recherche...",
|
"PlaceholderSearch": "Recherche...",
|
||||||
"PlaceholderSearchEpisode": "Search episode...",
|
"PlaceholderSearchEpisode": "Recherche d’épisode...",
|
||||||
"ToastAccountUpdateFailed": "Échec de la mise à jour du compte",
|
"ToastAccountUpdateFailed": "Échec de la mise à jour du compte",
|
||||||
"ToastAccountUpdateSuccess": "Compte mis à jour",
|
"ToastAccountUpdateSuccess": "Compte mis à jour",
|
||||||
"ToastAuthorImageRemoveFailed": "Échec de la suppression de l’image",
|
"ToastAuthorImageRemoveFailed": "Échec de la suppression de l’image",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "બધું કાઢી નાખો",
|
"ButtonRemoveAll": "બધું કાઢી નાખો",
|
||||||
"ButtonRemoveAllLibraryItems": "બધું પુસ્તકાલય વસ્તુઓ કાઢી નાખો",
|
"ButtonRemoveAllLibraryItems": "બધું પુસ્તકાલય વસ્તુઓ કાઢી નાખો",
|
||||||
"ButtonRemoveFromContinueListening": "સાંભળતી પુસ્તકો માંથી કાઢી નાખો",
|
"ButtonRemoveFromContinueListening": "સાંભળતી પુસ્તકો માંથી કાઢી નાખો",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "સાંભળતી સિરીઝ માંથી કાઢી નાખો",
|
"ButtonRemoveSeriesFromContinueSeries": "સાંભળતી સિરીઝ માંથી કાઢી નાખો",
|
||||||
"ButtonReScan": "ફરીથી સ્કેન કરો",
|
"ButtonReScan": "ફરીથી સ્કેન કરો",
|
||||||
"ButtonReset": "રીસેટ કરો",
|
"ButtonReset": "રીસેટ કરો",
|
||||||
@@ -197,6 +198,7 @@
|
|||||||
"LabelComplete": "Complete",
|
"LabelComplete": "Complete",
|
||||||
"LabelConfirmPassword": "Confirm Password",
|
"LabelConfirmPassword": "Confirm Password",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCover": "Cover",
|
"LabelCover": "Cover",
|
||||||
"LabelCoverImageURL": "Cover Image URL",
|
"LabelCoverImageURL": "Cover Image URL",
|
||||||
@@ -254,6 +256,7 @@
|
|||||||
"LabelIntervalEveryDay": "Every day",
|
"LabelIntervalEveryDay": "Every day",
|
||||||
"LabelIntervalEveryHour": "Every hour",
|
"LabelIntervalEveryHour": "Every hour",
|
||||||
"LabelInvalidParts": "Invalid Parts",
|
"LabelInvalidParts": "Invalid Parts",
|
||||||
|
"LabelInvert": "Invert",
|
||||||
"LabelItem": "Item",
|
"LabelItem": "Item",
|
||||||
"LabelLanguage": "Language",
|
"LabelLanguage": "Language",
|
||||||
"LabelLanguageDefaultServer": "Default Server Language",
|
"LabelLanguageDefaultServer": "Default Server Language",
|
||||||
@@ -330,6 +333,7 @@
|
|||||||
"LabelPubDate": "Pub Date",
|
"LabelPubDate": "Pub Date",
|
||||||
"LabelPublisher": "Publisher",
|
"LabelPublisher": "Publisher",
|
||||||
"LabelPublishYear": "Publish Year",
|
"LabelPublishYear": "Publish Year",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRecommended": "Recommended",
|
"LabelRecommended": "Recommended",
|
||||||
@@ -412,6 +416,7 @@
|
|||||||
"LabelTag": "Tag",
|
"LabelTag": "Tag",
|
||||||
"LabelTags": "Tags",
|
"LabelTags": "Tags",
|
||||||
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
||||||
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "Tasks Running",
|
"LabelTasks": "Tasks Running",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "Time Base",
|
||||||
"LabelTimeListened": "Time Listened",
|
"LabelTimeListened": "Time Listened",
|
||||||
@@ -481,6 +486,7 @@
|
|||||||
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
|
||||||
"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?",
|
||||||
|
"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?",
|
||||||
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "सभी हटाएं",
|
"ButtonRemoveAll": "सभी हटाएं",
|
||||||
"ButtonRemoveAllLibraryItems": "पुस्तकालय की सभी आइटम हटाएं",
|
"ButtonRemoveAllLibraryItems": "पुस्तकालय की सभी आइटम हटाएं",
|
||||||
"ButtonRemoveFromContinueListening": "सुनना जारी रखें से हटाएं",
|
"ButtonRemoveFromContinueListening": "सुनना जारी रखें से हटाएं",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "इस सीरीज को कंटिन्यू सीरीज से हटा दें",
|
"ButtonRemoveSeriesFromContinueSeries": "इस सीरीज को कंटिन्यू सीरीज से हटा दें",
|
||||||
"ButtonReScan": "पुन: स्कैन करें",
|
"ButtonReScan": "पुन: स्कैन करें",
|
||||||
"ButtonReset": "रीसेट करें",
|
"ButtonReset": "रीसेट करें",
|
||||||
@@ -197,6 +198,7 @@
|
|||||||
"LabelComplete": "Complete",
|
"LabelComplete": "Complete",
|
||||||
"LabelConfirmPassword": "Confirm Password",
|
"LabelConfirmPassword": "Confirm Password",
|
||||||
"LabelContinueListening": "Continue Listening",
|
"LabelContinueListening": "Continue Listening",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continue Series",
|
"LabelContinueSeries": "Continue Series",
|
||||||
"LabelCover": "Cover",
|
"LabelCover": "Cover",
|
||||||
"LabelCoverImageURL": "Cover Image URL",
|
"LabelCoverImageURL": "Cover Image URL",
|
||||||
@@ -254,6 +256,7 @@
|
|||||||
"LabelIntervalEveryDay": "Every day",
|
"LabelIntervalEveryDay": "Every day",
|
||||||
"LabelIntervalEveryHour": "Every hour",
|
"LabelIntervalEveryHour": "Every hour",
|
||||||
"LabelInvalidParts": "Invalid Parts",
|
"LabelInvalidParts": "Invalid Parts",
|
||||||
|
"LabelInvert": "Invert",
|
||||||
"LabelItem": "Item",
|
"LabelItem": "Item",
|
||||||
"LabelLanguage": "Language",
|
"LabelLanguage": "Language",
|
||||||
"LabelLanguageDefaultServer": "Default Server Language",
|
"LabelLanguageDefaultServer": "Default Server Language",
|
||||||
@@ -330,6 +333,7 @@
|
|||||||
"LabelPubDate": "Pub Date",
|
"LabelPubDate": "Pub Date",
|
||||||
"LabelPublisher": "Publisher",
|
"LabelPublisher": "Publisher",
|
||||||
"LabelPublishYear": "Publish Year",
|
"LabelPublishYear": "Publish Year",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
"LabelRecommended": "Recommended",
|
"LabelRecommended": "Recommended",
|
||||||
@@ -412,6 +416,7 @@
|
|||||||
"LabelTag": "Tag",
|
"LabelTag": "Tag",
|
||||||
"LabelTags": "Tags",
|
"LabelTags": "Tags",
|
||||||
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
"LabelTagsAccessibleToUser": "Tags Accessible to User",
|
||||||
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "Tasks Running",
|
"LabelTasks": "Tasks Running",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "Time Base",
|
||||||
"LabelTimeListened": "Time Listened",
|
"LabelTimeListened": "Time Listened",
|
||||||
@@ -481,6 +486,7 @@
|
|||||||
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
|
||||||
"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?",
|
||||||
|
"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?",
|
||||||
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "Ukloni sve",
|
"ButtonRemoveAll": "Ukloni sve",
|
||||||
"ButtonRemoveAllLibraryItems": "Ukloni sve stvari iz biblioteke",
|
"ButtonRemoveAllLibraryItems": "Ukloni sve stvari iz biblioteke",
|
||||||
"ButtonRemoveFromContinueListening": "Ukloni iz Nastavi slušati",
|
"ButtonRemoveFromContinueListening": "Ukloni iz Nastavi slušati",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Ukloni seriju iz Nastavi seriju",
|
"ButtonRemoveSeriesFromContinueSeries": "Ukloni seriju iz Nastavi seriju",
|
||||||
"ButtonReScan": "Skeniraj ponovno",
|
"ButtonReScan": "Skeniraj ponovno",
|
||||||
"ButtonReset": "Poništi",
|
"ButtonReset": "Poništi",
|
||||||
@@ -197,6 +198,7 @@
|
|||||||
"LabelComplete": "Complete",
|
"LabelComplete": "Complete",
|
||||||
"LabelConfirmPassword": "Potvrdi lozinku",
|
"LabelConfirmPassword": "Potvrdi lozinku",
|
||||||
"LabelContinueListening": "Nastavi slušanje",
|
"LabelContinueListening": "Nastavi slušanje",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Nastavi seriju",
|
"LabelContinueSeries": "Nastavi seriju",
|
||||||
"LabelCover": "Cover",
|
"LabelCover": "Cover",
|
||||||
"LabelCoverImageURL": "URL od covera",
|
"LabelCoverImageURL": "URL od covera",
|
||||||
@@ -254,6 +256,7 @@
|
|||||||
"LabelIntervalEveryDay": "Every day",
|
"LabelIntervalEveryDay": "Every day",
|
||||||
"LabelIntervalEveryHour": "Every hour",
|
"LabelIntervalEveryHour": "Every hour",
|
||||||
"LabelInvalidParts": "Nevaljajuči dijelovi",
|
"LabelInvalidParts": "Nevaljajuči dijelovi",
|
||||||
|
"LabelInvert": "Invert",
|
||||||
"LabelItem": "Stavka",
|
"LabelItem": "Stavka",
|
||||||
"LabelLanguage": "Jezik",
|
"LabelLanguage": "Jezik",
|
||||||
"LabelLanguageDefaultServer": "Default jezik servera",
|
"LabelLanguageDefaultServer": "Default jezik servera",
|
||||||
@@ -330,6 +333,7 @@
|
|||||||
"LabelPubDate": "Datam izdavanja",
|
"LabelPubDate": "Datam izdavanja",
|
||||||
"LabelPublisher": "Izdavač",
|
"LabelPublisher": "Izdavač",
|
||||||
"LabelPublishYear": "Godina izdavanja",
|
"LabelPublishYear": "Godina izdavanja",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Nedavno dodano",
|
"LabelRecentlyAdded": "Nedavno dodano",
|
||||||
"LabelRecentSeries": "Nedavne serije",
|
"LabelRecentSeries": "Nedavne serije",
|
||||||
"LabelRecommended": "Recommended",
|
"LabelRecommended": "Recommended",
|
||||||
@@ -412,6 +416,7 @@
|
|||||||
"LabelTag": "Tag",
|
"LabelTag": "Tag",
|
||||||
"LabelTags": "Tags",
|
"LabelTags": "Tags",
|
||||||
"LabelTagsAccessibleToUser": "Tags dostupni korisniku",
|
"LabelTagsAccessibleToUser": "Tags dostupni korisniku",
|
||||||
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "Tasks Running",
|
"LabelTasks": "Tasks Running",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "Time Base",
|
||||||
"LabelTimeListened": "Vremena odslušano",
|
"LabelTimeListened": "Vremena odslušano",
|
||||||
@@ -481,6 +486,7 @@
|
|||||||
"MessageConfirmRemoveCollection": "AJeste li sigurni da želite obrisati kolekciju \"{0}\"?",
|
"MessageConfirmRemoveCollection": "AJeste li sigurni da želite obrisati kolekciju \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "Jeste li sigurni da želite obrisati epizodu \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Jeste li sigurni da želite obrisati epizodu \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "Jeste li sigurni da želite obrisati {0} epizoda/-u?",
|
"MessageConfirmRemoveEpisodes": "Jeste li sigurni da želite obrisati {0} epizoda/-u?",
|
||||||
|
"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?",
|
||||||
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "Rimuovi Tutto",
|
"ButtonRemoveAll": "Rimuovi Tutto",
|
||||||
"ButtonRemoveAllLibraryItems": "Rimuovi tutto il contenuto della libreria",
|
"ButtonRemoveAllLibraryItems": "Rimuovi tutto il contenuto della libreria",
|
||||||
"ButtonRemoveFromContinueListening": "Rimuovi per proseguire l'ascolto",
|
"ButtonRemoveFromContinueListening": "Rimuovi per proseguire l'ascolto",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Rimuovi la Serie per Continuarla",
|
"ButtonRemoveSeriesFromContinueSeries": "Rimuovi la Serie per Continuarla",
|
||||||
"ButtonReScan": "Ri-scansiona",
|
"ButtonReScan": "Ri-scansiona",
|
||||||
"ButtonReset": "Reset",
|
"ButtonReset": "Reset",
|
||||||
@@ -197,6 +198,7 @@
|
|||||||
"LabelComplete": "Completo",
|
"LabelComplete": "Completo",
|
||||||
"LabelConfirmPassword": "Conferma Password",
|
"LabelConfirmPassword": "Conferma Password",
|
||||||
"LabelContinueListening": "Continua ad Ascoltare",
|
"LabelContinueListening": "Continua ad Ascoltare",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Continua Serie",
|
"LabelContinueSeries": "Continua Serie",
|
||||||
"LabelCover": "Cover",
|
"LabelCover": "Cover",
|
||||||
"LabelCoverImageURL": "Cover Image URL",
|
"LabelCoverImageURL": "Cover Image URL",
|
||||||
@@ -254,6 +256,7 @@
|
|||||||
"LabelIntervalEveryDay": "Ogni Giorno",
|
"LabelIntervalEveryDay": "Ogni Giorno",
|
||||||
"LabelIntervalEveryHour": "Ogni ora",
|
"LabelIntervalEveryHour": "Ogni ora",
|
||||||
"LabelInvalidParts": "Parti Invalide",
|
"LabelInvalidParts": "Parti Invalide",
|
||||||
|
"LabelInvert": "Invert",
|
||||||
"LabelItem": "Oggetti",
|
"LabelItem": "Oggetti",
|
||||||
"LabelLanguage": "Lingua",
|
"LabelLanguage": "Lingua",
|
||||||
"LabelLanguageDefaultServer": "Lingua di Default",
|
"LabelLanguageDefaultServer": "Lingua di Default",
|
||||||
@@ -330,6 +333,7 @@
|
|||||||
"LabelPubDate": "Data Pubblicazione",
|
"LabelPubDate": "Data Pubblicazione",
|
||||||
"LabelPublisher": "Editore",
|
"LabelPublisher": "Editore",
|
||||||
"LabelPublishYear": "Anno Pubblicazione",
|
"LabelPublishYear": "Anno Pubblicazione",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
||||||
"LabelRecentSeries": "Serie Recenti",
|
"LabelRecentSeries": "Serie Recenti",
|
||||||
"LabelRecommended": "Raccomandati",
|
"LabelRecommended": "Raccomandati",
|
||||||
@@ -412,6 +416,7 @@
|
|||||||
"LabelTag": "Tag",
|
"LabelTag": "Tag",
|
||||||
"LabelTags": "Tags",
|
"LabelTags": "Tags",
|
||||||
"LabelTagsAccessibleToUser": "Tags permessi agli Utenti",
|
"LabelTagsAccessibleToUser": "Tags permessi agli Utenti",
|
||||||
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "Processi in esecuzione",
|
"LabelTasks": "Processi in esecuzione",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "Time Base",
|
||||||
"LabelTimeListened": "Tempo di Ascolto",
|
"LabelTimeListened": "Tempo di Ascolto",
|
||||||
@@ -481,6 +486,7 @@
|
|||||||
"MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Sei sicuro di voler rimuovere la Raccolta \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "Sei sicuro di voler rimuovere l'episodio \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Sei sicuro di voler rimuovere l'episodio \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "Sei sicuro di voler rimuovere {0} episodi?",
|
"MessageConfirmRemoveEpisodes": "Sei sicuro di voler rimuovere {0} episodi?",
|
||||||
|
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
||||||
"MessageConfirmRemovePlaylist": "Sei sicuro di voler rimuovere la tua playlist \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "Sei sicuro di voler rimuovere la tua playlist \"{0}\"?",
|
||||||
"MessageConfirmRenameGenre": "Sei sicuro di voler rinominare il genere \"{0}\" in \"{1}\" per tutti gli oggetti?",
|
"MessageConfirmRenameGenre": "Sei sicuro di voler rinominare il genere \"{0}\" in \"{1}\" per tutti gli oggetti?",
|
||||||
"MessageConfirmRenameGenreMergeNote": "Note: Questo genere esiste già quindi verra unito.",
|
"MessageConfirmRenameGenreMergeNote": "Note: Questo genere esiste già quindi verra unito.",
|
||||||
|
|||||||
@@ -0,0 +1,660 @@
|
|||||||
|
{
|
||||||
|
"ButtonAdd": "Toevoegen",
|
||||||
|
"ButtonAddChapters": "Hoofdstukken toevoegen",
|
||||||
|
"ButtonAddPodcasts": "Podcasts toevoegen",
|
||||||
|
"ButtonAddYourFirstLibrary": "Voeg je eerste bibliotheek toe",
|
||||||
|
"ButtonApply": "Pas toe",
|
||||||
|
"ButtonApplyChapters": "Hoofdstukken toepassen",
|
||||||
|
"ButtonAuthors": "Auteurs",
|
||||||
|
"ButtonBrowseForFolder": "Bladeren naar map",
|
||||||
|
"ButtonCancel": "Annuleren",
|
||||||
|
"ButtonCancelEncode": "Encoding annuleren",
|
||||||
|
"ButtonChangeRootPassword": "Root-wachtwoord wijzigen",
|
||||||
|
"ButtonCheckAndDownloadNewEpisodes": "Check & Download nieuwe afleveringen",
|
||||||
|
"ButtonChooseAFolder": "Map kiezen",
|
||||||
|
"ButtonChooseFiles": "Bestanden kiezen",
|
||||||
|
"ButtonClearFilter": "Filter verwijderen",
|
||||||
|
"ButtonCloseFeed": "Feed sluiten",
|
||||||
|
"ButtonCollections": "Collecties",
|
||||||
|
"ButtonConfigureScanner": "Configureer scanner",
|
||||||
|
"ButtonCreate": "Creëer",
|
||||||
|
"ButtonCreateBackup": "Maak back-up",
|
||||||
|
"ButtonDelete": "Verwijder",
|
||||||
|
"ButtonDownloadQueue": "Wachtrij",
|
||||||
|
"ButtonEdit": "Wijzig",
|
||||||
|
"ButtonEditChapters": "Hoofdstukken wijzigen",
|
||||||
|
"ButtonEditPodcast": "Podcast wijzigen",
|
||||||
|
"ButtonForceReScan": "Forceer nieuwe scan",
|
||||||
|
"ButtonFullPath": "Volledig pad",
|
||||||
|
"ButtonHide": "Verberg",
|
||||||
|
"ButtonHome": "Home",
|
||||||
|
"ButtonIssues": "Issues",
|
||||||
|
"ButtonLatest": "Meest recent",
|
||||||
|
"ButtonLibrary": "Bibliotheek",
|
||||||
|
"ButtonLogout": "Log uit",
|
||||||
|
"ButtonLookup": "Zoeken",
|
||||||
|
"ButtonManageTracks": "Beheer tracks",
|
||||||
|
"ButtonMapChapterTitles": "Hoofdstuktitels mappen",
|
||||||
|
"ButtonMatchAllAuthors": "Alle auteurs matchen",
|
||||||
|
"ButtonMatchBooks": "Alle boeken matchen",
|
||||||
|
"ButtonNevermind": "Laat maar",
|
||||||
|
"ButtonOk": "Ok",
|
||||||
|
"ButtonOpenFeed": "Feed openen",
|
||||||
|
"ButtonOpenManager": "Manager openen",
|
||||||
|
"ButtonPlay": "Afspelen",
|
||||||
|
"ButtonPlaying": "Speelt",
|
||||||
|
"ButtonPlaylists": "Afspeellijsten",
|
||||||
|
"ButtonPurgeAllCache": "Volledige cache legen",
|
||||||
|
"ButtonPurgeItemsCache": "Onderdelen-cache legen",
|
||||||
|
"ButtonPurgeMediaProgress": "Mediavoortgang legen",
|
||||||
|
"ButtonQueueAddItem": "In wachtrij zetten",
|
||||||
|
"ButtonQueueRemoveItem": "Uit wachtrij verwijderen",
|
||||||
|
"ButtonQuickMatch": "Snelle match",
|
||||||
|
"ButtonRead": "Lees",
|
||||||
|
"ButtonRemove": "Verwijder",
|
||||||
|
"ButtonRemoveAll": "Alles verwijderen",
|
||||||
|
"ButtonRemoveAllLibraryItems": "Verwijder volledige bibliotheekinhoud",
|
||||||
|
"ButtonRemoveFromContinueListening": "Vewijder uit Verder luisteren",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
|
"ButtonRemoveSeriesFromContinueSeries": "Verwijder serie uit Serie vervolgen",
|
||||||
|
"ButtonReScan": "Nieuwe scan",
|
||||||
|
"ButtonReset": "Reset",
|
||||||
|
"ButtonRestore": "Herstel",
|
||||||
|
"ButtonSave": "Opslaan",
|
||||||
|
"ButtonSaveAndClose": "Opslaan & sluiten",
|
||||||
|
"ButtonSaveTracklist": "Afspeellijst opslaan",
|
||||||
|
"ButtonScan": "Scan",
|
||||||
|
"ButtonScanLibrary": "Scan bibliotheek",
|
||||||
|
"ButtonSearch": "Zoeken",
|
||||||
|
"ButtonSelectFolderPath": "Maplocatie selecteren",
|
||||||
|
"ButtonSeries": "Series",
|
||||||
|
"ButtonSetChaptersFromTracks": "Maak hoofdstukken op basis van tracks",
|
||||||
|
"ButtonShiftTimes": "Tijden verschuiven",
|
||||||
|
"ButtonShow": "Toon",
|
||||||
|
"ButtonStartM4BEncode": "Start M4B-encoding",
|
||||||
|
"ButtonStartMetadataEmbed": "Start insluiten metadata",
|
||||||
|
"ButtonSubmit": "Indienen",
|
||||||
|
"ButtonUpload": "Upload",
|
||||||
|
"ButtonUploadBackup": "Upload back-up",
|
||||||
|
"ButtonUploadCover": "Upload cover",
|
||||||
|
"ButtonUploadOPMLFile": "Upload OPML-bestand",
|
||||||
|
"ButtonUserDelete": "Verwijder gebruiker {0}",
|
||||||
|
"ButtonUserEdit": "Wijzig gebruiker {0}",
|
||||||
|
"ButtonViewAll": "Toon alle",
|
||||||
|
"ButtonYes": "Ja",
|
||||||
|
"HeaderAccount": "Account",
|
||||||
|
"HeaderAdvanced": "Geavanceerd",
|
||||||
|
"HeaderAppriseNotificationSettings": "Apprise-notificatie instellingen",
|
||||||
|
"HeaderAudiobookTools": "Audioboekbestandbeheer tools",
|
||||||
|
"HeaderAudioTracks": "Audiotracks",
|
||||||
|
"HeaderBackups": "Back-ups",
|
||||||
|
"HeaderChangePassword": "Wachtwoord wijzigen",
|
||||||
|
"HeaderChapters": "Hoofdstukken",
|
||||||
|
"HeaderChooseAFolder": "Map kiezen",
|
||||||
|
"HeaderCollection": "Collectie",
|
||||||
|
"HeaderCollectionItems": "Collectie-objecten",
|
||||||
|
"HeaderCover": "Cover",
|
||||||
|
"HeaderCurrentDownloads": "Huidige downloads",
|
||||||
|
"HeaderDetails": "Details",
|
||||||
|
"HeaderDownloadQueue": "Download-wachtrij",
|
||||||
|
"HeaderEpisodes": "Afleveringen",
|
||||||
|
"HeaderFiles": "Bestanden",
|
||||||
|
"HeaderFindChapters": "Zoek hoofdstukken",
|
||||||
|
"HeaderIgnoredFiles": "Genegeerde bestanden",
|
||||||
|
"HeaderItemFiles": "Onderdeel-bestanden",
|
||||||
|
"HeaderItemMetadataUtils": "Onderdeel-metadata Utils",
|
||||||
|
"HeaderLastListeningSession": "Laatste luistersessie",
|
||||||
|
"HeaderLatestEpisodes": "Laatste afleveringen",
|
||||||
|
"HeaderLibraries": "Bibliotheken",
|
||||||
|
"HeaderLibraryFiles": "Bibliotheekbestanden",
|
||||||
|
"HeaderLibraryStats": "Bibliotheekstatistieken",
|
||||||
|
"HeaderListeningSessions": "Luistersessies",
|
||||||
|
"HeaderListeningStats": "Luisterstatistieken",
|
||||||
|
"HeaderLogin": "Login",
|
||||||
|
"HeaderLogs": "Logs",
|
||||||
|
"HeaderManageGenres": "Genres beheren",
|
||||||
|
"HeaderManageTags": "Tags beheren",
|
||||||
|
"HeaderMapDetails": "Map details",
|
||||||
|
"HeaderMatch": "Match",
|
||||||
|
"HeaderMetadataToEmbed": "In te sluiten metadata",
|
||||||
|
"HeaderNewAccount": "Nieuwe account",
|
||||||
|
"HeaderNewLibrary": "Nieuwe bibliotheek",
|
||||||
|
"HeaderNotifications": "Notificaties",
|
||||||
|
"HeaderOpenRSSFeed": "Open RSS-feed",
|
||||||
|
"HeaderOtherFiles": "Andere bestanden",
|
||||||
|
"HeaderPermissions": "Toestemmingen",
|
||||||
|
"HeaderPlayerQueue": "Afspeelwachtrij",
|
||||||
|
"HeaderPlaylist": "Afspeellijst",
|
||||||
|
"HeaderPlaylistItems": "Onderdelen in afspeellijst",
|
||||||
|
"HeaderPodcastsToAdd": "Toe te voegen podcasts",
|
||||||
|
"HeaderPreviewCover": "Preview cover",
|
||||||
|
"HeaderRemoveEpisode": "Aflevering verwijderen",
|
||||||
|
"HeaderRemoveEpisodes": "Verwijder {0} afleveringen",
|
||||||
|
"HeaderRSSFeedGeneral": "RSS-details",
|
||||||
|
"HeaderRSSFeedIsOpen": "RSS-feed is open",
|
||||||
|
"HeaderSavedMediaProgress": "Opgeslagen mediavoortgang",
|
||||||
|
"HeaderSchedule": "Schema",
|
||||||
|
"HeaderScheduleLibraryScans": "Schema automatische bibliotheekscans",
|
||||||
|
"HeaderSession": "Sessie",
|
||||||
|
"HeaderSetBackupSchedule": "Kies schema voor back-up",
|
||||||
|
"HeaderSettings": "Instellingen",
|
||||||
|
"HeaderSettingsDisplay": "Toon",
|
||||||
|
"HeaderSettingsExperimental": "Experimentele functies",
|
||||||
|
"HeaderSettingsGeneral": "Algemeen",
|
||||||
|
"HeaderSettingsScanner": "Scanner",
|
||||||
|
"HeaderSleepTimer": "Slaaptimer",
|
||||||
|
"HeaderStatsLargestItems": "Grootste items",
|
||||||
|
"HeaderStatsLongestItems": "Langste items (uren)",
|
||||||
|
"HeaderStatsMinutesListeningChart": "Minuten geluisterd (laatste 7 dagen)",
|
||||||
|
"HeaderStatsRecentSessions": "Recente sessies",
|
||||||
|
"HeaderStatsTop10Authors": "Top 10 auteurs",
|
||||||
|
"HeaderStatsTop5Genres": "Top 5 genres",
|
||||||
|
"HeaderTools": "Tools",
|
||||||
|
"HeaderUpdateAccount": "Account bijwerken",
|
||||||
|
"HeaderUpdateAuthor": "Auteur bijwerken",
|
||||||
|
"HeaderUpdateDetails": "Details bijwerken",
|
||||||
|
"HeaderUpdateLibrary": "Bibliotheek bijwerken",
|
||||||
|
"HeaderUsers": "Gebruikers",
|
||||||
|
"HeaderYourStats": "Je statistieken",
|
||||||
|
"LabelAbridged": "Verkort",
|
||||||
|
"LabelAccountType": "Accounttype",
|
||||||
|
"LabelAccountTypeAdmin": "Beheerder",
|
||||||
|
"LabelAccountTypeGuest": "Gast",
|
||||||
|
"LabelAccountTypeUser": "Gebruiker",
|
||||||
|
"LabelActivity": "Activiteit",
|
||||||
|
"LabelAdded": "Toegevoegd",
|
||||||
|
"LabelAddedAt": "Toegevoegd op",
|
||||||
|
"LabelAddToCollection": "Toevoegen aan collectie",
|
||||||
|
"LabelAddToCollectionBatch": "{0} boeken toevoegen aan collectie",
|
||||||
|
"LabelAddToPlaylist": "Toevoegen aan afspeellijst",
|
||||||
|
"LabelAddToPlaylistBatch": "{0} onderdelen toevoegen aan afspeellijst",
|
||||||
|
"LabelAll": "Alle",
|
||||||
|
"LabelAllUsers": "Alle gebruikers",
|
||||||
|
"LabelAlreadyInYourLibrary": "Reeds in je bibliotheek",
|
||||||
|
"LabelAppend": "Append",
|
||||||
|
"LabelAuthor": "Auteur",
|
||||||
|
"LabelAuthorFirstLast": "Auteur (Voornaam Achternaam)",
|
||||||
|
"LabelAuthorLastFirst": "Auteur (Achternaam, Voornaam)",
|
||||||
|
"LabelAuthors": "Auteurs",
|
||||||
|
"LabelAutoDownloadEpisodes": "Afleveringen automatisch downloaden",
|
||||||
|
"LabelBackToUser": "Terug naar gebruiker",
|
||||||
|
"LabelBackupsEnableAutomaticBackups": "Automatische back-ups inschakelen",
|
||||||
|
"LabelBackupsEnableAutomaticBackupsHelp": "Back-ups opgeslagen in /metadata/backups",
|
||||||
|
"LabelBackupsMaxBackupSize": "Maximale back-up-grootte (in GB)",
|
||||||
|
"LabelBackupsMaxBackupSizeHelp": "Als een beveiliging tegen verkeerde instelling, zullen back-up mislukken als ze de ingestelde grootte overschrijden.",
|
||||||
|
"LabelBackupsNumberToKeep": "Aantal te bewaren back-ups",
|
||||||
|
"LabelBackupsNumberToKeepHelp": "Er wordt slechts 1 back-up per keer verwijderd, dus als je reeds meer back-ups dan dit hebt moet je ze handmatig verwijderen.",
|
||||||
|
"LabelBitrate": "Bitrate",
|
||||||
|
"LabelBooks": "Boeken",
|
||||||
|
"LabelChangePassword": "Wachtwoord wijzigen",
|
||||||
|
"LabelChannels": "Kanalen",
|
||||||
|
"LabelChapters": "Hoofdstukken",
|
||||||
|
"LabelChaptersFound": "Hoofdstukken gevonden",
|
||||||
|
"LabelChapterTitle": "Hoofdstuktitel",
|
||||||
|
"LabelClosePlayer": "Sluit speler",
|
||||||
|
"LabelCodec": "Codec",
|
||||||
|
"LabelCollapseSeries": "Series inklappen",
|
||||||
|
"LabelCollections": "Collecties",
|
||||||
|
"LabelComplete": "Compleet",
|
||||||
|
"LabelConfirmPassword": "Bevestig wachtwoord",
|
||||||
|
"LabelContinueListening": "Verder luisteren",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
|
"LabelContinueSeries": "Ga verder met serie",
|
||||||
|
"LabelCover": "Cover",
|
||||||
|
"LabelCoverImageURL": "Coverafbeelding URL",
|
||||||
|
"LabelCreatedAt": "Gecreëerd op",
|
||||||
|
"LabelCronExpression": "Cron-uitdrukking",
|
||||||
|
"LabelCurrent": "Huidig",
|
||||||
|
"LabelCurrently": "Op dit moment:",
|
||||||
|
"LabelCustomCronExpression": "Aangepaste Cron-uitdrukking:",
|
||||||
|
"LabelDatetime": "Datum-tijd",
|
||||||
|
"LabelDescription": "Beschrijving",
|
||||||
|
"LabelDeselectAll": "Deselecteer alle",
|
||||||
|
"LabelDevice": "Apparaat",
|
||||||
|
"LabelDeviceInfo": "Apparaat info",
|
||||||
|
"LabelDirectory": "Map",
|
||||||
|
"LabelDiscFromFilename": "Schijf uit bestandsnaam",
|
||||||
|
"LabelDiscFromMetadata": "Schijf uit metadata",
|
||||||
|
"LabelDownload": "Download",
|
||||||
|
"LabelDuration": "Duur",
|
||||||
|
"LabelDurationFound": "Gevonden duur:",
|
||||||
|
"LabelEdit": "Wijzig",
|
||||||
|
"LabelEmbeddedCover": "Ingesloten cover",
|
||||||
|
"LabelEnable": "Inschakelen",
|
||||||
|
"LabelEnd": "Einde",
|
||||||
|
"LabelEpisode": "Aflevering",
|
||||||
|
"LabelEpisodeTitle": "Afleveringtitel",
|
||||||
|
"LabelEpisodeType": "Afleveringtype",
|
||||||
|
"LabelExample": "Voorbeeld",
|
||||||
|
"LabelExplicit": "Expliciet",
|
||||||
|
"LabelFeedURL": "Feed URL",
|
||||||
|
"LabelFile": "Bestand",
|
||||||
|
"LabelFileBirthtime": "Aanmaaktijd bestand",
|
||||||
|
"LabelFileModified": "Bestand gewijzigd",
|
||||||
|
"LabelFilename": "Bestandsnaam",
|
||||||
|
"LabelFilterByUser": "Filter op gebruiker",
|
||||||
|
"LabelFindEpisodes": "Zoek afleveringen",
|
||||||
|
"LabelFinished": "Voltooid",
|
||||||
|
"LabelFolder": "Map",
|
||||||
|
"LabelFolders": "Mappen",
|
||||||
|
"LabelFormat": "Format",
|
||||||
|
"LabelGenre": "Genre",
|
||||||
|
"LabelGenres": "Genres",
|
||||||
|
"LabelHardDeleteFile": "Hard-delete bestand",
|
||||||
|
"LabelHour": "Uur",
|
||||||
|
"LabelIcon": "Icoon",
|
||||||
|
"LabelIncludeInTracklist": "Includeer in tracklijst",
|
||||||
|
"LabelIncomplete": "Incompleet",
|
||||||
|
"LabelInProgress": "Bezig",
|
||||||
|
"LabelInterval": "Interval",
|
||||||
|
"LabelIntervalCustomDailyWeekly": "Aangepast dagelijks/wekelijks",
|
||||||
|
"LabelIntervalEvery12Hours": "Iedere 12 uur",
|
||||||
|
"LabelIntervalEvery15Minutes": "Iedere 15 minuten",
|
||||||
|
"LabelIntervalEvery2Hours": "Iedere 2 uur",
|
||||||
|
"LabelIntervalEvery30Minutes": "Iedere 30 minuten",
|
||||||
|
"LabelIntervalEvery6Hours": "Iedere 6 uur",
|
||||||
|
"LabelIntervalEveryDay": "Iedere dag",
|
||||||
|
"LabelIntervalEveryHour": "Ieder uur",
|
||||||
|
"LabelInvalidParts": "Ongeldige delen",
|
||||||
|
"LabelInvert": "Omdraaien",
|
||||||
|
"LabelItem": "Onderdeel",
|
||||||
|
"LabelLanguage": "Taal",
|
||||||
|
"LabelLanguageDefaultServer": "Standaard servertaal",
|
||||||
|
"LabelLastBookAdded": "Laatst toegevoegde boek",
|
||||||
|
"LabelLastBookUpdated": "Laatst bijgewerkte boek",
|
||||||
|
"LabelLastSeen": "Laatst gezien",
|
||||||
|
"LabelLastTime": "Laatste keer",
|
||||||
|
"LabelLastUpdate": "Laatste update",
|
||||||
|
"LabelLess": "Minder",
|
||||||
|
"LabelLibrariesAccessibleToUser": "Voor gebruiker toegankelijke bibliotheken",
|
||||||
|
"LabelLibrary": "Bibliotheek",
|
||||||
|
"LabelLibraryItem": "Library Item",
|
||||||
|
"LabelLibraryName": "Library Name",
|
||||||
|
"LabelLimit": "Limiet",
|
||||||
|
"LabelListenAgain": "Luister opnieuw",
|
||||||
|
"LabelLogLevelDebug": "Debug",
|
||||||
|
"LabelLogLevelInfo": "Info",
|
||||||
|
"LabelLogLevelWarn": "Waarschuwing",
|
||||||
|
"LabelLookForNewEpisodesAfterDate": "Zoek naar nieuwe afleveringen na deze datum",
|
||||||
|
"LabelMediaPlayer": "Mediaspeler",
|
||||||
|
"LabelMediaType": "Mediatype",
|
||||||
|
"LabelMetadataProvider": "Metadatabron",
|
||||||
|
"LabelMetaTag": "Meta-tag",
|
||||||
|
"LabelMetaTags": "Meta-tags",
|
||||||
|
"LabelMinute": "Minuut",
|
||||||
|
"LabelMissing": "Ontbrekend",
|
||||||
|
"LabelMissingParts": "Ontbrekende delen",
|
||||||
|
"LabelMore": "Meer",
|
||||||
|
"LabelMoreInfo": "Meer info",
|
||||||
|
"LabelName": "Naam",
|
||||||
|
"LabelNarrator": "Verteller",
|
||||||
|
"LabelNarrators": "Vertellers",
|
||||||
|
"LabelNew": "Nieuw",
|
||||||
|
"LabelNewestAuthors": "Nieuwste auteurs",
|
||||||
|
"LabelNewestEpisodes": "Nieuwste afleveringen",
|
||||||
|
"LabelNewPassword": "Nieuw wachtwoord",
|
||||||
|
"LabelNextBackupDate": "Volgende back-up datum",
|
||||||
|
"LabelNextScheduledRun": "Volgende geplande run",
|
||||||
|
"LabelNotes": "Notities",
|
||||||
|
"LabelNotFinished": "Niet Voltooid",
|
||||||
|
"LabelNotificationAppriseURL": "Apprise URL(s)",
|
||||||
|
"LabelNotificationAvailableVariables": "Beschikbare variabelen",
|
||||||
|
"LabelNotificationBodyTemplate": "Body-template",
|
||||||
|
"LabelNotificationEvent": "Notificatie gebeurtenis",
|
||||||
|
"LabelNotificationsMaxFailedAttempts": "Max mislukte pogingen",
|
||||||
|
"LabelNotificationsMaxFailedAttemptsHelp": "Notificaties worden uitgeschakeld als verzenden zo vaak mislukt",
|
||||||
|
"LabelNotificationsMaxQueueSize": "Max rijgrootte voor notificatie gebeurtenissen",
|
||||||
|
"LabelNotificationsMaxQueueSizeHelp": "Gebeurtenissen zijn beperkt tot 1 aftrap per seconde. Gebeurtenissen zullen genegeerd worden als de rij aan de maximale grootte zit. Dit voorkomt notificatie-spamming.",
|
||||||
|
"LabelNotificationTitleTemplate": "Titel-template",
|
||||||
|
"LabelNotStarted": "Niet Gestart",
|
||||||
|
"LabelNumberOfBooks": "Aantal Boeken",
|
||||||
|
"LabelNumberOfEpisodes": "# afleveringen",
|
||||||
|
"LabelOpenRSSFeed": "Open RSS-feed",
|
||||||
|
"LabelOverwrite": "Overschrijf",
|
||||||
|
"LabelPassword": "Wachtwoord",
|
||||||
|
"LabelPath": "Pad",
|
||||||
|
"LabelPermissionsAccessAllLibraries": "Heeft toegang tot all bibliotheken",
|
||||||
|
"LabelPermissionsAccessAllTags": "Heeft toegang tot alle tags",
|
||||||
|
"LabelPermissionsAccessExplicitContent": "Heeft toegang tot expliciete inhoud",
|
||||||
|
"LabelPermissionsDelete": "Kan verwijderen",
|
||||||
|
"LabelPermissionsDownload": "Kan downloaden",
|
||||||
|
"LabelPermissionsUpdate": "Kan bijwerken",
|
||||||
|
"LabelPermissionsUpload": "Kan uploaden",
|
||||||
|
"LabelPhotoPathURL": "Foto pad/URL",
|
||||||
|
"LabelPlaylists": "Afspeellijsten",
|
||||||
|
"LabelPlayMethod": "Afspeelwijze",
|
||||||
|
"LabelPodcast": "Podcast",
|
||||||
|
"LabelPodcasts": "Podcasts",
|
||||||
|
"LabelPodcastType": "Podcasttype",
|
||||||
|
"LabelPrefixesToIgnore": "Te negeren voorzetsels (ongeacht hoofdlettergebruik)",
|
||||||
|
"LabelPreventIndexing": "Voorkom indexering van je feed door iTunes- en Google podcastmappen",
|
||||||
|
"LabelProgress": "Voortgang",
|
||||||
|
"LabelProvider": "Bron",
|
||||||
|
"LabelPubDate": "Publicatiedatum",
|
||||||
|
"LabelPublisher": "Uitgever",
|
||||||
|
"LabelPublishYear": "Jaar van uitgave",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
|
"LabelRecentlyAdded": "Recent toegevoegd",
|
||||||
|
"LabelRecentSeries": "Recente series",
|
||||||
|
"LabelRecommended": "Aangeraden",
|
||||||
|
"LabelRegion": "Regio",
|
||||||
|
"LabelReleaseDate": "Verschijningsdatum",
|
||||||
|
"LabelRemoveCover": "Verwijder cover",
|
||||||
|
"LabelRSSFeedCustomOwnerEmail": "Aangepast e-mailadres eigenaar",
|
||||||
|
"LabelRSSFeedCustomOwnerName": "Aangepaste naam eigenaar",
|
||||||
|
"LabelRSSFeedOpen": "RSS-feed open",
|
||||||
|
"LabelRSSFeedPreventIndexing": "Voorkom indexering",
|
||||||
|
"LabelRSSFeedSlug": "RSS-feed slug",
|
||||||
|
"LabelRSSFeedURL": "RSS-feed URL",
|
||||||
|
"LabelSearchTerm": "Zoekterm",
|
||||||
|
"LabelSearchTitle": "Zoek titel",
|
||||||
|
"LabelSearchTitleOrASIN": "Zoek titel of ASIN",
|
||||||
|
"LabelSeason": "Seizoen",
|
||||||
|
"LabelSequence": "Sequentie",
|
||||||
|
"LabelSeries": "Serie",
|
||||||
|
"LabelSeriesName": "Naam serie",
|
||||||
|
"LabelSeriesProgress": "Voortgang serie",
|
||||||
|
"LabelSettingsBookshelfViewHelp": "Skeumorphisch design met houten planken",
|
||||||
|
"LabelSettingsChromecastSupport": "Chromecast support",
|
||||||
|
"LabelSettingsDateFormat": "Datum format",
|
||||||
|
"LabelSettingsDisableWatcher": "Watcher uitschakelen",
|
||||||
|
"LabelSettingsDisableWatcherForLibrary": "Map-watcher voor bibliotheek uitschakelen",
|
||||||
|
"LabelSettingsDisableWatcherHelp": "Schakelt het automatisch toevoegen/bijwerken van onderdelen wanneer bestandswijzigingen gedetecteerd zijn uit. *Vereist herstart server",
|
||||||
|
"LabelSettingsEnableEReader": "E-reader inschakelen voor alle gebruikers",
|
||||||
|
"LabelSettingsEnableEReaderHelp": "E-reader is nog in ontwikkeling, maar gebruik deze instelling om het beschikbaar te maken voor al je gebruikers (of gebruik de \"Experimentele functies\"-schakelaar voor eigen gebruik)",
|
||||||
|
"LabelSettingsExperimentalFeatures": "Experimentele functies",
|
||||||
|
"LabelSettingsExperimentalFeaturesHelp": "Functies in ontwikkeling die je feedback en testing kunnen gebruiken. Klik om de Github-discussie te openen.",
|
||||||
|
"LabelSettingsFindCovers": "Zoek covers",
|
||||||
|
"LabelSettingsFindCoversHelp": "Als je audioboek geen ingesloten cover of cover in de map heeft, zal de scanner proberen een cover te vinden.<br>Opmerking: Dit zal de scan-duur verlengen",
|
||||||
|
"LabelSettingsHomePageBookshelfView": "Boekenplank-view voor homepagina",
|
||||||
|
"LabelSettingsLibraryBookshelfView": "Boekenplank-view voor bibliotheek",
|
||||||
|
"LabelSettingsOverdriveMediaMarkers": "Gebruik Overdrive media markers voor hoofdstukken",
|
||||||
|
"LabelSettingsOverdriveMediaMarkersHelp": "MP3-bestanden van Overdrive hebben hoofdstuktiming ingesloten als custom ingesloten metadata. Door dit in te schakelen worden deze tags voor hoofdstuktiming automatisch gebruikt.",
|
||||||
|
"LabelSettingsParseSubtitles": "Parseer subtitel",
|
||||||
|
"LabelSettingsParseSubtitlesHelp": "Haal subtitels uit mapnaam van audioboek.<br>Subtitel moet gescheiden zijn met \" - \"<br>b.v. \"Boektitel - Een Subtitel Hier\" heeft als subtitel \"Een Subtitel Hier\"",
|
||||||
|
"LabelSettingsPreferAudioMetadata": "Prefereer audio-metadata",
|
||||||
|
"LabelSettingsPreferAudioMetadataHelp": "Audiobestand ID3 metatags zullen worden gebruikt voor boekdetails in plaats van mapnamen",
|
||||||
|
"LabelSettingsPreferMatchedMetadata": "Prefereer gematchte metadata",
|
||||||
|
"LabelSettingsPreferMatchedMetadataHelp": "Gematchte data zal onderdeeldetails overschrijven bij gebruik van Quick Match. Standaard vult Quick Match uitsluitend ontbrekende details aan.",
|
||||||
|
"LabelSettingsPreferOPFMetadata": "Prefereer OPF-metadata",
|
||||||
|
"LabelSettingsPreferOPFMetadataHelp": "OPF-bestand metadata zal worden gebruik in plaats van mapnamen",
|
||||||
|
"LabelSettingsSkipMatchingBooksWithASIN": "Sla matchen van boeken over die al over een ASIN beschikken",
|
||||||
|
"LabelSettingsSkipMatchingBooksWithISBN": "Sla matchen van boeken over die al over een ISBN beschikken",
|
||||||
|
"LabelSettingsSortingIgnorePrefixes": "Negeer voorvoegsels bij sorteren",
|
||||||
|
"LabelSettingsSortingIgnorePrefixesHelp": "b.v. voor voorvoegsel \"The\" wordt titel \"The Title\" dan gesorteerd als \"Title, The\"",
|
||||||
|
"LabelSettingsSquareBookCovers": "Gebruik vierkante boekcovers",
|
||||||
|
"LabelSettingsSquareBookCoversHelp": "Prefereer gebruik van vierkante covers boven standaard 1.6:1 boekcovers",
|
||||||
|
"LabelSettingsStoreCoversWithItem": "Bewaar covers bij onderdeel",
|
||||||
|
"LabelSettingsStoreCoversWithItemHelp": "Standaard worden covers bewaard in /metadata/items, door deze instelling in te schakelen zullen covers in de map van je bibliotheekonderdeel bewaard worden. Slechts een bestand genaamd \"cover\" zal worden bewaard",
|
||||||
|
"LabelSettingsStoreMetadataWithItem": "Bewaar metadata bij onderdeel",
|
||||||
|
"LabelSettingsStoreMetadataWithItemHelp": "Standaard worden metadata-bestanden bewaard in /metadata/items, door deze instelling in te schakelen zullen metadata bestanden in de map van je bibliotheekonderdeel bewaard worden. Gebruikt .abs-extensie",
|
||||||
|
"LabelSettingsTimeFormat": "Tijdformat",
|
||||||
|
"LabelShowAll": "Toon alle",
|
||||||
|
"LabelSize": "Grootte",
|
||||||
|
"LabelSleepTimer": "Slaaptimer",
|
||||||
|
"LabelStart": "Start",
|
||||||
|
"LabelStarted": "Gestart",
|
||||||
|
"LabelStartedAt": "Gestart op",
|
||||||
|
"LabelStartTime": "Starttijd",
|
||||||
|
"LabelStatsAudioTracks": "Audiotracks",
|
||||||
|
"LabelStatsAuthors": "Auteurs",
|
||||||
|
"LabelStatsBestDay": "Beste dag",
|
||||||
|
"LabelStatsDailyAverage": "Dagelijks gemiddelde",
|
||||||
|
"LabelStatsDays": "Dagen",
|
||||||
|
"LabelStatsDaysListened": "Dagen geluisterd",
|
||||||
|
"LabelStatsHours": "Uren",
|
||||||
|
"LabelStatsInARow": "op een rij",
|
||||||
|
"LabelStatsItemsFinished": "Onderdelen voltooid",
|
||||||
|
"LabelStatsItemsInLibrary": "Onderdeel in bibliotheek",
|
||||||
|
"LabelStatsMinutes": "minuten",
|
||||||
|
"LabelStatsMinutesListening": "Minuten luisterend",
|
||||||
|
"LabelStatsOverallDays": "Overall dagen",
|
||||||
|
"LabelStatsOverallHours": "Overall uren",
|
||||||
|
"LabelStatsWeekListening": "Week luisterend",
|
||||||
|
"LabelSubtitle": "Subtitel",
|
||||||
|
"LabelSupportedFileTypes": "Ondersteunde bestandstypes",
|
||||||
|
"LabelTag": "Tag",
|
||||||
|
"LabelTags": "Tags",
|
||||||
|
"LabelTagsAccessibleToUser": "Tags toegankelijk voor de gebruiker",
|
||||||
|
"LabelTagsNotAccessibleToUser": "Tags niet toegankelijk voor de gebruiker",
|
||||||
|
"LabelTasks": "Lopende taken",
|
||||||
|
"LabelTimeBase": "Tijdsbasis",
|
||||||
|
"LabelTimeListened": "Tijd geluisterd",
|
||||||
|
"LabelTimeListenedToday": "Tijd geluisterd vandaag",
|
||||||
|
"LabelTimeRemaining": "{0} te gaan",
|
||||||
|
"LabelTimeToShift": "Tijd op te schuiven in seconden",
|
||||||
|
"LabelTitle": "Titel",
|
||||||
|
"LabelToolsEmbedMetadata": "Metadata insluiten",
|
||||||
|
"LabelToolsEmbedMetadataDescription": "Metadata insluiten in audiobestanden, inclusief coverafbeelding en hoofdstukken.",
|
||||||
|
"LabelToolsMakeM4b": "Maak M4B-audioboekbestand",
|
||||||
|
"LabelToolsMakeM4bDescription": "Genereer een .M4B-audioboekbestand met ingesloten metadata, coverafbeelding en hoofdstukken.",
|
||||||
|
"LabelToolsSplitM4b": "Splitst M4B in MP3's",
|
||||||
|
"LabelToolsSplitM4bDescription": "Maak MP3's van een M4B, gesplitst per hoofdstuk met ingesloten metadata, coverafbeelding en hoofdstukken.",
|
||||||
|
"LabelTotalDuration": "Totale duur",
|
||||||
|
"LabelTotalTimeListened": "Totale tijd geluisterd",
|
||||||
|
"LabelTrackFromFilename": "Track vanuit bestandsnaam",
|
||||||
|
"LabelTrackFromMetadata": "Track vanuit metadata",
|
||||||
|
"LabelTracks": "Tracks",
|
||||||
|
"LabelTracksMultiTrack": "Multi-track",
|
||||||
|
"LabelTracksSingleTrack": "Single-track",
|
||||||
|
"LabelType": "Type",
|
||||||
|
"LabelUnabridged": "Onverkort",
|
||||||
|
"LabelUnknown": "Onbekend",
|
||||||
|
"LabelUpdateCover": "Cover bijwerken",
|
||||||
|
"LabelUpdateCoverHelp": "Sta overschrijven van bestaande covers toe voor de geselecteerde boeken wanneer een match is gevonden",
|
||||||
|
"LabelUpdatedAt": "Bijgewerkt op",
|
||||||
|
"LabelUpdateDetails": "Details bijwerken",
|
||||||
|
"LabelUpdateDetailsHelp": "Sta overschrijven van bestaande details toe voor de geselecteerde boeken wanneer een match is gevonden",
|
||||||
|
"LabelUploaderDragAndDrop": "Slepen & neerzeten van bestanden of mappen",
|
||||||
|
"LabelUploaderDropFiles": "Bestanden neerzetten",
|
||||||
|
"LabelUseChapterTrack": "Gebruik hoofdstuktrack",
|
||||||
|
"LabelUseFullTrack": "Gebruik volledige track",
|
||||||
|
"LabelUser": "Gebruiker",
|
||||||
|
"LabelUsername": "Gebruikersnaam",
|
||||||
|
"LabelValue": "Waarde",
|
||||||
|
"LabelVersion": "Versie",
|
||||||
|
"LabelViewBookmarks": "Bekijk boekwijzers",
|
||||||
|
"LabelViewChapters": "Bekijk hoofdstukken",
|
||||||
|
"LabelViewQueue": "Bekijk afspeelwachtrij",
|
||||||
|
"LabelVolume": "Volume",
|
||||||
|
"LabelWeekdaysToRun": "Weekdagen om te draaien",
|
||||||
|
"LabelYourAudiobookDuration": "Je audioboekduur",
|
||||||
|
"LabelYourBookmarks": "Je boekwijzers",
|
||||||
|
"LabelYourPlaylists": "Je afspeellijsten",
|
||||||
|
"LabelYourProgress": "Je voortgang",
|
||||||
|
"MessageAddToPlayerQueue": "Toevoegen aan wachtrij",
|
||||||
|
"MessageAppriseDescription": "Om deze functie te gebruiken heb je een draaiende instantie van <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> nodig of een api die dezelfde requests afhandelt. <br />De Apprise API Url moet het volledige URL-pad zijn om de notificatie te verzenden, b.v., als je API-instantie draait op <code>http://192.168.1.1:8337</code> dan zou je <code>http://192.168.1.1:8337/notify</code> gebruiken.",
|
||||||
|
"MessageBackupsDescription": "Back-ups omvatten gebruikers, gebruikers' voortgang, bibliotheekonderdeeldetails, serverinstellingen en afbeeldingen bewaard in <code>/metadata/items</code> & <code>/metadata/authors</code>. Back-ups <strong>bevatten niet</strong> de bestanden bewaard in je bibliotheekmappen.",
|
||||||
|
"MessageBatchQuickMatchDescription": "Quick Match zal proberen ontbrekende covers en metadata voor de geselecteerde onderdelen te matchten. Schakel de opties hieronder in om Quick Match toe te staan bestaande covers en/of metadata te overschrijven.",
|
||||||
|
"MessageBookshelfNoCollections": "Je hebt nog geen collecties gemaakt",
|
||||||
|
"MessageBookshelfNoResultsForFilter": "Geen resultaten voor filter \"{0}: {1}\"",
|
||||||
|
"MessageBookshelfNoRSSFeeds": "Geen RSS-feeds geopend",
|
||||||
|
"MessageBookshelfNoSeries": "Je hebt geen series",
|
||||||
|
"MessageChapterEndIsAfter": "Hoofdstukeinde is na het einde van je audioboek",
|
||||||
|
"MessageChapterErrorFirstNotZero": "Eerste hoofdstuk moet starten op 0",
|
||||||
|
"MessageChapterErrorStartGteDuration": "Ongeldig: starttijd moet kleiner zijn dan duur van audioboek",
|
||||||
|
"MessageChapterErrorStartLtPrev": "Ongeldig: starttijd moet be groter zijn dan of equal aan starttijd van vorig hoofdstuk",
|
||||||
|
"MessageChapterStartIsAfter": "Start van hoofdstuk is na het einde van je audioboek",
|
||||||
|
"MessageCheckingCron": "Cron aan het checken...",
|
||||||
|
"MessageConfirmDeleteBackup": "Weet je zeker dat je de backup voor {0} wil verwijderen?",
|
||||||
|
"MessageConfirmDeleteLibrary": "Weet je zeker dat je de bibliotheek \"{0}\" permanent wil verwijderen?",
|
||||||
|
"MessageConfirmDeleteSession": "Weet je zeker dat je deze sessie wil verwijderen?",
|
||||||
|
"MessageConfirmForceReScan": "Weet je zeker dat je geforceerd opnieuw wil scannen?",
|
||||||
|
"MessageConfirmMarkSeriesFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als voltooid?",
|
||||||
|
"MessageConfirmMarkSeriesNotFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als niet voltooid?",
|
||||||
|
"MessageConfirmRemoveAllChapters": "Weet je zeker dat je alle hoofdstukken wil verwijderen?",
|
||||||
|
"MessageConfirmRemoveCollection": "Weet je zeker dat je de collectie \"{0}\" wil verwijderen?",
|
||||||
|
"MessageConfirmRemoveEpisode": "Weet je zeker dat je de aflevering \"{0}\" wil verwijderen?",
|
||||||
|
"MessageConfirmRemoveEpisodes": "Weet je zeker dat je {0} afleveringen wil verwijderen?",
|
||||||
|
"MessageConfirmRemoveNarrator": "Weet je zeker dat je verteller \"{0}\" wil verwijderen?",
|
||||||
|
"MessageConfirmRemovePlaylist": "Weet je zeker dat je je afspeellijst \"{0}\" wil verwijderen?",
|
||||||
|
"MessageConfirmRenameGenre": "Weet je zeker dat je genre \"{0}\" wil hernoemen naar \"{1}\" voor alle onderdelen?",
|
||||||
|
"MessageConfirmRenameGenreMergeNote": "Opmerking: Dit genre bestaat al, dus zullen ze worden samengevoegd.",
|
||||||
|
"MessageConfirmRenameGenreWarning": "Waarschuwing! Een gelijknamig genre met ander hoofdlettergebruik bestaat al: \"{0}\".",
|
||||||
|
"MessageConfirmRenameTag": "Weet je zeker dat je tag \"{0}\" wil hernoemen naar\"{1}\" voor alle onderdelen?",
|
||||||
|
"MessageConfirmRenameTagMergeNote": "Opmerking: Deze tag bestaat al, dus zullen ze worden samengevoegd.",
|
||||||
|
"MessageConfirmRenameTagWarning": "Waarschuwing! Een gelijknamige tag met ander hoofdlettergebruik bestaat al: \"{0}\".",
|
||||||
|
"MessageDownloadingEpisode": "Aflevering aan het dowloaden",
|
||||||
|
"MessageDragFilesIntoTrackOrder": "Sleep bestanden in de juiste trackvolgorde",
|
||||||
|
"MessageEmbedFinished": "Insluiting voltooid!",
|
||||||
|
"MessageEpisodesQueuedForDownload": "{0} aflevering(en) in de rij om te downloaden",
|
||||||
|
"MessageFeedURLWillBe": "Feed URL zal {0} zijn",
|
||||||
|
"MessageFetching": "Aan het ophalen...",
|
||||||
|
"MessageForceReScanDescription": "zal alle bestanden opnieuw scannen als een verse scan. Audiobestanden ID3-tags, OPF-bestanden en textbestanden zullen als nieuw worden gescand.",
|
||||||
|
"MessageImportantNotice": "Belangrijke opmerking!",
|
||||||
|
"MessageInsertChapterBelow": "Hoofdstuk hieronder invoegen",
|
||||||
|
"MessageItemsSelected": "{0} onderdelen geselecteerd",
|
||||||
|
"MessageItemsUpdated": "{0} onderdelen bijgewerkt",
|
||||||
|
"MessageJoinUsOn": "Doe mee op",
|
||||||
|
"MessageListeningSessionsInTheLastYear": "{0} luistersessies in het laatste jaar",
|
||||||
|
"MessageLoading": "Aan het laden...",
|
||||||
|
"MessageLoadingFolders": "Mappen aan het laden...",
|
||||||
|
"MessageM4BFailed": "M4B mislukt!",
|
||||||
|
"MessageM4BFinished": "M4B voltooid!",
|
||||||
|
"MessageMapChapterTitles": "Map hoofdstuktitels naar je bestaande audioboekhoofdstukken zonder aanpassing van tijden",
|
||||||
|
"MessageMarkAsFinished": "Markeer als Voltooid",
|
||||||
|
"MessageMarkAsNotFinished": "Markeer als Niet Voltooid",
|
||||||
|
"MessageMatchBooksDescription": "zal proberen boeken in de bibliotheek te matchen met een boek uit de geselecteerde bron en lege details en coverafbeelding te vullen. Overschrijft details niet.",
|
||||||
|
"MessageNoAudioTracks": "Geen audiotracks",
|
||||||
|
"MessageNoAuthors": "Geen auteurs",
|
||||||
|
"MessageNoBackups": "Geen back-ups",
|
||||||
|
"MessageNoBookmarks": "Geen boekwijzers",
|
||||||
|
"MessageNoChapters": "Geen hoofdstukken",
|
||||||
|
"MessageNoCollections": "Geen collecties",
|
||||||
|
"MessageNoCoversFound": "Geen covers gevonden",
|
||||||
|
"MessageNoDescription": "Geen beschrijving",
|
||||||
|
"MessageNoDownloadsInProgress": "Geen downloads bezig op dit moment",
|
||||||
|
"MessageNoDownloadsQueued": "Geen downloads in de wachtrij",
|
||||||
|
"MessageNoEpisodeMatchesFound": "Geen afleveringsmatches gevonden",
|
||||||
|
"MessageNoEpisodes": "Geen afleveringen",
|
||||||
|
"MessageNoFoldersAvailable": "Geen mappen beschikbaar",
|
||||||
|
"MessageNoGenres": "Geen genres",
|
||||||
|
"MessageNoIssues": "Geen issues",
|
||||||
|
"MessageNoItems": "Geen onderdelen",
|
||||||
|
"MessageNoItemsFound": "Geen onderdelen gevonden",
|
||||||
|
"MessageNoListeningSessions": "Geen luistersessies",
|
||||||
|
"MessageNoLogs": "Geen logs",
|
||||||
|
"MessageNoMediaProgress": "Geen mediavoortgang",
|
||||||
|
"MessageNoNotifications": "Geen notificaties",
|
||||||
|
"MessageNoPodcastsFound": "Geen podcasts gevonden",
|
||||||
|
"MessageNoResults": "Geen resultaten",
|
||||||
|
"MessageNoSearchResultsFor": "Geen zoekresultaten voor \"{0}\"",
|
||||||
|
"MessageNoSeries": "Geen series",
|
||||||
|
"MessageNoTags": "Geen tags",
|
||||||
|
"MessageNoTasksRunning": "Geen lopende taken",
|
||||||
|
"MessageNotYetImplemented": "Nog niet geimplementeerd",
|
||||||
|
"MessageNoUpdateNecessary": "Geen bijwerking noodzakelijk",
|
||||||
|
"MessageNoUpdatesWereNecessary": "Geen bijwerkingen waren noodzakelijk",
|
||||||
|
"MessageNoUserPlaylists": "Je hebt geen afspeellijsten",
|
||||||
|
"MessageOr": "of",
|
||||||
|
"MessagePauseChapter": "Pauzeer afspelen hoofdstuk",
|
||||||
|
"MessagePlayChapter": "Luister naar begin van hoofdstuk",
|
||||||
|
"MessagePlaylistCreateFromCollection": "Afspeellijst aanmaken vanuit collectie",
|
||||||
|
"MessagePodcastHasNoRSSFeedForMatching": "Podcast heeft geen RSS-feed URL om te gebruiken voor matching",
|
||||||
|
"MessageQuickMatchDescription": "Vul lege onderdeeldetails & cover met eerste matchresultaat van '{0}'. Overschrijft geen details tenzij 'Prefereer gematchte metadata' serverinstelling is ingeschakeld.",
|
||||||
|
"MessageRemoveAllItemsWarning": "WAARSCHUWING! Deze actie zal alle onderdelen in de bibliotheek verwijderen uit de database, inclusief enige bijwerkingen of matches die je hebt gemaakt. Dit doet niets met je onderliggende bestanden. Weet je het zeker?",
|
||||||
|
"MessageRemoveChapter": "Verwijder hoofdstuk",
|
||||||
|
"MessageRemoveEpisodes": "Verwijder {0} aflevering(en)",
|
||||||
|
"MessageRemoveFromPlayerQueue": "Verwijder uit afspeelwachtrij",
|
||||||
|
"MessageRemoveUserWarning": "Weet je zeker dat je gebruiker \"{0}\" permanent wil verwijderen?",
|
||||||
|
"MessageReportBugsAndContribute": "Rapporteer bugs, vraag functionaliteiten aan en draag bij op",
|
||||||
|
"MessageResetChaptersConfirm": "Weet je zeker dat je de hoofdstukken wil resetten en de wijzigingen die je gemaakt hebt ongedaan wil maken?",
|
||||||
|
"MessageRestoreBackupConfirm": "Weet je zeker dat je wil herstellen met behulp van de back-up gemaakt op",
|
||||||
|
"MessageRestoreBackupWarning": "Herstellen met een back-up zal de volledige database in /config en de covers in /metadata/items & /metadata/authors overschrijven.<br /><br />Back-ups wijzigen geen bestanden in je bibliotheekmappen. Als je de serverinstelling gebruikt om covers en metadata in je bibliotheekmappen te bewaren dan worden deze niet geback-upt of overschreven.<br /><br />Alle clients die van je server gebruik maken zullen automatisch worden ververst.",
|
||||||
|
"MessageSearchResultsFor": "Zoekresultaten voor",
|
||||||
|
"MessageServerCouldNotBeReached": "Server niet bereikbaar",
|
||||||
|
"MessageSetChaptersFromTracksDescription": "Stel hoofdstukken in met ieder audiobestand als een hoofdstuk en de audiobestandsnaam als hoofdstuktitel",
|
||||||
|
"MessageStartPlaybackAtTime": "Afspelen van \"{0}\" beginnen op {1}?",
|
||||||
|
"MessageThinking": "Aan het denken...",
|
||||||
|
"MessageUploaderItemFailed": "Uploaden mislukt",
|
||||||
|
"MessageUploaderItemSuccess": "Uploaden gelukt!",
|
||||||
|
"MessageUploading": "Aan het uploaden...",
|
||||||
|
"MessageValidCronExpression": "Geldige cron-uitdrukking",
|
||||||
|
"MessageWatcherIsDisabledGlobally": "Watcher is globaal uitgeschakeld in serverinstellingen",
|
||||||
|
"MessageXLibraryIsEmpty": "{0} bibliotheek is leeg!",
|
||||||
|
"MessageYourAudiobookDurationIsLonger": "Duur van je audioboek is langer dan de gevonden duur",
|
||||||
|
"MessageYourAudiobookDurationIsShorter": "Duur van je audioboek is korter dan de gevonden duur",
|
||||||
|
"NoteChangeRootPassword": "Root-gebruiker is de enige gebruiker die een leeg wachtwoord kan hebben",
|
||||||
|
"NoteChapterEditorTimes": "Opmerking: Starttijd van het eerste hoofdstuk moet op 0:00 blijven en de starttijd van het laatste hoofdstuk mag niet de duur van het audioboek overschrijden.",
|
||||||
|
"NoteFolderPicker": "Opmerking: Reeds gemapte mappen worden niet getoond",
|
||||||
|
"NoteFolderPickerDebian": "Opmerking: Mappenkiezer voor de debian installatie is niet volledig geimplementeerd. Je moet het pad naar je map zelf invoeren.",
|
||||||
|
"NoteRSSFeedPodcastAppsHttps": "Waarschuwing: De meeste podcast-apps zullen eisen dat de RSS-feed URL HTTPS gebruikt",
|
||||||
|
"NoteRSSFeedPodcastAppsPubDate": "Waarschuwing: 1 of meer van je afleveringen hebben geen Pub Datum. Sommige podcast-apps vereisen dit.",
|
||||||
|
"NoteUploaderFoldersWithMediaFiles": "Mappen met mediabestanden zullen worden behandeld als aparte bibliotheekonderdelen.",
|
||||||
|
"NoteUploaderOnlyAudioFiles": "Bij uploaden van uitsluitend audiobestanden wordt ieder audiobestand als apart audiobook worden behandeld.",
|
||||||
|
"NoteUploaderUnsupportedFiles": "Niet-ondersteunde bestanden worden genegeerd. Bij het kiezen of neerzetten van een map worden andere bestanden die niet in de map staan genegeerd.",
|
||||||
|
"PlaceholderNewCollection": "Nieuwe naam collectie",
|
||||||
|
"PlaceholderNewFolderPath": "Nieuwe locatie map",
|
||||||
|
"PlaceholderNewPlaylist": "Nieuwe naam afspeellijst",
|
||||||
|
"PlaceholderSearch": "Zoeken..",
|
||||||
|
"PlaceholderSearchEpisode": "Aflevering zoeken..",
|
||||||
|
"ToastAccountUpdateFailed": "Bijwerken account mislukt",
|
||||||
|
"ToastAccountUpdateSuccess": "Account bijgewerkt",
|
||||||
|
"ToastAuthorImageRemoveFailed": "Afbeelding verwijderen mislukt",
|
||||||
|
"ToastAuthorImageRemoveSuccess": "Afbeelding auteur verwijderd",
|
||||||
|
"ToastAuthorUpdateFailed": "Bijwerken auteur mislukt",
|
||||||
|
"ToastAuthorUpdateMerged": "Auteur samengevoegd",
|
||||||
|
"ToastAuthorUpdateSuccess": "Auteur bijgewerkt",
|
||||||
|
"ToastAuthorUpdateSuccessNoImageFound": "Auteur bijgewerkt (geen afbeelding gevonden)",
|
||||||
|
"ToastBackupCreateFailed": "Back-up maken mislukt",
|
||||||
|
"ToastBackupCreateSuccess": "Back-up gemaakt",
|
||||||
|
"ToastBackupDeleteFailed": "Verwijderen back-up mislukt",
|
||||||
|
"ToastBackupDeleteSuccess": "Back-up verwijderd",
|
||||||
|
"ToastBackupRestoreFailed": "Herstellen back-up mislukt",
|
||||||
|
"ToastBackupUploadFailed": "Uploaden back-up mislukt",
|
||||||
|
"ToastBackupUploadSuccess": "Back-up geüpload",
|
||||||
|
"ToastBatchUpdateFailed": "Bulk-bijwerking mislukt",
|
||||||
|
"ToastBatchUpdateSuccess": "Bulk-bijwerking gelukt",
|
||||||
|
"ToastBookmarkCreateFailed": "Aanmaken boekwijzer mislukt",
|
||||||
|
"ToastBookmarkCreateSuccess": "boekwijzer toegevoegd",
|
||||||
|
"ToastBookmarkRemoveFailed": "Verwijderen boekwijzer mislukt",
|
||||||
|
"ToastBookmarkRemoveSuccess": "Boekwijzer verwijderd",
|
||||||
|
"ToastBookmarkUpdateFailed": "Bijwerken boekwijzer mislukt",
|
||||||
|
"ToastBookmarkUpdateSuccess": "Boekwijzer bijgewerkt",
|
||||||
|
"ToastChaptersHaveErrors": "Hoofdstukken bevatten fouten",
|
||||||
|
"ToastChaptersMustHaveTitles": "Hoofdstukken moeten titels hebben",
|
||||||
|
"ToastCollectionItemsRemoveFailed": "Verwijderen onderdeel (of onderdelen) uit collectie mislukt",
|
||||||
|
"ToastCollectionItemsRemoveSuccess": "Onderdeel (of onderdelen) verwijderd uit collectie",
|
||||||
|
"ToastCollectionRemoveFailed": "Verwijderen collectie mislukt",
|
||||||
|
"ToastCollectionRemoveSuccess": "Collectie verwijderd",
|
||||||
|
"ToastCollectionUpdateFailed": "Bijwerken collectie mislukt",
|
||||||
|
"ToastCollectionUpdateSuccess": "Collectie bijgewerkt",
|
||||||
|
"ToastItemCoverUpdateFailed": "Bijwerken cover onderdeel mislukt",
|
||||||
|
"ToastItemCoverUpdateSuccess": "Cover onderdeel bijgewerkt",
|
||||||
|
"ToastItemDetailsUpdateFailed": "Bijwerken details onderdeel mislukt",
|
||||||
|
"ToastItemDetailsUpdateSuccess": "Details onderdeel bijgewerkt",
|
||||||
|
"ToastItemDetailsUpdateUnneeded": "Geen bijwerking nodig voor details onderdeel",
|
||||||
|
"ToastItemMarkedAsFinishedFailed": "Markeren als Voltooid mislukt",
|
||||||
|
"ToastItemMarkedAsFinishedSuccess": "Onderdeel gemarkeerd als Voltooid",
|
||||||
|
"ToastItemMarkedAsNotFinishedFailed": "Markeren als Niet Voltooid mislukt",
|
||||||
|
"ToastItemMarkedAsNotFinishedSuccess": "Onderdeel gemarkeerd als Niet Voltooid",
|
||||||
|
"ToastLibraryCreateFailed": "Bibliotheek aanmaken mislukt",
|
||||||
|
"ToastLibraryCreateSuccess": "Bibliotheek \"{0}\" aangemaakt",
|
||||||
|
"ToastLibraryDeleteFailed": "Bibliotheek verwijderen mislukt",
|
||||||
|
"ToastLibraryDeleteSuccess": "Bibliotheek verwijderd",
|
||||||
|
"ToastLibraryScanFailedToStart": "Starten scan mislukt",
|
||||||
|
"ToastLibraryScanStarted": "Scannen bibliotheek gestart",
|
||||||
|
"ToastLibraryUpdateFailed": "Bijwerken bibliotheek mislukt",
|
||||||
|
"ToastLibraryUpdateSuccess": "Bibliotheek \"{0}\" bijgewerkt",
|
||||||
|
"ToastPlaylistCreateFailed": "Aanmaken afspeellijst mislukt",
|
||||||
|
"ToastPlaylistCreateSuccess": "Afspeellijst aangemaakt",
|
||||||
|
"ToastPlaylistRemoveFailed": "Verwijderen afspeellijst mislukt",
|
||||||
|
"ToastPlaylistRemoveSuccess": "Afspeellijst verwijderd",
|
||||||
|
"ToastPlaylistUpdateFailed": "Afspeellijst bijwerken mislukt",
|
||||||
|
"ToastPlaylistUpdateSuccess": "Afspeellijst bijgewerkt",
|
||||||
|
"ToastPodcastCreateFailed": "Podcast aanmaken mislukt",
|
||||||
|
"ToastPodcastCreateSuccess": "Podcast aangemaakt",
|
||||||
|
"ToastRemoveItemFromCollectionFailed": "Onderdeel verwijderen uit collectie mislukt",
|
||||||
|
"ToastRemoveItemFromCollectionSuccess": "Onderdeel verwijderd uit collectie",
|
||||||
|
"ToastRSSFeedCloseFailed": "Sluiten RSS-feed mislukt",
|
||||||
|
"ToastRSSFeedCloseSuccess": "RSS-feed gesloten",
|
||||||
|
"ToastSeriesUpdateFailed": "Bijwerken serie mislukt",
|
||||||
|
"ToastSeriesUpdateSuccess": "Bijwerken serie gelukt",
|
||||||
|
"ToastSessionDeleteFailed": "Verwijderen sessie mislukt",
|
||||||
|
"ToastSessionDeleteSuccess": "Sessie verwijderd",
|
||||||
|
"ToastSocketConnected": "Socket verbonden",
|
||||||
|
"ToastSocketDisconnected": "Socket niet verbonden",
|
||||||
|
"ToastSocketFailedToConnect": "Verbinding Socket mislukt",
|
||||||
|
"ToastUserDeleteFailed": "Verwijderen gebruiker mislukt",
|
||||||
|
"ToastUserDeleteSuccess": "Gebruiker verwijderd"
|
||||||
|
}
|
||||||
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "Usuń wszystko",
|
"ButtonRemoveAll": "Usuń wszystko",
|
||||||
"ButtonRemoveAllLibraryItems": "Usuń wszystkie elementy z biblioteki",
|
"ButtonRemoveAllLibraryItems": "Usuń wszystkie elementy z biblioteki",
|
||||||
"ButtonRemoveFromContinueListening": "Usuń z listy odtwarzania",
|
"ButtonRemoveFromContinueListening": "Usuń z listy odtwarzania",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Usuń serię z listy odtwarzania",
|
"ButtonRemoveSeriesFromContinueSeries": "Usuń serię z listy odtwarzania",
|
||||||
"ButtonReScan": "Ponowne skanowanie",
|
"ButtonReScan": "Ponowne skanowanie",
|
||||||
"ButtonReset": "Resetowanie",
|
"ButtonReset": "Resetowanie",
|
||||||
@@ -197,6 +198,7 @@
|
|||||||
"LabelComplete": "Ukończone",
|
"LabelComplete": "Ukończone",
|
||||||
"LabelConfirmPassword": "Potwierdź hasło",
|
"LabelConfirmPassword": "Potwierdź hasło",
|
||||||
"LabelContinueListening": "Kontynuuj odtwarzanie",
|
"LabelContinueListening": "Kontynuuj odtwarzanie",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Kontynuuj serię",
|
"LabelContinueSeries": "Kontynuuj serię",
|
||||||
"LabelCover": "Okładka",
|
"LabelCover": "Okładka",
|
||||||
"LabelCoverImageURL": "URL okładki",
|
"LabelCoverImageURL": "URL okładki",
|
||||||
@@ -254,6 +256,7 @@
|
|||||||
"LabelIntervalEveryDay": "Każdego dnia",
|
"LabelIntervalEveryDay": "Każdego dnia",
|
||||||
"LabelIntervalEveryHour": "Każdej godziny",
|
"LabelIntervalEveryHour": "Każdej godziny",
|
||||||
"LabelInvalidParts": "Nieprawidłowe części",
|
"LabelInvalidParts": "Nieprawidłowe części",
|
||||||
|
"LabelInvert": "Invert",
|
||||||
"LabelItem": "Pozycja",
|
"LabelItem": "Pozycja",
|
||||||
"LabelLanguage": "Język",
|
"LabelLanguage": "Język",
|
||||||
"LabelLanguageDefaultServer": "Domyślny język serwera",
|
"LabelLanguageDefaultServer": "Domyślny język serwera",
|
||||||
@@ -330,6 +333,7 @@
|
|||||||
"LabelPubDate": "Data publikacji",
|
"LabelPubDate": "Data publikacji",
|
||||||
"LabelPublisher": "Wydawca",
|
"LabelPublisher": "Wydawca",
|
||||||
"LabelPublishYear": "Rok publikacji",
|
"LabelPublishYear": "Rok publikacji",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Niedawno dodany",
|
"LabelRecentlyAdded": "Niedawno dodany",
|
||||||
"LabelRecentSeries": "Ostatnie serie",
|
"LabelRecentSeries": "Ostatnie serie",
|
||||||
"LabelRecommended": "Recommended",
|
"LabelRecommended": "Recommended",
|
||||||
@@ -412,6 +416,7 @@
|
|||||||
"LabelTag": "Tag",
|
"LabelTag": "Tag",
|
||||||
"LabelTags": "Tagi",
|
"LabelTags": "Tagi",
|
||||||
"LabelTagsAccessibleToUser": "Tagi dostępne dla użytkownika",
|
"LabelTagsAccessibleToUser": "Tagi dostępne dla użytkownika",
|
||||||
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "Tasks Running",
|
"LabelTasks": "Tasks Running",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "Time Base",
|
||||||
"LabelTimeListened": "Czas odtwarzania",
|
"LabelTimeListened": "Czas odtwarzania",
|
||||||
@@ -481,6 +486,7 @@
|
|||||||
"MessageConfirmRemoveCollection": "Czy na pewno chcesz usunąć kolekcję \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Czy na pewno chcesz usunąć kolekcję \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "Czy na pewno chcesz usunąć odcinek \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Czy na pewno chcesz usunąć odcinek \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "Czy na pewno chcesz usunąć {0} odcinki?",
|
"MessageConfirmRemoveEpisodes": "Czy na pewno chcesz usunąć {0} odcinki?",
|
||||||
|
"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?",
|
||||||
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "Удалить всё",
|
"ButtonRemoveAll": "Удалить всё",
|
||||||
"ButtonRemoveAllLibraryItems": "Удалить все элементы библиотеки",
|
"ButtonRemoveAllLibraryItems": "Удалить все элементы библиотеки",
|
||||||
"ButtonRemoveFromContinueListening": "Удалить из Продолжить слушать",
|
"ButtonRemoveFromContinueListening": "Удалить из Продолжить слушать",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Удалить серию из Продолжить серию",
|
"ButtonRemoveSeriesFromContinueSeries": "Удалить серию из Продолжить серию",
|
||||||
"ButtonReScan": "Пересканировать",
|
"ButtonReScan": "Пересканировать",
|
||||||
"ButtonReset": "Сбросить",
|
"ButtonReset": "Сбросить",
|
||||||
@@ -197,6 +198,7 @@
|
|||||||
"LabelComplete": "Завершить",
|
"LabelComplete": "Завершить",
|
||||||
"LabelConfirmPassword": "Подтвердить пароль",
|
"LabelConfirmPassword": "Подтвердить пароль",
|
||||||
"LabelContinueListening": "Продолжить слушать",
|
"LabelContinueListening": "Продолжить слушать",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Продолжить серию",
|
"LabelContinueSeries": "Продолжить серию",
|
||||||
"LabelCover": "Обложка",
|
"LabelCover": "Обложка",
|
||||||
"LabelCoverImageURL": "URL изображения обложки",
|
"LabelCoverImageURL": "URL изображения обложки",
|
||||||
@@ -254,6 +256,7 @@
|
|||||||
"LabelIntervalEveryDay": "Каждый день",
|
"LabelIntervalEveryDay": "Каждый день",
|
||||||
"LabelIntervalEveryHour": "Каждый час",
|
"LabelIntervalEveryHour": "Каждый час",
|
||||||
"LabelInvalidParts": "Неверные части",
|
"LabelInvalidParts": "Неверные части",
|
||||||
|
"LabelInvert": "Invert",
|
||||||
"LabelItem": "Элемент",
|
"LabelItem": "Элемент",
|
||||||
"LabelLanguage": "Язык",
|
"LabelLanguage": "Язык",
|
||||||
"LabelLanguageDefaultServer": "Язык сервера по умолчанию",
|
"LabelLanguageDefaultServer": "Язык сервера по умолчанию",
|
||||||
@@ -330,6 +333,7 @@
|
|||||||
"LabelPubDate": "Дата публикации",
|
"LabelPubDate": "Дата публикации",
|
||||||
"LabelPublisher": "Издатель",
|
"LabelPublisher": "Издатель",
|
||||||
"LabelPublishYear": "Год публикации",
|
"LabelPublishYear": "Год публикации",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Недавно добавленные",
|
"LabelRecentlyAdded": "Недавно добавленные",
|
||||||
"LabelRecentSeries": "Последние серии",
|
"LabelRecentSeries": "Последние серии",
|
||||||
"LabelRecommended": "Рекомендованное",
|
"LabelRecommended": "Рекомендованное",
|
||||||
@@ -412,6 +416,7 @@
|
|||||||
"LabelTag": "Тег",
|
"LabelTag": "Тег",
|
||||||
"LabelTags": "Теги",
|
"LabelTags": "Теги",
|
||||||
"LabelTagsAccessibleToUser": "Теги доступные для пользователя",
|
"LabelTagsAccessibleToUser": "Теги доступные для пользователя",
|
||||||
|
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
||||||
"LabelTasks": "Запущенные задачи",
|
"LabelTasks": "Запущенные задачи",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "Time Base",
|
||||||
"LabelTimeListened": "Время прослушивания",
|
"LabelTimeListened": "Время прослушивания",
|
||||||
@@ -481,6 +486,7 @@
|
|||||||
"MessageConfirmRemoveCollection": "Вы уверены, что хотите удалить коллекцию \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Вы уверены, что хотите удалить коллекцию \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "Вы уверены, что хотите удалить эпизод \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Вы уверены, что хотите удалить эпизод \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "Вы уверены, что хотите удалить {0} эпизодов?",
|
"MessageConfirmRemoveEpisodes": "Вы уверены, что хотите удалить {0} эпизодов?",
|
||||||
|
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
||||||
"MessageConfirmRemovePlaylist": "Вы уверены, что хотите удалить плейлист \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "Вы уверены, что хотите удалить плейлист \"{0}\"?",
|
||||||
"MessageConfirmRenameGenre": "Вы уверены, что хотите переименовать жанр \"{0}\" в \"{1}\" для всех элементов?",
|
"MessageConfirmRenameGenre": "Вы уверены, что хотите переименовать жанр \"{0}\" в \"{1}\" для всех элементов?",
|
||||||
"MessageConfirmRenameGenreMergeNote": "Примечание: Этот жанр уже существует, поэтому они будут объединены.",
|
"MessageConfirmRenameGenreMergeNote": "Примечание: Этот жанр уже существует, поэтому они будут объединены.",
|
||||||
|
|||||||
+22
-16
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "移除所有",
|
"ButtonRemoveAll": "移除所有",
|
||||||
"ButtonRemoveAllLibraryItems": "移除所有媒体库项目",
|
"ButtonRemoveAllLibraryItems": "移除所有媒体库项目",
|
||||||
"ButtonRemoveFromContinueListening": "从继续收听中删除",
|
"ButtonRemoveFromContinueListening": "从继续收听中删除",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "从继续收听系列中删除",
|
"ButtonRemoveSeriesFromContinueSeries": "从继续收听系列中删除",
|
||||||
"ButtonReScan": "重新扫描",
|
"ButtonReScan": "重新扫描",
|
||||||
"ButtonReset": "重置",
|
"ButtonReset": "重置",
|
||||||
@@ -155,13 +156,13 @@
|
|||||||
"HeaderUpdateLibrary": "更新媒体库",
|
"HeaderUpdateLibrary": "更新媒体库",
|
||||||
"HeaderUsers": "用户",
|
"HeaderUsers": "用户",
|
||||||
"HeaderYourStats": "你的统计数据",
|
"HeaderYourStats": "你的统计数据",
|
||||||
"LabelAbridged": "Abridged",
|
"LabelAbridged": "概要",
|
||||||
"LabelAccountType": "帐户类型",
|
"LabelAccountType": "帐户类型",
|
||||||
"LabelAccountTypeAdmin": "管理员",
|
"LabelAccountTypeAdmin": "管理员",
|
||||||
"LabelAccountTypeGuest": "来宾",
|
"LabelAccountTypeGuest": "来宾",
|
||||||
"LabelAccountTypeUser": "用户",
|
"LabelAccountTypeUser": "用户",
|
||||||
"LabelActivity": "活动",
|
"LabelActivity": "活动",
|
||||||
"LabelAdded": "Added",
|
"LabelAdded": "添加",
|
||||||
"LabelAddedAt": "添加于",
|
"LabelAddedAt": "添加于",
|
||||||
"LabelAddToCollection": "添加到收藏",
|
"LabelAddToCollection": "添加到收藏",
|
||||||
"LabelAddToCollectionBatch": "批量添加 {0} 个媒体到收藏",
|
"LabelAddToCollectionBatch": "批量添加 {0} 个媒体到收藏",
|
||||||
@@ -183,20 +184,21 @@
|
|||||||
"LabelBackupsMaxBackupSizeHelp": "为了防止错误配置, 如果备份超过配置的大小, 备份将失败.",
|
"LabelBackupsMaxBackupSizeHelp": "为了防止错误配置, 如果备份超过配置的大小, 备份将失败.",
|
||||||
"LabelBackupsNumberToKeep": "要保留的备份个数",
|
"LabelBackupsNumberToKeep": "要保留的备份个数",
|
||||||
"LabelBackupsNumberToKeepHelp": "一次只能删除一个备份, 因此如果你已经有超过此数量的备份, 则应手动删除它们.",
|
"LabelBackupsNumberToKeepHelp": "一次只能删除一个备份, 因此如果你已经有超过此数量的备份, 则应手动删除它们.",
|
||||||
"LabelBitrate": "Bitrate",
|
"LabelBitrate": "比特率",
|
||||||
"LabelBooks": "图书",
|
"LabelBooks": "图书",
|
||||||
"LabelChangePassword": "修改密码",
|
"LabelChangePassword": "修改密码",
|
||||||
"LabelChannels": "Channels",
|
"LabelChannels": "声道",
|
||||||
"LabelChapters": "Chapters",
|
"LabelChapters": "章节",
|
||||||
"LabelChaptersFound": "找到的章节",
|
"LabelChaptersFound": "找到的章节",
|
||||||
"LabelChapterTitle": "章节标题",
|
"LabelChapterTitle": "章节标题",
|
||||||
"LabelClosePlayer": "关闭播放器",
|
"LabelClosePlayer": "关闭播放器",
|
||||||
"LabelCodec": "Codec",
|
"LabelCodec": "编解码",
|
||||||
"LabelCollapseSeries": "折叠系列",
|
"LabelCollapseSeries": "折叠系列",
|
||||||
"LabelCollections": "收藏",
|
"LabelCollections": "收藏",
|
||||||
"LabelComplete": "已完成",
|
"LabelComplete": "已完成",
|
||||||
"LabelConfirmPassword": "确认密码",
|
"LabelConfirmPassword": "确认密码",
|
||||||
"LabelContinueListening": "继续收听",
|
"LabelContinueListening": "继续收听",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "继续收听系列",
|
"LabelContinueSeries": "继续收听系列",
|
||||||
"LabelCover": "封面",
|
"LabelCover": "封面",
|
||||||
"LabelCoverImageURL": "封面图像 URL",
|
"LabelCoverImageURL": "封面图像 URL",
|
||||||
@@ -217,7 +219,7 @@
|
|||||||
"LabelDuration": "持续时间",
|
"LabelDuration": "持续时间",
|
||||||
"LabelDurationFound": "找到持续时间:",
|
"LabelDurationFound": "找到持续时间:",
|
||||||
"LabelEdit": "编辑",
|
"LabelEdit": "编辑",
|
||||||
"LabelEmbeddedCover": "Embedded Cover",
|
"LabelEmbeddedCover": "嵌入封面",
|
||||||
"LabelEnable": "启用",
|
"LabelEnable": "启用",
|
||||||
"LabelEnd": "结束",
|
"LabelEnd": "结束",
|
||||||
"LabelEpisode": "剧集",
|
"LabelEpisode": "剧集",
|
||||||
@@ -235,7 +237,7 @@
|
|||||||
"LabelFinished": "已听完",
|
"LabelFinished": "已听完",
|
||||||
"LabelFolder": "文件夹",
|
"LabelFolder": "文件夹",
|
||||||
"LabelFolders": "文件夹",
|
"LabelFolders": "文件夹",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "编码格式",
|
||||||
"LabelGenre": "流派",
|
"LabelGenre": "流派",
|
||||||
"LabelGenres": "流派",
|
"LabelGenres": "流派",
|
||||||
"LabelHardDeleteFile": "完全删除文件",
|
"LabelHardDeleteFile": "完全删除文件",
|
||||||
@@ -254,11 +256,12 @@
|
|||||||
"LabelIntervalEveryDay": "每天",
|
"LabelIntervalEveryDay": "每天",
|
||||||
"LabelIntervalEveryHour": "每小时",
|
"LabelIntervalEveryHour": "每小时",
|
||||||
"LabelInvalidParts": "无效部件",
|
"LabelInvalidParts": "无效部件",
|
||||||
|
"LabelInvert": "倒转",
|
||||||
"LabelItem": "项目",
|
"LabelItem": "项目",
|
||||||
"LabelLanguage": "语言",
|
"LabelLanguage": "语言",
|
||||||
"LabelLanguageDefaultServer": "默认服务器语言",
|
"LabelLanguageDefaultServer": "默认服务器语言",
|
||||||
"LabelLastBookAdded": "Last Book Added",
|
"LabelLastBookAdded": "最后添加的书",
|
||||||
"LabelLastBookUpdated": "Last Book Updated",
|
"LabelLastBookUpdated": "最后更新的书",
|
||||||
"LabelLastSeen": "上次查看时间",
|
"LabelLastSeen": "上次查看时间",
|
||||||
"LabelLastTime": "最近一次",
|
"LabelLastTime": "最近一次",
|
||||||
"LabelLastUpdate": "最近更新",
|
"LabelLastUpdate": "最近更新",
|
||||||
@@ -277,12 +280,12 @@
|
|||||||
"LabelMediaType": "媒体类型",
|
"LabelMediaType": "媒体类型",
|
||||||
"LabelMetadataProvider": "元数据提供者",
|
"LabelMetadataProvider": "元数据提供者",
|
||||||
"LabelMetaTag": "元数据标签",
|
"LabelMetaTag": "元数据标签",
|
||||||
"LabelMetaTags": "Meta Tags",
|
"LabelMetaTags": "元标签",
|
||||||
"LabelMinute": "分钟",
|
"LabelMinute": "分钟",
|
||||||
"LabelMissing": "丢失",
|
"LabelMissing": "丢失",
|
||||||
"LabelMissingParts": "丢失的部分",
|
"LabelMissingParts": "丢失的部分",
|
||||||
"LabelMore": "更多",
|
"LabelMore": "更多",
|
||||||
"LabelMoreInfo": "More Info",
|
"LabelMoreInfo": "更多..",
|
||||||
"LabelName": "名称",
|
"LabelName": "名称",
|
||||||
"LabelNarrator": "演播者",
|
"LabelNarrator": "演播者",
|
||||||
"LabelNarrators": "演播者",
|
"LabelNarrators": "演播者",
|
||||||
@@ -330,6 +333,7 @@
|
|||||||
"LabelPubDate": "出版日期",
|
"LabelPubDate": "出版日期",
|
||||||
"LabelPublisher": "出版商",
|
"LabelPublisher": "出版商",
|
||||||
"LabelPublishYear": "发布年份",
|
"LabelPublishYear": "发布年份",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "最近添加",
|
"LabelRecentlyAdded": "最近添加",
|
||||||
"LabelRecentSeries": "最近添加系列",
|
"LabelRecentSeries": "最近添加系列",
|
||||||
"LabelRecommended": "推荐内容",
|
"LabelRecommended": "推荐内容",
|
||||||
@@ -412,8 +416,9 @@
|
|||||||
"LabelTag": "标签",
|
"LabelTag": "标签",
|
||||||
"LabelTags": "标签",
|
"LabelTags": "标签",
|
||||||
"LabelTagsAccessibleToUser": "用户可访问的标签",
|
"LabelTagsAccessibleToUser": "用户可访问的标签",
|
||||||
|
"LabelTagsNotAccessibleToUser": "用户无法访问标签",
|
||||||
"LabelTasks": "正在运行的任务",
|
"LabelTasks": "正在运行的任务",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "时间基准",
|
||||||
"LabelTimeListened": "收听时间",
|
"LabelTimeListened": "收听时间",
|
||||||
"LabelTimeListenedToday": "今日收听的时间",
|
"LabelTimeListenedToday": "今日收听的时间",
|
||||||
"LabelTimeRemaining": "剩余 {0}",
|
"LabelTimeRemaining": "剩余 {0}",
|
||||||
@@ -433,7 +438,7 @@
|
|||||||
"LabelTracksMultiTrack": "多轨",
|
"LabelTracksMultiTrack": "多轨",
|
||||||
"LabelTracksSingleTrack": "单轨",
|
"LabelTracksSingleTrack": "单轨",
|
||||||
"LabelType": "类型",
|
"LabelType": "类型",
|
||||||
"LabelUnabridged": "Unabridged",
|
"LabelUnabridged": "未删节",
|
||||||
"LabelUnknown": "未知",
|
"LabelUnknown": "未知",
|
||||||
"LabelUpdateCover": "更新封面",
|
"LabelUpdateCover": "更新封面",
|
||||||
"LabelUpdateCoverHelp": "找到匹配项时允许覆盖所选书籍存在的封面",
|
"LabelUpdateCoverHelp": "找到匹配项时允许覆盖所选书籍存在的封面",
|
||||||
@@ -477,10 +482,11 @@
|
|||||||
"MessageConfirmForceReScan": "你确定要强制重新扫描吗?",
|
"MessageConfirmForceReScan": "你确定要强制重新扫描吗?",
|
||||||
"MessageConfirmMarkSeriesFinished": "你确定要将此系列中的所有书籍都标记为已听完吗?",
|
"MessageConfirmMarkSeriesFinished": "你确定要将此系列中的所有书籍都标记为已听完吗?",
|
||||||
"MessageConfirmMarkSeriesNotFinished": "你确定要将此系列中的所有书籍都标记为未听完吗?",
|
"MessageConfirmMarkSeriesNotFinished": "你确定要将此系列中的所有书籍都标记为未听完吗?",
|
||||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
"MessageConfirmRemoveAllChapters": "你确定要移除所有章节吗?",
|
||||||
"MessageConfirmRemoveCollection": "您确定要移除收藏 \"{0}\"?",
|
"MessageConfirmRemoveCollection": "您确定要移除收藏 \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "您确定要移除剧集 \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "您确定要移除剧集 \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "你确定要移除 {0} 剧集?",
|
"MessageConfirmRemoveEpisodes": "你确定要移除 {0} 剧集?",
|
||||||
|
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
|
||||||
"MessageConfirmRemovePlaylist": "你确定要移除播放列表 \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "你确定要移除播放列表 \"{0}\"?",
|
||||||
"MessageConfirmRenameGenre": "你确定要将所有项目流派 \"{0}\" 重命名到 \"{1}\"?",
|
"MessageConfirmRenameGenre": "你确定要将所有项目流派 \"{0}\" 重命名到 \"{1}\"?",
|
||||||
"MessageConfirmRenameGenreMergeNote": "注意: 该流派已经存在, 因此它们将被合并.",
|
"MessageConfirmRenameGenreMergeNote": "注意: 该流派已经存在, 因此它们将被合并.",
|
||||||
@@ -581,7 +587,7 @@
|
|||||||
"PlaceholderNewFolderPath": "输入文件夹路径",
|
"PlaceholderNewFolderPath": "输入文件夹路径",
|
||||||
"PlaceholderNewPlaylist": "输入播放列表名称",
|
"PlaceholderNewPlaylist": "输入播放列表名称",
|
||||||
"PlaceholderSearch": "查找..",
|
"PlaceholderSearch": "查找..",
|
||||||
"PlaceholderSearchEpisode": "Search episode..",
|
"PlaceholderSearchEpisode": "搜索剧集..",
|
||||||
"ToastAccountUpdateFailed": "账户更新失败",
|
"ToastAccountUpdateFailed": "账户更新失败",
|
||||||
"ToastAccountUpdateSuccess": "帐户已更新",
|
"ToastAccountUpdateSuccess": "帐户已更新",
|
||||||
"ToastAuthorImageRemoveFailed": "作者图像删除失败",
|
"ToastAuthorImageRemoveFailed": "作者图像删除失败",
|
||||||
|
|||||||
+1
-13
@@ -48,21 +48,9 @@
|
|||||||
<Mode>rw</Mode>
|
<Mode>rw</Mode>
|
||||||
</Volume>
|
</Volume>
|
||||||
</Data>
|
</Data>
|
||||||
<Environment>
|
|
||||||
<Variable>
|
|
||||||
<Value>99</Value>
|
|
||||||
<Name>AUDIOBOOKSHELF_UID</Name>
|
|
||||||
<Mode/>
|
|
||||||
</Variable>
|
|
||||||
<Variable>
|
|
||||||
<Value>100</Value>
|
|
||||||
<Name>AUDIOBOOKSHELF_GID</Name>
|
|
||||||
<Mode/>
|
|
||||||
</Variable>
|
|
||||||
</Environment>
|
|
||||||
<Labels/>
|
<Labels/>
|
||||||
<Config Name="Audiobooks" Target="/audiobooks" Default="" Mode="rw" Description="Container Path: /audiobooks" Type="Path" Display="always" Required="true" Mask="false" />
|
<Config Name="Audiobooks" Target="/audiobooks" Default="" Mode="rw" Description="Container Path: /audiobooks" Type="Path" Display="always" Required="true" Mask="false" />
|
||||||
<Config Name="Config" Target="/config" Default="/mnt/user/appdata/audiobookshelf/config/" Mode="rw" Description="Container Path: /config" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/audiobookshelf/config/</Config>
|
<Config Name="Config" Target="/config" Default="/mnt/user/appdata/audiobookshelf/config/" Mode="rw" Description="Container Path: /config" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/audiobookshelf/config/</Config>
|
||||||
<Config Name="Metadata" Target="/metadata" Default="/mnt/user/appdata/audiobookshelf/metadata/" Mode="rw" Description="Container Path: /metadata" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/audiobookshelf/metadata/</Config>
|
<Config Name="Metadata" Target="/metadata" Default="/mnt/user/appdata/audiobookshelf/metadata/" Mode="rw" Description="Container Path: /metadata" Type="Path" Display="always" Required="true" Mask="false">/mnt/user/appdata/audiobookshelf/metadata/</Config>
|
||||||
<Config Name="Web UI Port" Target="80" Default="13378" Mode="tcp" Description="Container Port: 80" Type="Port" Display="always" Required="false" Mask="false">13378</Config>
|
<Config Name="Web UI Port" Target="80" Default="13378" Mode="tcp" Description="Container Port: 80" Type="Port" Display="always" Required="false" Mask="false">13378</Config>
|
||||||
</Container>
|
</Container>
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.19",
|
"version": "2.2.21",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.19",
|
"version": "2.2.21",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.19",
|
"version": "2.2.21",
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ For this to work you must enable at least the following mods using `a2enmod`:
|
|||||||
|
|
||||||
### SWAG Reverse Proxy
|
### SWAG Reverse Proxy
|
||||||
|
|
||||||
[See this solution](https://forums.unraid.net/topic/112698-support-audiobookshelf/?do=findComment&comment=1049637)
|
[See LinuxServer.io config sample](https://github.com/linuxserver/reverse-proxy-confs/blob/master/audiobookshelf.subdomain.conf.sample)
|
||||||
|
|
||||||
### Synology Reverse Proxy
|
### Synology Reverse Proxy
|
||||||
|
|
||||||
|
|||||||
+7
-5
@@ -126,12 +126,12 @@ class Auth {
|
|||||||
|
|
||||||
async login(req, res) {
|
async login(req, res) {
|
||||||
const ipAddress = requestIp.getClientIp(req)
|
const ipAddress = requestIp.getClientIp(req)
|
||||||
var username = (req.body.username || '').toLowerCase()
|
const username = (req.body.username || '').toLowerCase()
|
||||||
var password = req.body.password || ''
|
const password = req.body.password || ''
|
||||||
|
|
||||||
var user = this.users.find(u => u.username.toLowerCase() === username)
|
const user = this.users.find(u => u.username.toLowerCase() === username)
|
||||||
|
|
||||||
if (!user || !user.isActive) {
|
if (!user?.isActive) {
|
||||||
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
|
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
|
||||||
if (req.rateLimit.remaining <= 2) {
|
if (req.rateLimit.remaining <= 2) {
|
||||||
Logger.error(`[Auth] Failed login attempt for username ${username} from ip ${ipAddress}. Attempts: ${req.rateLimit.current}`)
|
Logger.error(`[Auth] Failed login attempt for username ${username} from ip ${ipAddress}. Attempts: ${req.rateLimit.current}`)
|
||||||
@@ -145,13 +145,15 @@ class Auth {
|
|||||||
if (password) {
|
if (password) {
|
||||||
return res.status(401).send('Invalid root password (hint: there is none)')
|
return res.status(401).send('Invalid root password (hint: there is none)')
|
||||||
} else {
|
} else {
|
||||||
|
Logger.info(`[Auth] ${user.username} logged in from ${ipAddress}`)
|
||||||
return res.json(this.getUserLoginResponsePayload(user))
|
return res.json(this.getUserLoginResponsePayload(user))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check password match
|
// Check password match
|
||||||
var compare = await bcrypt.compare(password, user.pash)
|
const compare = await bcrypt.compare(password, user.pash)
|
||||||
if (compare) {
|
if (compare) {
|
||||||
|
Logger.info(`[Auth] ${user.username} logged in from ${ipAddress}`)
|
||||||
res.json(this.getUserLoginResponsePayload(user))
|
res.json(this.getUserLoginResponsePayload(user))
|
||||||
} else {
|
} else {
|
||||||
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
|
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
|
||||||
|
|||||||
+1
-1
@@ -75,7 +75,7 @@ class Server {
|
|||||||
this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.taskManager)
|
this.audioMetadataManager = new AudioMetadataMangaer(this.db, this.taskManager)
|
||||||
this.rssFeedManager = new RssFeedManager(this.db)
|
this.rssFeedManager = new RssFeedManager(this.db)
|
||||||
|
|
||||||
this.scanner = new Scanner(this.db, this.coverManager)
|
this.scanner = new Scanner(this.db, this.coverManager, this.taskManager)
|
||||||
this.cronManager = new CronManager(this.db, this.scanner, this.podcastManager)
|
this.cronManager = new CronManager(this.db, this.scanner, this.podcastManager)
|
||||||
|
|
||||||
// Routers
|
// Routers
|
||||||
|
|||||||
@@ -1,27 +1,50 @@
|
|||||||
const Logger = require('../Logger')
|
|
||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
|
const Logger = require('../Logger')
|
||||||
|
const fs = require('../libs/fsExtra')
|
||||||
|
|
||||||
class FileSystemController {
|
class FileSystemController {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
async getPaths(req, res) {
|
async getPaths(req, res) {
|
||||||
var excludedDirs = ['node_modules', 'client', 'server', '.git', 'static', 'build', 'dist', 'metadata', 'config', 'sys', 'proc'].map(dirname => {
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.error(`[FileSystemController] Non-admin user attempting to get filesystem paths`, req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
const excludedDirs = ['node_modules', 'client', 'server', '.git', 'static', 'build', 'dist', 'metadata', 'config', 'sys', 'proc'].map(dirname => {
|
||||||
return Path.sep + dirname
|
return Path.sep + dirname
|
||||||
})
|
})
|
||||||
|
|
||||||
// Do not include existing mapped library paths in response
|
// Do not include existing mapped library paths in response
|
||||||
this.db.libraries.forEach(lib => {
|
this.db.libraries.forEach(lib => {
|
||||||
lib.folders.forEach((folder) => {
|
lib.folders.forEach((folder) => {
|
||||||
var dir = folder.fullPath
|
let dir = folder.fullPath
|
||||||
if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '')
|
if (dir.includes(global.appRoot)) dir = dir.replace(global.appRoot, '')
|
||||||
excludedDirs.push(dir)
|
excludedDirs.push(dir)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Logger.debug(`[Server] get file system paths, excluded: ${excludedDirs.join(', ')}`)
|
|
||||||
res.json({
|
res.json({
|
||||||
directories: await this.getDirectories(global.appRoot, '/', excludedDirs)
|
directories: await this.getDirectories(global.appRoot, '/', excludedDirs)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST: api/filesystem/pathexists
|
||||||
|
async checkPathExists(req, res) {
|
||||||
|
if (!req.user.canUpload) {
|
||||||
|
Logger.error(`[FileSystemController] Non-admin user attempting to check path exists`, req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
const filepath = req.body.filepath
|
||||||
|
if (!filepath?.length) {
|
||||||
|
return res.sendStatus(400)
|
||||||
|
}
|
||||||
|
|
||||||
|
const exists = await fs.pathExists(filepath)
|
||||||
|
res.json({
|
||||||
|
exists
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = new FileSystemController()
|
module.exports = new FileSystemController()
|
||||||
@@ -596,6 +596,7 @@ class LibraryController {
|
|||||||
|
|
||||||
const itemMatches = []
|
const itemMatches = []
|
||||||
const authorMatches = {}
|
const authorMatches = {}
|
||||||
|
const narratorMatches = {}
|
||||||
const seriesMatches = {}
|
const seriesMatches = {}
|
||||||
const tagMatches = {}
|
const tagMatches = {}
|
||||||
|
|
||||||
@@ -608,7 +609,7 @@ class LibraryController {
|
|||||||
matchText: queryResult.matchText
|
matchText: queryResult.matchText
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (queryResult.series && queryResult.series.length) {
|
if (queryResult.series?.length) {
|
||||||
queryResult.series.forEach((se) => {
|
queryResult.series.forEach((se) => {
|
||||||
if (!seriesMatches[se.id]) {
|
if (!seriesMatches[se.id]) {
|
||||||
const _series = this.db.series.find(_se => _se.id === se.id)
|
const _series = this.db.series.find(_se => _se.id === se.id)
|
||||||
@@ -618,7 +619,7 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (queryResult.authors && queryResult.authors.length) {
|
if (queryResult.authors?.length) {
|
||||||
queryResult.authors.forEach((au) => {
|
queryResult.authors.forEach((au) => {
|
||||||
if (!authorMatches[au.id]) {
|
if (!authorMatches[au.id]) {
|
||||||
const _author = this.db.authors.find(_au => _au.id === au.id)
|
const _author = this.db.authors.find(_au => _au.id === au.id)
|
||||||
@@ -631,7 +632,7 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (queryResult.tags && queryResult.tags.length) {
|
if (queryResult.tags?.length) {
|
||||||
queryResult.tags.forEach((tag) => {
|
queryResult.tags.forEach((tag) => {
|
||||||
if (!tagMatches[tag]) {
|
if (!tagMatches[tag]) {
|
||||||
tagMatches[tag] = { name: tag, books: [li.toJSON()] }
|
tagMatches[tag] = { name: tag, books: [li.toJSON()] }
|
||||||
@@ -640,13 +641,23 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (queryResult.narrators?.length) {
|
||||||
|
queryResult.narrators.forEach((narrator) => {
|
||||||
|
if (!narratorMatches[narrator]) {
|
||||||
|
narratorMatches[narrator] = { name: narrator, books: [li.toJSON()] }
|
||||||
|
} else {
|
||||||
|
narratorMatches[narrator].books.push(li.toJSON())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
const itemKey = req.library.mediaType
|
const itemKey = req.library.mediaType
|
||||||
const results = {
|
const results = {
|
||||||
[itemKey]: itemMatches.slice(0, maxResults),
|
[itemKey]: itemMatches.slice(0, maxResults),
|
||||||
tags: Object.values(tagMatches).slice(0, maxResults),
|
tags: Object.values(tagMatches).slice(0, maxResults),
|
||||||
authors: Object.values(authorMatches).slice(0, maxResults),
|
authors: Object.values(authorMatches).slice(0, maxResults),
|
||||||
series: Object.values(seriesMatches).slice(0, maxResults)
|
series: Object.values(seriesMatches).slice(0, maxResults),
|
||||||
|
narrators: Object.values(narratorMatches).slice(0, maxResults)
|
||||||
}
|
}
|
||||||
res.json(results)
|
res.json(results)
|
||||||
}
|
}
|
||||||
@@ -673,13 +684,12 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAuthors(req, res) {
|
async getAuthors(req, res) {
|
||||||
var libraryItems = req.libraryItems
|
const authors = {}
|
||||||
var authors = {}
|
req.libraryItems.forEach((li) => {
|
||||||
libraryItems.forEach((li) => {
|
|
||||||
if (li.media.metadata.authors && li.media.metadata.authors.length) {
|
if (li.media.metadata.authors && li.media.metadata.authors.length) {
|
||||||
li.media.metadata.authors.forEach((au) => {
|
li.media.metadata.authors.forEach((au) => {
|
||||||
if (!authors[au.id]) {
|
if (!authors[au.id]) {
|
||||||
var _author = this.db.authors.find(_au => _au.id === au.id)
|
const _author = this.db.authors.find(_au => _au.id === au.id)
|
||||||
if (_author) {
|
if (_author) {
|
||||||
authors[au.id] = _author.toJSON()
|
authors[au.id] = _author.toJSON()
|
||||||
authors[au.id].numBooks = 1
|
authors[au.id].numBooks = 1
|
||||||
@@ -696,6 +706,85 @@ class LibraryController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getNarrators(req, res) {
|
||||||
|
const narrators = {}
|
||||||
|
req.libraryItems.forEach((li) => {
|
||||||
|
if (li.media.metadata.narrators?.length) {
|
||||||
|
li.media.metadata.narrators.forEach((n) => {
|
||||||
|
if (typeof n !== 'string') {
|
||||||
|
Logger.error(`[LibraryController] getNarrators: Invalid narrator "${n}" on book "${li.media.metadata.title}"`)
|
||||||
|
} else if (!narrators[n]) {
|
||||||
|
narrators[n] = {
|
||||||
|
id: encodeURIComponent(Buffer.from(n).toString('base64')),
|
||||||
|
name: n,
|
||||||
|
numBooks: 1
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
narrators[n].numBooks++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
narrators: naturalSort(Object.values(narrators)).asc(n => n.name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateNarrator(req, res) {
|
||||||
|
if (!req.user.canUpdate) {
|
||||||
|
Logger.error(`[LibraryController] Unauthorized user "${req.user.username}" attempted to update narrator`)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
const narratorName = libraryHelpers.decode(req.params.narratorId)
|
||||||
|
const updatedName = req.body.name
|
||||||
|
if (!updatedName) {
|
||||||
|
return res.status(400).send('Invalid request payload. Name not specified.')
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemsUpdated = []
|
||||||
|
for (const libraryItem of req.libraryItems) {
|
||||||
|
if (libraryItem.media.metadata.updateNarrator(narratorName, updatedName)) {
|
||||||
|
itemsUpdated.push(libraryItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemsUpdated.length) {
|
||||||
|
await this.db.updateLibraryItems(itemsUpdated)
|
||||||
|
SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded()))
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
updated: itemsUpdated.length
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeNarrator(req, res) {
|
||||||
|
if (!req.user.canUpdate) {
|
||||||
|
Logger.error(`[LibraryController] Unauthorized user "${req.user.username}" attempted to remove narrator`)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
const narratorName = libraryHelpers.decode(req.params.narratorId)
|
||||||
|
|
||||||
|
const itemsUpdated = []
|
||||||
|
for (const libraryItem of req.libraryItems) {
|
||||||
|
if (libraryItem.media.metadata.removeNarrator(narratorName)) {
|
||||||
|
itemsUpdated.push(libraryItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemsUpdated.length) {
|
||||||
|
await this.db.updateLibraryItems(itemsUpdated)
|
||||||
|
SocketAuthority.emitter('items_updated', itemsUpdated.map(li => li.toJSONExpanded()))
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
updated: itemsUpdated.length
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async matchAll(req, res) {
|
async matchAll(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error(`[LibraryController] Non-root user attempted to match library items`, req.user)
|
Logger.error(`[LibraryController] Non-root user attempted to match library items`, req.user)
|
||||||
@@ -765,7 +854,7 @@ class LibraryController {
|
|||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
|
|
||||||
var library = this.db.libraries.find(lib => lib.id === req.params.id)
|
const library = this.db.libraries.find(lib => lib.id === req.params.id)
|
||||||
if (!library) {
|
if (!library) {
|
||||||
return res.status(404).send('Library not found')
|
return res.status(404).send('Library not found')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -379,20 +379,23 @@ class LibraryItemController {
|
|||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
var itemsUpdated = 0
|
let itemsUpdated = 0
|
||||||
var itemsUnmatched = 0
|
let itemsUnmatched = 0
|
||||||
|
|
||||||
var matchData = req.body
|
const options = req.body.options || {}
|
||||||
var options = matchData.options || {}
|
if (!req.body.libraryItemIds?.length) {
|
||||||
var items = matchData.libraryItemIds
|
return res.sendStatus(400)
|
||||||
if (!items || !items.length) {
|
|
||||||
return res.sendStatus(500)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const libraryItems = req.body.libraryItemIds.map(lid => this.db.getLibraryItem(lid)).filter(li => li)
|
||||||
|
if (!libraryItems?.length) {
|
||||||
|
return res.sendStatus(400)
|
||||||
|
}
|
||||||
|
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (const libraryItem of libraryItems) {
|
||||||
var libraryItem = this.db.libraryItems.find(_li => _li.id === items[i])
|
const matchResult = await this.scanner.quickMatchLibraryItem(libraryItem, options)
|
||||||
var matchResult = await this.scanner.quickMatchLibraryItem(libraryItem, options)
|
|
||||||
if (matchResult.updated) {
|
if (matchResult.updated) {
|
||||||
itemsUpdated++
|
itemsUpdated++
|
||||||
} else if (matchResult.warning) {
|
} else if (matchResult.warning) {
|
||||||
@@ -400,7 +403,7 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = {
|
const result = {
|
||||||
success: itemsUpdated > 0,
|
success: itemsUpdated > 0,
|
||||||
updates: itemsUpdated,
|
updates: itemsUpdated,
|
||||||
unmatched: itemsUnmatched
|
unmatched: itemsUnmatched
|
||||||
@@ -408,6 +411,33 @@ class LibraryItemController {
|
|||||||
SocketAuthority.clientEmitter(req.user.id, 'batch_quickmatch_complete', result)
|
SocketAuthority.clientEmitter(req.user.id, 'batch_quickmatch_complete', result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST: api/items/batch/scan
|
||||||
|
async batchScan(req, res) {
|
||||||
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.warn('User other than admin attempted to batch scan library items', req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.body.libraryItemIds?.length) {
|
||||||
|
return res.sendStatus(400)
|
||||||
|
}
|
||||||
|
|
||||||
|
const libraryItems = req.body.libraryItemIds.map(lid => this.db.getLibraryItem(lid)).filter(li => li)
|
||||||
|
if (!libraryItems?.length) {
|
||||||
|
return res.sendStatus(400)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sendStatus(200)
|
||||||
|
|
||||||
|
for (const libraryItem of libraryItems) {
|
||||||
|
if (libraryItem.isFile) {
|
||||||
|
Logger.warn(`[LibraryItemController] Re-scanning file library items not yet supported`)
|
||||||
|
} else {
|
||||||
|
await this.scanner.scanLibraryItemByRequest(libraryItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// DELETE: api/items/all
|
// DELETE: api/items/all
|
||||||
async deleteAll(req, res) {
|
async deleteAll(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
if (!req.user.isAdminOrUp) {
|
||||||
@@ -432,7 +462,7 @@ class LibraryItemController {
|
|||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = await this.scanner.scanLibraryItemById(req.libraryItem.id)
|
const result = await this.scanner.scanLibraryItemByRequest(req.libraryItem)
|
||||||
res.json({
|
res.json({
|
||||||
result: Object.keys(ScanResult).find(key => ScanResult[key] == result)
|
result: Object.keys(ScanResult).find(key => ScanResult[key] == result)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -48,8 +48,7 @@ class MeController {
|
|||||||
|
|
||||||
// DELETE: api/me/progress/:id
|
// DELETE: api/me/progress/:id
|
||||||
async removeMediaProgress(req, res) {
|
async removeMediaProgress(req, res) {
|
||||||
var wasRemoved = req.user.removeMediaProgress(req.params.id)
|
if (!req.user.removeMediaProgress(req.params.id)) {
|
||||||
if (!wasRemoved) {
|
|
||||||
return res.sendStatus(200)
|
return res.sendStatus(200)
|
||||||
}
|
}
|
||||||
await this.db.updateEntity('user', req.user)
|
await this.db.updateEntity('user', req.user)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const Audible = require('../providers/Audible')
|
|||||||
const iTunes = require('../providers/iTunes')
|
const iTunes = require('../providers/iTunes')
|
||||||
const Audnexus = require('../providers/Audnexus')
|
const Audnexus = require('../providers/Audnexus')
|
||||||
const FantLab = require('../providers/FantLab')
|
const FantLab = require('../providers/FantLab')
|
||||||
|
const AudiobookCovers = require('../providers/AudiobookCovers')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const { levenshteinDistance } = require('../utils/index')
|
const { levenshteinDistance } = require('../utils/index')
|
||||||
|
|
||||||
@@ -15,6 +16,9 @@ class BookFinder {
|
|||||||
this.iTunesApi = new iTunes()
|
this.iTunesApi = new iTunes()
|
||||||
this.audnexus = new Audnexus()
|
this.audnexus = new Audnexus()
|
||||||
this.fantLab = new FantLab()
|
this.fantLab = new FantLab()
|
||||||
|
this.audiobookCovers = new AudiobookCovers()
|
||||||
|
|
||||||
|
this.providers = ['google', 'itunes', 'openlibrary', 'fantlab', 'audiobookcovers', 'audible', 'audible.ca', 'audible.uk', 'audible.au', 'audible.fr', 'audible.de', 'audible.jp', 'audible.it', 'audible.in', 'audible.es']
|
||||||
|
|
||||||
this.verbose = false
|
this.verbose = false
|
||||||
}
|
}
|
||||||
@@ -159,6 +163,12 @@ class BookFinder {
|
|||||||
return books
|
return books
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAudiobookCoversResults(search) {
|
||||||
|
const covers = await this.audiobookCovers.search(search)
|
||||||
|
if (this.verbose) Logger.debug(`AudiobookCovers Search Results: ${covers.length || 0}`)
|
||||||
|
return covers || []
|
||||||
|
}
|
||||||
|
|
||||||
async getiTunesAudiobooksResults(title, author) {
|
async getiTunesAudiobooksResults(title, author) {
|
||||||
return this.iTunesApi.searchAudiobooks(title)
|
return this.iTunesApi.searchAudiobooks(title)
|
||||||
}
|
}
|
||||||
@@ -175,7 +185,7 @@ class BookFinder {
|
|||||||
var books = []
|
var books = []
|
||||||
var maxTitleDistance = !isNaN(options.titleDistance) ? Number(options.titleDistance) : 4
|
var maxTitleDistance = !isNaN(options.titleDistance) ? Number(options.titleDistance) : 4
|
||||||
var maxAuthorDistance = !isNaN(options.authorDistance) ? Number(options.authorDistance) : 4
|
var maxAuthorDistance = !isNaN(options.authorDistance) ? Number(options.authorDistance) : 4
|
||||||
Logger.debug(`Book Search: title: "${title}", author: "${author}", provider: ${provider}`)
|
Logger.debug(`Book Search: title: "${title}", author: "${author || ''}", provider: ${provider}`)
|
||||||
|
|
||||||
if (provider === 'google') {
|
if (provider === 'google') {
|
||||||
books = await this.getGoogleBooksResults(title, author)
|
books = await this.getGoogleBooksResults(title, author)
|
||||||
@@ -187,6 +197,8 @@ class BookFinder {
|
|||||||
books = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance)
|
books = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance)
|
||||||
} else if (provider === 'fantlab') {
|
} else if (provider === 'fantlab') {
|
||||||
books = await this.getFantLabResults(title, author)
|
books = await this.getFantLabResults(title, author)
|
||||||
|
} else if (provider === 'audiobookcovers') {
|
||||||
|
books = await this.getAudiobookCoversResults(title)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
books = await this.getGoogleBooksResults(title, author)
|
books = await this.getGoogleBooksResults(title, author)
|
||||||
@@ -202,27 +214,39 @@ class BookFinder {
|
|||||||
return this.search(provider, cleanedTitle, cleanedAuthor, isbn, asin, options)
|
return this.search(provider, cleanedTitle, cleanedAuthor, isbn, asin, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (["google", "audible", "itunes", 'fantlab'].includes(provider)) return books
|
if (provider === 'openlibrary') {
|
||||||
|
books.sort((a, b) => {
|
||||||
|
return a.totalDistance - b.totalDistance
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return books.sort((a, b) => {
|
return books
|
||||||
return a.totalDistance - b.totalDistance
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async findCovers(provider, title, author, options = {}) {
|
async findCovers(provider, title, author, options = {}) {
|
||||||
var searchResults = await this.search(provider, title, author, options)
|
let searchResults = []
|
||||||
|
|
||||||
|
if (provider === 'all') {
|
||||||
|
for (const providerString of this.providers) {
|
||||||
|
const providerResults = await this.search(providerString, title, author, options)
|
||||||
|
Logger.debug(`[BookFinder] Found ${providerResults.length} covers from ${providerString}`)
|
||||||
|
searchResults.push(...providerResults)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
searchResults = await this.search(provider, title, author, options)
|
||||||
|
}
|
||||||
Logger.debug(`[BookFinder] FindCovers search results: ${searchResults.length}`)
|
Logger.debug(`[BookFinder] FindCovers search results: ${searchResults.length}`)
|
||||||
|
|
||||||
var covers = []
|
const covers = []
|
||||||
searchResults.forEach((result) => {
|
searchResults.forEach((result) => {
|
||||||
if (result.covers && result.covers.length) {
|
if (result.covers && result.covers.length) {
|
||||||
covers = covers.concat(result.covers)
|
covers.push(...result.covers)
|
||||||
}
|
}
|
||||||
if (result.cover) {
|
if (result.cover) {
|
||||||
covers.push(result.cover)
|
covers.push(result.cover)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return covers
|
return [...(new Set(covers))]
|
||||||
}
|
}
|
||||||
|
|
||||||
findChapters(asin, region) {
|
findChapters(asin, region) {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class AbMergeManager {
|
|||||||
toneJsonObject: null
|
toneJsonObject: null
|
||||||
}
|
}
|
||||||
const taskDescription = `Encoding audiobook "${libraryItem.media.metadata.title}" into a single m4b file.`
|
const taskDescription = `Encoding audiobook "${libraryItem.media.metadata.title}" into a single m4b file.`
|
||||||
task.setData('encode-m4b', 'Encoding M4b', taskDescription, taskData)
|
task.setData('encode-m4b', 'Encoding M4b', taskDescription, false, taskData)
|
||||||
this.taskManager.addTask(task)
|
this.taskManager.addTask(task)
|
||||||
Logger.info(`Start m4b encode for ${libraryItem.id} - TaskId: ${task.id}`)
|
Logger.info(`Start m4b encode for ${libraryItem.id} - TaskId: ${task.id}`)
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ class AbMergeManager {
|
|||||||
let toneJsonPath = null
|
let toneJsonPath = null
|
||||||
try {
|
try {
|
||||||
toneJsonPath = Path.join(task.data.itemCachePath, 'metadata.json')
|
toneJsonPath = Path.join(task.data.itemCachePath, 'metadata.json')
|
||||||
await toneHelpers.writeToneMetadataJsonFile(libraryItem, libraryItem.media.chapters, toneJsonPath, 1)
|
await toneHelpers.writeToneMetadataJsonFile(libraryItem, libraryItem.media.chapters, toneJsonPath, 1, 'audio/mp4')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(`[AbMergeManager] Write metadata.json failed`, error)
|
Logger.error(`[AbMergeManager] Write metadata.json failed`, error)
|
||||||
toneJsonPath = null
|
toneJsonPath = null
|
||||||
|
|||||||
@@ -34,7 +34,10 @@ class AudioMetadataMangaer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getToneMetadataObjectForApi(libraryItem) {
|
getToneMetadataObjectForApi(libraryItem) {
|
||||||
return toneHelpers.getToneMetadataObject(libraryItem, libraryItem.media.chapters, libraryItem.media.tracks.length)
|
const audioFiles = libraryItem.media.includedAudioFiles
|
||||||
|
let mimeType = audioFiles[0].mimeType
|
||||||
|
if (audioFiles.some(a => a.mimeType !== mimeType)) mimeType = null
|
||||||
|
return toneHelpers.getToneMetadataObject(libraryItem, libraryItem.media.chapters, libraryItem.media.tracks.length, mimeType)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBatchEmbed(user, libraryItems, options = {}) {
|
handleBatchEmbed(user, libraryItems, options = {}) {
|
||||||
@@ -56,6 +59,9 @@ class AudioMetadataMangaer {
|
|||||||
// Only writing chapters for single file audiobooks
|
// Only writing chapters for single file audiobooks
|
||||||
const chapters = (audioFiles.length == 1 || forceEmbedChapters) ? libraryItem.media.chapters.map(c => ({ ...c })) : null
|
const chapters = (audioFiles.length == 1 || forceEmbedChapters) ? libraryItem.media.chapters.map(c => ({ ...c })) : null
|
||||||
|
|
||||||
|
let mimeType = audioFiles[0].mimeType
|
||||||
|
if (audioFiles.some(a => a.mimeType !== mimeType)) mimeType = null
|
||||||
|
|
||||||
// Create task
|
// Create task
|
||||||
const taskData = {
|
const taskData = {
|
||||||
libraryItemId: libraryItem.id,
|
libraryItemId: libraryItem.id,
|
||||||
@@ -71,7 +77,7 @@ class AudioMetadataMangaer {
|
|||||||
}
|
}
|
||||||
)),
|
)),
|
||||||
coverPath: libraryItem.media.coverPath,
|
coverPath: libraryItem.media.coverPath,
|
||||||
metadataObject: toneHelpers.getToneMetadataObject(libraryItem, chapters, audioFiles.length),
|
metadataObject: toneHelpers.getToneMetadataObject(libraryItem, chapters, audioFiles.length, mimeType),
|
||||||
itemCachePath,
|
itemCachePath,
|
||||||
chapters,
|
chapters,
|
||||||
options: {
|
options: {
|
||||||
@@ -80,7 +86,7 @@ class AudioMetadataMangaer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const taskDescription = `Embedding metadata in audiobook "${libraryItem.media.metadata.title}".`
|
const taskDescription = `Embedding metadata in audiobook "${libraryItem.media.metadata.title}".`
|
||||||
task.setData('embed-metadata', 'Embedding Metadata', taskDescription, taskData)
|
task.setData('embed-metadata', 'Embedding Metadata', taskDescription, false, taskData)
|
||||||
|
|
||||||
if (this.tasksRunning.length >= this.MAX_CONCURRENT_TASKS) {
|
if (this.tasksRunning.length >= this.MAX_CONCURRENT_TASKS) {
|
||||||
Logger.info(`[AudioMetadataManager] Queueing embed metadata for audiobook "${libraryItem.media.metadata.title}"`)
|
Logger.info(`[AudioMetadataManager] Queueing embed metadata for audiobook "${libraryItem.media.metadata.title}"`)
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ class CronManager {
|
|||||||
for (const libraryItem of libraryItems) {
|
for (const libraryItem of libraryItems) {
|
||||||
const keepAutoDownloading = await this.podcastManager.runEpisodeCheck(libraryItem)
|
const keepAutoDownloading = await this.podcastManager.runEpisodeCheck(libraryItem)
|
||||||
if (!keepAutoDownloading) { // auto download was disabled
|
if (!keepAutoDownloading) { // auto download was disabled
|
||||||
podcastCron.libraryItemIds = podcastCron.libraryItemIds.filter(lid => lid !== libraryItemId) // Filter it out
|
podcastCron.libraryItemIds = podcastCron.libraryItemIds.filter(lid => lid !== libraryItem.id) // Filter it out
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -130,6 +130,8 @@ class PlaybackSessionManager {
|
|||||||
const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
|
const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
|
||||||
SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
|
SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
|
||||||
id: itemProgress.id,
|
id: itemProgress.id,
|
||||||
|
sessionId: session.id,
|
||||||
|
deviceDescription: session.deviceDescription,
|
||||||
data: itemProgress.toJSON()
|
data: itemProgress.toJSON()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -239,6 +241,8 @@ class PlaybackSessionManager {
|
|||||||
const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
|
const itemProgress = user.getMediaProgress(session.libraryItemId, session.episodeId)
|
||||||
SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
|
SocketAuthority.clientEmitter(user.id, 'user_item_progress_updated', {
|
||||||
id: itemProgress.id,
|
id: itemProgress.id,
|
||||||
|
sessionId: session.id,
|
||||||
|
deviceDescription: session.deviceDescription,
|
||||||
data: itemProgress.toJSON()
|
data: itemProgress.toJSON()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -306,7 +310,7 @@ class PlaybackSessionManager {
|
|||||||
// See https://github.com/advplyr/audiobookshelf/issues/868
|
// See https://github.com/advplyr/audiobookshelf/issues/868
|
||||||
// Remove playback sessions with listening time too high
|
// Remove playback sessions with listening time too high
|
||||||
async removeInvalidSessions() {
|
async removeInvalidSessions() {
|
||||||
const selectFunc = (session) => isNaN(session.timeListening) || Number(session.timeListening) > 3600000000
|
const selectFunc = (session) => isNaN(session.timeListening) || Number(session.timeListening) > 36000000
|
||||||
const numSessionsRemoved = await this.db.removeEntities('session', selectFunc, true)
|
const numSessionsRemoved = await this.db.removeEntities('session', selectFunc, true)
|
||||||
if (numSessionsRemoved) {
|
if (numSessionsRemoved) {
|
||||||
Logger.info(`[PlaybackSessionManager] Removed ${numSessionsRemoved} invalid playback sessions`)
|
Logger.info(`[PlaybackSessionManager] Removed ${numSessionsRemoved} invalid playback sessions`)
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class PodcastManager {
|
|||||||
libraryId: podcastEpisodeDownload.libraryId,
|
libraryId: podcastEpisodeDownload.libraryId,
|
||||||
libraryItemId: podcastEpisodeDownload.libraryItemId,
|
libraryItemId: podcastEpisodeDownload.libraryItemId,
|
||||||
}
|
}
|
||||||
task.setData('download-podcast-episode', 'Downloading Episode', taskDescription, taskData)
|
task.setData('download-podcast-episode', 'Downloading Episode', taskDescription, false, taskData)
|
||||||
this.taskManager.addTask(task)
|
this.taskManager.addTask(task)
|
||||||
|
|
||||||
SocketAuthority.emitter('episode_download_started', podcastEpisodeDownload.toJSONForClient())
|
SocketAuthority.emitter('episode_download_started', podcastEpisodeDownload.toJSONForClient())
|
||||||
@@ -140,8 +140,6 @@ class PodcastManager {
|
|||||||
async scanAddPodcastEpisodeAudioFile() {
|
async scanAddPodcastEpisodeAudioFile() {
|
||||||
const libraryFile = await this.getLibraryFile(this.currentDownload.targetPath, this.currentDownload.targetRelPath)
|
const libraryFile = await this.getLibraryFile(this.currentDownload.targetPath, this.currentDownload.targetRelPath)
|
||||||
|
|
||||||
// TODO: Set meta tags on new audio file
|
|
||||||
|
|
||||||
const audioFile = await this.probeAudioFile(libraryFile)
|
const audioFile = await this.probeAudioFile(libraryFile)
|
||||||
if (!audioFile) {
|
if (!audioFile) {
|
||||||
return false
|
return false
|
||||||
@@ -178,6 +176,9 @@ class PodcastManager {
|
|||||||
libraryItem.updatedAt = Date.now()
|
libraryItem.updatedAt = Date.now()
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
await this.db.updateLibraryItem(libraryItem)
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
|
const podcastEpisodeExpanded = podcastEpisode.toJSONExpanded()
|
||||||
|
podcastEpisodeExpanded.libraryItem = libraryItem.toJSONExpanded()
|
||||||
|
SocketAuthority.emitter('episode_added', podcastEpisodeExpanded)
|
||||||
|
|
||||||
if (this.currentDownload.isAutoDownload) { // Notifications only for auto downloaded episodes
|
if (this.currentDownload.isAutoDownload) { // Notifications only for auto downloaded episodes
|
||||||
this.notificationManager.onPodcastEpisodeDownloaded(libraryItem, podcastEpisode)
|
this.notificationManager.onPodcastEpisodeDownloaded(libraryItem, podcastEpisode)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const Podcast = require('./mediaTypes/Podcast')
|
|||||||
const Video = require('./mediaTypes/Video')
|
const Video = require('./mediaTypes/Video')
|
||||||
const Music = require('./mediaTypes/Music')
|
const Music = require('./mediaTypes/Music')
|
||||||
const { areEquivalent, copyValue, getId, cleanStringForSearch } = require('../utils/index')
|
const { areEquivalent, copyValue, getId, cleanStringForSearch } = require('../utils/index')
|
||||||
|
const { filePathToPOSIX } = require('../utils/fileUtils')
|
||||||
|
|
||||||
class LibraryItem {
|
class LibraryItem {
|
||||||
constructor(libraryItem = null) {
|
constructor(libraryItem = null) {
|
||||||
@@ -368,7 +369,7 @@ class LibraryItem {
|
|||||||
const fileFoundCheck = this.checkFileFound(lf, true)
|
const fileFoundCheck = this.checkFileFound(lf, true)
|
||||||
if (fileFoundCheck === null) {
|
if (fileFoundCheck === null) {
|
||||||
newLibraryFiles.push(lf)
|
newLibraryFiles.push(lf)
|
||||||
} else if (fileFoundCheck && lf.metadata.format !== 'abs') { // Ignore abs file updates
|
} else if (fileFoundCheck && lf.metadata.format !== 'abs' && lf.metadata.filename !== 'metadata.json') { // Ignore abs file updates
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
existingLibraryFiles.push(lf)
|
existingLibraryFiles.push(lf)
|
||||||
} else {
|
} else {
|
||||||
@@ -499,14 +500,56 @@ class LibraryItem {
|
|||||||
// Make sure metadata book dir exists
|
// Make sure metadata book dir exists
|
||||||
await fs.ensureDir(metadataPath)
|
await fs.ensureDir(metadataPath)
|
||||||
}
|
}
|
||||||
metadataPath = Path.join(metadataPath, 'metadata.abs')
|
|
||||||
|
|
||||||
return abmetadataGenerator.generate(this, metadataPath).then((success) => {
|
const metadataFileFormat = global.ServerSettings.metadataFileFormat
|
||||||
this.isSavingMetadata = false
|
const metadataFilePath = Path.join(metadataPath, `metadata.${metadataFileFormat}`)
|
||||||
if (!success) Logger.error(`[LibraryItem] Failed saving abmetadata to "${metadataPath}"`)
|
if (metadataFileFormat === 'json') {
|
||||||
else Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataPath}"`)
|
// Remove metadata.abs if it exists
|
||||||
return success
|
if (await fs.pathExists(Path.join(metadataPath, `metadata.abs`))) {
|
||||||
})
|
Logger.debug(`[LibraryItem] Removing metadata.abs for item "${this.media.metadata.title}"`)
|
||||||
|
await fs.remove(Path.join(metadataPath, `metadata.abs`))
|
||||||
|
this.libraryFiles = this.libraryFiles.filter(lf => lf.metadata.path !== filePathToPOSIX(Path.join(metadataPath, `metadata.abs`)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return fs.writeFile(metadataFilePath, JSON.stringify(this.media.toJSONForMetadataFile(), null, 2)).then(async () => {
|
||||||
|
this.isSavingMetadata = false
|
||||||
|
// Add metadata.json to libraryFiles array if it is new
|
||||||
|
if (global.ServerSettings.storeMetadataWithItem && !this.libraryFiles.some(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath))) {
|
||||||
|
const newLibraryFile = new LibraryFile()
|
||||||
|
await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.json`)
|
||||||
|
this.libraryFiles.push(newLibraryFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}).catch((error) => {
|
||||||
|
this.isSavingMetadata = false
|
||||||
|
Logger.error(`[LibraryItem] Failed to save json file at "${metadataFilePath}"`, error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Remove metadata.json if it exists
|
||||||
|
if (await fs.pathExists(Path.join(metadataPath, `metadata.json`))) {
|
||||||
|
Logger.debug(`[LibraryItem] Removing metadata.json for item "${this.media.metadata.title}"`)
|
||||||
|
await fs.remove(Path.join(metadataPath, `metadata.json`))
|
||||||
|
this.libraryFiles = this.libraryFiles.filter(lf => lf.metadata.path !== filePathToPOSIX(Path.join(metadataPath, `metadata.json`)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return abmetadataGenerator.generate(this, metadataFilePath).then(async (success) => {
|
||||||
|
this.isSavingMetadata = false
|
||||||
|
if (!success) Logger.error(`[LibraryItem] Failed saving abmetadata to "${metadataFilePath}"`)
|
||||||
|
else {
|
||||||
|
// Add metadata.abs to libraryFiles array if it is new
|
||||||
|
if (global.ServerSettings.storeMetadataWithItem && !this.libraryFiles.some(lf => lf.metadata.path === filePathToPOSIX(metadataFilePath))) {
|
||||||
|
const newLibraryFile = new LibraryFile()
|
||||||
|
await newLibraryFile.setDataFromPath(metadataFilePath, `metadata.abs`)
|
||||||
|
this.libraryFiles.push(newLibraryFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.debug(`[LibraryItem] Success saving abmetadata to "${metadataFilePath}"`)
|
||||||
|
}
|
||||||
|
return success
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
removeLibraryFile(ino) {
|
removeLibraryFile(ino) {
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ class PlaybackSession {
|
|||||||
this.episodeId = episodeId
|
this.episodeId = episodeId
|
||||||
this.mediaType = libraryItem.mediaType
|
this.mediaType = libraryItem.mediaType
|
||||||
this.mediaMetadata = libraryItem.media.metadata.clone()
|
this.mediaMetadata = libraryItem.media.metadata.clone()
|
||||||
this.chapters = (libraryItem.media.chapters || []).map(c => ({ ...c })) // Only book mediaType has chapters
|
this.chapters = libraryItem.media.getChapters(episodeId)
|
||||||
this.displayTitle = libraryItem.media.getPlaybackTitle(episodeId)
|
this.displayTitle = libraryItem.media.getPlaybackTitle(episodeId)
|
||||||
this.displayAuthor = libraryItem.media.getPlaybackAuthor()
|
this.displayAuthor = libraryItem.media.getPlaybackAuthor()
|
||||||
this.coverPath = libraryItem.media.coverPath
|
this.coverPath = libraryItem.media.coverPath
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class Task {
|
|||||||
this.title = null
|
this.title = null
|
||||||
this.description = null
|
this.description = null
|
||||||
this.error = null
|
this.error = null
|
||||||
|
this.showSuccess = false // If true client side should keep the task visible after success
|
||||||
|
|
||||||
this.isFailed = false
|
this.isFailed = false
|
||||||
this.isFinished = false
|
this.isFinished = false
|
||||||
@@ -25,6 +26,7 @@ class Task {
|
|||||||
title: this.title,
|
title: this.title,
|
||||||
description: this.description,
|
description: this.description,
|
||||||
error: this.error,
|
error: this.error,
|
||||||
|
showSuccess: this.showSuccess,
|
||||||
isFailed: this.isFailed,
|
isFailed: this.isFailed,
|
||||||
isFinished: this.isFinished,
|
isFinished: this.isFinished,
|
||||||
startedAt: this.startedAt,
|
startedAt: this.startedAt,
|
||||||
@@ -32,12 +34,13 @@ class Task {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(action, title, description, data = {}) {
|
setData(action, title, description, showSuccess, data = {}) {
|
||||||
this.id = getId(action)
|
this.id = getId(action)
|
||||||
this.action = action
|
this.action = action
|
||||||
this.data = { ...data }
|
this.data = { ...data }
|
||||||
this.title = title
|
this.title = title
|
||||||
this.description = description
|
this.description = description
|
||||||
|
this.showSuccess = showSuccess
|
||||||
this.startedAt = Date.now()
|
this.startedAt = Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,7 +51,10 @@ class Task {
|
|||||||
this.setFinished()
|
this.setFinished()
|
||||||
}
|
}
|
||||||
|
|
||||||
setFinished() {
|
setFinished(newDescription = null) {
|
||||||
|
if (newDescription) {
|
||||||
|
this.description = newDescription
|
||||||
|
}
|
||||||
this.isFinished = true
|
this.isFinished = true
|
||||||
this.finishedAt = Date.now()
|
this.finishedAt = Date.now()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const { getFileTimestampsWithIno } = require('../../utils/fileUtils')
|
const { getFileTimestampsWithIno, filePathToPOSIX } = require('../../utils/fileUtils')
|
||||||
const globals = require('../../utils/globals')
|
const globals = require('../../utils/globals')
|
||||||
const FileMetadata = require('../metadata/FileMetadata')
|
const FileMetadata = require('../metadata/FileMetadata')
|
||||||
|
|
||||||
@@ -59,8 +59,8 @@ class LibraryFile {
|
|||||||
var fileMetadata = new FileMetadata()
|
var fileMetadata = new FileMetadata()
|
||||||
fileMetadata.setData(fileTsData)
|
fileMetadata.setData(fileTsData)
|
||||||
fileMetadata.filename = Path.basename(relPath)
|
fileMetadata.filename = Path.basename(relPath)
|
||||||
fileMetadata.path = path
|
fileMetadata.path = filePathToPOSIX(path)
|
||||||
fileMetadata.relPath = relPath
|
fileMetadata.relPath = filePathToPOSIX(relPath)
|
||||||
fileMetadata.ext = Path.extname(relPath)
|
fileMetadata.ext = Path.extname(relPath)
|
||||||
this.ino = fileTsData.ino
|
this.ino = fileTsData.ino
|
||||||
this.metadata = fileMetadata
|
this.metadata = fileMetadata
|
||||||
|
|||||||
@@ -89,6 +89,14 @@ class Book {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSONForMetadataFile() {
|
||||||
|
return {
|
||||||
|
tags: [...this.tags],
|
||||||
|
chapters: this.chapters.map(c => ({ ...c })),
|
||||||
|
metadata: this.metadata.toJSONForMetadataFile()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get size() {
|
get size() {
|
||||||
var total = 0
|
var total = 0
|
||||||
this.audioFiles.forEach((af) => total += af.metadata.size)
|
this.audioFiles.forEach((af) => total += af.metadata.size)
|
||||||
@@ -134,6 +142,9 @@ class Book {
|
|||||||
get numTracks() {
|
get numTracks() {
|
||||||
return this.tracks.length
|
return this.tracks.length
|
||||||
}
|
}
|
||||||
|
get isEBookOnly() {
|
||||||
|
return this.ebookFile && !this.numTracks
|
||||||
|
}
|
||||||
|
|
||||||
update(payload) {
|
update(payload) {
|
||||||
const json = this.toJSON()
|
const json = this.toJSON()
|
||||||
@@ -229,7 +240,7 @@ class Book {
|
|||||||
// Look for desc.txt, reader.txt, metadata.abs and opf file then update details if found
|
// Look for desc.txt, reader.txt, metadata.abs and opf file then update details if found
|
||||||
async syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) {
|
async syncMetadataFiles(textMetadataFiles, opfMetadataOverrideDetails) {
|
||||||
let metadataUpdatePayload = {}
|
let metadataUpdatePayload = {}
|
||||||
let tagsUpdated = false
|
let hasUpdated = false
|
||||||
|
|
||||||
const descTxt = textMetadataFiles.find(lf => lf.metadata.filename === 'desc.txt')
|
const descTxt = textMetadataFiles.find(lf => lf.metadata.filename === 'desc.txt')
|
||||||
if (descTxt) {
|
if (descTxt) {
|
||||||
@@ -248,17 +259,25 @@ class Book {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const metadataIsJSON = global.ServerSettings.metadataFileFormat === 'json'
|
||||||
const metadataAbs = textMetadataFiles.find(lf => lf.metadata.filename === 'metadata.abs')
|
const metadataAbs = textMetadataFiles.find(lf => lf.metadata.filename === 'metadata.abs')
|
||||||
if (metadataAbs) {
|
const metadataJson = textMetadataFiles.find(lf => lf.metadata.filename === 'metadata.json')
|
||||||
Logger.debug(`[Book] Found metadata.abs file for "${this.metadata.title}"`)
|
|
||||||
const metadataText = await readTextFile(metadataAbs.metadata.path)
|
const metadataFile = metadataIsJSON ? metadataJson : metadataAbs
|
||||||
const abmetadataUpdates = abmetadataGenerator.parseAndCheckForUpdates(metadataText, this, 'book')
|
if (metadataFile) {
|
||||||
|
Logger.debug(`[Book] Found ${metadataFile.metadata.filename} file for "${this.metadata.title}"`)
|
||||||
|
const metadataText = await readTextFile(metadataFile.metadata.path)
|
||||||
|
const abmetadataUpdates = abmetadataGenerator.parseAndCheckForUpdates(metadataText, this, 'book', metadataIsJSON)
|
||||||
if (abmetadataUpdates && Object.keys(abmetadataUpdates).length) {
|
if (abmetadataUpdates && Object.keys(abmetadataUpdates).length) {
|
||||||
Logger.debug(`[Book] "${this.metadata.title}" changes found in metadata.abs file`, abmetadataUpdates)
|
Logger.debug(`[Book] "${this.metadata.title}" changes found in metadata.abs file`, abmetadataUpdates)
|
||||||
|
|
||||||
if (abmetadataUpdates.tags) { // Set media tags if updated
|
if (abmetadataUpdates.tags) { // Set media tags if updated
|
||||||
this.tags = abmetadataUpdates.tags
|
this.tags = abmetadataUpdates.tags
|
||||||
tagsUpdated = true
|
hasUpdated = true
|
||||||
|
}
|
||||||
|
if (abmetadataUpdates.chapters) { // Set chapters if updated
|
||||||
|
this.chapters = abmetadataUpdates.chapters
|
||||||
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
if (abmetadataUpdates.metadata) {
|
if (abmetadataUpdates.metadata) {
|
||||||
metadataUpdatePayload = {
|
metadataUpdatePayload = {
|
||||||
@@ -267,6 +286,9 @@ class Book {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (metadataAbs || metadataJson) { // Has different metadata file format so mark as updated
|
||||||
|
Logger.debug(`[Book] Found different format metadata file ${(metadataAbs || metadataJson).metadata.filename}, expecting .${global.ServerSettings.metadataFileFormat} for "${this.metadata.title}"`)
|
||||||
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const metadataOpf = textMetadataFiles.find(lf => lf.isOPFFile || lf.metadata.filename === 'metadata.xml')
|
const metadataOpf = textMetadataFiles.find(lf => lf.isOPFFile || lf.metadata.filename === 'metadata.xml')
|
||||||
@@ -280,7 +302,7 @@ class Book {
|
|||||||
if (key === 'tags') { // Add tags only if tags are empty
|
if (key === 'tags') { // Add tags only if tags are empty
|
||||||
if (opfMetadata.tags.length && (!this.tags.length || opfMetadataOverrideDetails)) {
|
if (opfMetadata.tags.length && (!this.tags.length || opfMetadataOverrideDetails)) {
|
||||||
this.tags = opfMetadata.tags
|
this.tags = opfMetadata.tags
|
||||||
tagsUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
} else if (key === 'genres') { // Add genres only if genres are empty
|
} else if (key === 'genres') { // Add genres only if genres are empty
|
||||||
if (opfMetadata.genres.length && (!this.metadata.genres.length || opfMetadataOverrideDetails)) {
|
if (opfMetadata.genres.length && (!this.metadata.genres.length || opfMetadataOverrideDetails)) {
|
||||||
@@ -312,9 +334,9 @@ class Book {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(metadataUpdatePayload).length) {
|
if (Object.keys(metadataUpdatePayload).length) {
|
||||||
return this.metadata.update(metadataUpdatePayload) || tagsUpdated
|
return this.metadata.update(metadataUpdatePayload) || hasUpdated
|
||||||
}
|
}
|
||||||
return tagsUpdated
|
return hasUpdated
|
||||||
}
|
}
|
||||||
|
|
||||||
searchQuery(query) {
|
searchQuery(query) {
|
||||||
@@ -322,6 +344,7 @@ class Book {
|
|||||||
tags: this.tags.filter(t => cleanStringForSearch(t).includes(query)),
|
tags: this.tags.filter(t => cleanStringForSearch(t).includes(query)),
|
||||||
series: this.metadata.searchSeries(query),
|
series: this.metadata.searchSeries(query),
|
||||||
authors: this.metadata.searchAuthors(query),
|
authors: this.metadata.searchAuthors(query),
|
||||||
|
narrators: this.metadata.searchNarrators(query),
|
||||||
matchKey: null,
|
matchKey: null,
|
||||||
matchText: null
|
matchText: null
|
||||||
}
|
}
|
||||||
@@ -336,10 +359,12 @@ class Book {
|
|||||||
} else if (payload.series.length) {
|
} else if (payload.series.length) {
|
||||||
payload.matchKey = 'series'
|
payload.matchKey = 'series'
|
||||||
payload.matchText = this.metadata.seriesName
|
payload.matchText = this.metadata.seriesName
|
||||||
}
|
} else if (payload.tags.length) {
|
||||||
else if (payload.tags.length) {
|
|
||||||
payload.matchKey = 'tags'
|
payload.matchKey = 'tags'
|
||||||
payload.matchText = this.tags.join(', ')
|
payload.matchText = this.tags.join(', ')
|
||||||
|
} else if (payload.narrators.length) {
|
||||||
|
payload.matchKey = 'narrators'
|
||||||
|
payload.matchText = this.metadata.narratorName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return payload
|
return payload
|
||||||
@@ -506,5 +531,9 @@ class Book {
|
|||||||
getPlaybackAuthor() {
|
getPlaybackAuthor() {
|
||||||
return this.metadata.authorName
|
return this.metadata.authorName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getChapters() {
|
||||||
|
return this.chapters?.map(ch => ({ ...ch })) || []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = Book
|
module.exports = Book
|
||||||
|
|||||||
@@ -94,6 +94,13 @@ class Podcast {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSONForMetadataFile() {
|
||||||
|
return {
|
||||||
|
tags: [...this.tags],
|
||||||
|
metadata: this.metadata.toJSON()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get size() {
|
get size() {
|
||||||
var total = 0
|
var total = 0
|
||||||
this.episodes.forEach((ep) => total += ep.size)
|
this.episodes.forEach((ep) => total += ep.size)
|
||||||
@@ -199,10 +206,11 @@ class Podcast {
|
|||||||
let metadataUpdatePayload = {}
|
let metadataUpdatePayload = {}
|
||||||
let tagsUpdated = false
|
let tagsUpdated = false
|
||||||
|
|
||||||
const metadataAbs = textMetadataFiles.find(lf => lf.metadata.filename === 'metadata.abs')
|
const metadataAbs = textMetadataFiles.find(lf => lf.metadata.filename === 'metadata.abs' || lf.metadata.filename === 'metadata.json')
|
||||||
if (metadataAbs) {
|
if (metadataAbs) {
|
||||||
|
const isJSON = metadataAbs.metadata.filename === 'metadata.json'
|
||||||
const metadataText = await readTextFile(metadataAbs.metadata.path)
|
const metadataText = await readTextFile(metadataAbs.metadata.path)
|
||||||
const abmetadataUpdates = abmetadataGenerator.parseAndCheckForUpdates(metadataText, this, 'podcast')
|
const abmetadataUpdates = abmetadataGenerator.parseAndCheckForUpdates(metadataText, this, 'podcast', isJSON)
|
||||||
if (abmetadataUpdates && Object.keys(abmetadataUpdates).length) {
|
if (abmetadataUpdates && Object.keys(abmetadataUpdates).length) {
|
||||||
Logger.debug(`[Podcast] "${this.metadata.title}" changes found in metadata.abs file`, abmetadataUpdates)
|
Logger.debug(`[Podcast] "${this.metadata.title}" changes found in metadata.abs file`, abmetadataUpdates)
|
||||||
|
|
||||||
@@ -331,5 +339,9 @@ class Podcast {
|
|||||||
if (!audioFile?.metaTags) return false
|
if (!audioFile?.metaTags) return false
|
||||||
return this.metadata.setDataFromAudioMetaTags(audioFile.metaTags, overrideExistingDetails)
|
return this.metadata.setDataFromAudioMetaTags(audioFile.metaTags, overrideExistingDetails)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getChapters(episodeId) {
|
||||||
|
return this.getEpisode(episodeId)?.chapters?.map(ch => ({ ...ch })) || []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = Podcast
|
module.exports = Podcast
|
||||||
|
|||||||
@@ -27,9 +27,9 @@ class BookMetadata {
|
|||||||
construct(metadata) {
|
construct(metadata) {
|
||||||
this.title = metadata.title
|
this.title = metadata.title
|
||||||
this.subtitle = metadata.subtitle
|
this.subtitle = metadata.subtitle
|
||||||
this.authors = (metadata.authors && metadata.authors.map) ? metadata.authors.map(a => ({ ...a })) : []
|
this.authors = (metadata.authors?.map) ? metadata.authors.map(a => ({ ...a })) : []
|
||||||
this.narrators = metadata.narrators ? [...metadata.narrators] : []
|
this.narrators = metadata.narrators ? [...metadata.narrators].filter(n => n) : []
|
||||||
this.series = (metadata.series && metadata.series.map) ? metadata.series.map(s => ({ ...s })) : []
|
this.series = (metadata.series?.map) ? metadata.series.map(s => ({ ...s })) : []
|
||||||
this.genres = metadata.genres ? [...metadata.genres] : []
|
this.genres = metadata.genres ? [...metadata.genres] : []
|
||||||
this.publishedYear = metadata.publishedYear || null
|
this.publishedYear = metadata.publishedYear || null
|
||||||
this.publishedDate = metadata.publishedDate || null
|
this.publishedDate = metadata.publishedDate || null
|
||||||
@@ -109,6 +109,16 @@ class BookMetadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
toJSONForMetadataFile() {
|
||||||
|
const json = this.toJSON()
|
||||||
|
json.authors = json.authors.map(au => au.name)
|
||||||
|
json.series = json.series.map(se => {
|
||||||
|
if (!se.sequence) return se.name
|
||||||
|
return `${se.name} #${se.sequence}`
|
||||||
|
})
|
||||||
|
return json
|
||||||
|
}
|
||||||
|
|
||||||
clone() {
|
clone() {
|
||||||
return new BookMetadata(this.toJSON())
|
return new BookMetadata(this.toJSON())
|
||||||
}
|
}
|
||||||
@@ -191,8 +201,9 @@ class BookMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update(payload) {
|
update(payload) {
|
||||||
var json = this.toJSON()
|
const json = this.toJSON()
|
||||||
var hasUpdates = false
|
let hasUpdates = false
|
||||||
|
|
||||||
for (const key in json) {
|
for (const key in json) {
|
||||||
if (payload[key] !== undefined) {
|
if (payload[key] !== undefined) {
|
||||||
if (!areEquivalent(payload[key], json[key])) {
|
if (!areEquivalent(payload[key], json[key])) {
|
||||||
@@ -221,6 +232,32 @@ class BookMetadata {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update narrator name if narrator is in book
|
||||||
|
* @param {String} oldNarratorName - Narrator name to get updated
|
||||||
|
* @param {String} newNarratorName - Updated narrator name
|
||||||
|
* @return {Boolean} True if narrator was updated
|
||||||
|
*/
|
||||||
|
updateNarrator(oldNarratorName, newNarratorName) {
|
||||||
|
if (!this.hasNarrator(oldNarratorName)) return false
|
||||||
|
this.narrators = this.narrators.filter(n => n !== oldNarratorName)
|
||||||
|
if (newNarratorName && !this.hasNarrator(newNarratorName)) {
|
||||||
|
this.narrators.push(newNarratorName)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove narrator name if narrator is in book
|
||||||
|
* @param {String} narratorName - Narrator name to remove
|
||||||
|
* @return {Boolean} True if narrator was updated
|
||||||
|
*/
|
||||||
|
removeNarrator(narratorName) {
|
||||||
|
if (!this.hasNarrator(narratorName)) return false
|
||||||
|
this.narrators = this.narrators.filter(n => n !== narratorName)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
setData(scanMediaData = {}) {
|
setData(scanMediaData = {}) {
|
||||||
this.title = scanMediaData.title || null
|
this.title = scanMediaData.title || null
|
||||||
this.subtitle = scanMediaData.subtitle || null
|
this.subtitle = scanMediaData.subtitle || null
|
||||||
@@ -347,8 +384,10 @@ class BookMetadata {
|
|||||||
const parsed = parseNameString.parse(authorsTag)
|
const parsed = parseNameString.parse(authorsTag)
|
||||||
if (!parsed) return []
|
if (!parsed) return []
|
||||||
return (parsed.names || []).map((au) => {
|
return (parsed.names || []).map((au) => {
|
||||||
|
const findAuthor = this.authors.find(_au => _au.name == au)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: `new-${Math.floor(Math.random() * 1000000)}`,
|
id: findAuthor?.id || `new-${Math.floor(Math.random() * 1000000)}`,
|
||||||
name: au
|
name: au
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -381,8 +420,11 @@ class BookMetadata {
|
|||||||
searchAuthors(query) {
|
searchAuthors(query) {
|
||||||
return this.authors.filter(au => cleanStringForSearch(au.name).includes(query))
|
return this.authors.filter(au => cleanStringForSearch(au.name).includes(query))
|
||||||
}
|
}
|
||||||
|
searchNarrators(query) {
|
||||||
|
return this.narrators.filter(n => cleanStringForSearch(n).includes(query))
|
||||||
|
}
|
||||||
searchQuery(query) { // Returns key if match is found
|
searchQuery(query) { // Returns key if match is found
|
||||||
const keysToCheck = ['title', 'asin', 'isbn']
|
const keysToCheck = ['title', 'asin', 'isbn', 'subtitle']
|
||||||
for (const key of keysToCheck) {
|
for (const key of keysToCheck) {
|
||||||
if (this[key] && cleanStringForSearch(String(this[key])).includes(query)) {
|
if (this[key] && cleanStringForSearch(String(this[key])).includes(query)) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
const { BookshelfView } = require('../../utils/constants')
|
const { BookshelfView } = require('../../utils/constants')
|
||||||
const { isNullOrNaN } = require('../../utils')
|
|
||||||
const Logger = require('../../Logger')
|
const Logger = require('../../Logger')
|
||||||
|
|
||||||
class ServerSettings {
|
class ServerSettings {
|
||||||
@@ -21,6 +20,7 @@ class ServerSettings {
|
|||||||
// Metadata - choose to store inside users library item folder
|
// Metadata - choose to store inside users library item folder
|
||||||
this.storeCoverWithItem = false
|
this.storeCoverWithItem = false
|
||||||
this.storeMetadataWithItem = false
|
this.storeMetadataWithItem = false
|
||||||
|
this.metadataFileFormat = 'json'
|
||||||
|
|
||||||
// Security/Rate limits
|
// Security/Rate limits
|
||||||
this.rateLimitLoginRequests = 10
|
this.rateLimitLoginRequests = 10
|
||||||
@@ -77,6 +77,7 @@ class ServerSettings {
|
|||||||
|
|
||||||
this.storeCoverWithItem = !!settings.storeCoverWithItem
|
this.storeCoverWithItem = !!settings.storeCoverWithItem
|
||||||
this.storeMetadataWithItem = !!settings.storeMetadataWithItem
|
this.storeMetadataWithItem = !!settings.storeMetadataWithItem
|
||||||
|
this.metadataFileFormat = settings.metadataFileFormat || 'json'
|
||||||
|
|
||||||
this.rateLimitLoginRequests = !isNaN(settings.rateLimitLoginRequests) ? Number(settings.rateLimitLoginRequests) : 10
|
this.rateLimitLoginRequests = !isNaN(settings.rateLimitLoginRequests) ? Number(settings.rateLimitLoginRequests) : 10
|
||||||
this.rateLimitLoginWindow = !isNaN(settings.rateLimitLoginWindow) ? Number(settings.rateLimitLoginWindow) : 10 * 60 * 1000 // 10 Minutes
|
this.rateLimitLoginWindow = !isNaN(settings.rateLimitLoginWindow) ? Number(settings.rateLimitLoginWindow) : 10 * 60 * 1000 // 10 Minutes
|
||||||
@@ -112,6 +113,16 @@ class ServerSettings {
|
|||||||
if (settings.homeBookshelfView == undefined) { // homeBookshelfView was added in 2.1.3
|
if (settings.homeBookshelfView == undefined) { // homeBookshelfView was added in 2.1.3
|
||||||
this.homeBookshelfView = settings.bookshelfView
|
this.homeBookshelfView = settings.bookshelfView
|
||||||
}
|
}
|
||||||
|
if (settings.metadataFileFormat == undefined) { // metadataFileFormat was added in 2.2.21
|
||||||
|
// All users using old settings will stay abs until changed
|
||||||
|
this.metadataFileFormat = 'abs'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
if (!['abs', 'json'].includes(this.metadataFileFormat)) {
|
||||||
|
Logger.error(`[ServerSettings] construct: Invalid metadataFileFormat ${this.metadataFileFormat}`)
|
||||||
|
this.metadataFileFormat = 'json'
|
||||||
|
}
|
||||||
|
|
||||||
if (this.logLevel !== Logger.logLevel) {
|
if (this.logLevel !== Logger.logLevel) {
|
||||||
Logger.setLogLevel(this.logLevel)
|
Logger.setLogLevel(this.logLevel)
|
||||||
@@ -133,6 +144,7 @@ class ServerSettings {
|
|||||||
scannerUseTone: this.scannerUseTone,
|
scannerUseTone: this.scannerUseTone,
|
||||||
storeCoverWithItem: this.storeCoverWithItem,
|
storeCoverWithItem: this.storeCoverWithItem,
|
||||||
storeMetadataWithItem: this.storeMetadataWithItem,
|
storeMetadataWithItem: this.storeMetadataWithItem,
|
||||||
|
metadataFileFormat: this.metadataFileFormat,
|
||||||
rateLimitLoginRequests: this.rateLimitLoginRequests,
|
rateLimitLoginRequests: this.rateLimitLoginRequests,
|
||||||
rateLimitLoginWindow: this.rateLimitLoginWindow,
|
rateLimitLoginWindow: this.rateLimitLoginWindow,
|
||||||
backupSchedule: this.backupSchedule,
|
backupSchedule: this.backupSchedule,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class MediaProgress {
|
|||||||
this.isFinished = false
|
this.isFinished = false
|
||||||
this.hideFromContinueListening = false
|
this.hideFromContinueListening = false
|
||||||
|
|
||||||
this.ebookLocation = null // current cfi tag
|
this.ebookLocation = null // cfi tag for epub, page number for pdf
|
||||||
this.ebookProgress = null // 0 to 1
|
this.ebookProgress = null // 0 to 1
|
||||||
|
|
||||||
this.lastUpdate = null
|
this.lastUpdate = null
|
||||||
@@ -46,18 +46,18 @@ class MediaProgress {
|
|||||||
this.episodeId = progress.episodeId
|
this.episodeId = progress.episodeId
|
||||||
this.duration = progress.duration || 0
|
this.duration = progress.duration || 0
|
||||||
this.progress = progress.progress
|
this.progress = progress.progress
|
||||||
this.currentTime = progress.currentTime
|
this.currentTime = progress.currentTime || 0
|
||||||
this.isFinished = !!progress.isFinished
|
this.isFinished = !!progress.isFinished
|
||||||
this.hideFromContinueListening = !!progress.hideFromContinueListening
|
this.hideFromContinueListening = !!progress.hideFromContinueListening
|
||||||
this.ebookLocation = progress.ebookLocation || null
|
this.ebookLocation = progress.ebookLocation || null
|
||||||
this.ebookProgress = progress.ebookProgress
|
this.ebookProgress = progress.ebookProgress || null
|
||||||
this.lastUpdate = progress.lastUpdate
|
this.lastUpdate = progress.lastUpdate
|
||||||
this.startedAt = progress.startedAt
|
this.startedAt = progress.startedAt
|
||||||
this.finishedAt = progress.finishedAt || null
|
this.finishedAt = progress.finishedAt || null
|
||||||
}
|
}
|
||||||
|
|
||||||
get inProgress() {
|
get inProgress() {
|
||||||
return !this.isFinished && (this.progress > 0 || this.ebookLocation != null)
|
return !this.isFinished && (this.progress > 0 || (this.ebookLocation != null && this.ebookProgress > 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
setData(libraryItemId, progress, episodeId = null) {
|
setData(libraryItemId, progress, episodeId = null) {
|
||||||
|
|||||||
+25
-14
@@ -20,7 +20,7 @@ class User {
|
|||||||
|
|
||||||
this.permissions = {}
|
this.permissions = {}
|
||||||
this.librariesAccessible = [] // Library IDs (Empty if ALL libraries)
|
this.librariesAccessible = [] // Library IDs (Empty if ALL libraries)
|
||||||
this.itemTagsAccessible = [] // Empty if ALL item tags accessible
|
this.itemTagsSelected = [] // Empty if ALL item tags accessible
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
this.construct(user)
|
this.construct(user)
|
||||||
@@ -86,7 +86,7 @@ class User {
|
|||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
permissions: this.permissions,
|
permissions: this.permissions,
|
||||||
librariesAccessible: [...this.librariesAccessible],
|
librariesAccessible: [...this.librariesAccessible],
|
||||||
itemTagsAccessible: [...this.itemTagsAccessible]
|
itemTagsSelected: [...this.itemTagsSelected]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +105,7 @@ class User {
|
|||||||
createdAt: this.createdAt,
|
createdAt: this.createdAt,
|
||||||
permissions: this.permissions,
|
permissions: this.permissions,
|
||||||
librariesAccessible: [...this.librariesAccessible],
|
librariesAccessible: [...this.librariesAccessible],
|
||||||
itemTagsAccessible: [...this.itemTagsAccessible]
|
itemTagsSelected: [...this.itemTagsSelected]
|
||||||
}
|
}
|
||||||
if (minimal) {
|
if (minimal) {
|
||||||
delete json.mediaProgress
|
delete json.mediaProgress
|
||||||
@@ -169,9 +169,14 @@ class User {
|
|||||||
if (this.permissions.accessAllTags === undefined) this.permissions.accessAllTags = true
|
if (this.permissions.accessAllTags === undefined) this.permissions.accessAllTags = true
|
||||||
// Explicit content restriction permission added v2.0.18
|
// Explicit content restriction permission added v2.0.18
|
||||||
if (this.permissions.accessExplicitContent === undefined) this.permissions.accessExplicitContent = true
|
if (this.permissions.accessExplicitContent === undefined) this.permissions.accessExplicitContent = true
|
||||||
|
// itemTagsAccessible was renamed to itemTagsSelected in version v2.2.20
|
||||||
|
if (user.itemTagsAccessible?.length) {
|
||||||
|
this.permissions.selectedTagsNotAccessible = false
|
||||||
|
user.itemTagsSelected = user.itemTagsAccessible
|
||||||
|
}
|
||||||
|
|
||||||
this.librariesAccessible = [...(user.librariesAccessible || [])]
|
this.librariesAccessible = [...(user.librariesAccessible || [])]
|
||||||
this.itemTagsAccessible = [...(user.itemTagsAccessible || [])]
|
this.itemTagsSelected = [...(user.itemTagsSelected || [])]
|
||||||
}
|
}
|
||||||
|
|
||||||
update(payload) {
|
update(payload) {
|
||||||
@@ -228,19 +233,21 @@ class User {
|
|||||||
// Update accessible tags
|
// Update accessible tags
|
||||||
if (this.permissions.accessAllTags) {
|
if (this.permissions.accessAllTags) {
|
||||||
// Access all tags
|
// Access all tags
|
||||||
if (this.itemTagsAccessible.length) {
|
if (this.itemTagsSelected.length) {
|
||||||
this.itemTagsAccessible = []
|
this.itemTagsSelected = []
|
||||||
|
this.permissions.selectedTagsNotAccessible = false
|
||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
} else if (payload.itemTagsAccessible !== undefined) {
|
} else if (payload.itemTagsSelected !== undefined) {
|
||||||
if (payload.itemTagsAccessible.length) {
|
if (payload.itemTagsSelected.length) {
|
||||||
if (payload.itemTagsAccessible.join(',') !== this.itemTagsAccessible.join(',')) {
|
if (payload.itemTagsSelected.join(',') !== this.itemTagsSelected.join(',')) {
|
||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
this.itemTagsAccessible = [...payload.itemTagsAccessible]
|
this.itemTagsSelected = [...payload.itemTagsSelected]
|
||||||
}
|
}
|
||||||
} else if (this.itemTagsAccessible.length > 0) {
|
} else if (this.itemTagsSelected.length > 0) {
|
||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
this.itemTagsAccessible = []
|
this.itemTagsSelected = []
|
||||||
|
this.permissions.selectedTagsNotAccessible = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return hasUpdates
|
return hasUpdates
|
||||||
@@ -343,8 +350,12 @@ class User {
|
|||||||
|
|
||||||
checkCanAccessLibraryItemWithTags(tags) {
|
checkCanAccessLibraryItemWithTags(tags) {
|
||||||
if (this.permissions.accessAllTags) return true
|
if (this.permissions.accessAllTags) return true
|
||||||
if (!tags || !tags.length) return false
|
if (this.permissions.selectedTagsNotAccessible) {
|
||||||
return this.itemTagsAccessible.some(tag => tags.includes(tag))
|
if (!tags?.length) return true
|
||||||
|
return tags.every(tag => !this.itemTagsSelected.includes(tag))
|
||||||
|
}
|
||||||
|
if (!tags?.length) return false
|
||||||
|
return this.itemTagsSelected.some(tag => tags.includes(tag))
|
||||||
}
|
}
|
||||||
|
|
||||||
checkCanAccessLibraryItem(libraryItem) {
|
checkCanAccessLibraryItem(libraryItem) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class Audible {
|
|||||||
'de': '.de',
|
'de': '.de',
|
||||||
'jp': '.co.jp',
|
'jp': '.co.jp',
|
||||||
'it': '.it',
|
'it': '.it',
|
||||||
'in': '.co.in',
|
'in': '.in',
|
||||||
'es': '.es'
|
'es': '.es'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
const axios = require('axios')
|
||||||
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
|
class AudiobookCovers {
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
async search(search) {
|
||||||
|
const url = `https://api.audiobookcovers.com/cover/bytext/`
|
||||||
|
const params = new URLSearchParams([['q', search]])
|
||||||
|
const items = await axios.get(url, { params }).then((res) => {
|
||||||
|
if (!res || !res.data) return []
|
||||||
|
return res.data
|
||||||
|
}).catch(error => {
|
||||||
|
Logger.error('[AudiobookCovers] Cover search error', error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
return items.map(item => ({ cover: item.filename }))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = AudiobookCovers
|
||||||
@@ -84,6 +84,9 @@ class ApiRouter {
|
|||||||
this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this))
|
this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this))
|
||||||
this.router.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this))
|
this.router.get('/libraries/:id/stats', LibraryController.middleware.bind(this), LibraryController.stats.bind(this))
|
||||||
this.router.get('/libraries/:id/authors', LibraryController.middleware.bind(this), LibraryController.getAuthors.bind(this))
|
this.router.get('/libraries/:id/authors', LibraryController.middleware.bind(this), LibraryController.getAuthors.bind(this))
|
||||||
|
this.router.get('/libraries/:id/narrators', LibraryController.middleware.bind(this), LibraryController.getNarrators.bind(this))
|
||||||
|
this.router.patch('/libraries/:id/narrators/:narratorId', LibraryController.middleware.bind(this), LibraryController.updateNarrator.bind(this))
|
||||||
|
this.router.delete('/libraries/:id/narrators/:narratorId', LibraryController.middleware.bind(this), LibraryController.removeNarrator.bind(this))
|
||||||
this.router.get('/libraries/:id/matchall', LibraryController.middleware.bind(this), LibraryController.matchAll.bind(this))
|
this.router.get('/libraries/:id/matchall', LibraryController.middleware.bind(this), LibraryController.matchAll.bind(this))
|
||||||
this.router.post('/libraries/:id/scan', LibraryController.middleware.bind(this), LibraryController.scan.bind(this))
|
this.router.post('/libraries/:id/scan', LibraryController.middleware.bind(this), LibraryController.scan.bind(this))
|
||||||
this.router.get('/libraries/:id/recent-episodes', LibraryController.middleware.bind(this), LibraryController.getRecentEpisodes.bind(this))
|
this.router.get('/libraries/:id/recent-episodes', LibraryController.middleware.bind(this), LibraryController.getRecentEpisodes.bind(this))
|
||||||
@@ -93,6 +96,11 @@ class ApiRouter {
|
|||||||
//
|
//
|
||||||
// Item Routes
|
// Item Routes
|
||||||
//
|
//
|
||||||
|
this.router.post('/items/batch/delete', LibraryItemController.batchDelete.bind(this))
|
||||||
|
this.router.post('/items/batch/update', LibraryItemController.batchUpdate.bind(this))
|
||||||
|
this.router.post('/items/batch/get', LibraryItemController.batchGet.bind(this))
|
||||||
|
this.router.post('/items/batch/quickmatch', LibraryItemController.batchQuickMatch.bind(this))
|
||||||
|
this.router.post('/items/batch/scan', LibraryItemController.batchScan.bind(this))
|
||||||
this.router.delete('/items/all', LibraryItemController.deleteAll.bind(this))
|
this.router.delete('/items/all', LibraryItemController.deleteAll.bind(this))
|
||||||
|
|
||||||
this.router.get('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.findOne.bind(this))
|
this.router.get('/items/:id', LibraryItemController.middleware.bind(this), LibraryItemController.findOne.bind(this))
|
||||||
@@ -114,11 +122,6 @@ class ApiRouter {
|
|||||||
this.router.post('/items/:id/tone-scan/:index?', LibraryItemController.middleware.bind(this), LibraryItemController.toneScan.bind(this))
|
this.router.post('/items/:id/tone-scan/:index?', LibraryItemController.middleware.bind(this), LibraryItemController.toneScan.bind(this))
|
||||||
this.router.delete('/items/:id/file/:ino', LibraryItemController.middleware.bind(this), LibraryItemController.deleteLibraryFile.bind(this))
|
this.router.delete('/items/:id/file/:ino', LibraryItemController.middleware.bind(this), LibraryItemController.deleteLibraryFile.bind(this))
|
||||||
|
|
||||||
this.router.post('/items/batch/delete', LibraryItemController.batchDelete.bind(this))
|
|
||||||
this.router.post('/items/batch/update', LibraryItemController.batchUpdate.bind(this))
|
|
||||||
this.router.post('/items/batch/get', LibraryItemController.batchGet.bind(this))
|
|
||||||
this.router.post('/items/batch/quickmatch', LibraryItemController.batchQuickMatch.bind(this))
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// User Routes
|
// User Routes
|
||||||
//
|
//
|
||||||
@@ -194,6 +197,7 @@ class ApiRouter {
|
|||||||
// File System Routes
|
// File System Routes
|
||||||
//
|
//
|
||||||
this.router.get('/filesystem', FileSystemController.getPaths.bind(this))
|
this.router.get('/filesystem', FileSystemController.getPaths.bind(this))
|
||||||
|
this.router.post('/filesystem/pathexists', FileSystemController.checkPathExists.bind(this))
|
||||||
|
|
||||||
//
|
//
|
||||||
// Author Routes
|
// Author Routes
|
||||||
|
|||||||
@@ -19,11 +19,13 @@ const ScanOptions = require('./ScanOptions')
|
|||||||
|
|
||||||
const Author = require('../objects/entities/Author')
|
const Author = require('../objects/entities/Author')
|
||||||
const Series = require('../objects/entities/Series')
|
const Series = require('../objects/entities/Series')
|
||||||
|
const Task = require('../objects/Task')
|
||||||
|
|
||||||
class Scanner {
|
class Scanner {
|
||||||
constructor(db, coverManager) {
|
constructor(db, coverManager, taskManager) {
|
||||||
this.db = db
|
this.db = db
|
||||||
this.coverManager = coverManager
|
this.coverManager = coverManager
|
||||||
|
this.taskManager = taskManager
|
||||||
|
|
||||||
this.cancelLibraryScan = {}
|
this.cancelLibraryScan = {}
|
||||||
this.librariesScanning = []
|
this.librariesScanning = []
|
||||||
@@ -46,12 +48,24 @@ class Scanner {
|
|||||||
this.cancelLibraryScan[libraryId] = true
|
this.cancelLibraryScan[libraryId] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
async scanLibraryItemById(libraryItemId) {
|
getScanResultDescription(result) {
|
||||||
const libraryItem = this.db.libraryItems.find(li => li.id === libraryItemId)
|
switch (result) {
|
||||||
if (!libraryItem) {
|
case ScanResult.ADDED:
|
||||||
Logger.error(`[Scanner] Scan libraryItem by id not found ${libraryItemId}`)
|
return 'Added to library'
|
||||||
return ScanResult.NOTHING
|
case ScanResult.NOTHING:
|
||||||
|
return 'No updates necessary'
|
||||||
|
case ScanResult.REMOVED:
|
||||||
|
return 'Removed from library'
|
||||||
|
case ScanResult.UPDATED:
|
||||||
|
return 'Item was updated'
|
||||||
|
case ScanResult.UPTODATE:
|
||||||
|
return 'No updates necessary'
|
||||||
|
default:
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async scanLibraryItemByRequest(libraryItem) {
|
||||||
const library = this.db.libraries.find(lib => lib.id === libraryItem.libraryId)
|
const library = this.db.libraries.find(lib => lib.id === libraryItem.libraryId)
|
||||||
if (!library) {
|
if (!library) {
|
||||||
Logger.error(`[Scanner] Scan libraryItem by id library not found "${libraryItem.libraryId}"`)
|
Logger.error(`[Scanner] Scan libraryItem by id library not found "${libraryItem.libraryId}"`)
|
||||||
@@ -63,7 +77,21 @@ class Scanner {
|
|||||||
return ScanResult.NOTHING
|
return ScanResult.NOTHING
|
||||||
}
|
}
|
||||||
Logger.info(`[Scanner] Scanning Library Item "${libraryItem.media.metadata.title}"`)
|
Logger.info(`[Scanner] Scanning Library Item "${libraryItem.media.metadata.title}"`)
|
||||||
return this.scanLibraryItem(library.mediaType, folder, libraryItem)
|
|
||||||
|
const task = new Task()
|
||||||
|
task.setData('scan-item', `Scan ${libraryItem.media.metadata.title}`, '', true, {
|
||||||
|
libraryItemId: libraryItem.id,
|
||||||
|
libraryId: library.id,
|
||||||
|
mediaType: library.mediaType
|
||||||
|
})
|
||||||
|
this.taskManager.addTask(task)
|
||||||
|
|
||||||
|
const result = await this.scanLibraryItem(library.mediaType, folder, libraryItem)
|
||||||
|
|
||||||
|
task.setFinished(this.getScanResultDescription(result))
|
||||||
|
this.taskManager.taskFinished(task)
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
async scanLibraryItem(libraryMediaType, folder, libraryItem) {
|
async scanLibraryItem(libraryMediaType, folder, libraryItem) {
|
||||||
@@ -111,8 +139,8 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
await this.db.updateLibraryItem(libraryItem)
|
||||||
|
SocketAuthority.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||||
return ScanResult.UPDATED
|
return ScanResult.UPDATED
|
||||||
}
|
}
|
||||||
return ScanResult.UPTODATE
|
return ScanResult.UPTODATE
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ const fs = require('../libs/fsExtra')
|
|||||||
const filePerms = require('./filePerms')
|
const filePerms = require('./filePerms')
|
||||||
const package = require('../../package.json')
|
const package = require('../../package.json')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const { getId, copyValue } = require('./index')
|
const { getId } = require('./index')
|
||||||
|
const areEquivalent = require('../utils/areEquivalent')
|
||||||
|
|
||||||
|
|
||||||
const CurrentAbMetadataVersion = 2
|
const CurrentAbMetadataVersion = 2
|
||||||
@@ -328,11 +329,11 @@ function parseAbMetadataText(text, mediaType) {
|
|||||||
module.exports.parse = parseAbMetadataText
|
module.exports.parse = parseAbMetadataText
|
||||||
|
|
||||||
function checkUpdatedBookAuthors(abmetadataAuthors, authors) {
|
function checkUpdatedBookAuthors(abmetadataAuthors, authors) {
|
||||||
var finalAuthors = []
|
const finalAuthors = []
|
||||||
var hasUpdates = false
|
let hasUpdates = false
|
||||||
|
|
||||||
abmetadataAuthors.forEach((authorName) => {
|
abmetadataAuthors.forEach((authorName) => {
|
||||||
var findAuthor = authors.find(au => au.name.toLowerCase() == authorName.toLowerCase())
|
const findAuthor = authors.find(au => au.name.toLowerCase() == authorName.toLowerCase())
|
||||||
if (!findAuthor) {
|
if (!findAuthor) {
|
||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
finalAuthors.push({
|
finalAuthors.push({
|
||||||
@@ -397,18 +398,81 @@ function checkArraysChanged(abmetadataArray, mediaArray) {
|
|||||||
return abmetadataArray.join(',') != mediaArray.join(',')
|
return abmetadataArray.join(',') != mediaArray.join(',')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseJsonMetadataText(text) {
|
||||||
|
try {
|
||||||
|
const abmetadataData = JSON.parse(text)
|
||||||
|
if (abmetadataData.metadata?.series?.length) {
|
||||||
|
abmetadataData.metadata.series = abmetadataData.metadata.series.map(series => {
|
||||||
|
let sequence = null
|
||||||
|
let name = series
|
||||||
|
// Series sequence match any characters after " #" other than whitespace and another #
|
||||||
|
// e.g. "Name #1a" is valid. "Name #1#a" or "Name #1 a" is not valid.
|
||||||
|
const matchResults = series.match(/ #([^#\s]+)$/) // Pull out sequence #
|
||||||
|
if (matchResults && matchResults.length && matchResults.length > 1) {
|
||||||
|
sequence = matchResults[1] // Group 1
|
||||||
|
name = series.replace(matchResults[0], '')
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
sequence
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return abmetadataData
|
||||||
|
} catch (error) {
|
||||||
|
Logger.error(`[abmetadataGenerator] Invalid metadata.json JSON`, error)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanChaptersArray(chaptersArray, mediaTitle) {
|
||||||
|
const chapters = []
|
||||||
|
let index = 0
|
||||||
|
for (const chap of chaptersArray) {
|
||||||
|
if (chap.start === null || isNaN(chap.start)) {
|
||||||
|
Logger.error(`[abmetadataGenerator] Invalid chapter start time ${chap.start} for "${mediaTitle}" metadata file`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (chap.end === null || isNaN(chap.end)) {
|
||||||
|
Logger.error(`[abmetadataGenerator] Invalid chapter end time ${chap.end} for "${mediaTitle}" metadata file`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (!chap.title || typeof chap.title !== 'string') {
|
||||||
|
Logger.error(`[abmetadataGenerator] Invalid chapter title ${chap.title} for "${mediaTitle}" metadata file`)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
chapters.push({
|
||||||
|
id: index++,
|
||||||
|
start: chap.start,
|
||||||
|
end: chap.end,
|
||||||
|
title: chap.title
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return chapters
|
||||||
|
}
|
||||||
|
|
||||||
// Input text from abmetadata file and return object of media changes
|
// Input text from abmetadata file and return object of media changes
|
||||||
// only returns object of changes. empty object means no changes
|
// only returns object of changes. empty object means no changes
|
||||||
function parseAndCheckForUpdates(text, media, mediaType) {
|
function parseAndCheckForUpdates(text, media, mediaType, isJSON) {
|
||||||
if (!text || !media || !media.metadata || !mediaType) {
|
if (!text || !media || !media.metadata || !mediaType) {
|
||||||
Logger.error(`Invalid inputs to parseAndCheckForUpdates`)
|
Logger.error(`Invalid inputs to parseAndCheckForUpdates`)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const mediaMetadata = media.metadata
|
const mediaMetadata = media.metadata
|
||||||
const metadataUpdatePayload = {} // Only updated key/values
|
const metadataUpdatePayload = {} // Only updated key/values
|
||||||
|
|
||||||
const abmetadataData = parseAbMetadataText(text, mediaType)
|
let abmetadataData = null
|
||||||
|
|
||||||
|
if (isJSON) {
|
||||||
|
abmetadataData = parseJsonMetadataText(text)
|
||||||
|
} else {
|
||||||
|
abmetadataData = parseAbMetadataText(text, mediaType)
|
||||||
|
}
|
||||||
|
|
||||||
if (!abmetadataData || !abmetadataData.metadata) {
|
if (!abmetadataData || !abmetadataData.metadata) {
|
||||||
|
Logger.error(`[abmetadataGenerator] Invalid metadata file`)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -441,6 +505,15 @@ function parseAndCheckForUpdates(text, media, mediaType) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (abmetadataData.chapters && mediaType === 'book') {
|
||||||
|
const abmetadataChaptersCleaned = cleanChaptersArray(abmetadataData.chapters)
|
||||||
|
if (abmetadataChaptersCleaned) {
|
||||||
|
if (!areEquivalent(abmetadataChaptersCleaned, media.chapters)) {
|
||||||
|
updatePayload.chapters = abmetadataChaptersCleaned
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (Object.keys(metadataUpdatePayload).length) {
|
if (Object.keys(metadataUpdatePayload).length) {
|
||||||
updatePayload.metadata = metadataUpdatePayload
|
updatePayload.metadata = metadataUpdatePayload
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ module.exports = function areEquivalent(value1, value2, stack = []) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Truthy check to handle value1=null, value2=Object
|
||||||
|
if ((value1 && !value2) || (!value1 && value2)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const type1 = typeof value1;
|
const type1 = typeof value1;
|
||||||
|
|
||||||
// Ensure types match
|
// Ensure types match
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user