mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-04 01:40:40 +02:00
Compare commits
165 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea60f19e7a | |||
| cefc75a4ed | |||
| d8753aafb9 | |||
| ba5ad228cc | |||
| 0203f9cc1b | |||
| 4770be5a39 | |||
| 1bac395bed | |||
| e818f270cd | |||
| c4e2726622 | |||
| 74d8a09f31 | |||
| 618338165e | |||
| db494001a2 | |||
| a67213fb7b | |||
| 5d96b2cc6e | |||
| 72d0b097ab | |||
| 36d2957fb4 | |||
| b5de517aad | |||
| 41db0e3bb1 | |||
| e8d582269b | |||
| 80ef8ee890 | |||
| a65859f575 | |||
| 5724887785 | |||
| 8908aa7a82 | |||
| f83dd29213 | |||
| 99d90778f4 | |||
| 49279430fc | |||
| 030c20b12e | |||
| 5e943ef152 | |||
| 4ae057f626 | |||
| 9ebe4b55dd | |||
| 2f7403adec | |||
| 2777b496ad | |||
| f7a3dbf209 | |||
| d900093976 | |||
| 08250e266e | |||
| da2d1455d7 | |||
| b6c6c4c939 | |||
| 22179d82b8 | |||
| 343ce312f1 | |||
| 10677d6fb0 | |||
| 49a8aead9b | |||
| 274b0e48be | |||
| 4d8ffc5d99 | |||
| 4f3029e5b2 | |||
| a1b49f5fcf | |||
| 89d497a305 | |||
| 9e095a4bc1 | |||
| 024d052a7b | |||
| c312979aec | |||
| 773e621944 | |||
| ed4f33b565 | |||
| f8a0852dfc | |||
| 6dec750d3e | |||
| 3c98a5fb24 | |||
| 702ee3d350 | |||
| fcc2f3650b | |||
| e4ad622c01 | |||
| 458403eec9 | |||
| aaede2752c | |||
| 39d8c2cf04 | |||
| dd5c940d36 | |||
| 277f024bbc | |||
| 59ad1e5e36 | |||
| 02c4b21d3f | |||
| 33ae5445be | |||
| 5ed06871b6 | |||
| e98eb8f1eb | |||
| ebedaeb3b0 | |||
| 62aec63d1d | |||
| 3c25e87e8d | |||
| 08d16ce7c2 | |||
| 2cb3808326 | |||
| bdb6f0c0aa | |||
| 5255bf13cc | |||
| 3588e1e8d3 | |||
| 8fa8360e99 | |||
| b305cfd268 | |||
| ff10287d05 | |||
| 7a7708403f | |||
| ddabd0ee75 | |||
| 5a26704c32 | |||
| 7ccf36a896 | |||
| e9a84dd7dd | |||
| b00510855e | |||
| 2cd9079692 | |||
| 3e4b1652fc | |||
| 878330b4fb | |||
| 9a85ad1f6b | |||
| f76f9c7f84 | |||
| 3426832f2b | |||
| 10fd51498c | |||
| 49c581ed35 | |||
| f095d89980 | |||
| 1609f1a499 | |||
| 88bd51e2da | |||
| 74388fe0b9 | |||
| 7f5356100d | |||
| 84d2d00a30 | |||
| 31dddfbb60 | |||
| d6da161b13 | |||
| 9de7be1cb4 | |||
| 5410aae8fc | |||
| 86bf6bfc62 | |||
| 0807146aab | |||
| 591d8a8ab1 | |||
| b1d4e28027 | |||
| 44363f05ac | |||
| 452af43916 | |||
| 70ba2f7850 | |||
| a364fe5031 | |||
| ca6765c8e7 | |||
| 6bfa281dc5 | |||
| d8ee61bfab | |||
| c6763dee2d | |||
| 0e6b0d3eff | |||
| 8bbfee334c | |||
| f806e4cce3 | |||
| 209ba308bd | |||
| 4cd9088a66 | |||
| ac5e2e5c73 | |||
| f1329d2847 | |||
| 27faefc64d | |||
| 0fa7e61dc1 | |||
| 5a3f14ae51 | |||
| 4e61185136 | |||
| 6ee06d5dae | |||
| 2c344a0bc0 | |||
| 315c83e4c3 | |||
| 9e4bc582cb | |||
| fc6aa1f91f | |||
| d4bea34423 | |||
| a551a2d288 | |||
| 4b0c59b174 | |||
| a0840d2a08 | |||
| 308ccf470f | |||
| 4021b6eca1 | |||
| 061695f922 | |||
| e803dcd325 | |||
| 128796bd36 | |||
| 775dedc338 | |||
| 45c9038954 | |||
| 8acf962864 | |||
| c3fc38639e | |||
| b60b75c8da | |||
| 0f7edec73b | |||
| 321277826f | |||
| 6e752af2c0 | |||
| 0717ae39db | |||
| 7bc5902ea8 | |||
| a28e1ed5e0 | |||
| 43d9e129a6 | |||
| b516019ddd | |||
| e4c20d677c | |||
| 33e183b802 | |||
| b884f8fe11 | |||
| 2cba83f1dd | |||
| a9ee9031c3 | |||
| c3717f6979 | |||
| 657d4dd705 | |||
| 17356ffd79 | |||
| c4be75b5bd | |||
| 57422d0759 | |||
| d2454201b4 | |||
| 3a92a69693 | |||
| d733c9ccc6 |
@@ -54,17 +54,6 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
src: url(~static/fonts/GentiumBookBasic.woff2) format('woff2');
|
src: url(~static/fonts/GentiumBookBasic.woff2) format('woff2');
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Gentium Book Basic';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url(~static/fonts/GentiumBookBasic.woff2) format('woff2');
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* cyrillic-ext */
|
/* cyrillic-ext */
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-60">
|
<div id="appbar" class="absolute top-0 bottom-0 left-0 w-full h-full px-2 md:px-6 py-1 z-60">
|
||||||
<div class="flex h-full items-center">
|
<div class="flex h-full items-center">
|
||||||
<nuxt-link to="/">
|
<nuxt-link to="/">
|
||||||
<img src="~static/icon.svg" class="w-8 min-w-8 h-8 mr-2 sm:w-12 sm:min-w-12 sm:h-12 sm:mr-4" />
|
<img src="~static/icon.svg" :alt="$strings.ButtonHome" class="w-8 min-w-8 h-8 mr-2 sm:w-12 sm:min-w-12 sm:h-12 sm:mr-4" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link to="/">
|
<nuxt-link to="/">
|
||||||
@@ -24,25 +24,25 @@
|
|||||||
<google-cast-launcher></google-cast-launcher>
|
<google-cast-launcher></google-cast-launcher>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nuxt-link v-if="currentLibrary" to="/config/stats" class="outline-none 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>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="userCanUpload && currentLibrary" to="/upload" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
<nuxt-link v-if="userCanUpload && currentLibrary" to="/upload" class="hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
||||||
<ui-tooltip :text="$strings.ButtonUpload" direction="bottom" class="flex items-center">
|
<ui-tooltip :text="$strings.ButtonUpload" direction="bottom" class="flex items-center">
|
||||||
<span class="material-icons text-2xl" aria-label="Upload Media" role="button">upload</span>
|
<span class="material-icons text-2xl" aria-label="Upload Media" role="button">upload</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="userIsAdminOrUp" to="/config" class="outline-none hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
<nuxt-link v-if="userIsAdminOrUp" to="/config" class="hover:text-gray-200 cursor-pointer w-8 h-8 flex items-center justify-center mx-1">
|
||||||
<ui-tooltip :text="$strings.HeaderSettings" direction="bottom" class="flex items-center">
|
<ui-tooltip :text="$strings.HeaderSettings" direction="bottom" class="flex items-center">
|
||||||
<span class="material-icons text-2xl" aria-label="System Settings" role="button">settings</span>
|
<span class="material-icons text-2xl" aria-label="System Settings" role="button">settings</span>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link to="/account" class="relative w-9 h-9 md:w-32 bg-fg border border-gray-500 rounded shadow-sm ml-1.5 sm:ml-3 md:ml-5 md:pl-3 md:pr-10 py-2 text-left focus:outline-none sm:text-sm cursor-pointer hover:bg-bg hover:bg-opacity-40" aria-haspopup="listbox" aria-expanded="true">
|
<nuxt-link to="/account" class="relative w-9 h-9 md:w-32 bg-fg border border-gray-500 rounded shadow-sm ml-1.5 sm:ml-3 md:ml-5 md:pl-3 md:pr-10 py-2 text-left sm:text-sm cursor-pointer hover:bg-bg hover:bg-opacity-40" aria-haspopup="listbox" aria-expanded="true">
|
||||||
<span class="items-center hidden md:flex">
|
<span class="items-center hidden md:flex">
|
||||||
<span class="block truncate">{{ username }}</span>
|
<span class="block truncate">{{ username }}</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -58,13 +58,13 @@
|
|||||||
<span class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
<span class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
||||||
{{ $strings.ButtonPlay }}
|
{{ $strings.ButtonPlay }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
<ui-tooltip v-if="userIsAdminOrUp && !isPodcastLibrary" :text="$strings.ButtonQuickMatch" direction="bottom">
|
<ui-tooltip v-if="userIsAdminOrUp && isBookLibrary" :text="$strings.ButtonQuickMatch" direction="bottom">
|
||||||
<ui-icon-btn :disabled="processingBatch" icon="auto_awesome" @click="batchAutoMatchClick" class="mx-1.5" />
|
<ui-icon-btn :disabled="processingBatch" icon="auto_awesome" @click="batchAutoMatchClick" class="mx-1.5" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<ui-tooltip v-if="!isPodcastLibrary" :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
|
<ui-tooltip v-if="isBookLibrary" :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
|
||||||
<ui-read-icon-btn :disabled="processingBatch" :is-read="selectedIsFinished" @click="toggleBatchRead" class="mx-1.5" />
|
<ui-read-icon-btn :disabled="processingBatch" :is-read="selectedIsFinished" @click="toggleBatchRead" class="mx-1.5" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<ui-tooltip v-if="userCanUpdate && !isPodcastLibrary" :text="$strings.LabelAddToCollection" direction="bottom">
|
<ui-tooltip v-if="userCanUpdate && isBookLibrary" :text="$strings.LabelAddToCollection" direction="bottom">
|
||||||
<ui-icon-btn :disabled="processingBatch" icon="collections_bookmark" @click="batchAddToCollectionClick" class="mx-1.5" />
|
<ui-icon-btn :disabled="processingBatch" icon="collections_bookmark" @click="batchAddToCollectionClick" class="mx-1.5" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
<template v-if="userCanUpdate">
|
<template v-if="userCanUpdate">
|
||||||
@@ -103,6 +103,9 @@ export default {
|
|||||||
isPodcastLibrary() {
|
isPodcastLibrary() {
|
||||||
return this.libraryMediaType === 'podcast'
|
return this.libraryMediaType === 'podcast'
|
||||||
},
|
},
|
||||||
|
isBookLibrary() {
|
||||||
|
return this.libraryMediaType === 'book'
|
||||||
|
},
|
||||||
isHome() {
|
isHome() {
|
||||||
return this.$route.name === 'library-library'
|
return this.$route.name === 'library-library'
|
||||||
},
|
},
|
||||||
@@ -181,12 +184,15 @@ export default {
|
|||||||
|
|
||||||
const queueItems = []
|
const queueItems = []
|
||||||
libraryItems.forEach((item) => {
|
libraryItems.forEach((item) => {
|
||||||
|
let subtitle = ''
|
||||||
|
if (item.mediaType === 'book') subtitle = item.media.metadata.authors.map((au) => au.name).join(', ')
|
||||||
|
else if (item.mediaType === 'music') subtitle = item.media.metadata.artists.join(', ')
|
||||||
queueItems.push({
|
queueItems.push({
|
||||||
libraryItemId: item.id,
|
libraryItemId: item.id,
|
||||||
libraryId: item.libraryId,
|
libraryId: item.libraryId,
|
||||||
episodeId: null,
|
episodeId: null,
|
||||||
title: item.media.metadata.title,
|
title: item.media.metadata.title,
|
||||||
subtitle: item.media.metadata.authors.map((au) => au.name).join(', '),
|
subtitle,
|
||||||
caption: '',
|
caption: '',
|
||||||
duration: item.media.duration || null,
|
duration: item.media.duration || null,
|
||||||
coverPath: item.media.coverPath || null
|
coverPath: item.media.coverPath || null
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ export default {
|
|||||||
const mediaItem = {
|
const mediaItem = {
|
||||||
id: thisEntity.id,
|
id: thisEntity.id,
|
||||||
mediaType: thisEntity.mediaType,
|
mediaType: thisEntity.mediaType,
|
||||||
hasTracks: thisEntity.mediaType === 'podcast' || thisEntity.media.numTracks || (thisEntity.media.tracks && thisEntity.media.tracks.length)
|
hasTracks: thisEntity.mediaType === 'podcast' || thisEntity.media.audioFile || thisEntity.media.numTracks || (thisEntity.media.tracks && thisEntity.media.tracks.length)
|
||||||
}
|
}
|
||||||
this.$store.commit('globals/setMediaItemSelected', { item: mediaItem, selected: isSelecting })
|
this.$store.commit('globals/setMediaItemSelected', { item: mediaItem, selected: isSelecting })
|
||||||
} else {
|
} else {
|
||||||
@@ -147,7 +147,7 @@ export default {
|
|||||||
const mediaItem = {
|
const mediaItem = {
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
mediaType: entity.mediaType,
|
mediaType: entity.mediaType,
|
||||||
hasTracks: entity.mediaType === 'podcast' || entity.media.numTracks || (entity.media.tracks && entity.media.tracks.length)
|
hasTracks: entity.mediaType === 'podcast' || entity.media.audioFile || entity.media.numTracks || (entity.media.tracks && entity.media.tracks.length)
|
||||||
}
|
}
|
||||||
this.$store.commit('globals/toggleMediaItemSelected', mediaItem)
|
this.$store.commit('globals/toggleMediaItemSelected', mediaItem)
|
||||||
}
|
}
|
||||||
@@ -167,8 +167,8 @@ export default {
|
|||||||
this.loaded = true
|
this.loaded = true
|
||||||
},
|
},
|
||||||
async fetchCategories() {
|
async fetchCategories() {
|
||||||
var categories = await this.$axios
|
const categories = await this.$axios
|
||||||
.$get(`/api/libraries/${this.currentLibraryId}/personalized`)
|
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed`)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
return data
|
return data
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,17 +16,17 @@
|
|||||||
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="flex-grow h-full flex justify-center items-center" :class="isPodcastLatestPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
<p class="text-sm">{{ $strings.ButtonLatest }}</p>
|
<p class="text-sm">{{ $strings.ButtonLatest }}</p>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="flex-grow h-full flex justify-center items-center" :class="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="flex-grow h-full flex justify-center items-center" :class="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
<p v-if="isSeriesPage" class="text-sm">{{ $strings.ButtonSeries }}</p>
|
<p v-if="isSeriesPage" class="text-sm">{{ $strings.ButtonSeries }}</p>
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
||||||
</svg>
|
</svg>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
<p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
|
<p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
|
||||||
<span v-else class="material-icons-outlined text-lg">collections_bookmark</span>
|
<span v-else class="material-icons-outlined text-lg">collections_bookmark</span>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/authors`" class="flex-grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
|
||||||
<p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p>
|
<p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p>
|
||||||
<svg v-else class="w-5 h-5" viewBox="0 0 24 24">
|
<svg v-else class="w-5 h-5" viewBox="0 0 24 24">
|
||||||
<path
|
<path
|
||||||
@@ -50,18 +50,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-checkbox v-if="!isBatchSelecting" v-model="settings.collapseBookSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseBookSeries" />
|
<ui-checkbox v-if="!isBatchSelecting" v-model="settings.collapseBookSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseBookSeries" />
|
||||||
<ui-btn v-if="!isBatchSelecting" color="primary" small :loading="processingSeries" class="items-center ml-1 sm:ml-4 hidden md:flex" @click="markSeriesFinished">
|
|
||||||
<div class="h-5 w-5">
|
<!-- RSS feed -->
|
||||||
<svg v-if="isSeriesFinished" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
|
<ui-tooltip v-if="seriesRssFeed" :text="$strings.LabelOpenRSSFeed" direction="top">
|
||||||
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
|
<ui-icon-btn icon="rss_feed" class="mx-0.5" :size="7" icon-font-size="1.2rem" bg-color="success" outlined @click="showOpenSeriesRSSFeed" />
|
||||||
</svg>
|
</ui-tooltip>
|
||||||
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-7 19.6l-7-4.66V3h14v12.93l-7 4.67zm-2.01-7.42l-2.58-2.59L6 12l4 4 8-8-1.42-1.42z" />
|
<ui-context-menu-dropdown v-if="!isBatchSelecting && seriesContextMenuItems.length" :items="seriesContextMenuItems" class="mx-px" @action="seriesContextMenuAction" />
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<span class="pl-2"> {{ $strings.LabelMarkSeries }} {{ isSeriesFinished ? $strings.LabelNotFinished : $strings.LabelFinished }}</span>
|
|
||||||
</ui-btn>
|
|
||||||
<ui-btn v-if="isSeriesRemovedFromContinueListening && !isBatchSelecting" small :loading="processingSeries" @click="reAddSeriesToContinueListening" class="hidden md:block ml-2"> Re-Add Series to Continue Listening </ui-btn>
|
|
||||||
</template>
|
</template>
|
||||||
<!-- library & collections page -->
|
<!-- library & collections page -->
|
||||||
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome">
|
<template v-else-if="page !== 'search' && page !== 'podcast-search' && page !== 'recent-episodes' && !isHome">
|
||||||
@@ -69,7 +64,7 @@
|
|||||||
|
|
||||||
<div class="flex-grow hidden sm:inline-block" />
|
<div class="flex-grow hidden sm:inline-block" />
|
||||||
|
|
||||||
<ui-checkbox v-if="isLibraryPage && !isPodcastLibrary && !isBatchSelecting" v-model="settings.collapseSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" />
|
<ui-checkbox v-if="isLibraryPage && isBookLibrary && !isBatchSelecting" v-model="settings.collapseSeries" :label="$strings.LabelCollapseSeries" checkbox-bg="bg" check-color="white" small class="mr-2" @input="updateCollapseSeries" />
|
||||||
<controls-library-filter-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" />
|
<controls-library-filter-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.filterBy" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateFilter" />
|
||||||
<controls-library-sort-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateOrder" />
|
<controls-library-sort-select v-if="isLibraryPage && !isBatchSelecting" v-model="settings.orderBy" :descending.sync="settings.orderDesc" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateOrder" />
|
||||||
<controls-library-filter-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesFilterBy" is-series class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesFilter" />
|
<controls-library-filter-select v-if="isSeriesPage && !isBatchSelecting" v-model="settings.seriesFilterBy" is-series class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateSeriesFilter" />
|
||||||
@@ -118,6 +113,32 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
seriesContextMenuItems() {
|
||||||
|
if (!this.selectedSeries) return []
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
text: this.isSeriesFinished ? this.$strings.MessageMarkAsNotFinished : this.$strings.MessageMarkAsFinished,
|
||||||
|
action: 'mark-series-finished'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if (this.userIsAdminOrUp || this.selectedSeries.rssFeed) {
|
||||||
|
items.push({
|
||||||
|
text: this.$strings.LabelOpenRSSFeed,
|
||||||
|
action: 'open-rss-feed'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isSeriesRemovedFromContinueListening) {
|
||||||
|
items.push({
|
||||||
|
text: 'Re-Add Series to Continue Listening',
|
||||||
|
action: 're-add-to-continue-listening'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return items
|
||||||
|
},
|
||||||
seriesSortItems() {
|
seriesSortItems() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -153,9 +174,15 @@ export default {
|
|||||||
currentLibraryMediaType() {
|
currentLibraryMediaType() {
|
||||||
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||||
},
|
},
|
||||||
|
isBookLibrary() {
|
||||||
|
return this.currentLibraryMediaType === 'book'
|
||||||
|
},
|
||||||
isPodcastLibrary() {
|
isPodcastLibrary() {
|
||||||
return this.currentLibraryMediaType === 'podcast'
|
return this.currentLibraryMediaType === 'podcast'
|
||||||
},
|
},
|
||||||
|
isMusicLibrary() {
|
||||||
|
return this.currentLibraryMediaType === 'music'
|
||||||
|
},
|
||||||
isLibraryPage() {
|
isLibraryPage() {
|
||||||
return this.page === ''
|
return this.page === ''
|
||||||
},
|
},
|
||||||
@@ -180,10 +207,16 @@ export default {
|
|||||||
isAuthorsPage() {
|
isAuthorsPage() {
|
||||||
return this.$route.name === 'library-library-authors'
|
return this.$route.name === 'library-library-authors'
|
||||||
},
|
},
|
||||||
|
isAlbumsPage() {
|
||||||
|
return this.page === 'albums'
|
||||||
|
},
|
||||||
numShowing() {
|
numShowing() {
|
||||||
return this.totalEntities
|
return this.totalEntities
|
||||||
},
|
},
|
||||||
entityName() {
|
entityName() {
|
||||||
|
if (this.isAlbumsPage) return 'Albums'
|
||||||
|
if (this.isMusicLibrary) return 'Tracks'
|
||||||
|
|
||||||
if (this.isPodcastLibrary) return this.$strings.LabelPodcasts
|
if (this.isPodcastLibrary) return this.$strings.LabelPodcasts
|
||||||
if (!this.page) return this.$strings.LabelBooks
|
if (!this.page) return this.$strings.LabelBooks
|
||||||
if (this.isSeriesPage) return this.$strings.LabelSeries
|
if (this.isSeriesPage) return this.$strings.LabelSeries
|
||||||
@@ -200,6 +233,9 @@ export default {
|
|||||||
seriesProgress() {
|
seriesProgress() {
|
||||||
return this.selectedSeries ? this.selectedSeries.progress : null
|
return this.selectedSeries ? this.selectedSeries.progress : null
|
||||||
},
|
},
|
||||||
|
seriesRssFeed() {
|
||||||
|
return this.selectedSeries ? this.selectedSeries.rssFeed : null
|
||||||
|
},
|
||||||
seriesLibraryItemIds() {
|
seriesLibraryItemIds() {
|
||||||
if (!this.seriesProgress) return []
|
if (!this.seriesProgress) return []
|
||||||
return this.seriesProgress.libraryItemIds || []
|
return this.seriesProgress.libraryItemIds || []
|
||||||
@@ -222,6 +258,31 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
seriesContextMenuAction(action) {
|
||||||
|
if (action === 'open-rss-feed') {
|
||||||
|
this.showOpenSeriesRSSFeed()
|
||||||
|
} else if (action === 're-add-to-continue-listening') {
|
||||||
|
if (this.processingSeries) {
|
||||||
|
console.warn('Already processing series')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.reAddSeriesToContinueListening()
|
||||||
|
} else if (action === 'mark-series-finished') {
|
||||||
|
if (this.processingSeries) {
|
||||||
|
console.warn('Already processing series')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.markSeriesFinished()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
showOpenSeriesRSSFeed() {
|
||||||
|
this.$store.commit('globals/setRSSFeedOpenCloseModal', {
|
||||||
|
id: this.selectedSeries.id,
|
||||||
|
name: this.selectedSeries.name,
|
||||||
|
type: 'series',
|
||||||
|
feed: this.selectedSeries.rssFeed
|
||||||
|
})
|
||||||
|
},
|
||||||
reAddSeriesToContinueListening() {
|
reAddSeriesToContinueListening() {
|
||||||
this.processingSeries = true
|
this.processingSeries = true
|
||||||
this.$axios
|
this.$axios
|
||||||
@@ -286,27 +347,38 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
markSeriesFinished() {
|
markSeriesFinished() {
|
||||||
var newIsFinished = !this.isSeriesFinished
|
const newIsFinished = !this.isSeriesFinished
|
||||||
this.processingSeries = true
|
|
||||||
var updateProgressPayloads = this.seriesLibraryItemIds.map((lid) => {
|
const payload = {
|
||||||
return {
|
message: newIsFinished ? this.$strings.MessageConfirmMarkSeriesFinished : this.$strings.MessageConfirmMarkSeriesNotFinished,
|
||||||
libraryItemId: lid,
|
callback: (confirmed) => {
|
||||||
isFinished: newIsFinished
|
if (confirmed) {
|
||||||
}
|
this.processingSeries = true
|
||||||
})
|
const updateProgressPayloads = this.seriesLibraryItemIds.map((lid) => {
|
||||||
console.log('Progress payloads', updateProgressPayloads)
|
return {
|
||||||
this.$axios
|
libraryItemId: lid,
|
||||||
.patch(`/api/me/progress/batch/update`, updateProgressPayloads)
|
isFinished: newIsFinished
|
||||||
.then(() => {
|
}
|
||||||
this.$toast.success('Series update success')
|
})
|
||||||
this.selectedSeries.progress.isFinished = newIsFinished
|
console.log('Progress payloads', updateProgressPayloads)
|
||||||
this.processingSeries = false
|
this.$axios
|
||||||
})
|
.patch(`/api/me/progress/batch/update`, updateProgressPayloads)
|
||||||
.catch((error) => {
|
.then(() => {
|
||||||
this.$toast.error('Series update failed')
|
this.$toast.success(this.$strings.ToastSeriesUpdateSuccess)
|
||||||
console.error('Failed to batch update read/not read', error)
|
this.selectedSeries.progress.isFinished = newIsFinished
|
||||||
this.processingSeries = false
|
})
|
||||||
})
|
.catch((error) => {
|
||||||
|
this.$toast.error(this.$strings.ToastSeriesUpdateFailed)
|
||||||
|
console.error('Failed to batch update read/not read', error)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processingSeries = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
},
|
},
|
||||||
updateOrder() {
|
updateOrder() {
|
||||||
this.saveSettings()
|
this.saveSettings()
|
||||||
@@ -339,16 +411,32 @@ export default {
|
|||||||
},
|
},
|
||||||
setBookshelfTotalEntities(totalEntities) {
|
setBookshelfTotalEntities(totalEntities) {
|
||||||
this.totalEntities = totalEntities
|
this.totalEntities = totalEntities
|
||||||
|
},
|
||||||
|
rssFeedOpen(data) {
|
||||||
|
if (data.entityId === this.seriesId) {
|
||||||
|
console.log('RSS Feed Opened', data)
|
||||||
|
this.selectedSeries.rssFeed = data
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rssFeedClosed(data) {
|
||||||
|
if (data.entityId === this.seriesId) {
|
||||||
|
console.log('RSS Feed Closed', data)
|
||||||
|
this.selectedSeries.rssFeed = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.init()
|
this.init()
|
||||||
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
||||||
this.$eventBus.$on('bookshelf-total-entities', this.setBookshelfTotalEntities)
|
this.$eventBus.$on('bookshelf-total-entities', this.setBookshelfTotalEntities)
|
||||||
|
this.$root.socket.on('rss_feed_open', this.rssFeedOpen)
|
||||||
|
this.$root.socket.on('rss_feed_closed', this.rssFeedClosed)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
||||||
this.$eventBus.$off('bookshelf-total-entities', this.setBookshelfTotalEntities)
|
this.$eventBus.$off('bookshelf-total-entities', this.setBookshelfTotalEntities)
|
||||||
|
this.$root.socket.off('rss_feed_open', this.rssFeedOpen)
|
||||||
|
this.$root.socket.off('rss_feed_closed', this.rssFeedClosed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-44 fixed left-0 top-16 h-full bg-bg bg-opacity-100 md:bg-opacity-70 shadow-lg border-r border-white border-opacity-5 py-3 transform transition-transform" :class="wrapperClass" v-click-outside="clickOutside">
|
<div class="w-44 fixed left-0 top-16 h-full bg-bg bg-opacity-100 md:bg-opacity-70 shadow-lg border-r border-white border-opacity-5 py-3 transform transition-transform" :class="wrapperClass" v-click-outside="clickOutside">
|
||||||
<div class="md:hidden flex items-center justify-end pb-2 px-4 mb-1" @click="closeDrawer">
|
<div v-show="isMobilePortrait" class="flex items-center justify-end pb-2 px-4 mb-1" @click="closeDrawer">
|
||||||
<span class="material-icons text-2xl">arrow_back</span>
|
<span class="material-icons text-2xl">arrow_back</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ export default {
|
|||||||
var classes = []
|
var classes = []
|
||||||
if (this.drawerOpen) classes.push('translate-x-0')
|
if (this.drawerOpen) classes.push('translate-x-0')
|
||||||
else classes.push('-translate-x-44')
|
else classes.push('-translate-x-44')
|
||||||
if (this.isMobile) classes.push('z-50')
|
if (this.isMobilePortrait) classes.push('z-50')
|
||||||
else classes.push('z-40')
|
else classes.push('z-40')
|
||||||
return classes.join(' ')
|
return classes.join(' ')
|
||||||
},
|
},
|
||||||
@@ -124,9 +124,11 @@ export default {
|
|||||||
isMobileLandscape() {
|
isMobileLandscape() {
|
||||||
return this.$store.state.globals.isMobileLandscape
|
return this.$store.state.globals.isMobileLandscape
|
||||||
},
|
},
|
||||||
|
isMobilePortrait() {
|
||||||
|
return this.$store.state.globals.isMobilePortrait
|
||||||
|
},
|
||||||
drawerOpen() {
|
drawerOpen() {
|
||||||
if (this.isMobile) return this.isOpen
|
return !this.isMobilePortrait || this.isOpen
|
||||||
return true
|
|
||||||
},
|
},
|
||||||
routeName() {
|
routeName() {
|
||||||
return this.$route.name
|
return this.$route.name
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="initialized && !totalShelves && !hasFilter && entityName === 'books'" class="w-full flex flex-col items-center justify-center py-12">
|
<div v-if="initialized && !totalShelves && !hasFilter && entityName === 'items'" class="w-full flex flex-col items-center justify-center py-12">
|
||||||
<p class="text-center text-2xl font-book mb-4 py-4">{{ $getString('MessageXLibraryIsEmpty', [libraryName]) }}</p>
|
<p class="text-center text-2xl font-book mb-4 py-4">{{ $getString('MessageXLibraryIsEmpty', [libraryName]) }}</p>
|
||||||
<div v-if="userIsAdminOrUp" class="flex">
|
<div v-if="userIsAdminOrUp" class="flex">
|
||||||
<ui-btn to="/config" color="primary" class="w-52 mr-2">{{ $strings.ButtonConfigureScanner }}</ui-btn>
|
<ui-btn to="/config" color="primary" class="w-52 mr-2">{{ $strings.ButtonConfigureScanner }}</ui-btn>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<div v-else-if="!totalShelves && initialized" class="w-full py-16">
|
<div v-else-if="!totalShelves && initialized" class="w-full py-16">
|
||||||
<p class="text-xl text-center">{{ emptyMessage }}</p>
|
<p class="text-xl text-center">{{ emptyMessage }}</p>
|
||||||
<!-- Clear filter only available on Library bookshelf -->
|
<!-- Clear filter only available on Library bookshelf -->
|
||||||
<div v-if="entityName === 'books'" class="flex justify-center mt-2">
|
<div v-if="entityName === 'items'" class="flex justify-center mt-2">
|
||||||
<ui-btn v-if="hasFilter" color="primary" @click="clearFilter">{{ $strings.ButtonClearFilter }}</ui-btn>
|
<ui-btn v-if="hasFilter" color="primary" @click="clearFilter">{{ $strings.ButtonClearFilter }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -81,8 +81,11 @@ export default {
|
|||||||
showExperimentalFeatures() {
|
showExperimentalFeatures() {
|
||||||
return this.$store.state.showExperimentalFeatures
|
return this.$store.state.showExperimentalFeatures
|
||||||
},
|
},
|
||||||
|
libraryMediaType() {
|
||||||
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||||
|
},
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast'
|
return this.libraryMediaType === 'podcast'
|
||||||
},
|
},
|
||||||
emptyMessage() {
|
emptyMessage() {
|
||||||
if (this.page === 'series') return this.$strings.MessageBookshelfNoSeries
|
if (this.page === 'series') return this.$strings.MessageBookshelfNoSeries
|
||||||
@@ -96,7 +99,7 @@ export default {
|
|||||||
return this.$strings.MessageNoResults
|
return this.$strings.MessageNoResults
|
||||||
},
|
},
|
||||||
entityName() {
|
entityName() {
|
||||||
if (!this.page) return 'books'
|
if (!this.page) return 'items'
|
||||||
return this.page
|
return this.page
|
||||||
},
|
},
|
||||||
seriesSortBy() {
|
seriesSortBy() {
|
||||||
@@ -158,11 +161,8 @@ export default {
|
|||||||
libraryName() {
|
libraryName() {
|
||||||
return this.$store.getters['libraries/getCurrentLibraryName']
|
return this.$store.getters['libraries/getCurrentLibraryName']
|
||||||
},
|
},
|
||||||
isEntityBook() {
|
|
||||||
return this.entityName === 'series-books' || this.entityName === 'books'
|
|
||||||
},
|
|
||||||
bookWidth() {
|
bookWidth() {
|
||||||
var coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
const coverSize = this.$store.getters['user/getUserSetting']('bookshelfCoverSize')
|
||||||
if (this.isCoverSquareAspectRatio || this.entityName === 'playlists') return coverSize * 1.6
|
if (this.isCoverSquareAspectRatio || this.entityName === 'playlists') return coverSize * 1.6
|
||||||
return coverSize
|
return coverSize
|
||||||
},
|
},
|
||||||
@@ -192,7 +192,8 @@ export default {
|
|||||||
},
|
},
|
||||||
shelfHeight() {
|
shelfHeight() {
|
||||||
if (this.isAlternativeBookshelfView) {
|
if (this.isAlternativeBookshelfView) {
|
||||||
var extraTitleSpace = this.isEntityBook ? 80 : 40
|
const isItemEntity = this.entityName === 'series-books' || this.entityName === 'items'
|
||||||
|
const extraTitleSpace = isItemEntity ? 80 : this.entityName === 'albums' ? 60 : 40
|
||||||
return this.entityHeight + extraTitleSpace * this.sizeMultiplier
|
return this.entityHeight + extraTitleSpace * this.sizeMultiplier
|
||||||
}
|
}
|
||||||
return this.entityHeight + 40
|
return this.entityHeight + 40
|
||||||
@@ -205,7 +206,7 @@ export default {
|
|||||||
return this.$store.state.globals.selectedMediaItems || []
|
return this.$store.state.globals.selectedMediaItems || []
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
|
const baseSize = this.isCoverSquareAspectRatio ? 192 : 120
|
||||||
return this.entityWidth / baseSize
|
return this.entityWidth / baseSize
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -214,8 +215,8 @@ export default {
|
|||||||
this.$store.dispatch('user/updateUserSettings', { filterBy: 'all' })
|
this.$store.dispatch('user/updateUserSettings', { filterBy: 'all' })
|
||||||
},
|
},
|
||||||
editEntity(entity) {
|
editEntity(entity) {
|
||||||
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
if (this.entityName === 'items' || this.entityName === 'series-books') {
|
||||||
var bookIds = this.entities.map((e) => e.id)
|
const bookIds = this.entities.map((e) => e.id)
|
||||||
this.$store.commit('setBookshelfBookIds', bookIds)
|
this.$store.commit('setBookshelfBookIds', bookIds)
|
||||||
this.$store.commit('showEditModal', entity)
|
this.$store.commit('showEditModal', entity)
|
||||||
} else if (this.entityName === 'collections') {
|
} else if (this.entityName === 'collections') {
|
||||||
@@ -229,7 +230,7 @@ export default {
|
|||||||
this.isSelectionMode = false
|
this.isSelectionMode = false
|
||||||
},
|
},
|
||||||
selectEntity(entity, shiftKey) {
|
selectEntity(entity, shiftKey) {
|
||||||
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
if (this.entityName === 'items' || this.entityName === 'series-books') {
|
||||||
const indexOf = this.entities.findIndex((ent) => ent && ent.id === entity.id)
|
const indexOf = this.entities.findIndex((ent) => ent && ent.id === entity.id)
|
||||||
const lastLastItemIndexSelected = this.lastItemIndexSelected
|
const lastLastItemIndexSelected = this.lastItemIndexSelected
|
||||||
if (!this.selectedMediaItems.some((i) => i.id === entity.id)) {
|
if (!this.selectedMediaItems.some((i) => i.id === entity.id)) {
|
||||||
@@ -273,9 +274,8 @@ export default {
|
|||||||
const mediaItem = {
|
const mediaItem = {
|
||||||
id: thisEntity.id,
|
id: thisEntity.id,
|
||||||
mediaType: thisEntity.mediaType,
|
mediaType: thisEntity.mediaType,
|
||||||
hasTracks: thisEntity.mediaType === 'podcast' || thisEntity.media.numTracks || (thisEntity.media.tracks && thisEntity.media.tracks.length)
|
hasTracks: thisEntity.mediaType === 'podcast' || thisEntity.media.audioFile || thisEntity.media.numTracks || (thisEntity.media.tracks && thisEntity.media.tracks.length)
|
||||||
}
|
}
|
||||||
console.log('Setting media item selected', mediaItem, 'Num Selected=', this.selectedMediaItems.length)
|
|
||||||
this.$store.commit('globals/setMediaItemSelected', { item: mediaItem, selected: isSelecting })
|
this.$store.commit('globals/setMediaItemSelected', { item: mediaItem, selected: isSelecting })
|
||||||
} else {
|
} else {
|
||||||
console.error('Invalid entity index', i)
|
console.error('Invalid entity index', i)
|
||||||
@@ -285,7 +285,7 @@ export default {
|
|||||||
const mediaItem = {
|
const mediaItem = {
|
||||||
id: entity.id,
|
id: entity.id,
|
||||||
mediaType: entity.mediaType,
|
mediaType: entity.mediaType,
|
||||||
hasTracks: entity.mediaType === 'podcast' || entity.media.numTracks || (entity.media.tracks && entity.media.tracks.length)
|
hasTracks: entity.mediaType === 'podcast' || entity.media.audioFile || entity.media.numTracks || (entity.media.tracks && entity.media.tracks.length)
|
||||||
}
|
}
|
||||||
this.$store.commit('globals/toggleMediaItemSelected', mediaItem)
|
this.$store.commit('globals/toggleMediaItemSelected', mediaItem)
|
||||||
}
|
}
|
||||||
@@ -308,7 +308,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async fetchEntites(page = 0) {
|
async fetchEntites(page = 0) {
|
||||||
var startIndex = page * this.booksPerFetch
|
const startIndex = page * this.booksPerFetch
|
||||||
|
|
||||||
this.isFetchingEntities = true
|
this.isFetchingEntities = true
|
||||||
|
|
||||||
@@ -316,9 +316,9 @@ export default {
|
|||||||
this.currentSFQueryString = this.buildSearchParams()
|
this.currentSFQueryString = this.buildSearchParams()
|
||||||
}
|
}
|
||||||
|
|
||||||
const entityPath = this.entityName === 'books' || this.entityName === 'series-books' ? 'items' : this.entityName
|
const entityPath = this.entityName === 'series-books' ? 'items' : this.entityName
|
||||||
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
||||||
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1`
|
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1&include=rssfeed`
|
||||||
|
|
||||||
const payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => {
|
const payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => {
|
||||||
console.error('failed to fetch books', error)
|
console.error('failed to fetch books', error)
|
||||||
@@ -340,7 +340,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < payload.results.length; i++) {
|
for (let i = 0; i < payload.results.length; i++) {
|
||||||
var index = i + startIndex
|
const index = i + startIndex
|
||||||
this.entities[index] = payload.results[i]
|
this.entities[index] = payload.results[i]
|
||||||
if (this.entityComponentRefs[index]) {
|
if (this.entityComponentRefs[index]) {
|
||||||
this.entityComponentRefs[index].setEntity(this.entities[index])
|
this.entityComponentRefs[index].setEntity(this.entities[index])
|
||||||
@@ -517,7 +517,7 @@ export default {
|
|||||||
},
|
},
|
||||||
libraryItemUpdated(libraryItem) {
|
libraryItemUpdated(libraryItem) {
|
||||||
console.log('Item updated', libraryItem)
|
console.log('Item updated', libraryItem)
|
||||||
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
if (this.entityName === 'items' || this.entityName === 'series-books') {
|
||||||
var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id)
|
var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id)
|
||||||
if (indexOf >= 0) {
|
if (indexOf >= 0) {
|
||||||
this.entities[indexOf] = libraryItem
|
this.entities[indexOf] = libraryItem
|
||||||
@@ -528,7 +528,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
libraryItemRemoved(libraryItem) {
|
libraryItemRemoved(libraryItem) {
|
||||||
if (this.entityName === 'books' || this.entityName === 'series-books') {
|
if (this.entityName === 'items' || this.entityName === 'series-books') {
|
||||||
var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id)
|
var indexOf = this.entities.findIndex((ent) => ent && ent.id === libraryItem.id)
|
||||||
if (indexOf >= 0) {
|
if (indexOf >= 0) {
|
||||||
this.entities = this.entities.filter((ent) => ent.id !== libraryItem.id)
|
this.entities = this.entities.filter((ent) => ent.id !== libraryItem.id)
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<h1 class="text-xl">{{ headerText }}</h1>
|
<h1 class="text-xl">{{ headerText }}</h1>
|
||||||
|
|
||||||
<div v-if="showAddButton" class="mx-2 w-7 h-7 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center" @click="clicked">
|
<div v-if="showAddButton" class="mx-2 w-7 h-7 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center" @click="clicked">
|
||||||
<span class="material-icons" style="font-size: 1.4rem">add</span>
|
<button type="button" class="material-icons" :aria-label="$strings.ButtonAdd + ': ' + headerText" style="font-size: 1.4rem">add</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
<div v-show="showLibrary" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="showLibrary" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" 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="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" 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="isSeriesPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<div v-show="isSeriesPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isSeriesPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<nuxt-link v-if="!isPodcastLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" 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="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" 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="paramId === 'collections' ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
<span class="material-icons-outlined text-2xl">collections_bookmark</span>
|
<span class="material-icons-outlined text-2xl">collections_bookmark</span>
|
||||||
|
|
||||||
<p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p>
|
<p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonCollections }}</p>
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
<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="!isPodcastLibrary" :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
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@@ -70,6 +70,14 @@
|
|||||||
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
<div v-show="isPodcastSearchPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
|
<nuxt-link v-if="isMusicLibrary" :to="`/library/${currentLibraryId}/bookshelf/albums`" 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="isMusicAlbumsPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||||
|
<span class="material-icons-outlined text-xl">album</span>
|
||||||
|
|
||||||
|
<p class="font-book pt-1.5 text-center leading-4" style="font-size: 0.9rem">Albums</p>
|
||||||
|
|
||||||
|
<div v-show="isMusicAlbumsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||||
|
</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'">
|
<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>
|
<span class="material-icons text-2.5xl">queue_music</span>
|
||||||
|
|
||||||
@@ -132,15 +140,24 @@ export default {
|
|||||||
currentLibraryMediaType() {
|
currentLibraryMediaType() {
|
||||||
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||||
},
|
},
|
||||||
|
isBookLibrary() {
|
||||||
|
return this.currentLibraryMediaType === 'book'
|
||||||
|
},
|
||||||
isPodcastLibrary() {
|
isPodcastLibrary() {
|
||||||
return this.currentLibraryMediaType === 'podcast'
|
return this.currentLibraryMediaType === 'podcast'
|
||||||
},
|
},
|
||||||
|
isMusicLibrary() {
|
||||||
|
return this.currentLibraryMediaType === 'music'
|
||||||
|
},
|
||||||
isPodcastSearchPage() {
|
isPodcastSearchPage() {
|
||||||
return this.$route.name === 'library-library-podcast-search'
|
return this.$route.name === 'library-library-podcast-search'
|
||||||
},
|
},
|
||||||
isPodcastLatestPage() {
|
isPodcastLatestPage() {
|
||||||
return this.$route.name === 'library-library-podcast-latest'
|
return this.$route.name === 'library-library-podcast-latest'
|
||||||
},
|
},
|
||||||
|
isMusicAlbumsPage() {
|
||||||
|
return this.paramId === 'albums'
|
||||||
|
},
|
||||||
homePage() {
|
homePage() {
|
||||||
return this.$route.name === 'library-library'
|
return this.$route.name === 'library-library'
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-50 bg-primary px-4 pb-1 md:pb-4 pt-2">
|
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 sm:h-44 md:h-40 z-50 bg-primary px-2 md:px-4 pb-1 md:pb-4 pt-2">
|
||||||
<div id="videoDock" />
|
<div id="videoDock" />
|
||||||
<nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-1 sm:left-4 cursor-pointer" :style="{ top: bookCoverPosTop + 'px' }">
|
<nuxt-link v-if="!playerHandler.isVideo" :to="`/item/${streamLibraryItem.id}`" class="absolute left-2 top-2 md:left-4 cursor-pointer">
|
||||||
<covers-book-cover :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
|
<covers-book-cover :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
<div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : 'pl-20 sm:pl-24'">
|
<div class="flex items-start mb-6 md:mb-0" :class="playerHandler.isVideo ? 'ml-4 pl-96' : isSquareCover ? 'pl-18 sm:pl-24' : 'pl-12 sm:pl-16'">
|
||||||
<div>
|
<div>
|
||||||
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg">
|
<nuxt-link :to="`/item/${streamLibraryItem.id}`" class="hover:underline cursor-pointer text-sm sm:text-lg">
|
||||||
{{ title }}
|
{{ title }}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center">
|
<div v-if="!playerHandler.isVideo" class="text-gray-400 flex items-center">
|
||||||
<span class="material-icons text-sm">person</span>
|
<span class="material-icons text-sm">person</span>
|
||||||
<p v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</p>
|
<p v-if="podcastAuthor" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ podcastAuthor }}</p>
|
||||||
|
<p v-else-if="musicArtists" class="pl-1 sm:pl-1.5 text-xs sm:text-base">{{ musicArtists }}</p>
|
||||||
<p v-else-if="authors.length" class="pl-1 sm:pl-1.5 text-xs sm:text-base">
|
<p 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}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
||||||
</p>
|
</p>
|
||||||
@@ -85,12 +86,15 @@ export default {
|
|||||||
coverAspectRatio() {
|
coverAspectRatio() {
|
||||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
bookCoverWidth() {
|
isSquareCover() {
|
||||||
return 88
|
return this.coverAspectRatio === 1
|
||||||
},
|
},
|
||||||
bookCoverPosTop() {
|
isMobile() {
|
||||||
if (this.coverAspectRatio == 1) return -10
|
return this.$store.state.globals.isMobile
|
||||||
return -64
|
},
|
||||||
|
bookCoverWidth() {
|
||||||
|
if (this.isMobile) return 64 / this.coverAspectRatio
|
||||||
|
return 77 / this.coverAspectRatio
|
||||||
},
|
},
|
||||||
cover() {
|
cover() {
|
||||||
if (this.media.coverPath) return this.media.coverPath
|
if (this.media.coverPath) return this.media.coverPath
|
||||||
@@ -122,6 +126,9 @@ export default {
|
|||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.streamLibraryItem ? this.streamLibraryItem.mediaType === 'podcast' : false
|
return this.streamLibraryItem ? this.streamLibraryItem.mediaType === 'podcast' : false
|
||||||
},
|
},
|
||||||
|
isMusic() {
|
||||||
|
return this.streamLibraryItem ? this.streamLibraryItem.mediaType === 'music' : false
|
||||||
|
},
|
||||||
mediaMetadata() {
|
mediaMetadata() {
|
||||||
return this.media.metadata || {}
|
return this.media.metadata || {}
|
||||||
},
|
},
|
||||||
@@ -145,6 +152,10 @@ export default {
|
|||||||
if (!this.isPodcast) return null
|
if (!this.isPodcast) return null
|
||||||
return this.mediaMetadata.author || 'Unknown'
|
return this.mediaMetadata.author || 'Unknown'
|
||||||
},
|
},
|
||||||
|
musicArtists() {
|
||||||
|
if (!this.isMusic) return null
|
||||||
|
return this.mediaMetadata.artists.join(', ')
|
||||||
|
},
|
||||||
playerQueueItems() {
|
playerQueueItems() {
|
||||||
return this.$store.state.playerQueueItems || []
|
return this.$store.state.playerQueueItems || []
|
||||||
}
|
}
|
||||||
@@ -350,13 +361,15 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
streamProgress(data) {
|
streamProgress(data) {
|
||||||
if (!data.numSegments) return
|
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === data.stream) {
|
||||||
var chunks = data.chunks
|
if (!data.numSegments) return
|
||||||
console.log(`[StreamContainer] Stream Progress ${data.percent}`)
|
var chunks = data.chunks
|
||||||
if (this.$refs.audioPlayer) {
|
console.log(`[StreamContainer] Stream Progress ${data.percent}`)
|
||||||
this.$refs.audioPlayer.setChunksReady(chunks, data.numSegments)
|
if (this.$refs.audioPlayer) {
|
||||||
} else {
|
this.$refs.audioPlayer.setChunksReady(chunks, data.numSegments)
|
||||||
console.error('No Audio Ref')
|
} else {
|
||||||
|
console.error('No Audio Ref')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sessionOpen(session) {
|
sessionOpen(session) {
|
||||||
@@ -405,8 +418,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async playLibraryItem(payload) {
|
async playLibraryItem(payload) {
|
||||||
var libraryItemId = payload.libraryItemId
|
const libraryItemId = payload.libraryItemId
|
||||||
var episodeId = payload.episodeId || null
|
const episodeId = payload.episodeId || null
|
||||||
|
|
||||||
if (this.playerHandler.libraryItemId == libraryItemId && this.playerHandler.episodeId == episodeId) {
|
if (this.playerHandler.libraryItemId == libraryItemId && this.playerHandler.episodeId == episodeId) {
|
||||||
if (payload.startTime !== null && !isNaN(payload.startTime)) {
|
if (payload.startTime !== null && !isNaN(payload.startTime)) {
|
||||||
@@ -417,11 +430,12 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var libraryItem = await this.$axios.$get(`/api/items/${libraryItemId}?expanded=1`).catch((error) => {
|
const libraryItem = await this.$axios.$get(`/api/items/${libraryItemId}?expanded=1`).catch((error) => {
|
||||||
console.error('Failed to fetch full item', error)
|
console.error('Failed to fetch full item', error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
if (!libraryItem) return
|
if (!libraryItem) return
|
||||||
|
|
||||||
this.$store.commit('setMediaPlaying', {
|
this.$store.commit('setMediaPlaying', {
|
||||||
libraryItem,
|
libraryItem,
|
||||||
episodeId,
|
episodeId,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">by {{ authorName }}</p>
|
<p v-if="matchKey !== 'authors'" class="text-xs text-gray-200 truncate">by {{ authorName }}</p>
|
||||||
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
|
<p v-else class="truncate text-xs text-gray-200" v-html="matchHtml" />
|
||||||
|
|
||||||
<div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
|
<div v-if="matchKey === 'series' || matchKey === 'tags' || matchKey === 'isbn' || matchKey === 'asin' || matchKey === 'episode'" class="m-0 p-0 truncate text-xs" v-html="matchHtml" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -67,6 +67,7 @@ export default {
|
|||||||
// but with removing commas periods etc this is no longer plausible
|
// but with removing commas periods etc this is no longer plausible
|
||||||
const html = this.matchText
|
const html = this.matchText
|
||||||
|
|
||||||
|
if (this.matchKey === 'episode') return `<p class="truncate">Episode: ${html}</p>`
|
||||||
if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${html}</p>`
|
if (this.matchKey === 'tags') return `<p class="truncate">Tags: ${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>`
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="card" :id="`album-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @mousedown.prevent @mouseup.prevent @mousemove.prevent @mouseover="mouseover" @mouseleave="mouseleave" @click="clickCard">
|
||||||
|
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||||
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||||
|
<covers-preview-cover ref="cover" :src="coverSrc" :width="width" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
||||||
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="absolute z-30 left-0 right-0 mx-auto -bottom-8 h-8 py-1 rounded-md text-center">
|
||||||
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||||
|
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ artist || ' ' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
index: Number,
|
||||||
|
width: Number,
|
||||||
|
height: Number,
|
||||||
|
bookCoverAspectRatio: Number,
|
||||||
|
bookshelfView: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
albumMount: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
album: null,
|
||||||
|
isSelectionMode: false,
|
||||||
|
selected: false,
|
||||||
|
isHovering: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
coverSrc() {
|
||||||
|
const config = this.$config || this.$nuxt.$config
|
||||||
|
if (!this.album || !this.album.libraryItemId) return `${config.routerBasePath}/book_placeholder.jpg`
|
||||||
|
return this.store.getters['globals/getLibraryItemCoverSrcById'](this.album.libraryItemId)
|
||||||
|
},
|
||||||
|
labelFontSize() {
|
||||||
|
if (this.width < 160) return 0.75
|
||||||
|
return 0.875
|
||||||
|
},
|
||||||
|
sizeMultiplier() {
|
||||||
|
const baseSize = this.bookCoverAspectRatio === 1 ? 192 : 120
|
||||||
|
return this.width / baseSize
|
||||||
|
},
|
||||||
|
title() {
|
||||||
|
return this.album ? this.album.title : ''
|
||||||
|
},
|
||||||
|
artist() {
|
||||||
|
return this.album ? this.album.artist : ''
|
||||||
|
},
|
||||||
|
store() {
|
||||||
|
return this.$store || this.$nuxt.$store
|
||||||
|
},
|
||||||
|
currentLibraryId() {
|
||||||
|
return this.store.state.libraries.currentLibraryId
|
||||||
|
},
|
||||||
|
isAlternativeBookshelfView() {
|
||||||
|
const constants = this.$constants || this.$nuxt.$constants
|
||||||
|
return this.bookshelfView == constants.BookshelfView.DETAIL
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setEntity(album) {
|
||||||
|
this.album = album
|
||||||
|
},
|
||||||
|
setSelectionMode(val) {
|
||||||
|
this.isSelectionMode = val
|
||||||
|
},
|
||||||
|
mouseover() {
|
||||||
|
this.isHovering = true
|
||||||
|
},
|
||||||
|
mouseleave() {
|
||||||
|
this.isHovering = false
|
||||||
|
},
|
||||||
|
clickCard() {
|
||||||
|
if (!this.album) return
|
||||||
|
// const router = this.$router || this.$nuxt.$router
|
||||||
|
// router.push(`/album/${this.$encode(this.title)}`)
|
||||||
|
},
|
||||||
|
clickEdit() {
|
||||||
|
this.$emit('edit', this.album)
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
// destroy the vue listeners, etc
|
||||||
|
this.$destroy()
|
||||||
|
|
||||||
|
// remove the element from the DOM
|
||||||
|
if (this.$el && this.$el.parentNode) {
|
||||||
|
this.$el.parentNode.removeChild(this.$el)
|
||||||
|
} else if (this.$el && this.$el.remove) {
|
||||||
|
this.$el.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.albumMount) {
|
||||||
|
this.setEntity(this.albumMount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- More Menu Icon -->
|
<!-- More Menu Icon -->
|
||||||
<div ref="moreIcon" v-show="!isSelectionMode" 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>
|
</div>
|
||||||
@@ -190,6 +190,9 @@ export default {
|
|||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.mediaType === 'podcast'
|
return this.mediaType === 'podcast'
|
||||||
},
|
},
|
||||||
|
isMusic() {
|
||||||
|
return this.mediaType === 'music'
|
||||||
|
},
|
||||||
placeholderUrl() {
|
placeholderUrl() {
|
||||||
const config = this.$config || this.$nuxt.$config
|
const config = this.$config || this.$nuxt.$config
|
||||||
return `${config.routerBasePath}/book_placeholder.jpg`
|
return `${config.routerBasePath}/book_placeholder.jpg`
|
||||||
@@ -257,7 +260,7 @@ export default {
|
|||||||
return this.bookCoverAspectRatio === 1
|
return this.bookCoverAspectRatio === 1
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
var baseSize = this.squareAspectRatio ? 192 : 120
|
const baseSize = this.squareAspectRatio ? 192 : 120
|
||||||
return this.width / baseSize
|
return this.width / baseSize
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
@@ -273,6 +276,10 @@ export default {
|
|||||||
authorLF() {
|
authorLF() {
|
||||||
return this.mediaMetadata.authorNameLF
|
return this.mediaMetadata.authorNameLF
|
||||||
},
|
},
|
||||||
|
artist() {
|
||||||
|
const artists = this.mediaMetadata.artists || []
|
||||||
|
return artists.join(', ')
|
||||||
|
},
|
||||||
displayTitle() {
|
displayTitle() {
|
||||||
if (this.recentEpisode) return this.recentEpisode.title
|
if (this.recentEpisode) return this.recentEpisode.title
|
||||||
const ignorePrefix = this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix
|
const ignorePrefix = this.orderBy === 'media.metadata.title' && this.sortingIgnorePrefix
|
||||||
@@ -282,6 +289,7 @@ export default {
|
|||||||
displayLineTwo() {
|
displayLineTwo() {
|
||||||
if (this.recentEpisode) return this.title
|
if (this.recentEpisode) return this.title
|
||||||
if (this.isPodcast) return this.author
|
if (this.isPodcast) return this.author
|
||||||
|
if (this.isMusic) return this.artist
|
||||||
if (this.collapsedSeries) return ''
|
if (this.collapsedSeries) return ''
|
||||||
if (this.isAuthorBookshelfView) {
|
if (this.isAuthorBookshelfView) {
|
||||||
return this.mediaMetadata.publishedYear || ''
|
return this.mediaMetadata.publishedYear || ''
|
||||||
@@ -305,6 +313,7 @@ export default {
|
|||||||
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id)
|
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id)
|
||||||
},
|
},
|
||||||
userProgress() {
|
userProgress() {
|
||||||
|
if (this.isMusic) return null
|
||||||
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)
|
||||||
},
|
},
|
||||||
@@ -341,7 +350,7 @@ export default {
|
|||||||
return !this.isSelectionMode && !this.showPlayButton && this.hasEbook && (this.showExperimentalFeatures || this.enableEReader)
|
return !this.isSelectionMode && !this.showPlayButton && this.hasEbook && (this.showExperimentalFeatures || this.enableEReader)
|
||||||
},
|
},
|
||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode)
|
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.hasEbook && (this.showExperimentalFeatures || this.enableEReader)
|
||||||
@@ -366,7 +375,7 @@ export default {
|
|||||||
if (this.isPodcast) return 'Podcast has no episodes'
|
if (this.isPodcast) return 'Podcast has no episodes'
|
||||||
return 'Item has no audio tracks & ebook'
|
return 'Item has no audio tracks & ebook'
|
||||||
}
|
}
|
||||||
var txt = ''
|
let txt = ''
|
||||||
if (this.numMissingParts) {
|
if (this.numMissingParts) {
|
||||||
txt += `${this.numMissingParts} missing parts.`
|
txt += `${this.numMissingParts} missing parts.`
|
||||||
}
|
}
|
||||||
@@ -377,7 +386,7 @@ export default {
|
|||||||
return txt || 'Unknown Error'
|
return txt || 'Unknown Error'
|
||||||
},
|
},
|
||||||
overlayWrapperClasslist() {
|
overlayWrapperClasslist() {
|
||||||
var classes = []
|
const classes = []
|
||||||
if (this.isSelectionMode) classes.push('bg-opacity-60')
|
if (this.isSelectionMode) classes.push('bg-opacity-60')
|
||||||
else classes.push('bg-opacity-40')
|
else classes.push('bg-opacity-40')
|
||||||
if (this.selected) {
|
if (this.selected) {
|
||||||
@@ -401,6 +410,8 @@ export default {
|
|||||||
return this.store.getters['user/getIsAdminOrUp']
|
return this.store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
moreMenuItems() {
|
moreMenuItems() {
|
||||||
|
if (this.isMusic) return []
|
||||||
|
|
||||||
if (this.recentEpisode) {
|
if (this.recentEpisode) {
|
||||||
const items = [
|
const items = [
|
||||||
{
|
{
|
||||||
@@ -438,7 +449,7 @@ export default {
|
|||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
|
|
||||||
var items = []
|
let items = []
|
||||||
if (!this.isPodcast) {
|
if (!this.isPodcast) {
|
||||||
items = [
|
items = [
|
||||||
{
|
{
|
||||||
@@ -534,11 +545,11 @@ export default {
|
|||||||
return this.author
|
return this.author
|
||||||
},
|
},
|
||||||
isAlternativeBookshelfView() {
|
isAlternativeBookshelfView() {
|
||||||
var constants = this.$constants || this.$nuxt.$constants
|
const constants = this.$constants || this.$nuxt.$constants
|
||||||
return this.bookshelfView === constants.BookshelfView.DETAIL
|
return this.bookshelfView === constants.BookshelfView.DETAIL
|
||||||
},
|
},
|
||||||
isAuthorBookshelfView() {
|
isAuthorBookshelfView() {
|
||||||
var constants = this.$constants || this.$nuxt.$constants
|
const constants = this.$constants || this.$nuxt.$constants
|
||||||
return this.bookshelfView === constants.BookshelfView.AUTHOR
|
return this.bookshelfView === constants.BookshelfView.AUTHOR
|
||||||
},
|
},
|
||||||
titleDisplayBottomOffset() {
|
titleDisplayBottomOffset() {
|
||||||
@@ -548,7 +559,7 @@ export default {
|
|||||||
},
|
},
|
||||||
rssFeed() {
|
rssFeed() {
|
||||||
if (this.booksInSeries) return null
|
if (this.booksInSeries) return null
|
||||||
return this.store.getters['feeds/getFeedForItem'](this.libraryItemId)
|
return this._libraryItem.rssFeed || null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -809,7 +820,6 @@ export default {
|
|||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
if (!libraryItem) return
|
if (!libraryItem) return
|
||||||
console.log('Got library itemn', libraryItem)
|
|
||||||
this.store.commit('showEReader', libraryItem)
|
this.store.commit('showEReader', libraryItem)
|
||||||
},
|
},
|
||||||
selectBtnClick(evt) {
|
selectBtnClick(evt) {
|
||||||
|
|||||||
@@ -9,6 +9,9 @@
|
|||||||
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
<span class="material-icons text-xl text-white text-opacity-75 hover:text-opacity-100">edit</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem', fontSize: 1.5 * sizeMultiplier + 'rem' }">rss_feed</span>
|
||||||
|
|
||||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||||
@@ -72,6 +75,9 @@ export default {
|
|||||||
},
|
},
|
||||||
userCanUpdate() {
|
userCanUpdate() {
|
||||||
return this.store.getters['user/getUserCanUpdate']
|
return this.store.getters['user/getUserCanUpdate']
|
||||||
|
},
|
||||||
|
rssFeed() {
|
||||||
|
return this.collection ? this.collection.rssFeed : null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
<p class="font-book" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
<p class="font-book" :style="{ fontSize: 1.2 * sizeMultiplier + 'rem' }">{{ displayTitle }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<span v-if="!isHovering && rssFeed" class="absolute z-10 material-icons text-success" :style="{ top: 0.5 * sizeMultiplier + 'rem', left: 0.5 * sizeMultiplier + 'rem', fontSize: 1.5 * sizeMultiplier + 'rem' }">rss_feed</span>
|
||||||
|
|
||||||
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
<div v-if="!isAlternativeBookshelfView" class="categoryPlacard absolute z-10 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(200, width) + 'px' }">
|
||||||
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
<div class="w-full h-full shinyBlack flex items-center justify-center rounded-sm border" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ displayTitle }}</p>
|
||||||
@@ -125,6 +127,9 @@ export default {
|
|||||||
isAlternativeBookshelfView() {
|
isAlternativeBookshelfView() {
|
||||||
const constants = this.$constants || this.$nuxt.$constants
|
const constants = this.$constants || this.$nuxt.$constants
|
||||||
return this.bookshelfView == constants.BookshelfView.DETAIL
|
return this.bookshelfView == constants.BookshelfView.DETAIL
|
||||||
|
},
|
||||||
|
rssFeed() {
|
||||||
|
return this.series ? this.series.rssFeed : null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -87,8 +87,14 @@ export default {
|
|||||||
this.$emit('input', val)
|
this.$emit('input', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
libraryMediaType() {
|
||||||
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||||
|
},
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast'
|
return this.libraryMediaType === 'podcast'
|
||||||
|
},
|
||||||
|
isMusic() {
|
||||||
|
return this.libraryMediaType === 'music'
|
||||||
},
|
},
|
||||||
seriesItems() {
|
seriesItems() {
|
||||||
return [
|
return [
|
||||||
@@ -214,9 +220,33 @@ export default {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
musicItems() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelAll,
|
||||||
|
value: 'all'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelGenre,
|
||||||
|
value: 'genres',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelTag,
|
||||||
|
value: 'tags',
|
||||||
|
sublist: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.ButtonIssues,
|
||||||
|
value: 'issues',
|
||||||
|
sublist: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
selectItems() {
|
selectItems() {
|
||||||
if (this.isSeries) return this.seriesItems
|
if (this.isSeries) return this.seriesItems
|
||||||
if (this.isPodcast) return this.podcastItems
|
if (this.isPodcast) return this.podcastItems
|
||||||
|
if (this.isMusic) return this.musicItems
|
||||||
return this.bookItems
|
return this.bookItems
|
||||||
},
|
},
|
||||||
selectedItemSublist() {
|
selectedItemSublist() {
|
||||||
|
|||||||
@@ -50,8 +50,14 @@ export default {
|
|||||||
this.$emit('update:descending', val)
|
this.$emit('update:descending', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
libraryMediaType() {
|
||||||
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||||
|
},
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.$store.getters['libraries/getCurrentLibraryMediaType'] == 'podcast'
|
return this.libraryMediaType === 'podcast'
|
||||||
|
},
|
||||||
|
isMusic() {
|
||||||
|
return this.libraryMediaType === 'music'
|
||||||
},
|
},
|
||||||
podcastItems() {
|
podcastItems() {
|
||||||
return [
|
return [
|
||||||
@@ -134,10 +140,40 @@ export default {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
musicItems() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelTitle,
|
||||||
|
value: 'media.metadata.title'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelAddedAt,
|
||||||
|
value: 'addedAt'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelSize,
|
||||||
|
value: 'size'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelDuration,
|
||||||
|
value: 'media.duration'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelFileBirthtime,
|
||||||
|
value: 'birthtimeMs'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.$strings.LabelFileModified,
|
||||||
|
value: 'mtimeMs'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
selectItems() {
|
selectItems() {
|
||||||
let items = null
|
let items = null
|
||||||
if (this.isPodcast) {
|
if (this.isPodcast) {
|
||||||
items = this.podcastItems
|
items = this.podcastItems
|
||||||
|
} else if (this.isMusic) {
|
||||||
|
items = this.musicItems
|
||||||
} else if (this.$store.getters['user/getUserSetting']('filterBy').startsWith('series.')) {
|
} else if (this.$store.getters['user/getUserSetting']('filterBy').startsWith('series.')) {
|
||||||
items = this.seriesItems
|
items = this.seriesItems
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export default {
|
|||||||
showMenu: false,
|
showMenu: false,
|
||||||
currentPlaybackRate: 0,
|
currentPlaybackRate: 0,
|
||||||
MIN_SPEED: 0.5,
|
MIN_SPEED: 0.5,
|
||||||
MAX_SPEED: 3,
|
MAX_SPEED: 10,
|
||||||
menuLeft: -92,
|
menuLeft: -92,
|
||||||
arrowLeft: 0
|
arrowLeft: 0
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ export default {
|
|||||||
return `${this.naturalWidth}x${this.naturalHeight}px`
|
return `${this.naturalWidth}x${this.naturalHeight}px`
|
||||||
},
|
},
|
||||||
placeholderUrl() {
|
placeholderUrl() {
|
||||||
return `${this.$config.routerBasePath}/book_placeholder.jpg`
|
const config = this.$config || this.$nuxt.$config
|
||||||
|
return `${config.routerBasePath}/book_placeholder.jpg`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -22,8 +22,8 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<div class="flex items-center pt-4 px-2">
|
<div class="flex items-center pt-4 px-2">
|
||||||
<p class="px-3 font-semibold" :class="isEditingRoot ? 'text-gray-300' : ''">{{ $strings.LabelEnable }}</p>
|
<p class="px-3 font-semibold" id="user-enabled-toggle" :class="isEditingRoot ? 'text-gray-300' : ''">{{ $strings.LabelEnable }}</p>
|
||||||
<ui-toggle-switch v-model="newUser.isActive" :disabled="isEditingRoot" />
|
<ui-toggle-switch labeledBy="user-enabled-toggle" v-model="newUser.isActive" :disabled="isEditingRoot" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -31,55 +31,55 @@
|
|||||||
<p class="text-lg mb-2 font-semibold">{{ $strings.HeaderPermissions }}</p>
|
<p class="text-lg mb-2 font-semibold">{{ $strings.HeaderPermissions }}</p>
|
||||||
<div class="flex items-center my-2 max-w-md">
|
<div class="flex items-center my-2 max-w-md">
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<p>{{ $strings.LabelPermissionsDownload }}</p>
|
<p id="download-permissions-toggle">{{ $strings.LabelPermissionsDownload }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<ui-toggle-switch v-model="newUser.permissions.download" />
|
<ui-toggle-switch labeledBy="download-permissions-toggle" v-model="newUser.permissions.download" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center my-2 max-w-md">
|
<div class="flex items-center my-2 max-w-md">
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<p>{{ $strings.LabelPermissionsUpdate }}</p>
|
<p id="update-permissions-toggle">{{ $strings.LabelPermissionsUpdate }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<ui-toggle-switch v-model="newUser.permissions.update" />
|
<ui-toggle-switch labeledBy="update-permissions-toggle" v-model="newUser.permissions.update" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center my-2 max-w-md">
|
<div class="flex items-center my-2 max-w-md">
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<p>{{ $strings.LabelPermissionsDelete }}</p>
|
<p id="delete-permissions-toggle">{{ $strings.LabelPermissionsDelete }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<ui-toggle-switch v-model="newUser.permissions.delete" />
|
<ui-toggle-switch labeledBy="delete-permissions-toggle" v-model="newUser.permissions.delete" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center my-2 max-w-md">
|
<div class="flex items-center my-2 max-w-md">
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<p>{{ $strings.LabelPermissionsUpload }}</p>
|
<p id="upload-permissions-toggle">{{ $strings.LabelPermissionsUpload }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<ui-toggle-switch v-model="newUser.permissions.upload" />
|
<ui-toggle-switch labeledBy="upload-permissions-toggle" v-model="newUser.permissions.upload" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center my-2 max-w-md">
|
<div class="flex items-center my-2 max-w-md">
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<p>{{ $strings.LabelPermissionsAccessExplicitContent }}</p>
|
<p id="explicit-content-permissions-toggle">{{ $strings.LabelPermissionsAccessExplicitContent }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<ui-toggle-switch v-model="newUser.permissions.accessExplicitContent" />
|
<ui-toggle-switch labeledBy="explicit-content-permissions-toggle" v-model="newUser.permissions.accessExplicitContent" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center my-2 max-w-md">
|
<div class="flex items-center my-2 max-w-md">
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<p>{{ $strings.LabelPermissionsAccessAllLibraries }}</p>
|
<p id="access-all-libs--permissions-toggle">{{ $strings.LabelPermissionsAccessAllLibraries }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/2">
|
<div class="w-1/2">
|
||||||
<ui-toggle-switch v-model="newUser.permissions.accessAllLibraries" @input="accessAllLibrariesToggled" />
|
<ui-toggle-switch labeledBy="access-all-libs--permissions-toggle" v-model="newUser.permissions.accessAllLibraries" @input="accessAllLibrariesToggled" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,8 @@ export default {
|
|||||||
this.processing = true
|
this.processing = true
|
||||||
var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => {
|
var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
this.$toast.error(this.$strings.ToastAuthorUpdateFailed)
|
const errorMsg = error.response ? error.response.data : null
|
||||||
|
this.$toast.error(errorMsg || this.$strings.ToastAuthorUpdateFailed)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
if (result) {
|
if (result) {
|
||||||
@@ -125,8 +126,7 @@ export default {
|
|||||||
},
|
},
|
||||||
async removeCover() {
|
async removeCover() {
|
||||||
var updatePayload = {
|
var updatePayload = {
|
||||||
imagePath: null,
|
imagePath: null
|
||||||
relImagePath: null
|
|
||||||
}
|
}
|
||||||
this.processing = true
|
this.processing = true
|
||||||
var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => {
|
var result = await this.$axios.$patch(`/api/authors/${this.authorId}`, updatePayload).catch((error) => {
|
||||||
@@ -161,8 +161,7 @@ export default {
|
|||||||
if (response.author.imagePath) {
|
if (response.author.imagePath) {
|
||||||
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
this.$toast.success(this.$strings.ToastAuthorUpdateSuccess)
|
||||||
this.$store.commit('globals/showEditAuthorModal', response.author)
|
this.$store.commit('globals/showEditAuthorModal', response.author)
|
||||||
}
|
} else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
|
||||||
else this.$toast.success(this.$strings.ToastAuthorUpdateSuccessNoImageFound)
|
|
||||||
} else {
|
} else {
|
||||||
this.$toast.info('No updates were made for Author')
|
this.$toast.info('No updates were made for Author')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -396,6 +396,12 @@ export default {
|
|||||||
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-provider') || 'google'
|
||||||
|
|
||||||
|
// Prefer using ASIN if set and using audible provider
|
||||||
|
if (this.provider.startsWith('audible') && this.libraryItem.media.metadata.asin) {
|
||||||
|
this.searchTitle = this.libraryItem.media.metadata.asin
|
||||||
|
this.searchAuthor = ''
|
||||||
|
}
|
||||||
|
|
||||||
if (this.searchTitle) {
|
if (this.searchTitle) {
|
||||||
this.submitSearch()
|
this.submitSearch()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<ui-dropdown v-model="mediaType" :items="mediaTypes" :label="$strings.LabelMediaType" :disabled="!isNew" small @input="changedMediaType" />
|
<ui-dropdown v-model="mediaType" :items="mediaTypes" :label="$strings.LabelMediaType" :disabled="!isNew" small @input="changedMediaType" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full md:flex-grow px-1 py-1 md:py-0">
|
<div class="w-full md:flex-grow px-1 py-1 md:py-0">
|
||||||
<ui-text-input-with-label v-model="name" :label="$strings.LabelLibraryName" @blur="nameBlurred" />
|
<ui-text-input-with-label ref="nameInput" v-model="name" :label="$strings.LabelLibraryName" @blur="nameBlurred" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/5 md:w-18 px-1 py-1 md:py-0">
|
<div class="w-1/5 md:w-18 px-1 py-1 md:py-0">
|
||||||
<ui-media-icon-picker v-model="icon" :label="$strings.LabelIcon" @input="iconChanged" />
|
<ui-media-icon-picker v-model="icon" :label="$strings.LabelIcon" @input="iconChanged" />
|
||||||
@@ -20,12 +20,12 @@
|
|||||||
<p class="px-1 text-sm font-semibold">{{ $strings.LabelFolders }}</p>
|
<p class="px-1 text-sm font-semibold">{{ $strings.LabelFolders }}</p>
|
||||||
<div v-for="(folder, index) in folders" :key="index" class="w-full flex items-center py-1 px-2">
|
<div v-for="(folder, index) in folders" :key="index" class="w-full flex items-center py-1 px-2">
|
||||||
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||||
<ui-editable-text v-model="folder.fullPath" readonly type="text" class="w-full" />
|
<ui-editable-text ref="folderInput" v-model="folder.fullPath" readonly type="text" class="w-full" />
|
||||||
<span v-show="folders.length > 1" class="material-icons text-2xl ml-2 cursor-pointer hover:text-error" @click="removeFolder(folder)">close</span>
|
<span v-show="folders.length > 1" class="material-icons text-2xl ml-2 cursor-pointer hover:text-error" @click="removeFolder(folder)">close</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex py-1 px-2 items-center w-full">
|
<div class="flex py-1 px-2 items-center w-full">
|
||||||
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
<span class="material-icons bg-opacity-50 mr-2 text-yellow-200" style="font-size: 1.2rem">folder</span>
|
||||||
<ui-editable-text v-model="newFolderPath" :placeholder="$strings.PlaceholderNewFolderPath" type="text" class="w-full" @blur="newFolderInputBlurred" />
|
<ui-editable-text ref="newFolderInput" v-model="newFolderPath" :placeholder="$strings.PlaceholderNewFolderPath" type="text" class="w-full" @blur="newFolderInputBlurred" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ui-btn class="w-full mt-2" color="primary" @click="browseForFolder">{{ $strings.ButtonBrowseForFolder }}</ui-btn>
|
<ui-btn class="w-full mt-2" color="primary" @click="browseForFolder">{{ $strings.ButtonBrowseForFolder }}</ui-btn>
|
||||||
@@ -67,6 +67,10 @@ export default {
|
|||||||
value: 'podcast',
|
value: 'podcast',
|
||||||
text: this.$strings.LabelPodcasts
|
text: this.$strings.LabelPodcasts
|
||||||
}
|
}
|
||||||
|
// {
|
||||||
|
// value: 'music',
|
||||||
|
// text: 'Music'
|
||||||
|
// }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
folderPaths() {
|
folderPaths() {
|
||||||
@@ -78,6 +82,19 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
checkBlurExpressionInput() {
|
||||||
|
if (this.$refs.nameInput) {
|
||||||
|
this.$refs.nameInput.blur()
|
||||||
|
}
|
||||||
|
if (this.$refs.folderInput && this.$refs.folderInput.length) {
|
||||||
|
this.$refs.folderInput.forEach((input) => {
|
||||||
|
if (input.blur) input.blur()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (this.$refs.newFolderInput) {
|
||||||
|
this.$refs.newFolderInput.blur()
|
||||||
|
}
|
||||||
|
},
|
||||||
browseForFolder() {
|
browseForFolder() {
|
||||||
this.showDirectoryPicker = true
|
this.showDirectoryPicker = true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -144,8 +144,6 @@ export default {
|
|||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
submit() {
|
submit() {
|
||||||
if (!this.validate()) return
|
|
||||||
|
|
||||||
// If custom expression input is focused then unfocus it instead of submitting
|
// If custom expression input is focused then unfocus it instead of submitting
|
||||||
if (this.$refs.tabComponent && this.$refs.tabComponent.checkBlurExpressionInput) {
|
if (this.$refs.tabComponent && this.$refs.tabComponent.checkBlurExpressionInput) {
|
||||||
if (this.$refs.tabComponent.checkBlurExpressionInput()) {
|
if (this.$refs.tabComponent.checkBlurExpressionInput()) {
|
||||||
@@ -153,6 +151,8 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.validate()) return
|
||||||
|
|
||||||
if (this.library) {
|
if (this.library) {
|
||||||
this.submitUpdateLibrary()
|
this.submitUpdateLibrary()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+33
-44
@@ -6,13 +6,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="wrapper" class="px-8 py-6 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
<div ref="wrapper" class="px-8 py-6 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-hidden">
|
||||||
<div v-if="currentFeedUrl" class="w-full">
|
<div v-if="currentFeed" class="w-full">
|
||||||
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedIsOpen }}</p>
|
<p class="text-lg font-semibold mb-4">{{ $strings.HeaderRSSFeedIsOpen }}</p>
|
||||||
|
|
||||||
<div class="w-full relative">
|
<div class="w-full relative">
|
||||||
<ui-text-input v-model="currentFeedUrl" readonly />
|
<ui-text-input v-model="currentFeed.feedUrl" readonly />
|
||||||
|
|
||||||
<span class="material-icons absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(currentFeedUrl)">content_copy</span>
|
<span class="material-icons absolute right-2 bottom-2 p-0.5 text-base transition-transform duration-100 text-gray-300 hover:text-white transform hover:scale-125 cursor-pointer" @click="copyToClipboard(currentFeed.feedUrl)">content_copy</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-full">
|
<div v-else class="w-full">
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-show="userIsAdminOrUp" class="flex items-center pt-6">
|
<div v-show="userIsAdminOrUp" class="flex items-center pt-6">
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn v-if="currentFeedUrl" color="error" small @click="closeFeed">{{ $strings.ButtonCloseFeed }}</ui-btn>
|
<ui-btn v-if="currentFeed" color="error" small @click="closeFeed">{{ $strings.ButtonCloseFeed }}</ui-btn>
|
||||||
<ui-btn v-else color="success" small @click="openFeed">{{ $strings.ButtonOpenFeed }}</ui-btn>
|
<ui-btn v-else color="success" small @click="openFeed">{{ $strings.ButtonOpenFeed }}</ui-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,19 +37,11 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: {
|
|
||||||
value: Boolean,
|
|
||||||
libraryItem: {
|
|
||||||
type: Object,
|
|
||||||
default: () => null
|
|
||||||
},
|
|
||||||
feedUrl: String
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
processing: false,
|
processing: false,
|
||||||
newFeedSlug: null,
|
newFeedSlug: null,
|
||||||
currentFeedUrl: null
|
currentFeed: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -65,23 +57,29 @@ export default {
|
|||||||
computed: {
|
computed: {
|
||||||
show: {
|
show: {
|
||||||
get() {
|
get() {
|
||||||
return this.value
|
return this.$store.state.globals.showRSSFeedOpenCloseModal
|
||||||
},
|
},
|
||||||
set(val) {
|
set(val) {
|
||||||
this.$emit('input', val)
|
this.$store.commit('globals/setShowRSSFeedOpenCloseModal', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
libraryItemId() {
|
rssFeedEntity() {
|
||||||
return this.libraryItem.id
|
return this.$store.state.globals.rssFeedEntity || {}
|
||||||
},
|
},
|
||||||
media() {
|
entityId() {
|
||||||
return this.libraryItem.media || {}
|
return this.rssFeedEntity.id
|
||||||
},
|
},
|
||||||
mediaMetadata() {
|
entityType() {
|
||||||
return this.media.metadata || {}
|
return this.rssFeedEntity.type
|
||||||
|
},
|
||||||
|
entityFeed() {
|
||||||
|
return this.rssFeedEntity.feed
|
||||||
|
},
|
||||||
|
hasEpisodesWithoutPubDate() {
|
||||||
|
return !!this.rssFeedEntity.hasEpisodesWithoutPubDate
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
return this.mediaMetadata.title
|
return this.rssFeedEntity.name
|
||||||
},
|
},
|
||||||
userIsAdminOrUp() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getIsAdminOrUp']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
@@ -91,12 +89,6 @@ export default {
|
|||||||
},
|
},
|
||||||
isHttp() {
|
isHttp() {
|
||||||
return window.origin.startsWith('http://')
|
return window.origin.startsWith('http://')
|
||||||
},
|
|
||||||
episodes() {
|
|
||||||
return this.media.episodes || []
|
|
||||||
},
|
|
||||||
hasEpisodesWithoutPubDate() {
|
|
||||||
return this.episodes.some((ep) => !ep.pubDate)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -106,7 +98,7 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var sanitized = this.$sanitizeSlug(this.newFeedSlug)
|
const sanitized = this.$sanitizeSlug(this.newFeedSlug)
|
||||||
if (this.newFeedSlug !== sanitized) {
|
if (this.newFeedSlug !== sanitized) {
|
||||||
this.newFeedSlug = sanitized
|
this.newFeedSlug = sanitized
|
||||||
this.$toast.warning('Slug had to be modified - Run again')
|
this.$toast.warning('Slug had to be modified - Run again')
|
||||||
@@ -121,19 +113,15 @@ export default {
|
|||||||
|
|
||||||
console.log('Payload', payload)
|
console.log('Payload', payload)
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post(`/api/items/${this.libraryItemId}/open-feed`, payload)
|
.$post(`/api/feeds/${this.entityType}/${this.entityId}/open`, payload)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.success) {
|
console.log('Opened RSS Feed', data)
|
||||||
console.log('Opened RSS Feed', data)
|
this.currentFeed = data.feed
|
||||||
this.currentFeedUrl = data.feedUrl
|
|
||||||
} else {
|
|
||||||
const errorMsg = data.error || 'Unknown error'
|
|
||||||
this.$toast.error(errorMsg)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to open RSS Feed', error)
|
console.error('Failed to open RSS Feed', error)
|
||||||
this.$toast.error()
|
const errorMsg = error.response ? error.response.data : null
|
||||||
|
this.$toast.error(errorMsg || 'Failed to open RSS Feed')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
copyToClipboard(str) {
|
copyToClipboard(str) {
|
||||||
@@ -142,22 +130,23 @@ export default {
|
|||||||
closeFeed() {
|
closeFeed() {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post(`/api/items/${this.libraryItem.id}/close-feed`)
|
.$post(`/api/feeds/${this.currentFeed.id}/close`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$toast.success(this.$strings.ToastRSSFeedCloseSuccess)
|
this.$toast.success(this.$strings.ToastRSSFeedCloseSuccess)
|
||||||
this.show = false
|
this.show = false
|
||||||
this.processing = false
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error('Failed to close RSS feed', error)
|
console.error('Failed to close RSS feed', error)
|
||||||
this.processing = false
|
|
||||||
this.$toast.error(this.$strings.ToastRSSFeedCloseFailed)
|
this.$toast.error(this.$strings.ToastRSSFeedCloseFailed)
|
||||||
})
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.processing = false
|
||||||
|
})
|
||||||
},
|
},
|
||||||
init() {
|
init() {
|
||||||
if (!this.libraryItem) return
|
if (!this.entityId) return
|
||||||
this.newFeedSlug = this.libraryItem.id
|
this.newFeedSlug = this.entityId
|
||||||
this.currentFeedUrl = this.feedUrl
|
this.currentFeed = this.entityFeed
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<template>
|
||||||
|
<div class="h-full w-full">
|
||||||
|
<div id="viewer" class="border border-gray-100 bg-white text-black shadow-md h-screen overflow-y-auto p-4" v-html="pageHtml"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
url: String,
|
||||||
|
libraryItem: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
bookInfo: {},
|
||||||
|
page: 0,
|
||||||
|
numPages: 0,
|
||||||
|
pageHtml: '',
|
||||||
|
progress: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
libraryItemId() {
|
||||||
|
return this.libraryItem ? this.libraryItem.id : null
|
||||||
|
},
|
||||||
|
hasPrev() {
|
||||||
|
return this.page > 0
|
||||||
|
},
|
||||||
|
hasNext() {
|
||||||
|
return this.page < this.numPages - 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
prev() {
|
||||||
|
if (!this.hasPrev) return
|
||||||
|
this.page--
|
||||||
|
this.loadPage()
|
||||||
|
},
|
||||||
|
next() {
|
||||||
|
if (!this.hasNext) return
|
||||||
|
this.page++
|
||||||
|
this.loadPage()
|
||||||
|
},
|
||||||
|
keyUp() {
|
||||||
|
if ((e.keyCode || e.which) == 37) {
|
||||||
|
this.prev()
|
||||||
|
} else if ((e.keyCode || e.which) == 39) {
|
||||||
|
this.next()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadPage() {
|
||||||
|
this.$axios
|
||||||
|
.$get(`/api/ebooks/${this.libraryItemId}/page/${this.page}?dev=${this.$isDev ? 1 : 0}`)
|
||||||
|
.then((html) => {
|
||||||
|
this.pageHtml = html
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to load page', error)
|
||||||
|
this.$toast.error('Failed to load page')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
loadInfo() {
|
||||||
|
this.$axios
|
||||||
|
.$get(`/api/ebooks/${this.libraryItemId}/info?dev=${this.$isDev ? 1 : 0}`)
|
||||||
|
.then((bookInfo) => {
|
||||||
|
this.bookInfo = bookInfo
|
||||||
|
this.numPages = bookInfo.pages
|
||||||
|
this.page = 0
|
||||||
|
this.loadPage()
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to load page', error)
|
||||||
|
this.$toast.error('Failed to load info')
|
||||||
|
})
|
||||||
|
},
|
||||||
|
initEpub() {
|
||||||
|
if (!this.libraryItemId) return
|
||||||
|
this.loadInfo()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initEpub()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
<p v-if="abAuthor">by {{ abAuthor }}</p>
|
<p v-if="abAuthor">by {{ abAuthor }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<component v-if="componentName" ref="readerComponent" :is="componentName" :url="ebookUrl" />
|
<component v-if="componentName" ref="readerComponent" :is="componentName" :url="ebookUrl" :library-item="selectedLibraryItem" />
|
||||||
|
|
||||||
<div class="absolute bottom-2 left-2">{{ ebookType }}</div>
|
<div class="absolute bottom-2 left-2">{{ ebookType }}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,7 +37,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
componentName() {
|
componentName() {
|
||||||
if (this.ebookType === 'epub') return 'readers-epub-reader'
|
if (this.ebookType === 'epub' && this.$isDev) return 'readers-epub-reader2'
|
||||||
|
else if (this.ebookType === 'epub') return 'readers-epub-reader'
|
||||||
else if (this.ebookType === 'mobi') return 'readers-mobi-reader'
|
else if (this.ebookType === 'mobi') return 'readers-mobi-reader'
|
||||||
else if (this.ebookType === 'pdf') return 'readers-pdf-reader'
|
else if (this.ebookType === 'pdf') return 'readers-pdf-reader'
|
||||||
else if (this.ebookType === 'comic') return 'readers-comic-reader'
|
else if (this.ebookType === 'comic') return 'readers-comic-reader'
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
<div class="absolute -bottom-2 left-0 flex ml-6">
|
<div class="absolute -bottom-2 left-0 flex ml-6">
|
||||||
<template v-for="dayObj in last7Days">
|
<template v-for="dayObj in last7Days">
|
||||||
<div :key="dayObj.date" :style="{ width: daySpacing + daySpacing / 14 + 'px' }">
|
<div :key="dayObj.date" :style="{ width: daySpacing + daySpacing / 14 + 'px' }">
|
||||||
<p class="text-sm font-book">{{ dayObj.dayOfWeek.slice(0, 3) }}</p>
|
<p class="text-sm font-book">{{ dayObj.dayOfWeekAbbr }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
@@ -108,6 +108,7 @@ export default {
|
|||||||
var _date = this.$addDaysToToday(i * -1)
|
var _date = this.$addDaysToToday(i * -1)
|
||||||
days.push({
|
days.push({
|
||||||
dayOfWeek: this.$formatJsDate(_date, 'EEEE'),
|
dayOfWeek: this.$formatJsDate(_date, 'EEEE'),
|
||||||
|
dayOfWeekAbbr: this.$formatJsDate(_date, 'EEE'),
|
||||||
date: this.$formatJsDate(_date, 'yyyy-MM-dd')
|
date: this.$formatJsDate(_date, 'yyyy-MM-dd')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -218,4 +219,4 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export default {
|
|||||||
dayLabels() {
|
dayLabels() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Mon',
|
label: this.$formatJsDate(new Date(2023, 0, 2), 'EEE'),
|
||||||
style: {
|
style: {
|
||||||
transform: `translate(${-25}px, ${13}px)`,
|
transform: `translate(${-25}px, ${13}px)`,
|
||||||
lineHeight: '10px',
|
lineHeight: '10px',
|
||||||
@@ -76,7 +76,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Wed',
|
label: this.$formatJsDate(new Date(2023, 0, 4), 'EEE'),
|
||||||
style: {
|
style: {
|
||||||
transform: `translate(${-25}px, ${13 * 3}px)`,
|
transform: `translate(${-25}px, ${13 * 3}px)`,
|
||||||
lineHeight: '10px',
|
lineHeight: '10px',
|
||||||
@@ -84,7 +84,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Fri',
|
label: this.$formatJsDate(new Date(2023, 0, 6), 'EEE'),
|
||||||
style: {
|
style: {
|
||||||
transform: `translate(${-25}px, ${13 * 5}px)`,
|
transform: `translate(${-25}px, ${13 * 5}px)`,
|
||||||
lineHeight: '10px',
|
lineHeight: '10px',
|
||||||
@@ -270,4 +270,4 @@ export default {
|
|||||||
},
|
},
|
||||||
beforeDestroy() {}
|
beforeDestroy() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -35,13 +35,13 @@
|
|||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</td>
|
</td>
|
||||||
<td class="py-0">
|
<td class="py-0">
|
||||||
<div class="w-full flex justify-center">
|
<div class="w-full flex justify-left">
|
||||||
<!-- Dont show edit for non-root users -->
|
<!-- Dont show edit for non-root users -->
|
||||||
<div v-if="user.type !== 'root' || userIsRoot" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-opacity-100 cursor-pointer" @click.stop="editUser(user)">
|
<div v-if="user.type !== 'root' || userIsRoot" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-opacity-100 cursor-pointer" @click.stop="editUser(user)">
|
||||||
<span class="material-icons text-base">edit</span>
|
<button type="button" :aria-label="$getString('ButtonUserEdit', [user.username])" class="material-icons text-base">edit</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="user.type !== 'root'" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="deleteUserClick(user)">
|
<div v-show="user.type !== 'root'" class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="deleteUserClick(user)">
|
||||||
<span class="material-icons text-base">delete</span>
|
<button type="button" :aria-label="$getString('ButtonUserDelete', [user.username])" class="material-icons text-base">delete</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,32 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full px-4 h-12 border border-white border-opacity-10 flex items-center relative -mt-px" :class="selected ? 'bg-primary bg-opacity-50' : 'hover:bg-primary hover:bg-opacity-25'" @mouseover="mouseover = true" @mouseleave="mouseover = false">
|
<div class="w-full pl-2 pr-4 md:px-4 h-12 border border-white border-opacity-10 flex items-center relative -mt-px" :class="selected ? 'bg-primary bg-opacity-50' : 'hover:bg-primary hover:bg-opacity-25'" @mouseover="mouseover = true" @mouseleave="mouseover = false">
|
||||||
<div v-show="selected" class="absolute top-0 left-0 h-full w-0.5 bg-warning z-10" />
|
<div v-show="selected" class="absolute top-0 left-0 h-full w-0.5 bg-warning z-10" />
|
||||||
<ui-library-icon v-if="!libraryScan" :icon="library.icon" :size="6" font-size="xl" class="text-white" :class="isHovering ? 'text-opacity-90' : 'text-opacity-50'" />
|
<ui-library-icon v-if="!libraryScan" :icon="library.icon" :size="6" font-size="lg md:text-xl" class="text-white" :class="isHovering ? 'text-opacity-90' : 'text-opacity-50'" />
|
||||||
<svg v-else viewBox="0 0 24 24" class="h-6 w-6 text-white text-opacity-50 animate-spin">
|
<svg v-else viewBox="0 0 24 24" class="h-6 w-6 text-white text-opacity-50 animate-spin">
|
||||||
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||||
</svg>
|
</svg>
|
||||||
<p class="text-xl font-book pl-4 hover:underline cursor-pointer" @click.stop="$emit('click', library)">{{ library.name }}</p>
|
<p class="text-base md:text-xl font-book pl-2 md:pl-4 hover:underline cursor-pointer" @click.stop="$emit('click', library)">{{ library.name }}</p>
|
||||||
|
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn v-show="isHovering && !libraryScan" class="hidden md:block" small color="success" @click.stop="scan">{{ $strings.ButtonScan }}</ui-btn>
|
|
||||||
<ui-btn v-show="isHovering && !libraryScan" small color="bg" class="ml-2 hidden md:block" @click.stop="forceScan">{{ $strings.ButtonForceReScan }}</ui-btn>
|
|
||||||
|
|
||||||
<ui-btn v-show="isHovering && !libraryScan && isBookLibrary" small color="bg" class="ml-2 hidden md:block" @click.stop="matchAll">{{ $strings.ButtonMatchBooks }}</ui-btn>
|
<!-- Desktop context menu icon -->
|
||||||
|
<ui-context-menu-dropdown v-if="!libraryScan && !isDeleting" :items="contextMenuItems" :icon-class="`text-1.5xl text-gray-${isHovering ? 50 : 400}`" class="!hidden md:!block" @action="contextMenuAction" />
|
||||||
|
|
||||||
<span v-if="isHovering && !libraryScan" class="!hidden md:!block material-icons text-xl text-gray-300 hover:text-gray-50 ml-4 cursor-pointer" @click.stop="editClick">edit</span>
|
<!-- Mobile context menu icon -->
|
||||||
<span v-if="!libraryScan && isHovering && !isDeleting" class="!hidden md:!block material-icons text-xl text-gray-300 ml-3 hover:text-gray-50 cursor-pointer" @click.stop="deleteClick">delete</span>
|
<span v-if="!libraryScan && !isDeleting" class="!block md:!hidden material-icons text-xl text-gray-300 ml-3 cursor-pointer" @click.stop="showMenu">more_vert</span>
|
||||||
|
|
||||||
<!-- For mobile -->
|
|
||||||
<span v-if="!libraryScan" class="!block md:!hidden material-icons text-xl text-gray-300 ml-4 cursor-pointer" @click.stop="editClick">edit</span>
|
|
||||||
<span v-if="!libraryScan && !isDeleting" class="!block md:!hidden material-icons text-2xl text-gray-300 ml-3 cursor-pointer" @click.stop="showMenu">more_vert</span>
|
|
||||||
<div v-show="isDeleting" class="text-xl text-gray-300 ml-3 animate-spin">
|
<div v-show="isDeleting" class="text-xl text-gray-300 ml-3 animate-spin">
|
||||||
<svg viewBox="0 0 24 24" class="w-6 h-6">
|
<svg viewBox="0 0 24 24" class="w-6 h-6">
|
||||||
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
<path fill="currentColor" d="M12,4V2A10,10 0 0,0 2,12H4A8,8 0 0,1 12,4Z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<span class="material-icons drag-handle text-xl text-gray-400 hover:text-gray-50 ml-4">reorder</span>
|
<span class="material-icons drag-handle text-xl text-gray-400 hover:text-gray-50 ml-2 md:ml-4">reorder</span>
|
||||||
|
|
||||||
<!-- For mobile -->
|
<!-- For mobile -->
|
||||||
<modals-dialog v-model="showMobileMenu" :title="menuTitle" :items="mobileMenuItems" @action="mobileMenuAction" />
|
<modals-dialog v-model="showMobileMenu" :title="menuTitle" :items="contextMenuItems" @action="contextMenuAction" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -63,34 +60,45 @@ export default {
|
|||||||
menuTitle() {
|
menuTitle() {
|
||||||
return this.library.name
|
return this.library.name
|
||||||
},
|
},
|
||||||
mobileMenuItems() {
|
contextMenuItems() {
|
||||||
const items = [
|
const items = [
|
||||||
|
{
|
||||||
|
text: this.$strings.ButtonEdit,
|
||||||
|
action: 'edit',
|
||||||
|
value: 'edit'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.ButtonScan,
|
text: this.$strings.ButtonScan,
|
||||||
|
action: 'scan',
|
||||||
value: 'scan'
|
value: 'scan'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.ButtonForceReScan,
|
text: this.$strings.ButtonForceReScan,
|
||||||
|
action: 'force-scan',
|
||||||
value: 'force-scan'
|
value: 'force-scan'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
if (this.isBookLibrary) {
|
if (this.isBookLibrary) {
|
||||||
items.push({
|
items.push({
|
||||||
text: this.$strings.ButtonMatchBooks,
|
text: this.$strings.ButtonMatchBooks,
|
||||||
|
action: 'match-books',
|
||||||
value: 'match-books'
|
value: 'match-books'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
items.push({
|
items.push({
|
||||||
text: this.$strings.ButtonDelete,
|
text: this.$strings.ButtonDelete,
|
||||||
|
action: 'delete',
|
||||||
value: 'delete'
|
value: 'delete'
|
||||||
})
|
})
|
||||||
return items
|
return items
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
mobileMenuAction(action) {
|
contextMenuAction(action) {
|
||||||
this.showMobileMenu = false
|
this.showMobileMenu = false
|
||||||
if (action === 'scan') {
|
if (action === 'edit') {
|
||||||
|
this.editClick()
|
||||||
|
} else if (action === 'scan') {
|
||||||
this.scan()
|
this.scan()
|
||||||
} else if (action === 'force-scan') {
|
} else if (action === 'force-scan') {
|
||||||
this.forceScan()
|
this.forceScan()
|
||||||
@@ -130,37 +138,52 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
forceScan() {
|
forceScan() {
|
||||||
if (confirm(this.$strings.MessageConfirmForceReScan)) {
|
const payload = {
|
||||||
this.$store
|
message: this.$strings.MessageConfirmForceReScan,
|
||||||
.dispatch('libraries/requestLibraryScan', { libraryId: this.library.id, force: 1 })
|
callback: (confirmed) => {
|
||||||
.then(() => {
|
if (confirmed) {
|
||||||
this.$toast.success(this.$strings.ToastLibraryScanStarted)
|
this.$store
|
||||||
})
|
.dispatch('libraries/requestLibraryScan', { libraryId: this.library.id, force: 1 })
|
||||||
.catch((error) => {
|
.then(() => {
|
||||||
console.error('Failed to start scan', error)
|
this.$toast.success(this.$strings.ToastLibraryScanStarted)
|
||||||
this.$toast.error(this.$strings.ToastLibraryScanFailedToStart)
|
})
|
||||||
})
|
.catch((error) => {
|
||||||
|
console.error('Failed to start scan', error)
|
||||||
|
this.$toast.error(this.$strings.ToastLibraryScanFailedToStart)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
}
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
},
|
},
|
||||||
deleteClick() {
|
deleteClick() {
|
||||||
if (confirm(this.$getString('MessageConfirmDeleteLibrary', [this.library.name]))) {
|
const payload = {
|
||||||
this.isDeleting = true
|
message: this.$getString('MessageConfirmDeleteLibrary', [this.library.name]),
|
||||||
this.$axios
|
callback: (confirmed) => {
|
||||||
.$delete(`/api/libraries/${this.library.id}`)
|
if (confirmed) {
|
||||||
.then((data) => {
|
this.isDeleting = true
|
||||||
this.isDeleting = false
|
this.$axios
|
||||||
if (data.error) {
|
.$delete(`/api/libraries/${this.library.id}`)
|
||||||
this.$toast.error(data.error)
|
.then((data) => {
|
||||||
} else {
|
if (data.error) {
|
||||||
this.$toast.success(this.$strings.ToastLibraryDeleteSuccess)
|
this.$toast.error(data.error)
|
||||||
}
|
} else {
|
||||||
})
|
this.$toast.success(this.$strings.ToastLibraryDeleteSuccess)
|
||||||
.catch((error) => {
|
}
|
||||||
console.error('Failed to delete library', error)
|
})
|
||||||
this.$toast.error(this.$strings.ToastLibraryDeleteFailed)
|
.catch((error) => {
|
||||||
this.isDeleting = false
|
console.error('Failed to delete library', error)
|
||||||
})
|
this.$toast.error(this.$strings.ToastLibraryDeleteFailed)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.isDeleting = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
}
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full py-6">
|
<div class="w-full py-6">
|
||||||
|
<p class="text-lg mb-2 font-semibold md:hidden">{{ $strings.HeaderEpisodes }}</p>
|
||||||
<div class="flex items-center mb-4">
|
<div class="flex items-center mb-4">
|
||||||
<p class="text-lg mb-0 font-semibold">{{ $strings.HeaderEpisodes }}</p>
|
<p class="text-lg mb-0 font-semibold hidden md:block">{{ $strings.HeaderEpisodes }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow hidden md:block" />
|
||||||
<template v-if="isSelectionMode">
|
<template v-if="isSelectionMode">
|
||||||
<ui-tooltip :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
|
<ui-tooltip :text="selectedIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="bottom">
|
||||||
<ui-read-icon-btn :disabled="processing" :is-read="selectedIsFinished" @click="toggleBatchFinished" class="mx-1.5" />
|
<ui-read-icon-btn :disabled="processing" :is-read="selectedIsFinished" @click="toggleBatchFinished" class="mx-1.5" />
|
||||||
@@ -11,8 +12,10 @@
|
|||||||
<ui-btn :disabled="processing" small class="ml-2 h-9" @click="clearSelected">{{ $strings.ButtonCancel }}</ui-btn>
|
<ui-btn :disabled="processing" small class="ml-2 h-9" @click="clearSelected">{{ $strings.ButtonCancel }}</ui-btn>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<controls-filter-select v-model="filterKey" :items="filterItems" class="w-32 md:w-36 h-9 ml-1 sm:ml-4" />
|
<controls-filter-select v-model="filterKey" :items="filterItems" class="w-36 h-9 sm:ml-4" />
|
||||||
<controls-sort-select v-model="sortKey" :descending.sync="sortDesc" :items="sortItems" class="w-32 sm:w-44 md:w-48 h-9 ml-1 sm:ml-4" />
|
<controls-sort-select v-model="sortKey" :descending.sync="sortDesc" :items="sortItems" class="w-44 md:w-48 h-9 ml-1 sm:ml-4" />
|
||||||
|
<div class="flex-grow md:hidden" />
|
||||||
|
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" class="ml-1" @action="contextMenuAction" />
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p>
|
<p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p>
|
||||||
@@ -42,15 +45,27 @@ export default {
|
|||||||
showPodcastRemoveModal: false,
|
showPodcastRemoveModal: false,
|
||||||
selectedEpisodes: [],
|
selectedEpisodes: [],
|
||||||
episodesToRemove: [],
|
episodesToRemove: [],
|
||||||
processing: false
|
processing: false,
|
||||||
|
quickMatchingEpisodes: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
libraryItem() {
|
libraryItem: {
|
||||||
this.init()
|
handler() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
contextMenuItems() {
|
||||||
|
if (!this.userIsAdminOrUp) return []
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: 'Quick match all episodes',
|
||||||
|
action: 'quick-match-episodes'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
sortItems() {
|
sortItems() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -94,8 +109,8 @@ export default {
|
|||||||
isSelectionMode() {
|
isSelectionMode() {
|
||||||
return this.selectedEpisodes.length > 0
|
return this.selectedEpisodes.length > 0
|
||||||
},
|
},
|
||||||
userCanUpdate() {
|
userIsAdminOrUp() {
|
||||||
return this.$store.getters['user/getUserCanUpdate']
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
},
|
},
|
||||||
media() {
|
media() {
|
||||||
return this.libraryItem.media || {}
|
return this.libraryItem.media || {}
|
||||||
@@ -131,6 +146,44 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
contextMenuAction(action) {
|
||||||
|
if (action === 'quick-match-episodes') {
|
||||||
|
if (this.quickMatchingEpisodes) return
|
||||||
|
|
||||||
|
this.quickMatchAllEpisodes()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
quickMatchAllEpisodes() {
|
||||||
|
if (!this.mediaMetadata.feedUrl) {
|
||||||
|
this.$toast.error(this.$strings.MessagePodcastHasNoRSSFeedForMatching)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.quickMatchingEpisodes = true
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
message: 'Quick matching episodes will overwrite details if a match is found. Only unmatched episodes will be updated. Are you sure?',
|
||||||
|
callback: (confirmed) => {
|
||||||
|
if (confirmed) {
|
||||||
|
this.$axios
|
||||||
|
.$post(`/api/podcasts/${this.libraryItem.id}/match-episodes?override=1`)
|
||||||
|
.then((data) => {
|
||||||
|
if (data.numEpisodesUpdated) {
|
||||||
|
this.$toast.success(`${data.numEpisodesUpdated} episodes updated`)
|
||||||
|
} else {
|
||||||
|
this.$toast.info('No changes were made')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to request match episodes', error)
|
||||||
|
this.$toast.error('Failed to match episodes')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
this.quickMatchingEpisodes = false
|
||||||
|
},
|
||||||
|
type: 'yesNo'
|
||||||
|
}
|
||||||
|
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||||
|
},
|
||||||
addToPlaylist(episode) {
|
addToPlaylist(episode) {
|
||||||
this.$store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode }])
|
this.$store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: this.libraryItem, episode }])
|
||||||
this.$store.commit('globals/setShowPlaylistsModal', true)
|
this.$store.commit('globals/setShowPlaylistsModal', true)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
|
<div class="relative h-9 w-9" v-click-outside="clickOutsideObj">
|
||||||
<button type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
<button type="button" :disabled="disabled" class="relative h-full w-full flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
||||||
<span class="material-icons">more_vert</span>
|
<span class="material-icons" :class="iconClass">more_vert</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<transition name="menu">
|
<transition name="menu">
|
||||||
@@ -23,6 +23,10 @@ export default {
|
|||||||
items: {
|
items: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
|
},
|
||||||
|
iconClass: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative w-full" v-click-outside="clickOutsideObj">
|
<div class="relative w-full" v-click-outside="clickOutsideObj">
|
||||||
<p class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
|
<p class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
|
||||||
<button type="button" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left focus:outline-none sm:text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
<button type="button" :aria-label="longLabel" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left sm:text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<span class="block truncate font-sans" :class="{ 'font-semibold': selectedSubtext, 'text-sm': small }">{{ selectedText }}</span>
|
<span class="block truncate font-sans" :class="{ 'font-semibold': selectedSubtext, 'text-sm': small }">{{ selectedText }}</span>
|
||||||
<span v-if="selectedSubtext">: </span>
|
<span v-if="selectedSubtext">: </span>
|
||||||
@@ -13,9 +13,9 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<transition name="menu">
|
<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 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox">
|
<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 ring-1 ring-black ring-opacity-5 overflow-auto sm:text-sm" tabindex="-1" role="listbox">
|
||||||
<template v-for="item in itemsToShow">
|
<template v-for="item in itemsToShow">
|
||||||
<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)">
|
<li :key="item.value" class="text-gray-100 relative py-2 cursor-pointer hover:bg-black-400" :id="'listbox-option-' + item.value" role="option" tabindex="0" @keyup.enter="clickedOption(item.value)" @click="clickedOption(item.value)">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<span class="ml-3 block truncate font-sans text-sm" :class="{ 'font-semibold': item.subtext }">{{ item.text }}</span>
|
<span class="ml-3 block truncate font-sans text-sm" :class="{ 'font-semibold': item.subtext }">{{ item.text }}</span>
|
||||||
<span v-if="item.subtext">: </span>
|
<span v-if="item.subtext">: </span>
|
||||||
@@ -91,6 +91,13 @@ export default {
|
|||||||
else classes.push('cursor-pointer border-gray-600 bg-primary text-gray-100')
|
else classes.push('cursor-pointer border-gray-600 bg-primary text-gray-100')
|
||||||
|
|
||||||
return classes.join(' ')
|
return classes.join(' ')
|
||||||
|
},
|
||||||
|
longLabel() {
|
||||||
|
let result = ''
|
||||||
|
if (this.label) result += this.label + ': '
|
||||||
|
if (this.selectedText) result += this.selectedText
|
||||||
|
if (this.selectedSubtext) result += ' ' + this.selectedSubtext
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<p class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</p>
|
<label class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</label>
|
||||||
<div ref="wrapper" class="relative">
|
<div ref="wrapper" class="relative">
|
||||||
<form @submit.prevent="submitForm">
|
<form @submit.prevent="submitForm">
|
||||||
<div ref="inputWrapper" class="input-wrapper flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-2" :class="disabled ? 'pointer-events-none bg-black-300 text-gray-400' : 'bg-primary'">
|
<div ref="inputWrapper" class="input-wrapper flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-2" :class="disabled ? 'pointer-events-none bg-black-300 text-gray-400' : 'bg-primary'">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="currentLibrary" class="relative h-8 max-w-52 md:min-w-32" v-click-outside="clickOutsideObj">
|
<div v-if="currentLibrary" class="relative h-8 max-w-52 md:min-w-32" v-click-outside="clickOutsideObj">
|
||||||
<button type="button" :disabled="disabled" class="w-10 sm:w-full relative h-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm px-2 text-left text-sm focus:outline-none cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200" aria-haspopup="listbox" :aria-expanded="showMenu" @click.stop.prevent="clickShowMenu">
|
<button type="button" :disabled="disabled" class="w-10 sm:w-full relative h-full border border-white border-opacity-10 hover:border-opacity-20 rounded shadow-sm px-2 text-left text-sm cursor-pointer bg-black bg-opacity-20 text-gray-400 hover:text-gray-200" aria-haspopup="listbox" :aria-expanded="showMenu" :aria-label="$strings.ButtonLibrary + ': ' + currentLibrary.name" @click.stop.prevent="clickShowMenu">
|
||||||
<div class="flex items-center justify-center sm:justify-start">
|
<div class="flex items-center justify-center sm:justify-start">
|
||||||
<ui-library-icon :icon="currentLibraryIcon" class="sm:mr-1.5" />
|
<ui-library-icon :icon="currentLibraryIcon" class="sm:mr-1.5" />
|
||||||
<span class="hidden sm:block truncate">{{ currentLibrary.name }}</span>
|
<span class="hidden sm:block truncate">{{ currentLibrary.name }}</span>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<transition name="menu">
|
<transition name="menu">
|
||||||
<ul v-show="showMenu" class="absolute z-10 -mt-px min-w-48 w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox">
|
<ul v-show="showMenu" class="absolute z-10 -mt-px min-w-48 w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox">
|
||||||
<template v-for="library in librariesFiltered">
|
<template v-for="library in librariesFiltered">
|
||||||
<li :key="library.id" class="text-gray-400 hover:text-white select-none relative py-2 cursor-pointer hover:bg-black-400" role="option" @click="selectLibrary(library)">
|
<li :key="library.id" class="text-gray-400 hover:text-white relative py-2 cursor-pointer hover:bg-black-400" role="option" tabindex="0" @keydown.enter="selectLibrary(library)" @click="selectLibrary(library)">
|
||||||
<div class="flex items-center px-2">
|
<div class="flex items-center px-2">
|
||||||
<ui-library-icon :icon="library.icon" class="mr-1.5" />
|
<ui-library-icon :icon="library.icon" class="mr-1.5" />
|
||||||
<span class="font-normal block truncate font-sans text-sm">{{ library.name }}</span>
|
<span class="font-normal block truncate font-sans text-sm">{{ library.name }}</span>
|
||||||
|
|||||||
@@ -204,15 +204,21 @@ export default {
|
|||||||
}
|
}
|
||||||
if (this.$refs.input) this.$refs.input.focus()
|
if (this.$refs.input) this.$refs.input.focus()
|
||||||
|
|
||||||
var newSelected = null
|
let newSelected = null
|
||||||
if (this.getIsSelected(item.id)) {
|
if (this.getIsSelected(item.id)) {
|
||||||
newSelected = this.selected.filter((s) => s.id !== item.id)
|
newSelected = this.selected.filter((s) => s.id !== item.id)
|
||||||
this.$emit('removedItem', item.id)
|
this.$emit('removedItem', item.id)
|
||||||
} else {
|
} else {
|
||||||
newSelected = this.selected.concat([item])
|
newSelected = this.selected.concat([
|
||||||
|
{
|
||||||
|
id: item.id,
|
||||||
|
name: item.name
|
||||||
|
}
|
||||||
|
])
|
||||||
}
|
}
|
||||||
this.textInput = null
|
this.textInput = null
|
||||||
this.currentSearch = null
|
this.currentSearch = null
|
||||||
|
|
||||||
this.$emit('input', newSelected)
|
this.$emit('input', newSelected)
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.recalcMenuPos()
|
this.recalcMenuPos()
|
||||||
@@ -246,10 +252,11 @@ export default {
|
|||||||
submitForm() {
|
submitForm() {
|
||||||
if (!this.textInput) return
|
if (!this.textInput) return
|
||||||
|
|
||||||
var cleaned = this.textInput.trim()
|
const cleaned = this.textInput.trim()
|
||||||
var matchesItem = this.items.find((i) => {
|
const matchesItem = this.items.find((i) => {
|
||||||
return i === cleaned
|
return i.name === cleaned
|
||||||
})
|
})
|
||||||
|
|
||||||
if (matchesItem) {
|
if (matchesItem) {
|
||||||
this.clickedOption(null, matchesItem)
|
this.clickedOption(null, matchesItem)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="wrapper" class="relative">
|
<div ref="wrapper" class="relative">
|
||||||
<input ref="input" v-model="inputValue" :type="actualType" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="rounded bg-primary text-gray-200 focus:border-gray-300 focus:bg-bg focus:outline-none border border-gray-600 h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
|
<input :id="inputId" ref="input" v-model="inputValue" :type="actualType" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="rounded bg-primary text-gray-200 focus:border-gray-300 focus:bg-bg focus:outline-none border border-gray-600 h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
|
||||||
<div v-if="clearable && inputValue" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center">
|
<div v-if="clearable && inputValue" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center">
|
||||||
<span class="material-icons text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span>
|
<span class="material-icons text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,7 +31,8 @@ export default {
|
|||||||
},
|
},
|
||||||
noSpinner: Boolean,
|
noSpinner: Boolean,
|
||||||
textCenter: Boolean,
|
textCenter: Boolean,
|
||||||
clearable: Boolean
|
clearable: Boolean,
|
||||||
|
inputId: String
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<slot>
|
<slot>
|
||||||
<p class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }">
|
<label :for="identifier" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }"
|
||||||
{{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
|
>{{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em></label
|
||||||
</p>
|
>
|
||||||
</slot>
|
</slot>
|
||||||
<ui-text-input ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" :class="inputClass" @blur="inputBlurred" />
|
<ui-text-input :placeholder="label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" :class="inputClass" @blur="inputBlurred" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -34,6 +34,9 @@ export default {
|
|||||||
set(val) {
|
set(val) {
|
||||||
this.$emit('input', val)
|
this.$emit('input', val)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
identifier() {
|
||||||
|
return Math.random().toString(36).substring(2)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="border rounded-full border-black-100 flex items-center cursor-pointer w-10 justify-start" :class="className" @click="clickToggle">
|
<button :aria-labelledby="labeledBy" role="checkbox" type="button" class="border rounded-full border-black-100 flex items-center cursor-pointer w-10 justify-start" :aria-checked="toggleValue" :class="className" @click="clickToggle">
|
||||||
<span class="rounded-full border w-5 h-5 border-black-50 shadow transform transition-transform duration-100" :class="switchClassName"></span>
|
<span class="rounded-full border w-5 h-5 border-black-50 shadow transform transition-transform duration-100" :class="switchClassName"></span>
|
||||||
</div>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -18,7 +18,8 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'primary'
|
default: 'primary'
|
||||||
},
|
},
|
||||||
disabled: Boolean
|
disabled: Boolean,
|
||||||
|
labeledBy: String
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
toggleValue: {
|
toggleValue: {
|
||||||
|
|||||||
@@ -210,11 +210,13 @@ export default {
|
|||||||
// array of objects with id key
|
// array of objects with id key
|
||||||
if (array1.length !== array2.length) return false
|
if (array1.length !== array2.length) return false
|
||||||
|
|
||||||
for (var item of array1) {
|
for (let i = 0; i < array1.length; i++) {
|
||||||
var matchingItem = array2.find((a) => a.id === item.id)
|
const item1 = array1[i]
|
||||||
if (!matchingItem) return false
|
const item2 = array2[i]
|
||||||
for (var key in item) {
|
if (!item1 || !item2) return false
|
||||||
if (item[key] !== matchingItem[key]) {
|
|
||||||
|
for (const key in item1) {
|
||||||
|
if (item1[key] !== item2[key]) {
|
||||||
// console.log('Object array item keys changed', key, item[key], matchingItem[key])
|
// console.log('Object array item keys changed', key, item[key], matchingItem[key])
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,31 +137,31 @@ export default {
|
|||||||
weekdays() {
|
weekdays() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
text: this.$strings.WeekdaySunday,
|
text: this.$formatJsDate(new Date(2023, 0, 1), 'EEEE'),
|
||||||
value: 0
|
value: 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.WeekdayMonday,
|
text: this.$formatJsDate(new Date(2023, 0, 2), 'EEEE'),
|
||||||
value: 1
|
value: 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.WeekdayTuesday,
|
text: this.$formatJsDate(new Date(2023, 0, 3), 'EEEE'),
|
||||||
value: 2
|
value: 2
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.WeekdayWednesday,
|
text: this.$formatJsDate(new Date(2023, 0, 4), 'EEEE'),
|
||||||
value: 3
|
value: 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.WeekdayThursday,
|
text: this.$formatJsDate(new Date(2023, 0, 5), 'EEEE'),
|
||||||
value: 4
|
value: 4
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.WeekdayFriday,
|
text: this.$formatJsDate(new Date(2023, 0, 6), 'EEEE'),
|
||||||
value: 5
|
value: 5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: this.$strings.WeekdaySaturday,
|
text: this.$formatJsDate(new Date(2023, 0, 7), 'EEEE'),
|
||||||
value: 6
|
value: 6
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ export default {
|
|||||||
..._series
|
..._series
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Selected series', this.selectedSeries)
|
|
||||||
this.showSeriesForm = true
|
this.showSeriesForm = true
|
||||||
},
|
},
|
||||||
addNewSeries() {
|
addNewSeries() {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
<modals-podcast-view-episode />
|
<modals-podcast-view-episode />
|
||||||
<modals-authors-edit-modal />
|
<modals-authors-edit-modal />
|
||||||
<modals-batch-quick-match-model />
|
<modals-batch-quick-match-model />
|
||||||
|
<modals-rssfeed-open-close-modal />
|
||||||
<prompt-confirm />
|
<prompt-confirm />
|
||||||
<readers-reader />
|
<readers-reader />
|
||||||
</div>
|
</div>
|
||||||
@@ -329,12 +330,6 @@ export default {
|
|||||||
}
|
}
|
||||||
this.$store.commit('libraries/removeUserPlaylist', playlist)
|
this.$store.commit('libraries/removeUserPlaylist', playlist)
|
||||||
},
|
},
|
||||||
rssFeedOpen(data) {
|
|
||||||
this.$store.commit('feeds/addFeed', data)
|
|
||||||
},
|
|
||||||
rssFeedClosed(data) {
|
|
||||||
this.$store.commit('feeds/removeFeed', data)
|
|
||||||
},
|
|
||||||
backupApplied() {
|
backupApplied() {
|
||||||
// Force refresh
|
// Force refresh
|
||||||
location.reload()
|
location.reload()
|
||||||
@@ -424,10 +419,6 @@ export default {
|
|||||||
this.socket.on('task_started', this.taskStarted)
|
this.socket.on('task_started', this.taskStarted)
|
||||||
this.socket.on('task_finished', this.taskFinished)
|
this.socket.on('task_finished', this.taskFinished)
|
||||||
|
|
||||||
// Feed Listeners
|
|
||||||
this.socket.on('rss_feed_open', this.rssFeedOpen)
|
|
||||||
this.socket.on('rss_feed_closed', this.rssFeedClosed)
|
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import LazyBookCard from '@/components/cards/LazyBookCard'
|
|||||||
import LazySeriesCard from '@/components/cards/LazySeriesCard'
|
import LazySeriesCard from '@/components/cards/LazySeriesCard'
|
||||||
import LazyCollectionCard from '@/components/cards/LazyCollectionCard'
|
import LazyCollectionCard from '@/components/cards/LazyCollectionCard'
|
||||||
import LazyPlaylistCard from '@/components/cards/LazyPlaylistCard'
|
import LazyPlaylistCard from '@/components/cards/LazyPlaylistCard'
|
||||||
|
import LazyAlbumCard from '@/components/cards/LazyAlbumCard'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
@@ -17,6 +18,7 @@ export default {
|
|||||||
if (this.entityName === 'series') return Vue.extend(LazySeriesCard)
|
if (this.entityName === 'series') return Vue.extend(LazySeriesCard)
|
||||||
if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard)
|
if (this.entityName === 'collections') return Vue.extend(LazyCollectionCard)
|
||||||
if (this.entityName === 'playlists') return Vue.extend(LazyPlaylistCard)
|
if (this.entityName === 'playlists') return Vue.extend(LazyPlaylistCard)
|
||||||
|
if (this.entityName === 'albums') return Vue.extend(LazyAlbumCard)
|
||||||
return Vue.extend(LazyBookCard)
|
return Vue.extend(LazyBookCard)
|
||||||
},
|
},
|
||||||
async mountEntityCard(index) {
|
async mountEntityCard(index) {
|
||||||
@@ -28,7 +30,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.entityIndexesMounted.push(index)
|
this.entityIndexesMounted.push(index)
|
||||||
if (this.entityComponentRefs[index]) {
|
if (this.entityComponentRefs[index]) {
|
||||||
var bookComponent = this.entityComponentRefs[index]
|
const bookComponent = this.entityComponentRefs[index]
|
||||||
shelfEl.appendChild(bookComponent.$el)
|
shelfEl.appendChild(bookComponent.$el)
|
||||||
if (this.isSelectionMode) {
|
if (this.isSelectionMode) {
|
||||||
bookComponent.setSelectionMode(true)
|
bookComponent.setSelectionMode(true)
|
||||||
@@ -43,13 +45,13 @@ export default {
|
|||||||
bookComponent.isHovering = false
|
bookComponent.isHovering = false
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var shelfOffsetY = 16
|
const shelfOffsetY = 16
|
||||||
var row = index % this.entitiesPerShelf
|
const row = index % this.entitiesPerShelf
|
||||||
var shelfOffsetX = row * this.totalEntityCardWidth + this.bookshelfMarginLeft
|
const shelfOffsetX = row * this.totalEntityCardWidth + this.bookshelfMarginLeft
|
||||||
|
|
||||||
var ComponentClass = this.getComponentClass()
|
const ComponentClass = this.getComponentClass()
|
||||||
|
|
||||||
var props = {
|
const props = {
|
||||||
index,
|
index,
|
||||||
width: this.entityWidth,
|
width: this.entityWidth,
|
||||||
height: this.entityHeight,
|
height: this.entityHeight,
|
||||||
@@ -58,15 +60,15 @@ export default {
|
|||||||
sortingIgnorePrefix: !!this.sortingIgnorePrefix
|
sortingIgnorePrefix: !!this.sortingIgnorePrefix
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.entityName === 'books') {
|
if (this.entityName === 'items') {
|
||||||
props.filterBy = this.filterBy
|
props.filterBy = this.filterBy
|
||||||
props.orderBy = this.orderBy
|
props.orderBy = this.orderBy
|
||||||
} else if (this.entityName === 'series') {
|
} else if (this.entityName === 'series') {
|
||||||
props.orderBy = this.seriesSortBy
|
props.orderBy = this.seriesSortBy
|
||||||
}
|
}
|
||||||
|
|
||||||
var _this = this
|
const _this = this
|
||||||
var instance = new ComponentClass({
|
const instance = new ComponentClass({
|
||||||
propsData: props,
|
propsData: props,
|
||||||
created() {
|
created() {
|
||||||
this.$on('edit', (entity) => {
|
this.$on('edit', (entity) => {
|
||||||
|
|||||||
@@ -114,6 +114,11 @@ module.exports = {
|
|||||||
{
|
{
|
||||||
src: (process.env.ROUTER_BASE_PATH || '') + '/icon.svg',
|
src: (process.env.ROUTER_BASE_PATH || '') + '/icon.svg',
|
||||||
sizes: "any"
|
sizes: "any"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: (process.env.ROUTER_BASE_PATH || '') + '/icon64.png',
|
||||||
|
type: "image/png",
|
||||||
|
sizes: "64x64"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.11",
|
"version": "2.2.13",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.11",
|
"version": "2.2.13",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.2.11",
|
"version": "2.2.13",
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -63,6 +63,10 @@
|
|||||||
|
|
||||||
<div class="w-full max-w-4xl mx-auto">
|
<div class="w-full max-w-4xl mx-auto">
|
||||||
<div v-if="isEmbedTool" class="w-full flex justify-end items-center mb-4">
|
<div v-if="isEmbedTool" class="w-full flex justify-end items-center mb-4">
|
||||||
|
<ui-checkbox v-if="!isFinished" v-model="shouldBackupAudioFiles" label="Backup audio files" medium checkbox-bg="bg" label-class="pl-2 text-base md:text-lg" @input="toggleBackupAudioFiles" />
|
||||||
|
|
||||||
|
<div class="flex-grow" />
|
||||||
|
|
||||||
<ui-btn v-if="!isFinished" color="primary" :loading="processing" @click.stop="embedClick">{{ $strings.ButtonStartMetadataEmbed }}</ui-btn>
|
<ui-btn v-if="!isFinished" color="primary" :loading="processing" @click.stop="embedClick">{{ $strings.ButtonStartMetadataEmbed }}</ui-btn>
|
||||||
<p v-else class="text-success text-lg font-semibold">{{ $strings.MessageEmbedFinished }}</p>
|
<p v-else class="text-success text-lg font-semibold">{{ $strings.MessageEmbedFinished }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,7 +108,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-start mb-2">
|
<div v-if="shouldBackupAudioFiles || isM4BTool" class="flex items-start mb-2">
|
||||||
<span class="material-icons text-base text-warning pt-1">star</span>
|
<span class="material-icons text-base text-warning pt-1">star</span>
|
||||||
<p class="text-gray-200 ml-2">
|
<p class="text-gray-200 ml-2">
|
||||||
A backup of your original audio files will be stored in <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">/metadata/cache/items/{{ libraryItemId }}/</span>. Make sure to periodically purge items cache.
|
A backup of your original audio files will be stored in <span class="rounded-md bg-neutral-600 text-sm text-white py-0.5 px-1 font-mono">/metadata/cache/items/{{ libraryItemId }}/</span>. Make sure to periodically purge items cache.
|
||||||
@@ -171,7 +175,7 @@ export default {
|
|||||||
if (!store.getters['user/getIsAdminOrUp']) {
|
if (!store.getters['user/getIsAdminOrUp']) {
|
||||||
return redirect('/?error=unauthorized')
|
return redirect('/?error=unauthorized')
|
||||||
}
|
}
|
||||||
var libraryItem = await app.$axios.$get(`/api/items/${params.id}?expanded=1`).catch((error) => {
|
const libraryItem = await app.$axios.$get(`/api/items/${params.id}?expanded=1`).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@@ -201,6 +205,7 @@ export default {
|
|||||||
selectedTool: 'embed',
|
selectedTool: 'embed',
|
||||||
isCancelingEncode: false,
|
isCancelingEncode: false,
|
||||||
showEncodeOptions: false,
|
showEncodeOptions: false,
|
||||||
|
shouldBackupAudioFiles: true,
|
||||||
encodingOptions: {
|
encodingOptions: {
|
||||||
bitrate: '64k',
|
bitrate: '64k',
|
||||||
channels: '2',
|
channels: '2',
|
||||||
@@ -275,6 +280,9 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
toggleBackupAudioFiles(val) {
|
||||||
|
localStorage.setItem('embedMetadataShouldBackup', val ? 1 : 0)
|
||||||
|
},
|
||||||
cancelEncodeClick() {
|
cancelEncodeClick() {
|
||||||
this.isCancelingEncode = true
|
this.isCancelingEncode = true
|
||||||
this.$axios
|
this.$axios
|
||||||
@@ -332,7 +340,7 @@ export default {
|
|||||||
updateAudioFileMetadata() {
|
updateAudioFileMetadata() {
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.$axios
|
this.$axios
|
||||||
.$post(`/api/tools/item/${this.libraryItemId}/embed-metadata?tone=1`)
|
.$post(`/api/tools/item/${this.libraryItemId}/embed-metadata?backup=${this.shouldBackupAudioFiles ? 1 : 0}`)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log('Audio metadata encode started')
|
console.log('Audio metadata encode started')
|
||||||
})
|
})
|
||||||
@@ -350,9 +358,14 @@ export default {
|
|||||||
console.log('audio metadata finished', data)
|
console.log('audio metadata finished', data)
|
||||||
if (data.libraryItemId !== this.libraryItemId) return
|
if (data.libraryItemId !== this.libraryItemId) return
|
||||||
this.processing = false
|
this.processing = false
|
||||||
this.isFinished = true
|
|
||||||
this.audiofilesEncoding = {}
|
this.audiofilesEncoding = {}
|
||||||
this.$toast.success('Audio file metadata updated')
|
|
||||||
|
if (data.failed) {
|
||||||
|
this.$toast.error(data.error)
|
||||||
|
} else {
|
||||||
|
this.isFinished = true
|
||||||
|
this.$toast.success('Audio file metadata updated')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
audiofileMetadataStarted(data) {
|
audiofileMetadataStarted(data) {
|
||||||
if (data.libraryItemId !== this.libraryItemId) return
|
if (data.libraryItemId !== this.libraryItemId) return
|
||||||
@@ -378,6 +391,9 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.task) this.taskUpdated(this.task)
|
if (this.task) this.taskUpdated(this.task)
|
||||||
|
|
||||||
|
const shouldBackupAudioFiles = localStorage.getItem('embedMetadataShouldBackup')
|
||||||
|
this.shouldBackupAudioFiles = shouldBackupAudioFiles != 0
|
||||||
},
|
},
|
||||||
fetchToneObject() {
|
fetchToneObject() {
|
||||||
this.$axios
|
this.$axios
|
||||||
|
|||||||
@@ -19,6 +19,11 @@
|
|||||||
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
{{ streaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
||||||
|
<!-- RSS feed -->
|
||||||
|
<ui-tooltip v-if="rssFeed" :text="$strings.LabelOpenRSSFeed" direction="top">
|
||||||
|
<ui-icon-btn icon="rss_feed" class="mx-0.5" :bg-color="rssFeed ? 'success' : 'primary'" outlined @click="showRSSFeedModal" />
|
||||||
|
</ui-tooltip>
|
||||||
|
|
||||||
<button type="button" class="h-9 w-9 flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5 mx-px" @click.stop.prevent="editClick">
|
<button type="button" class="h-9 w-9 flex items-center justify-center shadow-sm pl-3 pr-3 text-left focus:outline-none cursor-pointer text-gray-100 hover:text-gray-200 rounded-full hover:bg-white/5 mx-px" @click.stop.prevent="editClick">
|
||||||
<span class="material-icons text-xl">edit</span>
|
<span class="material-icons text-xl">edit</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -46,7 +51,7 @@ export default {
|
|||||||
if (!store.state.user.user) {
|
if (!store.state.user.user) {
|
||||||
return redirect(`/login?redirect=${route.path}`)
|
return redirect(`/login?redirect=${route.path}`)
|
||||||
}
|
}
|
||||||
var collection = await app.$axios.$get(`/api/collections/${params.id}`).catch((error) => {
|
const collection = await app.$axios.$get(`/api/collections/${params.id}?include=rssfeed`).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
@@ -61,7 +66,8 @@ export default {
|
|||||||
|
|
||||||
store.commit('libraries/addUpdateCollection', collection)
|
store.commit('libraries/addUpdateCollection', collection)
|
||||||
return {
|
return {
|
||||||
collectionId: collection.id
|
collectionId: collection.id,
|
||||||
|
rssFeed: collection.rssFeed || null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -99,6 +105,9 @@ export default {
|
|||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
return this.playableBooks.length
|
return this.playableBooks.length
|
||||||
},
|
},
|
||||||
|
userIsAdminOrUp() {
|
||||||
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
|
},
|
||||||
userCanUpdate() {
|
userCanUpdate() {
|
||||||
return this.$store.getters['user/getUserCanUpdate']
|
return this.$store.getters['user/getUserCanUpdate']
|
||||||
},
|
},
|
||||||
@@ -112,6 +121,12 @@ export default {
|
|||||||
action: 'create-playlist'
|
action: 'create-playlist'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
if (this.userIsAdminOrUp || this.rssFeed) {
|
||||||
|
items.push({
|
||||||
|
text: this.$strings.LabelOpenRSSFeed,
|
||||||
|
action: 'open-rss-feed'
|
||||||
|
})
|
||||||
|
}
|
||||||
if (this.userCanDelete) {
|
if (this.userCanDelete) {
|
||||||
items.push({
|
items.push({
|
||||||
text: this.$strings.ButtonDelete,
|
text: this.$strings.ButtonDelete,
|
||||||
@@ -122,11 +137,21 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
showRSSFeedModal() {
|
||||||
|
this.$store.commit('globals/setRSSFeedOpenCloseModal', {
|
||||||
|
id: this.collectionId,
|
||||||
|
name: this.collectionName,
|
||||||
|
type: 'collection',
|
||||||
|
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') {
|
||||||
this.createPlaylistFromCollection()
|
this.createPlaylistFromCollection()
|
||||||
|
} else if (action === 'open-rss-feed') {
|
||||||
|
this.showRSSFeedModal()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createPlaylistFromCollection() {
|
createPlaylistFromCollection() {
|
||||||
@@ -206,9 +231,27 @@ export default {
|
|||||||
queueItems
|
queueItems
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
rssFeedOpen(data) {
|
||||||
|
if (data.entityId === this.collectionId) {
|
||||||
|
console.log('RSS Feed Opened', data)
|
||||||
|
this.rssFeed = data
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rssFeedClosed(data) {
|
||||||
|
if (data.entityId === this.collectionId) {
|
||||||
|
console.log('RSS Feed Closed', data)
|
||||||
|
this.rssFeed = null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {},
|
mounted() {
|
||||||
beforeDestroy() {}
|
this.$root.socket.on('rss_feed_open', this.rssFeedOpen)
|
||||||
|
this.$root.socket.on('rss_feed_closed', this.rssFeedClosed)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.$root.socket.off('rss_feed_open', this.rssFeedOpen)
|
||||||
|
this.$root.socket.off('rss_feed_closed', this.rssFeedClosed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
<div id="page-wrapper" class="page p-2 md:p-6 overflow-y-auto relative" :class="streamLibraryItem ? 'streaming' : ''">
|
<div id="page-wrapper" class="page p-2 md:p-6 overflow-y-auto relative" :class="streamLibraryItem ? 'streaming' : ''">
|
||||||
<app-config-side-nav :is-open.sync="sideDrawerOpen" />
|
<app-config-side-nav :is-open.sync="sideDrawerOpen" />
|
||||||
<div class="configContent" :class="`page-${currentPage}`">
|
<div class="configContent" :class="`page-${currentPage}`">
|
||||||
<div v-show="isMobile" class="w-full pb-4 px-2 flex border-b border-white border-opacity-10 mb-2">
|
<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" @click.stop.prevent="showMore">more_vert</span>
|
<span class="material-icons text-2xl cursor-pointer">arrow_forward</span>
|
||||||
<p class="pl-3 capitalize">{{ currentPage }}</p>
|
<p class="pl-3 capitalize">{{ $strings.HeaderSettings }}</p>
|
||||||
</div>
|
</div>
|
||||||
<nuxt-child />
|
<nuxt-child />
|
||||||
</div>
|
</div>
|
||||||
@@ -35,8 +35,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isMobile() {
|
isMobilePortrait() {
|
||||||
return this.$store.state.globals.isMobile
|
return this.$store.state.globals.isMobilePortrait
|
||||||
},
|
},
|
||||||
streamLibraryItem() {
|
streamLibraryItem() {
|
||||||
return this.$store.state.streamLibraryItem
|
return this.$store.state.streamLibraryItem
|
||||||
@@ -60,8 +60,8 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showMore() {
|
toggleShowMore() {
|
||||||
this.sideDrawerOpen = true
|
this.sideDrawerOpen = !this.sideDrawerOpen
|
||||||
},
|
},
|
||||||
setDeveloperMode() {
|
setDeveloperMode() {
|
||||||
var value = !this.$store.state.developerMode
|
var value = !this.$store.state.developerMode
|
||||||
|
|||||||
@@ -33,6 +33,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<tables-backups-table />
|
<tables-backups-table />
|
||||||
|
|
||||||
|
<modals-backup-schedule-modal v-model="showCronBuilder" :cron-expression.sync="cronExpression" />
|
||||||
</app-settings-content>
|
</app-settings-content>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -7,30 +7,30 @@
|
|||||||
<h2 class="font-semibold">{{ $strings.HeaderSettingsGeneral }}</h2>
|
<h2 class="font-semibold">{{ $strings.HeaderSettingsGeneral }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-end py-2">
|
<div class="flex items-end py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.storeCoverWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithItem', val)" />
|
<ui-toggle-switch labeledBy="settings-store-cover-with-items" v-model="newServerSettings.storeCoverWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeCoverWithItem', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsStoreCoversWithItemHelp">
|
<ui-tooltip :text="$strings.LabelSettingsStoreCoversWithItemHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsStoreCoversWithItem }}
|
<span id="settings-store-cover-with-items">{{ $strings.LabelSettingsStoreCoversWithItem }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.storeMetadataWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeMetadataWithItem', val)" />
|
<ui-toggle-switch labeledBy="settings-store-metadata-with-items" v-model="newServerSettings.storeMetadataWithItem" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('storeMetadataWithItem', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsStoreMetadataWithItemHelp">
|
<ui-tooltip :text="$strings.LabelSettingsStoreMetadataWithItemHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsStoreMetadataWithItem }}
|
<span id="settings-store-metadata-with-items">{{ $strings.LabelSettingsStoreMetadataWithItem }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" />
|
<ui-toggle-switch labeledBy="settings-sorting-ignore-prefixes" v-model="newServerSettings.sortingIgnorePrefix" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('sortingIgnorePrefix', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsSortingIgnorePrefixesHelp">
|
<ui-tooltip :text="$strings.LabelSettingsSortingIgnorePrefixesHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsSortingIgnorePrefixes }}
|
<span id="settings-sorting-ignore-prefixes">{{ $strings.LabelSettingsSortingIgnorePrefixes }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@@ -40,8 +40,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch 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">{{ $strings.LabelSettingsChromecastSupport }}</p>
|
<p class="pl-4" id="settings-chromecast-support">{{ $strings.LabelSettingsChromecastSupport }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="pt-4">
|
<div class="pt-4">
|
||||||
@@ -49,33 +49,31 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="homepageUseBookshelfView" :disabled="updatingServerSettings" @input="updateHomeUseBookshelfView" />
|
<ui-toggle-switch labeledBy="settings-home-page-uses-bookshelf" v-model="homepageUseBookshelfView" :disabled="updatingServerSettings" @input="updateHomeUseBookshelfView" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsBookshelfViewHelp">
|
<ui-tooltip :text="$strings.LabelSettingsBookshelfViewHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsHomePageBookshelfView }}
|
<span id="settings-home-page-uses-bookshelf">{{ $strings.LabelSettingsHomePageBookshelfView }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="useBookshelfView" :disabled="updatingServerSettings" @input="updateUseBookshelfView" />
|
<ui-toggle-switch labeledBy="settings-library-uses-bookshelf" v-model="useBookshelfView" :disabled="updatingServerSettings" @input="updateUseBookshelfView" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsBookshelfViewHelp">
|
<ui-tooltip :text="$strings.LabelSettingsBookshelfViewHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsLibraryBookshelfView }}
|
<span id="settings-library-uses-bookshelf">{{ $strings.LabelSettingsLibraryBookshelfView }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<p class="px-1 text-sm font-semibold">{{ $strings.LabelSettingsDateFormat }}</p>
|
<ui-dropdown :label="$strings.LabelSettingsDateFormat" v-model="newServerSettings.dateFormat" :items="dateFormats" small class="max-w-52" @input="(val) => updateSettingsKey('dateFormat', val)" />
|
||||||
<ui-dropdown v-model="newServerSettings.dateFormat" :items="dateFormats" small class="max-w-52" @input="(val) => updateSettingsKey('dateFormat', val)" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="py-2">
|
<div class="py-2">
|
||||||
<p class="px-1 text-sm font-semibold">{{ $strings.LabelLanguageDefaultServer }}</p>
|
<ui-dropdown :label="$strings.LabelLanguageDefaultServer" ref="langDropdown" v-model="newServerSettings.language" :items="$languageCodeOptions" small class="max-w-52" @input="updateServerLanguage" />
|
||||||
<ui-dropdown ref="langDropdown" v-model="newServerSettings.language" :items="$languageCodeOptions" small class="max-w-52" @input="updateServerLanguage" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -85,20 +83,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerParseSubtitle" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerParseSubtitle', val)" />
|
<ui-toggle-switch labeledBy="settings-parse-subtitles" v-model="newServerSettings.scannerParseSubtitle" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerParseSubtitle', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsParseSubtitlesHelp">
|
<ui-tooltip :text="$strings.LabelSettingsParseSubtitlesHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsParseSubtitles }}
|
<span id="settings-parse-subtitles">{{ $strings.LabelSettingsParseSubtitles }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerFindCovers" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerFindCovers', val)" />
|
<ui-toggle-switch labeledBy="settings-find-covers" v-model="newServerSettings.scannerFindCovers" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerFindCovers', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsFindCoversHelp">
|
<ui-tooltip :text="$strings.LabelSettingsFindCoversHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsFindCovers }}
|
<span id="settings-find-covers">{{ $strings.LabelSettingsFindCovers }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@@ -109,50 +107,50 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerPreferOverdriveMediaMarker" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOverdriveMediaMarker', val)" />
|
<ui-toggle-switch labeledBy="settings-overdrive-media-markers" v-model="newServerSettings.scannerPreferOverdriveMediaMarker" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOverdriveMediaMarker', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsOverdriveMediaMarkersHelp">
|
<ui-tooltip :text="$strings.LabelSettingsOverdriveMediaMarkersHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsOverdriveMediaMarkers }}
|
<span id="settings-overdrive-media-markers">{{ $strings.LabelSettingsOverdriveMediaMarkers }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerPreferAudioMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferAudioMetadata', val)" />
|
<ui-toggle-switch labeledBy="settings-prefer-audio-metadata" v-model="newServerSettings.scannerPreferAudioMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferAudioMetadata', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsPreferAudioMetadataHelp">
|
<ui-tooltip :text="$strings.LabelSettingsPreferAudioMetadataHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsPreferAudioMetadata }}
|
<span id="settings-prefer-audio-metadata">{{ $strings.LabelSettingsPreferAudioMetadata }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerPreferOpfMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOpfMetadata', val)" />
|
<ui-toggle-switch labeledBy="settings-prefer-opf-metadata" v-model="newServerSettings.scannerPreferOpfMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferOpfMetadata', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsPreferOPFMetadataHelp">
|
<ui-tooltip :text="$strings.LabelSettingsPreferOPFMetadataHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsPreferOPFMetadata }}
|
<span id="settings-prefer-opf-metadata">{{ $strings.LabelSettingsPreferOPFMetadata }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerPreferMatchedMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferMatchedMetadata', val)" />
|
<ui-toggle-switch labeledBy="settings-prefer-matched-metadata" v-model="newServerSettings.scannerPreferMatchedMetadata" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerPreferMatchedMetadata', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsPreferMatchedMetadataHelp">
|
<ui-tooltip :text="$strings.LabelSettingsPreferMatchedMetadataHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsPreferMatchedMetadata }}
|
<span id="settings-prefer-matched-metadata">{{ $strings.LabelSettingsPreferMatchedMetadata }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.scannerDisableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', val)" />
|
<ui-toggle-switch labeledBy="settings-disable-watcher" v-model="newServerSettings.scannerDisableWatcher" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('scannerDisableWatcher', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsDisableWatcherHelp">
|
<ui-tooltip :text="$strings.LabelSettingsDisableWatcherHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsDisableWatcher }}
|
<span id="settings-disable-watcher">{{ $strings.LabelSettingsDisableWatcher }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
@@ -163,11 +161,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="showExperimentalFeatures" />
|
<ui-toggle-switch labeledBy="settings-experimental-features" v-model="showExperimentalFeatures" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsExperimentalFeaturesHelp">
|
<ui-tooltip :text="$strings.LabelSettingsExperimentalFeaturesHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsExperimentalFeatures }}
|
<span id="settings-experimental-features">{{ $strings.LabelSettingsExperimentalFeatures }}</span>
|
||||||
<a href="https://github.com/advplyr/audiobookshelf/discussions/75" target="_blank">
|
<a :aria-label="$strings.LabelSettingsExperimentalFeaturesHelp" href="https://github.com/advplyr/audiobookshelf/discussions/75" target="_blank">
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -175,10 +173,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-toggle-switch v-model="newServerSettings.enableEReader" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('enableEReader', val)" />
|
<ui-toggle-switch labeledBy="settings-enable-e-reader" v-model="newServerSettings.enableEReader" :disabled="updatingServerSettings" @input="(val) => updateSettingsKey('enableEReader', val)" />
|
||||||
<ui-tooltip :text="$strings.LabelSettingsEnableEReaderHelp">
|
<ui-tooltip :text="$strings.LabelSettingsEnableEReaderHelp">
|
||||||
<p class="pl-4">
|
<p class="pl-4">
|
||||||
{{ $strings.LabelSettingsEnableEReader }}
|
<span id="settings-enable-e-reader">{{ $strings.LabelSettingsEnableEReader }}</span>
|
||||||
<span class="material-icons icon-text">info_outlined</span>
|
<span class="material-icons icon-text">info_outlined</span>
|
||||||
</p>
|
</p>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|||||||
@@ -28,8 +28,10 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="loading" class="absolute top-0 left-0 w-full h-full bg-black/25 flex items-center justify-center">
|
<div v-if="loading" class="absolute top-0 left-0 w-full h-full bg-black/25 rounded-md">
|
||||||
<ui-loading-indicator />
|
<div class="sticky top-0 left-0 w-full h-full flex items-center justify-center" style="max-height: 80vh">
|
||||||
|
<ui-loading-indicator />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -28,8 +28,10 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="loading" class="absolute top-0 left-0 w-full h-full bg-black/25 flex items-center justify-center">
|
<div v-if="loading" class="absolute top-0 left-0 w-full h-full bg-black/25 rounded-md">
|
||||||
<ui-loading-indicator />
|
<div class="sticky top-0 left-0 w-full h-full flex items-center justify-center" style="max-height: 80vh">
|
||||||
|
<ui-loading-indicator />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+105
-22
@@ -34,6 +34,9 @@
|
|||||||
|
|
||||||
<template v-if="!isVideo">
|
<template v-if="!isVideo">
|
||||||
<p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">by {{ podcastAuthor || 'Unknown' }}</p>
|
<p v-if="isPodcast" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl">by {{ podcastAuthor || 'Unknown' }}</p>
|
||||||
|
<p v-else-if="musicArtists.length" class="mb-2 mt-0.5 text-gray-200 text-lg md:text-xl max-w-[calc(100vw-2rem)] overflow-hidden overflow-ellipsis">
|
||||||
|
<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 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}`" class="hover:underline">{{ author.name }}<span v-if="index < authors.length - 1">, </span></nuxt-link>
|
||||||
</p>
|
</p>
|
||||||
@@ -59,6 +62,38 @@
|
|||||||
{{ publishedYear }}
|
{{ publishedYear }}
|
||||||
</div>
|
</div>
|
||||||
</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="flex py-0.5" v-if="genres.length">
|
||||||
<div class="w-32">
|
<div class="w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelGenres }}</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelGenres }}</span>
|
||||||
@@ -70,7 +105,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="tracks.length" class="flex py-0.5">
|
<div v-if="tracks.length || audioFile || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
|
||||||
<div class="w-32">
|
<div class="w-32">
|
||||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -132,6 +167,7 @@
|
|||||||
<span v-show="!isStreaming" class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
<span v-show="!isStreaming" class="material-icons text-2xl -ml-2 pr-1 text-white">play_arrow</span>
|
||||||
{{ isStreaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
{{ isStreaming ? $strings.ButtonPlaying : $strings.ButtonPlay }}
|
||||||
</ui-btn>
|
</ui-btn>
|
||||||
|
|
||||||
<ui-btn v-else-if="isMissing || isInvalid" color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
|
<ui-btn v-else-if="isMissing || isInvalid" color="error" :padding-x="4" small class="flex items-center h-9 mr-2">
|
||||||
<span v-show="!isStreaming" class="material-icons text-2xl -ml-2 pr-1 text-white">error</span>
|
<span v-show="!isStreaming" class="material-icons text-2xl -ml-2 pr-1 text-white">error</span>
|
||||||
{{ isMissing ? $strings.LabelMissing : $strings.LabelIncomplete }}
|
{{ isMissing ? $strings.LabelMissing : $strings.LabelIncomplete }}
|
||||||
@@ -150,11 +186,11 @@
|
|||||||
<ui-icon-btn icon="edit" class="mx-0.5" @click="editClick" />
|
<ui-icon-btn icon="edit" class="mx-0.5" @click="editClick" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip v-if="!isPodcast" :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
|
<ui-tooltip v-if="!isPodcast && !isMusic" :text="userIsFinished ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" direction="top">
|
||||||
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="mx-0.5" @click="toggleFinished" />
|
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="mx-0.5" @click="toggleFinished" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
<ui-tooltip v-if="!isPodcast && userCanUpdate" :text="$strings.LabelCollections" direction="top">
|
<ui-tooltip v-if="showCollectionsButton" :text="$strings.LabelCollections" direction="top">
|
||||||
<ui-icon-btn icon="collections_bookmark" class="mx-0.5" outlined @click="collectionsClick" />
|
<ui-icon-btn icon="collections_bookmark" class="mx-0.5" outlined @click="collectionsClick" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
@@ -173,7 +209,7 @@
|
|||||||
|
|
||||||
<!-- RSS feed -->
|
<!-- RSS feed -->
|
||||||
<ui-tooltip v-if="showRssFeedBtn" :text="$strings.LabelOpenRSSFeed" direction="top">
|
<ui-tooltip v-if="showRssFeedBtn" :text="$strings.LabelOpenRSSFeed" direction="top">
|
||||||
<ui-icon-btn icon="rss_feed" class="mx-0.5" :bg-color="rssFeedUrl ? 'success' : 'primary'" outlined @click="clickRSSFeed" />
|
<ui-icon-btn icon="rss_feed" class="mx-0.5" :bg-color="rssFeed ? 'success' : 'primary'" outlined @click="clickRSSFeed" />
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -199,7 +235,6 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modals-podcast-episode-feed v-model="showPodcastEpisodeFeed" :library-item="libraryItem" :episodes="podcastFeedEpisodes" />
|
<modals-podcast-episode-feed v-model="showPodcastEpisodeFeed" :library-item="libraryItem" :episodes="podcastFeedEpisodes" />
|
||||||
<modals-rssfeed-view-modal v-model="showRssFeedModal" :library-item="libraryItem" :feed-url="rssFeedUrl" />
|
|
||||||
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :library-item-id="libraryItemId" hide-create @select="selectBookmark" />
|
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :library-item-id="libraryItemId" hide-create @select="selectBookmark" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -222,7 +257,7 @@ export default {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
libraryItem: item,
|
libraryItem: item,
|
||||||
rssFeedUrl: item.rssFeedUrl || null
|
rssFeed: item.rssFeed || null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
@@ -234,7 +269,6 @@ export default {
|
|||||||
podcastFeedEpisodes: [],
|
podcastFeedEpisodes: [],
|
||||||
episodesDownloading: [],
|
episodesDownloading: [],
|
||||||
episodeDownloadsQueued: [],
|
episodeDownloadsQueued: [],
|
||||||
showRssFeedModal: false,
|
|
||||||
showBookmarksModal: false
|
showBookmarksModal: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -263,12 +297,18 @@ export default {
|
|||||||
isDeveloperMode() {
|
isDeveloperMode() {
|
||||||
return this.$store.state.developerMode
|
return this.$store.state.developerMode
|
||||||
},
|
},
|
||||||
|
isBook() {
|
||||||
|
return this.libraryItem.mediaType === 'book'
|
||||||
|
},
|
||||||
isPodcast() {
|
isPodcast() {
|
||||||
return this.libraryItem.mediaType === 'podcast'
|
return this.libraryItem.mediaType === 'podcast'
|
||||||
},
|
},
|
||||||
isVideo() {
|
isVideo() {
|
||||||
return this.libraryItem.mediaType === 'video'
|
return this.libraryItem.mediaType === 'video'
|
||||||
},
|
},
|
||||||
|
isMusic() {
|
||||||
|
return this.libraryItem.mediaType === 'music'
|
||||||
|
},
|
||||||
isMissing() {
|
isMissing() {
|
||||||
return this.libraryItem.isMissing
|
return this.libraryItem.isMissing
|
||||||
},
|
},
|
||||||
@@ -276,11 +316,12 @@ export default {
|
|||||||
return this.libraryItem.isInvalid
|
return this.libraryItem.isInvalid
|
||||||
},
|
},
|
||||||
invalidAudioFiles() {
|
invalidAudioFiles() {
|
||||||
if (this.isPodcast || this.isVideo) return []
|
if (!this.isBook) return []
|
||||||
return this.libraryItem.media.audioFiles.filter((af) => af.invalid)
|
return this.libraryItem.media.audioFiles.filter((af) => af.invalid)
|
||||||
},
|
},
|
||||||
showPlayButton() {
|
showPlayButton() {
|
||||||
if (this.isMissing || this.isInvalid) return false
|
if (this.isMissing || this.isInvalid) return false
|
||||||
|
if (this.isMusic) return !!this.audioFile
|
||||||
if (this.isVideo) return !!this.videoFile
|
if (this.isVideo) return !!this.videoFile
|
||||||
if (this.isPodcast) return this.podcastEpisodes.length
|
if (this.isPodcast) return this.podcastEpisodes.length
|
||||||
return this.tracks.length
|
return this.tracks.length
|
||||||
@@ -338,6 +379,25 @@ export default {
|
|||||||
authors() {
|
authors() {
|
||||||
return this.mediaMetadata.authors || []
|
return this.mediaMetadata.authors || []
|
||||||
},
|
},
|
||||||
|
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() {
|
narrators() {
|
||||||
return this.mediaMetadata.narrators || []
|
return this.mediaMetadata.narrators || []
|
||||||
},
|
},
|
||||||
@@ -346,7 +406,7 @@ export default {
|
|||||||
},
|
},
|
||||||
seriesList() {
|
seriesList() {
|
||||||
return this.series.map((se) => {
|
return this.series.map((se) => {
|
||||||
var text = se.name
|
let text = se.name
|
||||||
if (se.sequence) text += ` #${se.sequence}`
|
if (se.sequence) text += ` #${se.sequence}`
|
||||||
return {
|
return {
|
||||||
...se,
|
...se,
|
||||||
@@ -355,13 +415,22 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
durationPretty() {
|
durationPretty() {
|
||||||
if (!this.tracks.length) return 'N/A'
|
if (this.isPodcast) return this.$elapsedPrettyExtended(this.totalPodcastDuration)
|
||||||
return this.$elapsedPretty(this.media.duration)
|
|
||||||
|
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) 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() {
|
sizePretty() {
|
||||||
return this.$bytesPretty(this.media.size)
|
return this.$bytesPretty(this.media.size)
|
||||||
},
|
},
|
||||||
@@ -374,6 +443,10 @@ export default {
|
|||||||
videoFile() {
|
videoFile() {
|
||||||
return this.media.videoFile
|
return this.media.videoFile
|
||||||
},
|
},
|
||||||
|
audioFile() {
|
||||||
|
// Music track
|
||||||
|
return this.media.audioFile
|
||||||
|
},
|
||||||
showExperimentalReadAlert() {
|
showExperimentalReadAlert() {
|
||||||
return !this.tracks.length && this.ebookFile && !this.showExperimentalFeatures && !this.enableEReader
|
return !this.tracks.length && this.ebookFile && !this.showExperimentalFeatures && !this.enableEReader
|
||||||
},
|
},
|
||||||
@@ -381,6 +454,7 @@ export default {
|
|||||||
return this.mediaMetadata.description || ''
|
return this.mediaMetadata.description || ''
|
||||||
},
|
},
|
||||||
userMediaProgress() {
|
userMediaProgress() {
|
||||||
|
if (this.isMusic) return null
|
||||||
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
userIsFinished() {
|
userIsFinished() {
|
||||||
@@ -388,7 +462,7 @@ export default {
|
|||||||
},
|
},
|
||||||
userTimeRemaining() {
|
userTimeRemaining() {
|
||||||
if (!this.userMediaProgress) return 0
|
if (!this.userMediaProgress) return 0
|
||||||
var duration = this.userMediaProgress.duration || this.duration
|
const duration = this.userMediaProgress.duration || this.duration
|
||||||
return duration - this.userMediaProgress.currentTime
|
return duration - this.userMediaProgress.currentTime
|
||||||
},
|
},
|
||||||
progressPercent() {
|
progressPercent() {
|
||||||
@@ -419,14 +493,17 @@ export default {
|
|||||||
return this.$store.getters['user/getUserCanDownload']
|
return this.$store.getters['user/getUserCanDownload']
|
||||||
},
|
},
|
||||||
showRssFeedBtn() {
|
showRssFeedBtn() {
|
||||||
if (!this.rssFeedUrl && !this.podcastEpisodes.length && !this.tracks.length) return false // Cannot open RSS feed with no episodes/tracks
|
if (!this.rssFeed && !this.podcastEpisodes.length && !this.tracks.length) return false // Cannot open RSS feed with no episodes/tracks
|
||||||
|
|
||||||
// If rss feed is open then show feed url to users otherwise just show to admins
|
// If rss feed is open then show feed url to users otherwise just show to admins
|
||||||
return this.userIsAdminOrUp || this.rssFeedUrl
|
return this.userIsAdminOrUp || this.rssFeed
|
||||||
},
|
},
|
||||||
showQueueBtn() {
|
showQueueBtn() {
|
||||||
if (this.isPodcast || this.isVideo) return false
|
if (!this.isBook) return false
|
||||||
return !this.$store.getters['getIsStreamingFromDifferentLibrary'] && this.streamLibraryItem
|
return !this.$store.getters['getIsStreamingFromDifferentLibrary'] && this.streamLibraryItem
|
||||||
|
},
|
||||||
|
showCollectionsButton() {
|
||||||
|
return this.isBook && this.userCanUpdate
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -531,14 +608,14 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
playItem(startTime = null) {
|
playItem(startTime = null) {
|
||||||
var episodeId = null
|
let episodeId = null
|
||||||
const queueItems = []
|
const queueItems = []
|
||||||
if (this.isPodcast) {
|
if (this.isPodcast) {
|
||||||
const episodesInListeningOrder = this.podcastEpisodes.map((ep) => ({ ...ep })).sort((a, b) => String(a.publishedAt).localeCompare(String(b.publishedAt), undefined, { numeric: true, sensitivity: 'base' }))
|
const episodesInListeningOrder = this.podcastEpisodes.map((ep) => ({ ...ep })).sort((a, b) => String(a.publishedAt).localeCompare(String(b.publishedAt), undefined, { numeric: true, sensitivity: 'base' }))
|
||||||
|
|
||||||
// Find most recent episode unplayed
|
// Find most recent episode unplayed
|
||||||
var episodeIndex = episodesInListeningOrder.findLastIndex((ep) => {
|
let episodeIndex = episodesInListeningOrder.findLastIndex((ep) => {
|
||||||
var podcastProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, ep.id)
|
const podcastProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, ep.id)
|
||||||
return !podcastProgress || !podcastProgress.isFinished
|
return !podcastProgress || !podcastProgress.isFinished
|
||||||
})
|
})
|
||||||
if (episodeIndex < 0) episodeIndex = 0
|
if (episodeIndex < 0) episodeIndex = 0
|
||||||
@@ -617,7 +694,13 @@ export default {
|
|||||||
this.$store.commit('globals/setShowPlaylistsModal', true)
|
this.$store.commit('globals/setShowPlaylistsModal', true)
|
||||||
},
|
},
|
||||||
clickRSSFeed() {
|
clickRSSFeed() {
|
||||||
this.showRssFeedModal = true
|
this.$store.commit('globals/setRSSFeedOpenCloseModal', {
|
||||||
|
id: this.libraryItemId,
|
||||||
|
name: this.title,
|
||||||
|
type: 'item',
|
||||||
|
feed: this.rssFeed,
|
||||||
|
hasEpisodesWithoutPubDate: this.podcastEpisodes.some((ep) => !ep.pubDate)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
episodeDownloadQueued(episodeDownload) {
|
episodeDownloadQueued(episodeDownload) {
|
||||||
if (episodeDownload.libraryItemId === this.libraryItemId) {
|
if (episodeDownload.libraryItemId === this.libraryItemId) {
|
||||||
@@ -639,13 +722,13 @@ export default {
|
|||||||
rssFeedOpen(data) {
|
rssFeedOpen(data) {
|
||||||
if (data.entityId === this.libraryItemId) {
|
if (data.entityId === this.libraryItemId) {
|
||||||
console.log('RSS Feed Opened', data)
|
console.log('RSS Feed Opened', data)
|
||||||
this.rssFeedUrl = data.feedUrl
|
this.rssFeed = data
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rssFeedClosed(data) {
|
rssFeedClosed(data) {
|
||||||
if (data.entityId === this.libraryItemId) {
|
if (data.entityId === this.libraryItemId) {
|
||||||
console.log('RSS Feed Closed', data)
|
console.log('RSS Feed Closed', data)
|
||||||
this.rssFeedUrl = null
|
this.rssFeed = null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
queueBtnClick() {
|
queueBtnClick() {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ store, params, redirect }) {
|
async asyncData({ store, params, redirect }) {
|
||||||
var libraryId = params.library
|
const libraryId = params.library
|
||||||
var library = await store.dispatch('libraries/fetch', libraryId)
|
const library = await store.dispatch('libraries/fetch', libraryId)
|
||||||
if (!library) {
|
if (!library) {
|
||||||
return redirect(`/oops?message=Library "${libraryId}" not found`)
|
return redirect(`/oops?message=Library "${libraryId}" not found`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
async asyncData({ store, params, redirect, query, app }) {
|
async asyncData({ store, params, redirect, query, app }) {
|
||||||
var libraryId = params.library
|
const libraryId = params.library
|
||||||
var libraryData = await store.dispatch('libraries/fetch', libraryId)
|
const libraryData = await store.dispatch('libraries/fetch', libraryId)
|
||||||
if (!libraryData) {
|
if (!libraryData) {
|
||||||
return redirect('/oops?message=Library not found')
|
return redirect('/oops?message=Library not found')
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ export default {
|
|||||||
return redirect(`/library/${libraryId}`)
|
return redirect(`/library/${libraryId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
var series = await app.$axios.$get(`/api/series/${params.id}?include=progress`).catch((error) => {
|
const series = await app.$axios.$get(`/api/series/${params.id}?include=progress,rssfeed`).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -127,7 +127,6 @@ export default {
|
|||||||
setUser({ user, userDefaultLibraryId, serverSettings, Source, feeds }) {
|
setUser({ user, userDefaultLibraryId, serverSettings, Source, feeds }) {
|
||||||
this.$store.commit('setServerSettings', serverSettings)
|
this.$store.commit('setServerSettings', serverSettings)
|
||||||
this.$store.commit('setSource', Source)
|
this.$store.commit('setSource', Source)
|
||||||
this.$store.commit('feeds/setFeeds', feeds)
|
|
||||||
this.$setServerLanguageCode(serverSettings.language)
|
this.$setServerLanguageCode(serverSettings.language)
|
||||||
|
|
||||||
if (serverSettings.chromecastEnabled) {
|
if (serverSettings.chromecastEnabled) {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export default class PlayerHandler {
|
|||||||
this.playerState = 'IDLE'
|
this.playerState = 'IDLE'
|
||||||
this.isHlsTranscode = false
|
this.isHlsTranscode = false
|
||||||
this.isVideo = false
|
this.isVideo = false
|
||||||
|
this.isMusic = false
|
||||||
this.currentSessionId = null
|
this.currentSessionId = null
|
||||||
this.startTimeOverride = undefined // Used for starting playback at a specific time (i.e. clicking bookmark from library item page)
|
this.startTimeOverride = undefined // Used for starting playback at a specific time (i.e. clicking bookmark from library item page)
|
||||||
this.startTime = 0
|
this.startTime = 0
|
||||||
@@ -54,10 +55,13 @@ export default class PlayerHandler {
|
|||||||
|
|
||||||
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.isMusic = libraryItem.mediaType === 'music'
|
||||||
|
|
||||||
this.episodeId = episodeId
|
this.episodeId = episodeId
|
||||||
this.playWhenReady = playWhenReady
|
this.playWhenReady = playWhenReady
|
||||||
this.initialPlaybackRate = playbackRate
|
this.initialPlaybackRate = this.isMusic ? 1 : playbackRate
|
||||||
this.isVideo = libraryItem.mediaType === 'video'
|
|
||||||
this.startTimeOverride = (startTimeOverride == null || isNaN(startTimeOverride)) ? undefined : Number(startTimeOverride)
|
this.startTimeOverride = (startTimeOverride == null || isNaN(startTimeOverride)) ? undefined : Number(startTimeOverride)
|
||||||
|
|
||||||
if (!this.player) this.switchPlayer(playWhenReady)
|
if (!this.player) this.switchPlayer(playWhenReady)
|
||||||
@@ -140,12 +144,14 @@ export default class PlayerHandler {
|
|||||||
playerStateChange(state) {
|
playerStateChange(state) {
|
||||||
console.log('[PlayerHandler] Player state change', state)
|
console.log('[PlayerHandler] Player state change', state)
|
||||||
this.playerState = state
|
this.playerState = state
|
||||||
|
|
||||||
if (this.playerState === 'PLAYING') {
|
if (this.playerState === 'PLAYING') {
|
||||||
this.setPlaybackRate(this.initialPlaybackRate)
|
this.setPlaybackRate(this.initialPlaybackRate)
|
||||||
this.startPlayInterval()
|
this.startPlayInterval()
|
||||||
} else {
|
} else {
|
||||||
this.stopPlayInterval()
|
this.stopPlayInterval()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.player) {
|
if (this.player) {
|
||||||
if (this.playerState === 'LOADED' || this.playerState === 'PLAYING') {
|
if (this.playerState === 'LOADED' || this.playerState === 'PLAYING') {
|
||||||
this.ctx.setDuration(this.getDuration())
|
this.ctx.setDuration(this.getDuration())
|
||||||
@@ -252,14 +258,14 @@ export default class PlayerHandler {
|
|||||||
|
|
||||||
startPlayInterval() {
|
startPlayInterval() {
|
||||||
clearInterval(this.playInterval)
|
clearInterval(this.playInterval)
|
||||||
var lastTick = Date.now()
|
let lastTick = Date.now()
|
||||||
this.playInterval = setInterval(() => {
|
this.playInterval = setInterval(() => {
|
||||||
// Update UI
|
// Update UI
|
||||||
if (!this.player) return
|
if (!this.player) return
|
||||||
var currentTime = this.player.getCurrentTime()
|
const currentTime = this.player.getCurrentTime()
|
||||||
this.ctx.setCurrentTime(currentTime)
|
this.ctx.setCurrentTime(currentTime)
|
||||||
|
|
||||||
var 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) {
|
if (this.listeningTimeSinceSync >= 5) {
|
||||||
@@ -269,9 +275,9 @@ export default class PlayerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendCloseSession() {
|
sendCloseSession() {
|
||||||
var syncData = null
|
let syncData = null
|
||||||
if (this.player) {
|
if (this.player) {
|
||||||
var listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
|
const listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
|
||||||
syncData = {
|
syncData = {
|
||||||
timeListened: listeningTimeToAdd,
|
timeListened: listeningTimeToAdd,
|
||||||
duration: this.getDuration(),
|
duration: this.getDuration(),
|
||||||
@@ -285,12 +291,14 @@ export default class PlayerHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sendProgressSync(currentTime) {
|
sendProgressSync(currentTime) {
|
||||||
var diffSinceLastSync = Math.abs(this.lastSyncTime - currentTime)
|
if (this.isMusic) return
|
||||||
|
|
||||||
|
const diffSinceLastSync = Math.abs(this.lastSyncTime - currentTime)
|
||||||
if (diffSinceLastSync < 1) return
|
if (diffSinceLastSync < 1) return
|
||||||
|
|
||||||
this.lastSyncTime = currentTime
|
this.lastSyncTime = currentTime
|
||||||
var listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
|
const listeningTimeToAdd = Math.max(0, Math.floor(this.listeningTimeSinceSync))
|
||||||
var syncData = {
|
const syncData = {
|
||||||
timeListened: listeningTimeToAdd,
|
timeListened: listeningTimeToAdd,
|
||||||
duration: this.getDuration(),
|
duration: this.getDuration(),
|
||||||
currentTime
|
currentTime
|
||||||
|
|||||||
+11
-9
@@ -5,18 +5,18 @@ import { supplant } from './utils'
|
|||||||
const defaultCode = 'en-us'
|
const defaultCode = 'en-us'
|
||||||
|
|
||||||
const languageCodeMap = {
|
const languageCodeMap = {
|
||||||
'de': 'Deutsch',
|
'de': { label: 'Deutsch', dateFnsLocale: 'de' },
|
||||||
'en-us': 'English',
|
'en-us': { label: 'English', dateFnsLocale: 'enUS' },
|
||||||
// 'es': 'Español',
|
// 'es': { label: 'Español', dateFnsLocale: 'es' },
|
||||||
'fr': 'Français',
|
'fr': { label: 'Français', dateFnsLocale: 'fr' },
|
||||||
'hr': 'Hrvatski',
|
'hr': { label: 'Hrvatski', dateFnsLocale: 'hr' },
|
||||||
'it': 'Italiano',
|
'it': { label: 'Italiano', dateFnsLocale: 'it' },
|
||||||
'pl': 'Polski',
|
'pl': { label: 'Polski', dateFnsLocale: 'pl' },
|
||||||
'zh-cn': '简体中文 (Simplified Chinese)'
|
'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' },
|
||||||
}
|
}
|
||||||
Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => {
|
Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => {
|
||||||
return {
|
return {
|
||||||
text: languageCodeMap[code],
|
text: languageCodeMap[code].label,
|
||||||
value: code
|
value: code
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -73,6 +73,8 @@ 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)
|
||||||
|
|
||||||
console.log('i18n strings=', Vue.prototype.$strings)
|
console.log('i18n strings=', Vue.prototype.$strings)
|
||||||
Vue.prototype.$eventBus.$emit('change-lang', code)
|
Vue.prototype.$eventBus.$emit('change-lang', code)
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import Path from 'path'
|
import Path from 'path'
|
||||||
import vClickOutside from 'v-click-outside'
|
import vClickOutside from 'v-click-outside'
|
||||||
import { formatDistance, format, addDays, isDate } from 'date-fns'
|
import { formatDistance, format, addDays, isDate, setDefaultOptions } from 'date-fns'
|
||||||
|
import * as locale from 'date-fns/locale'
|
||||||
|
|
||||||
Vue.directive('click-outside', vClickOutside.directive)
|
Vue.directive('click-outside', vClickOutside.directive)
|
||||||
|
|
||||||
|
|
||||||
|
Vue.prototype.$setDateFnsLocale = (localeString) => {
|
||||||
|
if (!locale[localeString]) return 0
|
||||||
|
return setDefaultOptions({ locale: locale[localeString] })
|
||||||
|
}
|
||||||
Vue.prototype.$dateDistanceFromNow = (unixms) => {
|
Vue.prototype.$dateDistanceFromNow = (unixms) => {
|
||||||
if (!unixms) return ''
|
if (!unixms) return ''
|
||||||
return formatDistance(unixms, Date.now(), { addSuffix: true })
|
return formatDistance(unixms, Date.now(), { addSuffix: true })
|
||||||
|
|||||||
@@ -54,18 +54,18 @@ Vue.prototype.$elapsedPrettyExtended = (seconds, useDays = true) => {
|
|||||||
if (isNaN(seconds) || seconds === null) return ''
|
if (isNaN(seconds) || seconds === null) return ''
|
||||||
seconds = Math.round(seconds)
|
seconds = Math.round(seconds)
|
||||||
|
|
||||||
var minutes = Math.floor(seconds / 60)
|
let minutes = Math.floor(seconds / 60)
|
||||||
seconds -= minutes * 60
|
seconds -= minutes * 60
|
||||||
var hours = Math.floor(minutes / 60)
|
let hours = Math.floor(minutes / 60)
|
||||||
minutes -= hours * 60
|
minutes -= hours * 60
|
||||||
|
|
||||||
var days = 0
|
let days = 0
|
||||||
if (useDays || Math.floor(hours / 24) >= 100) {
|
if (useDays || Math.floor(hours / 24) >= 100) {
|
||||||
days = Math.floor(hours / 24)
|
days = Math.floor(hours / 24)
|
||||||
hours -= days * 24
|
hours -= days * 24
|
||||||
}
|
}
|
||||||
|
|
||||||
var strs = []
|
const strs = []
|
||||||
if (days) strs.push(`${days}d`)
|
if (days) strs.push(`${days}d`)
|
||||||
if (hours) strs.push(`${hours}h`)
|
if (hours) strs.push(`${hours}h`)
|
||||||
if (minutes) strs.push(`${minutes}m`)
|
if (minutes) strs.push(`${minutes}m`)
|
||||||
|
|||||||
Binary file not shown.
@@ -1,28 +0,0 @@
|
|||||||
|
|
||||||
export const state = () => ({
|
|
||||||
feeds: []
|
|
||||||
})
|
|
||||||
|
|
||||||
export const getters = {
|
|
||||||
getFeedForItem: state => id => {
|
|
||||||
return state.feeds.find(feed => feed.id === id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const actions = {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export const mutations = {
|
|
||||||
addFeed(state, feed) {
|
|
||||||
var index = state.feeds.findIndex(f => f.id === feed.id)
|
|
||||||
if (index >= 0) state.feeds.splice(index, 1, feed)
|
|
||||||
else state.feeds.push(feed)
|
|
||||||
},
|
|
||||||
removeFeed(state, feed) {
|
|
||||||
state.feeds = state.feeds.filter(f => f.id !== feed.id)
|
|
||||||
},
|
|
||||||
setFeeds(state, feeds) {
|
|
||||||
state.feeds = feeds || []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+12
-1
@@ -1,6 +1,7 @@
|
|||||||
export const state = () => ({
|
export const state = () => ({
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
isMobileLandscape: false,
|
isMobileLandscape: false,
|
||||||
|
isMobilePortrait: false,
|
||||||
showBatchCollectionModal: false,
|
showBatchCollectionModal: false,
|
||||||
showCollectionsModal: false,
|
showCollectionsModal: false,
|
||||||
showEditCollectionModal: false,
|
showEditCollectionModal: false,
|
||||||
@@ -8,9 +9,11 @@ export const state = () => ({
|
|||||||
showEditPlaylistModal: false,
|
showEditPlaylistModal: false,
|
||||||
showEditPodcastEpisode: false,
|
showEditPodcastEpisode: false,
|
||||||
showViewPodcastEpisodeModal: false,
|
showViewPodcastEpisodeModal: false,
|
||||||
|
showRSSFeedOpenCloseModal: false,
|
||||||
showConfirmPrompt: false,
|
showConfirmPrompt: false,
|
||||||
confirmPromptOptions: null,
|
confirmPromptOptions: null,
|
||||||
showEditAuthorModal: false,
|
showEditAuthorModal: false,
|
||||||
|
rssFeedEntity: null,
|
||||||
selectedEpisode: null,
|
selectedEpisode: null,
|
||||||
selectedPlaylistItems: null,
|
selectedPlaylistItems: null,
|
||||||
selectedPlaylist: null,
|
selectedPlaylist: null,
|
||||||
@@ -74,7 +77,8 @@ export const getters = {
|
|||||||
export const mutations = {
|
export const mutations = {
|
||||||
updateWindowSize(state, { width, height }) {
|
updateWindowSize(state, { width, height }) {
|
||||||
state.isMobile = width < 640 || height < 640
|
state.isMobile = width < 640 || height < 640
|
||||||
state.isMobileLandscape = state.isMobile && height > width
|
state.isMobileLandscape = state.isMobile && height < width
|
||||||
|
state.isMobilePortrait = state.isMobile && height >= width
|
||||||
},
|
},
|
||||||
setShowCollectionsModal(state, val) {
|
setShowCollectionsModal(state, val) {
|
||||||
state.showBatchCollectionModal = false
|
state.showBatchCollectionModal = false
|
||||||
@@ -99,6 +103,13 @@ export const mutations = {
|
|||||||
setShowViewPodcastEpisodeModal(state, val) {
|
setShowViewPodcastEpisodeModal(state, val) {
|
||||||
state.showViewPodcastEpisodeModal = val
|
state.showViewPodcastEpisodeModal = val
|
||||||
},
|
},
|
||||||
|
setShowRSSFeedOpenCloseModal(state, val) {
|
||||||
|
state.showRSSFeedOpenCloseModal = val
|
||||||
|
},
|
||||||
|
setRSSFeedOpenCloseModal(state, entity) {
|
||||||
|
state.rssFeedEntity = entity
|
||||||
|
state.showRSSFeedOpenCloseModal = true
|
||||||
|
},
|
||||||
setShowConfirmPrompt(state, val) {
|
setShowConfirmPrompt(state, val) {
|
||||||
state.showConfirmPrompt = val
|
state.showConfirmPrompt = val
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ export const actions = {
|
|||||||
},
|
},
|
||||||
loadFolders({ state, commit }) {
|
loadFolders({ state, commit }) {
|
||||||
if (state.folders.length) {
|
if (state.folders.length) {
|
||||||
var lastCheck = Date.now() - state.folderLastUpdate
|
const lastCheck = Date.now() - state.folderLastUpdate
|
||||||
if (lastCheck < 1000 * 60 * 10) { // 10 minutes
|
if (lastCheck < 1000 * 5) { // 5 seconds
|
||||||
// Folders up to date
|
// Folders up to date
|
||||||
return state.folders
|
return state.folders
|
||||||
}
|
}
|
||||||
|
|||||||
+64
-64
@@ -20,9 +20,10 @@
|
|||||||
"ButtonCreate": "Ertsellen",
|
"ButtonCreate": "Ertsellen",
|
||||||
"ButtonCreateBackup": "Sicherung erstellen",
|
"ButtonCreateBackup": "Sicherung erstellen",
|
||||||
"ButtonDelete": "Löschen",
|
"ButtonDelete": "Löschen",
|
||||||
|
"ButtonEdit": "Bearbeiten",
|
||||||
"ButtonEditChapters": "Kapitel bearbeiten",
|
"ButtonEditChapters": "Kapitel bearbeiten",
|
||||||
"ButtonEditPodcast": "Podcast bearbeiten",
|
"ButtonEditPodcast": "Podcast bearbeiten",
|
||||||
"ButtonForceReScan": "Erzwinge kompletten Neu-Scan",
|
"ButtonForceReScan": "Komplett-Scan (alle Medien)",
|
||||||
"ButtonFullPath": "Vollständiger Pfad",
|
"ButtonFullPath": "Vollständiger Pfad",
|
||||||
"ButtonHide": "Ausblenden",
|
"ButtonHide": "Ausblenden",
|
||||||
"ButtonHome": "Startseite",
|
"ButtonHome": "Startseite",
|
||||||
@@ -33,8 +34,8 @@
|
|||||||
"ButtonLookup": "Online-Suche",
|
"ButtonLookup": "Online-Suche",
|
||||||
"ButtonManageTracks": "Tracks verwalten",
|
"ButtonManageTracks": "Tracks verwalten",
|
||||||
"ButtonMapChapterTitles": "Kapitelüberschriften zuordnen",
|
"ButtonMapChapterTitles": "Kapitelüberschriften zuordnen",
|
||||||
"ButtonMatchAllAuthors": "Online-Suche für alle Autoren",
|
"ButtonMatchAllAuthors": "Online Metadaten-Abgleich (alle Autoren)",
|
||||||
"ButtonMatchBooks": "Online-Suche für alle Hörbücher",
|
"ButtonMatchBooks": "Online Metadaten-Abgleich (alle Medien)",
|
||||||
"ButtonNevermind": "Vergiss es",
|
"ButtonNevermind": "Vergiss es",
|
||||||
"ButtonOk": "Ok",
|
"ButtonOk": "Ok",
|
||||||
"ButtonOpenFeed": "Feed öffnen",
|
"ButtonOpenFeed": "Feed öffnen",
|
||||||
@@ -42,9 +43,9 @@
|
|||||||
"ButtonPlay": "Abspielen",
|
"ButtonPlay": "Abspielen",
|
||||||
"ButtonPlaying": "Spielt",
|
"ButtonPlaying": "Spielt",
|
||||||
"ButtonPlaylists": "Wiedergabelisten",
|
"ButtonPlaylists": "Wiedergabelisten",
|
||||||
"ButtonPurgeAllCache": "Bereinige alle Zwischenspeicher",
|
"ButtonPurgeAllCache": "Lösche alle Zwischenspeicher",
|
||||||
"ButtonPurgeItemsCache": "Bereinige den Hörbuch/Podcast-Zwischenspeicher",
|
"ButtonPurgeItemsCache": "Lösche Medien-Zwischenspeicher",
|
||||||
"ButtonPurgeMediaProgress": "Bereinige die Hörfortschritte",
|
"ButtonPurgeMediaProgress": "Lösche Hörfortschritte",
|
||||||
"ButtonQueueAddItem": "Zur Warteschlange hinzufügen",
|
"ButtonQueueAddItem": "Zur Warteschlange hinzufügen",
|
||||||
"ButtonQueueRemoveItem": "Aus der Warteschlange entfernen",
|
"ButtonQueueRemoveItem": "Aus der Warteschlange entfernen",
|
||||||
"ButtonQuickMatch": "Schnellabgleich",
|
"ButtonQuickMatch": "Schnellabgleich",
|
||||||
@@ -60,7 +61,7 @@
|
|||||||
"ButtonSave": "Speichern",
|
"ButtonSave": "Speichern",
|
||||||
"ButtonSaveAndClose": "Speichern & Schließen",
|
"ButtonSaveAndClose": "Speichern & Schließen",
|
||||||
"ButtonSaveTracklist": "Speichere die Titelliste",
|
"ButtonSaveTracklist": "Speichere die Titelliste",
|
||||||
"ButtonScan": "Scan",
|
"ButtonScan": "Partial-Scan (nur geänderte/neue Medien)",
|
||||||
"ButtonScanLibrary": "Bibliothek scannen",
|
"ButtonScanLibrary": "Bibliothek scannen",
|
||||||
"ButtonSearch": "Suchen",
|
"ButtonSearch": "Suchen",
|
||||||
"ButtonSelectFolderPath": "Auswahl Ordnerpfad",
|
"ButtonSelectFolderPath": "Auswahl Ordnerpfad",
|
||||||
@@ -75,6 +76,8 @@
|
|||||||
"ButtonUploadBackup": "Sicherung hochladen",
|
"ButtonUploadBackup": "Sicherung hochladen",
|
||||||
"ButtonUploadCover": "Titelbild hochladen",
|
"ButtonUploadCover": "Titelbild hochladen",
|
||||||
"ButtonUploadOPMLFile": "OPML-Datei hochladen",
|
"ButtonUploadOPMLFile": "OPML-Datei hochladen",
|
||||||
|
"ButtonUserDelete": "Benutzer {0} löschen",
|
||||||
|
"ButtonUserEdit": "Benutzer {0} bearbeiten",
|
||||||
"ButtonViewAll": "Alles anzeigen",
|
"ButtonViewAll": "Alles anzeigen",
|
||||||
"ButtonYes": "Ja",
|
"ButtonYes": "Ja",
|
||||||
"HeaderAccount": "Konto",
|
"HeaderAccount": "Konto",
|
||||||
@@ -94,8 +97,8 @@
|
|||||||
"HeaderFiles": "Dateien",
|
"HeaderFiles": "Dateien",
|
||||||
"HeaderFindChapters": "Kapitel suchen",
|
"HeaderFindChapters": "Kapitel suchen",
|
||||||
"HeaderIgnoredFiles": "Ignorierte Dateien",
|
"HeaderIgnoredFiles": "Ignorierte Dateien",
|
||||||
"HeaderItemFiles": "Objekt-Dateien",
|
"HeaderItemFiles": "Medien-Dateien",
|
||||||
"HeaderItemMetadataUtils": "Hörbuch/Podcast Metadaten-Werkzeuge",
|
"HeaderItemMetadataUtils": "Metadaten",
|
||||||
"HeaderLastListeningSession": "Letzte Hörsitzung",
|
"HeaderLastListeningSession": "Letzte Hörsitzung",
|
||||||
"HeaderLatestEpisodes": "Letzte Episoden",
|
"HeaderLatestEpisodes": "Letzte Episoden",
|
||||||
"HeaderLibraries": "Bibliotheken",
|
"HeaderLibraries": "Bibliotheken",
|
||||||
@@ -108,7 +111,7 @@
|
|||||||
"HeaderManageGenres": "Kategorien verwalten",
|
"HeaderManageGenres": "Kategorien verwalten",
|
||||||
"HeaderManageTags": "Tags verwalten",
|
"HeaderManageTags": "Tags verwalten",
|
||||||
"HeaderMapDetails": "Stapelverarbeitung",
|
"HeaderMapDetails": "Stapelverarbeitung",
|
||||||
"HeaderMatch": "Online-Suche",
|
"HeaderMatch": "Metadaten",
|
||||||
"HeaderMetadataToEmbed": "Einzubettende Metadaten",
|
"HeaderMetadataToEmbed": "Einzubettende Metadaten",
|
||||||
"HeaderNewAccount": "Neues Konto",
|
"HeaderNewAccount": "Neues Konto",
|
||||||
"HeaderNewLibrary": "Neue Bibliothek",
|
"HeaderNewLibrary": "Neue Bibliothek",
|
||||||
@@ -118,7 +121,7 @@
|
|||||||
"HeaderPermissions": "Berechtigungen",
|
"HeaderPermissions": "Berechtigungen",
|
||||||
"HeaderPlayerQueue": "Spieler Warteschlange",
|
"HeaderPlayerQueue": "Spieler Warteschlange",
|
||||||
"HeaderPlaylist": "Wiedergabeliste",
|
"HeaderPlaylist": "Wiedergabeliste",
|
||||||
"HeaderPlaylistItems": "Hörbücher/Podcasts der Wiedergabeliste",
|
"HeaderPlaylistItems": "Einträge in der Wiedergabeliste",
|
||||||
"HeaderPodcastsToAdd": "Podcasts zum Hinzufügen",
|
"HeaderPodcastsToAdd": "Podcasts zum Hinzufügen",
|
||||||
"HeaderPreviewCover": "Vorschau Titelbild",
|
"HeaderPreviewCover": "Vorschau Titelbild",
|
||||||
"HeaderRemoveEpisode": "Episode löschen",
|
"HeaderRemoveEpisode": "Episode löschen",
|
||||||
@@ -146,7 +149,7 @@
|
|||||||
"HeaderUpdateDetails": "Details aktualisieren",
|
"HeaderUpdateDetails": "Details aktualisieren",
|
||||||
"HeaderUpdateLibrary": "Bibliothek aktualisieren",
|
"HeaderUpdateLibrary": "Bibliothek aktualisieren",
|
||||||
"HeaderUsers": "Benutzer",
|
"HeaderUsers": "Benutzer",
|
||||||
"HeaderYourStats": "Eigene Statistik",
|
"HeaderYourStats": "Eigene Statistiken",
|
||||||
"LabelAccountType": "Kontoart",
|
"LabelAccountType": "Kontoart",
|
||||||
"LabelAccountTypeAdmin": "Admin",
|
"LabelAccountTypeAdmin": "Admin",
|
||||||
"LabelAccountTypeGuest": "Gast",
|
"LabelAccountTypeGuest": "Gast",
|
||||||
@@ -235,7 +238,7 @@
|
|||||||
"LabelIntervalEveryDay": "Jeden Tag",
|
"LabelIntervalEveryDay": "Jeden Tag",
|
||||||
"LabelIntervalEveryHour": "Jede Stunde",
|
"LabelIntervalEveryHour": "Jede Stunde",
|
||||||
"LabelInvalidParts": "Ungültige Teile",
|
"LabelInvalidParts": "Ungültige Teile",
|
||||||
"LabelItem": "Hörbuch/Podcast",
|
"LabelItem": "Medium",
|
||||||
"LabelLanguage": "Sprache",
|
"LabelLanguage": "Sprache",
|
||||||
"LabelLanguageDefaultServer": "Standard-Server-Sprache",
|
"LabelLanguageDefaultServer": "Standard-Server-Sprache",
|
||||||
"LabelLastSeen": "Zuletzt angesehen",
|
"LabelLastSeen": "Zuletzt angesehen",
|
||||||
@@ -252,7 +255,6 @@
|
|||||||
"LabelLogLevelInfo": "Informationen",
|
"LabelLogLevelInfo": "Informationen",
|
||||||
"LabelLogLevelWarn": "Warnungen",
|
"LabelLogLevelWarn": "Warnungen",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Suchen nach neuen Episoden nach diesem Datum",
|
"LabelLookForNewEpisodesAfterDate": "Suchen nach neuen Episoden nach diesem Datum",
|
||||||
"LabelMarkSeries": "Serien markieren als",
|
|
||||||
"LabelMediaPlayer": "Mediaplayer",
|
"LabelMediaPlayer": "Mediaplayer",
|
||||||
"LabelMediaType": "Medientyp",
|
"LabelMediaType": "Medientyp",
|
||||||
"LabelMetadataProvider": "Metadatenanbieter",
|
"LabelMetadataProvider": "Metadatenanbieter",
|
||||||
@@ -306,6 +308,7 @@
|
|||||||
"LabelPublishYear": "Jahr",
|
"LabelPublishYear": "Jahr",
|
||||||
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
|
||||||
"LabelRecentSeries": "Aktuelle Serien",
|
"LabelRecentSeries": "Aktuelle Serien",
|
||||||
|
"LabelRecommended": "Recommended",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Veröffentlichungsdatum",
|
"LabelReleaseDate": "Veröffentlichungsdatum",
|
||||||
"LabelRemoveCover": "Lösche Titelbild",
|
"LabelRemoveCover": "Lösche Titelbild",
|
||||||
@@ -331,29 +334,29 @@
|
|||||||
"LabelSettingsExperimentalFeatures": "Experimentelle Funktionen",
|
"LabelSettingsExperimentalFeatures": "Experimentelle Funktionen",
|
||||||
"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 Hörbuch 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": "Starseite 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",
|
||||||
"LabelSettingsParseSubtitles": "Analysiere Untertitel",
|
"LabelSettingsParseSubtitles": "Analysiere Untertitel",
|
||||||
"LabelSettingsParseSubtitlesHelp": "Extrahiere den Untertitel von Hörbuchordnernamen.<br>Untertitel müssen vom eigentlichem Titel durch ein \" - \" getrennt sein. <br>Beispiel: \"Titel - Untertitel\"",
|
"LabelSettingsParseSubtitlesHelp": "Extrahiere den Untertitel von Medium-Ordnernamen.<br>Untertitel müssen vom eigentlichem Titel durch ein \" - \" getrennt sein. <br>Beispiel: \"Titel - Untertitel\"",
|
||||||
"LabelSettingsPreferAudioMetadata": "Bevorzuge lokale ID3-Audiometadaten",
|
"LabelSettingsPreferAudioMetadata": "Bevorzuge lokale ID3-Audiometadaten",
|
||||||
"LabelSettingsPreferAudioMetadataHelp": "In den Audiodateien eingebettete ID3 Metadaten werden für die Metadaten eines Hörbuchs anstelle der Ordnernamen verwendet. Wenn keine ID3 Metadaten zur Verfügung stehen, werden die Ordnernamen verwendet.",
|
"LabelSettingsPreferAudioMetadataHelp": "In den Audiodateien eingebettete ID3 Tags werden anstelle der Ordnernamen für die Bereitstellung der Metadaten verwendet. Wenn keine ID3 Tags zur Verfügung stehen, werden die Ordnernamen verwendet.",
|
||||||
"LabelSettingsPreferMatchedMetadata": "Bevorzuge online abgestimmte Metadaten",
|
"LabelSettingsPreferMatchedMetadata": "Bevorzuge online abgestimmte Metadaten",
|
||||||
"LabelSettingsPreferMatchedMetadataHelp": "Bei einem Schnellabgleich überschreiben neu abgestimmte online Metadaten alle schon vorhandenen Metadaten eines Hörbuchs. Standardmäßig werden bei einem Schnellabgleich nur fehlende Metadaten ersetzt.",
|
"LabelSettingsPreferMatchedMetadataHelp": "Bei einem Schnellabgleich überschreiben online neu abgestimmte Metadaten alle schon vorhandenen Metadaten eines Mediums. Standardmäßig werden bei einem Schnellabgleich nur fehlende Metadaten ersetzt.",
|
||||||
"LabelSettingsPreferOPFMetadata": "Bevorzuge OPF-Metadaten aus dem Hörbuchordner",
|
"LabelSettingsPreferOPFMetadata": "Bevorzuge OPF-Metadaten",
|
||||||
"LabelSettingsPreferOPFMetadataHelp": "In OPF-Dateien gespeicherte Metadaten werden anstelle der Ordnernamen für die Bereitstellung der Metadaten eines Hörbuchs verwendet. OPF-Datein sind seperate \"Textdateien\" mit der Endung \".abs\" welche in dem gleichen Ordner liegen wie das Hörbuch selber. In dieser sind verschiedene Matadaten (z.B. Titel, Autor, Jahr, Erzähler, Handlung, ISBN, ...) gespeichert. Wenn keine OPF Datei zur Verfügung steht, wird standardmäßig der Ordnername verwendet.",
|
"LabelSettingsPreferOPFMetadataHelp": "In OPF-Dateien gespeicherte Metadaten werden anstelle der Ordnernamen für die Bereitstellung der Metadaten verwendet. OPF-Datein sind seperate \"Textdateien\" mit der Endung \".abs\" welche in dem gleichen Ordner liegen wie das Medium selber. In dieser sind verschiedene Metadaten (z.B. Titel, Autor, Jahr, Erzähler, Handlung, ISBN, ...) gespeichert. Wenn keine OPF Datei zur Verfügung steht, wird der Ordnername verwendet.",
|
||||||
"LabelSettingsSkipMatchingBooksWithASIN": "Überspringe beim Online-Abgleich alle Bücher die bereits eine ASIN haben",
|
"LabelSettingsSkipMatchingBooksWithASIN": "Überspringe beim Online-Abgleich alle Bücher die bereits eine ASIN haben",
|
||||||
"LabelSettingsSkipMatchingBooksWithISBN": "Überspringe beim Online-Abgleich alle Bücher die bereits eine ISBN haben",
|
"LabelSettingsSkipMatchingBooksWithISBN": "Überspringe beim Online-Abgleich alle Bücher die bereits eine ISBN haben",
|
||||||
"LabelSettingsSortingIgnorePrefixes": "Vorwort/Artikel beim Sortieren ignorieren",
|
"LabelSettingsSortingIgnorePrefixes": "Vorwort/Artikel beim Sortieren ignorieren",
|
||||||
"LabelSettingsSortingIgnorePrefixesHelp": "Beispiel: für den Artikel \"der\" würde der Hörbuchtitel \"Der Buchtitel\" als \"Buchtitel, Der\" sortiert werden.",
|
"LabelSettingsSortingIgnorePrefixesHelp": "Beispiel: für den Artikel \"der\" würde der Mediumtitel \"Der Buchtitel\" als \"Buchtitel, Der\" sortiert werden.",
|
||||||
"LabelSettingsSquareBookCovers": "Benutze quadratische Titelbilder",
|
"LabelSettingsSquareBookCovers": "Benutze quadratische Titelbilder",
|
||||||
"LabelSettingsSquareBookCoversHelp": "Bevorzugen quadratische Titelbilder gegenüber den Standardtielbildern im Verhältnis 1,6:1",
|
"LabelSettingsSquareBookCoversHelp": "Bevorzugen quadratische Titelbilder gegenüber den Standardtielbildern im Verhältnis 1,6:1",
|
||||||
"LabelSettingsStoreCoversWithItem": "Titelbilder im Hörbuchordner speichern",
|
"LabelSettingsStoreCoversWithItem": "Titelbilder im Medienordner speichern",
|
||||||
"LabelSettingsStoreCoversWithItemHelp": "Standardmäßig werden die Titelbilder in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Titelbilder als jpg Datei in dem gleichen Ordner gespeichert in welchem sich auch das Hörbuch befindet. Es wird immer nur eine Datei mit dem Namen \"cover.jpg\" gespeichert.",
|
"LabelSettingsStoreCoversWithItemHelp": "Standardmäßig werden die Titelbilder in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Titelbilder als jpg Datei in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"cover.jpg\" gespeichert.",
|
||||||
"LabelSettingsStoreMetadataWithItem": "Metadaten als OPF-Datei im Hörbuchordner speichern",
|
"LabelSettingsStoreMetadataWithItem": "Metadaten als OPF-Datei im Medienordner speichern",
|
||||||
"LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Hörbuch befindet. Es wird immer nur eine Datei mit dem Namen \"matadata.abs\" gespeichert.",
|
"LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"matadata.abs\" gespeichert.",
|
||||||
"LabelShowAll": "Alles anzeigen",
|
"LabelShowAll": "Alles anzeigen",
|
||||||
"LabelSize": "Größe",
|
"LabelSize": "Größe",
|
||||||
"LabelSleepTimer": "Einschlaf-Timer",
|
"LabelSleepTimer": "Einschlaf-Timer",
|
||||||
@@ -369,7 +372,7 @@
|
|||||||
"LabelStatsDaysListened": "Gehörte Tage",
|
"LabelStatsDaysListened": "Gehörte Tage",
|
||||||
"LabelStatsHours": "Stunden",
|
"LabelStatsHours": "Stunden",
|
||||||
"LabelStatsInARow": "nacheinander",
|
"LabelStatsInARow": "nacheinander",
|
||||||
"LabelStatsItemsFinished": "Gehörte Hörbücher/Podcasts",
|
"LabelStatsItemsFinished": "Gehörte Medien",
|
||||||
"LabelStatsItemsInLibrary": "Bibliothekseinträge",
|
"LabelStatsItemsInLibrary": "Bibliothekseinträge",
|
||||||
"LabelStatsMinutes": "Minuten",
|
"LabelStatsMinutes": "Minuten",
|
||||||
"LabelStatsMinutesListening": "Gehörte Minuten",
|
"LabelStatsMinutesListening": "Gehörte Minuten",
|
||||||
@@ -388,10 +391,10 @@
|
|||||||
"LabelTitle": "Titel",
|
"LabelTitle": "Titel",
|
||||||
"LabelToolsEmbedMetadata": "Metadaten einbetten",
|
"LabelToolsEmbedMetadata": "Metadaten einbetten",
|
||||||
"LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodatein ein.",
|
"LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodatein ein.",
|
||||||
"LabelToolsMakeM4b": "M4B-Hörbuchdatei erstellen",
|
"LabelToolsMakeM4b": "M4B-Datei erstellen",
|
||||||
"LabelToolsMakeM4bDescription": "Erstellt eine M4B-Hörbuchdatei mit eingebetteten Metadaten, Titelbild und Kapiteln.",
|
"LabelToolsMakeM4bDescription": "Erstellt eine M4B-Datei (Endung \".m4b\") welche mehrere mp3-Dateien in einer einzigen Datei inkl. derer Metadaten (Beschreibung, Titelbild, Kapitel, ....) zusammenfasst. M4B-Datei können darüber hinaus Lesezeichen speichern und mit einem Abspielschutz (Passwort) versehen werden.",
|
||||||
"LabelToolsSplitM4b": "M4B in MP3's aufteilen",
|
"LabelToolsSplitM4b": "M4B in MP3's aufteilen",
|
||||||
"LabelToolsSplitM4bDescription": "Erstellt aus einer mit Metadaten und nach Kapiteln aufgeteilten M4B-Hörbuchdastei seperate MP3's mit eingebetteten Metadaten, Coverbild und Kapiteln.",
|
"LabelToolsSplitM4bDescription": "Erstellt aus einer mit Metadaten und nach Kapiteln aufgeteilten M4B-Datei seperate MP3's mit eingebetteten Metadaten, Coverbild und Kapiteln.",
|
||||||
"LabelTotalDuration": "Gesamtdauer",
|
"LabelTotalDuration": "Gesamtdauer",
|
||||||
"LabelTotalTimeListened": "Gehörte Gesamtzeit",
|
"LabelTotalTimeListened": "Gehörte Gesamtzeit",
|
||||||
"LabelTrackFromFilename": "Titel von Dateiname",
|
"LabelTrackFromFilename": "Titel von Dateiname",
|
||||||
@@ -419,28 +422,30 @@
|
|||||||
"LabelViewQueue": "Spieler-Warteschlange anzeigen",
|
"LabelViewQueue": "Spieler-Warteschlange anzeigen",
|
||||||
"LabelVolume": "Volume",
|
"LabelVolume": "Volume",
|
||||||
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
|
||||||
"LabelYourAudiobookDuration": "Laufzeit Ihres Hörbuchs",
|
"LabelYourAudiobookDuration": "Laufzeit Ihres Mediums",
|
||||||
"LabelYourBookmarks": "Lesezeichen",
|
"LabelYourBookmarks": "Lesezeichen",
|
||||||
"LabelYourPlaylists": "Eigene Wiedergabelisten",
|
"LabelYourPlaylists": "Eigene Wiedergabelisten",
|
||||||
"LabelYourProgress": "Fortschritt",
|
"LabelYourProgress": "Fortschritt",
|
||||||
"MessageAddToPlayerQueue": "Zur Abspielwarteliste hinzufügen",
|
"MessageAddToPlayerQueue": "Zur Abspielwarteliste hinzufügen",
|
||||||
"MessageAppriseDescription": "Um diese Funktion nutzen zu können, müssen Sie eine Instanz von <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> laufen haben oder eine API verwenden welche dieselbe Anfragen bearbeiten kann. <br />Die Apprise API Url muss der vollständige URL-Pfad sein, an den die Benachrichtigung gesendet werden soll, z.B. wenn Ihre API-Instanz unter <code>http://192.168.1.1:8337</code> läuft, würden Sie <code>http://192.168.1.1:8337/notify</code> eingeben.",
|
"MessageAppriseDescription": "Um diese Funktion nutzen zu können, müssen Sie eine Instanz von <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> laufen haben oder eine API verwenden welche dieselbe Anfragen bearbeiten kann. <br />Die Apprise API Url muss der vollständige URL-Pfad sein, an den die Benachrichtigung gesendet werden soll, z.B. wenn Ihre API-Instanz unter <code>http://192.168.1.1:8337</code> läuft, würden Sie <code>http://192.168.1.1:8337/notify</code> eingeben.",
|
||||||
"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 (Hörbuch-/Podcastordnern) 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": "Das Kapitelende liegt nach dem Ende Ihres Hörbuchs",
|
"MessageChapterEndIsAfter": "Ungültige Kapitelendzeit: Kapitelende > Mediumende (Kapitelende liegt nach dem Ende des Mediums)",
|
||||||
"MessageChapterErrorFirstNotZero": "Das erste Kapitel muss bei 0 beginnen",
|
"MessageChapterErrorFirstNotZero": "Ungültige Kapitelstartzeit: Das erste Kapitel muss bei 0 beginnen",
|
||||||
"MessageChapterErrorStartGteDuration": "Die ungültige Startzeit darf nicht größer als die gesamte Hörbuchdauer sein",
|
"MessageChapterErrorStartGteDuration": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumlänge (Kapitelanfang liegt zeitlich nach dem Ende des Mediums -> Lösung: Kapitelanfang < Mediumlänge)",
|
||||||
"MessageChapterErrorStartLtPrev": "Die ungültige Startzeit darf nicht größer oder gleich der Startzeit des vorherigen Kapitels sein",
|
"MessageChapterErrorStartLtPrev": "Ungültige Kapitelstartzeit: Kapitelanfang < Kapitelanfang vorheriges Kapitel (Kapitelanfang liegt zeitlich vor dem Beginn des vorherigen Kapitels -> Lösung: Kapitelanfang >= Startzeit des vorherigen Kapitels)",
|
||||||
"MessageChapterStartIsAfter": "Der Kapitelanfang liegt nach dem Ende Ihres Hörbuchs",
|
"MessageChapterStartIsAfter": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumende (Kapitelanfang liegt nach dem Ende des Mediums)",
|
||||||
"MessageCheckingCron": "Überprüfe cron...",
|
"MessageCheckingCron": "Überprüfe cron...",
|
||||||
"MessageConfirmDeleteBackup": "Sind Sie sicher, dass Sie die Sicherung für {0} löschen wollen?",
|
"MessageConfirmDeleteBackup": "Sind Sie sicher, dass Sie die Sicherung für {0} löschen wollen?",
|
||||||
"MessageConfirmDeleteLibrary": "Sind Sie sicher, dass Sie die Bibliothek \"{0}\" dauerhaft löschen wollen?",
|
"MessageConfirmDeleteLibrary": "Sind Sie sicher, dass Sie die Bibliothek \"{0}\" dauerhaft löschen wollen?",
|
||||||
"MessageConfirmDeleteSession": "Sind Sie sicher, dass Sie diese Sitzung löschen möchten?",
|
"MessageConfirmDeleteSession": "Sind Sie sicher, dass Sie diese Sitzung löschen möchten?",
|
||||||
"MessageConfirmForceReScan": "Sind Sie sicher, dass Sie einen erneuten Scanvorgang erzwingen wollen?",
|
"MessageConfirmForceReScan": "Sind Sie sicher, dass Sie einen erneuten Scanvorgang erzwingen wollen?",
|
||||||
|
"MessageConfirmMarkSeriesFinished": "Sind Sie sicher, dass Sie alle Medien dieser Reihe als abgeschlossen markieren wollen?",
|
||||||
|
"MessageConfirmMarkSeriesNotFinished": "Sind Sie sicher, dass Sie alle Medien dieser Reihe als nicht abgeschlossen markieren wollen?",
|
||||||
"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?",
|
||||||
@@ -460,15 +465,15 @@
|
|||||||
"MessageForceReScanDescription": "durchsucht alle Dateien neu, wie bei einem frischen Scan. ID3-Tags von Audiodateien, OPF-Dateien und Textdateien werden neu durchsucht.",
|
"MessageForceReScanDescription": "durchsucht alle Dateien neu, wie bei einem frischen Scan. ID3-Tags von Audiodateien, OPF-Dateien und Textdateien werden neu durchsucht.",
|
||||||
"MessageImportantNotice": "Wichtiger Hinweis!",
|
"MessageImportantNotice": "Wichtiger Hinweis!",
|
||||||
"MessageInsertChapterBelow": "Kapitel unten einfügen",
|
"MessageInsertChapterBelow": "Kapitel unten einfügen",
|
||||||
"MessageItemsSelected": "{0} ausgewählte Elemente",
|
"MessageItemsSelected": "{0} ausgewählte Medien",
|
||||||
"MessageItemsUpdated": "{0} Hörbüch(er)/Podcast(s) aktualisiert",
|
"MessageItemsUpdated": "{0} Medien aktualisiert",
|
||||||
"MessageJoinUsOn": "Besuchen Sie uns auf",
|
"MessageJoinUsOn": "Besuchen Sie uns auf",
|
||||||
"MessageListeningSessionsInTheLastYear": "{0} Ereignisse im letzten Jahr",
|
"MessageListeningSessionsInTheLastYear": "{0} Ereignisse im letzten Jahr",
|
||||||
"MessageLoading": "Laden...",
|
"MessageLoading": "Laden...",
|
||||||
"MessageLoadingFolders": "Lade Ordner...",
|
"MessageLoadingFolders": "Lade Ordner...",
|
||||||
"MessageM4BFailed": "M4B fehlgeschlagen!",
|
"MessageM4BFailed": "M4B fehlgeschlagen!",
|
||||||
"MessageM4BFinished": "M4B beendet!",
|
"MessageM4BFinished": "M4B beendet!",
|
||||||
"MessageMapChapterTitles": "Zuordnen von Kapiteltiteln zu Ihren vorhandenen Hörbuchkapiteln ohne Anpassung der Zeitangaben",
|
"MessageMapChapterTitles": "Zuordnen von Kapiteltiteln zu Ihren vorhandenen Medienkapiteln ohne Anpassung der Zeitangaben",
|
||||||
"MessageMarkAsFinished": "Als beendet markieren",
|
"MessageMarkAsFinished": "Als beendet markieren",
|
||||||
"MessageMarkAsNotFinished": "Als nicht abgeschlossen markieren",
|
"MessageMarkAsNotFinished": "Als nicht abgeschlossen markieren",
|
||||||
"MessageMatchBooksDescription": "versucht, Bücher in der Bibliothek mit einem Buch des ausgewählten Suchanbieters abzugleichen und leere Details und das Titelbild auszufüllen. Details werden nicht überschrieben.",
|
"MessageMatchBooksDescription": "versucht, Bücher in der Bibliothek mit einem Buch des ausgewählten Suchanbieters abzugleichen und leere Details und das Titelbild auszufüllen. Details werden nicht überschrieben.",
|
||||||
@@ -485,8 +490,8 @@
|
|||||||
"MessageNoFoldersAvailable": "Keine Ordner verfügbar",
|
"MessageNoFoldersAvailable": "Keine Ordner verfügbar",
|
||||||
"MessageNoGenres": "Keine Kategorien",
|
"MessageNoGenres": "Keine Kategorien",
|
||||||
"MessageNoIssues": "Keine Probleme",
|
"MessageNoIssues": "Keine Probleme",
|
||||||
"MessageNoItems": "Keine Elemente/Einträge",
|
"MessageNoItems": "Keine Medien",
|
||||||
"MessageNoItemsFound": "Keine Elemente/Einträge gefunden",
|
"MessageNoItemsFound": "Keine Medien gefunden",
|
||||||
"MessageNoListeningSessions": "Keine Hörsitzungen",
|
"MessageNoListeningSessions": "Keine Hörsitzungen",
|
||||||
"MessageNoLogs": "Keine Protokolle",
|
"MessageNoLogs": "Keine Protokolle",
|
||||||
"MessageNoMediaProgress": "Kein Medienfortschritt",
|
"MessageNoMediaProgress": "Kein Medienfortschritt",
|
||||||
@@ -526,16 +531,16 @@
|
|||||||
"MessageValidCronExpression": "Gültiger cron-ausdruck",
|
"MessageValidCronExpression": "Gültiger cron-ausdruck",
|
||||||
"MessageWatcherIsDisabledGlobally": "Überwachung ist in den Servereinstellungen global deaktiviert",
|
"MessageWatcherIsDisabledGlobally": "Überwachung ist in den Servereinstellungen global deaktiviert",
|
||||||
"MessageXLibraryIsEmpty": "{0} Bibliothek ist leer!",
|
"MessageXLibraryIsEmpty": "{0} Bibliothek ist leer!",
|
||||||
"MessageYourAudiobookDurationIsLonger": "Die Dauer Ihres Hörbuchs ist länger als die gefundene Dauer",
|
"MessageYourAudiobookDurationIsLonger": "Die Dauer Ihres Mediums ist länger als die gefundene Dauer",
|
||||||
"MessageYourAudiobookDurationIsShorter": "Die Dauer Ihres Hörbuchs ist kürzer als die gefundene Dauer",
|
"MessageYourAudiobookDurationIsShorter": "Die Dauer Ihres Mediums ist kürzer als die gefundene Dauer",
|
||||||
"NoteChangeRootPassword": "Der Root-Benutzer (Hauptbenutzer) ist der einzige Benutzer, der ein leeres Passwort haben kann",
|
"NoteChangeRootPassword": "Der Root-Benutzer (Hauptbenutzer) ist der einzige Benutzer, der ein leeres Passwort haben kann",
|
||||||
"NoteChapterEditorTimes": "Hinweis: Die Anfangszeit des ersten Kapitels muss bei 0:00 beginnen und die Anfangszeit des letzten Kapitels darf die Dauer des Hörbuchs nicht überschreiten.",
|
"NoteChapterEditorTimes": "Hinweis: Die Anfangszeit des ersten Kapitels muss bei 0:00 beginnen und die Anfangszeit des letzten Kapitels darf die Dauer des Mediums nicht überschreiten.",
|
||||||
"NoteFolderPicker": "Hinweis: Bereits zugeordnete Ordner werden nicht angezeigt.",
|
"NoteFolderPicker": "Hinweis: Bereits zugeordnete Ordner werden nicht angezeigt.",
|
||||||
"NoteFolderPickerDebian": "Hinweis: Der Ordnerauswahldialog für die Debian-Installation ist nicht vollständig implementiert. Sie sollten den Pfad zu Ihrer Bibliothek direkt eingeben.",
|
"NoteFolderPickerDebian": "Hinweis: Der Ordnerauswahldialog für die Debian-Installation ist nicht vollständig implementiert. Sie sollten den Pfad zu Ihrer Bibliothek direkt eingeben.",
|
||||||
"NoteRSSFeedPodcastAppsHttps": "Warnung: Die meisten Podcast-Apps verlangen, dass die URL des RSS-Feeds HTTPS verwendet.",
|
"NoteRSSFeedPodcastAppsHttps": "Warnung: Die meisten Podcast-Apps verlangen, dass die URL des RSS-Feeds HTTPS verwendet.",
|
||||||
"NoteRSSFeedPodcastAppsPubDate": "Warnung: 1 oder mehrere Ihrer Episoden haben kein Veröffentlichungsdatum. Einige Podcast-Apps verlangen dies.",
|
"NoteRSSFeedPodcastAppsPubDate": "Warnung: 1 oder mehrere Ihrer Episoden haben kein Veröffentlichungsdatum. Einige Podcast-Apps verlangen dies.",
|
||||||
"NoteUploaderFoldersWithMediaFiles": "Ordner mit Mediendateien werden als separate Bibliothekselemente behandelt.",
|
"NoteUploaderFoldersWithMediaFiles": "Ordner mit Mediendateien werden als separate Bibliothekselemente behandelt.",
|
||||||
"NoteUploaderOnlyAudioFiles": "Wenn Sie nur Audiodateien hochladen, wird jede Audiodatei als ein separates Hörbuch behandelt.",
|
"NoteUploaderOnlyAudioFiles": "Wenn Sie nur Audiodateien hochladen, wird jede Audiodatei als ein separates Medium behandelt.",
|
||||||
"NoteUploaderUnsupportedFiles": "Nicht unterstützte Dateien werden ignoriert. Bei der Auswahl oder dem Löschen eines Ordners werden andere Dateien, die sich nicht in einem Elementordner befinden, ignoriert.",
|
"NoteUploaderUnsupportedFiles": "Nicht unterstützte Dateien werden ignoriert. Bei der Auswahl oder dem Löschen eines Ordners werden andere Dateien, die sich nicht in einem Elementordner befinden, ignoriert.",
|
||||||
"PlaceholderNewCollection": "Neuer Sammlungsname",
|
"PlaceholderNewCollection": "Neuer Sammlungsname",
|
||||||
"PlaceholderNewFolderPath": "Neuer Ordnerpfad",
|
"PlaceholderNewFolderPath": "Neuer Ordnerpfad",
|
||||||
@@ -566,21 +571,21 @@
|
|||||||
"ToastBookmarkUpdateSuccess": "Lesezeichen aktualisiert",
|
"ToastBookmarkUpdateSuccess": "Lesezeichen aktualisiert",
|
||||||
"ToastChaptersHaveErrors": "Kapitel sind fehlerhaft",
|
"ToastChaptersHaveErrors": "Kapitel sind fehlerhaft",
|
||||||
"ToastChaptersMustHaveTitles": "Kapitel benötigen eindeutige Namen",
|
"ToastChaptersMustHaveTitles": "Kapitel benötigen eindeutige Namen",
|
||||||
"ToastCollectionItemsRemoveFailed": "Element(e) konnte(n) nicht aus der Sammlung entfernt werden",
|
"ToastCollectionItemsRemoveFailed": "Fehler beim Entfernen der Medien aus der Sammlung",
|
||||||
"ToastCollectionItemsRemoveSuccess": "Element(e) wurde(n) aus der Sammlung entfernt",
|
"ToastCollectionItemsRemoveSuccess": "Medien aus der Sammlung entfernt",
|
||||||
"ToastCollectionRemoveFailed": "Sammlung konnte nicht entfernt werden",
|
"ToastCollectionRemoveFailed": "Sammlung konnte nicht entfernt werden",
|
||||||
"ToastCollectionRemoveSuccess": "Sammlung gelöscht",
|
"ToastCollectionRemoveSuccess": "Sammlung gelöscht",
|
||||||
"ToastCollectionUpdateFailed": "Sammlung konnte nicht aktualisiert werden",
|
"ToastCollectionUpdateFailed": "Sammlung konnte nicht aktualisiert werden",
|
||||||
"ToastCollectionUpdateSuccess": "Sammlung aktualisiert",
|
"ToastCollectionUpdateSuccess": "Sammlung aktualisiert",
|
||||||
"ToastItemCoverUpdateFailed": "Aktualisierung des Titelbildes fehlgeschlagen",
|
"ToastItemCoverUpdateFailed": "Fehler bei der Aktualisierung des Titelbildes",
|
||||||
"ToastItemCoverUpdateSuccess": "Titelbild aktualisiert",
|
"ToastItemCoverUpdateSuccess": "Titelbild aktualisiert",
|
||||||
"ToastItemDetailsUpdateFailed": "Fehler bei der Aktualisierung der Artikeldetails",
|
"ToastItemDetailsUpdateFailed": "Fehler bei der Aktualisierung der Artikeldetails",
|
||||||
"ToastItemDetailsUpdateSuccess": "Artikeldetails aktualisiert",
|
"ToastItemDetailsUpdateSuccess": "Artikeldetails aktualisiert",
|
||||||
"ToastItemDetailsUpdateUnneeded": "Keine Aktualisierung für Artikeldetails erforderlichs",
|
"ToastItemDetailsUpdateUnneeded": "Keine Aktualisierung für die Artikeldetails erforderlich",
|
||||||
"ToastItemMarkedAsFinishedFailed": "Als \"abgeschlossen zu markieren\" ist fehlgeschlagen",
|
"ToastItemMarkedAsFinishedFailed": "Fehler bei der Markierung des Mediums als \"Beendet\"",
|
||||||
"ToastItemMarkedAsFinishedSuccess": "Artikel/Eintrag als fertig markiert",
|
"ToastItemMarkedAsFinishedSuccess": "Medium als \"Beendet\" markiert",
|
||||||
"ToastItemMarkedAsNotFinishedFailed": "Fehler bei der Markierung als \"Nicht Fertig\"",
|
"ToastItemMarkedAsNotFinishedFailed": "Fehler bei der Markierung des Mediums als \"Nicht Beendet\"",
|
||||||
"ToastItemMarkedAsNotFinishedSuccess": "Artikel/Eintrag als \"Nicht Fertig\" markiert",
|
"ToastItemMarkedAsNotFinishedSuccess": "Medium als \"Nicht Beendet\" markiert",
|
||||||
"ToastLibraryCreateFailed": "Bibliothek konnte nicht erstellt werden",
|
"ToastLibraryCreateFailed": "Bibliothek konnte nicht erstellt werden",
|
||||||
"ToastLibraryCreateSuccess": "Bibliothek \"{0}\" erstellt",
|
"ToastLibraryCreateSuccess": "Bibliothek \"{0}\" erstellt",
|
||||||
"ToastLibraryDeleteFailed": "Bibliothek konnte nicht gelöscht werden",
|
"ToastLibraryDeleteFailed": "Bibliothek konnte nicht gelöscht werden",
|
||||||
@@ -597,22 +602,17 @@
|
|||||||
"ToastPlaylistUpdateSuccess": "Wiedergabeliste aktualisiert",
|
"ToastPlaylistUpdateSuccess": "Wiedergabeliste aktualisiert",
|
||||||
"ToastPodcastCreateFailed": "Podcast konnte nicht erstellt werden",
|
"ToastPodcastCreateFailed": "Podcast konnte nicht erstellt werden",
|
||||||
"ToastPodcastCreateSuccess": "Podcast erstellt",
|
"ToastPodcastCreateSuccess": "Podcast erstellt",
|
||||||
"ToastRemoveItemFromCollectionFailed": "Löschen des Hörbuchs/Podcasts aus der Sammlung fehlgeschlagen",
|
"ToastRemoveItemFromCollectionFailed": "Löschen des Mediums aus der Sammlung fehlgeschlagen",
|
||||||
"ToastRemoveItemFromCollectionSuccess": "Hörbuch/Podcast 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",
|
||||||
|
"ToastSeriesUpdateFailed": "Aktualisierung der Serien fehlgeschlagen",
|
||||||
|
"ToastSeriesUpdateSuccess": "Serien aktualisiert",
|
||||||
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden",
|
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden",
|
||||||
"ToastSessionDeleteSuccess": "Sitzung gelöscht",
|
"ToastSessionDeleteSuccess": "Sitzung gelöscht",
|
||||||
"ToastSocketConnected": "Verbindung zum WebSocket hergestellt",
|
"ToastSocketConnected": "Verbindung zum WebSocket hergestellt",
|
||||||
"ToastSocketDisconnected": "Verbindung zum WebSocket verloren",
|
"ToastSocketDisconnected": "Verbindung zum WebSocket verloren",
|
||||||
"ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen",
|
"ToastSocketFailedToConnect": "Verbindung zum WebSocket fehlgeschlagen",
|
||||||
"ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden",
|
"ToastUserDeleteFailed": "Benutzer konnte nicht gelöscht werden",
|
||||||
"ToastUserDeleteSuccess": "Benutzer gelöscht",
|
"ToastUserDeleteSuccess": "Benutzer gelöscht"
|
||||||
"WeekdayFriday": "Freitag",
|
}
|
||||||
"WeekdayMonday": "Montag",
|
|
||||||
"WeekdaySaturday": "Samstag",
|
|
||||||
"WeekdaySunday": "Sonntag",
|
|
||||||
"WeekdayThursday": "Donnerstag",
|
|
||||||
"WeekdayTuesday": "Dienstag",
|
|
||||||
"WeekdayWednesday": "Mittwoch"
|
|
||||||
}
|
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
"ButtonCreate": "Create",
|
"ButtonCreate": "Create",
|
||||||
"ButtonCreateBackup": "Create Backup",
|
"ButtonCreateBackup": "Create Backup",
|
||||||
"ButtonDelete": "Delete",
|
"ButtonDelete": "Delete",
|
||||||
|
"ButtonEdit": "Edit",
|
||||||
"ButtonEditChapters": "Edit Chapters",
|
"ButtonEditChapters": "Edit Chapters",
|
||||||
"ButtonEditPodcast": "Edit Podcast",
|
"ButtonEditPodcast": "Edit Podcast",
|
||||||
"ButtonForceReScan": "Force Re-Scan",
|
"ButtonForceReScan": "Force Re-Scan",
|
||||||
@@ -75,6 +76,8 @@
|
|||||||
"ButtonUploadBackup": "Upload Backup",
|
"ButtonUploadBackup": "Upload Backup",
|
||||||
"ButtonUploadCover": "Upload Cover",
|
"ButtonUploadCover": "Upload Cover",
|
||||||
"ButtonUploadOPMLFile": "Upload OPML File",
|
"ButtonUploadOPMLFile": "Upload OPML File",
|
||||||
|
"ButtonUserDelete": "Delete user {0}",
|
||||||
|
"ButtonUserEdit": "Edit user {0}",
|
||||||
"ButtonViewAll": "View All",
|
"ButtonViewAll": "View All",
|
||||||
"ButtonYes": "Yes",
|
"ButtonYes": "Yes",
|
||||||
"HeaderAccount": "Account",
|
"HeaderAccount": "Account",
|
||||||
@@ -252,7 +255,6 @@
|
|||||||
"LabelLogLevelInfo": "Info",
|
"LabelLogLevelInfo": "Info",
|
||||||
"LabelLogLevelWarn": "Warn",
|
"LabelLogLevelWarn": "Warn",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
|
"LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
|
||||||
"LabelMarkSeries": "Mark Series",
|
|
||||||
"LabelMediaPlayer": "Media Player",
|
"LabelMediaPlayer": "Media Player",
|
||||||
"LabelMediaType": "Media Type",
|
"LabelMediaType": "Media Type",
|
||||||
"LabelMetadataProvider": "Metadata Provider",
|
"LabelMetadataProvider": "Metadata Provider",
|
||||||
@@ -306,6 +308,7 @@
|
|||||||
"LabelPublishYear": "Publish Year",
|
"LabelPublishYear": "Publish Year",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
|
"LabelRecommended": "Recommended",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Release Date",
|
"LabelReleaseDate": "Release Date",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Remove cover",
|
||||||
@@ -441,6 +444,8 @@
|
|||||||
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
||||||
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
|
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
|
||||||
|
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||||
|
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||||
"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?",
|
||||||
@@ -601,18 +606,13 @@
|
|||||||
"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",
|
||||||
|
"ToastSeriesUpdateFailed": "Series update failed",
|
||||||
|
"ToastSeriesUpdateSuccess": "Series update success",
|
||||||
"ToastSessionDeleteFailed": "Failed to delete session",
|
"ToastSessionDeleteFailed": "Failed to delete session",
|
||||||
"ToastSessionDeleteSuccess": "Session deleted",
|
"ToastSessionDeleteSuccess": "Session deleted",
|
||||||
"ToastSocketConnected": "Socket connected",
|
"ToastSocketConnected": "Socket connected",
|
||||||
"ToastSocketDisconnected": "Socket disconnected",
|
"ToastSocketDisconnected": "Socket disconnected",
|
||||||
"ToastSocketFailedToConnect": "Socket failed to connect",
|
"ToastSocketFailedToConnect": "Socket failed to connect",
|
||||||
"ToastUserDeleteFailed": "Failed to delete user",
|
"ToastUserDeleteFailed": "Failed to delete user",
|
||||||
"ToastUserDeleteSuccess": "User deleted",
|
"ToastUserDeleteSuccess": "User deleted"
|
||||||
"WeekdayFriday": "Friday",
|
|
||||||
"WeekdayMonday": "Monday",
|
|
||||||
"WeekdaySaturday": "Saturday",
|
|
||||||
"WeekdaySunday": "Sunday",
|
|
||||||
"WeekdayThursday": "Thursday",
|
|
||||||
"WeekdayTuesday": "Tuesday",
|
|
||||||
"WeekdayWednesday": "Wednesday"
|
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
"ButtonCreate": "Create",
|
"ButtonCreate": "Create",
|
||||||
"ButtonCreateBackup": "Create Backup",
|
"ButtonCreateBackup": "Create Backup",
|
||||||
"ButtonDelete": "Delete",
|
"ButtonDelete": "Delete",
|
||||||
|
"ButtonEdit": "Edit",
|
||||||
"ButtonEditChapters": "Edit Chapters",
|
"ButtonEditChapters": "Edit Chapters",
|
||||||
"ButtonEditPodcast": "Edit Podcast",
|
"ButtonEditPodcast": "Edit Podcast",
|
||||||
"ButtonForceReScan": "Force Re-Scan",
|
"ButtonForceReScan": "Force Re-Scan",
|
||||||
@@ -75,6 +76,8 @@
|
|||||||
"ButtonUploadBackup": "Upload Backup",
|
"ButtonUploadBackup": "Upload Backup",
|
||||||
"ButtonUploadCover": "Upload Cover",
|
"ButtonUploadCover": "Upload Cover",
|
||||||
"ButtonUploadOPMLFile": "Upload OPML File",
|
"ButtonUploadOPMLFile": "Upload OPML File",
|
||||||
|
"ButtonUserDelete": "Delete user {0}",
|
||||||
|
"ButtonUserEdit": "Edit user {0}",
|
||||||
"ButtonViewAll": "View All",
|
"ButtonViewAll": "View All",
|
||||||
"ButtonYes": "Yes",
|
"ButtonYes": "Yes",
|
||||||
"HeaderAccount": "Account",
|
"HeaderAccount": "Account",
|
||||||
@@ -252,7 +255,6 @@
|
|||||||
"LabelLogLevelInfo": "Info",
|
"LabelLogLevelInfo": "Info",
|
||||||
"LabelLogLevelWarn": "Warn",
|
"LabelLogLevelWarn": "Warn",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
|
"LabelLookForNewEpisodesAfterDate": "Look for new episodes after this date",
|
||||||
"LabelMarkSeries": "Mark Series",
|
|
||||||
"LabelMediaPlayer": "Media Player",
|
"LabelMediaPlayer": "Media Player",
|
||||||
"LabelMediaType": "Media Type",
|
"LabelMediaType": "Media Type",
|
||||||
"LabelMetadataProvider": "Metadata Provider",
|
"LabelMetadataProvider": "Metadata Provider",
|
||||||
@@ -306,6 +308,7 @@
|
|||||||
"LabelPublishYear": "Publish Year",
|
"LabelPublishYear": "Publish Year",
|
||||||
"LabelRecentlyAdded": "Recently Added",
|
"LabelRecentlyAdded": "Recently Added",
|
||||||
"LabelRecentSeries": "Recent Series",
|
"LabelRecentSeries": "Recent Series",
|
||||||
|
"LabelRecommended": "Recommended",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Release Date",
|
"LabelReleaseDate": "Release Date",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Remove cover",
|
||||||
@@ -441,6 +444,8 @@
|
|||||||
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
||||||
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
|
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
|
||||||
|
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||||
|
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||||
"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?",
|
||||||
@@ -601,18 +606,13 @@
|
|||||||
"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",
|
||||||
|
"ToastSeriesUpdateFailed": "Series update failed",
|
||||||
|
"ToastSeriesUpdateSuccess": "Series update success",
|
||||||
"ToastSessionDeleteFailed": "Failed to delete session",
|
"ToastSessionDeleteFailed": "Failed to delete session",
|
||||||
"ToastSessionDeleteSuccess": "Session deleted",
|
"ToastSessionDeleteSuccess": "Session deleted",
|
||||||
"ToastSocketConnected": "Socket connected",
|
"ToastSocketConnected": "Socket connected",
|
||||||
"ToastSocketDisconnected": "Socket disconnected",
|
"ToastSocketDisconnected": "Socket disconnected",
|
||||||
"ToastSocketFailedToConnect": "Socket failed to connect",
|
"ToastSocketFailedToConnect": "Socket failed to connect",
|
||||||
"ToastUserDeleteFailed": "Failed to delete user",
|
"ToastUserDeleteFailed": "Failed to delete user",
|
||||||
"ToastUserDeleteSuccess": "User deleted",
|
"ToastUserDeleteSuccess": "User deleted"
|
||||||
"WeekdayFriday": "Friday",
|
|
||||||
"WeekdayMonday": "Monday",
|
|
||||||
"WeekdaySaturday": "Saturday",
|
|
||||||
"WeekdaySunday": "Sunday",
|
|
||||||
"WeekdayThursday": "Thursday",
|
|
||||||
"WeekdayTuesday": "Tuesday",
|
|
||||||
"WeekdayWednesday": "Wednesday"
|
|
||||||
}
|
}
|
||||||
+57
-57
@@ -20,6 +20,7 @@
|
|||||||
"ButtonCreate": "Créer",
|
"ButtonCreate": "Créer",
|
||||||
"ButtonCreateBackup": "Créer une Sauvegarde",
|
"ButtonCreateBackup": "Créer une Sauvegarde",
|
||||||
"ButtonDelete": "Effacer",
|
"ButtonDelete": "Effacer",
|
||||||
|
"ButtonEdit": "Editer",
|
||||||
"ButtonEditChapters": "Editer Chapitre",
|
"ButtonEditChapters": "Editer Chapitre",
|
||||||
"ButtonEditPodcast": "Editer Podcast",
|
"ButtonEditPodcast": "Editer Podcast",
|
||||||
"ButtonForceReScan": "Forcer un Re-Scan",
|
"ButtonForceReScan": "Forcer un Re-Scan",
|
||||||
@@ -50,16 +51,16 @@
|
|||||||
"ButtonQuickMatch": "Recherche Rapide",
|
"ButtonQuickMatch": "Recherche Rapide",
|
||||||
"ButtonRead": "Lire",
|
"ButtonRead": "Lire",
|
||||||
"ButtonRemove": "Supprimer",
|
"ButtonRemove": "Supprimer",
|
||||||
"ButtonRemoveAll": "Supprimer Tout",
|
"ButtonRemoveAll": "Supprimer tout",
|
||||||
"ButtonRemoveAllLibraryItems": "Supprimer Tout les Articles de la Bibliothèque",
|
"ButtonRemoveAllLibraryItems": "Supprimer tous les Articles de la Bibliothèque",
|
||||||
"ButtonRemoveFromContinueListening": "Supprimer de Continuer à Ecouter",
|
"ButtonRemoveFromContinueListening": "Ne plus continuer à écouter",
|
||||||
"ButtonRemoveSeriesFromContinueSeries": "Supprimer la Série de Continuer la Série",
|
"ButtonRemoveSeriesFromContinueSeries": "Ne plus continuer à écouter la Série",
|
||||||
"ButtonReScan": "Re-Scan",
|
"ButtonReScan": "Re-Scan",
|
||||||
"ButtonReset": "Réinitialiser",
|
"ButtonReset": "Réinitialiser",
|
||||||
"ButtonRestore": "Rétablir",
|
"ButtonRestore": "Rétablir",
|
||||||
"ButtonSave": "Sauvegarder",
|
"ButtonSave": "Sauvegarder",
|
||||||
"ButtonSaveAndClose": "Sauvegarder & Fermer",
|
"ButtonSaveAndClose": "Sauvegarder & Fermer",
|
||||||
"ButtonSaveTracklist": "Sauvegarder la Tracklist",
|
"ButtonSaveTracklist": "Sauvegarder la liste de lecture",
|
||||||
"ButtonScan": "Scanner",
|
"ButtonScan": "Scanner",
|
||||||
"ButtonScanLibrary": "Scanner la Bibliothèque",
|
"ButtonScanLibrary": "Scanner la Bibliothèque",
|
||||||
"ButtonSearch": "Rechercher",
|
"ButtonSearch": "Rechercher",
|
||||||
@@ -67,15 +68,17 @@
|
|||||||
"ButtonSeries": "Séries",
|
"ButtonSeries": "Séries",
|
||||||
"ButtonSetChaptersFromTracks": "Positionner les Chapitre par rapports aux Pistes",
|
"ButtonSetChaptersFromTracks": "Positionner les Chapitre par rapports aux Pistes",
|
||||||
"ButtonShiftTimes": "Décaler le Temps",
|
"ButtonShiftTimes": "Décaler le Temps",
|
||||||
"ButtonShow": "Montrer",
|
"ButtonShow": "Afficher",
|
||||||
"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",
|
||||||
"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",
|
||||||
"ButtonUploadOPMLFile": "Téléverser un Fichier OPML",
|
"ButtonUploadOPMLFile": "Téléverser un Fichier OPML",
|
||||||
"ButtonViewAll": "Voir Tout",
|
"ButtonUserDelete": "Effacer l'utilisateur {0}",
|
||||||
|
"ButtonUserEdit": "Modifier l'utilisateur {0}",
|
||||||
|
"ButtonViewAll": "Afficher Tout",
|
||||||
"ButtonYes": "Oui",
|
"ButtonYes": "Oui",
|
||||||
"HeaderAccount": "Compte",
|
"HeaderAccount": "Compte",
|
||||||
"HeaderAdvanced": "Avancé",
|
"HeaderAdvanced": "Avancé",
|
||||||
@@ -83,7 +86,7 @@
|
|||||||
"HeaderAudiobookTools": "Outils de Gestion de Fichier Audiobook",
|
"HeaderAudiobookTools": "Outils de Gestion de Fichier Audiobook",
|
||||||
"HeaderAudioTracks": "Pistes Audio",
|
"HeaderAudioTracks": "Pistes Audio",
|
||||||
"HeaderBackups": "Sauvegardes",
|
"HeaderBackups": "Sauvegardes",
|
||||||
"HeaderChangePassword": "Chager le Mot de Passe",
|
"HeaderChangePassword": "Chager le mot de passe",
|
||||||
"HeaderChapters": "Chapitres",
|
"HeaderChapters": "Chapitres",
|
||||||
"HeaderChooseAFolder": "Choisir un Dossier",
|
"HeaderChooseAFolder": "Choisir un Dossier",
|
||||||
"HeaderCollection": "Collection",
|
"HeaderCollection": "Collection",
|
||||||
@@ -252,7 +255,6 @@
|
|||||||
"LabelLogLevelInfo": "Info",
|
"LabelLogLevelInfo": "Info",
|
||||||
"LabelLogLevelWarn": "Warn",
|
"LabelLogLevelWarn": "Warn",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Rechercher de Nouveaux Episode après cette Date",
|
"LabelLookForNewEpisodesAfterDate": "Rechercher de Nouveaux Episode après cette Date",
|
||||||
"LabelMarkSeries": "Marquer la Série",
|
|
||||||
"LabelMediaPlayer": "Lecteur Multimédia",
|
"LabelMediaPlayer": "Lecteur Multimédia",
|
||||||
"LabelMediaType": "Type de Média",
|
"LabelMediaType": "Type de Média",
|
||||||
"LabelMetadataProvider": "Fournisseur de Métadonnées",
|
"LabelMetadataProvider": "Fournisseur de Métadonnées",
|
||||||
@@ -306,6 +308,7 @@
|
|||||||
"LabelPublishYear": "Année d'Edition",
|
"LabelPublishYear": "Année d'Edition",
|
||||||
"LabelRecentlyAdded": "Derniers Ajouts",
|
"LabelRecentlyAdded": "Derniers Ajouts",
|
||||||
"LabelRecentSeries": "Séries Récentes",
|
"LabelRecentSeries": "Séries Récentes",
|
||||||
|
"LabelRecommended": "Recommended",
|
||||||
"LabelRegion": "Région",
|
"LabelRegion": "Région",
|
||||||
"LabelReleaseDate": "Date de Parution",
|
"LabelReleaseDate": "Date de Parution",
|
||||||
"LabelRemoveCover": "Supprimer la Couverture",
|
"LabelRemoveCover": "Supprimer la Couverture",
|
||||||
@@ -414,9 +417,9 @@
|
|||||||
"LabelUsername": "Nom d'Utilisateur",
|
"LabelUsername": "Nom d'Utilisateur",
|
||||||
"LabelValue": "Valeur",
|
"LabelValue": "Valeur",
|
||||||
"LabelVersion": "Version",
|
"LabelVersion": "Version",
|
||||||
"LabelViewBookmarks": "Voir les Signets",
|
"LabelViewBookmarks": "Afficher les Signets",
|
||||||
"LabelViewChapters": "Voir les Chapitres",
|
"LabelViewChapters": "Afficher les Chapitres",
|
||||||
"LabelViewQueue": "Voir la Liste de Lecture",
|
"LabelViewQueue": "Afficher la Liste de Lecture",
|
||||||
"LabelVolume": "Volume",
|
"LabelVolume": "Volume",
|
||||||
"LabelWeekdaysToRun": "Jours de la semaine à exécuter",
|
"LabelWeekdaysToRun": "Jours de la semaine à exécuter",
|
||||||
"LabelYourAudiobookDuration": "Durée de vos Livres Audios",
|
"LabelYourAudiobookDuration": "Durée de vos Livres Audios",
|
||||||
@@ -441,6 +444,8 @@
|
|||||||
"MessageConfirmDeleteLibrary": "Etes vous certain de vouloir supprimer définitivement la bibliothèque \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Etes vous certain de vouloir supprimer définitivement la bibliothèque \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "Etes vous certain de vouloir supprimer cette session?",
|
"MessageConfirmDeleteSession": "Etes vous certain de vouloir supprimer cette session?",
|
||||||
"MessageConfirmForceReScan": "Etes vous certain de vouloir lancer une Analyse Forcée?",
|
"MessageConfirmForceReScan": "Etes vous certain de vouloir lancer une Analyse Forcée?",
|
||||||
|
"MessageConfirmMarkSeriesFinished": "Etes vous certain de vouloir marquer comme terminé tous les livres de cette série?",
|
||||||
|
"MessageConfirmMarkSeriesNotFinished": "Etes vous certain de vouloir marquer comme non terminé tous les livres de cette série?",
|
||||||
"MessageConfirmRemoveCollection": "Etes vous certain de vouloir supprimer la collection \"{0}\"?",
|
"MessageConfirmRemoveCollection": "Etes vous certain de vouloir supprimer la collection \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "Etes vous certain de vouloir supprimer l'épisode \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "Etes vous certain de vouloir supprimer l'épisode \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "Etes vous certain de vouloir supprimer {0} épisodes?",
|
"MessageConfirmRemoveEpisodes": "Etes vous certain de vouloir supprimer {0} épisodes?",
|
||||||
@@ -519,8 +524,8 @@
|
|||||||
"MessageServerCouldNotBeReached": "Serveur inaccessible",
|
"MessageServerCouldNotBeReached": "Serveur inaccessible",
|
||||||
"MessageSetChaptersFromTracksDescription": "Positionne un chapitre par fichier audio, avec le titre du fichier comme titre de chapitre",
|
"MessageSetChaptersFromTracksDescription": "Positionne un chapitre par fichier audio, avec le titre du fichier comme titre de chapitre",
|
||||||
"MessageStartPlaybackAtTime": "Démarrer la lecture pour \"{0}\" à {1}?",
|
"MessageStartPlaybackAtTime": "Démarrer la lecture pour \"{0}\" à {1}?",
|
||||||
"MessageThinking": "On Réfléchit...",
|
"MessageThinking": "On réfléchit...",
|
||||||
"MessageUploaderItemFailed": "Echec du téléversement",
|
"MessageUploaderItemFailed": "Échec du téléversement",
|
||||||
"MessageUploaderItemSuccess": "Téléversement effectué!",
|
"MessageUploaderItemSuccess": "Téléversement effectué!",
|
||||||
"MessageUploading": "Téléversement...",
|
"MessageUploading": "Téléversement...",
|
||||||
"MessageValidCronExpression": "Expression cron valide",
|
"MessageValidCronExpression": "Expression cron valide",
|
||||||
@@ -541,78 +546,73 @@
|
|||||||
"PlaceholderNewFolderPath": "Nouveau chemin de dossier",
|
"PlaceholderNewFolderPath": "Nouveau chemin de dossier",
|
||||||
"PlaceholderNewPlaylist": "Nouveau nom de liste de lecture",
|
"PlaceholderNewPlaylist": "Nouveau nom de liste de lecture",
|
||||||
"PlaceholderSearch": "Recherche...",
|
"PlaceholderSearch": "Recherche...",
|
||||||
"ToastAccountUpdateFailed": "Echec de la mise à jour du compte",
|
"ToastAccountUpdateFailed": "Échec de la mise à jour du compte",
|
||||||
"ToastAccountUpdateSuccess": "Compte mis à jour",
|
"ToastAccountUpdateSuccess": "Compte mis à jour",
|
||||||
"ToastAuthorImageRemoveFailed": "Echec de la suppression de l'image",
|
"ToastAuthorImageRemoveFailed": "Échec de la suppression de l'image",
|
||||||
"ToastAuthorImageRemoveSuccess": "Image de l'auteur supprimée",
|
"ToastAuthorImageRemoveSuccess": "Image de l'auteur supprimée",
|
||||||
"ToastAuthorUpdateFailed": "Echec de la mise à jour de l'auteur",
|
"ToastAuthorUpdateFailed": "Échec de la mise à jour de l'auteur",
|
||||||
"ToastAuthorUpdateMerged": "Auteur fusionné",
|
"ToastAuthorUpdateMerged": "Auteur fusionné",
|
||||||
"ToastAuthorUpdateSuccess": "Auteur mis à jour",
|
"ToastAuthorUpdateSuccess": "Auteur mis à jour",
|
||||||
"ToastAuthorUpdateSuccessNoImageFound": "Auteur mis à jour (pas d'image trouvée)",
|
"ToastAuthorUpdateSuccessNoImageFound": "Auteur mis à jour (pas d'image trouvée)",
|
||||||
"ToastBackupCreateFailed": "Echec de la création de sauvegarde",
|
"ToastBackupCreateFailed": "Échec de la création de sauvegarde",
|
||||||
"ToastBackupCreateSuccess": "Sauvegarde créée",
|
"ToastBackupCreateSuccess": "Sauvegarde créée",
|
||||||
"ToastBackupDeleteFailed": "Echec de la suppression de sauvegarde",
|
"ToastBackupDeleteFailed": "Échec de la suppression de sauvegarde",
|
||||||
"ToastBackupDeleteSuccess": "Sauvegarde supprimée",
|
"ToastBackupDeleteSuccess": "Sauvegarde supprimée",
|
||||||
"ToastBackupRestoreFailed": "Echec de la restauration de sauvegarde",
|
"ToastBackupRestoreFailed": "Échec de la restauration de sauvegarde",
|
||||||
"ToastBackupUploadFailed": "Echec du téléversement de sauvegarde",
|
"ToastBackupUploadFailed": "Échec du téléversement de sauvegarde",
|
||||||
"ToastBackupUploadSuccess": "Sauvegarde téléversée",
|
"ToastBackupUploadSuccess": "Sauvegarde téléversée",
|
||||||
"ToastBatchUpdateFailed": "Echec de la mise à jour par lot",
|
"ToastBatchUpdateFailed": "Échec de la mise à jour par lot",
|
||||||
"ToastBatchUpdateSuccess": "Mise à jour par lot terminée",
|
"ToastBatchUpdateSuccess": "Mise à jour par lot terminée",
|
||||||
"ToastBookmarkCreateFailed": "Echec de la création de signet",
|
"ToastBookmarkCreateFailed": "Échec de la création de signet",
|
||||||
"ToastBookmarkCreateSuccess": "Signet ajouté",
|
"ToastBookmarkCreateSuccess": "Signet ajouté",
|
||||||
"ToastBookmarkRemoveFailed": "Echec de la suppression de signet",
|
"ToastBookmarkRemoveFailed": "Échec de la suppression de signet",
|
||||||
"ToastBookmarkRemoveSuccess": "Signet supprimé",
|
"ToastBookmarkRemoveSuccess": "Signet supprimé",
|
||||||
"ToastBookmarkUpdateFailed": "Echec de la mise à jour de signet",
|
"ToastBookmarkUpdateFailed": "Échec de la mise à jour de signet",
|
||||||
"ToastBookmarkUpdateSuccess": "Signet mis à jour",
|
"ToastBookmarkUpdateSuccess": "Signet mis à jour",
|
||||||
"ToastChaptersHaveErrors": "Les chapitres contiennent des erreurs",
|
"ToastChaptersHaveErrors": "Les chapitres contiennent des erreurs",
|
||||||
"ToastChaptersMustHaveTitles": "Les chapitre doivent avoir un titre",
|
"ToastChaptersMustHaveTitles": "Les chapitre doivent avoir un titre",
|
||||||
"ToastCollectionItemsRemoveFailed": "Echec de la suppression de(s) article(s) de la collection",
|
"ToastCollectionItemsRemoveFailed": "Échec de la suppression de(s) article(s) de la collection",
|
||||||
"ToastCollectionItemsRemoveSuccess": "Article(s) supprimé(s) de la collection",
|
"ToastCollectionItemsRemoveSuccess": "Article(s) supprimé(s) de la collection",
|
||||||
"ToastCollectionRemoveFailed": "Echec de la suppression de la collection",
|
"ToastCollectionRemoveFailed": "Échec de la suppression de la collection",
|
||||||
"ToastCollectionRemoveSuccess": "Collection supprimée",
|
"ToastCollectionRemoveSuccess": "Collection supprimée",
|
||||||
"ToastCollectionUpdateFailed": "Echec de la mise à jour de la collection",
|
"ToastCollectionUpdateFailed": "Échec de la mise à jour de la collection",
|
||||||
"ToastCollectionUpdateSuccess": "Collection mise à jour",
|
"ToastCollectionUpdateSuccess": "Collection mise à jour",
|
||||||
"ToastItemCoverUpdateFailed": "Echec de la mise à jour de la couverture de l'article",
|
"ToastItemCoverUpdateFailed": "Échec de la mise à jour de la couverture de l'article",
|
||||||
"ToastItemCoverUpdateSuccess": "Couverture de l'article mise à jour",
|
"ToastItemCoverUpdateSuccess": "Couverture de l'article mise à jour",
|
||||||
"ToastItemDetailsUpdateFailed": "Echec de la mise à jour des détails de l'article",
|
"ToastItemDetailsUpdateFailed": "Échec de la mise à jour des détails de l'article",
|
||||||
"ToastItemDetailsUpdateSuccess": "Détails de l'article mis à jour",
|
"ToastItemDetailsUpdateSuccess": "Détails de l'article mis à jour",
|
||||||
"ToastItemDetailsUpdateUnneeded": "Pas de mise à jour nécessaire pour les détails de l'article",
|
"ToastItemDetailsUpdateUnneeded": "Pas de mise à jour nécessaire pour les détails de l'article",
|
||||||
"ToastItemMarkedAsFinishedFailed": "Echec de l'annotation terminée",
|
"ToastItemMarkedAsFinishedFailed": "Échec de l'annotation terminée",
|
||||||
"ToastItemMarkedAsFinishedSuccess": "Article marqué comme terminé",
|
"ToastItemMarkedAsFinishedSuccess": "Article marqué comme terminé",
|
||||||
"ToastItemMarkedAsNotFinishedFailed": "Echec de l'annotation non-terminée",
|
"ToastItemMarkedAsNotFinishedFailed": "Échec de l'annotation non-terminée",
|
||||||
"ToastItemMarkedAsNotFinishedSuccess": "Article marqué comme non-terminé",
|
"ToastItemMarkedAsNotFinishedSuccess": "Article marqué comme non-terminé",
|
||||||
"ToastLibraryCreateFailed": "Echec de la création de bibliothèque",
|
"ToastLibraryCreateFailed": "Échec de la création de bibliothèque",
|
||||||
"ToastLibraryCreateSuccess": "Bibliothèque \"{0}\" créée",
|
"ToastLibraryCreateSuccess": "Bibliothèque \"{0}\" créée",
|
||||||
"ToastLibraryDeleteFailed": "Echec de la suppression de la bibliothèque",
|
"ToastLibraryDeleteFailed": "Échec de la suppression de la bibliothèque",
|
||||||
"ToastLibraryDeleteSuccess": "Bibliothèque supprimée",
|
"ToastLibraryDeleteSuccess": "Bibliothèque supprimée",
|
||||||
"ToastLibraryScanFailedToStart": "Echec du démarrage de l'analyse",
|
"ToastLibraryScanFailedToStart": "Échec du démarrage de l'analyse",
|
||||||
"ToastLibraryScanStarted": "Analyse de la bibliothèque démarrée",
|
"ToastLibraryScanStarted": "Analyse de la bibliothèque démarrée",
|
||||||
"ToastLibraryUpdateFailed": "Echec de la mise à jour de la bibliothèque",
|
"ToastLibraryUpdateFailed": "Échec de la mise à jour de la bibliothèque",
|
||||||
"ToastLibraryUpdateSuccess": "Bibliothèque \"{0}\" mise à jour",
|
"ToastLibraryUpdateSuccess": "Bibliothèque \"{0}\" mise à jour",
|
||||||
"ToastPlaylistCreateFailed": "Echec de la création de la liste de lecture",
|
"ToastPlaylistCreateFailed": "Échec de la création de la liste de lecture",
|
||||||
"ToastPlaylistCreateSuccess": "Liste de lecture créée",
|
"ToastPlaylistCreateSuccess": "Liste de lecture créée",
|
||||||
"ToastPlaylistRemoveFailed": "Echec de la suppression de la liste de lecture",
|
"ToastPlaylistRemoveFailed": "Échec de la suppression de la liste de lecture",
|
||||||
"ToastPlaylistRemoveSuccess": "Liste de lecture supprimée",
|
"ToastPlaylistRemoveSuccess": "Liste de lecture supprimée",
|
||||||
"ToastPlaylistUpdateFailed": "Echec de la mise à jour de la liste de lecture",
|
"ToastPlaylistUpdateFailed": "Échec de la mise à jour de la liste de lecture",
|
||||||
"ToastPlaylistUpdateSuccess": "Liste de lecture mise à jour",
|
"ToastPlaylistUpdateSuccess": "Liste de lecture mise à jour",
|
||||||
"ToastPodcastCreateFailed": "Echec de la création du Podcast",
|
"ToastPodcastCreateFailed": "Échec de la création du Podcast",
|
||||||
"ToastPodcastCreateSuccess": "Podcast créé",
|
"ToastPodcastCreateSuccess": "Podcast créé",
|
||||||
"ToastRemoveItemFromCollectionFailed": "Echec de la suppression de l'article de la collection",
|
"ToastRemoveItemFromCollectionFailed": "Échec de la suppression de l'article de la collection",
|
||||||
"ToastRemoveItemFromCollectionSuccess": "Article supprimé de la collection",
|
"ToastRemoveItemFromCollectionSuccess": "Article supprimé de la collection",
|
||||||
"ToastRSSFeedCloseFailed": "Echec de la fermeture du flux RSS",
|
"ToastRSSFeedCloseFailed": "Échec de la fermeture du flux RSS",
|
||||||
"ToastRSSFeedCloseSuccess": "Flux RSS fermé",
|
"ToastRSSFeedCloseSuccess": "Flux RSS fermé",
|
||||||
"ToastSessionDeleteFailed": "Echec de la suppression de session",
|
"ToastSeriesUpdateFailed": "Echec de la mise à jour de la série",
|
||||||
|
"ToastSeriesUpdateSuccess": "Mise à jour de la série réussie",
|
||||||
|
"ToastSessionDeleteFailed": "Échec de la suppression de session",
|
||||||
"ToastSessionDeleteSuccess": "Session supprimée",
|
"ToastSessionDeleteSuccess": "Session supprimée",
|
||||||
"ToastSocketConnected": "WebSocket connectée",
|
"ToastSocketConnected": "WebSocket connecté",
|
||||||
"ToastSocketDisconnected": "WebSocket déconnectée",
|
"ToastSocketDisconnected": "WebSocket déconnecté",
|
||||||
"ToastSocketFailedToConnect": "Echec de la connexion WebSocket",
|
"ToastSocketFailedToConnect": "Échec de la connexion WebSocket",
|
||||||
"ToastUserDeleteFailed": "Echec de la suppression de l'utilisateur",
|
"ToastUserDeleteFailed": "Échec de la suppression de l'utilisateur",
|
||||||
"ToastUserDeleteSuccess": "Utilisateur supprimé",
|
"ToastUserDeleteSuccess": "Utilisateur supprimé"
|
||||||
"WeekdayFriday": "Vendredi",
|
}
|
||||||
"WeekdayMonday": "Lundi",
|
|
||||||
"WeekdaySaturday": "Samedi",
|
|
||||||
"WeekdaySunday": "Dimanche",
|
|
||||||
"WeekdayThursday": "Jeudi",
|
|
||||||
"WeekdayTuesday": "Mardi",
|
|
||||||
"WeekdayWednesday": "Mercredi"
|
|
||||||
}
|
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
"ButtonCreate": "Napravi",
|
"ButtonCreate": "Napravi",
|
||||||
"ButtonCreateBackup": "Napravi backup",
|
"ButtonCreateBackup": "Napravi backup",
|
||||||
"ButtonDelete": "Obriši",
|
"ButtonDelete": "Obriši",
|
||||||
|
"ButtonEdit": "Edit",
|
||||||
"ButtonEditChapters": "Uredi poglavlja",
|
"ButtonEditChapters": "Uredi poglavlja",
|
||||||
"ButtonEditPodcast": "Uredi podcast",
|
"ButtonEditPodcast": "Uredi podcast",
|
||||||
"ButtonForceReScan": "Prisilno ponovno skeniranje",
|
"ButtonForceReScan": "Prisilno ponovno skeniranje",
|
||||||
@@ -75,6 +76,8 @@
|
|||||||
"ButtonUploadBackup": "Upload backup",
|
"ButtonUploadBackup": "Upload backup",
|
||||||
"ButtonUploadCover": "Upload Cover",
|
"ButtonUploadCover": "Upload Cover",
|
||||||
"ButtonUploadOPMLFile": "Upload OPML Datoteku",
|
"ButtonUploadOPMLFile": "Upload OPML Datoteku",
|
||||||
|
"ButtonUserDelete": "Delete user {0}",
|
||||||
|
"ButtonUserEdit": "Edit user {0}",
|
||||||
"ButtonViewAll": "Prikaži sve",
|
"ButtonViewAll": "Prikaži sve",
|
||||||
"ButtonYes": "Da",
|
"ButtonYes": "Da",
|
||||||
"HeaderAccount": "Korisnički račun",
|
"HeaderAccount": "Korisnički račun",
|
||||||
@@ -252,7 +255,6 @@
|
|||||||
"LabelLogLevelInfo": "Info",
|
"LabelLogLevelInfo": "Info",
|
||||||
"LabelLogLevelWarn": "Warn",
|
"LabelLogLevelWarn": "Warn",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Traži nove epizode nakon ovog datuma",
|
"LabelLookForNewEpisodesAfterDate": "Traži nove epizode nakon ovog datuma",
|
||||||
"LabelMarkSeries": "Označi seriju",
|
|
||||||
"LabelMediaPlayer": "Media Player",
|
"LabelMediaPlayer": "Media Player",
|
||||||
"LabelMediaType": "Media Type",
|
"LabelMediaType": "Media Type",
|
||||||
"LabelMetadataProvider": "Poslužitelj metapodataka ",
|
"LabelMetadataProvider": "Poslužitelj metapodataka ",
|
||||||
@@ -306,6 +308,7 @@
|
|||||||
"LabelPublishYear": "Godina izdavanja",
|
"LabelPublishYear": "Godina izdavanja",
|
||||||
"LabelRecentlyAdded": "Nedavno dodano",
|
"LabelRecentlyAdded": "Nedavno dodano",
|
||||||
"LabelRecentSeries": "Nedavne serije",
|
"LabelRecentSeries": "Nedavne serije",
|
||||||
|
"LabelRecommended": "Recommended",
|
||||||
"LabelRegion": "Regija",
|
"LabelRegion": "Regija",
|
||||||
"LabelReleaseDate": "Datum izlaska",
|
"LabelReleaseDate": "Datum izlaska",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Remove cover",
|
||||||
@@ -441,6 +444,8 @@
|
|||||||
"MessageConfirmDeleteLibrary": "Jeste li sigurni da želite trajno obrisati biblioteku \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Jeste li sigurni da želite trajno obrisati biblioteku \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "Jeste li sigurni da želite obrisati ovu sesiju?",
|
"MessageConfirmDeleteSession": "Jeste li sigurni da želite obrisati ovu sesiju?",
|
||||||
"MessageConfirmForceReScan": "Jeste li sigurni da želite ponovno skenirati?",
|
"MessageConfirmForceReScan": "Jeste li sigurni da želite ponovno skenirati?",
|
||||||
|
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||||
|
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||||
"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?",
|
||||||
@@ -601,18 +606,13 @@
|
|||||||
"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",
|
||||||
|
"ToastSeriesUpdateFailed": "Series update failed",
|
||||||
|
"ToastSeriesUpdateSuccess": "Series update success",
|
||||||
"ToastSessionDeleteFailed": "Neuspješno brisanje serije",
|
"ToastSessionDeleteFailed": "Neuspješno brisanje serije",
|
||||||
"ToastSessionDeleteSuccess": "Sesija obrisana",
|
"ToastSessionDeleteSuccess": "Sesija obrisana",
|
||||||
"ToastSocketConnected": "Socket connected",
|
"ToastSocketConnected": "Socket connected",
|
||||||
"ToastSocketDisconnected": "Socket disconnected",
|
"ToastSocketDisconnected": "Socket disconnected",
|
||||||
"ToastSocketFailedToConnect": "Socket failed to connect",
|
"ToastSocketFailedToConnect": "Socket failed to connect",
|
||||||
"ToastUserDeleteFailed": "Neuspješno brisanje korisnika",
|
"ToastUserDeleteFailed": "Neuspješno brisanje korisnika",
|
||||||
"ToastUserDeleteSuccess": "Korisnik obrisan",
|
"ToastUserDeleteSuccess": "Korisnik obrisan"
|
||||||
"WeekdayFriday": "Petak",
|
|
||||||
"WeekdayMonday": "Ponedjeljak",
|
|
||||||
"WeekdaySaturday": "Subota",
|
|
||||||
"WeekdaySunday": "Nedjelja",
|
|
||||||
"WeekdayThursday": "Četvrtak",
|
|
||||||
"WeekdayTuesday": "Utorak",
|
|
||||||
"WeekdayWednesday": "Srijeda"
|
|
||||||
}
|
}
|
||||||
+30
-30
@@ -20,6 +20,7 @@
|
|||||||
"ButtonCreate": "Crea",
|
"ButtonCreate": "Crea",
|
||||||
"ButtonCreateBackup": "Crea un Backup",
|
"ButtonCreateBackup": "Crea un Backup",
|
||||||
"ButtonDelete": "Elimina",
|
"ButtonDelete": "Elimina",
|
||||||
|
"ButtonEdit": "Edit",
|
||||||
"ButtonEditChapters": "Modifica Capitoli",
|
"ButtonEditChapters": "Modifica Capitoli",
|
||||||
"ButtonEditPodcast": "Modifica Podcast",
|
"ButtonEditPodcast": "Modifica Podcast",
|
||||||
"ButtonForceReScan": "Forza Re-Scan",
|
"ButtonForceReScan": "Forza Re-Scan",
|
||||||
@@ -75,6 +76,8 @@
|
|||||||
"ButtonUploadBackup": "Carica Backup",
|
"ButtonUploadBackup": "Carica Backup",
|
||||||
"ButtonUploadCover": "Carica Cover",
|
"ButtonUploadCover": "Carica Cover",
|
||||||
"ButtonUploadOPMLFile": "Carica File OPML",
|
"ButtonUploadOPMLFile": "Carica File OPML",
|
||||||
|
"ButtonUserDelete": "Cancella Utente {0}",
|
||||||
|
"ButtonUserEdit": "Modifica Utente {0}",
|
||||||
"ButtonViewAll": "Mostra Tutto",
|
"ButtonViewAll": "Mostra Tutto",
|
||||||
"ButtonYes": "Si",
|
"ButtonYes": "Si",
|
||||||
"HeaderAccount": "Account",
|
"HeaderAccount": "Account",
|
||||||
@@ -95,7 +98,7 @@
|
|||||||
"HeaderFindChapters": "Trova Capitoli",
|
"HeaderFindChapters": "Trova Capitoli",
|
||||||
"HeaderIgnoredFiles": "File Ignorati",
|
"HeaderIgnoredFiles": "File Ignorati",
|
||||||
"HeaderItemFiles": "Files",
|
"HeaderItemFiles": "Files",
|
||||||
"HeaderItemMetadataUtils": "Item Metadata Utils",
|
"HeaderItemMetadataUtils": "Utilità Metadata oggetti",
|
||||||
"HeaderLastListeningSession": "Ultima sessione di Ascolto",
|
"HeaderLastListeningSession": "Ultima sessione di Ascolto",
|
||||||
"HeaderLatestEpisodes": "Ultimi Episodi",
|
"HeaderLatestEpisodes": "Ultimi Episodi",
|
||||||
"HeaderLibraries": "Librerie",
|
"HeaderLibraries": "Librerie",
|
||||||
@@ -105,9 +108,9 @@
|
|||||||
"HeaderListeningStats": "Statistiche di Ascolto",
|
"HeaderListeningStats": "Statistiche di Ascolto",
|
||||||
"HeaderLogin": "Login",
|
"HeaderLogin": "Login",
|
||||||
"HeaderLogs": "Logs",
|
"HeaderLogs": "Logs",
|
||||||
"HeaderManageGenres": "Manage Genres",
|
"HeaderManageGenres": "Gestisci Generi",
|
||||||
"HeaderManageTags": "Manage Tags",
|
"HeaderManageTags": "Gestisci Tags",
|
||||||
"HeaderMapDetails": "Map details",
|
"HeaderMapDetails": "Mappa Dettagli",
|
||||||
"HeaderMatch": "Trova Corrispondenza",
|
"HeaderMatch": "Trova Corrispondenza",
|
||||||
"HeaderMetadataToEmbed": "Metadata da incorporare",
|
"HeaderMetadataToEmbed": "Metadata da incorporare",
|
||||||
"HeaderNewAccount": "Nuovo Account",
|
"HeaderNewAccount": "Nuovo Account",
|
||||||
@@ -157,9 +160,9 @@
|
|||||||
"LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Raccolta",
|
"LabelAddToCollectionBatch": "Aggiungi {0} Libri alla Raccolta",
|
||||||
"LabelAddToPlaylist": "aggiungi alla Playlist",
|
"LabelAddToPlaylist": "aggiungi alla Playlist",
|
||||||
"LabelAddToPlaylistBatch": "Aggiungi {0} file alla Playlist",
|
"LabelAddToPlaylistBatch": "Aggiungi {0} file alla Playlist",
|
||||||
"LabelAll": "All",
|
"LabelAll": "Tutti",
|
||||||
"LabelAllUsers": "Tutti gli Utenti",
|
"LabelAllUsers": "Tutti gli Utenti",
|
||||||
"LabelAppend": "Append",
|
"LabelAppend": "Appese",
|
||||||
"LabelAuthor": "Autore",
|
"LabelAuthor": "Autore",
|
||||||
"LabelAuthorFirstLast": "Autore (Per Nome)",
|
"LabelAuthorFirstLast": "Autore (Per Nome)",
|
||||||
"LabelAuthorLastFirst": "Autori (Per Cognome)",
|
"LabelAuthorLastFirst": "Autori (Per Cognome)",
|
||||||
@@ -241,7 +244,7 @@
|
|||||||
"LabelLastSeen": "Ultimi Visti",
|
"LabelLastSeen": "Ultimi Visti",
|
||||||
"LabelLastTime": "Ultima Volta",
|
"LabelLastTime": "Ultima Volta",
|
||||||
"LabelLastUpdate": "Ultimo Aggiornamento",
|
"LabelLastUpdate": "Ultimo Aggiornamento",
|
||||||
"LabelLess": "Meno",
|
"LabelLess": "Poco",
|
||||||
"LabelLibrariesAccessibleToUser": "Librerie Accessibili agli Utenti",
|
"LabelLibrariesAccessibleToUser": "Librerie Accessibili agli Utenti",
|
||||||
"LabelLibrary": "Libreria",
|
"LabelLibrary": "Libreria",
|
||||||
"LabelLibraryItem": "Elementi della Library",
|
"LabelLibraryItem": "Elementi della Library",
|
||||||
@@ -252,7 +255,6 @@
|
|||||||
"LabelLogLevelInfo": "Info",
|
"LabelLogLevelInfo": "Info",
|
||||||
"LabelLogLevelWarn": "Allarme",
|
"LabelLogLevelWarn": "Allarme",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Cerca nuovi episodi dopo questa data",
|
"LabelLookForNewEpisodesAfterDate": "Cerca nuovi episodi dopo questa data",
|
||||||
"LabelMarkSeries": "Segna Serie",
|
|
||||||
"LabelMediaPlayer": "Media Player",
|
"LabelMediaPlayer": "Media Player",
|
||||||
"LabelMediaType": "Tipo Media",
|
"LabelMediaType": "Tipo Media",
|
||||||
"LabelMetadataProvider": "Metadata Provider",
|
"LabelMetadataProvider": "Metadata Provider",
|
||||||
@@ -260,7 +262,7 @@
|
|||||||
"LabelMinute": "Minuto",
|
"LabelMinute": "Minuto",
|
||||||
"LabelMissing": "Altro",
|
"LabelMissing": "Altro",
|
||||||
"LabelMissingParts": "Parti rimantenti",
|
"LabelMissingParts": "Parti rimantenti",
|
||||||
"LabelMore": "Espandi",
|
"LabelMore": "Molto",
|
||||||
"LabelName": "Nome",
|
"LabelName": "Nome",
|
||||||
"LabelNarrator": "Narratore",
|
"LabelNarrator": "Narratore",
|
||||||
"LabelNarrators": "Narratori",
|
"LabelNarrators": "Narratori",
|
||||||
@@ -306,6 +308,7 @@
|
|||||||
"LabelPublishYear": "Anno Pubblicazione",
|
"LabelPublishYear": "Anno Pubblicazione",
|
||||||
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
"LabelRecentlyAdded": "Aggiunti Recentemente",
|
||||||
"LabelRecentSeries": "Serie Recenti",
|
"LabelRecentSeries": "Serie Recenti",
|
||||||
|
"LabelRecommended": "Recommended",
|
||||||
"LabelRegion": "Regione",
|
"LabelRegion": "Regione",
|
||||||
"LabelReleaseDate": "Data Release",
|
"LabelReleaseDate": "Data Release",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Remove cover",
|
||||||
@@ -347,7 +350,7 @@
|
|||||||
"LabelSettingsSkipMatchingBooksWithASIN": "Salta la ricerca dati in internet se è già presente un codice ASIN",
|
"LabelSettingsSkipMatchingBooksWithASIN": "Salta la ricerca dati in internet se è già presente un codice ASIN",
|
||||||
"LabelSettingsSkipMatchingBooksWithISBN": "Salta la ricerca dati in internet se è già presente un codice ISBN",
|
"LabelSettingsSkipMatchingBooksWithISBN": "Salta la ricerca dati in internet se è già presente un codice ISBN",
|
||||||
"LabelSettingsSortingIgnorePrefixes": "Ignora i prefissi nei titoli durante l'aggiunta",
|
"LabelSettingsSortingIgnorePrefixes": "Ignora i prefissi nei titoli durante l'aggiunta",
|
||||||
"LabelSettingsSortingIgnorePrefixesHelp": "Per prefisso si intende ad esempio \"il\" cone nel libro \"Il signore degli anelli\" che verrebbe ordinato come \"signore degli anelli, il\"",
|
"LabelSettingsSortingIgnorePrefixesHelp": "Per prefisso si intende ad esempio \"il\" come nel libro \"Il signore degli anelli\" che verrebbe ordinato come \"signore degli anelli, il\"",
|
||||||
"LabelSettingsSquareBookCovers": "Utilizza le copertine quadrate",
|
"LabelSettingsSquareBookCovers": "Utilizza le copertine quadrate",
|
||||||
"LabelSettingsSquareBookCoversHelp": "Preferisci usare copertine quadrate rispetto a copertine di libri standard 1,6:1",
|
"LabelSettingsSquareBookCoversHelp": "Preferisci usare copertine quadrate rispetto a copertine di libri standard 1,6:1",
|
||||||
"LabelSettingsStoreCoversWithItem": "Archivia le copertine con il file",
|
"LabelSettingsStoreCoversWithItem": "Archivia le copertine con il file",
|
||||||
@@ -428,7 +431,7 @@
|
|||||||
"MessageBackupsDescription": "I backup includono utenti, progressi degli utenti, dettagli sugli elementi della libreria, impostazioni del server e immagini archiviate in <code>/metadata/items</code> & <code>/metadata/authors</code>. I backup non includono i file archiviati nelle cartelle della libreria.",
|
"MessageBackupsDescription": "I backup includono utenti, progressi degli utenti, dettagli sugli elementi della libreria, impostazioni del server e immagini archiviate in <code>/metadata/items</code> & <code>/metadata/authors</code>. I backup non includono i file archiviati nelle cartelle della libreria.",
|
||||||
"MessageBatchQuickMatchDescription": "Quick Match tenterà di aggiungere copertine e metadati mancanti per gli elementi selezionati. Attiva l'opzione per consentire a Quick Match di sovrascrivere copertine e/o metadati esistenti.",
|
"MessageBatchQuickMatchDescription": "Quick Match tenterà di aggiungere copertine e metadati mancanti per gli elementi selezionati. Attiva l'opzione per consentire a Quick Match di sovrascrivere copertine e/o metadati esistenti.",
|
||||||
"MessageBookshelfNoCollections": "Non hai ancora creato nessuna raccolta ",
|
"MessageBookshelfNoCollections": "Non hai ancora creato nessuna raccolta ",
|
||||||
"MessageBookshelfNoResultsForFilter": "Nessul risultato per il filtro \"{0}: {1}\"",
|
"MessageBookshelfNoResultsForFilter": "Nessun risultato per il filtro \"{0}: {1}\"",
|
||||||
"MessageBookshelfNoRSSFeeds": "Nessun RSS feeds aperto",
|
"MessageBookshelfNoRSSFeeds": "Nessun RSS feeds aperto",
|
||||||
"MessageBookshelfNoSeries": "Non c'è nessuna Serie",
|
"MessageBookshelfNoSeries": "Non c'è nessuna Serie",
|
||||||
"MessageChapterEndIsAfter": "La fine del capitolo è dopo la fine del tuo audiolibro",
|
"MessageChapterEndIsAfter": "La fine del capitolo è dopo la fine del tuo audiolibro",
|
||||||
@@ -441,16 +444,18 @@
|
|||||||
"MessageConfirmDeleteLibrary": "Sei sicuro di voler eliminare definitivamente la libreria \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Sei sicuro di voler eliminare definitivamente la libreria \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "Sei sicuro di voler eliminare questa sessione?",
|
"MessageConfirmDeleteSession": "Sei sicuro di voler eliminare questa sessione?",
|
||||||
"MessageConfirmForceReScan": "Sei sicuro di voler forzare una nuova scansione?",
|
"MessageConfirmForceReScan": "Sei sicuro di voler forzare una nuova scansione?",
|
||||||
|
"MessageConfirmMarkSeriesFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come completati?",
|
||||||
|
"MessageConfirmMarkSeriesNotFinished": "Sei sicuro di voler contrassegnare tutti i libri di questa serie come non completati?",
|
||||||
"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?",
|
||||||
"MessageConfirmRemovePlaylist": "Sei sicuro di voler rimuovere la tua playlist \"{0}\"?",
|
"MessageConfirmRemovePlaylist": "Sei sicuro di voler rimuovere la tua playlist \"{0}\"?",
|
||||||
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
|
"MessageConfirmRenameGenre": "Sei sicuro di voler rinominare il genere \"{0}\" in \"{1}\" per tutti gli oggetti?",
|
||||||
"MessageConfirmRenameGenreMergeNote": "Note: This genre already exists so they will be merged.",
|
"MessageConfirmRenameGenreMergeNote": "Note: Questo genere esiste già quindi verra unito.",
|
||||||
"MessageConfirmRenameGenreWarning": "Warning! A similar genre with a different casing already exists \"{0}\".",
|
"MessageConfirmRenameGenreWarning": "Avvertimento! Esiste già un genere simile con un nome simile \"{0}\".",
|
||||||
"MessageConfirmRenameTag": "Are you sure you want to rename tag \"{0}\" to \"{1}\" for all items?",
|
"MessageConfirmRenameTag": "Sei sicuro di voler rinominare il tag \"{0}\" in \"{1}\" per tutti gli oggetti?",
|
||||||
"MessageConfirmRenameTagMergeNote": "Note: This tag already exists so they will be merged.",
|
"MessageConfirmRenameTagMergeNote": "Nota: Questo tag esiste già e verrà unito nel vecchio.",
|
||||||
"MessageConfirmRenameTagWarning": "Warning! A similar tag with a different casing already exists \"{0}\".",
|
"MessageConfirmRenameTagWarning": "Avvertimento! Esiste già un tag simile con un nome simile \"{0}\".",
|
||||||
"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!",
|
||||||
@@ -461,7 +466,7 @@
|
|||||||
"MessageImportantNotice": "Avviso Importante!",
|
"MessageImportantNotice": "Avviso Importante!",
|
||||||
"MessageInsertChapterBelow": "Inserisci capitolo sotto",
|
"MessageInsertChapterBelow": "Inserisci capitolo sotto",
|
||||||
"MessageItemsSelected": "{0} oggetti Selezionati",
|
"MessageItemsSelected": "{0} oggetti Selezionati",
|
||||||
"MessageItemsUpdated": "{0} Items Updated",
|
"MessageItemsUpdated": "{0} Oggetti aggiornati",
|
||||||
"MessageJoinUsOn": "Unisciti a noi su",
|
"MessageJoinUsOn": "Unisciti a noi su",
|
||||||
"MessageListeningSessionsInTheLastYear": "{0} sessioni di ascolto nell'ultimo anno",
|
"MessageListeningSessionsInTheLastYear": "{0} sessioni di ascolto nell'ultimo anno",
|
||||||
"MessageLoading": "Caricamento...",
|
"MessageLoading": "Caricamento...",
|
||||||
@@ -503,7 +508,7 @@
|
|||||||
"MessageOr": "o",
|
"MessageOr": "o",
|
||||||
"MessagePauseChapter": "Metti in Pausa Capitolo",
|
"MessagePauseChapter": "Metti in Pausa Capitolo",
|
||||||
"MessagePlayChapter": "Ascolta dall'inizio del capitolo",
|
"MessagePlayChapter": "Ascolta dall'inizio del capitolo",
|
||||||
"MessagePlaylistCreateFromCollection": "Create playlist from collection",
|
"MessagePlaylistCreateFromCollection": "Crea playlist da una Raccolta",
|
||||||
"MessagePodcastHasNoRSSFeedForMatching": "Podcast non ha l'URL del feed RSS da utilizzare per il match",
|
"MessagePodcastHasNoRSSFeedForMatching": "Podcast non ha l'URL del feed RSS da utilizzare per il match",
|
||||||
"MessageQuickMatchDescription": "Compila i dettagli dell'articolo vuoto e copri con il risultato della prima corrispondenza di '{0}'. Non sovrascrive i dettagli a meno che non sia abilitata l'impostazione del server \"Preferisci metadati corrispondenti\".",
|
"MessageQuickMatchDescription": "Compila i dettagli dell'articolo vuoto e copri con il risultato della prima corrispondenza di '{0}'. Non sovrascrive i dettagli a meno che non sia abilitata l'impostazione del server \"Preferisci metadati corrispondenti\".",
|
||||||
"MessageRemoveAllItemsWarning": "AVVERTIMENTO! Questa azione rimuoverà tutti gli elementi della libreria dal database, inclusi eventuali aggiornamenti o corrispondenze apportate. Questo non fa nulla ai tuoi file effettivi. Sei sicuro?",
|
"MessageRemoveAllItemsWarning": "AVVERTIMENTO! Questa azione rimuoverà tutti gli elementi della libreria dal database, inclusi eventuali aggiornamenti o corrispondenze apportate. Questo non fa nulla ai tuoi file effettivi. Sei sicuro?",
|
||||||
@@ -589,30 +594,25 @@
|
|||||||
"ToastLibraryScanStarted": "Scansione Libreria iniziata",
|
"ToastLibraryScanStarted": "Scansione Libreria iniziata",
|
||||||
"ToastLibraryUpdateFailed": "Errore Aggiornamento libreria",
|
"ToastLibraryUpdateFailed": "Errore Aggiornamento libreria",
|
||||||
"ToastLibraryUpdateSuccess": "Libreria \"{0}\" aggiornata",
|
"ToastLibraryUpdateSuccess": "Libreria \"{0}\" aggiornata",
|
||||||
"ToastPlaylistCreateFailed": "Failed to create playlist",
|
"ToastPlaylistCreateFailed": "Errore Creazione playlist",
|
||||||
"ToastPlaylistCreateSuccess": "Playlist created",
|
"ToastPlaylistCreateSuccess": "Playlist creata",
|
||||||
"ToastPlaylistRemoveFailed": "Rimozione Playlist Fallita",
|
"ToastPlaylistRemoveFailed": "Rimozione Playlist Fallita",
|
||||||
"ToastPlaylistRemoveSuccess": "Playlist rimossa",
|
"ToastPlaylistRemoveSuccess": "Playlist rimossa",
|
||||||
"ToastPlaylistUpdateFailed": "Aggiornamento Playlist Fallita",
|
"ToastPlaylistUpdateFailed": "Aggiornamento Playlist Fallita",
|
||||||
"ToastPlaylistUpdateSuccess": "Playlist Aggiornata",
|
"ToastPlaylistUpdateSuccess": "Playlist Aggiornata",
|
||||||
"ToastPodcastCreateFailed": "Errore Creazione podcast",
|
"ToastPodcastCreateFailed": "Errore Creazione podcast",
|
||||||
"ToastPodcastCreateSuccess": "Podcast creato Correttamwnte",
|
"ToastPodcastCreateSuccess": "Podcast creato Correttamente",
|
||||||
"ToastRemoveItemFromCollectionFailed": "Errore rimozione file dalla Raccolta",
|
"ToastRemoveItemFromCollectionFailed": "Errore rimozione file dalla Raccolta",
|
||||||
"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",
|
||||||
|
"ToastSeriesUpdateFailed": "Aggiornaemnto Serie Fallito",
|
||||||
|
"ToastSeriesUpdateSuccess": "Serie Aggornate",
|
||||||
"ToastSessionDeleteFailed": "Errore eliminazione sessione",
|
"ToastSessionDeleteFailed": "Errore eliminazione sessione",
|
||||||
"ToastSessionDeleteSuccess": "Sessione cancellata",
|
"ToastSessionDeleteSuccess": "Sessione cancellata",
|
||||||
"ToastSocketConnected": "Socket connesso",
|
"ToastSocketConnected": "Socket connesso",
|
||||||
"ToastSocketDisconnected": "Socket disconnesso",
|
"ToastSocketDisconnected": "Socket disconnesso",
|
||||||
"ToastSocketFailedToConnect": "Socket non riesce a connettersi",
|
"ToastSocketFailedToConnect": "Socket non riesce a connettersi",
|
||||||
"ToastUserDeleteFailed": "Errore eliminazione utente",
|
"ToastUserDeleteFailed": "Errore eliminazione utente",
|
||||||
"ToastUserDeleteSuccess": "Utente eliminato",
|
"ToastUserDeleteSuccess": "Utente eliminato"
|
||||||
"WeekdayFriday": "Venerdì",
|
|
||||||
"WeekdayMonday": "Lunedì",
|
|
||||||
"WeekdaySaturday": "Sabato",
|
|
||||||
"WeekdaySunday": "Domenica",
|
|
||||||
"WeekdayThursday": "Giovedi",
|
|
||||||
"WeekdayTuesday": "Martedì",
|
|
||||||
"WeekdayWednesday": "Mercoledì"
|
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
"ButtonCreate": "Utwórz",
|
"ButtonCreate": "Utwórz",
|
||||||
"ButtonCreateBackup": "Utwórz kopię zapasową",
|
"ButtonCreateBackup": "Utwórz kopię zapasową",
|
||||||
"ButtonDelete": "Usuń",
|
"ButtonDelete": "Usuń",
|
||||||
|
"ButtonEdit": "Edit",
|
||||||
"ButtonEditChapters": "Edytuj rozdziały",
|
"ButtonEditChapters": "Edytuj rozdziały",
|
||||||
"ButtonEditPodcast": "Edytuj podcast",
|
"ButtonEditPodcast": "Edytuj podcast",
|
||||||
"ButtonForceReScan": "Wymuś ponowne skanowanie",
|
"ButtonForceReScan": "Wymuś ponowne skanowanie",
|
||||||
@@ -75,6 +76,8 @@
|
|||||||
"ButtonUploadBackup": "Wgraj kopię zapasową",
|
"ButtonUploadBackup": "Wgraj kopię zapasową",
|
||||||
"ButtonUploadCover": "Wgraj okładkę",
|
"ButtonUploadCover": "Wgraj okładkę",
|
||||||
"ButtonUploadOPMLFile": "Wgraj plik OPML",
|
"ButtonUploadOPMLFile": "Wgraj plik OPML",
|
||||||
|
"ButtonUserDelete": "Delete user {0}",
|
||||||
|
"ButtonUserEdit": "Edit user {0}",
|
||||||
"ButtonViewAll": "Zobacz wszystko",
|
"ButtonViewAll": "Zobacz wszystko",
|
||||||
"ButtonYes": "Tak",
|
"ButtonYes": "Tak",
|
||||||
"HeaderAccount": "Konto",
|
"HeaderAccount": "Konto",
|
||||||
@@ -252,7 +255,6 @@
|
|||||||
"LabelLogLevelInfo": "Informacja",
|
"LabelLogLevelInfo": "Informacja",
|
||||||
"LabelLogLevelWarn": "Ostrzeżenie",
|
"LabelLogLevelWarn": "Ostrzeżenie",
|
||||||
"LabelLookForNewEpisodesAfterDate": "Szukaj nowych odcinków po dacie",
|
"LabelLookForNewEpisodesAfterDate": "Szukaj nowych odcinków po dacie",
|
||||||
"LabelMarkSeries": "Oznacz serię",
|
|
||||||
"LabelMediaPlayer": "Odtwarzacz",
|
"LabelMediaPlayer": "Odtwarzacz",
|
||||||
"LabelMediaType": "Typ mediów",
|
"LabelMediaType": "Typ mediów",
|
||||||
"LabelMetadataProvider": "Dostawca metadanych",
|
"LabelMetadataProvider": "Dostawca metadanych",
|
||||||
@@ -306,6 +308,7 @@
|
|||||||
"LabelPublishYear": "Rok publikacji",
|
"LabelPublishYear": "Rok publikacji",
|
||||||
"LabelRecentlyAdded": "Niedawno dodany",
|
"LabelRecentlyAdded": "Niedawno dodany",
|
||||||
"LabelRecentSeries": "Ostatnie serie",
|
"LabelRecentSeries": "Ostatnie serie",
|
||||||
|
"LabelRecommended": "Recommended",
|
||||||
"LabelRegion": "Region",
|
"LabelRegion": "Region",
|
||||||
"LabelReleaseDate": "Data wydania",
|
"LabelReleaseDate": "Data wydania",
|
||||||
"LabelRemoveCover": "Remove cover",
|
"LabelRemoveCover": "Remove cover",
|
||||||
@@ -441,6 +444,8 @@
|
|||||||
"MessageConfirmDeleteLibrary": "Czy na pewno chcesz trwale usunąć bibliotekę \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "Czy na pewno chcesz trwale usunąć bibliotekę \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "Czy na pewno chcesz usunąć tę sesję?",
|
"MessageConfirmDeleteSession": "Czy na pewno chcesz usunąć tę sesję?",
|
||||||
"MessageConfirmForceReScan": "Czy na pewno chcesz wymusić ponowne skanowanie?",
|
"MessageConfirmForceReScan": "Czy na pewno chcesz wymusić ponowne skanowanie?",
|
||||||
|
"MessageConfirmMarkSeriesFinished": "Are you sure you want to mark all books in this series as finished?",
|
||||||
|
"MessageConfirmMarkSeriesNotFinished": "Are you sure you want to mark all books in this series as not finished?",
|
||||||
"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?",
|
||||||
@@ -601,18 +606,13 @@
|
|||||||
"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ę",
|
||||||
|
"ToastSeriesUpdateFailed": "Series update failed",
|
||||||
|
"ToastSeriesUpdateSuccess": "Series update success",
|
||||||
"ToastSessionDeleteFailed": "Nie udało się usunąć sesji",
|
"ToastSessionDeleteFailed": "Nie udało się usunąć sesji",
|
||||||
"ToastSessionDeleteSuccess": "Sesja usunięta",
|
"ToastSessionDeleteSuccess": "Sesja usunięta",
|
||||||
"ToastSocketConnected": "Nawiązano połączenie z serwerem",
|
"ToastSocketConnected": "Nawiązano połączenie z serwerem",
|
||||||
"ToastSocketDisconnected": "Połączenie z serwerem zostało zamknięte",
|
"ToastSocketDisconnected": "Połączenie z serwerem zostało zamknięte",
|
||||||
"ToastSocketFailedToConnect": "Poączenie z serwerem nie powiodło się",
|
"ToastSocketFailedToConnect": "Poączenie z serwerem nie powiodło się",
|
||||||
"ToastUserDeleteFailed": "Nie udało się usunąć użytkownika",
|
"ToastUserDeleteFailed": "Nie udało się usunąć użytkownika",
|
||||||
"ToastUserDeleteSuccess": "Użytkownik usunięty",
|
"ToastUserDeleteSuccess": "Użytkownik usunięty"
|
||||||
"WeekdayFriday": "Piątek",
|
|
||||||
"WeekdayMonday": "Poniedziałek",
|
|
||||||
"WeekdaySaturday": "Sobota",
|
|
||||||
"WeekdaySunday": "Niedziela",
|
|
||||||
"WeekdayThursday": "Czwartek",
|
|
||||||
"WeekdayTuesday": "Wtorek",
|
|
||||||
"WeekdayWednesday": "Środa"
|
|
||||||
}
|
}
|
||||||
+10
-10
@@ -20,6 +20,7 @@
|
|||||||
"ButtonCreate": "创建",
|
"ButtonCreate": "创建",
|
||||||
"ButtonCreateBackup": "创建备份",
|
"ButtonCreateBackup": "创建备份",
|
||||||
"ButtonDelete": "删除",
|
"ButtonDelete": "删除",
|
||||||
|
"ButtonEdit": "编辑",
|
||||||
"ButtonEditChapters": "编辑章节",
|
"ButtonEditChapters": "编辑章节",
|
||||||
"ButtonEditPodcast": "编辑播客",
|
"ButtonEditPodcast": "编辑播客",
|
||||||
"ButtonForceReScan": "强制重新扫描",
|
"ButtonForceReScan": "强制重新扫描",
|
||||||
@@ -75,6 +76,8 @@
|
|||||||
"ButtonUploadBackup": "上传备份",
|
"ButtonUploadBackup": "上传备份",
|
||||||
"ButtonUploadCover": "上传封面",
|
"ButtonUploadCover": "上传封面",
|
||||||
"ButtonUploadOPMLFile": "上传 OPML 文件",
|
"ButtonUploadOPMLFile": "上传 OPML 文件",
|
||||||
|
"ButtonUserDelete": "删除用户 {0}",
|
||||||
|
"ButtonUserEdit": "编辑用户 {0}",
|
||||||
"ButtonViewAll": "查看全部",
|
"ButtonViewAll": "查看全部",
|
||||||
"ButtonYes": "确定",
|
"ButtonYes": "确定",
|
||||||
"HeaderAccount": "帐户",
|
"HeaderAccount": "帐户",
|
||||||
@@ -252,7 +255,6 @@
|
|||||||
"LabelLogLevelInfo": "信息",
|
"LabelLogLevelInfo": "信息",
|
||||||
"LabelLogLevelWarn": "警告",
|
"LabelLogLevelWarn": "警告",
|
||||||
"LabelLookForNewEpisodesAfterDate": "在此日期后查找新剧集",
|
"LabelLookForNewEpisodesAfterDate": "在此日期后查找新剧集",
|
||||||
"LabelMarkSeries": "标记系列",
|
|
||||||
"LabelMediaPlayer": "媒体播放器",
|
"LabelMediaPlayer": "媒体播放器",
|
||||||
"LabelMediaType": "媒体类型",
|
"LabelMediaType": "媒体类型",
|
||||||
"LabelMetadataProvider": "元数据提供者",
|
"LabelMetadataProvider": "元数据提供者",
|
||||||
@@ -306,6 +308,7 @@
|
|||||||
"LabelPublishYear": "发布年份",
|
"LabelPublishYear": "发布年份",
|
||||||
"LabelRecentlyAdded": "最近添加",
|
"LabelRecentlyAdded": "最近添加",
|
||||||
"LabelRecentSeries": "最近添加系列",
|
"LabelRecentSeries": "最近添加系列",
|
||||||
|
"LabelRecommended": "推荐内容",
|
||||||
"LabelRegion": "区域",
|
"LabelRegion": "区域",
|
||||||
"LabelReleaseDate": "发布日期",
|
"LabelReleaseDate": "发布日期",
|
||||||
"LabelRemoveCover": "移除封面",
|
"LabelRemoveCover": "移除封面",
|
||||||
@@ -441,6 +444,8 @@
|
|||||||
"MessageConfirmDeleteLibrary": "你确定要永久删除媒体库 \"{0}\"?",
|
"MessageConfirmDeleteLibrary": "你确定要永久删除媒体库 \"{0}\"?",
|
||||||
"MessageConfirmDeleteSession": "你确定要删除此会话吗?",
|
"MessageConfirmDeleteSession": "你确定要删除此会话吗?",
|
||||||
"MessageConfirmForceReScan": "你确定要强制重新扫描吗?",
|
"MessageConfirmForceReScan": "你确定要强制重新扫描吗?",
|
||||||
|
"MessageConfirmMarkSeriesFinished": "你确定要将此系列中的所有书籍都标记为已听完吗?",
|
||||||
|
"MessageConfirmMarkSeriesNotFinished": "你确定要将此系列中的所有书籍都标记为未听完吗?",
|
||||||
"MessageConfirmRemoveCollection": "您确定要移除收藏 \"{0}\"?",
|
"MessageConfirmRemoveCollection": "您确定要移除收藏 \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisode": "您确定要移除剧集 \"{0}\"?",
|
"MessageConfirmRemoveEpisode": "您确定要移除剧集 \"{0}\"?",
|
||||||
"MessageConfirmRemoveEpisodes": "你确定要移除 {0} 剧集?",
|
"MessageConfirmRemoveEpisodes": "你确定要移除 {0} 剧集?",
|
||||||
@@ -601,18 +606,13 @@
|
|||||||
"ToastRemoveItemFromCollectionSuccess": "项目已从收藏中删除",
|
"ToastRemoveItemFromCollectionSuccess": "项目已从收藏中删除",
|
||||||
"ToastRSSFeedCloseFailed": "关闭 RSS 源失败",
|
"ToastRSSFeedCloseFailed": "关闭 RSS 源失败",
|
||||||
"ToastRSSFeedCloseSuccess": "RSS 源已关闭",
|
"ToastRSSFeedCloseSuccess": "RSS 源已关闭",
|
||||||
|
"ToastSeriesUpdateFailed": "更新系列失败",
|
||||||
|
"ToastSeriesUpdateSuccess": "系列已更新",
|
||||||
"ToastSessionDeleteFailed": "删除会话失败",
|
"ToastSessionDeleteFailed": "删除会话失败",
|
||||||
"ToastSessionDeleteSuccess": "会话已删除",
|
"ToastSessionDeleteSuccess": "会话已删除",
|
||||||
"ToastSocketConnected": "网络已连接",
|
"ToastSocketConnected": "网络已连接",
|
||||||
"ToastSocketDisconnected": "网络已断开",
|
"ToastSocketDisconnected": "网络已断开",
|
||||||
"ToastSocketFailedToConnect": "网络连接失败",
|
"ToastSocketFailedToConnect": "网络连接失败",
|
||||||
"ToastUserDeleteFailed": "删除用户失败",
|
"ToastUserDeleteFailed": "删除用户失败",
|
||||||
"ToastUserDeleteSuccess": "用户已删除",
|
"ToastUserDeleteSuccess": "用户已删除"
|
||||||
"WeekdayFriday": "星期五",
|
}
|
||||||
"WeekdayMonday": "星期一",
|
|
||||||
"WeekdaySaturday": "星期六",
|
|
||||||
"WeekdaySunday": "星期日",
|
|
||||||
"WeekdayThursday": "星期四",
|
|
||||||
"WeekdayTuesday": "星期二",
|
|
||||||
"WeekdayWednesday": "星期三"
|
|
||||||
}
|
|
||||||
@@ -20,7 +20,8 @@ module.exports = {
|
|||||||
'w-3.5',
|
'w-3.5',
|
||||||
'h-3.5',
|
'h-3.5',
|
||||||
'border-warning',
|
'border-warning',
|
||||||
'mb-px'
|
'mb-px',
|
||||||
|
'text-1.5xl'
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
@@ -61,6 +62,7 @@ module.exports = {
|
|||||||
'80': '20rem'
|
'80': '20rem'
|
||||||
},
|
},
|
||||||
spacing: {
|
spacing: {
|
||||||
|
'18': '4.5rem',
|
||||||
'-54': '-13.5rem'
|
'-54': '-13.5rem'
|
||||||
},
|
},
|
||||||
rotate: {
|
rotate: {
|
||||||
@@ -94,6 +96,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
fontSize: {
|
fontSize: {
|
||||||
xxs: '0.625rem',
|
xxs: '0.625rem',
|
||||||
|
'1.5xl': '1.375rem',
|
||||||
'2.5xl': '1.6875rem'
|
'2.5xl': '1.6875rem'
|
||||||
},
|
},
|
||||||
zIndex: {
|
zIndex: {
|
||||||
|
|||||||
+92
-32
@@ -1,8 +1,24 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
<!-- Generator: Adobe Illustrator 24.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
viewBox="0 0 6702.7 1277.4" style="enable-background:new 0 0 6702.7 1277.4;" xml:space="preserve">
|
<svg
|
||||||
<style type="text/css">
|
version="1.1"
|
||||||
|
id="Layer_1"
|
||||||
|
x="0px"
|
||||||
|
y="0px"
|
||||||
|
viewBox="0 0 7501.0735 1237.1999"
|
||||||
|
xml:space="preserve"
|
||||||
|
width="7501.0737"
|
||||||
|
height="1237.2"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:cc="http://creativecommons.org/ns#"
|
||||||
|
xmlns:dc="http://purl.org/dc/elements/1.1/"><defs
|
||||||
|
id="defs104" />
|
||||||
|
<style
|
||||||
|
type="text/css"
|
||||||
|
id="style70">
|
||||||
.st0{fill:#FFFFFF;}
|
.st0{fill:#FFFFFF;}
|
||||||
.st1{fill:url(#SVGID_1_);}
|
.st1{fill:url(#SVGID_1_);}
|
||||||
.st2{fill:#C9C9C9;}
|
.st2{fill:#C9C9C9;}
|
||||||
@@ -12,38 +28,82 @@
|
|||||||
.st6{font-family:'GentiumBasic';}
|
.st6{font-family:'GentiumBasic';}
|
||||||
.st7{font-size:305px;}
|
.st7{font-size:305px;}
|
||||||
</style>
|
</style>
|
||||||
<title>bgAsset 6</title>
|
<title
|
||||||
<g id="Layer_2_1_">
|
id="title72">bgAsset 6</title>
|
||||||
<g id="Layer_2-2">
|
<g
|
||||||
<g id="Layer_4">
|
id="Layer_2_1_">
|
||||||
<g id="Layer_5">
|
<g
|
||||||
<circle class="st0" cx="618.6" cy="618.6" r="618.6"/>
|
id="Layer_2-2">
|
||||||
|
<g
|
||||||
|
id="Layer_4">
|
||||||
|
<g
|
||||||
|
id="Layer_5">
|
||||||
|
<circle
|
||||||
|
class="st0"
|
||||||
|
cx="618.59998"
|
||||||
|
cy="618.59998"
|
||||||
|
r="618.59998"
|
||||||
|
id="circle74" />
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="617.37" y1="1257.3" x2="617.37" y2="61.4399" gradientTransform="matrix(1 0 0 -1 0 1278)">
|
<linearGradient
|
||||||
<stop offset="0.32" style="stop-color:#CD9D49"/>
|
id="SVGID_1_"
|
||||||
<stop offset="0.99" style="stop-color:#875D27"/>
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="617.37"
|
||||||
|
y1="1257.3"
|
||||||
|
x2="617.37"
|
||||||
|
y2="61.439899"
|
||||||
|
gradientTransform="matrix(1,0,0,-1,0,1278)">
|
||||||
|
<stop
|
||||||
|
offset="0.32"
|
||||||
|
style="stop-color:#CD9D49"
|
||||||
|
id="stop77" />
|
||||||
|
<stop
|
||||||
|
offset="0.99"
|
||||||
|
style="stop-color:#875D27"
|
||||||
|
id="stop79" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<circle class="st1" cx="617.4" cy="618.6" r="597.9"/>
|
<circle
|
||||||
|
class="st1"
|
||||||
|
cx="617.40002"
|
||||||
|
cy="618.59998"
|
||||||
|
r="597.90002"
|
||||||
|
id="circle82"
|
||||||
|
style="fill:url(#SVGID_1_)" />
|
||||||
</g>
|
</g>
|
||||||
<path class="st0" d="M1005.6,574.1c-4.8-4-12.4-10-22.6-17v-79.2c0-201.9-163.7-365.6-365.6-365.6l0,0
|
<path
|
||||||
c-201.9,0-365.6,163.7-365.6,365.6v79.2c-10.2,7-17.7,13-22.6,17c-4.1,3.4-6.5,8.5-6.5,13.9v94.9c0,5.4,2.4,10.5,6.5,14
|
class="st0"
|
||||||
c11.3,9.4,37.2,29.1,77.5,49.3v9.2c0,24.9,16,45,35.8,45l0,0c19.8,0,35.8-20.2,35.8-45V527.8c0-24.9-16-45-35.8-45l0,0
|
d="m 1005.6,574.1 c -4.8,-4 -12.4,-10 -22.6,-17 V 477.9 C 983,276 819.3,112.3 617.4,112.3 v 0 C 415.5,112.3 251.8,276 251.8,477.9 v 79.2 c -10.2,7 -17.7,13 -22.6,17 -4.1,3.4 -6.5,8.5 -6.5,13.9 v 94.9 c 0,5.4 2.4,10.5 6.5,14 11.3,9.4 37.2,29.1 77.5,49.3 v 9.2 c 0,24.9 16,45 35.8,45 v 0 c 19.8,0 35.8,-20.2 35.8,-45 V 527.8 c 0,-24.9 -16,-45 -35.8,-45 v 0 c -19,0 -34.5,18.5 -35.8,41.9 h -0.1 v -46.9 c 0,-171.6 139.1,-310.7 310.7,-310.7 v 0 C 789,167.2 928,306.3 928,477.9 v 46.9 0 c -1.3,-23.4 -16.8,-41.9 -35.8,-41.9 v 0 c -19.8,0 -35.8,20.2 -35.8,45 v 227.6 c 0,24.9 16,45 35.8,45 v 0 c 19.8,0 35.8,-20.2 35.8,-45 v -9.2 c 40.3,-20.2 66.2,-39.9 77.5,-49.3 4.2,-3.5 6.5,-8.6 6.5,-14 v -95 c 0.1,-5.4 -2.3,-10.5 -6.4,-13.9 z"
|
||||||
c-19,0-34.5,18.5-35.8,41.9h-0.1v-46.9c0-171.6,139.1-310.7,310.7-310.7l0,0C789,167.2,928,306.3,928,477.9v46.9H928
|
id="path85" />
|
||||||
c-1.3-23.4-16.8-41.9-35.8-41.9l0,0c-19.8,0-35.8,20.2-35.8,45v227.6c0,24.9,16,45,35.8,45l0,0c19.8,0,35.8-20.2,35.8-45v-9.2
|
<path
|
||||||
c40.3-20.2,66.2-39.9,77.5-49.3c4.2-3.5,6.5-8.6,6.5-14V588C1012.1,582.6,1009.7,577.5,1005.6,574.1z"/>
|
class="st0"
|
||||||
<path class="st0" d="M489.9,969.7c23.9,0,43.3-19.4,43.3-43.3V441.6c0-23.9-19.4-43.3-43.3-43.3h-44.7
|
d="m 489.9,969.7 c 23.9,0 43.3,-19.4 43.3,-43.3 V 441.6 c 0,-23.9 -19.4,-43.3 -43.3,-43.3 h -44.7 c -23.9,0 -43.3,19.4 -43.3,43.3 v 484.8 c 0,23.9 19.4,43.3 43.3,43.3 z M 418.2,514.6 h 98.7 v 10.3 h -98.7 z"
|
||||||
c-23.9,0-43.3,19.4-43.3,43.3v484.8c0,23.9,19.4,43.3,43.3,43.3L489.9,969.7z M418.2,514.6h98.7v10.3h-98.7V514.6z"/>
|
id="path87" />
|
||||||
<path class="st0" d="M639.7,969.7c23.9,0,43.3-19.4,43.3-43.3V441.6c0-23.9-19.4-43.3-43.3-43.3H595c-23.9,0-43.3,19.4-43.3,43.3
|
<path
|
||||||
v484.8c0,23.9,19.4,43.3,43.3,43.3H639.7z M568,514.6h98.7v10.3H568V514.6z"/>
|
class="st0"
|
||||||
<path class="st0" d="M789.6,969.7c23.9,0,43.3-19.4,43.3-43.3V441.6c0-23.9-19.4-43.3-43.3-43.3h-44.7
|
d="m 639.7,969.7 c 23.9,0 43.3,-19.4 43.3,-43.3 V 441.6 c 0,-23.9 -19.4,-43.3 -43.3,-43.3 H 595 c -23.9,0 -43.3,19.4 -43.3,43.3 v 484.8 c 0,23.9 19.4,43.3 43.3,43.3 z M 568,514.6 h 98.7 v 10.3 H 568 Z"
|
||||||
c-23.9,0-43.3,19.4-43.3,43.3v484.8c0,23.9,19.4,43.3,43.3,43.3L789.6,969.7z M717.9,514.6h98.7v10.3h-98.7V514.6z"/>
|
id="path89" />
|
||||||
<path class="st0" d="M327.1,984.7h580.5c18,0,32.6,14.6,32.6,32.6v0c0,18-14.6,32.6-32.6,32.6H327.1c-18,0-32.6-14.6-32.6-32.6v0
|
<path
|
||||||
C294.5,999.3,309.1,984.7,327.1,984.7z"/>
|
class="st0"
|
||||||
|
d="m 789.6,969.7 c 23.9,0 43.3,-19.4 43.3,-43.3 V 441.6 c 0,-23.9 -19.4,-43.3 -43.3,-43.3 h -44.7 c -23.9,0 -43.3,19.4 -43.3,43.3 v 484.8 c 0,23.9 19.4,43.3 43.3,43.3 z M 717.9,514.6 h 98.7 v 10.3 h -98.7 z"
|
||||||
|
id="path91" />
|
||||||
|
<path
|
||||||
|
class="st0"
|
||||||
|
d="m 327.1,984.7 h 580.5 c 18,0 32.6,14.6 32.6,32.6 v 0 c 0,18 -14.6,32.6 -32.6,32.6 H 327.1 c -18,0 -32.6,-14.6 -32.6,-32.6 v 0 c 0,-18 14.6,-32.6 32.6,-32.6 z"
|
||||||
|
id="path93" />
|
||||||
</g>
|
</g>
|
||||||
<g id="Layer_6">
|
<g
|
||||||
<text transform="matrix(1 0 0 1 1492.27 735.42)" class="st2 st3 st4">audiobookshelf</text>
|
id="Layer_6">
|
||||||
<text id="self-hosted_audiobook_and_podcast_server" transform="matrix(1 0 0 1 1492.27 1103.6899)" class="st5 st6 st7">self-hosted audiobook and podcast server</text>
|
<text
|
||||||
|
transform="translate(1492.27,735.42)"
|
||||||
|
class="st2 st3 st4"
|
||||||
|
id="text96">audiobookshelf</text>
|
||||||
|
<text
|
||||||
|
id="self-hosted_audiobook_and_podcast_server"
|
||||||
|
transform="translate(1492.27,1103.6899)"
|
||||||
|
class="st5 st6 st7">self-hosted audiobook and podcast server</text>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
<metadata
|
||||||
|
id="metadata210"><rdf:RDF><cc:Work
|
||||||
|
rdf:about=""><dc:title>bgAsset 6</dc:title></cc:Work></rdf:RDF></metadata></svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.7 KiB |
@@ -5,11 +5,11 @@ const isDev = process.env.NODE_ENV !== 'production'
|
|||||||
if (isDev) {
|
if (isDev) {
|
||||||
const devEnv = require('./dev').config
|
const devEnv = require('./dev').config
|
||||||
process.env.NODE_ENV = 'development'
|
process.env.NODE_ENV = 'development'
|
||||||
process.env.PORT = devEnv.Port
|
if (devEnv.Port) process.env.PORT = devEnv.Port
|
||||||
process.env.CONFIG_PATH = devEnv.ConfigPath
|
if (devEnv.ConfigPath) process.env.CONFIG_PATH = devEnv.ConfigPath
|
||||||
process.env.METADATA_PATH = devEnv.MetadataPath
|
if (devEnv.MetadataPath) process.env.METADATA_PATH = devEnv.MetadataPath
|
||||||
process.env.FFMPEG_PATH = devEnv.FFmpegPath
|
if (devEnv.FFmpegPath) process.env.FFMPEG_PATH = devEnv.FFmpegPath
|
||||||
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 || ''
|
||||||
}
|
}
|
||||||
@@ -18,8 +18,8 @@ const PORT = process.env.PORT || 80
|
|||||||
const HOST = process.env.HOST
|
const HOST = process.env.HOST
|
||||||
const CONFIG_PATH = process.env.CONFIG_PATH || '/config'
|
const CONFIG_PATH = process.env.CONFIG_PATH || '/config'
|
||||||
const METADATA_PATH = process.env.METADATA_PATH || '/metadata'
|
const METADATA_PATH = process.env.METADATA_PATH || '/metadata'
|
||||||
const UID = process.env.AUDIOBOOKSHELF_UID || 99
|
const UID = process.env.AUDIOBOOKSHELF_UID
|
||||||
const GID = process.env.AUDIOBOOKSHELF_GID || 100
|
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 || ''
|
||||||
|
|
||||||
|
|||||||
Generated
+137
-52
@@ -1,20 +1,20 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.11",
|
"version": "2.2.13",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.11",
|
"version": "2.2.13",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.26.1",
|
"axios": "^1.2.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",
|
||||||
"node-tone": "^1.0.1",
|
"node-tone": "^1.0.1",
|
||||||
"socket.io": "^4.4.1",
|
"socket.io": "^4.5.4",
|
||||||
"xml2js": "^0.4.23"
|
"xml2js": "^0.4.23"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -35,14 +35,17 @@
|
|||||||
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
|
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
|
||||||
},
|
},
|
||||||
"node_modules/@types/cors": {
|
"node_modules/@types/cors": {
|
||||||
"version": "2.8.12",
|
"version": "2.8.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz",
|
||||||
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw=="
|
"integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "18.11.9",
|
"version": "18.11.18",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||||
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg=="
|
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA=="
|
||||||
},
|
},
|
||||||
"node_modules/abbrev": {
|
"node_modules/abbrev": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@@ -63,9 +66,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/anymatch": {
|
"node_modules/anymatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||||
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
|
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"normalize-path": "^3.0.0",
|
"normalize-path": "^3.0.0",
|
||||||
@@ -80,12 +83,19 @@
|
|||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "0.26.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.2.2.tgz",
|
||||||
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
|
"integrity": "sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.14.8"
|
"follow-redirects": "^1.15.0",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
@@ -203,6 +213,17 @@
|
|||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -261,6 +282,14 @@
|
|||||||
"ms": "2.0.0"
|
"ms": "2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
@@ -343,9 +372,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/engine.io": {
|
"node_modules/engine.io": {
|
||||||
"version": "6.2.0",
|
"version": "6.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz",
|
||||||
"integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==",
|
"integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/cookie": "^0.4.1",
|
"@types/cookie": "^0.4.1",
|
||||||
"@types/cors": "^2.8.12",
|
"@types/cors": "^2.8.12",
|
||||||
@@ -512,6 +541,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@@ -925,6 +967,11 @@
|
|||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||||
|
},
|
||||||
"node_modules/pstree.remy": {
|
"node_modules/pstree.remy": {
|
||||||
"version": "1.1.8",
|
"version": "1.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||||
@@ -1078,9 +1125,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/simple-update-notifier": {
|
"node_modules/simple-update-notifier": {
|
||||||
"version": "1.0.7",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz",
|
||||||
"integrity": "sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew==",
|
"integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"semver": "~7.0.0"
|
"semver": "~7.0.0"
|
||||||
@@ -1099,16 +1146,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socket.io": {
|
"node_modules/socket.io": {
|
||||||
"version": "4.5.3",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.4.tgz",
|
||||||
"integrity": "sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg==",
|
"integrity": "sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.4",
|
"accepts": "~1.3.4",
|
||||||
"base64id": "~2.0.0",
|
"base64id": "~2.0.0",
|
||||||
"debug": "~4.3.2",
|
"debug": "~4.3.2",
|
||||||
"engine.io": "~6.2.0",
|
"engine.io": "~6.2.1",
|
||||||
"socket.io-adapter": "~2.4.0",
|
"socket.io-adapter": "~2.4.0",
|
||||||
"socket.io-parser": "~4.2.0"
|
"socket.io-parser": "~4.2.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
@@ -1320,14 +1367,17 @@
|
|||||||
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
|
"integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q=="
|
||||||
},
|
},
|
||||||
"@types/cors": {
|
"@types/cors": {
|
||||||
"version": "2.8.12",
|
"version": "2.8.13",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz",
|
||||||
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw=="
|
"integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==",
|
||||||
|
"requires": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "18.11.9",
|
"version": "18.11.18",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz",
|
||||||
"integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg=="
|
"integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA=="
|
||||||
},
|
},
|
||||||
"abbrev": {
|
"abbrev": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
@@ -1345,9 +1395,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"anymatch": {
|
"anymatch": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
|
||||||
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
|
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"normalize-path": "^3.0.0",
|
"normalize-path": "^3.0.0",
|
||||||
@@ -1359,12 +1409,19 @@
|
|||||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="
|
||||||
},
|
},
|
||||||
|
"asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
|
},
|
||||||
"axios": {
|
"axios": {
|
||||||
"version": "0.26.1",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.2.2.tgz",
|
||||||
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
|
"integrity": "sha512-bz/J4gS2S3I7mpN/YZfGFTqhXTYzRho8Ay38w2otuuDR322KzFIWm/4W2K6gIwvWaws5n+mnb7D1lN9uD+QH6Q==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"follow-redirects": "^1.14.8"
|
"follow-redirects": "^1.15.0",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"balanced-match": {
|
"balanced-match": {
|
||||||
@@ -1452,6 +1509,14 @@
|
|||||||
"readdirp": "~3.6.0"
|
"readdirp": "~3.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"requires": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"concat-map": {
|
"concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -1498,6 +1563,11 @@
|
|||||||
"ms": "2.0.0"
|
"ms": "2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
|
||||||
|
},
|
||||||
"depd": {
|
"depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
@@ -1552,9 +1622,9 @@
|
|||||||
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
|
"integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="
|
||||||
},
|
},
|
||||||
"engine.io": {
|
"engine.io": {
|
||||||
"version": "6.2.0",
|
"version": "6.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.2.1.tgz",
|
||||||
"integrity": "sha512-4KzwW3F3bk+KlzSOY57fj/Jx6LyRQ1nbcyIadehl+AnXjKT7gDO0ORdRi/84ixvMKTym6ZKuxvbzN62HDDU1Lg==",
|
"integrity": "sha512-ECceEFcAaNRybd3lsGQKas3ZlMVjN3cyWwMP25D2i0zWfyiytVbTpRPa34qrr+FHddtpBVOmq4H/DCv1O0lZRA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@types/cookie": "^0.4.1",
|
"@types/cookie": "^0.4.1",
|
||||||
"@types/cors": "^2.8.12",
|
"@types/cors": "^2.8.12",
|
||||||
@@ -1674,6 +1744,16 @@
|
|||||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
|
||||||
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
|
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA=="
|
||||||
},
|
},
|
||||||
|
"form-data": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
|
"requires": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"forwarded": {
|
"forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@@ -1966,6 +2046,11 @@
|
|||||||
"ipaddr.js": "1.9.1"
|
"ipaddr.js": "1.9.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||||
|
},
|
||||||
"pstree.remy": {
|
"pstree.remy": {
|
||||||
"version": "1.1.8",
|
"version": "1.1.8",
|
||||||
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
|
||||||
@@ -2080,9 +2165,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"simple-update-notifier": {
|
"simple-update-notifier": {
|
||||||
"version": "1.0.7",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz",
|
||||||
"integrity": "sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew==",
|
"integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"semver": "~7.0.0"
|
"semver": "~7.0.0"
|
||||||
@@ -2097,16 +2182,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"socket.io": {
|
"socket.io": {
|
||||||
"version": "4.5.3",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.5.4.tgz",
|
||||||
"integrity": "sha512-zdpnnKU+H6mOp7nYRXH4GNv1ux6HL6+lHL8g7Ds7Lj8CkdK1jJK/dlwsKDculbyOHifcJ0Pr/yeXnZQ5GeFrcg==",
|
"integrity": "sha512-m3GC94iK9MfIEeIBfbhJs5BqFibMtkRk8ZpKwG2QwxV0m/eEhPIV4ara6XCF1LWNAus7z58RodiZlAH71U3EhQ==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"accepts": "~1.3.4",
|
"accepts": "~1.3.4",
|
||||||
"base64id": "~2.0.0",
|
"base64id": "~2.0.0",
|
||||||
"debug": "~4.3.2",
|
"debug": "~4.3.2",
|
||||||
"engine.io": "~6.2.0",
|
"engine.io": "~6.2.1",
|
||||||
"socket.io-adapter": "~2.4.0",
|
"socket.io-adapter": "~2.4.0",
|
||||||
"socket.io-parser": "~4.2.0"
|
"socket.io-parser": "~4.2.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": {
|
"debug": {
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.2.11",
|
"version": "2.2.13",
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -30,12 +30,12 @@
|
|||||||
"author": "advplyr",
|
"author": "advplyr",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.26.1",
|
"axios": "^1.2.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",
|
||||||
"node-tone": "^1.0.1",
|
"node-tone": "^1.0.1",
|
||||||
"socket.io": "^4.4.1",
|
"socket.io": "^4.5.4",
|
||||||
"xml2js": "^0.4.23"
|
"xml2js": "^0.4.23"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -20,12 +20,13 @@ var inputConfig = options.config ? Path.resolve(options.config) : null
|
|||||||
var inputMetadata = options.metadata ? Path.resolve(options.metadata) : null
|
var inputMetadata = options.metadata ? Path.resolve(options.metadata) : null
|
||||||
|
|
||||||
const PORT = options.port || process.env.PORT || 3333
|
const PORT = options.port || process.env.PORT || 3333
|
||||||
const HOST = options.host || process.env.HOST || "0.0.0.0"
|
const HOST = options.host || process.env.HOST
|
||||||
const CONFIG_PATH = inputConfig || process.env.CONFIG_PATH || Path.resolve('config')
|
const CONFIG_PATH = inputConfig || process.env.CONFIG_PATH || Path.resolve('config')
|
||||||
const METADATA_PATH = inputMetadata || process.env.METADATA_PATH || Path.resolve('metadata')
|
const METADATA_PATH = inputMetadata || process.env.METADATA_PATH || Path.resolve('metadata')
|
||||||
const UID = 99
|
const UID = process.env.AUDIOBOOKSHELF_UID
|
||||||
const GID = 100
|
const GID = process.env.AUDIOBOOKSHELF_GID
|
||||||
const SOURCE = options.source || 'debian'
|
const SOURCE = options.source || process.env.SOURCE || 'debian'
|
||||||
|
|
||||||
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || ''
|
const ROUTER_BASE_PATH = process.env.ROUTER_BASE_PATH || ''
|
||||||
|
|
||||||
console.log(process.env.NODE_ENV, 'Config', CONFIG_PATH, METADATA_PATH)
|
console.log(process.env.NODE_ENV, 'Config', CONFIG_PATH, METADATA_PATH)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<br />
|
<br />
|
||||||
<a href="https://audiobookshelf.org/docs">Documentation</a>
|
<a href="https://audiobookshelf.org/docs">Documentation</a>
|
||||||
·
|
·
|
||||||
<a href="https://audiobookshelf.org/install">Install Guides</a>
|
<a href="https://audiobookshelf.org/guides">User Guides</a>
|
||||||
·
|
·
|
||||||
<a href="https://audiobookshelf.org/support">Support</a>
|
<a href="https://audiobookshelf.org/support">Support</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -36,14 +36,17 @@ Audiobookshelf is a self-hosted audiobook and podcast server.
|
|||||||
|
|
||||||
Is there a feature you are looking for? [Suggest it](https://github.com/advplyr/audiobookshelf/issues/new/choose)
|
Is there a feature you are looking for? [Suggest it](https://github.com/advplyr/audiobookshelf/issues/new/choose)
|
||||||
|
|
||||||
Join us on [discord](https://discord.gg/pJsjuNCKRq) or [matrix](https://matrix.to/#/#audiobookshelf:matrix.org)
|
Join us on [Discord](https://discord.gg/pJsjuNCKRq) or [Matrix](https://matrix.to/#/#audiobookshelf:matrix.org)
|
||||||
|
|
||||||
### Android App (beta)
|
### Android App (beta)
|
||||||
Try it out on the [Google Play Store](https://play.google.com/store/apps/details?id=com.audiobookshelf.app)
|
Try it out on the [Google Play Store](https://play.google.com/store/apps/details?id=com.audiobookshelf.app)
|
||||||
|
|
||||||
### iOS App (early beta)
|
### iOS App (beta)
|
||||||
Available using Test Flight: https://testflight.apple.com/join/wiic7QIW - [Join the discussion](https://github.com/advplyr/audiobookshelf-app/discussions/60)
|
Available using Test Flight: https://testflight.apple.com/join/wiic7QIW - [Join the discussion](https://github.com/advplyr/audiobookshelf-app/discussions/60)
|
||||||
|
|
||||||
|
### Build your own tools & clients
|
||||||
|
Check out the [API documentation](https://api.audiobookshelf.org/)
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
<img alt="Library Screenshot" src="https://github.com/advplyr/audiobookshelf/raw/master/images/LibraryStreamSquare.png" />
|
<img alt="Library Screenshot" src="https://github.com/advplyr/audiobookshelf/raw/master/images/LibraryStreamSquare.png" />
|
||||||
@@ -54,106 +57,13 @@ Available using Test Flight: https://testflight.apple.com/join/wiic7QIW - [Join
|
|||||||
|
|
||||||
#### Directory structure and folder names are important to Audiobookshelf!
|
#### Directory structure and folder names are important to Audiobookshelf!
|
||||||
|
|
||||||
See [documentation](https://audiobookshelf.org/docs) for supported directory structure, folder naming conventions, and audio file metadata usage.
|
See [documentation](https://audiobookshelf.org/docs#book-directory-structure) for supported directory structure, folder naming conventions, and audio file metadata usage.
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
### Docker Install
|
See [install docs](https://www.audiobookshelf.org/docs)
|
||||||
Available in Unraid Community Apps
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker pull ghcr.io/advplyr/audiobookshelf:latest
|
|
||||||
|
|
||||||
docker run -d \
|
|
||||||
-e AUDIOBOOKSHELF_UID=99 \
|
|
||||||
-e AUDIOBOOKSHELF_GID=100 \
|
|
||||||
-p 13378:80 \
|
|
||||||
-v </path/to/audiobooks>:/audiobooks \
|
|
||||||
-v </path/to/podcasts>:/podcasts \
|
|
||||||
-v </path/to/config>:/config \
|
|
||||||
-v </path/to/metadata>:/metadata \
|
|
||||||
--name audiobookshelf \
|
|
||||||
ghcr.io/advplyr/audiobookshelf:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Update
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker stop audiobookshelf
|
|
||||||
docker rm audiobookshelf
|
|
||||||
docker pull ghcr.io/advplyr/audiobookshelf:latest
|
|
||||||
docker start audiobookshelf
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running with Docker Compose
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
### docker-compose.yml ###
|
|
||||||
services:
|
|
||||||
audiobookshelf:
|
|
||||||
container_name: audiobookshelf
|
|
||||||
image: ghcr.io/advplyr/audiobookshelf:latest
|
|
||||||
environment:
|
|
||||||
- AUDIOBOOKSHELF_UID=99
|
|
||||||
- AUDIOBOOKSHELF_GID=100
|
|
||||||
ports:
|
|
||||||
- 13378:80
|
|
||||||
volumes:
|
|
||||||
- </path/to/audiobooks>:/audiobooks
|
|
||||||
- </path/to/podcasts>:/podcasts
|
|
||||||
- </path/to/config>:/config
|
|
||||||
- </path/to/metadata>:/metadata
|
|
||||||
```
|
|
||||||
|
|
||||||
### Docker Compose Update
|
|
||||||
|
|
||||||
Depending on the version of Docker Compose please run one of the two commands. If not sure on which version you are running you can run the following command and check.
|
|
||||||
|
|
||||||
#### Version Check
|
|
||||||
|
|
||||||
docker-compose --version or docker compose version
|
|
||||||
|
|
||||||
#### v2 Update
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker compose --file <path/to/config>/docker-compose.yml pull
|
|
||||||
docker compose --file <path/to/config>/docker-compose.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
#### V1 Update
|
|
||||||
```bash
|
|
||||||
docker-compose --file <path/to/config>/docker-compose.yml pull
|
|
||||||
docker-compose --file <path/to/config>/docker-compose.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### Linux (amd64) Install
|
|
||||||
|
|
||||||
Debian package will use this config file `/etc/default/audiobookshelf` if exists. The install will create a user and group named `audiobookshelf`.
|
|
||||||
|
|
||||||
### Ubuntu Install via PPA
|
|
||||||
|
|
||||||
A PPA is hosted on [github](https://github.com/advplyr/audiobookshelf-ppa)
|
|
||||||
|
|
||||||
See [install docs](https://www.audiobookshelf.org/install/#ubuntu)
|
|
||||||
|
|
||||||
### Install via debian package
|
|
||||||
|
|
||||||
Get the `deb` file from the [github repo](https://github.com/advplyr/audiobookshelf-ppa).
|
|
||||||
|
|
||||||
See [install docs](https://www.audiobookshelf.org/install#debian)
|
|
||||||
|
|
||||||
|
|
||||||
#### Linux file locations
|
|
||||||
|
|
||||||
Project directory: `/usr/share/audiobookshelf/`
|
|
||||||
|
|
||||||
Config file: `/etc/default/audiobookshelf`
|
|
||||||
|
|
||||||
System Service: `/lib/systemd/system/audiobookshelf.service`
|
|
||||||
|
|
||||||
Ffmpeg static build: `/usr/lib/audiobookshelf-ffmpeg/`
|
|
||||||
|
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
@@ -161,6 +71,8 @@ Ffmpeg static build: `/usr/lib/audiobookshelf-ffmpeg/`
|
|||||||
|
|
||||||
#### Important! Audiobookshelf requires a websocket connection.
|
#### Important! Audiobookshelf requires a websocket connection.
|
||||||
|
|
||||||
|
#### Note: Subfolder paths (e.g. /audiobooks) are not supported yet. See [issue](https://github.com/advplyr/audiobookshelf/issues/385)
|
||||||
|
|
||||||
### NGINX Proxy Manager
|
### NGINX Proxy Manager
|
||||||
|
|
||||||
Toggle websockets support.
|
Toggle websockets support.
|
||||||
@@ -261,6 +173,16 @@ Middleware relating to CORS will cause the app to report Unknown Error when logg
|
|||||||
From [@Dondochaka](https://discord.com/channels/942908292873723984/942914154254176257/945074590374318170) and [@BeastleeUK](https://discord.com/channels/942908292873723984/942914154254176257/970366039294611506)
|
From [@Dondochaka](https://discord.com/channels/942908292873723984/942914154254176257/945074590374318170) and [@BeastleeUK](https://discord.com/channels/942908292873723984/942914154254176257/970366039294611506)
|
||||||
<br />
|
<br />
|
||||||
|
|
||||||
|
### Example Caddyfile - [Caddy Reverse Proxy](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy)
|
||||||
|
|
||||||
|
```
|
||||||
|
subdomain.domain.com {
|
||||||
|
encode gzip zstd
|
||||||
|
reverse_proxy <LOCAL_IP>:<PORT>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
# Run from source
|
# Run from source
|
||||||
|
|
||||||
[See discussion](https://github.com/advplyr/audiobookshelf/discussions/259#discussioncomment-1869729)
|
[See discussion](https://github.com/advplyr/audiobookshelf/discussions/259#discussioncomment-1869729)
|
||||||
|
|||||||
+4
-5
@@ -115,17 +115,16 @@ class Auth {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserLoginResponsePayload(user, feeds) {
|
getUserLoginResponsePayload(user) {
|
||||||
return {
|
return {
|
||||||
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(),
|
||||||
feeds,
|
|
||||||
Source: global.Source
|
Source: global.Source
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(req, res, feeds) {
|
async login(req, res) {
|
||||||
const ipAddress = requestIp.getClientIp(req)
|
const ipAddress = requestIp.getClientIp(req)
|
||||||
var username = (req.body.username || '').toLowerCase()
|
var username = (req.body.username || '').toLowerCase()
|
||||||
var password = req.body.password || ''
|
var password = req.body.password || ''
|
||||||
@@ -146,14 +145,14 @@ class Auth {
|
|||||||
if (password) {
|
if (password) {
|
||||||
return res.status(401).send('Invalid root password (hint: there is none)')
|
return res.status(401).send('Invalid root password (hint: there is none)')
|
||||||
} else {
|
} else {
|
||||||
return res.json(this.getUserLoginResponsePayload(user, feeds))
|
return res.json(this.getUserLoginResponsePayload(user))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check password match
|
// Check password match
|
||||||
var compare = await bcrypt.compare(password, user.pash)
|
var compare = await bcrypt.compare(password, user.pash)
|
||||||
if (compare) {
|
if (compare) {
|
||||||
res.json(this.getUserLoginResponsePayload(user, feeds))
|
res.json(this.getUserLoginResponsePayload(user))
|
||||||
} else {
|
} else {
|
||||||
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
|
Logger.warn(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit} from ${ipAddress}`)
|
||||||
if (req.rateLimit.remaining <= 2) {
|
if (req.rateLimit.remaining <= 2) {
|
||||||
|
|||||||
+9
-9
@@ -107,7 +107,7 @@ class Db {
|
|||||||
checkPreviousVersion() {
|
checkPreviousVersion() {
|
||||||
return this.settingsDb.select(() => true).then((results) => {
|
return this.settingsDb.select(() => true).then((results) => {
|
||||||
if (results.data && results.data.length) {
|
if (results.data && results.data.length) {
|
||||||
var serverSettings = results.data.find(s => s.id === 'server-settings')
|
const serverSettings = results.data.find(s => s.id === 'server-settings')
|
||||||
if (serverSettings && serverSettings.version && serverSettings.version !== version) {
|
if (serverSettings && serverSettings.version && serverSettings.version !== version) {
|
||||||
return serverSettings.version
|
return serverSettings.version
|
||||||
}
|
}
|
||||||
@@ -163,7 +163,7 @@ class Db {
|
|||||||
const p4 = this.settingsDb.select(() => true).then(async (results) => {
|
const p4 = this.settingsDb.select(() => true).then(async (results) => {
|
||||||
if (results.data && results.data.length) {
|
if (results.data && results.data.length) {
|
||||||
this.settings = results.data
|
this.settings = results.data
|
||||||
var serverSettings = this.settings.find(s => s.id === 'server-settings')
|
const serverSettings = this.settings.find(s => s.id === 'server-settings')
|
||||||
if (serverSettings) {
|
if (serverSettings) {
|
||||||
this.serverSettings = new ServerSettings(serverSettings)
|
this.serverSettings = new ServerSettings(serverSettings)
|
||||||
|
|
||||||
@@ -185,7 +185,7 @@ class Db {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var notificationSettings = this.settings.find(s => s.id === 'notification-settings')
|
const notificationSettings = this.settings.find(s => s.id === 'notification-settings')
|
||||||
if (notificationSettings) {
|
if (notificationSettings) {
|
||||||
this.notificationSettings = new NotificationSettings(notificationSettings)
|
this.notificationSettings = new NotificationSettings(notificationSettings)
|
||||||
}
|
}
|
||||||
@@ -280,7 +280,7 @@ class Db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAllEntities(entityName) {
|
getAllEntities(entityName) {
|
||||||
var entityDb = this.getEntityDb(entityName)
|
const entityDb = this.getEntityDb(entityName)
|
||||||
return entityDb.select(() => true).then((results) => results.data).catch((error) => {
|
return entityDb.select(() => true).then((results) => results.data).catch((error) => {
|
||||||
Logger.error(`[DB] Failed to get all ${entityName}`, error)
|
Logger.error(`[DB] Failed to get all ${entityName}`, error)
|
||||||
return null
|
return null
|
||||||
@@ -371,16 +371,16 @@ class Db {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateEntity(entityName, entity) {
|
updateEntity(entityName, entity) {
|
||||||
var entityDb = this.getEntityDb(entityName)
|
const entityDb = this.getEntityDb(entityName)
|
||||||
|
|
||||||
var jsonEntity = entity
|
let jsonEntity = entity
|
||||||
if (entity && entity.toJSON) {
|
if (entity && entity.toJSON) {
|
||||||
jsonEntity = entity.toJSON()
|
jsonEntity = entity.toJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
return entityDb.update((record) => record.id === entity.id, () => jsonEntity).then((results) => {
|
return entityDb.update((record) => record.id === entity.id, () => jsonEntity).then((results) => {
|
||||||
Logger.debug(`[DB] Updated ${entityName}: ${results.updated}`)
|
Logger.debug(`[DB] Updated ${entityName}: ${results.updated}`)
|
||||||
var arrayKey = this.getEntityArrayKey(entityName)
|
const arrayKey = this.getEntityArrayKey(entityName)
|
||||||
if (this[arrayKey]) {
|
if (this[arrayKey]) {
|
||||||
this[arrayKey] = this[arrayKey].map(e => {
|
this[arrayKey] = this[arrayKey].map(e => {
|
||||||
return e.id === entity.id ? entity : e
|
return e.id === entity.id ? entity : e
|
||||||
@@ -410,10 +410,10 @@ class Db {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
removeEntities(entityName, selectFunc) {
|
removeEntities(entityName, selectFunc, silent = false) {
|
||||||
var entityDb = this.getEntityDb(entityName)
|
var entityDb = this.getEntityDb(entityName)
|
||||||
return entityDb.delete(selectFunc).then((results) => {
|
return entityDb.delete(selectFunc).then((results) => {
|
||||||
Logger.debug(`[DB] Deleted entities ${entityName}: ${results.deleted}`)
|
if (!silent) Logger.debug(`[DB] Deleted entities ${entityName}: ${results.deleted}`)
|
||||||
var arrayKey = this.getEntityArrayKey(entityName)
|
var arrayKey = this.getEntityArrayKey(entityName)
|
||||||
if (this[arrayKey]) {
|
if (this[arrayKey]) {
|
||||||
this[arrayKey] = this[arrayKey].filter(e => {
|
this[arrayKey] = this[arrayKey].filter(e => {
|
||||||
|
|||||||
+15
-5
@@ -22,6 +22,15 @@ class Logger {
|
|||||||
return 'UNKNOWN'
|
return 'UNKNOWN'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get source() {
|
||||||
|
try {
|
||||||
|
throw new Error()
|
||||||
|
} catch (error) {
|
||||||
|
const regex = global.isWin ? /^.*\\([^\\:]*:[0-9]*):[0-9]*\)*/ : /^.*\/([^/:]*:[0-9]*):[0-9]*\)*/
|
||||||
|
return error.stack.split('\n')[3].replace(regex, '$1')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getLogLevelString(level) {
|
getLogLevelString(level) {
|
||||||
for (const key in LogLevel) {
|
for (const key in LogLevel) {
|
||||||
if (LogLevel[key] === level) {
|
if (LogLevel[key] === level) {
|
||||||
@@ -55,6 +64,7 @@ class Logger {
|
|||||||
handleLog(level, args) {
|
handleLog(level, args) {
|
||||||
const logObj = {
|
const logObj = {
|
||||||
timestamp: this.timestamp,
|
timestamp: this.timestamp,
|
||||||
|
source: this.source,
|
||||||
message: args.join(' '),
|
message: args.join(' '),
|
||||||
levelName: this.getLogLevelString(level),
|
levelName: this.getLogLevelString(level),
|
||||||
level
|
level
|
||||||
@@ -84,30 +94,30 @@ class Logger {
|
|||||||
|
|
||||||
debug(...args) {
|
debug(...args) {
|
||||||
if (this.logLevel > LogLevel.DEBUG) return
|
if (this.logLevel > LogLevel.DEBUG) return
|
||||||
console.debug(`[${this.timestamp}] DEBUG:`, ...args)
|
console.debug(`[${this.timestamp}] DEBUG:`, ...args, `(${this.source})`)
|
||||||
this.handleLog(LogLevel.DEBUG, args)
|
this.handleLog(LogLevel.DEBUG, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
info(...args) {
|
info(...args) {
|
||||||
if (this.logLevel > LogLevel.INFO) return
|
if (this.logLevel > LogLevel.INFO) return
|
||||||
console.info(`[${this.timestamp}] INFO:`, ...args)
|
console.info(`[${this.timestamp}] INFO:`, ...args)
|
||||||
this.handleLog(LogLevel.INFO, args)
|
this.handleLog(LogLevel.INFO, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
warn(...args) {
|
warn(...args) {
|
||||||
if (this.logLevel > LogLevel.WARN) return
|
if (this.logLevel > LogLevel.WARN) return
|
||||||
console.warn(`[${this.timestamp}] WARN:`, ...args)
|
console.warn(`[${this.timestamp}] WARN:`, ...args, `(${this.source})`)
|
||||||
this.handleLog(LogLevel.WARN, args)
|
this.handleLog(LogLevel.WARN, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
error(...args) {
|
error(...args) {
|
||||||
if (this.logLevel > LogLevel.ERROR) return
|
if (this.logLevel > LogLevel.ERROR) return
|
||||||
console.error(`[${this.timestamp}] ERROR:`, ...args)
|
console.error(`[${this.timestamp}] ERROR:`, ...args, `(${this.source})`)
|
||||||
this.handleLog(LogLevel.ERROR, args)
|
this.handleLog(LogLevel.ERROR, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
fatal(...args) {
|
fatal(...args) {
|
||||||
console.error(`[${this.timestamp}] FATAL:`, ...args)
|
console.error(`[${this.timestamp}] FATAL:`, ...args, `(${this.source})`)
|
||||||
this.handleLog(LogLevel.FATAL, args)
|
this.handleLog(LogLevel.FATAL, args)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-11
@@ -10,6 +10,7 @@ const { version } = require('../package.json')
|
|||||||
// Utils
|
// Utils
|
||||||
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 Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
|
|
||||||
const Auth = require('./Auth')
|
const Auth = require('./Auth')
|
||||||
@@ -34,23 +35,20 @@ const AudioMetadataMangaer = require('./managers/AudioMetadataManager')
|
|||||||
const RssFeedManager = require('./managers/RssFeedManager')
|
const RssFeedManager = require('./managers/RssFeedManager')
|
||||||
const CronManager = require('./managers/CronManager')
|
const CronManager = require('./managers/CronManager')
|
||||||
const TaskManager = require('./managers/TaskManager')
|
const TaskManager = require('./managers/TaskManager')
|
||||||
|
const EBookManager = require('./managers/EBookManager')
|
||||||
|
|
||||||
class Server {
|
class Server {
|
||||||
constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH, ROUTER_BASE_PATH) {
|
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
|
||||||
global.Uid = isNaN(UID) ? 0 : Number(UID)
|
global.isWin = process.platform === 'win32'
|
||||||
global.Gid = isNaN(GID) ? 0 : Number(GID)
|
global.Uid = isNaN(UID) ? undefined : Number(UID)
|
||||||
global.ConfigPath = Path.normalize(CONFIG_PATH)
|
global.Gid = isNaN(GID) ? undefined : Number(GID)
|
||||||
global.MetadataPath = Path.normalize(METADATA_PATH)
|
global.ConfigPath = fileUtils.filePathToPOSIX(Path.normalize(CONFIG_PATH))
|
||||||
|
global.MetadataPath = fileUtils.filePathToPOSIX(Path.normalize(METADATA_PATH))
|
||||||
global.RouterBasePath = ROUTER_BASE_PATH
|
global.RouterBasePath = ROUTER_BASE_PATH
|
||||||
|
global.XAccel = process.env.USE_X_ACCEL
|
||||||
// Fix backslash if not on Windows
|
|
||||||
if (process.platform !== 'win32') {
|
|
||||||
global.ConfigPath = global.ConfigPath.replace(/\\/g, '/')
|
|
||||||
global.MetadataPath = global.MetadataPath.replace(/\\/g, '/')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!fs.pathExistsSync(global.ConfigPath)) {
|
if (!fs.pathExistsSync(global.ConfigPath)) {
|
||||||
fs.mkdirSync(global.ConfigPath)
|
fs.mkdirSync(global.ConfigPath)
|
||||||
@@ -77,6 +75,7 @@ class Server {
|
|||||||
this.podcastManager = new PodcastManager(this.db, this.watcher, this.notificationManager)
|
this.podcastManager = new PodcastManager(this.db, this.watcher, this.notificationManager)
|
||||||
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.eBookManager = new EBookManager(this.db)
|
||||||
|
|
||||||
this.scanner = new Scanner(this.db, this.coverManager)
|
this.scanner = new Scanner(this.db, this.coverManager)
|
||||||
this.cronManager = new CronManager(this.db, this.scanner, this.podcastManager)
|
this.cronManager = new CronManager(this.db, this.scanner, this.podcastManager)
|
||||||
@@ -124,6 +123,7 @@ class Server {
|
|||||||
|
|
||||||
await this.backupManager.init()
|
await this.backupManager.init()
|
||||||
await this.logManager.init()
|
await this.logManager.init()
|
||||||
|
await this.apiRouter.checkRemoveEmptySeries(this.db.series) // Remove empty series
|
||||||
await this.rssFeedManager.init()
|
await this.rssFeedManager.init()
|
||||||
this.cronManager.init()
|
this.cronManager.init()
|
||||||
|
|
||||||
@@ -143,6 +143,7 @@ class Server {
|
|||||||
const app = express()
|
const app = express()
|
||||||
const router = express.Router()
|
const router = express.Router()
|
||||||
app.use(global.RouterBasePath, router)
|
app.use(global.RouterBasePath, router)
|
||||||
|
app.disable('x-powered-by')
|
||||||
|
|
||||||
this.server = http.createServer(app)
|
this.server = http.createServer(app)
|
||||||
|
|
||||||
@@ -212,7 +213,7 @@ class Server {
|
|||||||
]
|
]
|
||||||
dyanimicRoutes.forEach((route) => router.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))
|
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, this.rssFeedManager.feedsArray))
|
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) => {
|
||||||
if (this.db.hasRootUser) {
|
if (this.db.hasRootUser) {
|
||||||
|
|||||||
+8
-6
@@ -2,6 +2,8 @@ const EventEmitter = require('events')
|
|||||||
const Watcher = require('./libs/watcher/watcher')
|
const Watcher = require('./libs/watcher/watcher')
|
||||||
const Logger = require('./Logger')
|
const Logger = require('./Logger')
|
||||||
|
|
||||||
|
const { filePathToPOSIX } = require('./utils/fileUtils')
|
||||||
|
|
||||||
class FolderWatcher extends EventEmitter {
|
class FolderWatcher extends EventEmitter {
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
@@ -143,23 +145,23 @@ class FolderWatcher extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addFileUpdate(libraryId, path, type) {
|
addFileUpdate(libraryId, path, type) {
|
||||||
path = path.replace(/\\/g, '/')
|
path = filePathToPOSIX(path)
|
||||||
if (this.pendingFilePaths.includes(path)) return
|
if (this.pendingFilePaths.includes(path)) return
|
||||||
|
|
||||||
// Get file library
|
// Get file library
|
||||||
var libwatcher = this.libraryWatchers.find(lw => lw.id === libraryId)
|
const libwatcher = this.libraryWatchers.find(lw => lw.id === libraryId)
|
||||||
if (!libwatcher) {
|
if (!libwatcher) {
|
||||||
Logger.error(`[Watcher] Invalid library id from watcher ${libraryId}`)
|
Logger.error(`[Watcher] Invalid library id from watcher ${libraryId}`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get file folder
|
// Get file folder
|
||||||
var folder = libwatcher.folders.find(fold => path.startsWith(fold.fullPath.replace(/\\/g, '/')))
|
const folder = libwatcher.folders.find(fold => path.startsWith(filePathToPOSIX(fold.fullPath)))
|
||||||
if (!folder) {
|
if (!folder) {
|
||||||
Logger.error(`[Watcher] New file folder not found in library "${libwatcher.name}" with path "${path}"`)
|
Logger.error(`[Watcher] New file folder not found in library "${libwatcher.name}" with path "${path}"`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var folderFullPath = folder.fullPath.replace(/\\/g, '/')
|
const folderFullPath = filePathToPOSIX(folder.fullPath)
|
||||||
|
|
||||||
var relPath = path.replace(folderFullPath, '')
|
var relPath = path.replace(folderFullPath, '')
|
||||||
|
|
||||||
@@ -189,12 +191,12 @@ class FolderWatcher extends EventEmitter {
|
|||||||
|
|
||||||
checkShouldIgnorePath(path) {
|
checkShouldIgnorePath(path) {
|
||||||
return !!this.ignoreDirs.find(dirpath => {
|
return !!this.ignoreDirs.find(dirpath => {
|
||||||
return path.replace(/\\/g, '/').startsWith(dirpath)
|
return filePathToPOSIX(path).startsWith(dirpath)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanDirPath(path) {
|
cleanDirPath(path) {
|
||||||
var path = path.replace(/\\/g, '/')
|
path = filePathToPOSIX(path)
|
||||||
if (path.endsWith('/')) path = path.slice(0, -1)
|
if (path.endsWith('/')) path = path.slice(0, -1)
|
||||||
return path
|
return path
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,9 +80,17 @@ class AuthorController {
|
|||||||
await this.cacheManager.purgeImageCache(req.author.id) // Purge cache
|
await this.cacheManager.purgeImageCache(req.author.id) // Purge cache
|
||||||
}
|
}
|
||||||
payload.imagePath = imageData.path
|
payload.imagePath = imageData.path
|
||||||
payload.relImagePath = imageData.relPath
|
|
||||||
hasUpdated = true
|
hasUpdated = true
|
||||||
}
|
}
|
||||||
|
} else if (payload.imagePath && payload.imagePath !== req.author.imagePath) { // Changing image path locally
|
||||||
|
if (!await fs.pathExists(payload.imagePath)) { // Make sure image path exists
|
||||||
|
Logger.error(`[AuthorController] Image path does not exist: "${payload.imagePath}"`)
|
||||||
|
return res.status(400).send('Author image path does not exist')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.author.imagePath) {
|
||||||
|
await this.cacheManager.purgeImageCache(req.author.id) // Purge cache
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +128,8 @@ class AuthorController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
|
req.author.updatedAt = Date.now()
|
||||||
|
|
||||||
if (authorNameUpdate) { // Update author name on all books
|
if (authorNameUpdate) { // Update author name on all books
|
||||||
const itemsWithAuthor = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id))
|
const itemsWithAuthor = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasAuthor(req.author.id))
|
||||||
itemsWithAuthor.forEach(libraryItem => {
|
itemsWithAuthor.forEach(libraryItem => {
|
||||||
@@ -181,7 +191,6 @@ class AuthorController {
|
|||||||
var imageData = await this.authorFinder.saveAuthorImage(req.author.id, authorData.image)
|
var imageData = await this.authorFinder.saveAuthorImage(req.author.id, authorData.image)
|
||||||
if (imageData) {
|
if (imageData) {
|
||||||
req.author.imagePath = imageData.path
|
req.author.imagePath = imageData.path
|
||||||
req.author.relImagePath = imageData.relPath
|
|
||||||
hasUpdates = true
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,13 +26,22 @@ class CollectionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
findOne(req, res) {
|
findOne(req, res) {
|
||||||
res.json(req.collection.toJSONExpanded(this.db.libraryItems))
|
const includeEntities = (req.query.include || '').split(',')
|
||||||
|
|
||||||
|
const collectionExpanded = req.collection.toJSONExpanded(this.db.libraryItems)
|
||||||
|
|
||||||
|
if (includeEntities.includes('rssfeed')) {
|
||||||
|
const feedData = this.rssFeedManager.findFeedForEntityId(collectionExpanded.id)
|
||||||
|
collectionExpanded.rssFeed = feedData ? feedData.toJSONMinified() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(collectionExpanded)
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
const collection = req.collection
|
const collection = req.collection
|
||||||
var wasUpdated = collection.update(req.body)
|
const wasUpdated = collection.update(req.body)
|
||||||
var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
const jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
||||||
if (wasUpdated) {
|
if (wasUpdated) {
|
||||||
await this.db.updateEntity('collection', collection)
|
await this.db.updateEntity('collection', collection)
|
||||||
SocketAuthority.emitter('collection_updated', jsonExpanded)
|
SocketAuthority.emitter('collection_updated', jsonExpanded)
|
||||||
@@ -42,7 +51,11 @@ class CollectionController {
|
|||||||
|
|
||||||
async delete(req, res) {
|
async delete(req, res) {
|
||||||
const collection = req.collection
|
const collection = req.collection
|
||||||
var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
const jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
||||||
|
|
||||||
|
// Close rss feed - remove from db and emit socket event
|
||||||
|
await this.rssFeedManager.closeFeedForEntityId(collection.id)
|
||||||
|
|
||||||
await this.db.removeEntity('collection', collection.id)
|
await this.db.removeEntity('collection', collection.id)
|
||||||
SocketAuthority.emitter('collection_removed', jsonExpanded)
|
SocketAuthority.emitter('collection_removed', jsonExpanded)
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
@@ -50,7 +63,7 @@ class CollectionController {
|
|||||||
|
|
||||||
async addBook(req, res) {
|
async addBook(req, res) {
|
||||||
const collection = req.collection
|
const collection = req.collection
|
||||||
var libraryItem = this.db.libraryItems.find(li => li.id === req.body.id)
|
const libraryItem = this.db.libraryItems.find(li => li.id === req.body.id)
|
||||||
if (!libraryItem) {
|
if (!libraryItem) {
|
||||||
return res.status(500).send('Book not found')
|
return res.status(500).send('Book not found')
|
||||||
}
|
}
|
||||||
@@ -61,7 +74,7 @@ class CollectionController {
|
|||||||
return res.status(500).send('Book already in collection')
|
return res.status(500).send('Book already in collection')
|
||||||
}
|
}
|
||||||
collection.addBook(req.body.id)
|
collection.addBook(req.body.id)
|
||||||
var jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
const jsonExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
||||||
await this.db.updateEntity('collection', collection)
|
await this.db.updateEntity('collection', collection)
|
||||||
SocketAuthority.emitter('collection_updated', jsonExpanded)
|
SocketAuthority.emitter('collection_updated', jsonExpanded)
|
||||||
res.json(jsonExpanded)
|
res.json(jsonExpanded)
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
const Logger = require('../Logger')
|
||||||
|
const { isNullOrNaN } = require('../utils/index')
|
||||||
|
|
||||||
|
class EBookController {
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
async getEbookInfo(req, res) {
|
||||||
|
const isDev = req.query.dev == 1
|
||||||
|
const json = await this.eBookManager.getBookInfo(req.libraryItem, req.user, isDev)
|
||||||
|
res.json(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEbookPage(req, res) {
|
||||||
|
if (isNullOrNaN(req.params.page)) {
|
||||||
|
return res.status(400).send('Invalid page params')
|
||||||
|
}
|
||||||
|
const isDev = req.query.dev == 1
|
||||||
|
const pageIndex = Number(req.params.page)
|
||||||
|
const page = await this.eBookManager.getBookPage(req.libraryItem, req.user, pageIndex, isDev)
|
||||||
|
if (!page) {
|
||||||
|
return res.status(500).send('Failed to get page')
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
async getEbookResource(req, res) {
|
||||||
|
if (!req.query.path) {
|
||||||
|
return res.status(400).send('Invalid query path')
|
||||||
|
}
|
||||||
|
const isDev = req.query.dev == 1
|
||||||
|
this.eBookManager.getBookResource(req.libraryItem, req.user, req.query.path, isDev, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware(req, res, next) {
|
||||||
|
const item = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||||
|
if (!item || !item.media) return res.sendStatus(404)
|
||||||
|
|
||||||
|
// Check user can access this library item
|
||||||
|
if (!req.user.checkCanAccessLibraryItem(item)) {
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!item.isBook || !item.media.ebookFile) {
|
||||||
|
return res.status(400).send('Invalid ebook library item')
|
||||||
|
}
|
||||||
|
|
||||||
|
req.libraryItem = item
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = new EBookController()
|
||||||
@@ -170,8 +170,11 @@ class LibraryController {
|
|||||||
// api/libraries/:id/items
|
// api/libraries/:id/items
|
||||||
// TODO: Optimize this method, items are iterated through several times but can be combined
|
// TODO: Optimize this method, items are iterated through several times but can be combined
|
||||||
getLibraryItems(req, res) {
|
getLibraryItems(req, res) {
|
||||||
var libraryItems = req.libraryItems
|
let libraryItems = req.libraryItems
|
||||||
var payload = {
|
|
||||||
|
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
|
||||||
|
|
||||||
|
const payload = {
|
||||||
results: [],
|
results: [],
|
||||||
total: libraryItems.length,
|
total: libraryItems.length,
|
||||||
limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
|
limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
|
||||||
@@ -181,7 +184,8 @@ class LibraryController {
|
|||||||
filterBy: req.query.filter,
|
filterBy: req.query.filter,
|
||||||
mediaType: req.library.mediaType,
|
mediaType: req.library.mediaType,
|
||||||
minified: req.query.minified === '1',
|
minified: req.query.minified === '1',
|
||||||
collapseseries: req.query.collapseseries === '1'
|
collapseseries: req.query.collapseseries === '1',
|
||||||
|
include: include.join(',')
|
||||||
}
|
}
|
||||||
const mediaIsBook = payload.mediaType === 'book'
|
const mediaIsBook = payload.mediaType === 'book'
|
||||||
|
|
||||||
@@ -219,7 +223,7 @@ class LibraryController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Step 3 - Sort the retrieved library items.
|
// Step 3 - Sort the retrieved library items.
|
||||||
var sortArray = []
|
const sortArray = []
|
||||||
|
|
||||||
// When on the series page, sort by sequence only
|
// When on the series page, sort by sequence only
|
||||||
if (payload.sortBy === 'book.volumeNumber') payload.sortBy = null // TODO: Remove temp fix after mobile release 0.9.60
|
if (payload.sortBy === 'book.volumeNumber') payload.sortBy = null // TODO: Remove temp fix after mobile release 0.9.60
|
||||||
@@ -294,13 +298,13 @@ class LibraryController {
|
|||||||
|
|
||||||
// Step 3.5: Limit items
|
// Step 3.5: Limit items
|
||||||
if (payload.limit) {
|
if (payload.limit) {
|
||||||
var startIndex = payload.page * payload.limit
|
const startIndex = payload.page * payload.limit
|
||||||
libraryItems = libraryItems.slice(startIndex, startIndex + payload.limit)
|
libraryItems = libraryItems.slice(startIndex, startIndex + payload.limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Step 4 - Transform the items to pass to the client side
|
// Step 4 - Transform the items to pass to the client side
|
||||||
payload.results = libraryItems.map(li => {
|
payload.results = libraryItems.map(li => {
|
||||||
let json = payload.minified ? li.toJSONMinified() : li.toJSON()
|
const json = payload.minified ? li.toJSONMinified() : li.toJSON()
|
||||||
|
|
||||||
if (li.collapsedSeries) {
|
if (li.collapsedSeries) {
|
||||||
json.collapsedSeries = {
|
json.collapsedSeries = {
|
||||||
@@ -315,7 +319,7 @@ class LibraryController {
|
|||||||
// series represents in the filtered series
|
// series represents in the filtered series
|
||||||
if (filterSeries) {
|
if (filterSeries) {
|
||||||
json.collapsedSeries.seriesSequenceList =
|
json.collapsedSeries.seriesSequenceList =
|
||||||
naturalSort(li.collapsedSeries.books.map(b => b.filterSeriesSequence)).asc()
|
naturalSort(li.collapsedSeries.books.filter(b => b.filterSeriesSequence).map(b => b.filterSeriesSequence)).asc()
|
||||||
.reduce((ranges, currentSequence) => {
|
.reduce((ranges, currentSequence) => {
|
||||||
let lastRange = ranges.at(-1)
|
let lastRange = ranges.at(-1)
|
||||||
let isNumber = /^(\d+|\d+\.\d*|\d*\.\d+)$/.test(currentSequence)
|
let isNumber = /^(\d+|\d+\.\d*|\d*\.\d+)$/.test(currentSequence)
|
||||||
@@ -333,9 +337,17 @@ class LibraryController {
|
|||||||
.map(r => r.start == r.end ? r.start : `${r.start}-${r.end}`)
|
.map(r => r.start == r.end ? r.start : `${r.start}-${r.end}`)
|
||||||
.join(', ')
|
.join(', ')
|
||||||
}
|
}
|
||||||
} else if (filterSeries) {
|
} else {
|
||||||
// If filtering by series, make sure to include the series metadata
|
// add rssFeed object if "include=rssfeed" was put in query string (only for non-collapsed series)
|
||||||
json.media.metadata.series = li.media.metadata.getSeries(filterSeries)
|
if (include.includes('rssfeed')) {
|
||||||
|
const feedData = this.rssFeedManager.findFeedForEntityId(json.id)
|
||||||
|
json.rssFeed = feedData ? feedData.toJSONMinified() : null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterSeries) {
|
||||||
|
// If filtering by series, make sure to include the series metadata
|
||||||
|
json.media.metadata.series = li.media.metadata.getSeries(filterSeries)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return json
|
return json
|
||||||
@@ -363,6 +375,9 @@ class LibraryController {
|
|||||||
// api/libraries/:id/series
|
// api/libraries/:id/series
|
||||||
async getAllSeriesForLibrary(req, res) {
|
async getAllSeriesForLibrary(req, res) {
|
||||||
const libraryItems = req.libraryItems
|
const libraryItems = req.libraryItems
|
||||||
|
|
||||||
|
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
results: [],
|
results: [],
|
||||||
total: 0,
|
total: 0,
|
||||||
@@ -371,7 +386,8 @@ class LibraryController {
|
|||||||
sortBy: req.query.sort,
|
sortBy: req.query.sort,
|
||||||
sortDesc: req.query.desc === '1',
|
sortDesc: req.query.desc === '1',
|
||||||
filterBy: req.query.filter,
|
filterBy: req.query.filter,
|
||||||
minified: req.query.minified === '1'
|
minified: req.query.minified === '1',
|
||||||
|
include: include.join(',')
|
||||||
}
|
}
|
||||||
|
|
||||||
let series = libraryHelpers.getSeriesFromBooks(libraryItems, this.db.series, null, payload.filterBy, req.user, payload.minified)
|
let series = libraryHelpers.getSeriesFromBooks(libraryItems, this.db.series, null, payload.filterBy, req.user, payload.minified)
|
||||||
@@ -396,19 +412,30 @@ class LibraryController {
|
|||||||
payload.total = series.length
|
payload.total = series.length
|
||||||
|
|
||||||
if (payload.limit) {
|
if (payload.limit) {
|
||||||
var startIndex = payload.page * payload.limit
|
const startIndex = payload.page * payload.limit
|
||||||
series = series.slice(startIndex, startIndex + payload.limit)
|
series = series.slice(startIndex, startIndex + payload.limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// add rssFeed when "include=rssfeed" is in query string
|
||||||
|
if (include.includes('rssfeed')) {
|
||||||
|
series = series.map((se) => {
|
||||||
|
const feedData = this.rssFeedManager.findFeedForEntityId(se.id)
|
||||||
|
se.rssFeed = feedData?.toJSONMinified() || null
|
||||||
|
return se
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
payload.results = series
|
payload.results = series
|
||||||
res.json(payload)
|
res.json(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// api/libraries/:id/collections
|
// api/libraries/:id/collections
|
||||||
async getCollectionsForLibrary(req, res) {
|
async getCollectionsForLibrary(req, res) {
|
||||||
var libraryItems = req.libraryItems
|
const libraryItems = req.libraryItems
|
||||||
|
|
||||||
var payload = {
|
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
|
||||||
|
|
||||||
|
const payload = {
|
||||||
results: [],
|
results: [],
|
||||||
total: 0,
|
total: 0,
|
||||||
limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
|
limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
|
||||||
@@ -416,20 +443,28 @@ class LibraryController {
|
|||||||
sortBy: req.query.sort,
|
sortBy: req.query.sort,
|
||||||
sortDesc: req.query.desc === '1',
|
sortDesc: req.query.desc === '1',
|
||||||
filterBy: req.query.filter,
|
filterBy: req.query.filter,
|
||||||
minified: req.query.minified === '1'
|
minified: req.query.minified === '1',
|
||||||
|
include: include.join(',')
|
||||||
}
|
}
|
||||||
|
|
||||||
var collections = this.db.collections.filter(c => c.libraryId === req.library.id).map(c => {
|
let collections = this.db.collections.filter(c => c.libraryId === req.library.id).map(c => {
|
||||||
var expanded = c.toJSONExpanded(libraryItems, payload.minified)
|
const expanded = c.toJSONExpanded(libraryItems, payload.minified)
|
||||||
|
|
||||||
// If all books restricted to user in this collection then hide this collection
|
// If all books restricted to user in this collection then hide this collection
|
||||||
if (!expanded.books.length && c.books.length) return null
|
if (!expanded.books.length && c.books.length) return null
|
||||||
|
|
||||||
|
if (include.includes('rssfeed')) {
|
||||||
|
const feedData = this.rssFeedManager.findFeedForEntityId(c.id)
|
||||||
|
expanded.rssFeed = feedData?.toJSONMinified() || null
|
||||||
|
}
|
||||||
|
|
||||||
return expanded
|
return expanded
|
||||||
}).filter(c => !!c)
|
}).filter(c => !!c)
|
||||||
|
|
||||||
payload.total = collections.length
|
payload.total = collections.length
|
||||||
|
|
||||||
if (payload.limit) {
|
if (payload.limit) {
|
||||||
var startIndex = payload.page * payload.limit
|
const startIndex = payload.page * payload.limit
|
||||||
collections = collections.slice(startIndex, startIndex + payload.limit)
|
collections = collections.slice(startIndex, startIndex + payload.limit)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,6 +492,32 @@ class LibraryController {
|
|||||||
res.json(payload)
|
res.json(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// api/libraries/:id/albums
|
||||||
|
async getAlbumsForLibrary(req, res) {
|
||||||
|
if (!req.library.isMusic) {
|
||||||
|
return res.status(400).send('Invalid library media type')
|
||||||
|
}
|
||||||
|
|
||||||
|
let libraryItems = this.db.libraryItems.filter(li => li.libraryId === req.library.id)
|
||||||
|
let albums = libraryHelpers.groupMusicLibraryItemsIntoAlbums(libraryItems)
|
||||||
|
albums = naturalSort(albums).asc(a => a.title) // Alphabetical by album title
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
results: [],
|
||||||
|
total: albums.length,
|
||||||
|
limit: req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 0,
|
||||||
|
page: req.query.page && !isNaN(req.query.page) ? Number(req.query.page) : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.limit) {
|
||||||
|
const startIndex = payload.page * payload.limit
|
||||||
|
albums = albums.slice(startIndex, startIndex + payload.limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload.results = albums
|
||||||
|
res.json(payload)
|
||||||
|
}
|
||||||
|
|
||||||
async getLibraryFilterData(req, res) {
|
async getLibraryFilterData(req, res) {
|
||||||
res.json(libraryHelpers.getDistinctFilterDataNew(req.libraryItems))
|
res.json(libraryHelpers.getDistinctFilterDataNew(req.libraryItems))
|
||||||
}
|
}
|
||||||
@@ -466,9 +527,10 @@ class LibraryController {
|
|||||||
async getLibraryUserPersonalizedOptimal(req, res) {
|
async getLibraryUserPersonalizedOptimal(req, res) {
|
||||||
const mediaType = req.library.mediaType
|
const mediaType = req.library.mediaType
|
||||||
const libraryItems = req.libraryItems
|
const libraryItems = req.libraryItems
|
||||||
const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 10
|
const limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) || 10 : 10
|
||||||
|
const include = (req.query.include || '').split(',').map(v => v.trim().toLowerCase()).filter(v => !!v)
|
||||||
|
|
||||||
const categories = libraryHelpers.buildPersonalizedShelves(req.user, libraryItems, mediaType, this.db.series, this.db.authors, limitPerShelf)
|
const categories = libraryHelpers.buildPersonalizedShelves(this, req.user, libraryItems, mediaType, limitPerShelf, include)
|
||||||
res.json(categories)
|
res.json(categories)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,16 +572,16 @@ class LibraryController {
|
|||||||
if (!req.query.q) {
|
if (!req.query.q) {
|
||||||
return res.status(400).send('No query string')
|
return res.status(400).send('No query string')
|
||||||
}
|
}
|
||||||
var libraryItems = req.libraryItems
|
const libraryItems = req.libraryItems
|
||||||
var maxResults = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
|
const maxResults = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
|
||||||
|
|
||||||
var itemMatches = []
|
const itemMatches = []
|
||||||
var authorMatches = {}
|
const authorMatches = {}
|
||||||
var seriesMatches = {}
|
const seriesMatches = {}
|
||||||
var tagMatches = {}
|
const tagMatches = {}
|
||||||
|
|
||||||
libraryItems.forEach((li) => {
|
libraryItems.forEach((li) => {
|
||||||
var queryResult = li.searchQuery(req.query.q)
|
const queryResult = li.searchQuery(req.query.q)
|
||||||
if (queryResult.matchKey) {
|
if (queryResult.matchKey) {
|
||||||
itemMatches.push({
|
itemMatches.push({
|
||||||
libraryItem: li.toJSONExpanded(),
|
libraryItem: li.toJSONExpanded(),
|
||||||
@@ -530,7 +592,7 @@ class LibraryController {
|
|||||||
if (queryResult.series && queryResult.series.length) {
|
if (queryResult.series && queryResult.series.length) {
|
||||||
queryResult.series.forEach((se) => {
|
queryResult.series.forEach((se) => {
|
||||||
if (!seriesMatches[se.id]) {
|
if (!seriesMatches[se.id]) {
|
||||||
var _series = this.db.series.find(_se => _se.id === se.id)
|
const _series = this.db.series.find(_se => _se.id === se.id)
|
||||||
if (_series) seriesMatches[se.id] = { series: _series.toJSON(), books: [li.toJSON()] }
|
if (_series) seriesMatches[se.id] = { series: _series.toJSON(), books: [li.toJSON()] }
|
||||||
} else {
|
} else {
|
||||||
seriesMatches[se.id].books.push(li.toJSON())
|
seriesMatches[se.id].books.push(li.toJSON())
|
||||||
@@ -540,7 +602,7 @@ class LibraryController {
|
|||||||
if (queryResult.authors && queryResult.authors.length) {
|
if (queryResult.authors && queryResult.authors.length) {
|
||||||
queryResult.authors.forEach((au) => {
|
queryResult.authors.forEach((au) => {
|
||||||
if (!authorMatches[au.id]) {
|
if (!authorMatches[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) {
|
||||||
authorMatches[au.id] = _author.toJSON()
|
authorMatches[au.id] = _author.toJSON()
|
||||||
authorMatches[au.id].numBooks = 1
|
authorMatches[au.id].numBooks = 1
|
||||||
@@ -560,8 +622,8 @@ class LibraryController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
var itemKey = req.library.mediaType
|
const itemKey = req.library.mediaType
|
||||||
var results = {
|
const results = {
|
||||||
[itemKey]: itemMatches.slice(0, maxResults),
|
[itemKey]: itemMatches.slice(0, maxResults),
|
||||||
tags: Object.values(tagMatches).slice(0, maxResults),
|
tags: Object.values(tagMatches).slice(0, maxResults),
|
||||||
authors: Object.values(authorMatches).slice(0, maxResults),
|
authors: Object.values(authorMatches).slice(0, maxResults),
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (includeEntities.includes('rssfeed')) {
|
if (includeEntities.includes('rssfeed')) {
|
||||||
var feedData = this.rssFeedManager.findFeedForItem(item.id)
|
const feedData = this.rssFeedManager.findFeedForEntityId(item.id)
|
||||||
item.rssFeedUrl = feedData ? feedData.feedUrl : null
|
item.rssFeed = feedData ? feedData.toJSONMinified() : null
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.mediaType == 'book') {
|
if (item.mediaType == 'book') {
|
||||||
@@ -52,7 +52,7 @@ class LibraryItemController {
|
|||||||
await this.cacheManager.purgeCoverCache(libraryItem.id)
|
await this.cacheManager.purgeCoverCache(libraryItem.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasUpdates = libraryItem.update(req.body)
|
const hasUpdates = libraryItem.update(req.body)
|
||||||
if (hasUpdates) {
|
if (hasUpdates) {
|
||||||
Logger.debug(`[LibraryItemController] Updated now saving`)
|
Logger.debug(`[LibraryItemController] Updated now saving`)
|
||||||
await this.db.updateLibraryItem(libraryItem)
|
await this.db.updateLibraryItem(libraryItem)
|
||||||
@@ -70,8 +70,8 @@ class LibraryItemController {
|
|||||||
// PATCH: will create new authors & series if in payload
|
// PATCH: will create new authors & series if in payload
|
||||||
//
|
//
|
||||||
async updateMedia(req, res) {
|
async updateMedia(req, res) {
|
||||||
var libraryItem = req.libraryItem
|
const libraryItem = req.libraryItem
|
||||||
var mediaPayload = req.body
|
const mediaPayload = req.body
|
||||||
// Item has cover and update is removing cover so purge it from cache
|
// Item has cover and update is removing cover so purge it from cache
|
||||||
if (libraryItem.media.coverPath && (mediaPayload.coverPath === '' || mediaPayload.coverPath === null)) {
|
if (libraryItem.media.coverPath && (mediaPayload.coverPath === '' || mediaPayload.coverPath === null)) {
|
||||||
await this.cacheManager.purgeCoverCache(libraryItem.id)
|
await this.cacheManager.purgeCoverCache(libraryItem.id)
|
||||||
@@ -83,7 +83,7 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Podcast specific
|
// Podcast specific
|
||||||
var isPodcastAutoDownloadUpdated = false
|
let isPodcastAutoDownloadUpdated = false
|
||||||
if (libraryItem.isPodcast) {
|
if (libraryItem.isPodcast) {
|
||||||
if (mediaPayload.autoDownloadEpisodes !== undefined && libraryItem.media.autoDownloadEpisodes !== mediaPayload.autoDownloadEpisodes) {
|
if (mediaPayload.autoDownloadEpisodes !== undefined && libraryItem.media.autoDownloadEpisodes !== mediaPayload.autoDownloadEpisodes) {
|
||||||
isPodcastAutoDownloadUpdated = true
|
isPodcastAutoDownloadUpdated = true
|
||||||
@@ -92,8 +92,23 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasUpdates = libraryItem.media.update(mediaPayload)
|
// Book specific - Get all series being removed from this item
|
||||||
|
let seriesRemoved = []
|
||||||
|
if (libraryItem.isBook && mediaPayload.metadata?.series) {
|
||||||
|
const seriesIdsInUpdate = (mediaPayload.metadata?.series || []).map(se => se.id)
|
||||||
|
seriesRemoved = libraryItem.media.metadata.series.filter(se => !seriesIdsInUpdate.includes(se.id))
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasUpdates = libraryItem.media.update(mediaPayload)
|
||||||
if (hasUpdates) {
|
if (hasUpdates) {
|
||||||
|
libraryItem.updatedAt = Date.now()
|
||||||
|
|
||||||
|
if (seriesRemoved.length) {
|
||||||
|
// Check remove empty series
|
||||||
|
Logger.debug(`[LibraryItemController] Series was removed from book. Check if series is now empty.`)
|
||||||
|
await this.checkRemoveEmptySeries(seriesRemoved)
|
||||||
|
}
|
||||||
|
|
||||||
if (isPodcastAutoDownloadUpdated) {
|
if (isPodcastAutoDownloadUpdated) {
|
||||||
this.cronManager.checkUpdatePodcastCron(libraryItem)
|
this.cronManager.checkUpdatePodcastCron(libraryItem)
|
||||||
}
|
}
|
||||||
@@ -186,6 +201,10 @@ class LibraryItemController {
|
|||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (global.XAccel) {
|
||||||
|
Logger.debug(`Use X-Accel to serve static file ${libraryItem.media.coverPath}`)
|
||||||
|
return res.status(204).header({'X-Accel-Redirect': global.XAccel + libraryItem.media.coverPath}).send()
|
||||||
|
}
|
||||||
return res.sendFile(libraryItem.media.coverPath)
|
return res.sendFile(libraryItem.media.coverPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -432,38 +451,6 @@ class LibraryItemController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST: api/items/:id/open-feed
|
|
||||||
async openRSSFeed(req, res) {
|
|
||||||
if (!req.user.isAdminOrUp) {
|
|
||||||
Logger.error(`[LibraryItemController] Non-admin user attempted to open RSS feed`, req.user.username)
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
|
|
||||||
const feedData = await this.rssFeedManager.openFeedForItem(req.user, req.libraryItem, req.body)
|
|
||||||
if (feedData.error) {
|
|
||||||
return res.json({
|
|
||||||
success: false,
|
|
||||||
error: feedData.error
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
feedUrl: feedData.feedUrl
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async closeRSSFeed(req, res) {
|
|
||||||
if (!req.user.isAdminOrUp) {
|
|
||||||
Logger.error(`[LibraryItemController] Non-admin user attempted to close RSS feed`, req.user.username)
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.rssFeedManager.closeFeedForItem(req.params.id)
|
|
||||||
|
|
||||||
res.sendStatus(200)
|
|
||||||
}
|
|
||||||
|
|
||||||
async toneScan(req, res) {
|
async toneScan(req, res) {
|
||||||
if (!req.libraryItem.media.audioFiles.length) {
|
if (!req.libraryItem.media.audioFiles.length) {
|
||||||
return res.sendStatus(404)
|
return res.sendStatus(404)
|
||||||
@@ -481,7 +468,7 @@ class LibraryItemController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
middleware(req, res, next) {
|
middleware(req, res, next) {
|
||||||
var item = this.db.libraryItems.find(li => li.id === req.params.id)
|
const item = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||||
if (!item || !item.media) return res.sendStatus(404)
|
if (!item || !item.media) return res.sendStatus(404)
|
||||||
|
|
||||||
// Check user can access this library item
|
// Check user can access this library item
|
||||||
|
|||||||
@@ -192,7 +192,8 @@ class MeController {
|
|||||||
}
|
}
|
||||||
const updatedLocalMediaProgress = []
|
const updatedLocalMediaProgress = []
|
||||||
var numServerProgressUpdates = 0
|
var numServerProgressUpdates = 0
|
||||||
var localMediaProgress = req.body.localMediaProgress || []
|
const updatedServerMediaProgress = []
|
||||||
|
const localMediaProgress = req.body.localMediaProgress || []
|
||||||
|
|
||||||
localMediaProgress.forEach(localProgress => {
|
localMediaProgress.forEach(localProgress => {
|
||||||
if (!localProgress.libraryItemId) {
|
if (!localProgress.libraryItemId) {
|
||||||
@@ -205,18 +206,22 @@ class MeController {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId)
|
let mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId)
|
||||||
if (!mediaProgress) {
|
if (!mediaProgress) {
|
||||||
// New media progress from mobile
|
// New media progress from mobile
|
||||||
Logger.debug(`[MeController] syncLocalMediaProgress local progress is new - creating ${localProgress.id}`)
|
Logger.debug(`[MeController] syncLocalMediaProgress local progress is new - creating ${localProgress.id}`)
|
||||||
req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId)
|
req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId)
|
||||||
|
mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId)
|
||||||
|
updatedServerMediaProgress.push(mediaProgress)
|
||||||
numServerProgressUpdates++
|
numServerProgressUpdates++
|
||||||
} else if (mediaProgress.lastUpdate < localProgress.lastUpdate) {
|
} else if (mediaProgress.lastUpdate < localProgress.lastUpdate) {
|
||||||
Logger.debug(`[MeController] syncLocalMediaProgress local progress is more recent - updating ${mediaProgress.id}`)
|
Logger.debug(`[MeController] syncLocalMediaProgress local progress is more recent - updating ${mediaProgress.id}`)
|
||||||
req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId)
|
req.user.createUpdateMediaProgress(libraryItem, localProgress, localProgress.episodeId)
|
||||||
|
mediaProgress = req.user.getMediaProgress(localProgress.libraryItemId, localProgress.episodeId)
|
||||||
|
updatedServerMediaProgress.push(mediaProgress)
|
||||||
numServerProgressUpdates++
|
numServerProgressUpdates++
|
||||||
} else if (mediaProgress.lastUpdate > localProgress.lastUpdate) {
|
} else if (mediaProgress.lastUpdate > localProgress.lastUpdate) {
|
||||||
var updateTimeDifference = mediaProgress.lastUpdate - localProgress.lastUpdate
|
const updateTimeDifference = mediaProgress.lastUpdate - localProgress.lastUpdate
|
||||||
Logger.debug(`[MeController] syncLocalMediaProgress server progress is more recent by ${updateTimeDifference}ms - ${mediaProgress.id}`)
|
Logger.debug(`[MeController] syncLocalMediaProgress server progress is more recent by ${updateTimeDifference}ms - ${mediaProgress.id}`)
|
||||||
|
|
||||||
for (const key in localProgress) {
|
for (const key in localProgress) {
|
||||||
@@ -240,7 +245,8 @@ class MeController {
|
|||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
numServerProgressUpdates,
|
numServerProgressUpdates,
|
||||||
localProgressUpdates: updatedLocalMediaProgress
|
localProgressUpdates: updatedLocalMediaProgress, // Array of LocalMediaProgress that were updated from server (server more recent)
|
||||||
|
serverProgressUpdates: updatedServerMediaProgress // Array of MediaProgress that made updates to server (local more recent)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ class MiscController {
|
|||||||
Logger.error('Invalid user in authorize')
|
Logger.error('Invalid user in authorize')
|
||||||
return res.sendStatus(401)
|
return res.sendStatus(401)
|
||||||
}
|
}
|
||||||
const userResponse = this.auth.getUserLoginResponsePayload(req.user, this.rssFeedManager.feedsArray)
|
const userResponse = this.auth.getUserLoginResponsePayload(req.user)
|
||||||
res.json(userResponse)
|
res.json(userResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ const SocketAuthority = require('../SocketAuthority')
|
|||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
|
|
||||||
const { getPodcastFeed, findMatchingEpisodes } = require('../utils/podcastUtils')
|
const { getPodcastFeed, findMatchingEpisodes } = require('../utils/podcastUtils')
|
||||||
const { getFileTimestampsWithIno } = require('../utils/fileUtils')
|
const { getFileTimestampsWithIno, filePathToPOSIX } = require('../utils/fileUtils')
|
||||||
const filePerms = require('../utils/filePerms')
|
const filePerms = require('../utils/filePerms')
|
||||||
|
|
||||||
const LibraryItem = require('../objects/LibraryItem')
|
const LibraryItem = require('../objects/LibraryItem')
|
||||||
@@ -30,7 +30,7 @@ class PodcastController {
|
|||||||
return res.status(404).send('Folder not found')
|
return res.status(404).send('Folder not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
var podcastPath = payload.path.replace(/\\/g, '/')
|
const podcastPath = filePathToPOSIX(payload.path)
|
||||||
if (await fs.pathExists(podcastPath)) {
|
if (await fs.pathExists(podcastPath)) {
|
||||||
Logger.error(`[PodcastController] Podcast folder already exists "${podcastPath}"`)
|
Logger.error(`[PodcastController] Podcast folder already exists "${podcastPath}"`)
|
||||||
return res.status(400).send('Podcast already exists')
|
return res.status(400).send('Podcast already exists')
|
||||||
@@ -173,7 +173,7 @@ class PodcastController {
|
|||||||
async downloadEpisodes(req, res) {
|
async downloadEpisodes(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
if (!req.user.isAdminOrUp) {
|
||||||
Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user)
|
Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user)
|
||||||
return res.sendStatus(500)
|
return res.sendStatus(403)
|
||||||
}
|
}
|
||||||
var libraryItem = req.libraryItem
|
var libraryItem = req.libraryItem
|
||||||
|
|
||||||
@@ -186,8 +186,27 @@ class PodcastController {
|
|||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST: api/podcasts/:id/match-episodes
|
||||||
|
async quickMatchEpisodes(req, res) {
|
||||||
|
if (!req.user.isAdminOrUp) {
|
||||||
|
Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
const overrideDetails = req.query.override === '1'
|
||||||
|
const episodesUpdated = await this.scanner.quickMatchPodcastEpisodes(req.libraryItem, { overrideDetails })
|
||||||
|
if (episodesUpdated) {
|
||||||
|
await this.db.updateLibraryItem(req.libraryItem)
|
||||||
|
SocketAuthority.emitter('item_updated', req.libraryItem.toJSONExpanded())
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
numEpisodesUpdated: episodesUpdated
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async updateEpisode(req, res) {
|
async updateEpisode(req, res) {
|
||||||
var libraryItem = req.libraryItem
|
const libraryItem = req.libraryItem
|
||||||
|
|
||||||
var episodeId = req.params.episodeId
|
var episodeId = req.params.episodeId
|
||||||
if (!libraryItem.media.checkHasEpisode(episodeId)) {
|
if (!libraryItem.media.checkHasEpisode(episodeId)) {
|
||||||
@@ -237,7 +256,7 @@ class PodcastController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
middleware(req, res, next) {
|
middleware(req, res, next) {
|
||||||
var item = this.db.libraryItems.find(li => li.id === req.params.id)
|
const item = this.db.libraryItems.find(li => li.id === req.params.id)
|
||||||
if (!item || !item.media) return res.sendStatus(404)
|
if (!item || !item.media) return res.sendStatus(404)
|
||||||
|
|
||||||
if (!item.isPodcast) {
|
if (!item.isPodcast) {
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
const Logger = require('../Logger')
|
||||||
|
const SocketAuthority = require('../SocketAuthority')
|
||||||
|
|
||||||
|
class RSSFeedController {
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
// POST: api/feeds/item/:itemId/open
|
||||||
|
async openRSSFeedForItem(req, res) {
|
||||||
|
const options = req.body || {}
|
||||||
|
|
||||||
|
const item = this.db.libraryItems.find(li => li.id === req.params.itemId)
|
||||||
|
if (!item) return res.sendStatus(404)
|
||||||
|
|
||||||
|
// Check user can access this library item
|
||||||
|
if (!req.user.checkCanAccessLibraryItem(item)) {
|
||||||
|
Logger.error(`[RSSFeedController] User "${req.user.username}" attempted to open an RSS feed for item "${item.media.metadata.title}" that they don\'t have access to`)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check request body options exist
|
||||||
|
if (!options.serverAddress || !options.slug) {
|
||||||
|
Logger.error(`[RSSFeedController] Invalid request body to open RSS feed`)
|
||||||
|
return res.status(400).send('Invalid request body')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check item has audio tracks
|
||||||
|
if (!item.media.numTracks) {
|
||||||
|
Logger.error(`[RSSFeedController] Cannot open RSS feed for item "${item.media.metadata.title}" because it has no audio tracks`)
|
||||||
|
return res.status(400).send('Item has no audio tracks')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that this slug is not being used for another feed (slug will also be the Feed id)
|
||||||
|
if (this.rssFeedManager.feeds[options.slug]) {
|
||||||
|
Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${options.slug}" is already in use`)
|
||||||
|
return res.status(400).send('Slug already in use')
|
||||||
|
}
|
||||||
|
|
||||||
|
const feed = await this.rssFeedManager.openFeedForItem(req.user, item, req.body)
|
||||||
|
res.json({
|
||||||
|
feed: feed.toJSONMinified()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: api/feeds/collection/:collectionId/open
|
||||||
|
async openRSSFeedForCollection(req, res) {
|
||||||
|
const options = req.body || {}
|
||||||
|
|
||||||
|
const collection = this.db.collections.find(li => li.id === req.params.collectionId)
|
||||||
|
if (!collection) return res.sendStatus(404)
|
||||||
|
|
||||||
|
// Check request body options exist
|
||||||
|
if (!options.serverAddress || !options.slug) {
|
||||||
|
Logger.error(`[RSSFeedController] Invalid request body to open RSS feed`)
|
||||||
|
return res.status(400).send('Invalid request body')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that this slug is not being used for another feed (slug will also be the Feed id)
|
||||||
|
if (this.rssFeedManager.feeds[options.slug]) {
|
||||||
|
Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${options.slug}" is already in use`)
|
||||||
|
return res.status(400).send('Slug already in use')
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectionExpanded = collection.toJSONExpanded(this.db.libraryItems)
|
||||||
|
const collectionItemsWithTracks = collectionExpanded.books.filter(li => li.media.tracks.length)
|
||||||
|
|
||||||
|
// Check collection has audio tracks
|
||||||
|
if (!collectionItemsWithTracks.length) {
|
||||||
|
Logger.error(`[RSSFeedController] Cannot open RSS feed for collection "${collection.name}" because it has no audio tracks`)
|
||||||
|
return res.status(400).send('Collection has no audio tracks')
|
||||||
|
}
|
||||||
|
|
||||||
|
const feed = await this.rssFeedManager.openFeedForCollection(req.user, collectionExpanded, req.body)
|
||||||
|
res.json({
|
||||||
|
feed: feed.toJSONMinified()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: api/feeds/series/:seriesId/open
|
||||||
|
async openRSSFeedForSeries(req, res) {
|
||||||
|
const options = req.body || {}
|
||||||
|
|
||||||
|
const series = this.db.series.find(se => se.id === req.params.seriesId)
|
||||||
|
if (!series) return res.sendStatus(404)
|
||||||
|
|
||||||
|
// Check request body options exist
|
||||||
|
if (!options.serverAddress || !options.slug) {
|
||||||
|
Logger.error(`[RSSFeedController] Invalid request body to open RSS feed`)
|
||||||
|
return res.status(400).send('Invalid request body')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that this slug is not being used for another feed (slug will also be the Feed id)
|
||||||
|
if (this.rssFeedManager.feeds[options.slug]) {
|
||||||
|
Logger.error(`[RSSFeedController] Cannot open RSS feed because slug "${options.slug}" is already in use`)
|
||||||
|
return res.status(400).send('Slug already in use')
|
||||||
|
}
|
||||||
|
|
||||||
|
const seriesJson = series.toJSON()
|
||||||
|
// Get books in series that have audio tracks
|
||||||
|
seriesJson.books = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(series.id) && li.media.tracks.length)
|
||||||
|
|
||||||
|
// Check series has audio tracks
|
||||||
|
if (!seriesJson.books.length) {
|
||||||
|
Logger.error(`[RSSFeedController] Cannot open RSS feed for series "${seriesJson.name}" because it has no audio tracks`)
|
||||||
|
return res.status(400).send('Series has no audio tracks')
|
||||||
|
}
|
||||||
|
|
||||||
|
const feed = await this.rssFeedManager.openFeedForSeries(req.user, seriesJson, req.body)
|
||||||
|
res.json({
|
||||||
|
feed: feed.toJSONMinified()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST: api/feeds/:id/close
|
||||||
|
async closeRSSFeed(req, res) {
|
||||||
|
await this.rssFeedManager.closeRssFeed(req.params.id)
|
||||||
|
|
||||||
|
res.sendStatus(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
middleware(req, res, next) {
|
||||||
|
if (!req.user.isAdminOrUp) { // Only admins can manage rss feeds
|
||||||
|
Logger.error(`[RSSFeedController] Non-admin user attempted to make a request to an RSS feed route`, req.user.username)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.params.id) {
|
||||||
|
const feed = this.rssFeedManager.findFeed(req.params.id)
|
||||||
|
if (!feed) {
|
||||||
|
Logger.error(`[RSSFeedController] RSS feed not found with id "${req.params.id}"`)
|
||||||
|
return res.sendStatus(404)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = new RSSFeedController()
|
||||||
@@ -49,5 +49,12 @@ class SearchController {
|
|||||||
}
|
}
|
||||||
res.json(chapterData)
|
res.json(chapterData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findMusicTrack(req, res) {
|
||||||
|
const tracks = await this.musicFinder.searchTrack(req.query || {})
|
||||||
|
res.json({
|
||||||
|
tracks
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = new SearchController()
|
module.exports = new SearchController()
|
||||||
@@ -5,15 +5,15 @@ class SeriesController {
|
|||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
async findOne(req, res) {
|
async findOne(req, res) {
|
||||||
var include = (req.query.include || '').split(',')
|
const include = (req.query.include || '').split(',').map(v => v.trim()).filter(v => !!v)
|
||||||
|
|
||||||
var seriesJson = req.series.toJSON()
|
const seriesJson = req.series.toJSON()
|
||||||
|
|
||||||
// Add progress map with isFinished flag
|
// Add progress map with isFinished flag
|
||||||
if (include.includes('progress')) {
|
if (include.includes('progress')) {
|
||||||
var libraryItemsInSeries = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(seriesJson.id))
|
const libraryItemsInSeries = this.db.libraryItems.filter(li => li.mediaType === 'book' && li.media.metadata.hasSeries(seriesJson.id))
|
||||||
var libraryItemsFinished = libraryItemsInSeries.filter(li => {
|
const libraryItemsFinished = libraryItemsInSeries.filter(li => {
|
||||||
var mediaProgress = req.user.getMediaProgress(li.id)
|
const mediaProgress = req.user.getMediaProgress(li.id)
|
||||||
return mediaProgress && mediaProgress.isFinished
|
return mediaProgress && mediaProgress.isFinished
|
||||||
})
|
})
|
||||||
seriesJson.progress = {
|
seriesJson.progress = {
|
||||||
@@ -23,6 +23,11 @@ class SeriesController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (include.includes('rssfeed')) {
|
||||||
|
const feedObj = this.rssFeedManager.findFeedForEntityId(seriesJson.id)
|
||||||
|
seriesJson.rssFeed = feedObj?.toJSONMinified() || null
|
||||||
|
}
|
||||||
|
|
||||||
return res.json(seriesJson)
|
return res.json(seriesJson)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,13 +46,13 @@ class SeriesController {
|
|||||||
const hasUpdated = req.series.update(req.body)
|
const hasUpdated = req.series.update(req.body)
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
await this.db.updateEntity('series', req.series)
|
await this.db.updateEntity('series', req.series)
|
||||||
SocketAuthority.emitter('series_updated', req.series)
|
SocketAuthority.emitter('series_updated', req.series.toJSON())
|
||||||
}
|
}
|
||||||
res.json(req.series)
|
res.json(req.series.toJSON())
|
||||||
}
|
}
|
||||||
|
|
||||||
middleware(req, res, next) {
|
middleware(req, res, next) {
|
||||||
var 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)
|
||||||
|
|
||||||
if (req.method == 'DELETE' && !req.user.canDelete) {
|
if (req.method == 'DELETE' && !req.user.canDelete) {
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ class ToolsController {
|
|||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// POST: api/tools/item/:id/embed-metadata
|
// POST: api/tools/item/:id/embed-metadata
|
||||||
async embedAudioFileMetadata(req, res) {
|
async embedAudioFileMetadata(req, res) {
|
||||||
if (!req.user.isAdminOrUp) {
|
if (!req.user.isAdminOrUp) {
|
||||||
@@ -60,9 +59,11 @@ class ToolsController {
|
|||||||
return res.sendStatus(500)
|
return res.sendStatus(500)
|
||||||
}
|
}
|
||||||
|
|
||||||
const useTone = req.query.tone === '1'
|
const options = {
|
||||||
const forceEmbedChapters = req.query.forceEmbedChapters === '1'
|
forceEmbedChapters: req.query.forceEmbedChapters === '1',
|
||||||
this.audioMetadataManager.updateMetadataForItem(req.user, req.libraryItem, useTone, forceEmbedChapters)
|
backup: req.query.backup === '1'
|
||||||
|
}
|
||||||
|
this.audioMetadataManager.updateMetadataForItem(req.user, req.libraryItem, options)
|
||||||
res.sendStatus(200)
|
res.sendStatus(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class UserController {
|
|||||||
var newUser = new User(account)
|
var newUser = new User(account)
|
||||||
var success = await this.db.insertEntity('user', newUser)
|
var success = await this.db.insertEntity('user', newUser)
|
||||||
if (success) {
|
if (success) {
|
||||||
SocketAuthority.adminEmitter('user_added', newUser)
|
SocketAuthority.adminEmitter('user_added', newUser.toJSONForBrowser())
|
||||||
res.json({
|
res.json({
|
||||||
user: newUser.toJSONForBrowser()
|
user: newUser.toJSONForBrowser()
|
||||||
})
|
})
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user