mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-04 09:50:42 +02:00
Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fdc792cb82 | |||
| a16fb31e6e | |||
| 4d8a1b5b6d | |||
| c382f07b05 | |||
| 9f6a7d065c | |||
| 11aa75ecbe | |||
| 05ce9c6eda | |||
| 15aaf2863c | |||
| 019063e6f4 | |||
| ea79948122 | |||
| 7a0f27e3cc | |||
| 4f75a89633 | |||
| b3f19ef628 | |||
| f16e312319 | |||
| 056da0ef70 | |||
| 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 |
@@ -3,9 +3,6 @@ node_modules
|
|||||||
npm-debug.log
|
npm-debug.log
|
||||||
.git
|
.git
|
||||||
.gitignore
|
.gitignore
|
||||||
.devcontainer/
|
|
||||||
.github/
|
|
||||||
.vscode/
|
|
||||||
/config
|
/config
|
||||||
/audiobooks
|
/audiobooks
|
||||||
/audiobooks2
|
/audiobooks2
|
||||||
@@ -16,5 +13,4 @@ test/
|
|||||||
/client/.nuxt/
|
/client/.nuxt/
|
||||||
/client/dist/
|
/client/dist/
|
||||||
/dist/
|
/dist/
|
||||||
/build/
|
|
||||||
/deploy/
|
/deploy/
|
||||||
+6
-17
@@ -1,19 +1,14 @@
|
|||||||
### STAGE 0: Build client ###
|
### STAGE 0: Build client ###
|
||||||
FROM node:16-alpine AS build
|
FROM node:16-alpine AS build
|
||||||
WORKDIR /client
|
WORKDIR /client
|
||||||
|
|
||||||
COPY /client/package*.json /client/
|
|
||||||
RUN npm ci && npm cache clean --force
|
|
||||||
|
|
||||||
COPY /client /client
|
COPY /client /client
|
||||||
RUN npm run build
|
RUN npm ci && npm cache clean --force
|
||||||
|
RUN npm run generate
|
||||||
|
|
||||||
### STAGE 1: Build server ###
|
### STAGE 1: Build server ###
|
||||||
FROM sandreas/tone:v0.1.5 AS tone
|
FROM sandreas/tone:v0.1.5 AS tone
|
||||||
FROM node:16-alpine
|
FROM node:16-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk add --no-cache --update \
|
apk add --no-cache --update \
|
||||||
@@ -22,17 +17,11 @@ RUN apk update && \
|
|||||||
ffmpeg
|
ffmpeg
|
||||||
|
|
||||||
COPY --from=tone /usr/local/bin/tone /usr/local/bin/
|
COPY --from=tone /usr/local/bin/tone /usr/local/bin/
|
||||||
COPY package* ./
|
COPY --from=build /client/dist /client/dist
|
||||||
COPY --from=build /client/package*.json client/
|
COPY index.js package* /
|
||||||
|
|
||||||
RUN npm ci --omit=dev
|
|
||||||
RUN cd client && npm ci --omit=dev && cd ..
|
|
||||||
|
|
||||||
COPY index.js package* ./
|
|
||||||
COPY server server
|
COPY server server
|
||||||
COPY --from=build /client/nuxt.config.js* client/
|
|
||||||
COPY --from=build /client/.nuxt client/.nuxt
|
RUN npm ci --only=production
|
||||||
COPY --from=build /client/modules client/modules
|
|
||||||
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
HEALTHCHECK \
|
HEALTHCHECK \
|
||||||
|
|||||||
+16
-2
@@ -5,7 +5,7 @@
|
|||||||
@import './absicons.css';
|
@import './absicons.css';
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--bookshelf-texture-img: url(~static//textures/wood_default.jpg);
|
--bookshelf-texture-img: url(/textures/wood_default.jpg);
|
||||||
--bookshelf-divider-bg: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%);
|
--bookshelf-divider-bg: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -206,13 +211,39 @@ export default {
|
|||||||
}
|
}
|
||||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
},
|
},
|
||||||
contextMenuAction(action) {
|
contextMenuAction({ action }) {
|
||||||
if (action === 'quick-embed') {
|
if (action === 'quick-embed') {
|
||||||
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)">
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<!-- 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>
|
||||||
@@ -286,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) {
|
||||||
@@ -336,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()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -346,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') {
|
||||||
@@ -357,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) => {
|
||||||
@@ -373,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) => {
|
||||||
@@ -417,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')
|
||||||
}
|
}
|
||||||
@@ -431,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')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,8 @@
|
|||||||
|
|
||||||
<!-- issues page remove all button -->
|
<!-- issues page remove all button -->
|
||||||
<ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ numShowing }} {{ entityName }}</ui-btn>
|
<ui-btn v-if="isIssuesFilter && userCanDelete && !isBatchSelecting" :loading="processingIssues" color="error" small class="ml-4" @click="removeAllIssues">{{ $strings.ButtonRemoveAll }} {{ numShowing }} {{ entityName }}</ui-btn>
|
||||||
|
|
||||||
|
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" menu-width="110px" class="ml-2" @action="contextMenuAction" />
|
||||||
</template>
|
</template>
|
||||||
<!-- search page -->
|
<!-- search page -->
|
||||||
<template v-else-if="page === 'search'">
|
<template v-else-if="page === 'search'">
|
||||||
@@ -186,6 +188,9 @@ export default {
|
|||||||
userCanUpdate() {
|
userCanUpdate() {
|
||||||
return this.$store.getters['user/getUserCanUpdate']
|
return this.$store.getters['user/getUserCanUpdate']
|
||||||
},
|
},
|
||||||
|
userCanDownload() {
|
||||||
|
return this.$store.getters['user/getUserCanDownload']
|
||||||
|
},
|
||||||
currentLibraryId() {
|
currentLibraryId() {
|
||||||
return this.$store.state.libraries.currentLibraryId
|
return this.$store.state.libraries.currentLibraryId
|
||||||
},
|
},
|
||||||
@@ -276,10 +281,30 @@ export default {
|
|||||||
},
|
},
|
||||||
isIssuesFilter() {
|
isIssuesFilter() {
|
||||||
return this.filterBy === 'issues' && this.$route.query.filter === 'issues'
|
return this.filterBy === 'issues' && this.$route.query.filter === 'issues'
|
||||||
|
},
|
||||||
|
contextMenuItems() {
|
||||||
|
const items = []
|
||||||
|
|
||||||
|
if (this.isPodcastLibrary && this.isLibraryPage && this.userCanDownload) {
|
||||||
|
items.push({
|
||||||
|
text: 'Export OPML',
|
||||||
|
action: 'export-opml'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
seriesContextMenuAction(action) {
|
contextMenuAction({ action }) {
|
||||||
|
if (action === 'export-opml') {
|
||||||
|
this.exportOPML()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
exportOPML() {
|
||||||
|
this.$downloadFile(`/api/libraries/${this.currentLibraryId}/opml?token=${this.$store.getters['user/getToken']}`, null, true)
|
||||||
|
},
|
||||||
|
seriesContextMenuAction({ action }) {
|
||||||
if (action === 'open-rss-feed') {
|
if (action === 'open-rss-feed') {
|
||||||
this.showOpenSeriesRSSFeed()
|
this.showOpenSeriesRSSFeed()
|
||||||
} else if (action === 're-add-to-continue-listening') {
|
} else if (action === 're-add-to-continue-listening') {
|
||||||
|
|||||||
@@ -90,6 +90,11 @@ export default {
|
|||||||
title: this.$strings.HeaderNotifications,
|
title: this.$strings.HeaderNotifications,
|
||||||
path: '/config/notifications'
|
path: '/config/notifications'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'config-email',
|
||||||
|
title: this.$strings.HeaderEmail,
|
||||||
|
path: '/config/email'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'config-item-metadata-utils',
|
id: 'config-item-metadata-utils',
|
||||||
title: this.$strings.HeaderItemMetadataUtils,
|
title: this.$strings.HeaderItemMetadataUtils,
|
||||||
|
|||||||
@@ -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,7 +5,7 @@
|
|||||||
<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" />
|
||||||
@@ -61,7 +61,6 @@ 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
|
||||||
@@ -69,6 +68,7 @@ export default {
|
|||||||
|
|
||||||
if (this.matchKey === 'episode') return `<p class="truncate">${this.$strings.LabelEpisode}: ${html}</p>`
|
if (this.matchKey === 'episode') return `<p class="truncate">${this.$strings.LabelEpisode}: ${html}</p>`
|
||||||
if (this.matchKey === 'tags') return `<p class="truncate">${this.$strings.LabelTags}: ${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>`
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -482,6 +489,18 @@ export default {
|
|||||||
text: this.$strings.LabelAddToPlaylist
|
text: this.$strings.LabelAddToPlaylist
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
if (this.ebookFormat && this.store.state.libraries.ereaderDevices?.length) {
|
||||||
|
items.push({
|
||||||
|
text: this.$strings.LabelSendEbookToDevice,
|
||||||
|
subitems: this.store.state.libraries.ereaderDevices.map((d) => {
|
||||||
|
return {
|
||||||
|
text: d.name,
|
||||||
|
func: 'sendToDevice',
|
||||||
|
data: d.name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (this.userCanUpdate) {
|
if (this.userCanUpdate) {
|
||||||
items.push({
|
items.push({
|
||||||
@@ -508,7 +527,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) {
|
||||||
@@ -712,6 +731,37 @@ export default {
|
|||||||
// More menu func
|
// More menu func
|
||||||
this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'match' })
|
this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'match' })
|
||||||
},
|
},
|
||||||
|
sendToDevice(deviceName) {
|
||||||
|
// More menu func
|
||||||
|
const payload = {
|
||||||
|
// message: `Are you sure you want to send ${this.ebookFormat} ebook "${this.title}" to device "${deviceName}"?`,
|
||||||
|
message: this.$getString('MessageConfirmSendEbookToDevice', [this.ebookFormat, this.title, deviceName]),
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
const payload = {
|
||||||
|
libraryItemId: this.libraryItemId,
|
||||||
|
deviceName
|
||||||
|
}
|
||||||
|
this.processing = true
|
||||||
|
const axios = this.$axios || this.$nuxt.$axios
|
||||||
|
axios
|
||||||
|
.$post(`/api/emails/send-ebook-to-device`, payload)
|
||||||
|
.then(() => {
|
||||||
|
this.$toast.success(this.$getString('ToastSendEbookToDeviceSuccess', [deviceName]))
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to send ebook to device', error)
|
||||||
|
this.$toast.error(this.$strings.ToastSendEbookToDeviceFailed)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
removeSeriesFromContinueListening() {
|
removeSeriesFromContinueListening() {
|
||||||
const axios = this.$axios || this.$nuxt.$axios
|
const axios = this.$axios || this.$nuxt.$axios
|
||||||
this.processing = true
|
this.processing = true
|
||||||
@@ -825,8 +875,8 @@ export default {
|
|||||||
items: this.moreMenuItems
|
items: this.moreMenuItems
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.$on('action', (func) => {
|
this.$on('action', (action) => {
|
||||||
if (_this[func]) _this[func]()
|
if (action.func && _this[action.func]) _this[action.func](action.data)
|
||||||
})
|
})
|
||||||
this.$on('close', () => {
|
this.$on('close', () => {
|
||||||
_this.isMoreMenuOpen = false
|
_this.isMoreMenuOpen = false
|
||||||
@@ -865,7 +915,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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf?filter=narrators.${$encode(narrator.name)}`">
|
<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 :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-20">
|
<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-8xl">record_voice_over</span>
|
<span class="material-icons-outlined text-[10rem]">record_voice_over</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Narrator name & num books overlay -->
|
<!-- Narrator name & num books overlay -->
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -197,8 +197,8 @@ export default {
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style scoped>
|
||||||
.globalSearchMenu {
|
.globalSearchMenu {
|
||||||
max-height: 80vh;
|
max-height: calc(100vh - 75px);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -14,12 +14,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
<div v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-sm libraryFilterMenu">
|
||||||
<ul v-show="!sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
<ul v-show="!sublist" class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-for="item in selectItems">
|
<template v-for="item in selectItems">
|
||||||
<li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="item.value === selected ? 'bg-primary bg-opacity-50' : ''" role="option" @click="clickedOption(item)">
|
<li :key="item.value" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="item.value === selected ? 'bg-primary bg-opacity-50' : ''" role="option" @click="clickedOption(item)">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="font-normal ml-3 block truncate text-sm md:text-base">{{ item.text }}</span>
|
<span class="font-normal ml-3 block truncate text-sm">{{ item.text }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="item.sublist" class="absolute right-1 top-0 bottom-0 h-full flex items-center">
|
<div v-if="item.sublist" class="absolute right-1 top-0 bottom-0 h-full flex items-center">
|
||||||
<span class="material-icons text-2xl">arrow_right</span>
|
<span class="material-icons text-2xl">arrow_right</span>
|
||||||
@@ -185,6 +185,11 @@ export default {
|
|||||||
value: 'tracks',
|
value: 'tracks',
|
||||||
sublist: true
|
sublist: true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelEbook,
|
||||||
|
value: 'ebook',
|
||||||
|
sublist: false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.LabelAbridged,
|
text: this.$strings.LabelAbridged,
|
||||||
value: 'abridged',
|
value: 'abridged',
|
||||||
@@ -440,3 +445,9 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.libraryFilterMenu {
|
||||||
|
max-height: calc(100vh - 125px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
<template>
|
||||||
|
<modals-modal ref="modal" v-model="show" name="ereader-device-edit" :width="800" :height="'unset'" :processing="processing">
|
||||||
|
<template #outer>
|
||||||
|
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||||
|
<p class="text-3xl text-white truncate">{{ title }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<form @submit.prevent="submitForm">
|
||||||
|
<div class="w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300">
|
||||||
|
<div class="w-full px-3 py-5 md:p-12">
|
||||||
|
<div class="flex items-center -mx-1 mb-2">
|
||||||
|
<div class="w-full md:w-1/2 px-1">
|
||||||
|
<ui-text-input-with-label ref="ereaderNameInput" v-model="newDevice.name" :disabled="processing" :label="$strings.LabelName" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full md:w-1/2 px-1">
|
||||||
|
<ui-text-input-with-label ref="ereaderEmailInput" v-model="newDevice.email" :disabled="processing" :label="$strings.LabelEmail" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center pt-4">
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</modals-modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: Boolean,
|
||||||
|
existingDevices: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
ereaderDevice: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
processing: false,
|
||||||
|
newDevice: {
|
||||||
|
name: '',
|
||||||
|
email: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show: {
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.value
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('input', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title() {
|
||||||
|
return this.ereaderDevice ? 'Create Device' : 'Update Device'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
submitForm() {
|
||||||
|
this.$refs.ereaderNameInput.blur()
|
||||||
|
this.$refs.ereaderEmailInput.blur()
|
||||||
|
|
||||||
|
if (!this.newDevice.name?.trim() || !this.newDevice.email?.trim()) {
|
||||||
|
this.$toast.error('Name and email required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.newDevice.name = this.newDevice.name.trim()
|
||||||
|
this.newDevice.email = this.newDevice.email.trim()
|
||||||
|
|
||||||
|
if (!this.ereaderDevice) {
|
||||||
|
if (this.existingDevices.some((d) => d.name === this.newDevice.name)) {
|
||||||
|
this.$toast.error('EReader device with that name already exists')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submitCreate()
|
||||||
|
} else {
|
||||||
|
if (this.ereaderDevice.name !== this.newDevice.name && this.existingDevices.some((d) => d.name === this.newDevice.name)) {
|
||||||
|
this.$toast.error('EReader device with that name already exists')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.submitUpdate()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
submitUpdate() {
|
||||||
|
this.processing = true
|
||||||
|
|
||||||
|
const existingDevicesWithoutThisOne = this.existingDevices.filter((d) => d.name !== this.ereaderDevice.name)
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
ereaderDevices: [
|
||||||
|
...existingDevicesWithoutThisOne,
|
||||||
|
{
|
||||||
|
...this.newDevice
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$axios
|
||||||
|
.$post(`/api/emails/ereader-devices`, payload)
|
||||||
|
.then((data) => {
|
||||||
|
this.$emit('update', data.ereaderDevices)
|
||||||
|
this.$toast.success('Device updated')
|
||||||
|
this.show = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to update device', error)
|
||||||
|
this.$toast.error('Failed to update device')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submitCreate() {
|
||||||
|
this.processing = true
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
ereaderDevices: [
|
||||||
|
...this.existingDevices,
|
||||||
|
{
|
||||||
|
...this.newDevice
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$axios
|
||||||
|
.$post('/api/emails/ereader-devices', payload)
|
||||||
|
.then((data) => {
|
||||||
|
this.$emit('update', data.ereaderDevices || [])
|
||||||
|
this.$toast.success('Device added')
|
||||||
|
this.show = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to add device', error)
|
||||||
|
this.$toast.error('Failed to add device')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
if (this.ereaderDevice) {
|
||||||
|
this.newDevice.name = this.ereaderDevice.name
|
||||||
|
this.newDevice.email = this.ereaderDevice.email
|
||||||
|
} else {
|
||||||
|
this.newDevice.name = ''
|
||||||
|
this.newDevice.email = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -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" />
|
||||||
@@ -36,10 +36,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="showLocalCovers" class="flex items-center justify-center pb-2">
|
<div v-if="showLocalCovers" class="flex items-center justify-center pb-2">
|
||||||
<template v-for="cover in localCovers">
|
<template v-for="localCoverFile in localCovers">
|
||||||
<div :key="cover.path" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.metadata.path === coverPath ? 'border-yellow-300' : ''" @click="setCover(cover)">
|
<div :key="localCoverFile.ino" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="localCoverFile.metadata.path === coverPath ? 'border-yellow-300' : ''" @click="setCover(localCoverFile)">
|
||||||
<div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }">
|
<div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }">
|
||||||
<covers-preview-cover :src="`${cover.localPath}?token=${userToken}`" :width="96 / bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-preview-cover :src="localCoverFile.localPath" :width="96 / bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -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, ...this.$store.state.scanners.coverOnlyProviders]
|
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
|
||||||
@@ -169,8 +169,8 @@ export default {
|
|||||||
return this.libraryFiles
|
return this.libraryFiles
|
||||||
.filter((f) => f.fileType === 'image')
|
.filter((f) => f.fileType === 'image')
|
||||||
.map((file) => {
|
.map((file) => {
|
||||||
var _file = { ...file }
|
const _file = { ...file }
|
||||||
_file.localPath = `${process.env.serverUrl}/s/item/${this.libraryItemId}/${this.$encodeUriPath(file.metadata.relPath).replace(/^\//, '')}`
|
_file.localPath = `${process.env.serverUrl}/api/items/${this.libraryItemId}/file/${file.ino}?token=${this.userToken}`
|
||||||
return _file
|
return _file
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,11 @@ Archive.init({
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
url: String
|
libraryItem: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
playerOpen: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -87,6 +87,15 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
userToken() {
|
||||||
|
return this.$store.getters['user/getToken']
|
||||||
|
},
|
||||||
|
libraryItemId() {
|
||||||
|
return this.libraryItem?.id
|
||||||
|
},
|
||||||
|
ebookUrl() {
|
||||||
|
return `/api/items/${this.libraryItemId}/ebook`
|
||||||
|
},
|
||||||
comicMetadataKeys() {
|
comicMetadataKeys() {
|
||||||
return this.comicMetadata ? Object.keys(this.comicMetadata) : []
|
return this.comicMetadata ? Object.keys(this.comicMetadata) : []
|
||||||
},
|
},
|
||||||
@@ -145,10 +154,11 @@ export default {
|
|||||||
},
|
},
|
||||||
async extract() {
|
async extract() {
|
||||||
this.loading = true
|
this.loading = true
|
||||||
console.log('Extracting', this.url)
|
var buff = await this.$axios.$get(this.ebookUrl, {
|
||||||
|
responseType: 'blob',
|
||||||
var buff = await this.$axios.$get(this.url, {
|
headers: {
|
||||||
responseType: 'blob'
|
Authorization: `Bearer ${this.userToken}`
|
||||||
|
}
|
||||||
})
|
})
|
||||||
const archive = await Archive.open(buff)
|
const archive = await Archive.open(buff)
|
||||||
const originalFilesObject = await archive.getFilesObject()
|
const originalFilesObject = await archive.getFilesObject()
|
||||||
@@ -249,15 +259,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>
|
||||||
@@ -24,22 +24,31 @@ import ePub from 'epubjs'
|
|||||||
*/
|
*/
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
url: String,
|
|
||||||
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: {
|
||||||
|
userToken() {
|
||||||
|
return this.$store.getters['user/getToken']
|
||||||
|
},
|
||||||
/** @returns {string} */
|
/** @returns {string} */
|
||||||
libraryItemId() {
|
libraryItemId() {
|
||||||
return this.libraryItem?.id
|
return this.libraryItem?.id
|
||||||
@@ -64,6 +73,13 @@ 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
|
||||||
|
},
|
||||||
|
epubUrl() {
|
||||||
|
return `/api/items/${this.libraryItemId}/ebook`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -201,22 +217,26 @@ export default {
|
|||||||
const reader = this
|
const reader = this
|
||||||
|
|
||||||
/** @type {ePub.Book} */
|
/** @type {ePub.Book} */
|
||||||
reader.book = new ePub(reader.url, {
|
reader.book = new ePub(reader.epubUrl, {
|
||||||
width: this.readerWidth,
|
width: this.readerWidth,
|
||||||
height: window.innerHeight - 50
|
height: this.readerHeight - 50,
|
||||||
|
openAs: 'epub',
|
||||||
|
requestHeaders: {
|
||||||
|
Authorization: `Bearer ${this.userToken}`
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
/** @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
|
||||||
reader.rendition.display(this.userMediaProgress?.ebookLocation || reader.book.locations.start)
|
reader.rendition.display(this.userMediaProgress?.ebookLocation || reader.book.locations.start)
|
||||||
|
|
||||||
// load style
|
// load style
|
||||||
reader.rendition.themes.default({ '*': { color: '#fff!important' } })
|
reader.rendition.themes.default({ '*': { color: '#fff!important', 'background-color': 'rgb(35 35 35)!important' } })
|
||||||
|
|
||||||
reader.book.ready.then(() => {
|
reader.book.ready.then(() => {
|
||||||
// set up event listeners
|
// set up event listeners
|
||||||
@@ -253,17 +273,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,12 +15,26 @@ import defaultCss from '@/assets/ebooks/basic.js'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
url: String
|
libraryItem: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
playerOpen: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
computed: {},
|
computed: {
|
||||||
|
userToken() {
|
||||||
|
return this.$store.getters['user/getToken']
|
||||||
|
},
|
||||||
|
libraryItemId() {
|
||||||
|
return this.libraryItem?.id
|
||||||
|
},
|
||||||
|
ebookUrl() {
|
||||||
|
return `/api/items/${this.libraryItemId}/ebook`
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addHtmlCss() {
|
addHtmlCss() {
|
||||||
let iframe = document.getElementsByTagName('iframe')[0]
|
let iframe = document.getElementsByTagName('iframe')[0]
|
||||||
@@ -78,8 +92,11 @@ export default {
|
|||||||
},
|
},
|
||||||
async initMobi() {
|
async initMobi() {
|
||||||
// Fetch mobi file as blob
|
// Fetch mobi file as blob
|
||||||
var buff = await this.$axios.$get(this.url, {
|
var buff = await this.$axios.$get(this.ebookUrl, {
|
||||||
responseType: 'blob'
|
responseType: 'blob',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.userToken}`
|
||||||
|
}
|
||||||
})
|
})
|
||||||
var reader = new FileReader()
|
var reader = new FileReader()
|
||||||
reader.onload = async (event) => {
|
reader.onload = async (event) => {
|
||||||
|
|||||||
@@ -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="pdfDocInitParams" :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,24 @@
|
|||||||
</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
|
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 +59,110 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
userToken() {
|
||||||
|
return this.$store.getters['user/getToken']
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
},
|
||||||
|
pdfDocInitParams() {
|
||||||
|
return {
|
||||||
|
url: `/api/items/${this.libraryItemId}/ebook`,
|
||||||
|
httpHeaders: {
|
||||||
|
Authorization: `Bearer ${this.userToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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" :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
|
||||||
},
|
},
|
||||||
@@ -120,21 +128,6 @@ export default {
|
|||||||
isComic() {
|
isComic() {
|
||||||
return this.ebookFormat == 'cbz' || this.ebookFormat == 'cbr'
|
return this.ebookFormat == 'cbz' || this.ebookFormat == 'cbr'
|
||||||
},
|
},
|
||||||
ebookUrl() {
|
|
||||||
if (!this.ebookFile) return null
|
|
||||||
let filepath = ''
|
|
||||||
if (this.selectedLibraryItem.isFile) {
|
|
||||||
filepath = this.$encodeUriPath(this.ebookFile.metadata.filename)
|
|
||||||
} else {
|
|
||||||
const itemRelPath = this.selectedLibraryItem.relPath
|
|
||||||
if (itemRelPath.startsWith('/')) itemRelPath = itemRelPath.slice(1)
|
|
||||||
const relPath = this.ebookFile.metadata.relPath
|
|
||||||
if (relPath.startsWith('/')) relPath = relPath.slice(1)
|
|
||||||
|
|
||||||
filepath = this.$encodeUriPath(`${itemRelPath}/${relPath}`)
|
|
||||||
}
|
|
||||||
return `/ebook/${this.libraryId}/${this.folderId}/${filepath}`
|
|
||||||
},
|
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
}
|
}
|
||||||
@@ -146,7 +139,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 +179,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>
|
||||||
@@ -73,11 +73,11 @@ export default {
|
|||||||
return items
|
return items
|
||||||
},
|
},
|
||||||
downloadUrl() {
|
downloadUrl() {
|
||||||
return `${process.env.serverUrl}/s/item/${this.libraryItemId}/${this.$encodeUriPath(this.track.metadata.relPath).replace(/^\//, '')}?token=${this.userToken}`
|
return `${process.env.serverUrl}/api/items/${this.libraryItemId}/file/${this.track.audioFile.ino}/download?token=${this.userToken}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
contextMenuAction(action) {
|
contextMenuAction({ action }) {
|
||||||
if (action === 'delete') {
|
if (action === 'delete') {
|
||||||
this.deleteLibraryFile()
|
this.deleteLibraryFile()
|
||||||
} else if (action === 'download') {
|
} else if (action === 'download') {
|
||||||
@@ -107,15 +107,7 @@ export default {
|
|||||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
},
|
},
|
||||||
downloadLibraryFile() {
|
downloadLibraryFile() {
|
||||||
const a = document.createElement('a')
|
this.$downloadFile(this.downloadUrl, this.track.metadata.filename)
|
||||||
a.style.display = 'none'
|
|
||||||
a.href = this.downloadUrl
|
|
||||||
a.download = this.track.metadata.filename
|
|
||||||
document.body.appendChild(a)
|
|
||||||
a.click()
|
|
||||||
setTimeout(() => {
|
|
||||||
a.remove()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export default {
|
|||||||
return this.$store.getters['user/getIsAdminOrUp']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
downloadUrl() {
|
downloadUrl() {
|
||||||
return `${process.env.serverUrl}/s/item/${this.libraryItemId}/${this.$encodeUriPath(this.file.metadata.relPath).replace(/^\//, '')}?token=${this.userToken}`
|
return `${process.env.serverUrl}/api/items/${this.libraryItemId}/file/${this.file.ino}/download?token=${this.userToken}`
|
||||||
},
|
},
|
||||||
contextMenuItems() {
|
contextMenuItems() {
|
||||||
const items = []
|
const items = []
|
||||||
@@ -72,7 +72,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
contextMenuAction(action) {
|
contextMenuAction({ action }) {
|
||||||
if (action === 'delete') {
|
if (action === 'delete') {
|
||||||
this.deleteLibraryFile()
|
this.deleteLibraryFile()
|
||||||
} else if (action === 'download') {
|
} else if (action === 'download') {
|
||||||
@@ -102,15 +102,7 @@ export default {
|
|||||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
},
|
},
|
||||||
downloadLibraryFile() {
|
downloadLibraryFile() {
|
||||||
const a = document.createElement('a')
|
this.$downloadFile(this.downloadUrl, this.file.metadata.filename)
|
||||||
a.style.display = 'none'
|
|
||||||
a.href = this.downloadUrl
|
|
||||||
a.download = this.file.metadata.filename
|
|
||||||
document.body.appendChild(a)
|
|
||||||
a.click()
|
|
||||||
setTimeout(() => {
|
|
||||||
a.remove()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
contextMenuAction(action) {
|
contextMenuAction({ action }) {
|
||||||
this.showMobileMenu = false
|
this.showMobileMenu = false
|
||||||
if (action === 'edit') {
|
if (action === 'edit') {
|
||||||
this.editClick()
|
this.editClick()
|
||||||
|
|||||||
@@ -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 || ''
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ export default {
|
|||||||
this.searchText = this.search.toLowerCase().trim()
|
this.searchText = this.search.toLowerCase().trim()
|
||||||
}, 500)
|
}, 500)
|
||||||
},
|
},
|
||||||
contextMenuAction(action) {
|
contextMenuAction({ action }) {
|
||||||
if (action === 'quick-match-episodes') {
|
if (action === 'quick-match-episodes') {
|
||||||
if (this.quickMatchingEpisodes) return
|
if (this.quickMatchingEpisodes) return
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,19 @@
|
|||||||
</slot>
|
</slot>
|
||||||
|
|
||||||
<transition name="menu">
|
<transition name="menu">
|
||||||
<div v-show="showMenu" class="absolute right-0 mt-1 z-10 bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 overflow-auto focus:outline-none sm:text-sm" :style="{ width: menuWidth }">
|
<div v-show="showMenu" class="absolute right-0 mt-1 z-10 bg-bg border border-black-200 shadow-lg rounded-md py-1 focus:outline-none sm:text-sm" :style="{ width: menuWidth }">
|
||||||
<template v-for="(item, index) in items">
|
<template v-for="(item, index) in items">
|
||||||
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @click.stop="clickAction(item.action)">
|
<template v-if="item.subitems">
|
||||||
|
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-default" @mouseover="mouseoverItem(index)" @mouseleave="mouseleaveItem(index)" @click.stop>
|
||||||
|
<p>{{ item.text }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="mouseoverItemIndex === index" :key="`subitems-${index}`" @mouseover="mouseoverSubItemMenu(index)" @mouseleave="mouseleaveSubItemMenu(index)" class="absolute w-36 bg-bg rounded-md border border-black-200 shadow-lg z-50 -ml-px" :style="{ left: menuWidth, top: index * 29 + 'px' }">
|
||||||
|
<div v-for="(subitem, subitemindex) in item.subitems" :key="`subitem-${subitemindex}`" class="flex items-center px-2 py-1.5 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @click.stop="clickAction(subitem.action, subitem.data)">
|
||||||
|
<p>{{ subitem.text }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else :key="index" class="flex items-center px-2 py-1.5 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @click.stop="clickAction(item.action)">
|
||||||
<p>{{ item.text }}</p>
|
<p>{{ item.text }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -42,11 +52,31 @@ export default {
|
|||||||
events: ['mousedown'],
|
events: ['mousedown'],
|
||||||
isActive: true
|
isActive: true
|
||||||
},
|
},
|
||||||
showMenu: false
|
showMenu: false,
|
||||||
|
mouseoverItemIndex: null,
|
||||||
|
isOverSubItemMenu: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {},
|
computed: {},
|
||||||
methods: {
|
methods: {
|
||||||
|
mouseoverSubItemMenu(index) {
|
||||||
|
this.isOverSubItemMenu = true
|
||||||
|
},
|
||||||
|
mouseleaveSubItemMenu(index) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.isOverSubItemMenu && this.mouseoverItemIndex === index) this.mouseoverItemIndex = null
|
||||||
|
}, 1)
|
||||||
|
},
|
||||||
|
mouseoverItem(index) {
|
||||||
|
this.isOverSubItemMenu = false
|
||||||
|
this.mouseoverItemIndex = index
|
||||||
|
},
|
||||||
|
mouseleaveItem(index) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.isOverSubItemMenu) return
|
||||||
|
if (this.mouseoverItemIndex === index) this.mouseoverItemIndex = null
|
||||||
|
}, 1)
|
||||||
|
},
|
||||||
clickShowMenu() {
|
clickShowMenu() {
|
||||||
if (this.disabled) return
|
if (this.disabled) return
|
||||||
this.showMenu = !this.showMenu
|
this.showMenu = !this.showMenu
|
||||||
@@ -54,10 +84,10 @@ export default {
|
|||||||
clickedOutside() {
|
clickedOutside() {
|
||||||
this.showMenu = false
|
this.showMenu = false
|
||||||
},
|
},
|
||||||
clickAction(action) {
|
clickAction(action, data) {
|
||||||
if (this.disabled) return
|
if (this.disabled) return
|
||||||
this.showMenu = false
|
this.showMenu = false
|
||||||
this.$emit('action', action)
|
this.$emit('action', { action, data })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="relative" v-click-outside="clickOutside">
|
|
||||||
<button type="button" class="relative w-full bg-fg border border-gray-500 rounded shadow-sm pl-3 pr-10 py-2 text-left focus:outline-none sm:text-sm cursor-pointer" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="showMenu = !showMenu">
|
|
||||||
<span class="flex items-center">
|
|
||||||
<span class="block truncate">{{ label }}</span>
|
|
||||||
</span>
|
|
||||||
<span class="ml-3 absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
|
||||||
<span class="material-icons text-2xl text-gray-100" aria-label="User Account" role="button">person</span>
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<transition name="menu">
|
|
||||||
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox" aria-activedescendant="listbox-option-3">
|
|
||||||
<template v-for="item in items">
|
|
||||||
<nuxt-link :key="item.value" v-if="item.to" :to="item.to">
|
|
||||||
<li :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="font-normal ml-3 block truncate font-sans">{{ item.text }}</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</nuxt-link>
|
|
||||||
<li v-else :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="font-normal ml-3 block truncate font-sans">{{ item.text }}</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
</ul>
|
|
||||||
</transition>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
label: {
|
|
||||||
type: String,
|
|
||||||
default: 'Menu'
|
|
||||||
},
|
|
||||||
items: {
|
|
||||||
type: Array,
|
|
||||||
default: () => []
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
showMenu: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
clickOutside() {
|
|
||||||
this.showMenu = false
|
|
||||||
},
|
|
||||||
clickedOption(itemValue) {
|
|
||||||
this.$emit('action', itemValue)
|
|
||||||
this.showMenu = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted() {}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,7 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="absolute w-36 bg-bg rounded-md border border-black-200 shadow-lg z-50" v-click-outside="clickOutsideObj" style="top: 0; left: 0">
|
<div class="absolute w-36 bg-bg rounded-md border border-black-200 shadow-lg z-50" v-click-outside="clickOutsideObj" style="top: 0; left: 0">
|
||||||
<template v-for="(item, index) in items">
|
<template v-for="(item, index) in items">
|
||||||
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @click.stop="clickAction(item.func)">
|
<template v-if="item.subitems">
|
||||||
|
<div :key="index" class="flex items-center px-2 py-1.5 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-default" @mouseover="mouseoverItem(index)" @mouseleave="mouseleaveItem(index)" @click.stop>
|
||||||
|
<p>{{ item.text }}</p>
|
||||||
|
</div>
|
||||||
|
<div v-if="mouseoverItemIndex === index" :key="`subitems-${index}`" @mouseover="mouseoverSubItemMenu(index)" @mouseleave="mouseleaveSubItemMenu(index)" class="absolute w-36 bg-bg rounded-md border border-black-200 shadow-lg z-50" :style="{ left: 143 + 'px', top: index * 28 + 'px' }">
|
||||||
|
<div v-for="(subitem, subitemindex) in item.subitems" :key="`subitem-${subitemindex}`" class="flex items-center px-2 py-1.5 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @click.stop="clickAction(subitem.func, subitem.data)">
|
||||||
|
<p>{{ subitem.text }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else :key="index" class="flex items-center px-2 py-1.5 hover:bg-white hover:bg-opacity-5 text-white text-xs cursor-pointer" @mouseover="mouseoverItem(index)" @mouseleave="mouseleaveItem(index)" @click.stop="clickAction(item.func)">
|
||||||
<p>{{ item.text }}</p>
|
<p>{{ item.text }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -22,13 +32,36 @@ export default {
|
|||||||
handler: this.clickedOutside,
|
handler: this.clickedOutside,
|
||||||
events: ['mousedown'],
|
events: ['mousedown'],
|
||||||
isActive: true
|
isActive: true
|
||||||
}
|
},
|
||||||
|
mouseoverItemIndex: null,
|
||||||
|
isOverSubItemMenu: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {},
|
computed: {},
|
||||||
methods: {
|
methods: {
|
||||||
clickAction(func) {
|
mouseoverSubItemMenu(index) {
|
||||||
this.$emit('action', func)
|
this.isOverSubItemMenu = true
|
||||||
|
},
|
||||||
|
mouseleaveSubItemMenu(index) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.isOverSubItemMenu && this.mouseoverItemIndex === index) this.mouseoverItemIndex = null
|
||||||
|
}, 1)
|
||||||
|
},
|
||||||
|
mouseoverItem(index) {
|
||||||
|
this.isOverSubItemMenu = false
|
||||||
|
this.mouseoverItemIndex = index
|
||||||
|
},
|
||||||
|
mouseleaveItem(index) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (this.isOverSubItemMenu) return
|
||||||
|
if (this.mouseoverItemIndex === index) this.mouseoverItemIndex = null
|
||||||
|
}, 1)
|
||||||
|
},
|
||||||
|
clickAction(func, data) {
|
||||||
|
this.$emit('action', {
|
||||||
|
func,
|
||||||
|
data
|
||||||
|
})
|
||||||
this.close()
|
this.close()
|
||||||
},
|
},
|
||||||
clickedOutside(e) {
|
clickedOutside(e) {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -365,6 +380,11 @@ export default {
|
|||||||
adminMessageEvt(message) {
|
adminMessageEvt(message) {
|
||||||
this.$toast.info(message)
|
this.$toast.info(message)
|
||||||
},
|
},
|
||||||
|
ereaderDevicesUpdated(data) {
|
||||||
|
if (!data?.ereaderDevices) return
|
||||||
|
|
||||||
|
this.$store.commit('libraries/setEReaderDevices', data.ereaderDevices)
|
||||||
|
},
|
||||||
initializeSocket() {
|
initializeSocket() {
|
||||||
this.socket = this.$nuxtSocket({
|
this.socket = this.$nuxtSocket({
|
||||||
name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
|
name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
|
||||||
@@ -437,6 +457,9 @@ export default {
|
|||||||
this.socket.on('task_finished', this.taskFinished)
|
this.socket.on('task_finished', this.taskFinished)
|
||||||
this.socket.on('metadata_embed_queue_update', this.metadataEmbedQueueUpdate)
|
this.socket.on('metadata_embed_queue_update', this.metadataEmbedQueueUpdate)
|
||||||
|
|
||||||
|
// EReader Device Listeners
|
||||||
|
this.socket.on('ereader-devices-updated', this.ereaderDevicesUpdated)
|
||||||
|
|
||||||
this.socket.on('backup_applied', this.backupApplied)
|
this.socket.on('backup_applied', this.backupApplied)
|
||||||
|
|
||||||
this.socket.on('batch_quickmatch_complete', this.batchQuickMatchComplete)
|
this.socket.on('batch_quickmatch_complete', this.batchQuickMatchComplete)
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
const Path = require('path')
|
|
||||||
const { promises: { readFile, writeFile } } = require('fs')
|
|
||||||
const { glob } = require('glob')
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rewrite PWA manifest paths to include router base path
|
|
||||||
* @nuxtjs/pwa module does not support a dynamic router base path,
|
|
||||||
* so we have to rewrite the manifest paths manually
|
|
||||||
*
|
|
||||||
* @see https://github.com/nuxt/nuxt/pull/8520
|
|
||||||
* @see https://github.com/nuxt-community/pwa-module/issues/435
|
|
||||||
*/
|
|
||||||
export default function rewritePwaManifest () {
|
|
||||||
this.nuxt.hook('ready', async ({ options }) => {
|
|
||||||
// Do not run on build, this is where the manifest is generated
|
|
||||||
if (options._build) return
|
|
||||||
|
|
||||||
const routerBasePath = options.router.base
|
|
||||||
const clientDir = Path.join(options.buildDir, 'dist/client')
|
|
||||||
let rewritten = false
|
|
||||||
|
|
||||||
// Find manifest file generated to the build directory
|
|
||||||
const manifestPaths = await glob('manifest.*.json', { cwd: clientDir })
|
|
||||||
if (manifestPaths.length === 0) {
|
|
||||||
console.warn(`[PWA] No manifest not found under ${clientDir}/manifest.*.json`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rewrite manifest paths for all found manifest files
|
|
||||||
for (const manifestPath of manifestPaths) {
|
|
||||||
const manifestJson = await readFile(Path.join(clientDir, manifestPath), 'utf8')
|
|
||||||
const manifest = JSON.parse(manifestJson)
|
|
||||||
|
|
||||||
const currentBasePath = manifest.start_url.split('?')[0]
|
|
||||||
|
|
||||||
if (currentBasePath !== routerBasePath) {
|
|
||||||
// Rewrite start_url and icons paths
|
|
||||||
manifest.start_url = `${routerBasePath || '/'}${manifest.start_url.slice(currentBasePath.length)}`
|
|
||||||
for (const icon of manifest.icons) {
|
|
||||||
icon.src = currentBasePath.startsWith('.')
|
|
||||||
? `${routerBasePath}${icon.src}` // Initially, the start_url is `./`
|
|
||||||
: `${routerBasePath}/${icon.src.slice(currentBasePath.length)}`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update manifest file
|
|
||||||
await writeFile(Path.join(clientDir, manifestPath), JSON.stringify(manifest), 'utf8')
|
|
||||||
rewritten = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rewritten) {
|
|
||||||
console.info('[PWA] Manifest paths rewritten')
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
+14
-25
@@ -1,9 +1,9 @@
|
|||||||
const pkg = require('./package.json')
|
const pkg = require('./package.json')
|
||||||
|
|
||||||
module.exports = ({ command }) => ({
|
module.exports = {
|
||||||
// Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode
|
// Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode
|
||||||
ssr: false,
|
ssr: false,
|
||||||
target: 'server',
|
target: 'static',
|
||||||
dev: process.env.NODE_ENV !== 'production',
|
dev: process.env.NODE_ENV !== 'production',
|
||||||
env: {
|
env: {
|
||||||
serverUrl: process.env.NODE_ENV === 'production' ? process.env.ROUTER_BASE_PATH || '' : 'http://localhost:3333',
|
serverUrl: process.env.NODE_ENV === 'production' ? process.env.ROUTER_BASE_PATH || '' : 'http://localhost:3333',
|
||||||
@@ -26,12 +26,15 @@ module.exports = ({ command }) => ({
|
|||||||
{ charset: 'utf-8' },
|
{ charset: 'utf-8' },
|
||||||
{ 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: [],
|
||||||
|
link: [
|
||||||
|
{ rel: 'icon', type: 'image/x-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/favicon.ico' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
router: {
|
router: {
|
||||||
// We must specify `./` during build to support dynamic router base paths (https://github.com/nuxt/nuxt/issues/10088)
|
base: process.env.ROUTER_BASE_PATH || ''
|
||||||
base: command == 'build' ? './' : process.env.ROUTER_BASE_PATH || ''
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Global CSS: https://go.nuxtjs.dev/config-css
|
// Global CSS: https://go.nuxtjs.dev/config-css
|
||||||
@@ -39,14 +42,10 @@ module.exports = ({ command }) => ({
|
|||||||
'@/assets/app.css'
|
'@/assets/app.css'
|
||||||
],
|
],
|
||||||
|
|
||||||
favicon: '/favicon.ico',
|
|
||||||
|
|
||||||
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
|
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
|
||||||
plugins: [
|
plugins: [
|
||||||
'@/plugins/constants.js',
|
'@/plugins/constants.js',
|
||||||
'@/plugins/init.client.js',
|
'@/plugins/init.client.js',
|
||||||
'@/plugins/sortable.js',
|
|
||||||
'@/plugins/favicon.js',
|
|
||||||
'@/plugins/axios.js',
|
'@/plugins/axios.js',
|
||||||
'@/plugins/toast.js',
|
'@/plugins/toast.js',
|
||||||
'@/plugins/utils.js',
|
'@/plugins/utils.js',
|
||||||
@@ -68,21 +67,12 @@ module.exports = ({ command }) => ({
|
|||||||
modules: [
|
modules: [
|
||||||
'nuxt-socket-io',
|
'nuxt-socket-io',
|
||||||
'@nuxtjs/axios',
|
'@nuxtjs/axios',
|
||||||
'@nuxtjs/proxy',
|
'@nuxtjs/proxy'
|
||||||
'@/modules/rewritePwaManifest.js'
|
|
||||||
],
|
],
|
||||||
|
|
||||||
proxy: {
|
proxy: {
|
||||||
[`${process.env.ROUTER_BASE_PATH || ''}/dev/`]: {
|
'/s/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' },
|
||||||
target: `http://localhost:3333${process.env.ROUTER_BASE_PATH || ''}`,
|
'/api/': { target: process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '/' }
|
||||||
pathRewrite: { [`^${process.env.ROUTER_BASE_PATH || ''}/dev/`]: process.env.ROUTER_BASE_PATH || '' }
|
|
||||||
},
|
|
||||||
[`${process.env.ROUTER_BASE_PATH || ''}/ebook/`]: {
|
|
||||||
target: (process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '') + `${process.env.ROUTER_BASE_PATH || ''}/`
|
|
||||||
},
|
|
||||||
[`${process.env.ROUTER_BASE_PATH || ''}/s/`]: {
|
|
||||||
target: (process.env.NODE_ENV !== 'production' ? 'http://localhost:3333' : '') + `${process.env.ROUTER_BASE_PATH || ''}/`
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
io: {
|
io: {
|
||||||
@@ -111,18 +101,17 @@ module.exports = ({ command }) => ({
|
|||||||
nativeUI: true
|
nativeUI: true
|
||||||
},
|
},
|
||||||
manifest: {
|
manifest: {
|
||||||
publicPath: `${(process.env.ROUTER_BASE_PATH || '')}_nuxt`,
|
|
||||||
name: 'Audiobookshelf',
|
name: 'Audiobookshelf',
|
||||||
short_name: 'Audiobookshelf',
|
short_name: 'Audiobookshelf',
|
||||||
display: 'standalone',
|
display: 'standalone',
|
||||||
background_color: '#373838',
|
background_color: '#373838',
|
||||||
icons: [
|
icons: [
|
||||||
{
|
{
|
||||||
src: '/icon.svg',
|
src: (process.env.ROUTER_BASE_PATH || '') + '/icon.svg',
|
||||||
sizes: "any"
|
sizes: "any"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: '/icon64.png',
|
src: (process.env.ROUTER_BASE_PATH || '') + '/icon64.png',
|
||||||
type: "image/png",
|
type: "image/png",
|
||||||
sizes: "64x64"
|
sizes: "64x64"
|
||||||
}
|
}
|
||||||
@@ -152,7 +141,7 @@ module.exports = ({ command }) => ({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: process.env.CLIENT_PORT || 3000,
|
port: process.env.NODE_ENV === 'production' ? 80 : 3000,
|
||||||
host: '0.0.0.0'
|
host: '0.0.0.0'
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -163,4 +152,4 @@ module.exports = ({ command }) => ({
|
|||||||
* See: [Issue tracker](https://github.com/nuxt-community/tailwindcss-module/issues/480)
|
* See: [Issue tracker](https://github.com/nuxt-community/tailwindcss-module/issues/480)
|
||||||
*/
|
*/
|
||||||
devServerHandlers: [],
|
devServerHandlers: [],
|
||||||
})
|
}
|
||||||
|
|||||||
Generated
+94
-682
File diff suppressed because it is too large
Load Diff
+2
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.19",
|
"version": "2.2.22",
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -15,18 +15,17 @@
|
|||||||
"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",
|
||||||
"epubjs": "^0.3.88",
|
"epubjs": "^0.3.88",
|
||||||
"glob": "^10.2.2",
|
|
||||||
"hls.js": "^1.0.7",
|
"hls.js": "^1.0.7",
|
||||||
"libarchive.js": "^1.3.0",
|
"libarchive.js": "^1.3.0",
|
||||||
"nuxt": "^2.15.8",
|
"nuxt": "^2.15.8",
|
||||||
"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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ export default {
|
|||||||
feed: this.rssFeed
|
feed: this.rssFeed
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
contextMenuAction(action) {
|
contextMenuAction({ action }) {
|
||||||
if (action === 'delete') {
|
if (action === 'delete') {
|
||||||
this.removeClick()
|
this.removeClick()
|
||||||
} else if (action === 'create-playlist') {
|
} else if (action === 'create-playlist') {
|
||||||
|
|||||||
+2
-12
@@ -4,7 +4,7 @@
|
|||||||
<div class="configContent" :class="`page-${currentPage}`">
|
<div class="configContent" :class="`page-${currentPage}`">
|
||||||
<div v-show="isMobilePortrait" class="w-full pb-4 px-2 flex border-b border-white border-opacity-10 mb-2 cursor-pointer" @click.stop.prevent="toggleShowMore">
|
<div v-show="isMobilePortrait" class="w-full pb-4 px-2 flex border-b border-white border-opacity-10 mb-2 cursor-pointer" @click.stop.prevent="toggleShowMore">
|
||||||
<span class="material-icons text-2xl cursor-pointer">arrow_forward</span>
|
<span class="material-icons text-2xl cursor-pointer">arrow_forward</span>
|
||||||
<p class="pl-3 capitalize">{{ $strings.HeaderSettings }}</p>
|
<p class="pl-3 capitalize">{{ currentPage }}</p>
|
||||||
</div>
|
</div>
|
||||||
<nuxt-child />
|
<nuxt-child />
|
||||||
</div>
|
</div>
|
||||||
@@ -55,6 +55,7 @@ export default {
|
|||||||
else if (pageName === 'library-stats') return this.$strings.HeaderLibraryStats
|
else if (pageName === 'library-stats') return this.$strings.HeaderLibraryStats
|
||||||
else if (pageName === 'users') return this.$strings.HeaderUsers
|
else if (pageName === 'users') return this.$strings.HeaderUsers
|
||||||
else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils
|
else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils
|
||||||
|
else if (pageName === 'email') return this.$strings.HeaderEmail
|
||||||
}
|
}
|
||||||
return this.$strings.HeaderSettings
|
return this.$strings.HeaderSettings
|
||||||
}
|
}
|
||||||
@@ -79,14 +80,6 @@ export default {
|
|||||||
width: 900px;
|
width: 900px;
|
||||||
max-width: calc(100% - 176px);
|
max-width: calc(100% - 176px);
|
||||||
}
|
}
|
||||||
.configContent.page-library-stats {
|
|
||||||
width: 1200px;
|
|
||||||
}
|
|
||||||
@media (max-width: 1550px) {
|
|
||||||
.configContent.page-library-stats {
|
|
||||||
margin-left: 176px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 1240px) {
|
@media (max-width: 1240px) {
|
||||||
.configContent {
|
.configContent {
|
||||||
margin-left: 176px;
|
margin-left: 176px;
|
||||||
@@ -98,8 +91,5 @@ export default {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
.configContent.page-library-stats {
|
|
||||||
margin-left: 0px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,244 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<app-settings-content :header-text="$strings.HeaderEmailSettings" :description="''">
|
||||||
|
<form @submit.prevent="submitForm">
|
||||||
|
<div class="flex items-center -mx-1 mb-2">
|
||||||
|
<div class="w-full md:w-3/4 px-1">
|
||||||
|
<ui-text-input-with-label ref="hostInput" v-model="newSettings.host" :disabled="savingSettings" :label="$strings.LabelHost" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full md:w-1/4 px-1">
|
||||||
|
<ui-text-input-with-label ref="portInput" v-model="newSettings.port" type="number" :disabled="savingSettings" :label="$strings.LabelPort" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center mb-2 py-3">
|
||||||
|
<ui-toggle-switch labeledBy="email-settings-secure" v-model="newSettings.secure" :disabled="savingSettings" />
|
||||||
|
<ui-tooltip :text="$strings.LabelEmailSettingsSecureHelp">
|
||||||
|
<div class="pl-4 flex items-center">
|
||||||
|
<span id="email-settings-secure">{{ $strings.LabelEmailSettingsSecure }}</span>
|
||||||
|
<span class="material-icons text-lg pl-1">info_outlined</span>
|
||||||
|
</div>
|
||||||
|
</ui-tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center -mx-1 mb-2">
|
||||||
|
<div class="w-full md:w-1/2 px-1">
|
||||||
|
<ui-text-input-with-label ref="userInput" v-model="newSettings.user" :disabled="savingSettings" :label="$strings.LabelUsername" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full md:w-1/2 px-1">
|
||||||
|
<ui-text-input-with-label ref="passInput" v-model="newSettings.pass" type="password" :disabled="savingSettings" :label="$strings.LabelPassword" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center -mx-1 mb-2">
|
||||||
|
<div class="w-full md:w-1/2 px-1">
|
||||||
|
<ui-text-input-with-label ref="fromInput" v-model="newSettings.fromAddress" :disabled="savingSettings" :label="$strings.LabelEmailSettingsFromAddress" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center justify-between pt-4">
|
||||||
|
<ui-btn :loading="sendingTest" :disabled="savingSettings || !newSettings.host" type="button" @click="sendTestClick">{{ $strings.ButtonTest }}</ui-btn>
|
||||||
|
<ui-btn :loading="savingSettings" :disabled="!hasUpdates" type="submit">{{ $strings.ButtonSave }}</ui-btn>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div v-show="loading" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-25 flex items-center justify-center">
|
||||||
|
<ui-loading-indicator />
|
||||||
|
</div>
|
||||||
|
</app-settings-content>
|
||||||
|
|
||||||
|
<app-settings-content :header-text="$strings.HeaderEReaderDevices" showAddButton :description="''" @clicked="addNewDeviceClick">
|
||||||
|
<table v-if="existingEReaderDevices.length" class="tracksTable my-4">
|
||||||
|
<tr>
|
||||||
|
<th class="text-left">{{ $strings.LabelName }}</th>
|
||||||
|
<th class="text-left">{{ $strings.LabelEmail }}</th>
|
||||||
|
<th class="w-40"></th>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="device in existingEReaderDevices" :key="device.name">
|
||||||
|
<td>
|
||||||
|
<p class="text-sm md:text-base text-gray-100">{{ device.name }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="text-left">
|
||||||
|
<p class="text-sm md:text-base text-gray-100">{{ device.email }}</p>
|
||||||
|
</td>
|
||||||
|
<td class="w-40">
|
||||||
|
<div class="flex justify-end items-center h-10">
|
||||||
|
<ui-icon-btn icon="edit" borderless :size="8" icon-font-size="1.1rem" :disabled="deletingDeviceName === device.name" class="mx-1" @click="editDeviceClick(device)" />
|
||||||
|
<ui-icon-btn icon="delete" borderless :size="8" icon-font-size="1.1rem" :disabled="deletingDeviceName === device.name" @click="deleteDeviceClick(device)" />
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
<div v-else class="text-center py-4">
|
||||||
|
<p class="text-lg text-gray-100">No Devices</p>
|
||||||
|
</div>
|
||||||
|
</app-settings-content>
|
||||||
|
|
||||||
|
<modals-emails-e-reader-device-modal v-model="showEReaderDeviceModal" :existing-devices="existingEReaderDevices" :ereader-device="selectedEReaderDevice" @update="ereaderDevicesUpdated" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
savingSettings: false,
|
||||||
|
sendingTest: false,
|
||||||
|
deletingDeviceName: null,
|
||||||
|
settings: null,
|
||||||
|
newSettings: {
|
||||||
|
host: null,
|
||||||
|
port: 465,
|
||||||
|
secure: true,
|
||||||
|
user: null,
|
||||||
|
pass: null,
|
||||||
|
fromAddress: null
|
||||||
|
},
|
||||||
|
newEReaderDevice: {
|
||||||
|
name: '',
|
||||||
|
email: ''
|
||||||
|
},
|
||||||
|
selectedEReaderDevice: null,
|
||||||
|
showEReaderDeviceModal: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
hasUpdates() {
|
||||||
|
if (!this.settings) return true
|
||||||
|
for (const key in this.newSettings) {
|
||||||
|
if (key === 'ereaderDevices') continue
|
||||||
|
if (this.newSettings[key] !== this.settings[key]) return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
existingEReaderDevices() {
|
||||||
|
return this.settings?.ereaderDevices || []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
editDeviceClick(device) {
|
||||||
|
this.selectedEReaderDevice = device
|
||||||
|
this.showEReaderDeviceModal = true
|
||||||
|
},
|
||||||
|
deleteDeviceClick(device) {
|
||||||
|
const payload = {
|
||||||
|
message: `Are you sure you want to delete e-reader device "${device.name}"?`,
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.deleteDevice(device)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
|
deleteDevice(device) {
|
||||||
|
const payload = {
|
||||||
|
ereaderDevices: this.existingEReaderDevices.filter((d) => d.name !== device.name)
|
||||||
|
}
|
||||||
|
this.deletingDeviceName = device.name
|
||||||
|
this.$axios
|
||||||
|
.$patch(`/emails/ereader-devices`, payload)
|
||||||
|
.then((data) => {
|
||||||
|
this.ereaderDevicesUpdated(data.ereaderDevices)
|
||||||
|
this.$toast.success('Device deleted')
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to delete device', error)
|
||||||
|
this.$toast.error('Failed to delete device')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.deletingDeviceName = null
|
||||||
|
})
|
||||||
|
},
|
||||||
|
ereaderDevicesUpdated(ereaderDevices) {
|
||||||
|
this.settings.ereaderDevices = ereaderDevices
|
||||||
|
this.newSettings.ereaderDevices = ereaderDevices.map((d) => ({ ...d }))
|
||||||
|
},
|
||||||
|
addNewDeviceClick() {
|
||||||
|
this.selectedEReaderDevice = null
|
||||||
|
this.showEReaderDeviceModal = true
|
||||||
|
},
|
||||||
|
sendTestClick() {
|
||||||
|
this.sendingTest = true
|
||||||
|
this.$axios
|
||||||
|
.$post('/api/emails/test')
|
||||||
|
.then(() => {
|
||||||
|
this.$toast.success('Test Email Sent')
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to send test email', error)
|
||||||
|
const errorMsg = error.response.data || 'Failed to send test email'
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.sendingTest = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
validateForm() {
|
||||||
|
for (const ref of [this.$refs.hostInput, this.$refs.portInput, this.$refs.userInput, this.$refs.passInput, this.$refs.fromInput]) {
|
||||||
|
if (ref?.blur) ref.blur()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.newSettings.port) {
|
||||||
|
this.newSettings.port = Number(this.newSettings.port)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
submitForm() {
|
||||||
|
if (!this.validateForm()) return
|
||||||
|
|
||||||
|
const updatePayload = {
|
||||||
|
host: this.newSettings.host,
|
||||||
|
port: this.newSettings.port,
|
||||||
|
secure: this.newSettings.secure,
|
||||||
|
user: this.newSettings.user,
|
||||||
|
pass: this.newSettings.pass,
|
||||||
|
fromAddress: this.newSettings.fromAddress
|
||||||
|
}
|
||||||
|
this.savingSettings = true
|
||||||
|
this.$axios
|
||||||
|
.$patch('/api/emails/settings', updatePayload)
|
||||||
|
.then((data) => {
|
||||||
|
this.settings = data.settings
|
||||||
|
this.newSettings = {
|
||||||
|
...data.settings
|
||||||
|
}
|
||||||
|
this.$toast.success('Email settings updated')
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to update email settings', error)
|
||||||
|
this.$toast.error('Failed to update email settings')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.savingSettings = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.loading = true
|
||||||
|
|
||||||
|
this.$axios
|
||||||
|
.$get(`/api/emails/settings`)
|
||||||
|
.then((data) => {
|
||||||
|
this.settings = data.settings
|
||||||
|
this.newSettings = {
|
||||||
|
...this.settings
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to get email settings', error)
|
||||||
|
this.$toast.error('Failed to load email settings')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.loading = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
},
|
||||||
|
beforeDestroy() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -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>
|
||||||
|
|||||||
+49
-136
@@ -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 || []
|
||||||
},
|
},
|
||||||
@@ -555,6 +431,19 @@ export default {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.ebookFile && this.$store.state.libraries.ereaderDevices?.length) {
|
||||||
|
items.push({
|
||||||
|
text: this.$strings.LabelSendEbookToDevice,
|
||||||
|
subitems: this.$store.state.libraries.ereaderDevices.map((d) => {
|
||||||
|
return {
|
||||||
|
text: d.name,
|
||||||
|
action: 'sendToDevice',
|
||||||
|
data: d.name
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (this.userCanDelete) {
|
if (this.userCanDelete) {
|
||||||
items.push({
|
items.push({
|
||||||
text: this.$strings.ButtonDelete,
|
text: this.$strings.ButtonDelete,
|
||||||
@@ -726,10 +615,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`)
|
||||||
@@ -800,14 +690,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
downloadLibraryItem() {
|
downloadLibraryItem() {
|
||||||
const a = document.createElement('a')
|
this.$downloadFile(this.downloadUrl)
|
||||||
a.style.display = 'none'
|
|
||||||
a.href = this.downloadUrl
|
|
||||||
document.body.appendChild(a)
|
|
||||||
a.click()
|
|
||||||
setTimeout(() => {
|
|
||||||
a.remove()
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
deleteLibraryItem() {
|
deleteLibraryItem() {
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -834,7 +717,35 @@ export default {
|
|||||||
}
|
}
|
||||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
},
|
},
|
||||||
contextMenuAction(action) {
|
sendToDevice(deviceName) {
|
||||||
|
const payload = {
|
||||||
|
message: this.$getString('MessageConfirmSendEbookToDevice', [this.ebookFile.ebookFormat, this.title, deviceName]),
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
const payload = {
|
||||||
|
libraryItemId: this.libraryItemId,
|
||||||
|
deviceName
|
||||||
|
}
|
||||||
|
this.processing = true
|
||||||
|
this.$axios
|
||||||
|
.$post(`/api/emails/send-ebook-to-device`, payload)
|
||||||
|
.then(() => {
|
||||||
|
this.$toast.success(this.$getString('ToastSendEbookToDeviceSuccess', [deviceName]))
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to send ebook to device', error)
|
||||||
|
this.$toast.error(this.$strings.ToastSendEbookToDeviceFailed)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
|
contextMenuAction({ action, data }) {
|
||||||
if (action === 'collections') {
|
if (action === 'collections') {
|
||||||
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
this.$store.commit('setSelectedLibraryItem', this.libraryItem)
|
||||||
this.$store.commit('globals/setShowCollectionsModal', true)
|
this.$store.commit('globals/setShowCollectionsModal', true)
|
||||||
@@ -849,6 +760,8 @@ export default {
|
|||||||
this.downloadLibraryItem()
|
this.downloadLibraryItem()
|
||||||
} else if (action === 'delete') {
|
} else if (action === 'delete') {
|
||||||
this.deleteLibraryItem()
|
this.deleteLibraryItem()
|
||||||
|
} else if (action === 'sendToDevice') {
|
||||||
|
this.sendToDevice(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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)">
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export default {
|
|||||||
const payload = {
|
const payload = {
|
||||||
newRoot: { ...this.newRoot }
|
newRoot: { ...this.newRoot }
|
||||||
}
|
}
|
||||||
var success = await this.$axios
|
const success = await this.$axios
|
||||||
.$post('/init', payload)
|
.$post('/init', payload)
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -124,9 +124,10 @@ export default {
|
|||||||
|
|
||||||
location.reload()
|
location.reload()
|
||||||
},
|
},
|
||||||
setUser({ user, userDefaultLibraryId, serverSettings, Source, feeds }) {
|
setUser({ user, userDefaultLibraryId, serverSettings, Source, ereaderDevices }) {
|
||||||
this.$store.commit('setServerSettings', serverSettings)
|
this.$store.commit('setServerSettings', serverSettings)
|
||||||
this.$store.commit('setSource', Source)
|
this.$store.commit('setSource', Source)
|
||||||
|
this.$store.commit('libraries/setEReaderDevices', ereaderDevices)
|
||||||
this.$setServerLanguageCode(serverSettings.language)
|
this.$setServerLanguageCode(serverSettings.language)
|
||||||
|
|
||||||
if (serverSettings.chromecastEnabled) {
|
if (serverSettings.chromecastEnabled) {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-11
@@ -7,20 +7,10 @@ export default function ({ $axios, store, $config }) {
|
|||||||
if (config.url.startsWith('http:') || config.url.startsWith('https:')) {
|
if (config.url.startsWith('http:') || config.url.startsWith('https:')) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var bearerToken = store.state.user.user ? store.state.user.user.token : null
|
const bearerToken = store.state.user.user?.token || null
|
||||||
if (bearerToken) {
|
if (bearerToken) {
|
||||||
config.headers.common['Authorization'] = `Bearer ${bearerToken}`
|
config.headers.common['Authorization'] = `Bearer ${bearerToken}`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
config.url = `/dev${config.url}`
|
|
||||||
console.log('Making request to ' + config.url)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log($config)
|
|
||||||
if ($config.routerBasePath) {
|
|
||||||
config.url = `${$config.routerBasePath}${config.url}`
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
$axios.onError(error => {
|
$axios.onError(error => {
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
export default function ({ $config }) {
|
|
||||||
const faviconPath = $config.favicon || '/favicon.ico'
|
|
||||||
|
|
||||||
const link = document.createElement('link')
|
|
||||||
link.rel = 'icon'
|
|
||||||
link.type = 'image/x-icon'
|
|
||||||
link.href = `${$config.routerBasePath || ''}${faviconPath}`
|
|
||||||
|
|
||||||
document.head.appendChild(link)
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
export default function ({ $config }) {
|
|
||||||
const script = document.createElement('script')
|
|
||||||
script.type = 'text/javascript'
|
|
||||||
script.src = `${$config.routerBasePath || ''}/libs/sortable.js`
|
|
||||||
|
|
||||||
document.head.appendChild(script)
|
|
||||||
}
|
|
||||||
@@ -145,6 +145,25 @@ Vue.prototype.$getNextScheduledDate = (expression) => {
|
|||||||
return interval.next().toDate()
|
return interval.next().toDate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Vue.prototype.$downloadFile = (url, filename = null, openInNewTab = false) => {
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.style.display = 'none'
|
||||||
|
a.href = url
|
||||||
|
|
||||||
|
if (filename) {
|
||||||
|
a.download = filename
|
||||||
|
}
|
||||||
|
if (openInNewTab) {
|
||||||
|
a.target = '_blank'
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(a)
|
||||||
|
a.click()
|
||||||
|
setTimeout(() => {
|
||||||
|
a.remove()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function supplant(str, subs) {
|
export function supplant(str, subs) {
|
||||||
// source: http://crockford.com/javascript/remedial.html
|
// source: http://crockford.com/javascript/remedial.html
|
||||||
return str.replace(/{([^{}]*)}/g,
|
return str.replace(/{([^{}]*)}/g,
|
||||||
|
|||||||
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
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ export const state = () => ({
|
|||||||
filterData: null,
|
filterData: null,
|
||||||
numUserPlaylists: 0,
|
numUserPlaylists: 0,
|
||||||
collections: [],
|
collections: [],
|
||||||
userPlaylists: []
|
userPlaylists: [],
|
||||||
|
ereaderDevices: []
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
@@ -339,5 +340,8 @@ export const mutations = {
|
|||||||
removeUserPlaylist(state, playlist) {
|
removeUserPlaylist(state, playlist) {
|
||||||
state.userPlaylists = state.userPlaylists.filter(p => p.id !== playlist.id)
|
state.userPlaylists = state.userPlaylists.filter(p => p.id !== playlist.id)
|
||||||
state.numUserPlaylists = state.userPlaylists.length
|
state.numUserPlaylists = state.userPlaylists.length
|
||||||
|
},
|
||||||
|
setEReaderDevices(state, ereaderDevices) {
|
||||||
|
state.ereaderDevices = ereaderDevices
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ export const getters = {
|
|||||||
getIsRoot: (state) => state.user && state.user.type === 'root',
|
getIsRoot: (state) => state.user && state.user.type === 'root',
|
||||||
getIsAdminOrUp: (state) => state.user && (state.user.type === 'admin' || state.user.type === 'root'),
|
getIsAdminOrUp: (state) => state.user && (state.user.type === 'admin' || state.user.type === 'root'),
|
||||||
getToken: (state) => {
|
getToken: (state) => {
|
||||||
return state.user ? state.user.token : null
|
return state.user?.token || null
|
||||||
},
|
},
|
||||||
getUserMediaProgress: (state) => (libraryItemId, episodeId = null) => {
|
getUserMediaProgress: (state) => (libraryItemId, episodeId = null) => {
|
||||||
if (!state.user.mediaProgress) return null
|
if (!state.user.mediaProgress) return null
|
||||||
|
|||||||
+24
-5
@@ -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",
|
||||||
@@ -73,6 +74,7 @@
|
|||||||
"ButtonStartM4BEncode": "M4B-Kodierung starten",
|
"ButtonStartM4BEncode": "M4B-Kodierung starten",
|
||||||
"ButtonStartMetadataEmbed": "Metadateneinbettung starten",
|
"ButtonStartMetadataEmbed": "Metadateneinbettung starten",
|
||||||
"ButtonSubmit": "Ok",
|
"ButtonSubmit": "Ok",
|
||||||
|
"ButtonTest": "Test",
|
||||||
"ButtonUpload": "Hochladen",
|
"ButtonUpload": "Hochladen",
|
||||||
"ButtonUploadBackup": "Sicherung hochladen",
|
"ButtonUploadBackup": "Sicherung hochladen",
|
||||||
"ButtonUploadCover": "Titelbild hochladen",
|
"ButtonUploadCover": "Titelbild hochladen",
|
||||||
@@ -96,7 +98,10 @@
|
|||||||
"HeaderCurrentDownloads": "Aktuelle Downloads",
|
"HeaderCurrentDownloads": "Aktuelle Downloads",
|
||||||
"HeaderDetails": "Details",
|
"HeaderDetails": "Details",
|
||||||
"HeaderDownloadQueue": "Download Warteschlange",
|
"HeaderDownloadQueue": "Download Warteschlange",
|
||||||
|
"HeaderEmail": "Email",
|
||||||
|
"HeaderEmailSettings": "Email Settings",
|
||||||
"HeaderEpisodes": "Episoden",
|
"HeaderEpisodes": "Episoden",
|
||||||
|
"HeaderEReaderDevices": "E-Reader Devices",
|
||||||
"HeaderFiles": "Dateien",
|
"HeaderFiles": "Dateien",
|
||||||
"HeaderFindChapters": "Kapitel suchen",
|
"HeaderFindChapters": "Kapitel suchen",
|
||||||
"HeaderIgnoredFiles": "Ignorierte Dateien",
|
"HeaderIgnoredFiles": "Ignorierte Dateien",
|
||||||
@@ -197,6 +202,7 @@
|
|||||||
"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",
|
||||||
@@ -216,7 +222,12 @@
|
|||||||
"LabelDownload": "Herunterladen",
|
"LabelDownload": "Herunterladen",
|
||||||
"LabelDuration": "Laufzeit",
|
"LabelDuration": "Laufzeit",
|
||||||
"LabelDurationFound": "Gefundene Laufzeit:",
|
"LabelDurationFound": "Gefundene Laufzeit:",
|
||||||
|
"LabelEbook": "Ebook",
|
||||||
"LabelEdit": "Bearbeiten",
|
"LabelEdit": "Bearbeiten",
|
||||||
|
"LabelEmail": "Email",
|
||||||
|
"LabelEmailSettingsFromAddress": "From Address",
|
||||||
|
"LabelEmailSettingsSecure": "Secure",
|
||||||
|
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||||
"LabelEmbeddedCover": "Eingebettetes Cover",
|
"LabelEmbeddedCover": "Eingebettetes Cover",
|
||||||
"LabelEnable": "Aktivieren",
|
"LabelEnable": "Aktivieren",
|
||||||
"LabelEnd": "Ende",
|
"LabelEnd": "Ende",
|
||||||
@@ -239,6 +250,7 @@
|
|||||||
"LabelGenre": "Kategorie",
|
"LabelGenre": "Kategorie",
|
||||||
"LabelGenres": "Kategorien",
|
"LabelGenres": "Kategorien",
|
||||||
"LabelHardDeleteFile": "Datei dauerhaft löschen",
|
"LabelHardDeleteFile": "Datei dauerhaft löschen",
|
||||||
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Stunde",
|
"LabelHour": "Stunde",
|
||||||
"LabelIcon": "Symbol",
|
"LabelIcon": "Symbol",
|
||||||
"LabelIncludeInTracklist": "In die Titelliste aufnehmen",
|
"LabelIncludeInTracklist": "In die Titelliste aufnehmen",
|
||||||
@@ -323,7 +335,8 @@
|
|||||||
"LabelPlayMethod": "Abspielmethode",
|
"LabelPlayMethod": "Abspielmethode",
|
||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
"LabelPodcastType": "Podcast Type",
|
"LabelPodcastType": "Podcast Typ",
|
||||||
|
"LabelPort": "Port",
|
||||||
"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": "Verhindere, dass dein Feed von iTunes- und Google-Podcast-Verzeichnissen indiziert wird",
|
"LabelPreventIndexing": "Verhindere, dass dein Feed von iTunes- und Google-Podcast-Verzeichnissen indiziert wird",
|
||||||
"LabelProgress": "Fortschritt",
|
"LabelProgress": "Fortschritt",
|
||||||
@@ -331,6 +344,7 @@
|
|||||||
"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": "Empfohlen",
|
"LabelRecommended": "Empfohlen",
|
||||||
@@ -347,6 +361,7 @@
|
|||||||
"LabelSearchTitle": "Titel",
|
"LabelSearchTitle": "Titel",
|
||||||
"LabelSearchTitleOrASIN": "Titel oder ASIN",
|
"LabelSearchTitleOrASIN": "Titel oder ASIN",
|
||||||
"LabelSeason": "Staffel",
|
"LabelSeason": "Staffel",
|
||||||
|
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||||
"LabelSequence": "Reihenfolge",
|
"LabelSequence": "Reihenfolge",
|
||||||
"LabelSeries": "Serien",
|
"LabelSeries": "Serien",
|
||||||
"LabelSeriesName": "Serienname",
|
"LabelSeriesName": "Serienname",
|
||||||
@@ -363,7 +378,7 @@
|
|||||||
"LabelSettingsExperimentalFeaturesHelp": "Funktionen welche sich in der Entwicklung befinden, benötigen Ihr Feedback und Ihre Hilfe beim Testen. Klicken Sie hier, um die Github-Diskussion zu öffnen.",
|
"LabelSettingsExperimentalFeaturesHelp": "Funktionen welche sich in der Entwicklung befinden, benötigen Ihr Feedback und Ihre Hilfe beim Testen. Klicken Sie hier, um die Github-Diskussion zu öffnen.",
|
||||||
"LabelSettingsFindCovers": "Suche Titelbilder",
|
"LabelSettingsFindCovers": "Suche Titelbilder",
|
||||||
"LabelSettingsFindCoversHelp": "Wenn Ihr Medium kein eingebettetes Titelbild oder kein Titelbild im Ordner hat, versucht der Scanner, ein Titelbild online zu finden.<br>Hinweis: Dies verlängert die Scandauer",
|
"LabelSettingsFindCoversHelp": "Wenn Ihr Medium kein eingebettetes Titelbild oder kein Titelbild im Ordner hat, versucht der Scanner, ein Titelbild online zu finden.<br>Hinweis: Dies verlängert die Scandauer",
|
||||||
"LabelSettingsHomePageBookshelfView": "Starseite verwendet die Bücherregalansicht",
|
"LabelSettingsHomePageBookshelfView": "Startseite verwendet die Bücherregalansicht",
|
||||||
"LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht",
|
"LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht",
|
||||||
"LabelSettingsOverdriveMediaMarkers": "Verwende Overdrive Media Marker für Kapitel",
|
"LabelSettingsOverdriveMediaMarkers": "Verwende Overdrive Media Marker für Kapitel",
|
||||||
"LabelSettingsOverdriveMediaMarkersHelp": "MP3-Dateien von Overdrive werden mit eingebetteten Kapitel-Timings als benutzerdefinierte Metadaten geliefert. Wenn Sie dies aktivieren, werden diese Markierungen automatisch für die Kapiteltaktung verwendet",
|
"LabelSettingsOverdriveMediaMarkersHelp": "MP3-Dateien von Overdrive werden mit eingebetteten Kapitel-Timings als benutzerdefinierte Metadaten geliefert. Wenn Sie dies aktivieren, werden diese Markierungen automatisch für die Kapiteltaktung verwendet",
|
||||||
@@ -413,7 +428,7 @@
|
|||||||
"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",
|
||||||
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
"LabelTagsNotAccessibleToUser": "Für Benutzer nicht zugängliche Schlagwörter",
|
||||||
"LabelTasks": "Laufende Aufgaben",
|
"LabelTasks": "Laufende Aufgaben",
|
||||||
"LabelTimeBase": "Basiszeit",
|
"LabelTimeBase": "Basiszeit",
|
||||||
"LabelTimeListened": "Gehörte Zeit",
|
"LabelTimeListened": "Gehörte Zeit",
|
||||||
@@ -464,7 +479,7 @@
|
|||||||
"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)",
|
||||||
@@ -479,10 +494,11 @@
|
|||||||
"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.",
|
||||||
@@ -490,6 +506,7 @@
|
|||||||
"MessageConfirmRenameTag": "Sind Sie sicher, dass Sie den Tag \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts umbenennen wollen?",
|
"MessageConfirmRenameTag": "Sind Sie sicher, dass Sie den Tag \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts umbenennen wollen?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Hinweis: Tag existiert bereits -> Tags werden zusammengelegt.",
|
"MessageConfirmRenameTagMergeNote": "Hinweis: Tag existiert bereits -> Tags werden zusammengelegt.",
|
||||||
"MessageConfirmRenameTagWarning": "Warnung! Ein ähnlicher Tag mit einem anderen Wortlaut existiert bereits: \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Warnung! Ein ähnlicher Tag mit einem anderen Wortlaut existiert bereits: \"{0}\".",
|
||||||
|
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
|
||||||
"MessageDownloadingEpisode": "Episode herunterladen",
|
"MessageDownloadingEpisode": "Episode herunterladen",
|
||||||
"MessageDragFilesIntoTrackOrder": "Verschieben Sie die Dateien in die richtige Reihenfolge",
|
"MessageDragFilesIntoTrackOrder": "Verschieben Sie die Dateien in die richtige Reihenfolge",
|
||||||
"MessageEmbedFinished": "Einbettung abgeschlossen!",
|
"MessageEmbedFinished": "Einbettung abgeschlossen!",
|
||||||
@@ -644,6 +661,8 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "Medium aus der Sammlung gelöscht",
|
"ToastRemoveItemFromCollectionSuccess": "Medium aus der Sammlung gelöscht",
|
||||||
"ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden",
|
"ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen",
|
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen",
|
||||||
|
"ToastSendEbookToDeviceFailed": "Failed to send ebook to device",
|
||||||
|
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
|
||||||
"ToastSeriesUpdateFailed": "Aktualisierung der Serien fehlgeschlagen",
|
"ToastSeriesUpdateFailed": "Aktualisierung der Serien fehlgeschlagen",
|
||||||
"ToastSeriesUpdateSuccess": "Serien aktualisiert",
|
"ToastSeriesUpdateSuccess": "Serien aktualisiert",
|
||||||
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden",
|
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht 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",
|
||||||
@@ -73,6 +74,7 @@
|
|||||||
"ButtonStartM4BEncode": "Start M4B Encode",
|
"ButtonStartM4BEncode": "Start M4B Encode",
|
||||||
"ButtonStartMetadataEmbed": "Start Metadata Embed",
|
"ButtonStartMetadataEmbed": "Start Metadata Embed",
|
||||||
"ButtonSubmit": "Submit",
|
"ButtonSubmit": "Submit",
|
||||||
|
"ButtonTest": "Test",
|
||||||
"ButtonUpload": "Upload",
|
"ButtonUpload": "Upload",
|
||||||
"ButtonUploadBackup": "Upload Backup",
|
"ButtonUploadBackup": "Upload Backup",
|
||||||
"ButtonUploadCover": "Upload Cover",
|
"ButtonUploadCover": "Upload Cover",
|
||||||
@@ -96,7 +98,10 @@
|
|||||||
"HeaderCurrentDownloads": "Current Downloads",
|
"HeaderCurrentDownloads": "Current Downloads",
|
||||||
"HeaderDetails": "Details",
|
"HeaderDetails": "Details",
|
||||||
"HeaderDownloadQueue": "Download Queue",
|
"HeaderDownloadQueue": "Download Queue",
|
||||||
|
"HeaderEmail": "Email",
|
||||||
|
"HeaderEmailSettings": "Email Settings",
|
||||||
"HeaderEpisodes": "Episodes",
|
"HeaderEpisodes": "Episodes",
|
||||||
|
"HeaderEReaderDevices": "E-Reader Devices",
|
||||||
"HeaderFiles": "Files",
|
"HeaderFiles": "Files",
|
||||||
"HeaderFindChapters": "Find Chapters",
|
"HeaderFindChapters": "Find Chapters",
|
||||||
"HeaderIgnoredFiles": "Ignored Files",
|
"HeaderIgnoredFiles": "Ignored Files",
|
||||||
@@ -197,6 +202,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",
|
||||||
@@ -216,7 +222,12 @@
|
|||||||
"LabelDownload": "Download",
|
"LabelDownload": "Download",
|
||||||
"LabelDuration": "Duration",
|
"LabelDuration": "Duration",
|
||||||
"LabelDurationFound": "Duration found:",
|
"LabelDurationFound": "Duration found:",
|
||||||
|
"LabelEbook": "Ebook",
|
||||||
"LabelEdit": "Edit",
|
"LabelEdit": "Edit",
|
||||||
|
"LabelEmail": "Email",
|
||||||
|
"LabelEmailSettingsFromAddress": "From Address",
|
||||||
|
"LabelEmailSettingsSecure": "Secure",
|
||||||
|
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||||
"LabelEmbeddedCover": "Embedded Cover",
|
"LabelEmbeddedCover": "Embedded Cover",
|
||||||
"LabelEnable": "Enable",
|
"LabelEnable": "Enable",
|
||||||
"LabelEnd": "End",
|
"LabelEnd": "End",
|
||||||
@@ -239,6 +250,7 @@
|
|||||||
"LabelGenre": "Genre",
|
"LabelGenre": "Genre",
|
||||||
"LabelGenres": "Genres",
|
"LabelGenres": "Genres",
|
||||||
"LabelHardDeleteFile": "Hard delete file",
|
"LabelHardDeleteFile": "Hard delete file",
|
||||||
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Hour",
|
"LabelHour": "Hour",
|
||||||
"LabelIcon": "Icon",
|
"LabelIcon": "Icon",
|
||||||
"LabelIncludeInTracklist": "Include in Tracklist",
|
"LabelIncludeInTracklist": "Include in Tracklist",
|
||||||
@@ -324,6 +336,7 @@
|
|||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
"LabelPodcastType": "Podcast Type",
|
"LabelPodcastType": "Podcast Type",
|
||||||
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
|
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
|
||||||
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
||||||
"LabelProgress": "Progress",
|
"LabelProgress": "Progress",
|
||||||
@@ -331,6 +344,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",
|
||||||
@@ -347,6 +361,7 @@
|
|||||||
"LabelSearchTitle": "Search Title",
|
"LabelSearchTitle": "Search Title",
|
||||||
"LabelSearchTitleOrASIN": "Search Title or ASIN",
|
"LabelSearchTitleOrASIN": "Search Title or ASIN",
|
||||||
"LabelSeason": "Season",
|
"LabelSeason": "Season",
|
||||||
|
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||||
"LabelSequence": "Sequence",
|
"LabelSequence": "Sequence",
|
||||||
"LabelSeries": "Series",
|
"LabelSeries": "Series",
|
||||||
"LabelSeriesName": "Series Name",
|
"LabelSeriesName": "Series Name",
|
||||||
@@ -483,6 +498,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.",
|
||||||
@@ -490,6 +506,7 @@
|
|||||||
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
|
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
|
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
|
||||||
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
|
||||||
|
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
|
||||||
"MessageDownloadingEpisode": "Downloading episode",
|
"MessageDownloadingEpisode": "Downloading episode",
|
||||||
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
|
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
|
||||||
"MessageEmbedFinished": "Embed Finished!",
|
"MessageEmbedFinished": "Embed Finished!",
|
||||||
@@ -644,6 +661,8 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
|
"ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
|
||||||
"ToastRSSFeedCloseFailed": "Failed to close RSS feed",
|
"ToastRSSFeedCloseFailed": "Failed to close RSS feed",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS feed closed",
|
"ToastRSSFeedCloseSuccess": "RSS feed closed",
|
||||||
|
"ToastSendEbookToDeviceFailed": "Failed to send ebook to device",
|
||||||
|
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
|
||||||
"ToastSeriesUpdateFailed": "Series update failed",
|
"ToastSeriesUpdateFailed": "Series update failed",
|
||||||
"ToastSeriesUpdateSuccess": "Series update success",
|
"ToastSeriesUpdateSuccess": "Series update success",
|
||||||
"ToastSessionDeleteFailed": "Failed to delete session",
|
"ToastSessionDeleteFailed": "Failed to delete session",
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -73,6 +74,7 @@
|
|||||||
"ButtonStartM4BEncode": "Iniciar Codificación M4B",
|
"ButtonStartM4BEncode": "Iniciar Codificación M4B",
|
||||||
"ButtonStartMetadataEmbed": "Iniciar la Inserción de Metadata",
|
"ButtonStartMetadataEmbed": "Iniciar la Inserción de Metadata",
|
||||||
"ButtonSubmit": "Enviar",
|
"ButtonSubmit": "Enviar",
|
||||||
|
"ButtonTest": "Test",
|
||||||
"ButtonUpload": "Subir",
|
"ButtonUpload": "Subir",
|
||||||
"ButtonUploadBackup": "Subir Respaldo",
|
"ButtonUploadBackup": "Subir Respaldo",
|
||||||
"ButtonUploadCover": "Subir Portada",
|
"ButtonUploadCover": "Subir Portada",
|
||||||
@@ -96,7 +98,10 @@
|
|||||||
"HeaderCurrentDownloads": "Descargando Actualmente",
|
"HeaderCurrentDownloads": "Descargando Actualmente",
|
||||||
"HeaderDetails": "Detalles",
|
"HeaderDetails": "Detalles",
|
||||||
"HeaderDownloadQueue": "Lista de Descarga",
|
"HeaderDownloadQueue": "Lista de Descarga",
|
||||||
|
"HeaderEmail": "Email",
|
||||||
|
"HeaderEmailSettings": "Email Settings",
|
||||||
"HeaderEpisodes": "Episodios",
|
"HeaderEpisodes": "Episodios",
|
||||||
|
"HeaderEReaderDevices": "E-Reader Devices",
|
||||||
"HeaderFiles": "Elemento",
|
"HeaderFiles": "Elemento",
|
||||||
"HeaderFindChapters": "Buscar Capitulo",
|
"HeaderFindChapters": "Buscar Capitulo",
|
||||||
"HeaderIgnoredFiles": "Ignorar Elemento",
|
"HeaderIgnoredFiles": "Ignorar Elemento",
|
||||||
@@ -197,6 +202,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",
|
||||||
@@ -216,7 +222,12 @@
|
|||||||
"LabelDownload": "Descargar",
|
"LabelDownload": "Descargar",
|
||||||
"LabelDuration": "Duración",
|
"LabelDuration": "Duración",
|
||||||
"LabelDurationFound": "Duración Comprobada:",
|
"LabelDurationFound": "Duración Comprobada:",
|
||||||
|
"LabelEbook": "Ebook",
|
||||||
"LabelEdit": "Editar",
|
"LabelEdit": "Editar",
|
||||||
|
"LabelEmail": "Email",
|
||||||
|
"LabelEmailSettingsFromAddress": "From Address",
|
||||||
|
"LabelEmailSettingsSecure": "Secure",
|
||||||
|
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||||
"LabelEmbeddedCover": "Portada Integrada",
|
"LabelEmbeddedCover": "Portada Integrada",
|
||||||
"LabelEnable": "Habilitar",
|
"LabelEnable": "Habilitar",
|
||||||
"LabelEnd": "Fin",
|
"LabelEnd": "Fin",
|
||||||
@@ -239,6 +250,7 @@
|
|||||||
"LabelGenre": "Genero",
|
"LabelGenre": "Genero",
|
||||||
"LabelGenres": "Géneros",
|
"LabelGenres": "Géneros",
|
||||||
"LabelHardDeleteFile": "Eliminar Definitivamente",
|
"LabelHardDeleteFile": "Eliminar Definitivamente",
|
||||||
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Hora",
|
"LabelHour": "Hora",
|
||||||
"LabelIcon": "Icono",
|
"LabelIcon": "Icono",
|
||||||
"LabelIncludeInTracklist": "Incluir en Tracklist",
|
"LabelIncludeInTracklist": "Incluir en Tracklist",
|
||||||
@@ -324,6 +336,7 @@
|
|||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
"LabelPodcastType": "Tipo Podcast",
|
"LabelPodcastType": "Tipo Podcast",
|
||||||
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Prefijos para Ignorar (no distingue entre mayúsculas y minúsculas.)",
|
"LabelPrefixesToIgnore": "Prefijos para Ignorar (no distingue entre mayúsculas y minúsculas.)",
|
||||||
"LabelPreventIndexing": "Evite que su fuente sea indexado por iTunes y Google podcast directories",
|
"LabelPreventIndexing": "Evite que su fuente sea indexado por iTunes y Google podcast directories",
|
||||||
"LabelProgress": "Progreso",
|
"LabelProgress": "Progreso",
|
||||||
@@ -331,6 +344,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",
|
||||||
@@ -347,6 +361,7 @@
|
|||||||
"LabelSearchTitle": "Buscar Titulo",
|
"LabelSearchTitle": "Buscar Titulo",
|
||||||
"LabelSearchTitleOrASIN": "Buscar Titulo o ASIN",
|
"LabelSearchTitleOrASIN": "Buscar Titulo o ASIN",
|
||||||
"LabelSeason": "Temporada",
|
"LabelSeason": "Temporada",
|
||||||
|
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||||
"LabelSequence": "Secuencia",
|
"LabelSequence": "Secuencia",
|
||||||
"LabelSeries": "Series",
|
"LabelSeries": "Series",
|
||||||
"LabelSeriesName": "Nombre de la Serie",
|
"LabelSeriesName": "Nombre de la Serie",
|
||||||
@@ -483,6 +498,7 @@
|
|||||||
"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.",
|
||||||
@@ -490,6 +506,7 @@
|
|||||||
"MessageConfirmRenameTag": "Esta seguro que desea renombrar la etiqueta \"{0}\" a \"{1}\" de todos los elementos?",
|
"MessageConfirmRenameTag": "Esta seguro que desea renombrar la etiqueta \"{0}\" a \"{1}\" de todos los elementos?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Nota: Esta etiqueta ya existe por lo que se fusionarán.",
|
"MessageConfirmRenameTagMergeNote": "Nota: Esta etiqueta ya existe por lo que se fusionarán.",
|
||||||
"MessageConfirmRenameTagWarning": "Advertencia! Una etiqueta similar ya existe \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Advertencia! Una etiqueta similar ya existe \"{0}\".",
|
||||||
|
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
|
||||||
"MessageDownloadingEpisode": "Descargando Capitulo",
|
"MessageDownloadingEpisode": "Descargando Capitulo",
|
||||||
"MessageDragFilesIntoTrackOrder": "Arrastras los archivos en el orden correcto de la pista.",
|
"MessageDragFilesIntoTrackOrder": "Arrastras los archivos en el orden correcto de la pista.",
|
||||||
"MessageEmbedFinished": "Incorporación Terminada!",
|
"MessageEmbedFinished": "Incorporación Terminada!",
|
||||||
@@ -644,6 +661,8 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "Elemento eliminado de la colección.",
|
"ToastRemoveItemFromCollectionSuccess": "Elemento eliminado de la colección.",
|
||||||
"ToastRSSFeedCloseFailed": "Error al cerrar fuente RSS",
|
"ToastRSSFeedCloseFailed": "Error al cerrar fuente RSS",
|
||||||
"ToastRSSFeedCloseSuccess": "Fuente RSS cerrada",
|
"ToastRSSFeedCloseSuccess": "Fuente RSS cerrada",
|
||||||
|
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
|
||||||
|
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
|
||||||
"ToastSeriesUpdateFailed": "Error al actualizar la serie",
|
"ToastSeriesUpdateFailed": "Error al actualizar la serie",
|
||||||
"ToastSeriesUpdateSuccess": "Series actualizada",
|
"ToastSeriesUpdateSuccess": "Series actualizada",
|
||||||
"ToastSessionDeleteFailed": "Error al eliminar sesión",
|
"ToastSessionDeleteFailed": "Error al eliminar sesión",
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -73,6 +74,7 @@
|
|||||||
"ButtonStartM4BEncode": "Démarrer l’encodage M4B",
|
"ButtonStartM4BEncode": "Démarrer l’encodage M4B",
|
||||||
"ButtonStartMetadataEmbed": "Démarrer les Métadonnées intégrées",
|
"ButtonStartMetadataEmbed": "Démarrer les Métadonnées intégrées",
|
||||||
"ButtonSubmit": "Soumettre",
|
"ButtonSubmit": "Soumettre",
|
||||||
|
"ButtonTest": "Test",
|
||||||
"ButtonUpload": "Téléverser",
|
"ButtonUpload": "Téléverser",
|
||||||
"ButtonUploadBackup": "Téléverser une sauvegarde",
|
"ButtonUploadBackup": "Téléverser une sauvegarde",
|
||||||
"ButtonUploadCover": "Téléverser une couverture",
|
"ButtonUploadCover": "Téléverser une couverture",
|
||||||
@@ -96,7 +98,10 @@
|
|||||||
"HeaderCurrentDownloads": "File d’attente de téléchargement",
|
"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",
|
||||||
|
"HeaderEmail": "Email",
|
||||||
|
"HeaderEmailSettings": "Email Settings",
|
||||||
"HeaderEpisodes": "Épisodes",
|
"HeaderEpisodes": "Épisodes",
|
||||||
|
"HeaderEReaderDevices": "E-Reader Devices",
|
||||||
"HeaderFiles": "Fichiers",
|
"HeaderFiles": "Fichiers",
|
||||||
"HeaderFindChapters": "Trouver les chapitres",
|
"HeaderFindChapters": "Trouver les chapitres",
|
||||||
"HeaderIgnoredFiles": "Fichiers Ignorés",
|
"HeaderIgnoredFiles": "Fichiers Ignorés",
|
||||||
@@ -197,6 +202,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",
|
||||||
@@ -216,7 +222,12 @@
|
|||||||
"LabelDownload": "Téléchargement",
|
"LabelDownload": "Téléchargement",
|
||||||
"LabelDuration": "Durée",
|
"LabelDuration": "Durée",
|
||||||
"LabelDurationFound": "Durée trouvée :",
|
"LabelDurationFound": "Durée trouvée :",
|
||||||
|
"LabelEbook": "Ebook",
|
||||||
"LabelEdit": "Modifier",
|
"LabelEdit": "Modifier",
|
||||||
|
"LabelEmail": "Email",
|
||||||
|
"LabelEmailSettingsFromAddress": "From Address",
|
||||||
|
"LabelEmailSettingsSecure": "Secure",
|
||||||
|
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||||
"LabelEmbeddedCover": "Couverture du livre intégrée",
|
"LabelEmbeddedCover": "Couverture du livre intégrée",
|
||||||
"LabelEnable": "Activer",
|
"LabelEnable": "Activer",
|
||||||
"LabelEnd": "Fin",
|
"LabelEnd": "Fin",
|
||||||
@@ -239,6 +250,7 @@
|
|||||||
"LabelGenre": "Genre",
|
"LabelGenre": "Genre",
|
||||||
"LabelGenres": "Genres",
|
"LabelGenres": "Genres",
|
||||||
"LabelHardDeleteFile": "Suppression du fichier",
|
"LabelHardDeleteFile": "Suppression du fichier",
|
||||||
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Heure",
|
"LabelHour": "Heure",
|
||||||
"LabelIcon": "Icone",
|
"LabelIcon": "Icone",
|
||||||
"LabelIncludeInTracklist": "Inclure dans la liste des pistes",
|
"LabelIncludeInTracklist": "Inclure dans la liste des pistes",
|
||||||
@@ -324,6 +336,7 @@
|
|||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
"LabelPodcastType": "Type de Podcast",
|
"LabelPodcastType": "Type de Podcast",
|
||||||
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)",
|
"LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)",
|
||||||
"LabelPreventIndexing": "Empêcher l’indexation de votre flux par les bases de donénes iTunes et Google podcast",
|
"LabelPreventIndexing": "Empêcher l’indexation de votre flux par les bases de donénes iTunes et Google podcast",
|
||||||
"LabelProgress": "Progression",
|
"LabelProgress": "Progression",
|
||||||
@@ -331,6 +344,7 @@
|
|||||||
"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é",
|
||||||
@@ -347,6 +361,7 @@
|
|||||||
"LabelSearchTitle": "Titre de recherche",
|
"LabelSearchTitle": "Titre de recherche",
|
||||||
"LabelSearchTitleOrASIN": "Recherche du titre ou ASIN",
|
"LabelSearchTitleOrASIN": "Recherche du titre ou ASIN",
|
||||||
"LabelSeason": "Saison",
|
"LabelSeason": "Saison",
|
||||||
|
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||||
"LabelSequence": "Séquence",
|
"LabelSequence": "Séquence",
|
||||||
"LabelSeries": "Séries",
|
"LabelSeries": "Séries",
|
||||||
"LabelSeriesName": "Nom de la série",
|
"LabelSeriesName": "Nom de la série",
|
||||||
@@ -483,6 +498,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é.",
|
||||||
@@ -490,6 +506,7 @@
|
|||||||
"MessageConfirmRenameTag": "Êtes-vous sûr de vouloir renommer l’étiquette « {0} » vers « {1} » pour tous les articles ?",
|
"MessageConfirmRenameTag": "Êtes-vous sûr de vouloir renommer l’étiquette « {0} » vers « {1} » pour tous les articles ?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Information: Cette étiquette existe déjà et sera fusionnée.",
|
"MessageConfirmRenameTagMergeNote": "Information: Cette étiquette existe déjà et sera fusionnée.",
|
||||||
"MessageConfirmRenameTagWarning": "Attention ! Une étiquette similaire avec une casse différente existe déjà « {0} ».",
|
"MessageConfirmRenameTagWarning": "Attention ! Une étiquette similaire avec une casse différente existe déjà « {0} ».",
|
||||||
|
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
|
||||||
"MessageDownloadingEpisode": "Téléchargement de l’épisode",
|
"MessageDownloadingEpisode": "Téléchargement de l’épisode",
|
||||||
"MessageDragFilesIntoTrackOrder": "Faire glisser les fichiers dans l’ordre correct",
|
"MessageDragFilesIntoTrackOrder": "Faire glisser les fichiers dans l’ordre correct",
|
||||||
"MessageEmbedFinished": "Intégration Terminée !",
|
"MessageEmbedFinished": "Intégration Terminée !",
|
||||||
@@ -644,6 +661,8 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "Article supprimé de la collection",
|
"ToastRemoveItemFromCollectionSuccess": "Article supprimé de la collection",
|
||||||
"ToastRSSFeedCloseFailed": "Échec de la fermeture du flux RSS",
|
"ToastRSSFeedCloseFailed": "Échec de la fermeture du flux RSS",
|
||||||
"ToastRSSFeedCloseSuccess": "Flux RSS fermé",
|
"ToastRSSFeedCloseSuccess": "Flux RSS fermé",
|
||||||
|
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
|
||||||
|
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
|
||||||
"ToastSeriesUpdateFailed": "Échec de la mise à jour de la série",
|
"ToastSeriesUpdateFailed": "Échec de la mise à jour de la série",
|
||||||
"ToastSeriesUpdateSuccess": "Mise à jour de la série réussie",
|
"ToastSeriesUpdateSuccess": "Mise à jour de la série réussie",
|
||||||
"ToastSessionDeleteFailed": "Échec de la suppression de session",
|
"ToastSessionDeleteFailed": "Échec de la suppression de session",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "બધું કાઢી નાખો",
|
"ButtonRemoveAll": "બધું કાઢી નાખો",
|
||||||
"ButtonRemoveAllLibraryItems": "બધું પુસ્તકાલય વસ્તુઓ કાઢી નાખો",
|
"ButtonRemoveAllLibraryItems": "બધું પુસ્તકાલય વસ્તુઓ કાઢી નાખો",
|
||||||
"ButtonRemoveFromContinueListening": "સાંભળતી પુસ્તકો માંથી કાઢી નાખો",
|
"ButtonRemoveFromContinueListening": "સાંભળતી પુસ્તકો માંથી કાઢી નાખો",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "સાંભળતી સિરીઝ માંથી કાઢી નાખો",
|
"ButtonRemoveSeriesFromContinueSeries": "સાંભળતી સિરીઝ માંથી કાઢી નાખો",
|
||||||
"ButtonReScan": "ફરીથી સ્કેન કરો",
|
"ButtonReScan": "ફરીથી સ્કેન કરો",
|
||||||
"ButtonReset": "રીસેટ કરો",
|
"ButtonReset": "રીસેટ કરો",
|
||||||
@@ -73,6 +74,7 @@
|
|||||||
"ButtonStartM4BEncode": "M4B એન્કોડ શરૂ કરો",
|
"ButtonStartM4BEncode": "M4B એન્કોડ શરૂ કરો",
|
||||||
"ButtonStartMetadataEmbed": "મેટાડેટા એમ્બેડ શરૂ કરો",
|
"ButtonStartMetadataEmbed": "મેટાડેટા એમ્બેડ શરૂ કરો",
|
||||||
"ButtonSubmit": "સબમિટ કરો",
|
"ButtonSubmit": "સબમિટ કરો",
|
||||||
|
"ButtonTest": "Test",
|
||||||
"ButtonUpload": "અપલોડ કરો",
|
"ButtonUpload": "અપલોડ કરો",
|
||||||
"ButtonUploadBackup": "બેકઅપ અપલોડ કરો",
|
"ButtonUploadBackup": "બેકઅપ અપલોડ કરો",
|
||||||
"ButtonUploadCover": "કવર અપલોડ કરો",
|
"ButtonUploadCover": "કવર અપલોડ કરો",
|
||||||
@@ -96,7 +98,10 @@
|
|||||||
"HeaderCurrentDownloads": "Current Downloads",
|
"HeaderCurrentDownloads": "Current Downloads",
|
||||||
"HeaderDetails": "Details",
|
"HeaderDetails": "Details",
|
||||||
"HeaderDownloadQueue": "Download Queue",
|
"HeaderDownloadQueue": "Download Queue",
|
||||||
|
"HeaderEmail": "Email",
|
||||||
|
"HeaderEmailSettings": "Email Settings",
|
||||||
"HeaderEpisodes": "Episodes",
|
"HeaderEpisodes": "Episodes",
|
||||||
|
"HeaderEReaderDevices": "E-Reader Devices",
|
||||||
"HeaderFiles": "Files",
|
"HeaderFiles": "Files",
|
||||||
"HeaderFindChapters": "Find Chapters",
|
"HeaderFindChapters": "Find Chapters",
|
||||||
"HeaderIgnoredFiles": "Ignored Files",
|
"HeaderIgnoredFiles": "Ignored Files",
|
||||||
@@ -197,6 +202,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",
|
||||||
@@ -216,7 +222,12 @@
|
|||||||
"LabelDownload": "Download",
|
"LabelDownload": "Download",
|
||||||
"LabelDuration": "Duration",
|
"LabelDuration": "Duration",
|
||||||
"LabelDurationFound": "Duration found:",
|
"LabelDurationFound": "Duration found:",
|
||||||
|
"LabelEbook": "Ebook",
|
||||||
"LabelEdit": "Edit",
|
"LabelEdit": "Edit",
|
||||||
|
"LabelEmail": "Email",
|
||||||
|
"LabelEmailSettingsFromAddress": "From Address",
|
||||||
|
"LabelEmailSettingsSecure": "Secure",
|
||||||
|
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||||
"LabelEmbeddedCover": "Embedded Cover",
|
"LabelEmbeddedCover": "Embedded Cover",
|
||||||
"LabelEnable": "Enable",
|
"LabelEnable": "Enable",
|
||||||
"LabelEnd": "End",
|
"LabelEnd": "End",
|
||||||
@@ -239,6 +250,7 @@
|
|||||||
"LabelGenre": "Genre",
|
"LabelGenre": "Genre",
|
||||||
"LabelGenres": "Genres",
|
"LabelGenres": "Genres",
|
||||||
"LabelHardDeleteFile": "Hard delete file",
|
"LabelHardDeleteFile": "Hard delete file",
|
||||||
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Hour",
|
"LabelHour": "Hour",
|
||||||
"LabelIcon": "Icon",
|
"LabelIcon": "Icon",
|
||||||
"LabelIncludeInTracklist": "Include in Tracklist",
|
"LabelIncludeInTracklist": "Include in Tracklist",
|
||||||
@@ -324,6 +336,7 @@
|
|||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
"LabelPodcastType": "Podcast Type",
|
"LabelPodcastType": "Podcast Type",
|
||||||
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
|
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
|
||||||
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
||||||
"LabelProgress": "Progress",
|
"LabelProgress": "Progress",
|
||||||
@@ -331,6 +344,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",
|
||||||
@@ -347,6 +361,7 @@
|
|||||||
"LabelSearchTitle": "Search Title",
|
"LabelSearchTitle": "Search Title",
|
||||||
"LabelSearchTitleOrASIN": "Search Title or ASIN",
|
"LabelSearchTitleOrASIN": "Search Title or ASIN",
|
||||||
"LabelSeason": "Season",
|
"LabelSeason": "Season",
|
||||||
|
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||||
"LabelSequence": "Sequence",
|
"LabelSequence": "Sequence",
|
||||||
"LabelSeries": "Series",
|
"LabelSeries": "Series",
|
||||||
"LabelSeriesName": "Series Name",
|
"LabelSeriesName": "Series Name",
|
||||||
@@ -483,6 +498,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.",
|
||||||
@@ -490,6 +506,7 @@
|
|||||||
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
|
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
|
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
|
||||||
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
|
||||||
|
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
|
||||||
"MessageDownloadingEpisode": "Downloading episode",
|
"MessageDownloadingEpisode": "Downloading episode",
|
||||||
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
|
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
|
||||||
"MessageEmbedFinished": "Embed Finished!",
|
"MessageEmbedFinished": "Embed Finished!",
|
||||||
@@ -644,6 +661,8 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
|
"ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
|
||||||
"ToastRSSFeedCloseFailed": "Failed to close RSS feed",
|
"ToastRSSFeedCloseFailed": "Failed to close RSS feed",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS feed closed",
|
"ToastRSSFeedCloseSuccess": "RSS feed closed",
|
||||||
|
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
|
||||||
|
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
|
||||||
"ToastSeriesUpdateFailed": "Series update failed",
|
"ToastSeriesUpdateFailed": "Series update failed",
|
||||||
"ToastSeriesUpdateSuccess": "Series update success",
|
"ToastSeriesUpdateSuccess": "Series update success",
|
||||||
"ToastSessionDeleteFailed": "Failed to delete session",
|
"ToastSessionDeleteFailed": "Failed to delete session",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "सभी हटाएं",
|
"ButtonRemoveAll": "सभी हटाएं",
|
||||||
"ButtonRemoveAllLibraryItems": "पुस्तकालय की सभी आइटम हटाएं",
|
"ButtonRemoveAllLibraryItems": "पुस्तकालय की सभी आइटम हटाएं",
|
||||||
"ButtonRemoveFromContinueListening": "सुनना जारी रखें से हटाएं",
|
"ButtonRemoveFromContinueListening": "सुनना जारी रखें से हटाएं",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "इस सीरीज को कंटिन्यू सीरीज से हटा दें",
|
"ButtonRemoveSeriesFromContinueSeries": "इस सीरीज को कंटिन्यू सीरीज से हटा दें",
|
||||||
"ButtonReScan": "पुन: स्कैन करें",
|
"ButtonReScan": "पुन: स्कैन करें",
|
||||||
"ButtonReset": "रीसेट करें",
|
"ButtonReset": "रीसेट करें",
|
||||||
@@ -73,6 +74,7 @@
|
|||||||
"ButtonStartM4BEncode": "M4B एन्कोडिंग शुरू करें",
|
"ButtonStartM4BEncode": "M4B एन्कोडिंग शुरू करें",
|
||||||
"ButtonStartMetadataEmbed": "मेटाडेटा एम्बेडिंग शुरू करें",
|
"ButtonStartMetadataEmbed": "मेटाडेटा एम्बेडिंग शुरू करें",
|
||||||
"ButtonSubmit": "जमा करें",
|
"ButtonSubmit": "जमा करें",
|
||||||
|
"ButtonTest": "Test",
|
||||||
"ButtonUpload": "अपलोड करें",
|
"ButtonUpload": "अपलोड करें",
|
||||||
"ButtonUploadBackup": "बैकअप अपलोड करें",
|
"ButtonUploadBackup": "बैकअप अपलोड करें",
|
||||||
"ButtonUploadCover": "कवर अपलोड करें",
|
"ButtonUploadCover": "कवर अपलोड करें",
|
||||||
@@ -96,7 +98,10 @@
|
|||||||
"HeaderCurrentDownloads": "Current Downloads",
|
"HeaderCurrentDownloads": "Current Downloads",
|
||||||
"HeaderDetails": "Details",
|
"HeaderDetails": "Details",
|
||||||
"HeaderDownloadQueue": "Download Queue",
|
"HeaderDownloadQueue": "Download Queue",
|
||||||
|
"HeaderEmail": "Email",
|
||||||
|
"HeaderEmailSettings": "Email Settings",
|
||||||
"HeaderEpisodes": "Episodes",
|
"HeaderEpisodes": "Episodes",
|
||||||
|
"HeaderEReaderDevices": "E-Reader Devices",
|
||||||
"HeaderFiles": "Files",
|
"HeaderFiles": "Files",
|
||||||
"HeaderFindChapters": "Find Chapters",
|
"HeaderFindChapters": "Find Chapters",
|
||||||
"HeaderIgnoredFiles": "Ignored Files",
|
"HeaderIgnoredFiles": "Ignored Files",
|
||||||
@@ -197,6 +202,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",
|
||||||
@@ -216,7 +222,12 @@
|
|||||||
"LabelDownload": "Download",
|
"LabelDownload": "Download",
|
||||||
"LabelDuration": "Duration",
|
"LabelDuration": "Duration",
|
||||||
"LabelDurationFound": "Duration found:",
|
"LabelDurationFound": "Duration found:",
|
||||||
|
"LabelEbook": "Ebook",
|
||||||
"LabelEdit": "Edit",
|
"LabelEdit": "Edit",
|
||||||
|
"LabelEmail": "Email",
|
||||||
|
"LabelEmailSettingsFromAddress": "From Address",
|
||||||
|
"LabelEmailSettingsSecure": "Secure",
|
||||||
|
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||||
"LabelEmbeddedCover": "Embedded Cover",
|
"LabelEmbeddedCover": "Embedded Cover",
|
||||||
"LabelEnable": "Enable",
|
"LabelEnable": "Enable",
|
||||||
"LabelEnd": "End",
|
"LabelEnd": "End",
|
||||||
@@ -239,6 +250,7 @@
|
|||||||
"LabelGenre": "Genre",
|
"LabelGenre": "Genre",
|
||||||
"LabelGenres": "Genres",
|
"LabelGenres": "Genres",
|
||||||
"LabelHardDeleteFile": "Hard delete file",
|
"LabelHardDeleteFile": "Hard delete file",
|
||||||
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Hour",
|
"LabelHour": "Hour",
|
||||||
"LabelIcon": "Icon",
|
"LabelIcon": "Icon",
|
||||||
"LabelIncludeInTracklist": "Include in Tracklist",
|
"LabelIncludeInTracklist": "Include in Tracklist",
|
||||||
@@ -324,6 +336,7 @@
|
|||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
"LabelPodcastType": "Podcast Type",
|
"LabelPodcastType": "Podcast Type",
|
||||||
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
|
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
|
||||||
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
||||||
"LabelProgress": "Progress",
|
"LabelProgress": "Progress",
|
||||||
@@ -331,6 +344,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",
|
||||||
@@ -347,6 +361,7 @@
|
|||||||
"LabelSearchTitle": "Search Title",
|
"LabelSearchTitle": "Search Title",
|
||||||
"LabelSearchTitleOrASIN": "Search Title or ASIN",
|
"LabelSearchTitleOrASIN": "Search Title or ASIN",
|
||||||
"LabelSeason": "Season",
|
"LabelSeason": "Season",
|
||||||
|
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||||
"LabelSequence": "Sequence",
|
"LabelSequence": "Sequence",
|
||||||
"LabelSeries": "Series",
|
"LabelSeries": "Series",
|
||||||
"LabelSeriesName": "Series Name",
|
"LabelSeriesName": "Series Name",
|
||||||
@@ -483,6 +498,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.",
|
||||||
@@ -490,6 +506,7 @@
|
|||||||
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
|
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
|
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
|
||||||
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
|
||||||
|
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
|
||||||
"MessageDownloadingEpisode": "Downloading episode",
|
"MessageDownloadingEpisode": "Downloading episode",
|
||||||
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
|
"MessageDragFilesIntoTrackOrder": "Drag files into correct track order",
|
||||||
"MessageEmbedFinished": "Embed Finished!",
|
"MessageEmbedFinished": "Embed Finished!",
|
||||||
@@ -644,6 +661,8 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
|
"ToastRemoveItemFromCollectionSuccess": "Item removed from collection",
|
||||||
"ToastRSSFeedCloseFailed": "Failed to close RSS feed",
|
"ToastRSSFeedCloseFailed": "Failed to close RSS feed",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS feed closed",
|
"ToastRSSFeedCloseSuccess": "RSS feed closed",
|
||||||
|
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
|
||||||
|
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
|
||||||
"ToastSeriesUpdateFailed": "Series update failed",
|
"ToastSeriesUpdateFailed": "Series update failed",
|
||||||
"ToastSeriesUpdateSuccess": "Series update success",
|
"ToastSeriesUpdateSuccess": "Series update success",
|
||||||
"ToastSessionDeleteFailed": "Failed to delete session",
|
"ToastSessionDeleteFailed": "Failed to delete session",
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -73,6 +74,7 @@
|
|||||||
"ButtonStartM4BEncode": "Pokreni M4B kodiranje",
|
"ButtonStartM4BEncode": "Pokreni M4B kodiranje",
|
||||||
"ButtonStartMetadataEmbed": "Pokreni ugradnju metapodataka",
|
"ButtonStartMetadataEmbed": "Pokreni ugradnju metapodataka",
|
||||||
"ButtonSubmit": "Submit",
|
"ButtonSubmit": "Submit",
|
||||||
|
"ButtonTest": "Test",
|
||||||
"ButtonUpload": "Upload",
|
"ButtonUpload": "Upload",
|
||||||
"ButtonUploadBackup": "Upload backup",
|
"ButtonUploadBackup": "Upload backup",
|
||||||
"ButtonUploadCover": "Upload Cover",
|
"ButtonUploadCover": "Upload Cover",
|
||||||
@@ -96,7 +98,10 @@
|
|||||||
"HeaderCurrentDownloads": "Current Downloads",
|
"HeaderCurrentDownloads": "Current Downloads",
|
||||||
"HeaderDetails": "Detalji",
|
"HeaderDetails": "Detalji",
|
||||||
"HeaderDownloadQueue": "Download Queue",
|
"HeaderDownloadQueue": "Download Queue",
|
||||||
|
"HeaderEmail": "Email",
|
||||||
|
"HeaderEmailSettings": "Email Settings",
|
||||||
"HeaderEpisodes": "Epizode",
|
"HeaderEpisodes": "Epizode",
|
||||||
|
"HeaderEReaderDevices": "E-Reader Devices",
|
||||||
"HeaderFiles": "Datoteke",
|
"HeaderFiles": "Datoteke",
|
||||||
"HeaderFindChapters": "Pronađi poglavlja",
|
"HeaderFindChapters": "Pronađi poglavlja",
|
||||||
"HeaderIgnoredFiles": "Zanemarene datoteke",
|
"HeaderIgnoredFiles": "Zanemarene datoteke",
|
||||||
@@ -197,6 +202,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",
|
||||||
@@ -216,7 +222,12 @@
|
|||||||
"LabelDownload": "Preuzmi",
|
"LabelDownload": "Preuzmi",
|
||||||
"LabelDuration": "Trajanje",
|
"LabelDuration": "Trajanje",
|
||||||
"LabelDurationFound": "Pronađeno trajanje:",
|
"LabelDurationFound": "Pronađeno trajanje:",
|
||||||
|
"LabelEbook": "Ebook",
|
||||||
"LabelEdit": "Uredi",
|
"LabelEdit": "Uredi",
|
||||||
|
"LabelEmail": "Email",
|
||||||
|
"LabelEmailSettingsFromAddress": "From Address",
|
||||||
|
"LabelEmailSettingsSecure": "Secure",
|
||||||
|
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||||
"LabelEmbeddedCover": "Embedded Cover",
|
"LabelEmbeddedCover": "Embedded Cover",
|
||||||
"LabelEnable": "Uključi",
|
"LabelEnable": "Uključi",
|
||||||
"LabelEnd": "Kraj",
|
"LabelEnd": "Kraj",
|
||||||
@@ -239,6 +250,7 @@
|
|||||||
"LabelGenre": "Genre",
|
"LabelGenre": "Genre",
|
||||||
"LabelGenres": "Žanrovi",
|
"LabelGenres": "Žanrovi",
|
||||||
"LabelHardDeleteFile": "Obriši datoteku zauvijek",
|
"LabelHardDeleteFile": "Obriši datoteku zauvijek",
|
||||||
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Sat",
|
"LabelHour": "Sat",
|
||||||
"LabelIcon": "Ikona",
|
"LabelIcon": "Ikona",
|
||||||
"LabelIncludeInTracklist": "Dodaj u Tracklist",
|
"LabelIncludeInTracklist": "Dodaj u Tracklist",
|
||||||
@@ -324,6 +336,7 @@
|
|||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
"LabelPodcastType": "Podcast Type",
|
"LabelPodcastType": "Podcast Type",
|
||||||
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Prefiksi za ignorirati (mala i velika slova nisu bitna)",
|
"LabelPrefixesToIgnore": "Prefiksi za ignorirati (mala i velika slova nisu bitna)",
|
||||||
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
||||||
"LabelProgress": "Napredak",
|
"LabelProgress": "Napredak",
|
||||||
@@ -331,6 +344,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",
|
||||||
@@ -347,6 +361,7 @@
|
|||||||
"LabelSearchTitle": "Traži naslov",
|
"LabelSearchTitle": "Traži naslov",
|
||||||
"LabelSearchTitleOrASIN": "Traži naslov ili ASIN",
|
"LabelSearchTitleOrASIN": "Traži naslov ili ASIN",
|
||||||
"LabelSeason": "Sezona",
|
"LabelSeason": "Sezona",
|
||||||
|
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||||
"LabelSequence": "Sekvenca",
|
"LabelSequence": "Sekvenca",
|
||||||
"LabelSeries": "Serije",
|
"LabelSeries": "Serije",
|
||||||
"LabelSeriesName": "Ime serije",
|
"LabelSeriesName": "Ime serije",
|
||||||
@@ -483,6 +498,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.",
|
||||||
@@ -490,6 +506,7 @@
|
|||||||
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
|
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
|
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
|
||||||
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
|
||||||
|
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
|
||||||
"MessageDownloadingEpisode": "Preuzimam epizodu",
|
"MessageDownloadingEpisode": "Preuzimam epizodu",
|
||||||
"MessageDragFilesIntoTrackOrder": "Povuci datoteke u pravilan redoslijed tracka.",
|
"MessageDragFilesIntoTrackOrder": "Povuci datoteke u pravilan redoslijed tracka.",
|
||||||
"MessageEmbedFinished": "Embed završen!",
|
"MessageEmbedFinished": "Embed završen!",
|
||||||
@@ -644,6 +661,8 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "Stavka uklonjena iz kolekcije",
|
"ToastRemoveItemFromCollectionSuccess": "Stavka uklonjena iz kolekcije",
|
||||||
"ToastRSSFeedCloseFailed": "Neuspješno zatvaranje RSS Feeda",
|
"ToastRSSFeedCloseFailed": "Neuspješno zatvaranje RSS Feeda",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS Feed zatvoren",
|
"ToastRSSFeedCloseSuccess": "RSS Feed zatvoren",
|
||||||
|
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
|
||||||
|
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
|
||||||
"ToastSeriesUpdateFailed": "Series update failed",
|
"ToastSeriesUpdateFailed": "Series update failed",
|
||||||
"ToastSeriesUpdateSuccess": "Series update success",
|
"ToastSeriesUpdateSuccess": "Series update success",
|
||||||
"ToastSessionDeleteFailed": "Neuspješno brisanje serije",
|
"ToastSessionDeleteFailed": "Neuspješno brisanje serije",
|
||||||
|
|||||||
@@ -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",
|
||||||
@@ -73,6 +74,7 @@
|
|||||||
"ButtonStartM4BEncode": "Inizia L'Encoda del M4B",
|
"ButtonStartM4BEncode": "Inizia L'Encoda del M4B",
|
||||||
"ButtonStartMetadataEmbed": "Inizia Incorporo Metadata",
|
"ButtonStartMetadataEmbed": "Inizia Incorporo Metadata",
|
||||||
"ButtonSubmit": "Invia",
|
"ButtonSubmit": "Invia",
|
||||||
|
"ButtonTest": "Test",
|
||||||
"ButtonUpload": "Carica",
|
"ButtonUpload": "Carica",
|
||||||
"ButtonUploadBackup": "Carica Backup",
|
"ButtonUploadBackup": "Carica Backup",
|
||||||
"ButtonUploadCover": "Carica Cover",
|
"ButtonUploadCover": "Carica Cover",
|
||||||
@@ -96,7 +98,10 @@
|
|||||||
"HeaderCurrentDownloads": "Current Downloads",
|
"HeaderCurrentDownloads": "Current Downloads",
|
||||||
"HeaderDetails": "Dettagli",
|
"HeaderDetails": "Dettagli",
|
||||||
"HeaderDownloadQueue": "Download Queue",
|
"HeaderDownloadQueue": "Download Queue",
|
||||||
|
"HeaderEmail": "Email",
|
||||||
|
"HeaderEmailSettings": "Email Settings",
|
||||||
"HeaderEpisodes": "Episodi",
|
"HeaderEpisodes": "Episodi",
|
||||||
|
"HeaderEReaderDevices": "E-Reader Devices",
|
||||||
"HeaderFiles": "File",
|
"HeaderFiles": "File",
|
||||||
"HeaderFindChapters": "Trova Capitoli",
|
"HeaderFindChapters": "Trova Capitoli",
|
||||||
"HeaderIgnoredFiles": "File Ignorati",
|
"HeaderIgnoredFiles": "File Ignorati",
|
||||||
@@ -197,6 +202,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",
|
||||||
@@ -216,7 +222,12 @@
|
|||||||
"LabelDownload": "Download",
|
"LabelDownload": "Download",
|
||||||
"LabelDuration": "Durata",
|
"LabelDuration": "Durata",
|
||||||
"LabelDurationFound": "Durata Trovata:",
|
"LabelDurationFound": "Durata Trovata:",
|
||||||
|
"LabelEbook": "Ebook",
|
||||||
"LabelEdit": "Modifica",
|
"LabelEdit": "Modifica",
|
||||||
|
"LabelEmail": "Email",
|
||||||
|
"LabelEmailSettingsFromAddress": "From Address",
|
||||||
|
"LabelEmailSettingsSecure": "Secure",
|
||||||
|
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||||
"LabelEmbeddedCover": "Embedded Cover",
|
"LabelEmbeddedCover": "Embedded Cover",
|
||||||
"LabelEnable": "Abilita",
|
"LabelEnable": "Abilita",
|
||||||
"LabelEnd": "Fine",
|
"LabelEnd": "Fine",
|
||||||
@@ -239,6 +250,7 @@
|
|||||||
"LabelGenre": "Genere",
|
"LabelGenre": "Genere",
|
||||||
"LabelGenres": "Generi",
|
"LabelGenres": "Generi",
|
||||||
"LabelHardDeleteFile": "Elimina Definitivamente",
|
"LabelHardDeleteFile": "Elimina Definitivamente",
|
||||||
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Ora",
|
"LabelHour": "Ora",
|
||||||
"LabelIcon": "Icona",
|
"LabelIcon": "Icona",
|
||||||
"LabelIncludeInTracklist": "Includi nella Tracklist",
|
"LabelIncludeInTracklist": "Includi nella Tracklist",
|
||||||
@@ -324,6 +336,7 @@
|
|||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasts",
|
"LabelPodcasts": "Podcasts",
|
||||||
"LabelPodcastType": "Timo di Podcast",
|
"LabelPodcastType": "Timo di Podcast",
|
||||||
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Suffissi da ignorare (specificando maiuscole e minuscole)",
|
"LabelPrefixesToIgnore": "Suffissi da ignorare (specificando maiuscole e minuscole)",
|
||||||
"LabelPreventIndexing": "Impedisci che il tuo feed venga indicizzato da iTunes e dalle directory dei podcast di Google",
|
"LabelPreventIndexing": "Impedisci che il tuo feed venga indicizzato da iTunes e dalle directory dei podcast di Google",
|
||||||
"LabelProgress": "Cominciati",
|
"LabelProgress": "Cominciati",
|
||||||
@@ -331,6 +344,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",
|
||||||
@@ -347,6 +361,7 @@
|
|||||||
"LabelSearchTitle": "Cerca Titolo",
|
"LabelSearchTitle": "Cerca Titolo",
|
||||||
"LabelSearchTitleOrASIN": "Cerca titolo o ASIN",
|
"LabelSearchTitleOrASIN": "Cerca titolo o ASIN",
|
||||||
"LabelSeason": "Stagione",
|
"LabelSeason": "Stagione",
|
||||||
|
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||||
"LabelSequence": "Sequenza",
|
"LabelSequence": "Sequenza",
|
||||||
"LabelSeries": "Serie",
|
"LabelSeries": "Serie",
|
||||||
"LabelSeriesName": "Nome Serie",
|
"LabelSeriesName": "Nome Serie",
|
||||||
@@ -483,6 +498,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.",
|
||||||
@@ -490,6 +506,7 @@
|
|||||||
"MessageConfirmRenameTag": "Sei sicuro di voler rinominare il tag \"{0}\" in \"{1}\" per tutti gli oggetti?",
|
"MessageConfirmRenameTag": "Sei sicuro di voler rinominare il tag \"{0}\" in \"{1}\" per tutti gli oggetti?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Nota: Questo tag esiste già e verrà unito nel vecchio.",
|
"MessageConfirmRenameTagMergeNote": "Nota: Questo tag esiste già e verrà unito nel vecchio.",
|
||||||
"MessageConfirmRenameTagWarning": "Avvertimento! Esiste già un tag simile con un nome simile \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Avvertimento! Esiste già un tag simile con un nome simile \"{0}\".",
|
||||||
|
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
|
||||||
"MessageDownloadingEpisode": "Download episodio in corso",
|
"MessageDownloadingEpisode": "Download episodio in corso",
|
||||||
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
|
"MessageDragFilesIntoTrackOrder": "Trascina i file nell'ordine di traccia corretto",
|
||||||
"MessageEmbedFinished": "Incorporamento finito!",
|
"MessageEmbedFinished": "Incorporamento finito!",
|
||||||
@@ -644,6 +661,8 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "Oggetto rimosso dalla Raccolta",
|
"ToastRemoveItemFromCollectionSuccess": "Oggetto rimosso dalla Raccolta",
|
||||||
"ToastRSSFeedCloseFailed": "Errore chiusura RSS feed",
|
"ToastRSSFeedCloseFailed": "Errore chiusura RSS feed",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS feed chiuso",
|
"ToastRSSFeedCloseSuccess": "RSS feed chiuso",
|
||||||
|
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
|
||||||
|
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
|
||||||
"ToastSeriesUpdateFailed": "Aggiornaento Serie Fallito",
|
"ToastSeriesUpdateFailed": "Aggiornaento Serie Fallito",
|
||||||
"ToastSeriesUpdateSuccess": "Serie Aggornate",
|
"ToastSeriesUpdateSuccess": "Serie Aggornate",
|
||||||
"ToastSessionDeleteFailed": "Errore eliminazione sessione",
|
"ToastSessionDeleteFailed": "Errore eliminazione sessione",
|
||||||
|
|||||||
@@ -0,0 +1,675 @@
|
|||||||
|
{
|
||||||
|
"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",
|
||||||
|
"ButtonTest": "Test",
|
||||||
|
"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",
|
||||||
|
"HeaderEmail": "Email",
|
||||||
|
"HeaderEmailSettings": "Email Settings",
|
||||||
|
"HeaderEpisodes": "Afleveringen",
|
||||||
|
"HeaderEReaderDevices": "E-Reader Devices",
|
||||||
|
"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:",
|
||||||
|
"LabelEbook": "Ebook",
|
||||||
|
"LabelEdit": "Wijzig",
|
||||||
|
"LabelEmail": "Email",
|
||||||
|
"LabelEmailSettingsFromAddress": "From Address",
|
||||||
|
"LabelEmailSettingsSecure": "Secure",
|
||||||
|
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||||
|
"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",
|
||||||
|
"LabelHost": "Host",
|
||||||
|
"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",
|
||||||
|
"LabelPort": "Port",
|
||||||
|
"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",
|
||||||
|
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||||
|
"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}\".",
|
||||||
|
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
|
||||||
|
"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",
|
||||||
|
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
|
||||||
|
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
|
||||||
|
"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",
|
||||||
@@ -73,6 +74,7 @@
|
|||||||
"ButtonStartM4BEncode": "Eksportuj jako plik M4B",
|
"ButtonStartM4BEncode": "Eksportuj jako plik M4B",
|
||||||
"ButtonStartMetadataEmbed": "Osadź metadane",
|
"ButtonStartMetadataEmbed": "Osadź metadane",
|
||||||
"ButtonSubmit": "Zaloguj",
|
"ButtonSubmit": "Zaloguj",
|
||||||
|
"ButtonTest": "Test",
|
||||||
"ButtonUpload": "Wgraj",
|
"ButtonUpload": "Wgraj",
|
||||||
"ButtonUploadBackup": "Wgraj kopię zapasową",
|
"ButtonUploadBackup": "Wgraj kopię zapasową",
|
||||||
"ButtonUploadCover": "Wgraj okładkę",
|
"ButtonUploadCover": "Wgraj okładkę",
|
||||||
@@ -96,7 +98,10 @@
|
|||||||
"HeaderCurrentDownloads": "Current Downloads",
|
"HeaderCurrentDownloads": "Current Downloads",
|
||||||
"HeaderDetails": "Szczegóły",
|
"HeaderDetails": "Szczegóły",
|
||||||
"HeaderDownloadQueue": "Download Queue",
|
"HeaderDownloadQueue": "Download Queue",
|
||||||
|
"HeaderEmail": "Email",
|
||||||
|
"HeaderEmailSettings": "Email Settings",
|
||||||
"HeaderEpisodes": "Rozdziały",
|
"HeaderEpisodes": "Rozdziały",
|
||||||
|
"HeaderEReaderDevices": "E-Reader Devices",
|
||||||
"HeaderFiles": "Pliki",
|
"HeaderFiles": "Pliki",
|
||||||
"HeaderFindChapters": "Wyszukaj rozdziały",
|
"HeaderFindChapters": "Wyszukaj rozdziały",
|
||||||
"HeaderIgnoredFiles": "Zignoruj pliki",
|
"HeaderIgnoredFiles": "Zignoruj pliki",
|
||||||
@@ -197,6 +202,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",
|
||||||
@@ -216,7 +222,12 @@
|
|||||||
"LabelDownload": "Pobierz",
|
"LabelDownload": "Pobierz",
|
||||||
"LabelDuration": "Czas trwania",
|
"LabelDuration": "Czas trwania",
|
||||||
"LabelDurationFound": "Znaleziona długość:",
|
"LabelDurationFound": "Znaleziona długość:",
|
||||||
|
"LabelEbook": "Ebook",
|
||||||
"LabelEdit": "Edytuj",
|
"LabelEdit": "Edytuj",
|
||||||
|
"LabelEmail": "Email",
|
||||||
|
"LabelEmailSettingsFromAddress": "From Address",
|
||||||
|
"LabelEmailSettingsSecure": "Secure",
|
||||||
|
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||||
"LabelEmbeddedCover": "Embedded Cover",
|
"LabelEmbeddedCover": "Embedded Cover",
|
||||||
"LabelEnable": "Włącz",
|
"LabelEnable": "Włącz",
|
||||||
"LabelEnd": "Zakończ",
|
"LabelEnd": "Zakończ",
|
||||||
@@ -239,6 +250,7 @@
|
|||||||
"LabelGenre": "Gatunek",
|
"LabelGenre": "Gatunek",
|
||||||
"LabelGenres": "Gatunki",
|
"LabelGenres": "Gatunki",
|
||||||
"LabelHardDeleteFile": "Usuń trwale plik",
|
"LabelHardDeleteFile": "Usuń trwale plik",
|
||||||
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Godzina",
|
"LabelHour": "Godzina",
|
||||||
"LabelIcon": "Ikona",
|
"LabelIcon": "Ikona",
|
||||||
"LabelIncludeInTracklist": "Dołącz do listy odtwarzania",
|
"LabelIncludeInTracklist": "Dołącz do listy odtwarzania",
|
||||||
@@ -324,6 +336,7 @@
|
|||||||
"LabelPodcast": "Podcast",
|
"LabelPodcast": "Podcast",
|
||||||
"LabelPodcasts": "Podcasty",
|
"LabelPodcasts": "Podcasty",
|
||||||
"LabelPodcastType": "Podcast Type",
|
"LabelPodcastType": "Podcast Type",
|
||||||
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Ignorowane prefiksy (wielkość liter nie ma znaczenia)",
|
"LabelPrefixesToIgnore": "Ignorowane prefiksy (wielkość liter nie ma znaczenia)",
|
||||||
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
"LabelPreventIndexing": "Prevent your feed from being indexed by iTunes and Google podcast directories",
|
||||||
"LabelProgress": "Postęp",
|
"LabelProgress": "Postęp",
|
||||||
@@ -331,6 +344,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",
|
||||||
@@ -347,6 +361,7 @@
|
|||||||
"LabelSearchTitle": "Wyszukaj tytuł",
|
"LabelSearchTitle": "Wyszukaj tytuł",
|
||||||
"LabelSearchTitleOrASIN": "Szukaj tytuł lub ASIN",
|
"LabelSearchTitleOrASIN": "Szukaj tytuł lub ASIN",
|
||||||
"LabelSeason": "Sezon",
|
"LabelSeason": "Sezon",
|
||||||
|
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||||
"LabelSequence": "Kolejność",
|
"LabelSequence": "Kolejność",
|
||||||
"LabelSeries": "Serie",
|
"LabelSeries": "Serie",
|
||||||
"LabelSeriesName": "Nazwy serii",
|
"LabelSeriesName": "Nazwy serii",
|
||||||
@@ -483,6 +498,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.",
|
||||||
@@ -490,6 +506,7 @@
|
|||||||
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
|
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
|
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
|
||||||
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
|
||||||
|
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
|
||||||
"MessageDownloadingEpisode": "Pobieranie odcinka",
|
"MessageDownloadingEpisode": "Pobieranie odcinka",
|
||||||
"MessageDragFilesIntoTrackOrder": "przeciągnij pliki aby ustawić właściwą kolejność utworów",
|
"MessageDragFilesIntoTrackOrder": "przeciągnij pliki aby ustawić właściwą kolejność utworów",
|
||||||
"MessageEmbedFinished": "Osadzanie zakończone!",
|
"MessageEmbedFinished": "Osadzanie zakończone!",
|
||||||
@@ -644,6 +661,8 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "Pozycja usunięta z kolekcji",
|
"ToastRemoveItemFromCollectionSuccess": "Pozycja usunięta z kolekcji",
|
||||||
"ToastRSSFeedCloseFailed": "Zamknięcie kanału RSS nie powiodło się",
|
"ToastRSSFeedCloseFailed": "Zamknięcie kanału RSS nie powiodło się",
|
||||||
"ToastRSSFeedCloseSuccess": "Zamknięcie kanału RSS powiodło się",
|
"ToastRSSFeedCloseSuccess": "Zamknięcie kanału RSS powiodło się",
|
||||||
|
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
|
||||||
|
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
|
||||||
"ToastSeriesUpdateFailed": "Series update failed",
|
"ToastSeriesUpdateFailed": "Series update failed",
|
||||||
"ToastSeriesUpdateSuccess": "Series update success",
|
"ToastSeriesUpdateSuccess": "Series update success",
|
||||||
"ToastSessionDeleteFailed": "Nie udało się usunąć sesji",
|
"ToastSessionDeleteFailed": "Nie udało się usunąć sesji",
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "Удалить всё",
|
"ButtonRemoveAll": "Удалить всё",
|
||||||
"ButtonRemoveAllLibraryItems": "Удалить все элементы библиотеки",
|
"ButtonRemoveAllLibraryItems": "Удалить все элементы библиотеки",
|
||||||
"ButtonRemoveFromContinueListening": "Удалить из Продолжить слушать",
|
"ButtonRemoveFromContinueListening": "Удалить из Продолжить слушать",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Удалить серию из Продолжить серию",
|
"ButtonRemoveSeriesFromContinueSeries": "Удалить серию из Продолжить серию",
|
||||||
"ButtonReScan": "Пересканировать",
|
"ButtonReScan": "Пересканировать",
|
||||||
"ButtonReset": "Сбросить",
|
"ButtonReset": "Сбросить",
|
||||||
@@ -73,6 +74,7 @@
|
|||||||
"ButtonStartM4BEncode": "Начать кодирование M4B",
|
"ButtonStartM4BEncode": "Начать кодирование M4B",
|
||||||
"ButtonStartMetadataEmbed": "Начать встраивание метаданных",
|
"ButtonStartMetadataEmbed": "Начать встраивание метаданных",
|
||||||
"ButtonSubmit": "Применить",
|
"ButtonSubmit": "Применить",
|
||||||
|
"ButtonTest": "Test",
|
||||||
"ButtonUpload": "Загрузить",
|
"ButtonUpload": "Загрузить",
|
||||||
"ButtonUploadBackup": "Загрузить бэкап",
|
"ButtonUploadBackup": "Загрузить бэкап",
|
||||||
"ButtonUploadCover": "Загрузить обложку",
|
"ButtonUploadCover": "Загрузить обложку",
|
||||||
@@ -96,7 +98,10 @@
|
|||||||
"HeaderCurrentDownloads": "Текущие закачки",
|
"HeaderCurrentDownloads": "Текущие закачки",
|
||||||
"HeaderDetails": "Подробности",
|
"HeaderDetails": "Подробности",
|
||||||
"HeaderDownloadQueue": "Очередь скачивания",
|
"HeaderDownloadQueue": "Очередь скачивания",
|
||||||
|
"HeaderEmail": "Email",
|
||||||
|
"HeaderEmailSettings": "Email Settings",
|
||||||
"HeaderEpisodes": "Эпизоды",
|
"HeaderEpisodes": "Эпизоды",
|
||||||
|
"HeaderEReaderDevices": "E-Reader Devices",
|
||||||
"HeaderFiles": "Файлы",
|
"HeaderFiles": "Файлы",
|
||||||
"HeaderFindChapters": "Найти главы",
|
"HeaderFindChapters": "Найти главы",
|
||||||
"HeaderIgnoredFiles": "Игнорируемые Файлы",
|
"HeaderIgnoredFiles": "Игнорируемые Файлы",
|
||||||
@@ -197,6 +202,7 @@
|
|||||||
"LabelComplete": "Завершить",
|
"LabelComplete": "Завершить",
|
||||||
"LabelConfirmPassword": "Подтвердить пароль",
|
"LabelConfirmPassword": "Подтвердить пароль",
|
||||||
"LabelContinueListening": "Продолжить слушать",
|
"LabelContinueListening": "Продолжить слушать",
|
||||||
|
"LabelContinueReading": "Continue Reading",
|
||||||
"LabelContinueSeries": "Продолжить серию",
|
"LabelContinueSeries": "Продолжить серию",
|
||||||
"LabelCover": "Обложка",
|
"LabelCover": "Обложка",
|
||||||
"LabelCoverImageURL": "URL изображения обложки",
|
"LabelCoverImageURL": "URL изображения обложки",
|
||||||
@@ -216,7 +222,12 @@
|
|||||||
"LabelDownload": "Скачать",
|
"LabelDownload": "Скачать",
|
||||||
"LabelDuration": "Длина",
|
"LabelDuration": "Длина",
|
||||||
"LabelDurationFound": "Найденная длина:",
|
"LabelDurationFound": "Найденная длина:",
|
||||||
|
"LabelEbook": "Ebook",
|
||||||
"LabelEdit": "Редактировать",
|
"LabelEdit": "Редактировать",
|
||||||
|
"LabelEmail": "Email",
|
||||||
|
"LabelEmailSettingsFromAddress": "From Address",
|
||||||
|
"LabelEmailSettingsSecure": "Secure",
|
||||||
|
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||||
"LabelEmbeddedCover": "Embedded Cover",
|
"LabelEmbeddedCover": "Embedded Cover",
|
||||||
"LabelEnable": "Включить",
|
"LabelEnable": "Включить",
|
||||||
"LabelEnd": "Конец",
|
"LabelEnd": "Конец",
|
||||||
@@ -239,6 +250,7 @@
|
|||||||
"LabelGenre": "Жанр",
|
"LabelGenre": "Жанр",
|
||||||
"LabelGenres": "Жанры",
|
"LabelGenres": "Жанры",
|
||||||
"LabelHardDeleteFile": "Жесткое удаление файла",
|
"LabelHardDeleteFile": "Жесткое удаление файла",
|
||||||
|
"LabelHost": "Host",
|
||||||
"LabelHour": "Часы",
|
"LabelHour": "Часы",
|
||||||
"LabelIcon": "Иконка",
|
"LabelIcon": "Иконка",
|
||||||
"LabelIncludeInTracklist": "Включать в список воспроизведения",
|
"LabelIncludeInTracklist": "Включать в список воспроизведения",
|
||||||
@@ -324,6 +336,7 @@
|
|||||||
"LabelPodcast": "Подкаст",
|
"LabelPodcast": "Подкаст",
|
||||||
"LabelPodcasts": "Подкасты",
|
"LabelPodcasts": "Подкасты",
|
||||||
"LabelPodcastType": "Тип подкаста",
|
"LabelPodcastType": "Тип подкаста",
|
||||||
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "Игнорируемые префиксы (без учета регистра)",
|
"LabelPrefixesToIgnore": "Игнорируемые префиксы (без учета регистра)",
|
||||||
"LabelPreventIndexing": "Запретить индексацию фида каталогами подкастов iTunes и Google",
|
"LabelPreventIndexing": "Запретить индексацию фида каталогами подкастов iTunes и Google",
|
||||||
"LabelProgress": "Прогресс",
|
"LabelProgress": "Прогресс",
|
||||||
@@ -331,6 +344,7 @@
|
|||||||
"LabelPubDate": "Дата публикации",
|
"LabelPubDate": "Дата публикации",
|
||||||
"LabelPublisher": "Издатель",
|
"LabelPublisher": "Издатель",
|
||||||
"LabelPublishYear": "Год публикации",
|
"LabelPublishYear": "Год публикации",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "Недавно добавленные",
|
"LabelRecentlyAdded": "Недавно добавленные",
|
||||||
"LabelRecentSeries": "Последние серии",
|
"LabelRecentSeries": "Последние серии",
|
||||||
"LabelRecommended": "Рекомендованное",
|
"LabelRecommended": "Рекомендованное",
|
||||||
@@ -347,6 +361,7 @@
|
|||||||
"LabelSearchTitle": "Поиск по названию",
|
"LabelSearchTitle": "Поиск по названию",
|
||||||
"LabelSearchTitleOrASIN": "Поиск по названию или ASIN",
|
"LabelSearchTitleOrASIN": "Поиск по названию или ASIN",
|
||||||
"LabelSeason": "Сезон",
|
"LabelSeason": "Сезон",
|
||||||
|
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||||
"LabelSequence": "Последовательность",
|
"LabelSequence": "Последовательность",
|
||||||
"LabelSeries": "Серия",
|
"LabelSeries": "Серия",
|
||||||
"LabelSeriesName": "Имя серии",
|
"LabelSeriesName": "Имя серии",
|
||||||
@@ -483,6 +498,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": "Примечание: Этот жанр уже существует, поэтому они будут объединены.",
|
||||||
@@ -490,6 +506,7 @@
|
|||||||
"MessageConfirmRenameTag": "Вы уверены, что хотите переименовать тег \"{0}\" в \"{1}\" для всех элементов?",
|
"MessageConfirmRenameTag": "Вы уверены, что хотите переименовать тег \"{0}\" в \"{1}\" для всех элементов?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Примечание: Этот тег уже существует, поэтому они будут объединены.",
|
"MessageConfirmRenameTagMergeNote": "Примечание: Этот тег уже существует, поэтому они будут объединены.",
|
||||||
"MessageConfirmRenameTagWarning": "Предупреждение! Похожий тег с другими начальными буквами уже существует \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Предупреждение! Похожий тег с другими начальными буквами уже существует \"{0}\".",
|
||||||
|
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
|
||||||
"MessageDownloadingEpisode": "Эпизод скачивается",
|
"MessageDownloadingEpisode": "Эпизод скачивается",
|
||||||
"MessageDragFilesIntoTrackOrder": "Перетащите файлы для исправления порядка треков",
|
"MessageDragFilesIntoTrackOrder": "Перетащите файлы для исправления порядка треков",
|
||||||
"MessageEmbedFinished": "Встраивание завершено!",
|
"MessageEmbedFinished": "Встраивание завершено!",
|
||||||
@@ -644,6 +661,8 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "Элемент удален из коллекции",
|
"ToastRemoveItemFromCollectionSuccess": "Элемент удален из коллекции",
|
||||||
"ToastRSSFeedCloseFailed": "Не удалось закрыть RSS-канал",
|
"ToastRSSFeedCloseFailed": "Не удалось закрыть RSS-канал",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS-канал закрыт",
|
"ToastRSSFeedCloseSuccess": "RSS-канал закрыт",
|
||||||
|
"ToastSendEbookToDeviceFailed": "Failed to Send Ebook to device",
|
||||||
|
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
|
||||||
"ToastSeriesUpdateFailed": "Не удалось обновить серию",
|
"ToastSeriesUpdateFailed": "Не удалось обновить серию",
|
||||||
"ToastSeriesUpdateSuccess": "Успешное обновление серии",
|
"ToastSeriesUpdateSuccess": "Успешное обновление серии",
|
||||||
"ToastSessionDeleteFailed": "Не удалось удалить сеанс",
|
"ToastSessionDeleteFailed": "Не удалось удалить сеанс",
|
||||||
|
|||||||
+37
-18
@@ -55,6 +55,7 @@
|
|||||||
"ButtonRemoveAll": "移除所有",
|
"ButtonRemoveAll": "移除所有",
|
||||||
"ButtonRemoveAllLibraryItems": "移除所有媒体库项目",
|
"ButtonRemoveAllLibraryItems": "移除所有媒体库项目",
|
||||||
"ButtonRemoveFromContinueListening": "从继续收听中删除",
|
"ButtonRemoveFromContinueListening": "从继续收听中删除",
|
||||||
|
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "从继续收听系列中删除",
|
"ButtonRemoveSeriesFromContinueSeries": "从继续收听系列中删除",
|
||||||
"ButtonReScan": "重新扫描",
|
"ButtonReScan": "重新扫描",
|
||||||
"ButtonReset": "重置",
|
"ButtonReset": "重置",
|
||||||
@@ -73,6 +74,7 @@
|
|||||||
"ButtonStartM4BEncode": "开始 M4B 编码",
|
"ButtonStartM4BEncode": "开始 M4B 编码",
|
||||||
"ButtonStartMetadataEmbed": "开始嵌入元数据",
|
"ButtonStartMetadataEmbed": "开始嵌入元数据",
|
||||||
"ButtonSubmit": "提交",
|
"ButtonSubmit": "提交",
|
||||||
|
"ButtonTest": "Test",
|
||||||
"ButtonUpload": "上传",
|
"ButtonUpload": "上传",
|
||||||
"ButtonUploadBackup": "上传备份",
|
"ButtonUploadBackup": "上传备份",
|
||||||
"ButtonUploadCover": "上传封面",
|
"ButtonUploadCover": "上传封面",
|
||||||
@@ -96,7 +98,10 @@
|
|||||||
"HeaderCurrentDownloads": "当前下载",
|
"HeaderCurrentDownloads": "当前下载",
|
||||||
"HeaderDetails": "详情",
|
"HeaderDetails": "详情",
|
||||||
"HeaderDownloadQueue": "下载队列",
|
"HeaderDownloadQueue": "下载队列",
|
||||||
|
"HeaderEmail": "Email",
|
||||||
|
"HeaderEmailSettings": "Email Settings",
|
||||||
"HeaderEpisodes": "剧集",
|
"HeaderEpisodes": "剧集",
|
||||||
|
"HeaderEReaderDevices": "E-Reader Devices",
|
||||||
"HeaderFiles": "文件",
|
"HeaderFiles": "文件",
|
||||||
"HeaderFindChapters": "查找章节",
|
"HeaderFindChapters": "查找章节",
|
||||||
"HeaderIgnoredFiles": "忽略的文件",
|
"HeaderIgnoredFiles": "忽略的文件",
|
||||||
@@ -155,13 +160,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 +188,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",
|
||||||
@@ -216,8 +222,13 @@
|
|||||||
"LabelDownload": "下载",
|
"LabelDownload": "下载",
|
||||||
"LabelDuration": "持续时间",
|
"LabelDuration": "持续时间",
|
||||||
"LabelDurationFound": "找到持续时间:",
|
"LabelDurationFound": "找到持续时间:",
|
||||||
|
"LabelEbook": "Ebook",
|
||||||
"LabelEdit": "编辑",
|
"LabelEdit": "编辑",
|
||||||
"LabelEmbeddedCover": "Embedded Cover",
|
"LabelEmail": "Email",
|
||||||
|
"LabelEmailSettingsFromAddress": "From Address",
|
||||||
|
"LabelEmailSettingsSecure": "Secure",
|
||||||
|
"LabelEmailSettingsSecureHelp": "If true the connection will use TLS when connecting to server. If false then TLS is used if server supports the STARTTLS extension. In most cases set this value to true if you are connecting to port 465. For port 587 or 25 keep it false. (from nodemailer.com/smtp/#authentication)",
|
||||||
|
"LabelEmbeddedCover": "嵌入封面",
|
||||||
"LabelEnable": "启用",
|
"LabelEnable": "启用",
|
||||||
"LabelEnd": "结束",
|
"LabelEnd": "结束",
|
||||||
"LabelEpisode": "剧集",
|
"LabelEpisode": "剧集",
|
||||||
@@ -235,10 +246,11 @@
|
|||||||
"LabelFinished": "已听完",
|
"LabelFinished": "已听完",
|
||||||
"LabelFolder": "文件夹",
|
"LabelFolder": "文件夹",
|
||||||
"LabelFolders": "文件夹",
|
"LabelFolders": "文件夹",
|
||||||
"LabelFormat": "Format",
|
"LabelFormat": "编码格式",
|
||||||
"LabelGenre": "流派",
|
"LabelGenre": "流派",
|
||||||
"LabelGenres": "流派",
|
"LabelGenres": "流派",
|
||||||
"LabelHardDeleteFile": "完全删除文件",
|
"LabelHardDeleteFile": "完全删除文件",
|
||||||
|
"LabelHost": "Host",
|
||||||
"LabelHour": "小时",
|
"LabelHour": "小时",
|
||||||
"LabelIcon": "图标",
|
"LabelIcon": "图标",
|
||||||
"LabelIncludeInTracklist": "包含在音轨列表中",
|
"LabelIncludeInTracklist": "包含在音轨列表中",
|
||||||
@@ -254,12 +266,12 @@
|
|||||||
"LabelIntervalEveryDay": "每天",
|
"LabelIntervalEveryDay": "每天",
|
||||||
"LabelIntervalEveryHour": "每小时",
|
"LabelIntervalEveryHour": "每小时",
|
||||||
"LabelInvalidParts": "无效部件",
|
"LabelInvalidParts": "无效部件",
|
||||||
"LabelInvert": "Invert",
|
"LabelInvert": "倒转",
|
||||||
"LabelItem": "项目",
|
"LabelItem": "项目",
|
||||||
"LabelLanguage": "语言",
|
"LabelLanguage": "语言",
|
||||||
"LabelLanguageDefaultServer": "默认服务器语言",
|
"LabelLanguageDefaultServer": "默认服务器语言",
|
||||||
"LabelLastBookAdded": "Last Book Added",
|
"LabelLastBookAdded": "最后添加的书",
|
||||||
"LabelLastBookUpdated": "Last Book Updated",
|
"LabelLastBookUpdated": "最后更新的书",
|
||||||
"LabelLastSeen": "上次查看时间",
|
"LabelLastSeen": "上次查看时间",
|
||||||
"LabelLastTime": "最近一次",
|
"LabelLastTime": "最近一次",
|
||||||
"LabelLastUpdate": "最近更新",
|
"LabelLastUpdate": "最近更新",
|
||||||
@@ -278,12 +290,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": "演播者",
|
||||||
@@ -324,6 +336,7 @@
|
|||||||
"LabelPodcast": "播客",
|
"LabelPodcast": "播客",
|
||||||
"LabelPodcasts": "播客",
|
"LabelPodcasts": "播客",
|
||||||
"LabelPodcastType": "播客类型",
|
"LabelPodcastType": "播客类型",
|
||||||
|
"LabelPort": "Port",
|
||||||
"LabelPrefixesToIgnore": "忽略的前缀 (不区分大小写)",
|
"LabelPrefixesToIgnore": "忽略的前缀 (不区分大小写)",
|
||||||
"LabelPreventIndexing": "防止 iTunes 和 Google 播客目录对你的源进行索引",
|
"LabelPreventIndexing": "防止 iTunes 和 Google 播客目录对你的源进行索引",
|
||||||
"LabelProgress": "进度",
|
"LabelProgress": "进度",
|
||||||
@@ -331,6 +344,7 @@
|
|||||||
"LabelPubDate": "出版日期",
|
"LabelPubDate": "出版日期",
|
||||||
"LabelPublisher": "出版商",
|
"LabelPublisher": "出版商",
|
||||||
"LabelPublishYear": "发布年份",
|
"LabelPublishYear": "发布年份",
|
||||||
|
"LabelReadAgain": "Read Again",
|
||||||
"LabelRecentlyAdded": "最近添加",
|
"LabelRecentlyAdded": "最近添加",
|
||||||
"LabelRecentSeries": "最近添加系列",
|
"LabelRecentSeries": "最近添加系列",
|
||||||
"LabelRecommended": "推荐内容",
|
"LabelRecommended": "推荐内容",
|
||||||
@@ -347,6 +361,7 @@
|
|||||||
"LabelSearchTitle": "搜索标题",
|
"LabelSearchTitle": "搜索标题",
|
||||||
"LabelSearchTitleOrASIN": "搜索标题或 ASIN",
|
"LabelSearchTitleOrASIN": "搜索标题或 ASIN",
|
||||||
"LabelSeason": "季",
|
"LabelSeason": "季",
|
||||||
|
"LabelSendEbookToDevice": "Send Ebook to...",
|
||||||
"LabelSequence": "序列",
|
"LabelSequence": "序列",
|
||||||
"LabelSeries": "系列",
|
"LabelSeries": "系列",
|
||||||
"LabelSeriesName": "系列名称",
|
"LabelSeriesName": "系列名称",
|
||||||
@@ -413,9 +428,9 @@
|
|||||||
"LabelTag": "标签",
|
"LabelTag": "标签",
|
||||||
"LabelTags": "标签",
|
"LabelTags": "标签",
|
||||||
"LabelTagsAccessibleToUser": "用户可访问的标签",
|
"LabelTagsAccessibleToUser": "用户可访问的标签",
|
||||||
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
|
"LabelTagsNotAccessibleToUser": "用户无法访问标签",
|
||||||
"LabelTasks": "正在运行的任务",
|
"LabelTasks": "正在运行的任务",
|
||||||
"LabelTimeBase": "Time Base",
|
"LabelTimeBase": "时间基准",
|
||||||
"LabelTimeListened": "收听时间",
|
"LabelTimeListened": "收听时间",
|
||||||
"LabelTimeListenedToday": "今日收听的时间",
|
"LabelTimeListenedToday": "今日收听的时间",
|
||||||
"LabelTimeRemaining": "剩余 {0}",
|
"LabelTimeRemaining": "剩余 {0}",
|
||||||
@@ -435,7 +450,7 @@
|
|||||||
"LabelTracksMultiTrack": "多轨",
|
"LabelTracksMultiTrack": "多轨",
|
||||||
"LabelTracksSingleTrack": "单轨",
|
"LabelTracksSingleTrack": "单轨",
|
||||||
"LabelType": "类型",
|
"LabelType": "类型",
|
||||||
"LabelUnabridged": "Unabridged",
|
"LabelUnabridged": "未删节",
|
||||||
"LabelUnknown": "未知",
|
"LabelUnknown": "未知",
|
||||||
"LabelUpdateCover": "更新封面",
|
"LabelUpdateCover": "更新封面",
|
||||||
"LabelUpdateCoverHelp": "找到匹配项时允许覆盖所选书籍存在的封面",
|
"LabelUpdateCoverHelp": "找到匹配项时允许覆盖所选书籍存在的封面",
|
||||||
@@ -479,10 +494,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": "注意: 该流派已经存在, 因此它们将被合并.",
|
||||||
@@ -490,6 +506,7 @@
|
|||||||
"MessageConfirmRenameTag": "你确定要将所有项目标签 \"{0}\" 重命名到 \"{1}\"?",
|
"MessageConfirmRenameTag": "你确定要将所有项目标签 \"{0}\" 重命名到 \"{1}\"?",
|
||||||
"MessageConfirmRenameTagMergeNote": "注意: 该标签已经存在, 因此它们将被合并.",
|
"MessageConfirmRenameTagMergeNote": "注意: 该标签已经存在, 因此它们将被合并.",
|
||||||
"MessageConfirmRenameTagWarning": "警告! 已经存在有大小写不同的类似标签 \"{0}\".",
|
"MessageConfirmRenameTagWarning": "警告! 已经存在有大小写不同的类似标签 \"{0}\".",
|
||||||
|
"MessageConfirmSendEbookToDevice": "Are you sure you want to send {0} ebook \"{1}\" to device \"{2}\"?",
|
||||||
"MessageDownloadingEpisode": "正在下载剧集",
|
"MessageDownloadingEpisode": "正在下载剧集",
|
||||||
"MessageDragFilesIntoTrackOrder": "将文件拖动到正确的音轨顺序",
|
"MessageDragFilesIntoTrackOrder": "将文件拖动到正确的音轨顺序",
|
||||||
"MessageEmbedFinished": "嵌入完成!",
|
"MessageEmbedFinished": "嵌入完成!",
|
||||||
@@ -583,7 +600,7 @@
|
|||||||
"PlaceholderNewFolderPath": "输入文件夹路径",
|
"PlaceholderNewFolderPath": "输入文件夹路径",
|
||||||
"PlaceholderNewPlaylist": "输入播放列表名称",
|
"PlaceholderNewPlaylist": "输入播放列表名称",
|
||||||
"PlaceholderSearch": "查找..",
|
"PlaceholderSearch": "查找..",
|
||||||
"PlaceholderSearchEpisode": "Search episode..",
|
"PlaceholderSearchEpisode": "搜索剧集..",
|
||||||
"ToastAccountUpdateFailed": "账户更新失败",
|
"ToastAccountUpdateFailed": "账户更新失败",
|
||||||
"ToastAccountUpdateSuccess": "帐户已更新",
|
"ToastAccountUpdateSuccess": "帐户已更新",
|
||||||
"ToastAuthorImageRemoveFailed": "作者图像删除失败",
|
"ToastAuthorImageRemoveFailed": "作者图像删除失败",
|
||||||
@@ -644,6 +661,8 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "项目已从收藏中删除",
|
"ToastRemoveItemFromCollectionSuccess": "项目已从收藏中删除",
|
||||||
"ToastRSSFeedCloseFailed": "关闭 RSS 源失败",
|
"ToastRSSFeedCloseFailed": "关闭 RSS 源失败",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS 源已关闭",
|
"ToastRSSFeedCloseSuccess": "RSS 源已关闭",
|
||||||
|
"ToastSendEbookToDeviceFailed": "Failed to send Ebook to device",
|
||||||
|
"ToastSendEbookToDeviceSuccess": "Ebook sent to device \"{0}\"",
|
||||||
"ToastSeriesUpdateFailed": "更新系列失败",
|
"ToastSeriesUpdateFailed": "更新系列失败",
|
||||||
"ToastSeriesUpdateSuccess": "系列已更新",
|
"ToastSeriesUpdateSuccess": "系列已更新",
|
||||||
"ToastSessionDeleteFailed": "删除会话失败",
|
"ToastSessionDeleteFailed": "删除会话失败",
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ if (isDev) {
|
|||||||
if (devEnv.FFProbePath) process.env.FFPROBE_PATH = devEnv.FFProbePath
|
if (devEnv.FFProbePath) process.env.FFPROBE_PATH = devEnv.FFProbePath
|
||||||
process.env.SOURCE = 'local'
|
process.env.SOURCE = 'local'
|
||||||
process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath || ''
|
process.env.ROUTER_BASE_PATH = devEnv.RouterBasePath || ''
|
||||||
process.env.CLIENT_PORT = devEnv.ClientPort || 8080
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PORT = process.env.PORT || 80
|
const PORT = process.env.PORT || 80
|
||||||
@@ -23,9 +22,8 @@ const UID = process.env.AUDIOBOOKSHELF_UID
|
|||||||
const GID = process.env.AUDIOBOOKSHELF_GID
|
const GID = process.env.AUDIOBOOKSHELF_GID
|
||||||
const SOURCE = process.env.SOURCE || 'docker'
|
const SOURCE = process.env.SOURCE || 'docker'
|
||||||
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || ''
|
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || ''
|
||||||
const CLIENT_PORT = process.env.CLIENT_PORT || 3000
|
|
||||||
|
|
||||||
console.log('Config', CONFIG_PATH, METADATA_PATH)
|
console.log('Config', CONFIG_PATH, METADATA_PATH)
|
||||||
|
|
||||||
const Server = new server(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH, CLIENT_PORT)
|
const Server = new server(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH)
|
||||||
Server.start()
|
Server.start()
|
||||||
|
|||||||
Generated
+33
-167
@@ -1,20 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.19",
|
"version": "2.2.22",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.19",
|
"version": "2.2.22",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.27.2",
|
"axios": "^0.27.2",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"graceful-fs": "^4.2.10",
|
"graceful-fs": "^4.2.10",
|
||||||
"htmlparser2": "^8.0.1",
|
"htmlparser2": "^8.0.1",
|
||||||
"http-proxy-middleware": "^3.0.0-beta.1",
|
|
||||||
"node-tone": "^1.0.1",
|
"node-tone": "^1.0.1",
|
||||||
|
"nodemailer": "^6.9.2",
|
||||||
"socket.io": "^4.5.4",
|
"socket.io": "^4.5.4",
|
||||||
"xml2js": "^0.5.0"
|
"xml2js": "^0.5.0"
|
||||||
},
|
},
|
||||||
@@ -43,14 +43,6 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/http-proxy": {
|
|
||||||
"version": "1.17.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.11.tgz",
|
|
||||||
"integrity": "sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA==",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "18.11.18",
|
"version": "18.11.18",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||||
@@ -166,6 +158,7 @@
|
|||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fill-range": "^7.0.1"
|
"fill-range": "^7.0.1"
|
||||||
},
|
},
|
||||||
@@ -459,11 +452,6 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eventemitter3": {
|
|
||||||
"version": "4.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
|
||||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
|
|
||||||
},
|
|
||||||
"node_modules/express": {
|
"node_modules/express": {
|
||||||
"version": "4.18.2",
|
"version": "4.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
||||||
@@ -509,6 +497,7 @@
|
|||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"to-regex-range": "^5.0.1"
|
"to-regex-range": "^5.0.1"
|
||||||
},
|
},
|
||||||
@@ -694,56 +683,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/http-proxy": {
|
|
||||||
"version": "1.18.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
|
||||||
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"eventemitter3": "^4.0.0",
|
|
||||||
"follow-redirects": "^1.0.0",
|
|
||||||
"requires-port": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/http-proxy-middleware": {
|
|
||||||
"version": "3.0.0-beta.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.0-beta.1.tgz",
|
|
||||||
"integrity": "sha512-hdiTlVVoaxncf239csnEpG5ew2lRWnoNR1PMWOO6kYulSphlrfLs5JFZtFVH3R5EUWSZNMkeUqvkvfctuWaK8A==",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/http-proxy": "^1.17.10",
|
|
||||||
"debug": "^4.3.4",
|
|
||||||
"http-proxy": "^1.18.1",
|
|
||||||
"is-glob": "^4.0.1",
|
|
||||||
"is-plain-obj": "^3.0.0",
|
|
||||||
"micromatch": "^4.0.5"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/http-proxy-middleware/node_modules/debug": {
|
|
||||||
"version": "4.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
|
||||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"ms": "2.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"supports-color": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/http-proxy-middleware/node_modules/ms": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
|
||||||
},
|
|
||||||
"node_modules/iconv-lite": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.4.24",
|
"version": "0.4.24",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
@@ -790,6 +729,7 @@
|
|||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -798,6 +738,7 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-extglob": "^2.1.1"
|
"is-extglob": "^2.1.1"
|
||||||
},
|
},
|
||||||
@@ -809,21 +750,11 @@
|
|||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.12.0"
|
"node": ">=0.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/is-plain-obj": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/media-typer": {
|
"node_modules/media-typer": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||||
@@ -845,18 +776,6 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/micromatch": {
|
|
||||||
"version": "4.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
|
|
||||||
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
|
|
||||||
"dependencies": {
|
|
||||||
"braces": "^3.0.2",
|
|
||||||
"picomatch": "^2.3.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mime": {
|
"node_modules/mime": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||||
@@ -917,6 +836,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/node-tone/-/node-tone-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-tone/-/node-tone-1.0.1.tgz",
|
||||||
"integrity": "sha512-wi7L0taDZMN6tM5l85TDKHsYzdhqJTtPNgvgpk2zHeZzPt6ZIUZ9vBLTJRRDpm0xzCvbsvFHjAaudeQjLHTE4w=="
|
"integrity": "sha512-wi7L0taDZMN6tM5l85TDKHsYzdhqJTtPNgvgpk2zHeZzPt6ZIUZ9vBLTJRRDpm0xzCvbsvFHjAaudeQjLHTE4w=="
|
||||||
},
|
},
|
||||||
|
"node_modules/nodemailer": {
|
||||||
|
"version": "6.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.2.tgz",
|
||||||
|
"integrity": "sha512-4+TYaa/e1nIxQfyw/WzNPYTEZ5OvHIDEnmjs4LPmIfccPQN+2CYKmGHjWixn/chzD3bmUTu5FMfpltizMxqzdg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nodemon": {
|
"node_modules/nodemon": {
|
||||||
"version": "2.0.20",
|
"version": "2.0.20",
|
||||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz",
|
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz",
|
||||||
@@ -1028,6 +955,7 @@
|
|||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||||
|
"dev": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
},
|
},
|
||||||
@@ -1101,11 +1029,6 @@
|
|||||||
"node": ">=8.10.0"
|
"node": ">=8.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/requires-port": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
|
|
||||||
},
|
|
||||||
"node_modules/safe-buffer": {
|
"node_modules/safe-buffer": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
@@ -1324,6 +1247,7 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||||
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"is-number": "^7.0.0"
|
"is-number": "^7.0.0"
|
||||||
},
|
},
|
||||||
@@ -1453,14 +1377,6 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/http-proxy": {
|
|
||||||
"version": "1.17.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.11.tgz",
|
|
||||||
"integrity": "sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA==",
|
|
||||||
"requires": {
|
|
||||||
"@types/node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "18.11.18",
|
"version": "18.11.18",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||||
@@ -1560,6 +1476,7 @@
|
|||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"fill-range": "^7.0.1"
|
"fill-range": "^7.0.1"
|
||||||
}
|
}
|
||||||
@@ -1763,11 +1680,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
|
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="
|
||||||
},
|
},
|
||||||
"eventemitter3": {
|
|
||||||
"version": "4.0.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
|
||||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
|
|
||||||
},
|
|
||||||
"express": {
|
"express": {
|
||||||
"version": "4.18.2",
|
"version": "4.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
||||||
@@ -1810,6 +1722,7 @@
|
|||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"to-regex-range": "^5.0.1"
|
"to-regex-range": "^5.0.1"
|
||||||
}
|
}
|
||||||
@@ -1931,44 +1844,6 @@
|
|||||||
"toidentifier": "1.0.1"
|
"toidentifier": "1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"http-proxy": {
|
|
||||||
"version": "1.18.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
|
||||||
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
|
|
||||||
"requires": {
|
|
||||||
"eventemitter3": "^4.0.0",
|
|
||||||
"follow-redirects": "^1.0.0",
|
|
||||||
"requires-port": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"http-proxy-middleware": {
|
|
||||||
"version": "3.0.0-beta.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.0-beta.1.tgz",
|
|
||||||
"integrity": "sha512-hdiTlVVoaxncf239csnEpG5ew2lRWnoNR1PMWOO6kYulSphlrfLs5JFZtFVH3R5EUWSZNMkeUqvkvfctuWaK8A==",
|
|
||||||
"requires": {
|
|
||||||
"@types/http-proxy": "^1.17.10",
|
|
||||||
"debug": "^4.3.4",
|
|
||||||
"http-proxy": "^1.18.1",
|
|
||||||
"is-glob": "^4.0.1",
|
|
||||||
"is-plain-obj": "^3.0.0",
|
|
||||||
"micromatch": "^4.0.5"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"debug": {
|
|
||||||
"version": "4.3.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
|
||||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
|
||||||
"requires": {
|
|
||||||
"ms": "2.1.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ms": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"iconv-lite": {
|
"iconv-lite": {
|
||||||
"version": "0.4.24",
|
"version": "0.4.24",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
@@ -2005,12 +1880,14 @@
|
|||||||
"is-extglob": {
|
"is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
|
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"is-glob": {
|
"is-glob": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-extglob": "^2.1.1"
|
"is-extglob": "^2.1.1"
|
||||||
}
|
}
|
||||||
@@ -2018,12 +1895,8 @@
|
|||||||
"is-number": {
|
"is-number": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
|
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||||
},
|
"dev": true
|
||||||
"is-plain-obj": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA=="
|
|
||||||
},
|
},
|
||||||
"media-typer": {
|
"media-typer": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
@@ -2040,15 +1913,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||||
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="
|
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="
|
||||||
},
|
},
|
||||||
"micromatch": {
|
|
||||||
"version": "4.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
|
|
||||||
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
|
|
||||||
"requires": {
|
|
||||||
"braces": "^3.0.2",
|
|
||||||
"picomatch": "^2.3.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"mime": {
|
"mime": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||||
@@ -2091,6 +1955,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/node-tone/-/node-tone-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/node-tone/-/node-tone-1.0.1.tgz",
|
||||||
"integrity": "sha512-wi7L0taDZMN6tM5l85TDKHsYzdhqJTtPNgvgpk2zHeZzPt6ZIUZ9vBLTJRRDpm0xzCvbsvFHjAaudeQjLHTE4w=="
|
"integrity": "sha512-wi7L0taDZMN6tM5l85TDKHsYzdhqJTtPNgvgpk2zHeZzPt6ZIUZ9vBLTJRRDpm0xzCvbsvFHjAaudeQjLHTE4w=="
|
||||||
},
|
},
|
||||||
|
"nodemailer": {
|
||||||
|
"version": "6.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.2.tgz",
|
||||||
|
"integrity": "sha512-4+TYaa/e1nIxQfyw/WzNPYTEZ5OvHIDEnmjs4LPmIfccPQN+2CYKmGHjWixn/chzD3bmUTu5FMfpltizMxqzdg=="
|
||||||
|
},
|
||||||
"nodemon": {
|
"nodemon": {
|
||||||
"version": "2.0.20",
|
"version": "2.0.20",
|
||||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz",
|
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz",
|
||||||
@@ -2172,7 +2041,8 @@
|
|||||||
"picomatch": {
|
"picomatch": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
|
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||||
|
"dev": true
|
||||||
},
|
},
|
||||||
"proxy-addr": {
|
"proxy-addr": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
@@ -2222,11 +2092,6 @@
|
|||||||
"picomatch": "^2.2.1"
|
"picomatch": "^2.2.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"requires-port": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
|
|
||||||
},
|
|
||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
@@ -2393,6 +2258,7 @@
|
|||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||||
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"is-number": "^7.0.0"
|
"is-number": "^7.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.19",
|
"version": "2.2.22",
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -34,8 +34,8 @@
|
|||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"graceful-fs": "^4.2.10",
|
"graceful-fs": "^4.2.10",
|
||||||
"htmlparser2": "^8.0.1",
|
"htmlparser2": "^8.0.1",
|
||||||
"http-proxy-middleware": "^3.0.0-beta.1",
|
|
||||||
"node-tone": "^1.0.1",
|
"node-tone": "^1.0.1",
|
||||||
|
"nodemailer": "^6.9.2",
|
||||||
"socket.io": "^4.5.4",
|
"socket.io": "^4.5.4",
|
||||||
"xml2js": "^0.5.0"
|
"xml2js": "^0.5.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ class Auth {
|
|||||||
user: user.toJSONForBrowser(),
|
user: user.toJSONForBrowser(),
|
||||||
userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries),
|
userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries),
|
||||||
serverSettings: this.db.serverSettings.toJSONForBrowser(),
|
serverSettings: this.db.serverSettings.toJSONForBrowser(),
|
||||||
|
ereaderDevices: this.db.emailSettings.getEReaderDevices(user),
|
||||||
Source: global.Source
|
Source: global.Source
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ const Author = require('./objects/entities/Author')
|
|||||||
const Series = require('./objects/entities/Series')
|
const Series = require('./objects/entities/Series')
|
||||||
const ServerSettings = require('./objects/settings/ServerSettings')
|
const ServerSettings = require('./objects/settings/ServerSettings')
|
||||||
const NotificationSettings = require('./objects/settings/NotificationSettings')
|
const NotificationSettings = require('./objects/settings/NotificationSettings')
|
||||||
|
const EmailSettings = require('./objects/settings/EmailSettings')
|
||||||
const PlaybackSession = require('./objects/PlaybackSession')
|
const PlaybackSession = require('./objects/PlaybackSession')
|
||||||
|
|
||||||
class Db {
|
class Db {
|
||||||
@@ -49,6 +50,7 @@ class Db {
|
|||||||
|
|
||||||
this.serverSettings = null
|
this.serverSettings = null
|
||||||
this.notificationSettings = null
|
this.notificationSettings = null
|
||||||
|
this.emailSettings = null
|
||||||
|
|
||||||
// Stores previous version only if upgraded
|
// Stores previous version only if upgraded
|
||||||
this.previousVersion = null
|
this.previousVersion = null
|
||||||
@@ -156,6 +158,10 @@ class Db {
|
|||||||
this.notificationSettings = new NotificationSettings()
|
this.notificationSettings = new NotificationSettings()
|
||||||
await this.insertEntity('settings', this.notificationSettings)
|
await this.insertEntity('settings', this.notificationSettings)
|
||||||
}
|
}
|
||||||
|
if (!this.emailSettings) {
|
||||||
|
this.emailSettings = new EmailSettings()
|
||||||
|
await this.insertEntity('settings', this.emailSettings)
|
||||||
|
}
|
||||||
global.ServerSettings = this.serverSettings.toJSON()
|
global.ServerSettings = this.serverSettings.toJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,6 +208,11 @@ class Db {
|
|||||||
if (notificationSettings) {
|
if (notificationSettings) {
|
||||||
this.notificationSettings = new NotificationSettings(notificationSettings)
|
this.notificationSettings = new NotificationSettings(notificationSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emailSettings = this.settings.find(s => s.id === 'email-settings')
|
||||||
|
if (emailSettings) {
|
||||||
|
this.emailSettings = new EmailSettings(emailSettings)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const p5 = this.collectionsDb.select(() => true).then((results) => {
|
const p5 = this.collectionsDb.select(() => true).then((results) => {
|
||||||
|
|||||||
+52
-12
@@ -11,6 +11,7 @@ const { version } = require('../package.json')
|
|||||||
const dbMigration = require('./utils/dbMigration')
|
const dbMigration = require('./utils/dbMigration')
|
||||||
const filePerms = require('./utils/filePerms')
|
const filePerms = require('./utils/filePerms')
|
||||||
const fileUtils = require('./utils/fileUtils')
|
const fileUtils = require('./utils/fileUtils')
|
||||||
|
const globals = require('./utils/globals')
|
||||||
const Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
|
|
||||||
const Auth = require('./Auth')
|
const Auth = require('./Auth')
|
||||||
@@ -22,9 +23,9 @@ const SocketAuthority = require('./SocketAuthority')
|
|||||||
const ApiRouter = require('./routers/ApiRouter')
|
const ApiRouter = require('./routers/ApiRouter')
|
||||||
const HlsRouter = require('./routers/HlsRouter')
|
const HlsRouter = require('./routers/HlsRouter')
|
||||||
const StaticRouter = require('./routers/StaticRouter')
|
const StaticRouter = require('./routers/StaticRouter')
|
||||||
const ClientRouter = require('./routers/ClientRouter')
|
|
||||||
|
|
||||||
const NotificationManager = require('./managers/NotificationManager')
|
const NotificationManager = require('./managers/NotificationManager')
|
||||||
|
const EmailManager = require('./managers/EmailManager')
|
||||||
const CoverManager = require('./managers/CoverManager')
|
const CoverManager = require('./managers/CoverManager')
|
||||||
const AbMergeManager = require('./managers/AbMergeManager')
|
const AbMergeManager = require('./managers/AbMergeManager')
|
||||||
const CacheManager = require('./managers/CacheManager')
|
const CacheManager = require('./managers/CacheManager')
|
||||||
@@ -38,7 +39,7 @@ const CronManager = require('./managers/CronManager')
|
|||||||
const TaskManager = require('./managers/TaskManager')
|
const TaskManager = require('./managers/TaskManager')
|
||||||
|
|
||||||
class Server {
|
class Server {
|
||||||
constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH, CLIENT_PORT) {
|
constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) {
|
||||||
this.Port = PORT
|
this.Port = PORT
|
||||||
this.Host = HOST
|
this.Host = HOST
|
||||||
global.Source = SOURCE
|
global.Source = SOURCE
|
||||||
@@ -48,7 +49,6 @@ class Server {
|
|||||||
global.ConfigPath = fileUtils.filePathToPOSIX(Path.normalize(CONFIG_PATH))
|
global.ConfigPath = fileUtils.filePathToPOSIX(Path.normalize(CONFIG_PATH))
|
||||||
global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH))
|
global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH))
|
||||||
global.RouterBasePath = ROUTER_BASE_PATH
|
global.RouterBasePath = ROUTER_BASE_PATH
|
||||||
global.ClientPort = CLIENT_PORT
|
|
||||||
global.XAccel = process.env.USE_X_ACCEL
|
global.XAccel = process.env.USE_X_ACCEL
|
||||||
|
|
||||||
if (!fs.pathExistsSync(global.ConfigPath)) {
|
if (!fs.pathExistsSync(global.ConfigPath)) {
|
||||||
@@ -67,6 +67,7 @@ class Server {
|
|||||||
// Managers
|
// Managers
|
||||||
this.taskManager = new TaskManager()
|
this.taskManager = new TaskManager()
|
||||||
this.notificationManager = new NotificationManager(this.db)
|
this.notificationManager = new NotificationManager(this.db)
|
||||||
|
this.emailManager = new EmailManager(this.db)
|
||||||
this.backupManager = new BackupManager(this.db)
|
this.backupManager = new BackupManager(this.db)
|
||||||
this.logManager = new LogManager(this.db)
|
this.logManager = new LogManager(this.db)
|
||||||
this.cacheManager = new CacheManager()
|
this.cacheManager = new CacheManager()
|
||||||
@@ -77,14 +78,13 @@ 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
|
||||||
this.apiRouter = new ApiRouter(this)
|
this.apiRouter = new ApiRouter(this)
|
||||||
this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager)
|
this.hlsRouter = new HlsRouter(this.db, this.auth, this.playbackSessionManager)
|
||||||
this.staticRouter = new StaticRouter(this.db)
|
this.staticRouter = new StaticRouter(this.db)
|
||||||
this.clientRouter = new ClientRouter(global.appRoot, global.ClientPort, global.RouterBasePath)
|
|
||||||
|
|
||||||
Logger.logManager = this.logManager
|
Logger.logManager = this.logManager
|
||||||
|
|
||||||
@@ -139,7 +139,6 @@ class Server {
|
|||||||
async start() {
|
async start() {
|
||||||
Logger.info('=== Starting Server ===')
|
Logger.info('=== Starting Server ===')
|
||||||
await this.init()
|
await this.init()
|
||||||
this.clientRouter.start()
|
|
||||||
|
|
||||||
const app = express()
|
const app = express()
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
@@ -153,6 +152,10 @@ class Server {
|
|||||||
router.use(express.urlencoded({ extended: true, limit: "5mb" }));
|
router.use(express.urlencoded({ extended: true, limit: "5mb" }));
|
||||||
router.use(express.json({ limit: "5mb" }))
|
router.use(express.json({ limit: "5mb" }))
|
||||||
|
|
||||||
|
// Static path to generated nuxt
|
||||||
|
const distPath = Path.join(global.appRoot, '/client/dist')
|
||||||
|
router.use(express.static(distPath))
|
||||||
|
|
||||||
// Metadata folder static path
|
// Metadata folder static path
|
||||||
router.use('/metadata', this.authMiddleware.bind(this), express.static(global.MetadataPath))
|
router.use('/metadata', this.authMiddleware.bind(this), express.static(global.MetadataPath))
|
||||||
|
|
||||||
@@ -161,16 +164,35 @@ class Server {
|
|||||||
|
|
||||||
router.use('/api', this.authMiddleware.bind(this), this.apiRouter.router)
|
router.use('/api', this.authMiddleware.bind(this), this.apiRouter.router)
|
||||||
router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router)
|
router.use('/hls', this.authMiddleware.bind(this), this.hlsRouter.router)
|
||||||
|
|
||||||
|
// TODO: Deprecated as of 2.2.21 edge
|
||||||
router.use('/s', this.authMiddleware.bind(this), this.staticRouter.router)
|
router.use('/s', this.authMiddleware.bind(this), this.staticRouter.router)
|
||||||
|
|
||||||
// EBook static file routes
|
// EBook static file routes
|
||||||
|
// TODO: Deprecated as of 2.2.21 edge
|
||||||
router.get('/ebook/:library/:folder/*', (req, res) => {
|
router.get('/ebook/:library/:folder/*', (req, res) => {
|
||||||
const library = this.db.libraries.find(lib => lib.id === req.params.library)
|
const library = this.db.libraries.find(lib => lib.id === req.params.library)
|
||||||
if (!library) return res.sendStatus(404)
|
if (!library) return res.sendStatus(404)
|
||||||
const folder = library.folders.find(fol => fol.id === req.params.folder)
|
const folder = library.folders.find(fol => fol.id === req.params.folder)
|
||||||
if (!folder) return res.status(404).send('Folder not found')
|
if (!folder) return res.status(404).send('Folder not found')
|
||||||
|
|
||||||
const remainingPath = req.params['0']
|
// Replace backslashes with forward slashes
|
||||||
|
const remainingPath = req.params['0'].replace(/\\/g, '/')
|
||||||
|
|
||||||
|
// Prevent path traversal
|
||||||
|
// e.g. ../../etc/passwd
|
||||||
|
if (/\/?\.?\.\//.test(remainingPath)) {
|
||||||
|
Logger.error(`[Server] Invalid path to get ebook "${remainingPath}"`)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file ext is a valid ebook file
|
||||||
|
const filext = (Path.extname(remainingPath) || '').slice(1).toLowerCase()
|
||||||
|
if (!globals.SupportedEbookTypes.includes(filext)) {
|
||||||
|
Logger.error(`[Server] Invalid ebook file ext requested "${remainingPath}"`)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
const fullPath = Path.join(folder.fullPath, remainingPath)
|
const fullPath = Path.join(folder.fullPath, remainingPath)
|
||||||
res.sendFile(fullPath)
|
res.sendFile(fullPath)
|
||||||
})
|
})
|
||||||
@@ -188,6 +210,28 @@ class Server {
|
|||||||
this.rssFeedManager.getFeedItem(req, res)
|
this.rssFeedManager.getFeedItem(req, res)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Client dynamic routes
|
||||||
|
const dyanimicRoutes = [
|
||||||
|
'/item/:id',
|
||||||
|
'/author/:id',
|
||||||
|
'/audiobook/:id/chapters',
|
||||||
|
'/audiobook/:id/edit',
|
||||||
|
'/audiobook/:id/manage',
|
||||||
|
'/library/:library',
|
||||||
|
'/library/:library/search',
|
||||||
|
'/library/:library/bookshelf/:id?',
|
||||||
|
'/library/:library/authors',
|
||||||
|
'/library/:library/series/:id?',
|
||||||
|
'/library/:library/podcast/search',
|
||||||
|
'/library/:library/podcast/latest',
|
||||||
|
'/config/users/:id',
|
||||||
|
'/config/users/:id/sessions',
|
||||||
|
'/config/item-metadata-utils/:id',
|
||||||
|
'/collection/:id',
|
||||||
|
'/playlist/:id'
|
||||||
|
]
|
||||||
|
dyanimicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))
|
||||||
|
|
||||||
router.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res))
|
router.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res))
|
||||||
router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this))
|
router.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this))
|
||||||
router.post('/init', (req, res) => {
|
router.post('/init', (req, res) => {
|
||||||
@@ -216,9 +260,6 @@ class Server {
|
|||||||
})
|
})
|
||||||
app.get('/healthcheck', (req, res) => res.sendStatus(200))
|
app.get('/healthcheck', (req, res) => res.sendStatus(200))
|
||||||
|
|
||||||
// Serve client on all other routes, 404 will be handled by client
|
|
||||||
router.use(this.clientRouter.router)
|
|
||||||
|
|
||||||
this.server.listen(this.Port, this.Host, () => {
|
this.server.listen(this.Port, this.Host, () => {
|
||||||
if (this.Host) Logger.info(`Listening on http://${this.Host}:${this.Port}`)
|
if (this.Host) Logger.info(`Listening on http://${this.Host}:${this.Port}`)
|
||||||
else Logger.info(`Listening on port :${this.Port}`)
|
else Logger.info(`Listening on port :${this.Port}`)
|
||||||
@@ -329,8 +370,7 @@ class Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async stop() {
|
async stop() {
|
||||||
this.watcher.close()
|
await this.watcher.close()
|
||||||
this.clientRouter.stop()
|
|
||||||
Logger.info('Watcher Closed')
|
Logger.info('Watcher Closed')
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
const Logger = require('../Logger')
|
||||||
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
|
||||||
|
class EmailController {
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
getSettings(req, res) {
|
||||||
|
res.json({
|
||||||
|
settings: this.db.emailSettings
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateSettings(req, res) {
|
||||||
|
const updated = this.db.emailSettings.update(req.body)
|
||||||
|
if (updated) {
|
||||||
|
await this.db.updateEntity('settings', this.db.emailSettings)
|
||||||
|
}
|
||||||
|
res.json({
|
||||||
|
settings: this.db.emailSettings
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendTest(req, res) {
|
||||||
|
this.emailManager.sendTest(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateEReaderDevices(req, res) {
|
||||||
|
if (!req.body.ereaderDevices || !Array.isArray(req.body.ereaderDevices)) {
|
||||||
|
return res.status(400).send('Invalid payload. ereaderDevices array required')
|
||||||
|
}
|
||||||
|
|
||||||
|
const ereaderDevices = req.body.ereaderDevices
|
||||||
|
for (const device of ereaderDevices) {
|
||||||
|
if (!device.name || !device.email) {
|
||||||
|
return res.status(400).send('Invalid payload. ereaderDevices array items must have name and email')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = this.db.emailSettings.update({
|
||||||
|
ereaderDevices
|
||||||
|
})
|
||||||
|
if (updated) {
|
||||||
|
await this.db.updateEntity('settings', this.db.emailSettings)
|
||||||
|
SocketAuthority.adminEmitter('ereader-devices-updated', {
|
||||||
|
ereaderDevices: this.db.emailSettings.ereaderDevices
|
||||||
|
})
|
||||||
|
}
|
||||||
|
res.json({
|
||||||
|
ereaderDevices: this.db.emailSettings.ereaderDevices
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendEBookToDevice(req, res) {
|
||||||
|
Logger.debug(`[EmailController] Send ebook to device request for libraryItemId=${req.body.libraryItemId}, deviceName=${req.body.deviceName}`)
|
||||||
|
|
||||||
|
const libraryItem = this.db.getLibraryItem(req.body.libraryItemId)
|
||||||
|
if (!libraryItem) {
|
||||||
|
return res.status(404).send('Library item not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!req.user.checkCanAccessLibraryItem(libraryItem)) {
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ebookFile = libraryItem.media.ebookFile
|
||||||
|
if (!ebookFile) {
|
||||||
|
return res.status(404).send('EBook file not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
const device = this.db.emailSettings.getEReaderDevice(req.body.deviceName)
|
||||||
|
if (!device) {
|
||||||
|
return res.status(404).send('E-reader device not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emailManager.sendEBookToDevice(ebookFile, device, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware(req, res, next) {
|
||||||
|
if (!req.user.isAdminOrUp) {
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = new EmailController()
|
||||||
@@ -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()
|
||||||
@@ -684,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
|
||||||
@@ -707,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)
|
||||||
@@ -770,13 +848,19 @@ class LibraryController {
|
|||||||
res.json(payload)
|
res.json(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOPMLFile(req, res) {
|
||||||
|
const opmlText = this.podcastManager.generateOPMLFileText(req.libraryItems)
|
||||||
|
res.type('application/xml')
|
||||||
|
res.send(opmlText)
|
||||||
|
}
|
||||||
|
|
||||||
middleware(req, res, next) {
|
middleware(req, res, next) {
|
||||||
if (!req.user.checkCanAccessLibrary(req.params.id)) {
|
if (!req.user.checkCanAccessLibrary(req.params.id)) {
|
||||||
Logger.warn(`[LibraryController] Library ${req.params.id} not accessible to user ${req.user.username}`)
|
Logger.warn(`[LibraryController] Library ${req.params.id} not accessible to user ${req.user.username}`)
|
||||||
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')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const Path = require('path')
|
||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const SocketAuthority = require('../SocketAuthority')
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
@@ -5,6 +6,7 @@ const SocketAuthority = require('../SocketAuthority')
|
|||||||
const zipHelpers = require('../utils/zipHelpers')
|
const zipHelpers = require('../utils/zipHelpers')
|
||||||
const { reqSupportsWebp, isNullOrNaN } = require('../utils/index')
|
const { reqSupportsWebp, isNullOrNaN } = require('../utils/index')
|
||||||
const { ScanResult } = require('../utils/constants')
|
const { ScanResult } = require('../utils/constants')
|
||||||
|
const { getAudioMimeTypeFromExtname } = require('../utils/fileUtils')
|
||||||
|
|
||||||
class LibraryItemController {
|
class LibraryItemController {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
@@ -379,20 +381,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 +405,7 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = {
|
const result = {
|
||||||
success: itemsUpdated > 0,
|
success: itemsUpdated > 0,
|
||||||
updates: itemsUpdated,
|
updates: itemsUpdated,
|
||||||
unmatched: itemsUnmatched
|
unmatched: itemsUnmatched
|
||||||
@@ -408,6 +413,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 +464,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)
|
||||||
})
|
})
|
||||||
@@ -498,19 +530,45 @@ class LibraryItemController {
|
|||||||
res.json(toneData)
|
res.json(toneData)
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteLibraryFile(req, res) {
|
/**
|
||||||
const libraryFile = req.libraryItem.libraryFiles.find(lf => lf.ino === req.params.ino)
|
* GET api/items/:id/file/:fileid
|
||||||
if (!libraryFile) {
|
*
|
||||||
Logger.error(`[LibraryItemController] Unable to delete library file. Not found. "${req.params.ino}"`)
|
* @param {express.Request} req
|
||||||
return res.sendStatus(404)
|
* @param {express.Response} res
|
||||||
|
*/
|
||||||
|
async getLibraryFile(req, res) {
|
||||||
|
const libraryFile = req.libraryFile
|
||||||
|
|
||||||
|
if (global.XAccel) {
|
||||||
|
Logger.debug(`Use X-Accel to serve static file ${libraryFile.metadata.path}`)
|
||||||
|
return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + libraryFile.metadata.path }).send()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Express does not set the correct mimetype for m4b files so use our defined mimetypes if available
|
||||||
|
const audioMimeType = getAudioMimeTypeFromExtname(Path.extname(libraryFile.metadata.path))
|
||||||
|
if (audioMimeType) {
|
||||||
|
res.setHeader('Content-Type', audioMimeType)
|
||||||
|
}
|
||||||
|
res.sendFile(libraryFile.metadata.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE api/items/:id/file/:fileid
|
||||||
|
*
|
||||||
|
* @param {express.Request} req
|
||||||
|
* @param {express.Response} res
|
||||||
|
*/
|
||||||
|
async deleteLibraryFile(req, res) {
|
||||||
|
const libraryFile = req.libraryFile
|
||||||
|
|
||||||
|
Logger.info(`[LibraryItemController] User "${req.user.username}" requested file delete at "${libraryFile.metadata.path}"`)
|
||||||
|
|
||||||
await fs.remove(libraryFile.metadata.path).catch((error) => {
|
await fs.remove(libraryFile.metadata.path).catch((error) => {
|
||||||
Logger.error(`[LibraryItemController] Failed to delete library file at "${libraryFile.metadata.path}"`, error)
|
Logger.error(`[LibraryItemController] Failed to delete library file at "${libraryFile.metadata.path}"`, error)
|
||||||
})
|
})
|
||||||
req.libraryItem.removeLibraryFile(req.params.ino)
|
req.libraryItem.removeLibraryFile(req.params.fileid)
|
||||||
|
|
||||||
if (req.libraryItem.media.removeFileWithInode(req.params.ino)) {
|
if (req.libraryItem.media.removeFileWithInode(req.params.fileid)) {
|
||||||
// If book has no more media files then mark it as missing
|
// If book has no more media files then mark it as missing
|
||||||
if (req.libraryItem.mediaType === 'book' && !req.libraryItem.media.hasMediaEntities) {
|
if (req.libraryItem.mediaType === 'book' && !req.libraryItem.media.hasMediaEntities) {
|
||||||
req.libraryItem.setMissing()
|
req.libraryItem.setMissing()
|
||||||
@@ -522,15 +580,76 @@ class LibraryItemController {
|
|||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET api/items/:id/file/:fileid/download
|
||||||
|
* Same as GET api/items/:id/file/:fileid but allows logging and restricting downloads
|
||||||
|
* @param {express.Request} req
|
||||||
|
* @param {express.Response} res
|
||||||
|
*/
|
||||||
|
async downloadLibraryFile(req, res) {
|
||||||
|
const libraryFile = req.libraryFile
|
||||||
|
|
||||||
|
if (!req.user.canDownload) {
|
||||||
|
Logger.error(`[LibraryItemController] User without download permission attempted to download file "${libraryFile.metadata.path}"`, req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.info(`[LibraryItemController] User "${req.user.username}" requested file download at "${libraryFile.metadata.path}"`)
|
||||||
|
|
||||||
|
if (global.XAccel) {
|
||||||
|
Logger.debug(`Use X-Accel to serve static file ${libraryFile.metadata.path}`)
|
||||||
|
return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + libraryFile.metadata.path }).send()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Express does not set the correct mimetype for m4b files so use our defined mimetypes if available
|
||||||
|
const audioMimeType = getAudioMimeTypeFromExtname(Path.extname(libraryFile.metadata.path))
|
||||||
|
if (audioMimeType) {
|
||||||
|
res.setHeader('Content-Type', audioMimeType)
|
||||||
|
}
|
||||||
|
|
||||||
|
res.download(libraryFile.metadata.path, libraryFile.metadata.filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET api/items/:id/ebook
|
||||||
|
*
|
||||||
|
* @param {express.Request} req
|
||||||
|
* @param {express.Response} res
|
||||||
|
*/
|
||||||
|
async getEBookFile(req, res) {
|
||||||
|
const ebookFile = req.libraryItem.media.ebookFile
|
||||||
|
if (!ebookFile) {
|
||||||
|
Logger.error(`[LibraryItemController] No ebookFile for library item "${req.libraryItem.media.metadata.title}"`)
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
const ebookFilePath = ebookFile.metadata.path
|
||||||
|
|
||||||
|
if (global.XAccel) {
|
||||||
|
Logger.debug(`Use X-Accel to serve static file ${ebookFilePath}`)
|
||||||
|
return res.status(204).header({ 'X-Accel-Redirect': global.XAccel + ebookFilePath }).send()
|
||||||
|
}
|
||||||
|
|
||||||
|
res.sendFile(ebookFilePath)
|
||||||
|
}
|
||||||
|
|
||||||
middleware(req, res, next) {
|
middleware(req, res, next) {
|
||||||
const item = this.db.libraryItems.find(li => li.id === req.params.id)
|
req.libraryItem = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||||
if (!item || !item.media) return res.sendStatus(404)
|
if (!req.libraryItem?.media) return res.sendStatus(404)
|
||||||
|
|
||||||
// Check user can access this library item
|
// Check user can access this library item
|
||||||
if (!req.user.checkCanAccessLibraryItem(item)) {
|
if (!req.user.checkCanAccessLibraryItem(req.libraryItem)) {
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// For library file routes, get the library file
|
||||||
|
if (req.params.fileid) {
|
||||||
|
req.libraryFile = req.libraryItem.libraryFiles.find(lf => lf.ino === req.params.fileid)
|
||||||
|
if (!req.libraryFile) {
|
||||||
|
Logger.error(`[LibraryItemController] Library file "${req.params.fileid}" does not exist for library item`)
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (req.path.includes('/play')) {
|
if (req.path.includes('/play')) {
|
||||||
// allow POST requests using /play and /play/:episodeId
|
// allow POST requests using /play and /play/:episodeId
|
||||||
} else if (req.method == 'DELETE' && !req.user.canDelete) {
|
} else if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||||
@@ -541,7 +660,6 @@ class LibraryItemController {
|
|||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.libraryItem = item
|
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ class PodcastController {
|
|||||||
res.json({ podcast })
|
res.json({ podcast })
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOPMLFeeds(req, res) {
|
async getFeedsFromOPMLText(req, res) {
|
||||||
if (!req.body.opmlText) {
|
if (!req.body.opmlText) {
|
||||||
return res.sendStatus(400)
|
return res.sendStatus(400)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class SeriesController {
|
|||||||
|
|
||||||
// Add progress map with isFinished flag
|
// Add progress map with isFinished flag
|
||||||
if (include.includes('progress')) {
|
if (include.includes('progress')) {
|
||||||
const libraryItemsInSeries = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(seriesJson.id))
|
const libraryItemsInSeries = req.libraryItemsInSeries
|
||||||
const libraryItemsFinished = libraryItemsInSeries.filter(li => {
|
const libraryItemsFinished = libraryItemsInSeries.filter(li => {
|
||||||
const mediaProgress = req.user.getMediaProgress(li.id)
|
const mediaProgress = req.user.getMediaProgress(li.id)
|
||||||
return mediaProgress && mediaProgress.isFinished
|
return mediaProgress && mediaProgress.isFinished
|
||||||
@@ -55,6 +55,12 @@ class SeriesController {
|
|||||||
const series = this.db.series.find(se => se.id === req.params.id)
|
const series = this.db.series.find(se => se.id === req.params.id)
|
||||||
if (!series) return res.sendStatus(404)
|
if (!series) return res.sendStatus(404)
|
||||||
|
|
||||||
|
const libraryItemsInSeries = this.db.libraryItems.filter(li => li.media.metadata.hasSeries?.(series.id))
|
||||||
|
if (libraryItemsInSeries.some(li => !req.user.checkCanAccessLibrary(li.libraryId))) {
|
||||||
|
Logger.warn(`[SeriesController] User attempted to access series "${series.id}" without access to the library`, req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
if (req.method == 'DELETE' && !req.user.canDelete) {
|
if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||||
Logger.warn(`[SeriesController] User attempted to delete without permission`, req.user)
|
Logger.warn(`[SeriesController] User attempted to delete without permission`, req.user)
|
||||||
return res.sendStatus(403)
|
return res.sendStatus(403)
|
||||||
@@ -64,6 +70,7 @@ class SeriesController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.series = series
|
req.series = series
|
||||||
|
req.libraryItemsInSeries = libraryItemsInSeries
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,7 +74,9 @@ class SessionController {
|
|||||||
|
|
||||||
// POST: api/session/:id/close
|
// POST: api/session/:id/close
|
||||||
close(req, res) {
|
close(req, res) {
|
||||||
this.playbackSessionManager.closeSessionRequest(req.user, req.session, req.body, res)
|
let syncData = req.body
|
||||||
|
if (syncData && !Object.keys(syncData).length) syncData = null
|
||||||
|
this.playbackSessionManager.closeSessionRequest(req.user, req.session, syncData, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DELETE: api/session/:id
|
// DELETE: api/session/:id
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ class BookFinder {
|
|||||||
this.fantLab = new FantLab()
|
this.fantLab = new FantLab()
|
||||||
this.audiobookCovers = new AudiobookCovers()
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,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)
|
||||||
@@ -222,19 +224,29 @@ class BookFinder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
const nodemailer = require('nodemailer')
|
||||||
|
const Logger = require("../Logger")
|
||||||
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
|
||||||
|
class EmailManager {
|
||||||
|
constructor(db) {
|
||||||
|
this.db = db
|
||||||
|
}
|
||||||
|
|
||||||
|
getTransporter() {
|
||||||
|
return nodemailer.createTransport(this.db.emailSettings.getTransportObject())
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendTest(res) {
|
||||||
|
Logger.info(`[EmailManager] Sending test email`)
|
||||||
|
const transporter = this.getTransporter()
|
||||||
|
|
||||||
|
const success = await transporter.verify().catch((error) => {
|
||||||
|
Logger.error(`[EmailManager] Failed to verify SMTP connection config`, error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
return res.status(400).send('Failed to verify SMTP connection configuration')
|
||||||
|
}
|
||||||
|
|
||||||
|
transporter.sendMail({
|
||||||
|
from: this.db.emailSettings.fromAddress,
|
||||||
|
to: this.db.emailSettings.fromAddress,
|
||||||
|
subject: 'Test email from Audiobookshelf',
|
||||||
|
text: 'Success!'
|
||||||
|
}).then((result) => {
|
||||||
|
Logger.info(`[EmailManager] Test email sent successfully`, result)
|
||||||
|
res.sendStatus(200)
|
||||||
|
}).catch((error) => {
|
||||||
|
Logger.error(`[EmailManager] Failed to send test email`, error)
|
||||||
|
res.status(400).send(error.message || 'Failed to send test email')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendEBookToDevice(ebookFile, device, res) {
|
||||||
|
Logger.info(`[EmailManager] Sending ebook "${ebookFile.metadata.filename}" to device "${device.name}"/"${device.email}"`)
|
||||||
|
const transporter = this.getTransporter()
|
||||||
|
|
||||||
|
const success = await transporter.verify().catch((error) => {
|
||||||
|
Logger.error(`[EmailManager] Failed to verify SMTP connection config`, error)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
return res.status(400).send('Failed to verify SMTP connection configuration')
|
||||||
|
}
|
||||||
|
|
||||||
|
transporter.sendMail({
|
||||||
|
from: this.db.emailSettings.fromAddress,
|
||||||
|
to: device.email,
|
||||||
|
html: '<div dir="auto"></div>',
|
||||||
|
attachments: [
|
||||||
|
{
|
||||||
|
filename: ebookFile.metadata.filename,
|
||||||
|
path: ebookFile.metadata.path,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).then((result) => {
|
||||||
|
Logger.info(`[EmailManager] Ebook sent to device successfully`, result)
|
||||||
|
res.sendStatus(200)
|
||||||
|
}).catch((error) => {
|
||||||
|
Logger.error(`[EmailManager] Failed to send ebook to device`, error)
|
||||||
|
res.status(400).send(error.message || 'Failed to send ebook to device')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = EmailManager
|
||||||
@@ -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`)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const { removeFile, downloadFile } = require('../utils/fileUtils')
|
|||||||
const filePerms = require('../utils/filePerms')
|
const filePerms = require('../utils/filePerms')
|
||||||
const { levenshteinDistance } = require('../utils/index')
|
const { levenshteinDistance } = require('../utils/index')
|
||||||
const opmlParser = require('../utils/parsers/parseOPML')
|
const opmlParser = require('../utils/parsers/parseOPML')
|
||||||
|
const opmlGenerator = require('../utils/generators/opmlGenerator')
|
||||||
const prober = require('../utils/prober')
|
const prober = require('../utils/prober')
|
||||||
const ffmpegHelpers = require('../utils/ffmpegHelpers')
|
const ffmpegHelpers = require('../utils/ffmpegHelpers')
|
||||||
|
|
||||||
@@ -78,12 +79,19 @@ 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())
|
||||||
this.currentDownload = podcastEpisodeDownload
|
this.currentDownload = podcastEpisodeDownload
|
||||||
|
|
||||||
|
// If this file already exists then append the episode id to the filename
|
||||||
|
// e.g. "/tagesschau 20 Uhr.mp3" becomes "/tagesschau 20 Uhr (ep_asdfasdf).mp3"
|
||||||
|
// this handles podcasts where every title is the same (ref https://github.com/advplyr/audiobookshelf/issues/1802)
|
||||||
|
if (await fs.pathExists(this.currentDownload.targetPath)) {
|
||||||
|
this.currentDownload.appendEpisodeId = true
|
||||||
|
}
|
||||||
|
|
||||||
// Ignores all added files to this dir
|
// Ignores all added files to this dir
|
||||||
this.watcher.addIgnoreDir(this.currentDownload.libraryItem.path)
|
this.watcher.addIgnoreDir(this.currentDownload.libraryItem.path)
|
||||||
|
|
||||||
@@ -140,8 +148,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 +184,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)
|
||||||
@@ -365,6 +374,10 @@ class PodcastManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateOPMLFileText(libraryItems) {
|
||||||
|
return opmlGenerator.generate(libraryItems)
|
||||||
|
}
|
||||||
|
|
||||||
getDownloadQueueDetails(libraryId = null) {
|
getDownloadQueueDetails(libraryId = null) {
|
||||||
let _currentDownload = this.currentDownload
|
let _currentDownload = this.currentDownload
|
||||||
if (libraryId && _currentDownload?.libraryId !== libraryId) _currentDownload = null
|
if (libraryId && _currentDownload?.libraryId !== libraryId) _currentDownload = null
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ const fs = require('../libs/fsExtra')
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const { version } = require('../../package.json')
|
const { version } = require('../../package.json')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const abmetadataGenerator = require('../utils/abmetadataGenerator')
|
const abmetadataGenerator = require('../utils/generators/abmetadataGenerator')
|
||||||
const LibraryFile = require('./files/LibraryFile')
|
const LibraryFile = require('./files/LibraryFile')
|
||||||
const Book = require('./mediaTypes/Book')
|
const Book = require('./mediaTypes/Book')
|
||||||
const Podcast = require('./mediaTypes/Podcast')
|
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) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user