mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-06-04 09:50:42 +02:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd2d61f38e | |||
| ca2c2f2702 | |||
| 1fc929ab33 | |||
| f5495d64a9 | |||
| d6afb17bf2 | |||
| 2cba9d8f4a | |||
| e02169907d | |||
| 24a142e718 | |||
| 2cb4f972d7 | |||
| 513d946faa | |||
| 87d1f457ba | |||
| 8810f90226 | |||
| 3d3571013f | |||
| 605a6d8b25 | |||
| 1bfa4b31f2 | |||
| 7a14b49aea | |||
| 95ac74d748 | |||
| fddf850a41 | |||
| d93d4f3236 | |||
| 91f15d5a23 | |||
| 516c5c3308 |
@@ -88,6 +88,7 @@ export default {
|
|||||||
if (this.page === 'collections') return "You haven't made any collections yet"
|
if (this.page === 'collections') return "You haven't made any collections yet"
|
||||||
if (this.hasFilter) {
|
if (this.hasFilter) {
|
||||||
if (this.filterName === 'Issues') return 'No Issues'
|
if (this.filterName === 'Issues') return 'No Issues'
|
||||||
|
else if (this.filterName === 'Feed-open') return 'No RSS feeds are open'
|
||||||
return `No Results for filter "${this.filterName}: ${this.filterValue}"`
|
return `No Results for filter "${this.filterName}: ${this.filterValue}"`
|
||||||
}
|
}
|
||||||
return 'No results'
|
return 'No results'
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full border-b border-gray-700 pb-2">
|
<div v-if="book" class="w-full border-b border-gray-700 pb-2">
|
||||||
<div class="flex py-1 hover:bg-gray-300 hover:bg-opacity-10 cursor-pointer" @click="selectMatch">
|
<div class="flex py-1 hover:bg-gray-300 hover:bg-opacity-10 cursor-pointer" @click="selectMatch">
|
||||||
<div class="h-24 bg-primary" :style="{ minWidth: 96 / bookCoverAspectRatio + 'px' }">
|
<div class="min-w-12 max-w-12 md:min-w-20 md:max-w-20">
|
||||||
<img v-if="selectedCover" :src="selectedCover" class="h-full w-full object-contain" />
|
<div class="w-full bg-primary">
|
||||||
</div>
|
<img v-if="selectedCover" :src="selectedCover" class="h-full w-full object-contain" />
|
||||||
<div v-if="!isPodcast" class="px-4 flex-grow">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<h1 class="text-base">{{ book.title }}</h1>
|
|
||||||
<div class="flex-grow" />
|
|
||||||
<p>{{ book.publishedYear }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="text-gray-300 text-sm">{{ book.author }}</p>
|
</div>
|
||||||
|
<div v-if="!isPodcast" class="px-2 md:px-4 flex-grow">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<h1 class="text-sm md:text-base">{{ book.title }}</h1>
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<p class="text-sm md:text-base">{{ book.publishedYear }}</p>
|
||||||
|
</div>
|
||||||
|
<p class="text-gray-300 text-xs md:text-sm">by {{ book.author }}</p>
|
||||||
|
<p v-if="book.narrator" class="text-gray-400 text-xs">Narrated by {{ book.narrator }}</p>
|
||||||
|
<p v-if="book.duration" class="text-gray-400 text-xs">Runtime: {{ $elapsedPrettyExtended(book.duration) }}</p>
|
||||||
<div v-if="book.series && book.series.length" class="flex py-1 -mx-1">
|
<div v-if="book.series && book.series.length" class="flex py-1 -mx-1">
|
||||||
<div v-for="(series, index) in book.series" :key="index" class="bg-white bg-opacity-10 rounded-full px-1 py-0.5 mx-1">
|
<div v-for="(series, index) in book.series" :key="index" class="bg-white bg-opacity-10 rounded-full px-1 py-0.5 mx-1">
|
||||||
<p class="leading-3 text-xs text-gray-400">
|
<p class="leading-3 text-xs text-gray-400">
|
||||||
@@ -25,7 +29,7 @@
|
|||||||
<div v-else class="px-4 flex-grow">
|
<div v-else class="px-4 flex-grow">
|
||||||
<h1>{{ book.title }}</h1>
|
<h1>{{ book.title }}</h1>
|
||||||
<p class="text-base text-gray-300 whitespace-nowrap truncate">by {{ book.author }}</p>
|
<p class="text-base text-gray-300 whitespace-nowrap truncate">by {{ book.author }}</p>
|
||||||
<p class="text-xs text-gray-400 leading-5">{{ book.genres.join(', ') }}</p>
|
<p v-if="book.genres" class="text-xs text-gray-400 leading-5">{{ book.genres.join(', ') }}</p>
|
||||||
<p class="text-xs text-gray-400 leading-5">{{ book.trackCount }} Episodes</p>
|
<p class="text-xs text-gray-400 leading-5">{{ book.trackCount }} Episodes</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,7 +67,7 @@ export default {
|
|||||||
selectMatch() {
|
selectMatch() {
|
||||||
var book = { ...this.book }
|
var book = { ...this.book }
|
||||||
book.cover = this.selectedCover
|
book.cover = this.selectedCover
|
||||||
this.$emit('select', this.book)
|
this.$emit('select', book)
|
||||||
},
|
},
|
||||||
clickCover(cover) {
|
clickCover(cover) {
|
||||||
this.selectedCover = cover
|
this.selectedCover = cover
|
||||||
|
|||||||
@@ -78,6 +78,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
|
|
||||||
|
<div v-if="rssFeed && !isSelectionMode && !isHovering" class="absolute text-success top-0 left-0 z-10" :style="{ padding: 0.375 * sizeMultiplier + 'rem' }">
|
||||||
|
<span class="material-icons" :style="{ fontSize: sizeMultiplier * 1.5 + 'rem' }">rss_feed</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Series sequence -->
|
<!-- Series sequence -->
|
||||||
<div v-if="seriesSequence && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
<div v-if="seriesSequence && !isHovering && !isSelectionMode" class="absolute rounded-lg bg-black bg-opacity-90 box-shadow-md z-10" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
|
||||||
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequence }}</p>
|
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }">#{{ seriesSequence }}</p>
|
||||||
@@ -444,6 +448,10 @@ export default {
|
|||||||
if (!this.isAlternativeBookshelfView && !this.isAuthorBookshelfView) return 0
|
if (!this.isAlternativeBookshelfView && !this.isAuthorBookshelfView) return 0
|
||||||
else if (!this.displaySortLine) return 3 * this.sizeMultiplier
|
else if (!this.displaySortLine) return 3 * this.sizeMultiplier
|
||||||
return 4.25 * this.sizeMultiplier
|
return 4.25 * this.sizeMultiplier
|
||||||
|
},
|
||||||
|
rssFeed() {
|
||||||
|
if (this.booksInSeries) return null
|
||||||
|
return this.store.getters['feeds/getFeedForItem'](this.libraryItemId)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -116,6 +116,11 @@ export default {
|
|||||||
text: 'Issues',
|
text: 'Issues',
|
||||||
value: 'issues',
|
value: 'issues',
|
||||||
sublist: false
|
sublist: false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'RSS Feed Open',
|
||||||
|
value: 'feed-open',
|
||||||
|
sublist: false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
podcastItems: [
|
podcastItems: [
|
||||||
|
|||||||
@@ -125,6 +125,9 @@ export default {
|
|||||||
},
|
},
|
||||||
userToken() {
|
userToken() {
|
||||||
return this.$store.getters['user/getToken']
|
return this.$store.getters['user/getToken']
|
||||||
|
},
|
||||||
|
resolution() {
|
||||||
|
return `${this.naturalWidth}x${this.naturalHeight}px`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative rounded-sm overflow-hidden" :style="{ height: width * bookCoverAspectRatio + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }" @mouseover="isHovering = true" @mouseleave="isHovering = false">
|
<div class="relative rounded-sm" :style="{ height: width * bookCoverAspectRatio + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }" @mouseover="isHovering = true" @mouseleave="isHovering = false">
|
||||||
<div class="w-full h-full relative">
|
<div class="w-full h-full relative overflow-hidden">
|
||||||
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
||||||
<div class="absolute cover-bg" ref="coverBg" />
|
<div class="absolute cover-bg" ref="coverBg" />
|
||||||
</div>
|
</div>
|
||||||
@@ -17,6 +17,8 @@
|
|||||||
<p class="text-center font-book text-error" :style="{ fontSize: sizeMultiplier + 'rem' }">Invalid Cover</p>
|
<p class="text-center font-book text-error" :style="{ fontSize: sizeMultiplier + 'rem' }">Invalid Cover</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p v-if="!imageFailed" class="absolute -bottom-5 left-0 right-0 mx-auto text-xs text-gray-300 text-center">{{ resolution }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -54,6 +56,9 @@ export default {
|
|||||||
},
|
},
|
||||||
placeholderCoverPadding() {
|
placeholderCoverPadding() {
|
||||||
return 0.8 * this.sizeMultiplier
|
return 0.8 * this.sizeMultiplier
|
||||||
|
},
|
||||||
|
resolution() {
|
||||||
|
return `${this.naturalWidth}x${this.naturalHeight}px`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
<div class="bg-bg rounded-lg px-2 py-6 sm:p-6 md:p-8" @click.stop>
|
<div class="bg-bg rounded-lg px-2 py-6 sm:p-6 md:p-8" @click.stop>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="flex-grow p-1 min-w-48 sm:min-w-64 md:min-w-80">
|
<div class="flex-grow p-1 min-w-48 sm:min-w-64 md:min-w-80">
|
||||||
<ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!selectedSeries.id.startsWith('new')" label="Series Name" />
|
<ui-input-dropdown ref="newSeriesSelect" v-model="selectedSeries.name" :items="existingSeriesNames" :disabled="!isNewSeries" label="Series Name" />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-24 sm:w-28 md:w-40 p-1">
|
<div class="w-24 sm:w-28 md:w-40 p-1">
|
||||||
<ui-text-input-with-label v-model="selectedSeries.sequence" label="Sequence" />
|
<ui-text-input-with-label ref="sequenceInput" v-model="selectedSeries.sequence" label="Sequence" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex justify-end mt-2 p-1">
|
<div class="flex justify-end mt-2 p-1">
|
||||||
@@ -59,9 +59,26 @@ export default {
|
|||||||
set(val) {
|
set(val) {
|
||||||
this.$emit('input', val)
|
this.$emit('input', val)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
isNewSeries() {
|
||||||
|
if (!this.selectedSeries || !this.selectedSeries.id) return false
|
||||||
|
return this.selectedSeries.id.startsWith('new')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setInputFocus() {
|
||||||
|
if (this.isNewSeries) {
|
||||||
|
// Focus on series input if new series
|
||||||
|
if (this.$refs.newSeriesSelect) {
|
||||||
|
this.$refs.newSeriesSelect.setFocus()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Focus on sequence input if existing series
|
||||||
|
if (this.$refs.sequenceInput) {
|
||||||
|
this.$refs.sequenceInput.setFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
submitSeriesForm() {
|
submitSeriesForm() {
|
||||||
if (this.$refs.newSeriesSelect) {
|
if (this.$refs.newSeriesSelect) {
|
||||||
this.$refs.newSeriesSelect.blur()
|
this.$refs.newSeriesSelect.blur()
|
||||||
@@ -92,6 +109,8 @@ export default {
|
|||||||
|
|
||||||
this.$store.commit('setInnerModalOpen', true)
|
this.$store.commit('setInnerModalOpen', true)
|
||||||
this.$eventBus.$on('modal-hotkey', this.hotkey)
|
this.$eventBus.$on('modal-hotkey', this.hotkey)
|
||||||
|
|
||||||
|
this.setInputFocus()
|
||||||
},
|
},
|
||||||
setHide() {
|
setHide() {
|
||||||
if (this.content) this.content.style.transform = 'scale(0)'
|
if (this.content) this.content.style.transform = 'scale(0)'
|
||||||
|
|||||||
@@ -31,9 +31,9 @@
|
|||||||
|
|
||||||
<div v-if="showLocalCovers" class="flex items-center justify-center">
|
<div v-if="showLocalCovers" class="flex items-center justify-center">
|
||||||
<template v-for="cover in localCovers">
|
<template v-for="cover in localCovers">
|
||||||
<div :key="cover.path" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.metadata.path === coverPath ? 'border-yellow-300' : ''" @click="setCover(cover)">
|
<div :key="cover.path" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover.metadata.path === coverPath ? 'border-yellow-300' : ''" @click="setCover(cover)">
|
||||||
<div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }">
|
<div class="h-24 bg-primary" :style="{ width: 96 / bookCoverAspectRatio + 'px' }">
|
||||||
<img :src="`${cover.localPath}?token=${userToken}`" class="h-full w-full object-contain" />
|
<covers-preview-cover :src="`${cover.localPath}?token=${userToken}`" :width="96 / bookCoverAspectRatio" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
<div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-80 overflow-y-scroll mt-2 max-w-full">
|
<div v-if="hasSearched" class="flex items-center flex-wrap justify-center max-h-80 overflow-y-scroll mt-2 max-w-full">
|
||||||
<p v-if="!coversFound.length">No Covers Found</p>
|
<p v-if="!coversFound.length">No Covers Found</p>
|
||||||
<template v-for="cover in coversFound">
|
<template v-for="cover in coversFound">
|
||||||
<div :key="cover" class="m-0.5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="updateCover(cover)">
|
<div :key="cover" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === imageUrl ? 'border-yellow-300' : ''" @click="updateCover(cover)">
|
||||||
<covers-preview-cover :src="cover" :width="80" show-open-new-tab :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-preview-cover :src="cover" :width="80" show-open-new-tab :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="match-wrapper" class="w-full h-full overflow-hidden px-4 py-6 relative">
|
<div id="match-wrapper" class="w-full h-full overflow-hidden px-2 md:px-4 py-4 md:py-6 relative">
|
||||||
<form @submit.prevent="submitSearch">
|
<form @submit.prevent="submitSearch">
|
||||||
<div class="flex items-center justify-start -mx-1 h-20">
|
<div class="flex flex-wrap md:flex-nowrap items-center justify-start -mx-1">
|
||||||
<div class="w-40 px-1">
|
<div class="w-36 px-1">
|
||||||
<ui-dropdown v-model="provider" :items="providers" label="Provider" small />
|
<ui-dropdown v-model="provider" :items="providers" label="Provider" small />
|
||||||
</div>
|
</div>
|
||||||
<div class="w-72 px-1">
|
<div class="flex-grow md:w-72 px-1">
|
||||||
<ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" placeholder="Search" />
|
<ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" placeholder="Search" />
|
||||||
</div>
|
</div>
|
||||||
<div v-show="provider != 'itunes'" class="w-72 px-1">
|
<div v-show="provider != 'itunes'" class="w-60 md:w-72 px-1">
|
||||||
<ui-text-input-with-label v-model="searchAuthor" label="Author" />
|
<ui-text-input-with-label v-model="searchAuthor" label="Author" />
|
||||||
</div>
|
</div>
|
||||||
<ui-btn class="mt-5 ml-1" type="submit">Search</ui-btn>
|
<ui-btn class="mt-5 ml-1" type="submit">Search</ui-btn>
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
<div v-show="!processing && !searchResults.length && hasSearched" class="flex h-full items-center justify-center">
|
<div v-show="!processing && !searchResults.length && hasSearched" class="flex h-full items-center justify-center">
|
||||||
<p>No Results</p>
|
<p>No Results</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="!processing" class="w-full max-h-full overflow-y-auto overflow-x-hidden matchListWrapper">
|
<div v-show="!processing" class="w-full max-h-full overflow-y-auto overflow-x-hidden matchListWrapper mt-4">
|
||||||
<template v-for="(res, index) in searchResults">
|
<template v-for="(res, index) in searchResults">
|
||||||
<cards-book-match-card :key="index" :book="res" :is-podcast="isPodcast" :book-cover-aspect-ratio="bookCoverAspectRatio" @select="selectMatch" />
|
<cards-book-match-card :key="index" :book="res" :is-podcast="isPodcast" :book-cover-aspect-ratio="bookCoverAspectRatio" @select="selectMatch" />
|
||||||
</template>
|
</template>
|
||||||
@@ -299,7 +299,7 @@ export default {
|
|||||||
this.isProcessing = true
|
this.isProcessing = true
|
||||||
this.lastSearch = searchQuery
|
this.lastSearch = searchQuery
|
||||||
var searchEntity = this.isPodcast ? 'podcast' : 'books'
|
var searchEntity = this.isPodcast ? 'podcast' : 'books'
|
||||||
var results = await this.$axios.$get(`/api/search/${searchEntity}?${searchQuery}`).catch((error) => {
|
var results = await this.$axios.$get(`/api/search/${searchEntity}?${searchQuery}`, { timeout: 10000 }).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
@@ -363,6 +363,10 @@ export default {
|
|||||||
this.searchAuthor = this.libraryItem.media.metadata.authorName || ''
|
this.searchAuthor = this.libraryItem.media.metadata.authorName || ''
|
||||||
if (this.isPodcast) this.provider = 'itunes'
|
if (this.isPodcast) this.provider = 'itunes'
|
||||||
else this.provider = localStorage.getItem('book-provider') || 'google'
|
else this.provider = localStorage.getItem('book-provider') || 'google'
|
||||||
|
|
||||||
|
if (this.searchTitle) {
|
||||||
|
this.submitSearch()
|
||||||
|
}
|
||||||
},
|
},
|
||||||
selectMatch(match) {
|
selectMatch(match) {
|
||||||
if (match) {
|
if (match) {
|
||||||
@@ -497,6 +501,11 @@ export default {
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.matchListWrapper {
|
.matchListWrapper {
|
||||||
height: calc(100% - 80px);
|
height: calc(100% - 124px);
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.matchListWrapper {
|
||||||
|
height: calc(100% - 80px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,33 +5,14 @@
|
|||||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div ref="wrapper" class="p-4 w-full text-sm py-2 rounded-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh">
|
<div class="absolute -top-10 left-0 z-10 w-full flex">
|
||||||
<div class="flex flex-wrap">
|
<template v-for="tab in tabs">
|
||||||
<div class="w-1/5 p-1">
|
<div :key="tab.id" class="w-28 rounded-t-lg flex items-center justify-center mr-0.5 sm:mr-1 cursor-pointer hover:bg-bg font-book border-t border-l border-r border-black-300 tab text-xs sm:text-base" :class="selectedTab === tab.id ? 'tab-selected bg-bg pb-px' : 'bg-primary text-gray-400'" @click="selectTab(tab.id)">{{ tab.title }}</div>
|
||||||
<ui-text-input-with-label v-model="newEpisode.season" label="Season" />
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-1/5 p-1">
|
|
||||||
<ui-text-input-with-label v-model="newEpisode.episode" label="Episode" />
|
<div ref="wrapper" class="p-4 w-full text-sm rounded-b-lg rounded-tr-lg bg-bg shadow-lg border border-black-300 relative overflow-y-auto" style="max-height: 80vh">
|
||||||
</div>
|
<component v-if="libraryItem && show" :is="tabComponentName" :library-item="libraryItem" :episode="episode" :processing.sync="processing" @close="show = false" @selectTab="selectTab" />
|
||||||
<div class="w-1/5 p-1">
|
|
||||||
<ui-text-input-with-label v-model="newEpisode.episodeType" label="Episode Type" />
|
|
||||||
</div>
|
|
||||||
<div class="w-2/5 p-1">
|
|
||||||
<ui-text-input-with-label v-model="pubDateInput" @input="updatePubDate" type="datetime-local" label="Pub Date" />
|
|
||||||
</div>
|
|
||||||
<div class="w-full p-1">
|
|
||||||
<ui-text-input-with-label v-model="newEpisode.title" label="Title" />
|
|
||||||
</div>
|
|
||||||
<div class="w-full p-1">
|
|
||||||
<ui-textarea-with-label v-model="newEpisode.subtitle" label="Subtitle" :rows="3" />
|
|
||||||
</div>
|
|
||||||
<div class="w-full p-1 default-style">
|
|
||||||
<ui-rich-text-editor v-if="show" label="Description" v-model="newEpisode.description" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-end pt-4">
|
|
||||||
<ui-btn @click="submit">Submit</ui-btn>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</modals-modal>
|
</modals-modal>
|
||||||
</template>
|
</template>
|
||||||
@@ -41,25 +22,19 @@ export default {
|
|||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
processing: false,
|
processing: false,
|
||||||
newEpisode: {
|
selectedTab: 'details',
|
||||||
season: null,
|
tabs: [
|
||||||
episode: null,
|
{
|
||||||
episodeType: null,
|
id: 'details',
|
||||||
title: null,
|
title: 'Details',
|
||||||
subtitle: null,
|
component: 'modals-podcast-tabs-episode-details'
|
||||||
description: null,
|
},
|
||||||
pubDate: null,
|
{
|
||||||
publishedAt: null
|
id: 'match',
|
||||||
},
|
title: 'Match',
|
||||||
pubDateInput: null
|
component: 'modals-podcast-tabs-episode-match'
|
||||||
}
|
}
|
||||||
},
|
]
|
||||||
watch: {
|
|
||||||
episode: {
|
|
||||||
immediate: true,
|
|
||||||
handler(newVal) {
|
|
||||||
if (newVal) this.init()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -77,67 +52,29 @@ export default {
|
|||||||
episode() {
|
episode() {
|
||||||
return this.$store.state.globals.selectedEpisode
|
return this.$store.state.globals.selectedEpisode
|
||||||
},
|
},
|
||||||
episodeId() {
|
|
||||||
return this.episode ? this.episode.id : null
|
|
||||||
},
|
|
||||||
title() {
|
title() {
|
||||||
if (!this.libraryItem) return ''
|
if (!this.libraryItem) return ''
|
||||||
return this.libraryItem.media.metadata.title || 'Unknown'
|
return this.libraryItem.media.metadata.title || 'Unknown'
|
||||||
|
},
|
||||||
|
tabComponentName() {
|
||||||
|
var _tab = this.tabs.find((t) => t.id === this.selectedTab)
|
||||||
|
return _tab ? _tab.component : ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updatePubDate(val) {
|
selectTab(tab) {
|
||||||
if (val) {
|
this.selectedTab = tab
|
||||||
this.newEpisode.pubDate = this.$formatJsDate(new Date(val), 'E, d MMM yyyy HH:mm:ssxx')
|
|
||||||
this.newEpisode.publishedAt = new Date(val).valueOf()
|
|
||||||
} else {
|
|
||||||
this.newEpisode.pubDate = null
|
|
||||||
this.newEpisode.publishedAt = null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
init() {
|
|
||||||
this.newEpisode.season = this.episode.season || ''
|
|
||||||
this.newEpisode.episode = this.episode.episode || ''
|
|
||||||
this.newEpisode.episodeType = this.episode.episodeType || ''
|
|
||||||
this.newEpisode.title = this.episode.title || ''
|
|
||||||
this.newEpisode.subtitle = this.episode.subtitle || ''
|
|
||||||
this.newEpisode.description = this.episode.description || ''
|
|
||||||
this.newEpisode.pubDate = this.episode.pubDate || ''
|
|
||||||
this.newEpisode.publishedAt = this.episode.publishedAt
|
|
||||||
|
|
||||||
this.pubDateInput = this.episode.pubDate ? this.$formatJsDate(new Date(this.episode.pubDate), "yyyy-MM-dd'T'HH:mm") : null
|
|
||||||
},
|
|
||||||
getUpdatePayload() {
|
|
||||||
var updatePayload = {}
|
|
||||||
for (const key in this.newEpisode) {
|
|
||||||
if (this.newEpisode[key] != this.episode[key]) {
|
|
||||||
updatePayload[key] = this.newEpisode[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return updatePayload
|
|
||||||
},
|
|
||||||
submit() {
|
|
||||||
const payload = this.getUpdatePayload()
|
|
||||||
if (!Object.keys(payload).length) {
|
|
||||||
return this.$toast.info('No updates were made')
|
|
||||||
}
|
|
||||||
|
|
||||||
this.processing = true
|
|
||||||
this.$axios
|
|
||||||
.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, payload)
|
|
||||||
.then(() => {
|
|
||||||
this.processing = false
|
|
||||||
this.$toast.success('Podcast episode updated')
|
|
||||||
this.show = false
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed update episode'
|
|
||||||
console.error('Failed update episode', error)
|
|
||||||
this.processing = false
|
|
||||||
this.$toast.error(errorMsg)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.tab {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
.tab.tab-selected {
|
||||||
|
height: 41px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="flex flex-wrap">
|
||||||
|
<div class="w-1/5 p-1">
|
||||||
|
<ui-text-input-with-label v-model="newEpisode.season" label="Season" />
|
||||||
|
</div>
|
||||||
|
<div class="w-1/5 p-1">
|
||||||
|
<ui-text-input-with-label v-model="newEpisode.episode" label="Episode" />
|
||||||
|
</div>
|
||||||
|
<div class="w-1/5 p-1">
|
||||||
|
<ui-text-input-with-label v-model="newEpisode.episodeType" label="Episode Type" />
|
||||||
|
</div>
|
||||||
|
<div class="w-2/5 p-1">
|
||||||
|
<ui-text-input-with-label v-model="pubDateInput" @input="updatePubDate" type="datetime-local" label="Pub Date" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full p-1">
|
||||||
|
<ui-text-input-with-label v-model="newEpisode.title" label="Title" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full p-1">
|
||||||
|
<ui-textarea-with-label v-model="newEpisode.subtitle" label="Subtitle" :rows="3" />
|
||||||
|
</div>
|
||||||
|
<div class="w-full p-1 default-style">
|
||||||
|
<ui-rich-text-editor label="Description" v-model="newEpisode.description" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end pt-4">
|
||||||
|
<ui-btn @click="submit">Submit</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
processing: Boolean,
|
||||||
|
libraryItem: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
episode: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
newEpisode: {
|
||||||
|
season: null,
|
||||||
|
episode: null,
|
||||||
|
episodeType: null,
|
||||||
|
title: null,
|
||||||
|
subtitle: null,
|
||||||
|
description: null,
|
||||||
|
pubDate: null,
|
||||||
|
publishedAt: null
|
||||||
|
},
|
||||||
|
pubDateInput: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
episode: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isProcessing: {
|
||||||
|
get() {
|
||||||
|
return this.processing
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('update:processing', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
episodeId() {
|
||||||
|
return this.episode ? this.episode.id : null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updatePubDate(val) {
|
||||||
|
if (val) {
|
||||||
|
this.newEpisode.pubDate = this.$formatJsDate(new Date(val), 'E, d MMM yyyy HH:mm:ssxx')
|
||||||
|
this.newEpisode.publishedAt = new Date(val).valueOf()
|
||||||
|
} else {
|
||||||
|
this.newEpisode.pubDate = null
|
||||||
|
this.newEpisode.publishedAt = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.newEpisode.season = this.episode.season || ''
|
||||||
|
this.newEpisode.episode = this.episode.episode || ''
|
||||||
|
this.newEpisode.episodeType = this.episode.episodeType || ''
|
||||||
|
this.newEpisode.title = this.episode.title || ''
|
||||||
|
this.newEpisode.subtitle = this.episode.subtitle || ''
|
||||||
|
this.newEpisode.description = this.episode.description || ''
|
||||||
|
this.newEpisode.pubDate = this.episode.pubDate || ''
|
||||||
|
this.newEpisode.publishedAt = this.episode.publishedAt
|
||||||
|
|
||||||
|
this.pubDateInput = this.episode.pubDate ? this.$formatJsDate(new Date(this.episode.pubDate), "yyyy-MM-dd'T'HH:mm") : null
|
||||||
|
},
|
||||||
|
getUpdatePayload() {
|
||||||
|
var updatePayload = {}
|
||||||
|
for (const key in this.newEpisode) {
|
||||||
|
if (this.newEpisode[key] != this.episode[key]) {
|
||||||
|
updatePayload[key] = this.newEpisode[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updatePayload
|
||||||
|
},
|
||||||
|
submit() {
|
||||||
|
const payload = this.getUpdatePayload()
|
||||||
|
if (!Object.keys(payload).length) {
|
||||||
|
return this.$toast.info('No updates were made')
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isProcessing = true
|
||||||
|
this.$axios
|
||||||
|
.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, payload)
|
||||||
|
.then(() => {
|
||||||
|
this.isProcessing = false
|
||||||
|
this.$toast.success('Podcast episode updated')
|
||||||
|
this.$emit('close')
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to update episode'
|
||||||
|
console.error('Failed update episode', error)
|
||||||
|
this.isProcessing = false
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<template>
|
||||||
|
<div style="min-height: 200px">
|
||||||
|
<template v-if="!podcastFeedUrl">
|
||||||
|
<div class="py-8">
|
||||||
|
<widgets-alert type="error">Podcast has no RSS feed url to use for matching</widgets-alert>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<form @submit.prevent="submitForm">
|
||||||
|
<div class="flex mb-2">
|
||||||
|
<ui-text-input-with-label v-model="episodeTitle" :disabled="isProcessing" label="Episode Title" class="pr-1" />
|
||||||
|
<ui-btn class="mt-5 ml-1" :loading="isProcessing" type="submit">Search</ui-btn>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div v-if="!isProcessing && searchedTitle && !episodesFound.length" class="w-full py-8">
|
||||||
|
<p class="text-center text-lg">No episode matches found</p>
|
||||||
|
</div>
|
||||||
|
<div v-for="(episode, index) in episodesFound" :key="index" class="w-full py-4 border-b border-white border-opacity-5 hover:bg-gray-300 hover:bg-opacity-10 cursor-pointer px-2" @click.stop="selectEpisode(episode)">
|
||||||
|
<p v-if="episode.episode" class="font-semibold text-gray-200">#{{ episode.episode }}</p>
|
||||||
|
<p class="break-words mb-1">{{ episode.title }}</p>
|
||||||
|
<p v-if="episode.subtitle" class="break-words mb-1 text-sm text-gray-300 episode-subtitle">{{ episode.subtitle }}</p>
|
||||||
|
<p class="text-xs text-gray-400">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
processing: Boolean,
|
||||||
|
libraryItem: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
},
|
||||||
|
episode: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
episodeTitle: '',
|
||||||
|
searchedTitle: '',
|
||||||
|
episodesFound: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
episode: {
|
||||||
|
immediate: true,
|
||||||
|
handler(newVal) {
|
||||||
|
if (newVal) this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isProcessing: {
|
||||||
|
get() {
|
||||||
|
return this.processing
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('update:processing', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
episodeId() {
|
||||||
|
return this.episode ? this.episode.id : null
|
||||||
|
},
|
||||||
|
media() {
|
||||||
|
return this.libraryItem ? this.libraryItem.media || {} : {}
|
||||||
|
},
|
||||||
|
mediaMetadata() {
|
||||||
|
return this.media.metadata || {}
|
||||||
|
},
|
||||||
|
podcastFeedUrl() {
|
||||||
|
return this.mediaMetadata.feedUrl
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getUpdatePayload(episodeData) {
|
||||||
|
var updatePayload = {}
|
||||||
|
for (const key in episodeData) {
|
||||||
|
if (key === 'enclosure') {
|
||||||
|
if (!this.episode.enclosure || JSON.stringify(this.episode.enclosure) !== JSON.stringify(episodeData.enclosure)) {
|
||||||
|
updatePayload[key] = {
|
||||||
|
...episodeData.enclosure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (episodeData[key] != this.episode[key]) {
|
||||||
|
updatePayload[key] = episodeData[key]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return updatePayload
|
||||||
|
},
|
||||||
|
selectEpisode(episode) {
|
||||||
|
const episodeData = {
|
||||||
|
title: episode.title || '',
|
||||||
|
subtitle: episode.subtitle || '',
|
||||||
|
description: episode.description || '',
|
||||||
|
enclosure: episode.enclosure || null,
|
||||||
|
episode: episode.episode || '',
|
||||||
|
episodeType: episode.episodeType || '',
|
||||||
|
season: episode.season || '',
|
||||||
|
pubDate: episode.pubDate || '',
|
||||||
|
publishedAt: episode.publishedAt
|
||||||
|
}
|
||||||
|
const updatePayload = this.getUpdatePayload(episodeData)
|
||||||
|
if (!Object.keys(updatePayload).length) {
|
||||||
|
return this.$toast.info('No updates are necessary')
|
||||||
|
}
|
||||||
|
console.log('Episode update payload', updatePayload)
|
||||||
|
|
||||||
|
this.isProcessing = true
|
||||||
|
this.$axios
|
||||||
|
.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, updatePayload)
|
||||||
|
.then(() => {
|
||||||
|
this.isProcessing = false
|
||||||
|
this.$toast.success('Podcast episode updated')
|
||||||
|
this.$emit('selectTab', 'details')
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to update episode'
|
||||||
|
console.error('Failed update episode', error)
|
||||||
|
this.isProcessing = false
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
submitForm() {
|
||||||
|
if (!this.episodeTitle || !this.episodeTitle.length) {
|
||||||
|
this.$toast.error('Must enter an episode title')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.searchedTitle = this.episodeTitle
|
||||||
|
this.isProcessing = true
|
||||||
|
this.$axios
|
||||||
|
.$get(`/api/podcasts/${this.libraryItem.id}/search-episode?title=${this.$encodeUriPath(this.episodeTitle)}`)
|
||||||
|
.then((results) => {
|
||||||
|
this.episodesFound = results.episodes.map((ep) => ep.episode)
|
||||||
|
console.log('Episodes found', this.episodesFound)
|
||||||
|
this.isProcessing = false
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed to search for episode', error)
|
||||||
|
var errMsg = error.response ? error.response.data || '' : ''
|
||||||
|
this.$toast.error(errMsg || 'Failed to search for episode')
|
||||||
|
this.isProcessing = false
|
||||||
|
})
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
this.searchedTitle = null
|
||||||
|
this.episodesFound = []
|
||||||
|
this.episodeTitle = this.episode ? this.episode.title || '' : ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full bg-primary bg-opacity-40">
|
<div class="w-full bg-primary bg-opacity-40">
|
||||||
<div class="w-full h-14 flex items-center px-4 bg-primary">
|
<div class="w-full h-14 flex items-center px-4 md:px-6 py-2 bg-primary">
|
||||||
<p>Collection List</p>
|
<p class="pr-4">Collection List</p>
|
||||||
<div class="w-6 h-6 bg-white bg-opacity-10 flex items-center justify-center rounded-full ml-2">
|
|
||||||
<p class="font-mono text-sm">{{ books.length }}</p>
|
<div class="w-6 h-6 md:w-7 md:h-7 bg-white bg-opacity-10 rounded-full flex items-center justify-center">
|
||||||
|
<span class="text-xs md:text-sm font-mono leading-none">{{ books.length }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<!-- <p v-if="totalDuration">{{ totalDurationPretty }}</p> -->
|
<p v-if="totalDuration" class="text-sm text-gray-200">{{ totalDurationPretty }}</p>
|
||||||
</div>
|
</div>
|
||||||
<draggable v-model="booksCopy" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="drag = true" @end="drag = false" @update="draggableUpdate">
|
<draggable v-model="booksCopy" v-bind="dragOptions" class="list-group" handle=".drag-handle" draggable=".item" tag="div" @start="drag = true" @end="drag = false" @update="draggableUpdate">
|
||||||
<transition-group type="transition" :name="!drag ? 'collection-book' : null">
|
<transition-group type="transition" :name="!drag ? 'collection-book' : null">
|
||||||
@@ -56,6 +57,16 @@ export default {
|
|||||||
},
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
|
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6
|
||||||
|
},
|
||||||
|
totalDuration() {
|
||||||
|
var _total = 0
|
||||||
|
this.books.forEach((book) => {
|
||||||
|
_total += book.media.duration
|
||||||
|
})
|
||||||
|
return _total
|
||||||
|
},
|
||||||
|
totalDurationPretty() {
|
||||||
|
return this.$elapsedPrettyExtended(this.totalDuration)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full px-2 py-2 overflow-hidden relative" @mouseover="mouseover" @mouseleave="mouseleave" :class="isHovering ? 'bg-white bg-opacity-5' : ''">
|
<div class="w-full px-1 md:px-2 py-2 overflow-hidden relative" @mouseover="mouseover" @mouseleave="mouseleave" :class="isHovering ? 'bg-white bg-opacity-5' : ''">
|
||||||
<div v-if="book" class="flex h-20">
|
<div v-if="book" class="flex h-16 md:h-20">
|
||||||
<div class="w-16 max-w-16 h-full">
|
<div class="w-10 min-w-10 md:w-16 md:max-w-16 h-full">
|
||||||
<div class="flex h-full items-center justify-center">
|
<div class="flex h-full items-center justify-center">
|
||||||
<span class="material-icons drag-handle text-xl">menu</span>
|
<span class="material-icons drag-handle text-lg md:text-xl">menu</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-full relative" :style="{ width: coverWidth + 'px' }">
|
<div class="h-full relative" :style="{ width: coverWidth + 'px', minWidth: coverWidth + 'px', maxWidth: coverWidth + 'px' }">
|
||||||
<covers-book-cover :library-item="book" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-book-cover :library-item="book" :width="coverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
<div class="absolute top-0 left-0 bg-black bg-opacity-50 flex items-center justify-center h-full w-full z-10" v-show="isHovering && showPlayBtn">
|
<div class="absolute top-0 left-0 bg-black bg-opacity-50 flex items-center justify-center h-full w-full z-10" v-show="isHovering && showPlayBtn">
|
||||||
<div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick">
|
<div class="w-8 h-8 bg-white bg-opacity-20 rounded-full flex items-center justify-center hover:bg-opacity-40 cursor-pointer" @click="playClick">
|
||||||
@@ -107,9 +107,12 @@ export default {
|
|||||||
userIsFinished() {
|
userIsFinished() {
|
||||||
return this.itemProgress ? !!this.itemProgress.isFinished : false
|
return this.itemProgress ? !!this.itemProgress.isFinished : false
|
||||||
},
|
},
|
||||||
|
coverSize() {
|
||||||
|
return this.$store.state.globals.isMobile ? 40 : 50
|
||||||
|
},
|
||||||
coverWidth() {
|
coverWidth() {
|
||||||
if (this.bookCoverAspectRatio === 1) return 50 * 1.6
|
if (this.bookCoverAspectRatio === 1) return this.coverSize * 1.6
|
||||||
return 50
|
return this.coverSize
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ export default {
|
|||||||
// this.currentSearch = this.textInput
|
// this.currentSearch = this.textInput
|
||||||
}, 100)
|
}, 100)
|
||||||
},
|
},
|
||||||
|
setFocus() {
|
||||||
|
if (this.$refs.input && this.editable) this.$refs.input.focus()
|
||||||
|
},
|
||||||
inputFocus() {
|
inputFocus() {
|
||||||
this.isFocused = true
|
this.isFocused = true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="currentLibrary" class="relative h-8 max-w-52" v-click-outside="clickOutsideObj">
|
<div v-if="currentLibrary" class="relative h-8 max-w-52 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 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">
|
||||||
<div class="flex items-center justify-center sm:justify-start">
|
<div class="flex items-center justify-center sm:justify-start">
|
||||||
<widgets-library-icon :icon="currentLibraryIcon" class="sm:mr-1.5" />
|
<widgets-library-icon :icon="currentLibraryIcon" class="sm:mr-1.5" />
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ export default {
|
|||||||
blur() {
|
blur() {
|
||||||
if (this.$refs.input) this.$refs.input.blur()
|
if (this.$refs.input) this.$refs.input.blur()
|
||||||
},
|
},
|
||||||
|
setFocus() {
|
||||||
|
if (this.$refs.input) this.$refs.input.focus()
|
||||||
|
},
|
||||||
mouseover() {
|
mouseover() {
|
||||||
this.isHovering = true
|
this.isHovering = true
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setFocus() {
|
||||||
|
if (this.$refs.input && this.$refs.input.setFocus) {
|
||||||
|
this.$refs.input.setFocus()
|
||||||
|
}
|
||||||
|
},
|
||||||
blur() {
|
blur() {
|
||||||
if (this.$refs.input && this.$refs.input.blur) {
|
if (this.$refs.input && this.$refs.input.blur) {
|
||||||
this.$refs.input.blur()
|
this.$refs.input.blur()
|
||||||
|
|||||||
@@ -211,6 +211,12 @@ export default {
|
|||||||
libraryItemUpdated(libraryItem) {
|
libraryItemUpdated(libraryItem) {
|
||||||
if (this.$store.state.selectedLibraryItem && this.$store.state.selectedLibraryItem.id === libraryItem.id) {
|
if (this.$store.state.selectedLibraryItem && this.$store.state.selectedLibraryItem.id === libraryItem.id) {
|
||||||
this.$store.commit('setSelectedLibraryItem', libraryItem)
|
this.$store.commit('setSelectedLibraryItem', libraryItem)
|
||||||
|
if (this.$store.state.globals.selectedEpisode && libraryItem.mediaType === 'podcast') {
|
||||||
|
const episode = libraryItem.media.episodes.find((ep) => ep.id === this.$store.state.globals.selectedEpisode.id)
|
||||||
|
if (episode) {
|
||||||
|
this.$store.commit('globals/setSelectedEpisode', episode)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.$eventBus.$emit(`${libraryItem.id}_updated`, libraryItem)
|
this.$eventBus.$emit(`${libraryItem.id}_updated`, libraryItem)
|
||||||
this.$store.commit('libraries/updateFilterDataWithItem', libraryItem)
|
this.$store.commit('libraries/updateFilterDataWithItem', libraryItem)
|
||||||
@@ -355,11 +361,11 @@ export default {
|
|||||||
download.status = this.$constants.DownloadStatus.EXPIRED
|
download.status = this.$constants.DownloadStatus.EXPIRED
|
||||||
this.$store.commit('downloads/addUpdateDownload', download)
|
this.$store.commit('downloads/addUpdateDownload', download)
|
||||||
},
|
},
|
||||||
showErrorToast(message) {
|
rssFeedOpen(data) {
|
||||||
this.$toast.error(message)
|
this.$store.commit('feeds/addFeed', data)
|
||||||
},
|
},
|
||||||
showSuccessToast(message) {
|
rssFeedClosed(data) {
|
||||||
this.$toast.success(message)
|
this.$store.commit('feeds/removeFeed', data)
|
||||||
},
|
},
|
||||||
backupApplied() {
|
backupApplied() {
|
||||||
// Force refresh
|
// Force refresh
|
||||||
@@ -431,9 +437,9 @@ export default {
|
|||||||
this.socket.on('abmerge_killed', this.abmergeKilled)
|
this.socket.on('abmerge_killed', this.abmergeKilled)
|
||||||
this.socket.on('abmerge_expired', this.abmergeExpired)
|
this.socket.on('abmerge_expired', this.abmergeExpired)
|
||||||
|
|
||||||
// Toast Listeners
|
// Feed Listeners
|
||||||
this.socket.on('show_error_toast', this.showErrorToast)
|
this.socket.on('rss_feed_open', this.rssFeedOpen)
|
||||||
this.socket.on('show_success_toast', this.showSuccessToast)
|
this.socket.on('rss_feed_closed', this.rssFeedClosed)
|
||||||
|
|
||||||
this.socket.on('backup_applied', this.backupApplied)
|
this.socket.on('backup_applied', this.backupApplied)
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.1.1",
|
"version": "2.1.2",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "2.1.1",
|
"version": "2.1.2",
|
||||||
"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.1.1",
|
"version": "2.1.2",
|
||||||
"description": "Self-hosted audiobook and podcast client",
|
"description": "Self-hosted audiobook and podcast client",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-2 py-6 md:py-0 md:px-10">
|
<div class="flex-grow px-2 py-6 md:py-0 md:px-10">
|
||||||
<div class="flex sm:items-end flex-col sm:flex-row">
|
<div class="flex items-end flex-row flex-wrap md:flex-nowrap">
|
||||||
<h1 class="text-2xl md:text-3xl font-sans">
|
<h1 class="text-2xl md:text-3xl font-sans w-full md:w-fit mb-4 md:mb-0">
|
||||||
{{ collectionName }}
|
{{ collectionName }}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
|
|||||||
@@ -71,6 +71,11 @@ export default {
|
|||||||
.configContent.page-library-stats {
|
.configContent.page-library-stats {
|
||||||
width: 1200px;
|
width: 1200px;
|
||||||
}
|
}
|
||||||
|
@media (max-width: 1550px) {
|
||||||
|
.configContent.page-library-stats {
|
||||||
|
margin-left: 176px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@media (max-width: 1240px) {
|
@media (max-width: 1240px) {
|
||||||
.configContent {
|
.configContent {
|
||||||
margin-left: 176px;
|
margin-left: 176px;
|
||||||
@@ -82,5 +87,8 @@ export default {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
.configContent.page-library-stats {
|
||||||
|
margin-left: 0px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -14,6 +14,12 @@
|
|||||||
</ui-tooltip>
|
</ui-tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- <div class="flex items-center py-2">
|
||||||
|
<ui-text-input v-model="cronExpression" :disabled="updatingServerSettings" class="w-32" @change="changedCronExpression" />
|
||||||
|
|
||||||
|
<p class="pl-4 text-lg">Cron expression</p>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
<div class="flex items-center py-2">
|
<div class="flex items-center py-2">
|
||||||
<ui-text-input type="number" v-model="backupsToKeep" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateBackupsSettings" />
|
<ui-text-input type="number" v-model="backupsToKeep" no-spinner :disabled="updatingServerSettings" :padding-x="1" text-center class="w-10" @change="updateBackupsSettings" />
|
||||||
|
|
||||||
@@ -41,6 +47,7 @@ export default {
|
|||||||
dailyBackups: true,
|
dailyBackups: true,
|
||||||
backupsToKeep: 2,
|
backupsToKeep: 2,
|
||||||
maxBackupSize: 1,
|
maxBackupSize: 1,
|
||||||
|
// cronExpression: '',
|
||||||
newServerSettings: {}
|
newServerSettings: {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -64,6 +71,18 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// changedCronExpression() {
|
||||||
|
// this.$axios
|
||||||
|
// .$post('/api/validate-cron', { expression: this.cronExpression })
|
||||||
|
// .then(() => {
|
||||||
|
// console.log('Cron is valid')
|
||||||
|
// })
|
||||||
|
// .catch((error) => {
|
||||||
|
// console.error('Cron validation failed', error)
|
||||||
|
// const msg = (error.response ? error.response.data : null) || 'Unknown cron validation error'
|
||||||
|
// this.$toast.error(msg)
|
||||||
|
// })
|
||||||
|
// },
|
||||||
updateBackupsSettings() {
|
updateBackupsSettings() {
|
||||||
if (isNaN(this.maxBackupSize) || this.maxBackupSize <= 0) {
|
if (isNaN(this.maxBackupSize) || this.maxBackupSize <= 0) {
|
||||||
this.$toast.error('Invalid maximum backup size')
|
this.$toast.error('Invalid maximum backup size')
|
||||||
@@ -99,6 +118,7 @@ export default {
|
|||||||
this.backupsToKeep = this.newServerSettings.backupsToKeep || 2
|
this.backupsToKeep = this.newServerSettings.backupsToKeep || 2
|
||||||
this.dailyBackups = !!this.newServerSettings.backupSchedule
|
this.dailyBackups = !!this.newServerSettings.backupSchedule
|
||||||
this.maxBackupSize = this.newServerSettings.maxBackupSize || 1
|
this.maxBackupSize = this.newServerSettings.maxBackupSize || 1
|
||||||
|
// this.cronExpression = '30 1 * * *'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<h1 class="text-xl">Stats for library {{ currentLibraryName }}</h1>
|
<h1 class="text-xl">Stats for library {{ currentLibraryName }}</h1>
|
||||||
<stats-preview-icons v-if="totalItems" :library-stats="libraryStats" />
|
<stats-preview-icons v-if="totalItems" :library-stats="libraryStats" />
|
||||||
|
|
||||||
<div class="flex md:flex-row flex-wrap justify-between flex-col mt-8">
|
<div class="flex lg:flex-row flex-wrap justify-between flex-col mt-8">
|
||||||
<div class="w-80 my-6 mx-auto">
|
<div class="w-80 my-6 mx-auto">
|
||||||
<h1 class="text-2xl mb-4 font-book">Top 5 Genres</h1>
|
<h1 class="text-2xl mb-4 font-book">Top 5 Genres</h1>
|
||||||
<p v-if="!top5Genres.length">No Genres</p>
|
<p v-if="!top5Genres.length">No Genres</p>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<covers-book-cover :library-item="libraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-book-cover :library-item="libraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||||
|
|
||||||
<!-- Item Progress Bar -->
|
<!-- Item Progress Bar -->
|
||||||
<div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 208 * progressPercent + 'px' }"></div>
|
<div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1.5 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 208 * progressPercent + 'px' }"></div>
|
||||||
|
|
||||||
<!-- Item Cover Overlay -->
|
<!-- Item Cover Overlay -->
|
||||||
<div class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-30 opacity-0 hover:opacity-100 transition-opacity" @mousedown.prevent @mouseup.prevent>
|
<div class="absolute top-0 left-0 w-full h-full z-10 bg-black bg-opacity-30 opacity-0 hover:opacity-100 transition-opacity" @mousedown.prevent @mouseup.prevent>
|
||||||
|
|||||||
@@ -124,9 +124,10 @@ export default {
|
|||||||
|
|
||||||
location.reload()
|
location.reload()
|
||||||
},
|
},
|
||||||
setUser({ user, userDefaultLibraryId, serverSettings, Source }) {
|
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)
|
||||||
|
|
||||||
if (serverSettings.chromecastEnabled) {
|
if (serverSettings.chromecastEnabled) {
|
||||||
console.log('Chromecast enabled import script')
|
console.log('Chromecast enabled import script')
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
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 || []
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.1.1",
|
"version": "2.1.2",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.1.1",
|
"version": "2.1.2",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.26.1",
|
"axios": "^0.26.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "2.1.1",
|
"version": "2.1.2",
|
||||||
"description": "Self-hosted audiobook and podcast server",
|
"description": "Self-hosted audiobook and podcast server",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
+5
-4
@@ -114,16 +114,17 @@ class Auth {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getUserLoginResponsePayload(user) {
|
getUserLoginResponsePayload(user, feeds) {
|
||||||
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) {
|
async login(req, res, feeds) {
|
||||||
var username = (req.body.username || '').toLowerCase()
|
var username = (req.body.username || '').toLowerCase()
|
||||||
var password = req.body.password || ''
|
var password = req.body.password || ''
|
||||||
|
|
||||||
@@ -142,14 +143,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))
|
return res.json(this.getUserLoginResponsePayload(user, feeds))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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))
|
res.json(this.getUserLoginResponsePayload(user, feeds))
|
||||||
} else {
|
} else {
|
||||||
Logger.debug(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit}`)
|
Logger.debug(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit}`)
|
||||||
if (req.rateLimit.remaining <= 2) {
|
if (req.rateLimit.remaining <= 2) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const Author = require('./objects/entities/Author')
|
|||||||
const Series = require('./objects/entities/Series')
|
const Series = require('./objects/entities/Series')
|
||||||
const ServerSettings = require('./objects/settings/ServerSettings')
|
const ServerSettings = require('./objects/settings/ServerSettings')
|
||||||
const PlaybackSession = require('./objects/PlaybackSession')
|
const PlaybackSession = require('./objects/PlaybackSession')
|
||||||
|
const Feed = require('./objects/Feed')
|
||||||
|
|
||||||
class Db {
|
class Db {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
+1
-1
@@ -230,7 +230,7 @@ class Server {
|
|||||||
]
|
]
|
||||||
dyanimicRoutes.forEach((route) => app.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))
|
dyanimicRoutes.forEach((route) => app.get(route, (req, res) => res.sendFile(Path.join(distPath, 'index.html'))))
|
||||||
|
|
||||||
app.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res))
|
app.post('/login', this.getLoginRateLimiter(), (req, res) => this.auth.login(req, res, this.rssFeedManager.feedsArray))
|
||||||
app.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this))
|
app.post('/logout', this.authMiddleware.bind(this), this.logout.bind(this))
|
||||||
app.post('/init', (req, res) => {
|
app.post('/init', (req, res) => {
|
||||||
if (this.db.hasRootUser) {
|
if (this.db.hasRootUser) {
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ class LibraryController {
|
|||||||
// If filtering by series, will include seriesName and seriesSequence on media metadata
|
// If filtering by series, will include seriesName and seriesSequence on media metadata
|
||||||
filterSeries = (payload.mediaType == 'book' && payload.filterBy.startsWith('series.')) ? libraryHelpers.decode(payload.filterBy.replace('series.', '')) : null
|
filterSeries = (payload.mediaType == 'book' && payload.filterBy.startsWith('series.')) ? libraryHelpers.decode(payload.filterBy.replace('series.', '')) : null
|
||||||
|
|
||||||
libraryItems = libraryHelpers.getFilteredLibraryItems(libraryItems, payload.filterBy, req.user)
|
libraryItems = libraryHelpers.getFilteredLibraryItems(libraryItems, payload.filterBy, req.user, this.rssFeedManager.feedsArray)
|
||||||
payload.total = libraryItems.length
|
payload.total = libraryItems.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const Path = require('path')
|
|||||||
const fs = require('../libs/fsExtra')
|
const fs = require('../libs/fsExtra')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
const filePerms = require('../utils/filePerms')
|
const filePerms = require('../utils/filePerms')
|
||||||
|
const patternValidation = require('../libs/nodeCron/pattern-validation')
|
||||||
const { isObject } = require('../utils/index')
|
const { isObject } = require('../utils/index')
|
||||||
|
|
||||||
//
|
//
|
||||||
@@ -239,12 +239,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 = {
|
const userResponse = this.auth.getUserLoginResponsePayload(req.user, this.rssFeedManager.feedsArray)
|
||||||
user: req.user,
|
|
||||||
userDefaultLibraryId: req.user.getDefaultLibraryId(this.db.libraries),
|
|
||||||
serverSettings: this.db.serverSettings.toJSONForBrowser(),
|
|
||||||
Source: global.Source
|
|
||||||
}
|
|
||||||
res.json(userResponse)
|
res.json(userResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,5 +258,20 @@ class MiscController {
|
|||||||
})
|
})
|
||||||
res.json(tags)
|
res.json(tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateCronExpression(req, res) {
|
||||||
|
const expression = req.body.expression
|
||||||
|
if (!expression) {
|
||||||
|
return res.sendStatus(400)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
patternValidation(expression)
|
||||||
|
res.sendStatus(200)
|
||||||
|
} catch (error) {
|
||||||
|
Logger.warn(`[MiscController] Invalid cron expression ${expression}`, error.message)
|
||||||
|
res.status(400).send(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
module.exports = new MiscController()
|
module.exports = new MiscController()
|
||||||
@@ -164,6 +164,25 @@ class PodcastController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findEpisode(req, res) {
|
||||||
|
const rssFeedUrl = req.libraryItem.media.metadata.feedUrl
|
||||||
|
if (!rssFeedUrl) {
|
||||||
|
Logger.error(`[PodcastController] findEpisode: Podcast has no feed url`)
|
||||||
|
return res.status(500).send('Podcast does not have an RSS feed URL')
|
||||||
|
}
|
||||||
|
|
||||||
|
var searchTitle = req.query.title
|
||||||
|
if (!searchTitle) {
|
||||||
|
return res.sendStatus(500)
|
||||||
|
}
|
||||||
|
searchTitle = searchTitle.toLowerCase().trim()
|
||||||
|
|
||||||
|
const episodes = await this.podcastManager.findEpisode(rssFeedUrl, searchTitle)
|
||||||
|
res.json({
|
||||||
|
episodes: episodes || []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
@@ -185,7 +204,7 @@ class PodcastController {
|
|||||||
|
|
||||||
var episodeId = req.params.episodeId
|
var episodeId = req.params.episodeId
|
||||||
if (!libraryItem.media.checkHasEpisode(episodeId)) {
|
if (!libraryItem.media.checkHasEpisode(episodeId)) {
|
||||||
return res.status(500).send('Episode not found')
|
return res.status(404).send('Episode not found')
|
||||||
}
|
}
|
||||||
|
|
||||||
var wasUpdated = libraryItem.media.updateEpisode(episodeId, req.body)
|
var wasUpdated = libraryItem.media.updateEpisode(episodeId, req.body)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const { parsePodcastRssFeedXml } = require('../utils/podcastUtils')
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
const { downloadFile } = require('../utils/fileUtils')
|
const { downloadFile } = require('../utils/fileUtils')
|
||||||
|
const { levenshteinDistance } = require('../utils/index')
|
||||||
const opmlParser = require('../utils/parsers/parseOPML')
|
const opmlParser = require('../utils/parsers/parseOPML')
|
||||||
const prober = require('../utils/prober')
|
const prober = require('../utils/prober')
|
||||||
const LibraryFile = require('../objects/files/LibraryFile')
|
const LibraryFile = require('../objects/files/LibraryFile')
|
||||||
@@ -23,7 +24,8 @@ class PodcastManager {
|
|||||||
this.currentDownload = null
|
this.currentDownload = null
|
||||||
|
|
||||||
this.episodeScheduleTask = null
|
this.episodeScheduleTask = null
|
||||||
this.failedCheckMap = {}
|
this.failedCheckMap = {},
|
||||||
|
this.MaxFailedEpisodeChecks = 24
|
||||||
}
|
}
|
||||||
|
|
||||||
get serverSettings() {
|
get serverSettings() {
|
||||||
@@ -199,8 +201,8 @@ class PodcastManager {
|
|||||||
// Allow up to 3 failed attempts before disabling auto download
|
// Allow up to 3 failed attempts before disabling auto download
|
||||||
if (!this.failedCheckMap[libraryItem.id]) this.failedCheckMap[libraryItem.id] = 0
|
if (!this.failedCheckMap[libraryItem.id]) this.failedCheckMap[libraryItem.id] = 0
|
||||||
this.failedCheckMap[libraryItem.id]++
|
this.failedCheckMap[libraryItem.id]++
|
||||||
if (this.failedCheckMap[libraryItem.id] > 2) {
|
if (this.failedCheckMap[libraryItem.id] >= this.MaxFailedEpisodeChecks) {
|
||||||
Logger.error(`[PodcastManager] checkForNewEpisodes 3 failed attempts at checking episodes for "${libraryItem.media.metadata.title}" - disabling auto download`)
|
Logger.error(`[PodcastManager] checkForNewEpisodes ${this.failedCheckMap[libraryItem.id]} failed attempts at checking episodes for "${libraryItem.media.metadata.title}" - disabling auto download`)
|
||||||
libraryItem.media.autoDownloadEpisodes = false
|
libraryItem.media.autoDownloadEpisodes = false
|
||||||
delete this.failedCheckMap[libraryItem.id]
|
delete this.failedCheckMap[libraryItem.id]
|
||||||
} else {
|
} else {
|
||||||
@@ -259,6 +261,37 @@ class PodcastManager {
|
|||||||
return newEpisodes
|
return newEpisodes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async findEpisode(rssFeedUrl, searchTitle) {
|
||||||
|
const feed = await this.getPodcastFeed(rssFeedUrl).catch(() => {
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
if (!feed || !feed.episodes) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = []
|
||||||
|
feed.episodes.forEach(ep => {
|
||||||
|
if (!ep.title) return
|
||||||
|
|
||||||
|
const epTitle = ep.title.toLowerCase().trim()
|
||||||
|
if (epTitle === searchTitle) {
|
||||||
|
matches.push({
|
||||||
|
episode: ep,
|
||||||
|
levenshtein: 0
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const levenshtein = levenshteinDistance(searchTitle, epTitle, true)
|
||||||
|
if (levenshtein <= 6 && epTitle.length > levenshtein) {
|
||||||
|
matches.push({
|
||||||
|
episode: ep,
|
||||||
|
levenshtein
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return matches.sort((a, b) => a.levenshtein - b.levenshtein)
|
||||||
|
}
|
||||||
|
|
||||||
getPodcastFeed(feedUrl, excludeEpisodeMetadata = false) {
|
getPodcastFeed(feedUrl, excludeEpisodeMetadata = false) {
|
||||||
Logger.debug(`[PodcastManager] getPodcastFeed for "${feedUrl}"`)
|
Logger.debug(`[PodcastManager] getPodcastFeed for "${feedUrl}"`)
|
||||||
return axios.get(feedUrl, { timeout: 5000 }).then(async (data) => {
|
return axios.get(feedUrl, { timeout: 5000 }).then(async (data) => {
|
||||||
@@ -273,7 +306,7 @@ class PodcastManager {
|
|||||||
}
|
}
|
||||||
return payload.podcast
|
return payload.podcast
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error('Failed', error)
|
Logger.error('[PodcastManager] getPodcastFeed Error', error)
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ class RssFeedManager {
|
|||||||
this.feeds = {}
|
this.feeds = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get feedsArray() {
|
||||||
|
return Object.values(this.feeds)
|
||||||
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
var feedObjects = await this.db.getAllEntities('feed')
|
var feedObjects = await this.db.getAllEntities('feed')
|
||||||
if (feedObjects && feedObjects.length) {
|
if (feedObjects && feedObjects.length) {
|
||||||
@@ -91,7 +95,7 @@ class RssFeedManager {
|
|||||||
|
|
||||||
Logger.debug(`[RssFeedManager] Opened RSS feed ${feed.feedUrl}`)
|
Logger.debug(`[RssFeedManager] Opened RSS feed ${feed.feedUrl}`)
|
||||||
await this.db.insertEntity('feed', feed)
|
await this.db.insertEntity('feed', feed)
|
||||||
this.emitter('rss_feed_open', { entityType: feed.entityType, entityId: feed.entityId, feedUrl: feed.feedUrl })
|
this.emitter('rss_feed_open', { id: feed.id, entityType: feed.entityType, entityId: feed.entityId, feedUrl: feed.feedUrl })
|
||||||
return feed
|
return feed
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +109,7 @@ class RssFeedManager {
|
|||||||
if (!this.feeds[id]) return
|
if (!this.feeds[id]) return
|
||||||
var feed = this.feeds[id]
|
var feed = this.feeds[id]
|
||||||
await this.db.removeEntity('feed', id)
|
await this.db.removeEntity('feed', id)
|
||||||
this.emitter('rss_feed_closed', { entityType: feed.entityType, entityId: feed.entityId, feedUrl: feed.feedUrl })
|
this.emitter('rss_feed_closed', { id: feed.id, entityType: feed.entityType, entityId: feed.entityId, feedUrl: feed.feedUrl })
|
||||||
delete this.feeds[id]
|
delete this.feeds[id]
|
||||||
Logger.info(`[RssFeedManager] Closed RSS feed "${feed.feedUrl}"`)
|
Logger.info(`[RssFeedManager] Closed RSS feed "${feed.feedUrl}"`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,8 @@ class FeedEpisode {
|
|||||||
|
|
||||||
setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta) {
|
setFromAudiobookTrack(libraryItem, serverAddress, slug, audioTrack, meta) {
|
||||||
// Example: <pubDate>Fri, 04 Feb 2015 00:00:00 GMT</pubDate>
|
// Example: <pubDate>Fri, 04 Feb 2015 00:00:00 GMT</pubDate>
|
||||||
const audiobookPubDate = date.format(new Date(libraryItem.addedAt), 'ddd, DD MMM YYYY HH:mm:ss [GMT]')
|
const timeOffset = isNaN(audioTrack.index) ? 0 : (Number(audioTrack.index) * 1000) // Offset pubdate to ensure correct order
|
||||||
|
const audiobookPubDate = date.format(new Date(libraryItem.addedAt + timeOffset), 'ddd, DD MMM YYYY HH:mm:ss [GMT]')
|
||||||
|
|
||||||
const contentUrl = `/feed/${slug}/item/${audioTrack.index}/${audioTrack.metadata.filename}`
|
const contentUrl = `/feed/${slug}/item/${audioTrack.index}/${audioTrack.metadata.filename}`
|
||||||
const media = libraryItem.media
|
const media = libraryItem.media
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
const Path = require('path')
|
||||||
const { getId } = require('../../utils/index')
|
const { getId } = require('../../utils/index')
|
||||||
const AudioFile = require('../files/AudioFile')
|
const AudioFile = require('../files/AudioFile')
|
||||||
const AudioTrack = require('../files/AudioTrack')
|
const AudioTrack = require('../files/AudioTrack')
|
||||||
@@ -126,7 +127,7 @@ class PodcastEpisode {
|
|||||||
setDataFromAudioFile(audioFile, index) {
|
setDataFromAudioFile(audioFile, index) {
|
||||||
this.id = getId('ep')
|
this.id = getId('ep')
|
||||||
this.audioFile = audioFile
|
this.audioFile = audioFile
|
||||||
this.title = audioFile.metadata.filename
|
this.title = Path.basename(audioFile.metadata.filename, Path.extname(audioFile.metadata.filename))
|
||||||
this.index = index
|
this.index = index
|
||||||
this.addedAt = Date.now()
|
this.addedAt = Date.now()
|
||||||
this.updatedAt = Date.now()
|
this.updatedAt = Date.now()
|
||||||
|
|||||||
@@ -402,9 +402,12 @@ class Book {
|
|||||||
var includedAudioFiles = this.audioFiles.filter(af => !af.exclude)
|
var includedAudioFiles = this.audioFiles.filter(af => !af.exclude)
|
||||||
|
|
||||||
// If overdrive media markers are present and preferred, use those instead
|
// If overdrive media markers are present and preferred, use those instead
|
||||||
if (preferOverdriveMediaMarker && overdriveMediaMarkersExist(includedAudioFiles)) {
|
if (preferOverdriveMediaMarker) {
|
||||||
Logger.info('[Book] Overdrive Media Markers and preference found! Using these for chapter definitions')
|
var overdriveChapters = parseOverdriveMediaMarkersAsChapters(includedAudioFiles)
|
||||||
return this.chapters = parseOverdriveMediaMarkersAsChapters(includedAudioFiles)
|
if (overdriveChapters) {
|
||||||
|
Logger.info('[Book] Overdrive Media Markers and preference found! Using these for chapter definitions')
|
||||||
|
return this.chapters = overdriveChapters
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (includedAudioFiles.length === 1) {
|
if (includedAudioFiles.length === 1) {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class Audible {
|
|||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
cleanResult(item) {
|
cleanResult(item) {
|
||||||
var { title, subtitle, asin, authors, narrators, publisherName, summary, releaseDate, image, genres, seriesPrimary, seriesSecondary, language } = item
|
var { title, subtitle, asin, authors, narrators, publisherName, summary, releaseDate, image, genres, seriesPrimary, seriesSecondary, language, runtimeLengthMin } = item
|
||||||
|
|
||||||
var series = []
|
var series = []
|
||||||
if (seriesPrimary) series.push(seriesPrimary)
|
if (seriesPrimary) series.push(seriesPrimary)
|
||||||
@@ -28,7 +28,8 @@ class Audible {
|
|||||||
genres: genresFiltered.length > 0 ? genresFiltered.map(({ name }) => name).join(', ') : null,
|
genres: genresFiltered.length > 0 ? genresFiltered.map(({ name }) => name).join(', ') : null,
|
||||||
tags: tagsFiltered.length > 0 ? tagsFiltered.map(({ name }) => name).join(', ') : null,
|
tags: tagsFiltered.length > 0 ? tagsFiltered.map(({ name }) => name).join(', ') : null,
|
||||||
series: series != [] ? series.map(({ name, position }) => ({ series: name, volumeNumber: position })) : null,
|
series: series != [] ? series.map(({ name, position }) => ({ series: name, volumeNumber: position })) : null,
|
||||||
language: language ? language.charAt(0).toUpperCase() + language.slice(1) : null
|
language: language ? language.charAt(0).toUpperCase() + language.slice(1) : null,
|
||||||
|
duration: runtimeLengthMin && !isNaN(runtimeLengthMin) ? Number(runtimeLengthMin) : 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +78,6 @@ class Audible {
|
|||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return items ? items.map(item => this.cleanResult(item)) : []
|
return items ? items.map(item => this.cleanResult(item)) : []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ class ApiRouter {
|
|||||||
this.router.get('/podcasts/:id/checknew', PodcastController.middleware.bind(this), PodcastController.checkNewEpisodes.bind(this))
|
this.router.get('/podcasts/:id/checknew', PodcastController.middleware.bind(this), PodcastController.checkNewEpisodes.bind(this))
|
||||||
this.router.get('/podcasts/:id/downloads', PodcastController.middleware.bind(this), PodcastController.getEpisodeDownloads.bind(this))
|
this.router.get('/podcasts/:id/downloads', PodcastController.middleware.bind(this), PodcastController.getEpisodeDownloads.bind(this))
|
||||||
this.router.get('/podcasts/:id/clear-queue', PodcastController.middleware.bind(this), PodcastController.clearEpisodeDownloadQueue.bind(this))
|
this.router.get('/podcasts/:id/clear-queue', PodcastController.middleware.bind(this), PodcastController.clearEpisodeDownloadQueue.bind(this))
|
||||||
|
this.router.get('/podcasts/:id/search-episode', PodcastController.middleware.bind(this), PodcastController.findEpisode.bind(this))
|
||||||
this.router.post('/podcasts/:id/download-episodes', PodcastController.middleware.bind(this), PodcastController.downloadEpisodes.bind(this))
|
this.router.post('/podcasts/:id/download-episodes', PodcastController.middleware.bind(this), PodcastController.downloadEpisodes.bind(this))
|
||||||
this.router.patch('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.updateEpisode.bind(this))
|
this.router.patch('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.updateEpisode.bind(this))
|
||||||
this.router.delete('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.removeEpisode.bind(this))
|
this.router.delete('/podcasts/:id/episode/:episodeId', PodcastController.middleware.bind(this), PodcastController.removeEpisode.bind(this))
|
||||||
@@ -210,6 +211,7 @@ class ApiRouter {
|
|||||||
this.router.get('/search/authors', MiscController.findAuthor.bind(this))
|
this.router.get('/search/authors', MiscController.findAuthor.bind(this))
|
||||||
this.router.get('/search/chapters', MiscController.findChapters.bind(this))
|
this.router.get('/search/chapters', MiscController.findChapters.bind(this))
|
||||||
this.router.get('/tags', MiscController.getAllTags.bind(this))
|
this.router.get('/tags', MiscController.getAllTags.bind(this))
|
||||||
|
this.router.post('/validate-cron', MiscController.validateCronExpression.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
async getDirectories(dir, relpath, excludedDirs, level = 0) {
|
async getDirectories(dir, relpath, excludedDirs, level = 0) {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ module.exports = {
|
|||||||
return Buffer.from(decodeURIComponent(text), 'base64').toString()
|
return Buffer.from(decodeURIComponent(text), 'base64').toString()
|
||||||
},
|
},
|
||||||
|
|
||||||
getFilteredLibraryItems(libraryItems, filterBy, user) {
|
getFilteredLibraryItems(libraryItems, filterBy, user, feedsArray) {
|
||||||
var filtered = libraryItems
|
var filtered = libraryItems
|
||||||
|
|
||||||
var searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'missing', 'languages']
|
var searchGroups = ['genres', 'tags', 'series', 'authors', 'progress', 'narrators', 'missing', 'languages']
|
||||||
@@ -61,6 +61,8 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
} else if (filterBy === 'issues') {
|
} else if (filterBy === 'issues') {
|
||||||
filtered = filtered.filter(li => li.hasIssues)
|
filtered = filtered.filter(li => li.hasIssues)
|
||||||
|
} else if (filterBy === 'feed-open') {
|
||||||
|
filtered = filtered.filter(li => feedsArray.some(feed => feed.entityId === li.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
return filtered
|
return filtered
|
||||||
@@ -450,7 +452,7 @@ module.exports = {
|
|||||||
if (bookInProgress) { // Update if this series is in progress
|
if (bookInProgress) { // Update if this series is in progress
|
||||||
seriesMap[librarySeries.id].inProgress = true
|
seriesMap[librarySeries.id].inProgress = true
|
||||||
|
|
||||||
if (seriesMap[librarySeries.id].bookInProgressLastUpdate > mediaProgress.lastUpdate) {
|
if (seriesMap[librarySeries.id].bookInProgressLastUpdate < mediaProgress.lastUpdate) {
|
||||||
seriesMap[librarySeries.id].bookInProgressLastUpdate = mediaProgress.lastUpdate
|
seriesMap[librarySeries.id].bookInProgressLastUpdate = mediaProgress.lastUpdate
|
||||||
}
|
}
|
||||||
} else if (!seriesMap[librarySeries.id].firstBookUnread) {
|
} else if (!seriesMap[librarySeries.id].firstBookUnread) {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ const Logger = require('../../Logger')
|
|||||||
// given a list of audio files, extract all of the Overdrive Media Markers metaTags, and return an array of them as XML
|
// given a list of audio files, extract all of the Overdrive Media Markers metaTags, and return an array of them as XML
|
||||||
function extractOverdriveMediaMarkers(includedAudioFiles) {
|
function extractOverdriveMediaMarkers(includedAudioFiles) {
|
||||||
Logger.debug('[parseOverdriveMediaMarkers] Extracting overdrive media markers')
|
Logger.debug('[parseOverdriveMediaMarkers] Extracting overdrive media markers')
|
||||||
var markers = includedAudioFiles.map((af) => af.metaTags.tagOverdriveMediaMarker).filter(notUndefined => notUndefined !== undefined).filter(elem => { return elem !== null }) || []
|
var markers = includedAudioFiles.map((af) => af.metaTags.tagOverdriveMediaMarker).filter(af => af) || []
|
||||||
|
|
||||||
return markers
|
return markers
|
||||||
}
|
}
|
||||||
@@ -29,11 +29,11 @@ function cleanOverdriveMediaMarkers(overdriveMediaMarkers) {
|
|||||||
]
|
]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
var parseString = require('xml2js').parseString; // function to convert xml to JSON
|
var parseString = require('xml2js').parseString // function to convert xml to JSON
|
||||||
var parsedOverdriveMediaMarkers = []
|
var parsedOverdriveMediaMarkers = []
|
||||||
|
|
||||||
overdriveMediaMarkers.forEach(function (item, index) {
|
overdriveMediaMarkers.forEach((item, index) => {
|
||||||
var parsed_result
|
var parsed_result = null
|
||||||
parseString(item, function (err, result) {
|
parseString(item, function (err, result) {
|
||||||
/*
|
/*
|
||||||
result.Markers.Marker is the result of parsing the XML for the MediaMarker tags for the MP3 file (Part##.mp3)
|
result.Markers.Marker is the result of parsing the XML for the MediaMarker tags for the MP3 file (Part##.mp3)
|
||||||
@@ -54,10 +54,14 @@ function cleanOverdriveMediaMarkers(overdriveMediaMarkers) {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// The values for Name and Time in results.Markers.Marker are returned as Arrays from parseString and should be strings
|
// The values for Name and Time in results.Markers.Marker are returned as Arrays from parseString and should be strings
|
||||||
parsed_result = objectValuesArrayToString(result.Markers.Marker)
|
if (result && result.Markers && result.Markers.Marker) {
|
||||||
|
parsed_result = objectValuesArrayToString(result.Markers.Marker)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
parsedOverdriveMediaMarkers.push(parsed_result)
|
if (parsed_result) {
|
||||||
|
parsedOverdriveMediaMarkers.push(parsed_result)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return removeExtraChapters(parsedOverdriveMediaMarkers)
|
return removeExtraChapters(parsedOverdriveMediaMarkers)
|
||||||
@@ -114,6 +118,7 @@ function generateParsedChapters(includedAudioFiles, cleanedOverdriveMediaMarkers
|
|||||||
|
|
||||||
// cleanedOverdriveMediaMarkers is an array of array of objects, where the inner array matches to the included audio files tracks
|
// cleanedOverdriveMediaMarkers is an array of array of objects, where the inner array matches to the included audio files tracks
|
||||||
// this allows us to leverage the individual track durations when calculating the start times of chapters in tracks after the first (using length)
|
// this allows us to leverage the individual track durations when calculating the start times of chapters in tracks after the first (using length)
|
||||||
|
// TODO: can we guarantee the inner array matches the included audio files?
|
||||||
includedAudioFiles.forEach((track, track_index) => {
|
includedAudioFiles.forEach((track, track_index) => {
|
||||||
cleanedOverdriveMediaMarkers[track_index].forEach((chapter) => {
|
cleanedOverdriveMediaMarkers[track_index].forEach((chapter) => {
|
||||||
time = chapter.Time.split(":")
|
time = chapter.Time.split(":")
|
||||||
@@ -141,7 +146,13 @@ module.exports.parseOverdriveMediaMarkersAsChapters = (includedAudioFiles) => {
|
|||||||
Logger.info('[parseOverdriveMediaMarkers] Parsing of Overdrive Media Markers started')
|
Logger.info('[parseOverdriveMediaMarkers] Parsing of Overdrive Media Markers started')
|
||||||
|
|
||||||
var overdriveMediaMarkers = extractOverdriveMediaMarkers(includedAudioFiles)
|
var overdriveMediaMarkers = extractOverdriveMediaMarkers(includedAudioFiles)
|
||||||
|
if (!overdriveMediaMarkers.length) return null
|
||||||
|
|
||||||
var cleanedOverdriveMediaMarkers = cleanOverdriveMediaMarkers(overdriveMediaMarkers)
|
var cleanedOverdriveMediaMarkers = cleanOverdriveMediaMarkers(overdriveMediaMarkers)
|
||||||
|
// TODO: generateParsedChapters requires overdrive media markers and included audio files length to be the same
|
||||||
|
// so if not equal then we must exit
|
||||||
|
if (cleanedOverdriveMediaMarkers.length !== includedAudioFiles.length) return null
|
||||||
|
|
||||||
var parsedChapters = generateParsedChapters(includedAudioFiles, cleanedOverdriveMediaMarkers)
|
var parsedChapters = generateParsedChapters(includedAudioFiles, cleanedOverdriveMediaMarkers)
|
||||||
|
|
||||||
return parsedChapters
|
return parsedChapters
|
||||||
|
|||||||
Reference in New Issue
Block a user