mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-12-31 11:38:47 -05:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7beca048e7 | ||
|
|
ec998dc1ac | ||
|
|
ddc54c8811 | ||
|
|
72e306935f | ||
|
|
96a7c7f4d1 | ||
|
|
9c65d655b8 | ||
|
|
b108f2241b | ||
|
|
9439acf300 | ||
|
|
d181e66d83 | ||
|
|
a87c3f2c77 | ||
|
|
2834f6077e | ||
|
|
918013ccb3 | ||
|
|
4c4672c6c1 | ||
|
|
b3991574c7 | ||
|
|
47b9ee557e |
@@ -168,7 +168,7 @@ export default {
|
||||
},
|
||||
async fetchCategories() {
|
||||
const categories = await this.$axios
|
||||
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed`)
|
||||
.$get(`/api/libraries/${this.currentLibraryId}/personalized?include=rssfeed,numEpisodesIncomplete`)
|
||||
.then((data) => {
|
||||
return data
|
||||
})
|
||||
|
||||
@@ -315,10 +315,10 @@ export default {
|
||||
|
||||
const entityPath = this.entityName === 'series-books' ? 'items' : this.entityName
|
||||
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
||||
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1&include=rssfeed`
|
||||
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1&include=rssfeed,numEpisodesIncomplete`
|
||||
|
||||
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 items', error)
|
||||
return null
|
||||
})
|
||||
|
||||
|
||||
@@ -116,9 +116,14 @@
|
||||
</div>
|
||||
|
||||
<!-- Podcast Num Episodes -->
|
||||
<div v-else-if="numEpisodes && !isHovering && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
|
||||
<div v-else-if="!numEpisodesIncomplete && numEpisodes && !isHovering && !isSelectionMode" class="absolute rounded-full bg-black bg-opacity-90 box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
|
||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodes }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Podcast Num Episodes -->
|
||||
<div v-else-if="numEpisodesIncomplete && !isHovering && !isSelectionMode" class="absolute rounded-full bg-yellow-400 text-black font-semibold box-shadow-md z-10 flex items-center justify-center" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', width: 1.25 * sizeMultiplier + 'rem', height: 1.25 * sizeMultiplier + 'rem' }">
|
||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">{{ numEpisodesIncomplete }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -227,9 +232,11 @@ export default {
|
||||
return this.media.numTracks || 0 // toJSONMinified
|
||||
},
|
||||
numEpisodes() {
|
||||
if (!this.isPodcast) return 0
|
||||
return this.media.numEpisodes || 0
|
||||
},
|
||||
numEpisodesIncomplete() {
|
||||
return this._libraryItem.numEpisodesIncomplete || 0
|
||||
},
|
||||
processingBatch() {
|
||||
return this.store.state.processingBatch
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="narrators?.length" class="flex py-0.5 mt-4">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelNarrators }}</span>
|
||||
</div>
|
||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||
@@ -12,7 +12,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="publishedYear" class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublishYear }}</span>
|
||||
</div>
|
||||
<div>
|
||||
@@ -20,15 +20,15 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="publisher" class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPublisher }}</span>
|
||||
</div>
|
||||
<div>
|
||||
{{ publisher }}
|
||||
<nuxt-link :to="`/library/${libraryId}/bookshelf?filter=publishers.${$encode(publisher)}`" class="hover:underline">{{ publisher }}</nuxt-link>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="musicAlbum" class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">Album</span>
|
||||
</div>
|
||||
<div>
|
||||
@@ -36,7 +36,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="musicAlbumArtist" class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">Album Artist</span>
|
||||
</div>
|
||||
<div>
|
||||
@@ -44,7 +44,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="musicTrackPretty" class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">Track</span>
|
||||
</div>
|
||||
<div>
|
||||
@@ -52,7 +52,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="musicDiscPretty" class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">Disc</span>
|
||||
</div>
|
||||
<div>
|
||||
@@ -60,7 +60,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="podcastType" class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelPodcastType }}</span>
|
||||
</div>
|
||||
<div class="capitalize">
|
||||
@@ -68,7 +68,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex py-0.5" v-if="genres.length">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelGenres }}</span>
|
||||
</div>
|
||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||
@@ -79,7 +79,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex py-0.5" v-if="tags.length">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelTags }}</span>
|
||||
</div>
|
||||
<div class="max-w-[calc(100vw-10rem)] overflow-hidden overflow-ellipsis">
|
||||
@@ -90,7 +90,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="tracks.length || audioFile || (isPodcast && totalPodcastDuration)" class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelDuration }}</span>
|
||||
</div>
|
||||
<div>
|
||||
@@ -98,7 +98,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex py-0.5">
|
||||
<div class="w-32">
|
||||
<div class="w-24 min-w-24 sm:w-32 sm:min-w-32">
|
||||
<span class="text-white text-opacity-60 uppercase text-sm">{{ $strings.LabelSize }}</span>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -124,6 +124,11 @@ export default {
|
||||
value: 'narrators',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelPublisher,
|
||||
value: 'publishers',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelLanguage,
|
||||
value: 'languages',
|
||||
@@ -167,6 +172,11 @@ export default {
|
||||
value: 'narrators',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelPublisher,
|
||||
value: 'publishers',
|
||||
sublist: true
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelLanguage,
|
||||
value: 'languages',
|
||||
@@ -313,6 +323,9 @@ export default {
|
||||
languages() {
|
||||
return this.filterData.languages || []
|
||||
},
|
||||
publishers() {
|
||||
return this.filterData.publishers || []
|
||||
},
|
||||
progress() {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="w-full h-full overflow-hidden overflow-y-auto px-2 sm:px-4 py-6 relative">
|
||||
<div class="flex flex-wrap">
|
||||
<div class="flex flex-wrap mb-4">
|
||||
<div class="relative">
|
||||
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, null, true)" :width="120" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, libraryItemUpdatedAt, true)" :width="120" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
|
||||
<!-- book cover overlay -->
|
||||
<div v-if="media.coverPath" class="absolute top-0 left-0 w-full h-full z-10 opacity-0 hover:opacity-100 transition-opacity duration-100">
|
||||
@@ -139,16 +139,19 @@ export default {
|
||||
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||
},
|
||||
libraryItemId() {
|
||||
return this.libraryItem ? this.libraryItem.id : null
|
||||
return this.libraryItem?.id || null
|
||||
},
|
||||
libraryItemUpdatedAt() {
|
||||
return this.libraryItem?.updatedAt || null
|
||||
},
|
||||
mediaType() {
|
||||
return this.libraryItem ? this.libraryItem.mediaType : null
|
||||
return this.libraryItem?.mediaType || null
|
||||
},
|
||||
isPodcast() {
|
||||
return this.mediaType == 'podcast'
|
||||
},
|
||||
media() {
|
||||
return this.libraryItem ? this.libraryItem.media || {} : {}
|
||||
return this.libraryItem?.media || {}
|
||||
},
|
||||
coverPath() {
|
||||
return this.media.coverPath
|
||||
@@ -157,7 +160,7 @@ export default {
|
||||
return this.media.metadata || {}
|
||||
},
|
||||
libraryFiles() {
|
||||
return this.libraryItem ? this.libraryItem.libraryFiles || [] : []
|
||||
return this.libraryItem?.libraryFiles || []
|
||||
},
|
||||
userCanUpload() {
|
||||
return this.$store.getters['user/getUserCanUpload']
|
||||
|
||||
@@ -58,7 +58,6 @@ export default {
|
||||
selectedEpisodes: [],
|
||||
episodesToRemove: [],
|
||||
processing: false,
|
||||
quickMatchingEpisodes: false,
|
||||
search: null,
|
||||
searchTimeout: null,
|
||||
searchText: null
|
||||
@@ -78,6 +77,10 @@ export default {
|
||||
{
|
||||
text: 'Quick match all episodes',
|
||||
action: 'quick-match-episodes'
|
||||
},
|
||||
{
|
||||
text: this.allEpisodesFinished ? this.$strings.MessageMarkAllEpisodesNotFinished : this.$strings.MessageMarkAllEpisodesFinished,
|
||||
action: 'batch-mark-as-finished'
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -169,9 +172,15 @@ export default {
|
||||
},
|
||||
selectedIsFinished() {
|
||||
// Find an item that is not finished, if none then all items finished
|
||||
return !this.selectedEpisodes.find((episode) => {
|
||||
var itemProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, episode.id)
|
||||
return !itemProgress || !itemProgress.isFinished
|
||||
return !this.selectedEpisodes.some((episode) => {
|
||||
const itemProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, episode.id)
|
||||
return !itemProgress?.isFinished
|
||||
})
|
||||
},
|
||||
allEpisodesFinished() {
|
||||
return !this.episodesSorted.some((episode) => {
|
||||
const itemProgress = this.$store.getters['user/getUserMediaProgress'](this.libraryItem.id, episode.id)
|
||||
return !itemProgress?.isFinished
|
||||
})
|
||||
},
|
||||
dateFormat() {
|
||||
@@ -194,17 +203,34 @@ export default {
|
||||
},
|
||||
contextMenuAction({ action }) {
|
||||
if (action === 'quick-match-episodes') {
|
||||
if (this.quickMatchingEpisodes) return
|
||||
if (this.processing) return
|
||||
|
||||
this.quickMatchAllEpisodes()
|
||||
} else if (action === 'batch-mark-as-finished') {
|
||||
if (this.processing) return
|
||||
|
||||
this.markAllEpisodesFinished()
|
||||
}
|
||||
},
|
||||
markAllEpisodesFinished() {
|
||||
const newIsFinished = !this.allEpisodesFinished
|
||||
const payload = {
|
||||
message: newIsFinished ? this.$strings.MessageConfirmMarkAllEpisodesFinished : this.$strings.MessageConfirmMarkAllEpisodesNotFinished,
|
||||
callback: (confirmed) => {
|
||||
if (confirmed) {
|
||||
this.batchUpdateEpisodesFinished(this.episodesSorted, newIsFinished)
|
||||
}
|
||||
},
|
||||
type: 'yesNo'
|
||||
}
|
||||
this.$store.commit('globals/setConfirmPrompt', payload)
|
||||
},
|
||||
quickMatchAllEpisodes() {
|
||||
if (!this.mediaMetadata.feedUrl) {
|
||||
this.$toast.error(this.$strings.MessagePodcastHasNoRSSFeedForMatching)
|
||||
return
|
||||
}
|
||||
this.quickMatchingEpisodes = true
|
||||
this.processing = true
|
||||
|
||||
const payload = {
|
||||
message: 'Quick matching episodes will overwrite details if a match is found. Only unmatched episodes will be updated. Are you sure?',
|
||||
@@ -224,7 +250,7 @@ export default {
|
||||
this.$toast.error('Failed to match episodes')
|
||||
})
|
||||
}
|
||||
this.quickMatchingEpisodes = false
|
||||
this.processing = false
|
||||
},
|
||||
type: 'yesNo'
|
||||
}
|
||||
@@ -248,17 +274,19 @@ export default {
|
||||
this.$store.commit('addItemToQueue', queueItem)
|
||||
},
|
||||
toggleBatchFinished() {
|
||||
this.batchUpdateEpisodesFinished(this.selectedEpisodes, !this.selectedIsFinished)
|
||||
},
|
||||
batchUpdateEpisodesFinished(episodes, newIsFinished) {
|
||||
this.processing = true
|
||||
var newIsFinished = !this.selectedIsFinished
|
||||
var updateProgressPayloads = this.selectedEpisodes.map((episode) => {
|
||||
|
||||
const updateProgressPayloads = episodes.map((episode) => {
|
||||
return {
|
||||
libraryItemId: this.libraryItem.id,
|
||||
episodeId: episode.id,
|
||||
isFinished: newIsFinished
|
||||
}
|
||||
})
|
||||
|
||||
this.$axios
|
||||
return this.$axios
|
||||
.patch(`/api/me/progress/batch/update`, updateProgressPayloads)
|
||||
.then(() => {
|
||||
this.$toast.success(this.$strings.ToastBatchUpdateSuccess)
|
||||
|
||||
4
client/package-lock.json
generated
4
client/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.2.23",
|
||||
"version": "2.3.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.2.23",
|
||||
"version": "2.3.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@nuxtjs/axios": "^5.13.6",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.2.23",
|
||||
"version": "2.3.0",
|
||||
"description": "Self-hosted audiobook and podcast client",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -83,8 +83,8 @@ export const getters = {
|
||||
getLibraryItemCoverSrc: (state, getters, rootState, rootGetters) => (libraryItem, placeholder = null) => {
|
||||
if (!placeholder) placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
|
||||
if (!libraryItem) return placeholder
|
||||
var media = libraryItem.media
|
||||
if (!media || !media.coverPath || media.coverPath === placeholder) return placeholder
|
||||
const media = libraryItem.media
|
||||
if (!media?.coverPath || media.coverPath === placeholder) return placeholder
|
||||
|
||||
// Absolute URL covers (should no longer be used)
|
||||
if (media.coverPath.startsWith('http:') || media.coverPath.startsWith('https:')) return media.coverPath
|
||||
@@ -99,14 +99,14 @@ export const getters = {
|
||||
|
||||
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}`
|
||||
},
|
||||
getLibraryItemCoverSrcById: (state, getters, rootState, rootGetters) => (libraryItemId, placeholder = null, raw = false) => {
|
||||
if (!placeholder) placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
|
||||
getLibraryItemCoverSrcById: (state, getters, rootState, rootGetters) => (libraryItemId, timestamp = null, raw = false) => {
|
||||
const placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`
|
||||
if (!libraryItemId) return placeholder
|
||||
var userToken = rootGetters['user/getToken']
|
||||
const userToken = rootGetters['user/getToken']
|
||||
if (process.env.NODE_ENV !== 'production') { // Testing
|
||||
return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}${raw ? '&raw=1' : ''}`
|
||||
return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}${raw ? '&raw=1' : ''}${timestamp ? `&ts=${timestamp}` : ''}`
|
||||
}
|
||||
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}${raw ? '&raw=1' : ''}`
|
||||
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}${raw ? '&raw=1' : ''}${timestamp ? `&ts=${timestamp}` : ''}`
|
||||
},
|
||||
getIsBatchSelectingMediaItems: (state) => {
|
||||
return state.selectedMediaItems.length
|
||||
|
||||
@@ -238,21 +238,23 @@ export const mutations = {
|
||||
if (!libraryItem || !state.filterData) return
|
||||
if (state.currentLibraryId !== libraryItem.libraryId) return
|
||||
/*
|
||||
var data = {
|
||||
structure of filterData:
|
||||
{
|
||||
authors: [],
|
||||
genres: [],
|
||||
tags: [],
|
||||
series: [],
|
||||
narrators: [],
|
||||
languages: []
|
||||
languages: [],
|
||||
publishers: []
|
||||
}
|
||||
*/
|
||||
var mediaMetadata = libraryItem.media.metadata
|
||||
const mediaMetadata = libraryItem.media.metadata
|
||||
|
||||
// Add/update book authors
|
||||
if (mediaMetadata.authors && mediaMetadata.authors.length) {
|
||||
if (mediaMetadata.authors?.length) {
|
||||
mediaMetadata.authors.forEach((author) => {
|
||||
var indexOf = state.filterData.authors.findIndex(au => au.id === author.id)
|
||||
const indexOf = state.filterData.authors.findIndex(au => au.id === author.id)
|
||||
if (indexOf >= 0) {
|
||||
state.filterData.authors.splice(indexOf, 1, author)
|
||||
} else {
|
||||
@@ -263,9 +265,9 @@ export const mutations = {
|
||||
}
|
||||
|
||||
// Add/update series
|
||||
if (mediaMetadata.series && mediaMetadata.series.length) {
|
||||
if (mediaMetadata.series?.length) {
|
||||
mediaMetadata.series.forEach((series) => {
|
||||
var indexOf = state.filterData.series.findIndex(se => se.id === series.id)
|
||||
const indexOf = state.filterData.series.findIndex(se => se.id === series.id)
|
||||
if (indexOf >= 0) {
|
||||
state.filterData.series.splice(indexOf, 1, { id: series.id, name: series.name })
|
||||
} else {
|
||||
@@ -276,7 +278,7 @@ export const mutations = {
|
||||
}
|
||||
|
||||
// Add genres
|
||||
if (mediaMetadata.genres && mediaMetadata.genres.length) {
|
||||
if (mediaMetadata.genres?.length) {
|
||||
mediaMetadata.genres.forEach((genre) => {
|
||||
if (!state.filterData.genres.includes(genre)) {
|
||||
state.filterData.genres.push(genre)
|
||||
@@ -286,7 +288,7 @@ export const mutations = {
|
||||
}
|
||||
|
||||
// Add tags
|
||||
if (libraryItem.media.tags && libraryItem.media.tags.length) {
|
||||
if (libraryItem.media.tags?.length) {
|
||||
libraryItem.media.tags.forEach((tag) => {
|
||||
if (!state.filterData.tags.includes(tag)) {
|
||||
state.filterData.tags.push(tag)
|
||||
@@ -296,7 +298,7 @@ export const mutations = {
|
||||
}
|
||||
|
||||
// Add narrators
|
||||
if (mediaMetadata.narrators && mediaMetadata.narrators.length) {
|
||||
if (mediaMetadata.narrators?.length) {
|
||||
mediaMetadata.narrators.forEach((narrator) => {
|
||||
if (!state.filterData.narrators.includes(narrator)) {
|
||||
state.filterData.narrators.push(narrator)
|
||||
@@ -305,12 +307,16 @@ export const mutations = {
|
||||
})
|
||||
}
|
||||
|
||||
// Add publishers
|
||||
if (mediaMetadata.publisher && !state.filterData.publishers.includes(mediaMetadata.publisher)) {
|
||||
state.filterData.publishers.push(mediaMetadata.publisher)
|
||||
state.filterData.publishers.sort((a, b) => a.localeCompare(b))
|
||||
}
|
||||
|
||||
// Add language
|
||||
if (mediaMetadata.language) {
|
||||
if (!state.filterData.languages.includes(mediaMetadata.language)) {
|
||||
state.filterData.languages.push(mediaMetadata.language)
|
||||
state.filterData.languages.sort((a, b) => a.localeCompare(b))
|
||||
}
|
||||
if (mediaMetadata.language && !state.filterData.languages.includes(mediaMetadata.language)) {
|
||||
state.filterData.languages.push(mediaMetadata.language)
|
||||
state.filterData.languages.sort((a, b) => a.localeCompare(b))
|
||||
}
|
||||
},
|
||||
setCollections(state, collections) {
|
||||
|
||||
@@ -80,7 +80,7 @@ export const actions = {
|
||||
if (state.settings.orderBy == 'media.metadata.publishedYear') {
|
||||
settingsUpdate.orderBy = 'media.metadata.title'
|
||||
}
|
||||
const invalidFilters = ['series', 'authors', 'narrators', 'languages', 'progress', 'issues', 'ebooks', 'abridged']
|
||||
const invalidFilters = ['series', 'authors', 'narrators', 'publishers', 'languages', 'progress', 'issues', 'ebooks', 'abridged']
|
||||
const filterByFirstPart = (state.settings.filterBy || '').split('.').shift()
|
||||
if (invalidFilters.includes(filterByFirstPart)) {
|
||||
settingsUpdate.filterBy = 'all'
|
||||
|
||||
@@ -519,6 +519,8 @@
|
||||
"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?",
|
||||
"MessageConfirmForceReScan": "Sind Sie sicher, dass Sie einen erneuten Scanvorgang erzwingen wollen?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"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?",
|
||||
"MessageConfirmRemoveAllChapters": "Sind Sie sicher, dass Sie alle Kapitel entfernen möchten?",
|
||||
@@ -552,6 +554,8 @@
|
||||
"MessageM4BFailed": "M4B fehlgeschlagen!",
|
||||
"MessageM4BFinished": "M4B beendet!",
|
||||
"MessageMapChapterTitles": "Zuordnen von Kapiteltiteln zu Ihren vorhandenen Medienkapiteln ohne Anpassung der Zeitangaben",
|
||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||
"MessageMarkAsFinished": "Als beendet 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.",
|
||||
|
||||
@@ -519,6 +519,8 @@
|
||||
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
|
||||
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
||||
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"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?",
|
||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||
@@ -552,6 +554,8 @@
|
||||
"MessageM4BFailed": "M4B Failed!",
|
||||
"MessageM4BFinished": "M4B Finished!",
|
||||
"MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
|
||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||
"MessageMarkAsFinished": "Mark as Finished",
|
||||
"MessageMarkAsNotFinished": "Mark as Not Finished",
|
||||
"MessageMatchBooksDescription": "will attempt to match books in the library with a book from the selected search provider and fill in empty details and cover art. Does not overwrite details.",
|
||||
|
||||
@@ -519,6 +519,8 @@
|
||||
"MessageConfirmDeleteLibrary": "Esta seguro que desea eliminar permanentemente la biblioteca \"{0}\"?",
|
||||
"MessageConfirmDeleteSession": "Esta seguro que desea eliminar esta session?",
|
||||
"MessageConfirmForceReScan": "Esta seguro que desea forzar re-escanear?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"MessageConfirmMarkSeriesFinished": "Esta seguro que desea marcar todos los libros en esta serie como terminados?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Esta seguro que desea marcar todos los libros en esta serie como no terminados?",
|
||||
"MessageConfirmRemoveAllChapters": "Esta seguro que desea remover todos los capitulos?",
|
||||
@@ -552,6 +554,8 @@
|
||||
"MessageM4BFailed": "M4B Fallo!",
|
||||
"MessageM4BFinished": "M4B Terminado!",
|
||||
"MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
|
||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||
"MessageMarkAsFinished": "Marcar como Terminado",
|
||||
"MessageMarkAsNotFinished": "Marcar como No Terminado",
|
||||
"MessageMatchBooksDescription": "intentará hacer coincidir los libros de la biblioteca con un libro del proveedor de búsqueda seleccionado y rellenará los detalles vacíos y la portada. No sobrescribe los detalles.",
|
||||
|
||||
@@ -519,6 +519,8 @@
|
||||
"MessageConfirmDeleteLibrary": "Êtes-vous sûr de vouloir supprimer définitivement la bibliothèque « {0} » ?",
|
||||
"MessageConfirmDeleteSession": "Êtes-vous sûr de vouloir supprimer cette session ?",
|
||||
"MessageConfirmForceReScan": "Êtes-vous sûr de vouloir lancer une Analyse Forcée ?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"MessageConfirmMarkSeriesFinished": "Êtes-vous sûr de vouloir marquer comme terminé tous les livres de cette série ?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Êtes-vous sûr de vouloir marquer comme non terminé tous les livres de cette série ?",
|
||||
"MessageConfirmRemoveAllChapters": "Êtes-vous sûr de vouloir supprimer tous les chapitres ?",
|
||||
@@ -552,6 +554,8 @@
|
||||
"MessageM4BFailed": "M4B en échec !",
|
||||
"MessageM4BFinished": "M4B terminé !",
|
||||
"MessageMapChapterTitles": "Faire correspondre les titres des chapitres aux chapitres existants de votre livre audio sans ajuster l’horodatage.",
|
||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||
"MessageMarkAsFinished": "Marquer comme terminé",
|
||||
"MessageMarkAsNotFinished": "Marquer comme non Terminé",
|
||||
"MessageMatchBooksDescription": "tentera de faire correspondre les livres de la bibliothèque avec les livres du fournisseur sélectionné pour combler les détails et couverture manquants. N’écrase pas les données existantes.",
|
||||
|
||||
@@ -519,6 +519,8 @@
|
||||
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
|
||||
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
||||
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"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?",
|
||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||
@@ -552,6 +554,8 @@
|
||||
"MessageM4BFailed": "M4B Failed!",
|
||||
"MessageM4BFinished": "M4B Finished!",
|
||||
"MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
|
||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||
"MessageMarkAsFinished": "Mark as Finished",
|
||||
"MessageMarkAsNotFinished": "Mark as Not Finished",
|
||||
"MessageMatchBooksDescription": "will attempt to match books in the library with a book from the selected search provider and fill in empty details and cover art. Does not overwrite details.",
|
||||
|
||||
@@ -519,6 +519,8 @@
|
||||
"MessageConfirmDeleteLibrary": "Are you sure you want to permanently delete library \"{0}\"?",
|
||||
"MessageConfirmDeleteSession": "Are you sure you want to delete this session?",
|
||||
"MessageConfirmForceReScan": "Are you sure you want to force re-scan?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"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?",
|
||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||
@@ -552,6 +554,8 @@
|
||||
"MessageM4BFailed": "M4B Failed!",
|
||||
"MessageM4BFinished": "M4B Finished!",
|
||||
"MessageMapChapterTitles": "Map chapter titles to your existing audiobook chapters without adjusting timestamps",
|
||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||
"MessageMarkAsFinished": "Mark as Finished",
|
||||
"MessageMarkAsNotFinished": "Mark as Not Finished",
|
||||
"MessageMatchBooksDescription": "will attempt to match books in the library with a book from the selected search provider and fill in empty details and cover art. Does not overwrite details.",
|
||||
|
||||
@@ -519,6 +519,8 @@
|
||||
"MessageConfirmDeleteLibrary": "Jeste li sigurni da želite trajno obrisati biblioteku \"{0}\"?",
|
||||
"MessageConfirmDeleteSession": "Jeste li sigurni da želite obrisati ovu sesiju?",
|
||||
"MessageConfirmForceReScan": "Jeste li sigurni da želite ponovno skenirati?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"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?",
|
||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||
@@ -552,6 +554,8 @@
|
||||
"MessageM4BFailed": "M4B neuspješan!",
|
||||
"MessageM4BFinished": "M4B završio!",
|
||||
"MessageMapChapterTitles": "Mapiraj imena poglavlja u postoječa poglavlja bez izmijene timestampova.",
|
||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||
"MessageMarkAsFinished": "Označi kao završeno",
|
||||
"MessageMarkAsNotFinished": "Označi kao nezavršeno",
|
||||
"MessageMatchBooksDescription": "će probati matchati knjige iz biblioteke sa knjigom od odabranog poslužitelja i popuniti prazne detalje i cover. Ne briše postojeće detalje.",
|
||||
|
||||
@@ -519,6 +519,8 @@
|
||||
"MessageConfirmDeleteLibrary": "Sei sicuro di voler eliminare definitivamente la libreria \"{0}\"?",
|
||||
"MessageConfirmDeleteSession": "Sei sicuro di voler eliminare questa sessione?",
|
||||
"MessageConfirmForceReScan": "Sei sicuro di voler forzare una nuova scansione?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"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?",
|
||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||
@@ -552,6 +554,8 @@
|
||||
"MessageM4BFailed": "M4B Fallito!",
|
||||
"MessageM4BFinished": "M4B Finito!",
|
||||
"MessageMapChapterTitles": "Associa i titoli dei capitoli ai capitoli dell'audiolibro esistente senza modificare i timestamp",
|
||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||
"MessageMarkAsFinished": "Segna come finito",
|
||||
"MessageMarkAsNotFinished": "Segna come da completare",
|
||||
"MessageMatchBooksDescription": "tenterà di abbinare i libri nella biblioteca con un libro del provider di ricerca selezionato e inserirà i dettagli vuoti e la copertina. Non sovrascrive i dettagli.",
|
||||
|
||||
@@ -519,6 +519,8 @@
|
||||
"MessageConfirmDeleteLibrary": "Weet je zeker dat je de bibliotheek \"{0}\" permanent wil verwijderen?",
|
||||
"MessageConfirmDeleteSession": "Weet je zeker dat je deze sessie wil verwijderen?",
|
||||
"MessageConfirmForceReScan": "Weet je zeker dat je geforceerd opnieuw wil scannen?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"MessageConfirmMarkSeriesFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als voltooid?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Weet je zeker dat je alle boeken in deze serie wil markeren als niet voltooid?",
|
||||
"MessageConfirmRemoveAllChapters": "Weet je zeker dat je alle hoofdstukken wil verwijderen?",
|
||||
@@ -552,6 +554,8 @@
|
||||
"MessageM4BFailed": "M4B mislukt!",
|
||||
"MessageM4BFinished": "M4B voltooid!",
|
||||
"MessageMapChapterTitles": "Map hoofdstuktitels naar je bestaande audioboekhoofdstukken zonder aanpassing van tijden",
|
||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||
"MessageMarkAsFinished": "Markeer als Voltooid",
|
||||
"MessageMarkAsNotFinished": "Markeer als Niet Voltooid",
|
||||
"MessageMatchBooksDescription": "zal proberen boeken in de bibliotheek te matchen met een boek uit de geselecteerde bron en lege details en coverafbeelding te vullen. Overschrijft details niet.",
|
||||
|
||||
@@ -519,6 +519,8 @@
|
||||
"MessageConfirmDeleteLibrary": "Czy na pewno chcesz trwale usunąć bibliotekę \"{0}\"?",
|
||||
"MessageConfirmDeleteSession": "Czy na pewno chcesz usunąć tę sesję?",
|
||||
"MessageConfirmForceReScan": "Czy na pewno chcesz wymusić ponowne skanowanie?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"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?",
|
||||
"MessageConfirmRemoveAllChapters": "Are you sure you want to remove all chapters?",
|
||||
@@ -552,6 +554,8 @@
|
||||
"MessageM4BFailed": "Tworzenie pliku M4B nie powiodło się",
|
||||
"MessageM4BFinished": "Tworzenie pliku M4B zakończyło się!",
|
||||
"MessageMapChapterTitles": "Mapowanie tytułów rozdziałów do istniejących rozdziałów audiobooka bez dostosowywania znaczników czasu",
|
||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||
"MessageMarkAsFinished": "Oznacz jako ukończone",
|
||||
"MessageMarkAsNotFinished": "Oznacz jako nieukończone",
|
||||
"MessageMatchBooksDescription": "spróbuje dopasować książki w bibliotece bez plików audio, korzystając z wybranego dostawcy wyszukiwania i wypełnić puste szczegóły i okładki. Nie nadpisuje informacji.",
|
||||
|
||||
@@ -519,6 +519,8 @@
|
||||
"MessageConfirmDeleteLibrary": "Вы уверены, что хотите навсегда удалить библиотеку \"{0}\"?",
|
||||
"MessageConfirmDeleteSession": "Вы уверены, что хотите удалить этот сеанс?",
|
||||
"MessageConfirmForceReScan": "Вы уверены, что хотите принудительно выполнить повторное сканирование?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"MessageConfirmMarkSeriesFinished": "Вы уверены, что хотите отметить все книги этой серии как законченные?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "Вы уверены, что хотите отметить все книги этой серии как незаконченные?",
|
||||
"MessageConfirmRemoveAllChapters": "Вы уверены, что хотите удалить все главы?",
|
||||
@@ -552,6 +554,8 @@
|
||||
"MessageM4BFailed": "M4B Ошибка!",
|
||||
"MessageM4BFinished": "M4B Завершено!",
|
||||
"MessageMapChapterTitles": "Сопоставление названий глав с существующими главами аудиокниги без корректировки временных меток",
|
||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||
"MessageMarkAsFinished": "Отметить, как завершенную",
|
||||
"MessageMarkAsNotFinished": "Отметить, как не завершенную",
|
||||
"MessageMatchBooksDescription": "попытается сопоставить книги в библиотеке с книгой из выбранного поставщика поиска и заполнить пустые детали и обложку. Не перезаписывает сведения.",
|
||||
|
||||
@@ -519,6 +519,8 @@
|
||||
"MessageConfirmDeleteLibrary": "你确定要永久删除媒体库 \"{0}\"?",
|
||||
"MessageConfirmDeleteSession": "你确定要删除此会话吗?",
|
||||
"MessageConfirmForceReScan": "你确定要强制重新扫描吗?",
|
||||
"MessageConfirmMarkAllEpisodesFinished": "Are you sure you want to mark all episodes as finished?",
|
||||
"MessageConfirmMarkAllEpisodesNotFinished": "Are you sure you want to mark all episodes as not finished?",
|
||||
"MessageConfirmMarkSeriesFinished": "你确定要将此系列中的所有书籍都标记为已听完吗?",
|
||||
"MessageConfirmMarkSeriesNotFinished": "你确定要将此系列中的所有书籍都标记为未听完吗?",
|
||||
"MessageConfirmRemoveAllChapters": "你确定要移除所有章节吗?",
|
||||
@@ -552,6 +554,8 @@
|
||||
"MessageM4BFailed": "M4B 失败!",
|
||||
"MessageM4BFinished": "M4B 完成!",
|
||||
"MessageMapChapterTitles": "将章节标题映射到现有的有声读物章节, 无需调整时间戳",
|
||||
"MessageMarkAllEpisodesFinished": "Mark all episodes finished",
|
||||
"MessageMarkAllEpisodesNotFinished": "Mark all episodes not finished",
|
||||
"MessageMarkAsFinished": "标记为已听完",
|
||||
"MessageMarkAsNotFinished": "标记为未听完",
|
||||
"MessageMatchBooksDescription": "尝试将媒体库中的图书与所选搜索提供商的图书进行匹配, 并填写空白的详细信息和封面. 不覆盖详细信息.",
|
||||
|
||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.2.23",
|
||||
"version": "2.3.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.2.23",
|
||||
"version": "2.3.0",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
@@ -4672,4 +4672,4 @@
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.2.23",
|
||||
"version": "2.3.0",
|
||||
"description": "Self-hosted audiobook and podcast server",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -198,6 +198,7 @@ class LibraryController {
|
||||
include: include.join(',')
|
||||
}
|
||||
const mediaIsBook = payload.mediaType === 'book'
|
||||
const mediaIsPodcast = payload.mediaType === 'podcast'
|
||||
|
||||
// Step 1 - Filter the retrieved library items
|
||||
let filterSeries = null
|
||||
@@ -236,7 +237,6 @@ class LibraryController {
|
||||
const sortArray = []
|
||||
|
||||
// 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 (filterSeries && !payload.sortBy) {
|
||||
sortArray.push({ asc: (li) => li.media.metadata.getSeries(filterSeries).sequence })
|
||||
// If no series sequence then fallback to sorting by title (or collapsed series name for sub-series)
|
||||
@@ -360,6 +360,11 @@ class LibraryController {
|
||||
json.rssFeed = feedData ? feedData.toJSONMinified() : null
|
||||
}
|
||||
|
||||
// add numEpisodesIncomplete if "include=numEpisodesIncomplete" was put in query string (only for podcasts)
|
||||
if (mediaIsPodcast && include.includes('numepisodesincomplete')) {
|
||||
json.numEpisodesIncomplete = req.user.getNumEpisodesIncompleteForPodcast(li)
|
||||
}
|
||||
|
||||
if (filterSeries) {
|
||||
// If filtering by series, make sure to include the series metadata
|
||||
json.media.metadata.series = li.media.metadata.getSeries(filterSeries)
|
||||
|
||||
@@ -88,7 +88,9 @@ class LibraryItemController {
|
||||
}
|
||||
|
||||
const libraryItemPath = req.libraryItem.path
|
||||
const filename = `${req.libraryItem.media.metadata.title}.zip`
|
||||
const itemTitle = req.libraryItem.media.metadata.title
|
||||
Logger.info(`[LibraryItemController] User "${req.user.username}" requested download for item "${itemTitle}" at "${libraryItemPath}"`)
|
||||
const filename = `${itemTitle}.zip`
|
||||
zipHelpers.zipDirectoryPipe(libraryItemPath, filename, res)
|
||||
}
|
||||
|
||||
|
||||
@@ -416,5 +416,23 @@ class User {
|
||||
if (!progress) return false
|
||||
return progress.removeFromContinueListening()
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of podcast episodes not finished for library item
|
||||
* Note: libraryItem passed in from libraryHelpers is not a LibraryItem class instance
|
||||
* @param {LibraryItem|object} libraryItem
|
||||
* @returns {number}
|
||||
*/
|
||||
getNumEpisodesIncompleteForPodcast(libraryItem) {
|
||||
if (!libraryItem?.media.episodes) return 0
|
||||
let numEpisodesIncomplete = 0
|
||||
for (const episode of libraryItem.media.episodes) {
|
||||
const mediaProgress = this.getMediaProgress(libraryItem.id, episode.id)
|
||||
if (!mediaProgress?.isFinished) {
|
||||
numEpisodesIncomplete++
|
||||
}
|
||||
}
|
||||
return numEpisodesIncomplete
|
||||
}
|
||||
}
|
||||
module.exports = User
|
||||
@@ -14,7 +14,7 @@ module.exports = {
|
||||
getFilteredLibraryItems(libraryItems, filterBy, user, feedsArray) {
|
||||
let filtered = libraryItems
|
||||
|
||||
const searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'missing', 'languages', 'tracks', 'ebooks']
|
||||
const searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'publishers', 'missing', 'languages', 'tracks', 'ebooks']
|
||||
const group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
||||
if (group) {
|
||||
const filterVal = filterBy.replace(`${group}.`, '')
|
||||
@@ -29,6 +29,7 @@ module.exports = {
|
||||
}
|
||||
else if (group === 'authors') filtered = filtered.filter(li => li.isBook && li.media.metadata.hasAuthor(filter))
|
||||
else if (group === 'narrators') filtered = filtered.filter(li => li.isBook && li.media.metadata.hasNarrator(filter))
|
||||
else if (group === 'publishers') filtered = filtered.filter(li => li.isBook && li.media.metadata.publisher === filter)
|
||||
else if (group === 'progress') {
|
||||
filtered = filtered.filter(li => {
|
||||
const itemProgress = user.getMediaProgress(li.id)
|
||||
@@ -82,16 +83,17 @@ module.exports = {
|
||||
|
||||
// Returns false if should be filtered out
|
||||
checkFilterForSeriesLibraryItem(libraryItem, filterBy) {
|
||||
var searchGroups = ['genres', 'tags', 'authors', 'progress', 'narrators', 'languages']
|
||||
var group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
||||
const searchGroups = ['genres', 'tags', 'authors', 'progress', 'narrators', 'publishers', 'languages']
|
||||
const group = searchGroups.find(_group => filterBy.startsWith(_group + '.'))
|
||||
if (group) {
|
||||
var filterVal = filterBy.replace(`${group}.`, '')
|
||||
var filter = this.decode(filterVal)
|
||||
const filterVal = filterBy.replace(`${group}.`, '')
|
||||
const filter = this.decode(filterVal)
|
||||
|
||||
if (group === 'genres') return libraryItem.media.metadata.genres.includes(filter)
|
||||
else if (group === 'tags') return libraryItem.media.tags.includes(filter)
|
||||
else if (group === 'authors') return libraryItem.isBook && libraryItem.media.metadata.hasAuthor(filter)
|
||||
else if (group === 'narrators') return libraryItem.isBook && libraryItem.media.metadata.hasNarrator(filter)
|
||||
else if (group === 'publishers') return libraryItem.isBook && libraryItem.media.metadata.publisher === filter
|
||||
else if (group === 'languages') {
|
||||
return libraryItem.media.metadata.language === filter
|
||||
}
|
||||
@@ -123,27 +125,28 @@ module.exports = {
|
||||
},
|
||||
|
||||
getDistinctFilterDataNew(libraryItems) {
|
||||
var data = {
|
||||
const data = {
|
||||
authors: [],
|
||||
genres: [],
|
||||
tags: [],
|
||||
series: [],
|
||||
narrators: [],
|
||||
languages: []
|
||||
languages: [],
|
||||
publishers: []
|
||||
}
|
||||
libraryItems.forEach((li) => {
|
||||
var mediaMetadata = li.media.metadata
|
||||
if (mediaMetadata.authors && mediaMetadata.authors.length) {
|
||||
const mediaMetadata = li.media.metadata
|
||||
if (mediaMetadata.authors?.length) {
|
||||
mediaMetadata.authors.forEach((author) => {
|
||||
if (author && !data.authors.find(au => au.id === author.id)) data.authors.push({ id: author.id, name: author.name })
|
||||
if (author && !data.authors.some(au => au.id === author.id)) data.authors.push({ id: author.id, name: author.name })
|
||||
})
|
||||
}
|
||||
if (mediaMetadata.series && mediaMetadata.series.length) {
|
||||
if (mediaMetadata.series?.length) {
|
||||
mediaMetadata.series.forEach((series) => {
|
||||
if (series && !data.series.find(se => se.id === series.id)) data.series.push({ id: series.id, name: series.name })
|
||||
if (series && !data.series.some(se => se.id === series.id)) data.series.push({ id: series.id, name: series.name })
|
||||
})
|
||||
}
|
||||
if (mediaMetadata.genres && mediaMetadata.genres.length) {
|
||||
if (mediaMetadata.genres?.length) {
|
||||
mediaMetadata.genres.forEach((genre) => {
|
||||
if (genre && !data.genres.includes(genre)) data.genres.push(genre)
|
||||
})
|
||||
@@ -153,18 +156,24 @@ module.exports = {
|
||||
if (tag && !data.tags.includes(tag)) data.tags.push(tag)
|
||||
})
|
||||
}
|
||||
if (mediaMetadata.narrators && mediaMetadata.narrators.length) {
|
||||
if (mediaMetadata.narrators?.length) {
|
||||
mediaMetadata.narrators.forEach((narrator) => {
|
||||
if (narrator && !data.narrators.includes(narrator)) data.narrators.push(narrator)
|
||||
})
|
||||
}
|
||||
if (mediaMetadata.language && !data.languages.includes(mediaMetadata.language)) data.languages.push(mediaMetadata.language)
|
||||
if (mediaMetadata.publisher && !data.publishers.includes(mediaMetadata.publisher)) {
|
||||
data.publishers.push(mediaMetadata.publisher)
|
||||
}
|
||||
if (mediaMetadata.language && !data.languages.includes(mediaMetadata.language)) {
|
||||
data.languages.push(mediaMetadata.language)
|
||||
}
|
||||
})
|
||||
data.authors = naturalSort(data.authors).asc(au => au.name)
|
||||
data.genres = naturalSort(data.genres).asc()
|
||||
data.tags = naturalSort(data.tags).asc()
|
||||
data.series = naturalSort(data.series).asc(se => se.name)
|
||||
data.narrators = naturalSort(data.narrators).asc()
|
||||
data.publishers = naturalSort(data.publishers).asc()
|
||||
data.languages = naturalSort(data.languages).asc()
|
||||
return data
|
||||
},
|
||||
@@ -351,6 +360,7 @@ module.exports = {
|
||||
const mediaType = library.mediaType
|
||||
const isPodcastLibrary = mediaType === 'podcast'
|
||||
const includeRssFeed = include.includes('rssfeed')
|
||||
const includeNumEpisodesIncomplete = include.includes('numepisodesincomplete') // Podcasts only
|
||||
const hideSingleBookSeries = library.settings.hideSingleBookSeries
|
||||
|
||||
const shelves = [
|
||||
@@ -447,12 +457,18 @@ module.exports = {
|
||||
|
||||
for (const libraryItem of libraryItems) {
|
||||
if (libraryItem.addedAt > categoryMap['recently-added'].smallest) {
|
||||
const libraryItemObj = libraryItem.toJSONMinified()
|
||||
|
||||
// add numEpisodesIncomplete if "include=numEpisodesIncomplete" was put in query string (only for podcasts)
|
||||
if (includeNumEpisodesIncomplete && libraryItem.isPodcast) {
|
||||
libraryItemObj.numEpisodesIncomplete = user.getNumEpisodesIncompleteForPodcast(libraryItem)
|
||||
}
|
||||
|
||||
const indexToPut = categoryMap['recently-added'].items.findIndex(i => libraryItem.addedAt > i.addedAt)
|
||||
if (indexToPut >= 0) {
|
||||
categoryMap['recently-added'].items.splice(indexToPut, 0, libraryItem.toJSONMinified())
|
||||
categoryMap['recently-added'].items.splice(indexToPut, 0, libraryItemObj)
|
||||
} else {
|
||||
categoryMap['recently-added'].items.push(libraryItem.toJSONMinified())
|
||||
categoryMap['recently-added'].items.push(libraryItemObj)
|
||||
}
|
||||
|
||||
if (categoryMap['recently-added'].items.length > maxEntitiesPerShelf) {
|
||||
|
||||
@@ -43,6 +43,8 @@ module.exports.parse = (nameString) => {
|
||||
// Example &LF: Friedman, Milton & Friedman, Rose
|
||||
if (nameString.includes('&')) {
|
||||
nameString.split('&').forEach((asa) => splitNames = splitNames.concat(asa.split(',')))
|
||||
} else if (nameString.includes(' and ')) {
|
||||
nameString.split(' and ').forEach((asa) => splitNames = splitNames.concat(asa.split(',')))
|
||||
} else if (nameString.includes(';')) {
|
||||
nameString.split(';').forEach((asa) => splitNames = splitNames.concat(asa.split(',')))
|
||||
} else {
|
||||
|
||||
@@ -17,7 +17,7 @@ function parseCreators(metadata) {
|
||||
|
||||
function fetchCreators(creators, role) {
|
||||
if (!creators || !creators.length) return null
|
||||
return creators.filter(c => c.role === role).map(c => c.value)
|
||||
return [...new Set(creators.filter(c => c.role === role).map(c => c.value))]
|
||||
}
|
||||
|
||||
function fetchTagString(metadata, tag) {
|
||||
@@ -92,7 +92,7 @@ function fetchDescription(metadata) {
|
||||
|
||||
function fetchGenres(metadata) {
|
||||
if (!metadata['dc:subject'] || !metadata['dc:subject'].length) return []
|
||||
return metadata['dc:subject'].map(g => typeof g === 'string' ? g : null).filter(g => !!g)
|
||||
return [...new Set(metadata['dc:subject'].filter(g => typeof g === 'string'))]
|
||||
}
|
||||
|
||||
function fetchLanguage(metadata) {
|
||||
@@ -122,7 +122,7 @@ function fetchNarrators(creators, metadata) {
|
||||
|
||||
function fetchTags(metadata) {
|
||||
if (!metadata['dc:tag'] || !metadata['dc:tag'].length) return []
|
||||
return metadata['dc:tag'].filter(tag => (typeof tag === 'string'))
|
||||
return [...new Set(metadata['dc:tag'].filter(tag => typeof tag === 'string'))]
|
||||
}
|
||||
|
||||
function stripPrefix(str) {
|
||||
|
||||
@@ -139,15 +139,16 @@ function isNullOrNaN(val) {
|
||||
*/
|
||||
function parseChapters(chapters) {
|
||||
if (!chapters) return []
|
||||
let index = 0
|
||||
return chapters.map(chap => {
|
||||
var title = chap['TAG:title'] || chap.title || ''
|
||||
if (!title && chap.tags && chap.tags.title) title = chap.tags.title
|
||||
let title = chap['TAG:title'] || chap.title || ''
|
||||
if (!title && chap.tags?.title) title = chap.tags.title
|
||||
|
||||
var timebase = chap.time_base && chap.time_base.includes('/') ? Number(chap.time_base.split('/')[1]) : 1
|
||||
var start = !isNullOrNaN(chap.start_time) ? Number(chap.start_time) : !isNullOrNaN(chap.start) ? Number(chap.start) / timebase : 0
|
||||
var end = !isNullOrNaN(chap.end_time) ? Number(chap.end_time) : !isNullOrNaN(chap.end) ? Number(chap.end) / timebase : 0
|
||||
const timebase = chap.time_base?.includes('/') ? Number(chap.time_base.split('/')[1]) : 1
|
||||
const start = !isNullOrNaN(chap.start_time) ? Number(chap.start_time) : !isNullOrNaN(chap.start) ? Number(chap.start) / timebase : 0
|
||||
const end = !isNullOrNaN(chap.end_time) ? Number(chap.end_time) : !isNullOrNaN(chap.end) ? Number(chap.end) / timebase : 0
|
||||
return {
|
||||
id: chap.id,
|
||||
id: index++,
|
||||
start,
|
||||
end,
|
||||
title
|
||||
|
||||
Reference in New Issue
Block a user