mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-01-02 04:28:01 -05:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54389e3c25 | ||
|
|
bf0da1c6ec | ||
|
|
591a866f8c | ||
|
|
fc8473ed84 | ||
|
|
b19442e440 | ||
|
|
7a51e0693d | ||
|
|
21785c8e72 | ||
|
|
bdf6ccbd2d | ||
|
|
ceb163570f | ||
|
|
049ae73d74 | ||
|
|
729fdd5c9f | ||
|
|
4dac8ac16c | ||
|
|
220bbc3d2d | ||
|
|
c2a4b32192 | ||
|
|
09d0d47549 | ||
|
|
4185807da4 | ||
|
|
620bf7990f |
@@ -54,10 +54,16 @@
|
||||
<ui-icon-btn :disabled="processingBatch" icon="collections_bookmark" @click="batchAddToCollectionClick" class="mx-1.5" />
|
||||
</ui-tooltip>
|
||||
<template v-if="userCanUpdate && numLibraryItemsSelected < 50">
|
||||
<ui-icon-btn v-show="!processingBatchDelete" icon="edit" bg-color="warning" class="mx-1.5" @click="batchEditClick" />
|
||||
<ui-tooltip text="Edit" direction="bottom">
|
||||
<ui-icon-btn v-show="!processingBatchDelete" icon="edit" bg-color="warning" class="mx-1.5" @click="batchEditClick" />
|
||||
</ui-tooltip>
|
||||
</template>
|
||||
<ui-icon-btn v-show="userCanDelete" :disabled="processingBatchDelete" icon="delete" bg-color="error" class="mx-1.5" @click="batchDeleteClick" />
|
||||
<span class="material-icons text-4xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatchDelete ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span>
|
||||
<ui-tooltip v-if="userCanDelete" text="Delete" direction="bottom">
|
||||
<ui-icon-btn :disabled="processingBatchDelete" icon="delete" bg-color="error" class="mx-1.5" @click="batchDeleteClick" />
|
||||
</ui-tooltip>
|
||||
<ui-tooltip text="Deselect All" direction="bottom">
|
||||
<span class="material-icons text-4xl px-4 hover:text-gray-100 cursor-pointer" :class="processingBatchDelete ? 'text-gray-400' : ''" @click="cancelSelectionMode">close</span>
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -229,4 +235,4 @@ export default {
|
||||
#appbar {
|
||||
box-shadow: 0px 5px 5px #11111155;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -128,8 +128,7 @@ export default {
|
||||
type: 'series',
|
||||
entities: this.results.series.map((seriesObj) => {
|
||||
return {
|
||||
name: seriesObj.series.name,
|
||||
series: seriesObj.series,
|
||||
...seriesObj.series,
|
||||
books: seriesObj.books,
|
||||
type: 'series'
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
<div v-show="isAuthorsPage" class="h-full w-0.5 bg-yellow-400 absolute top-0 left-0" />
|
||||
</nuxt-link>
|
||||
|
||||
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="w-full h-20 flex flex-col items-center justify-center text-white text-opacity-80 border-b border-primary border-opacity-70 hover:bg-primary cursor-pointer relative" :class="isPodcastSearchPage ? 'bg-primary bg-opacity-80' : 'bg-bg bg-opacity-60'">
|
||||
<icons-podcast-svg class="w-6 h-6" />
|
||||
|
||||
<p class="font-book pt-1.5" style="font-size: 0.9rem">Search</p>
|
||||
@@ -82,6 +82,9 @@ export default {
|
||||
showExperimentalFeatures() {
|
||||
return this.$store.state.showExperimentalFeatures
|
||||
},
|
||||
userIsAdminOrUp() {
|
||||
return this.$store.getters['user/getIsAdminOrUp']
|
||||
},
|
||||
paramId() {
|
||||
return this.$route.params ? this.$route.params.id || '' : ''
|
||||
},
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
</ui-tooltip>
|
||||
|
||||
<!-- Series sequence -->
|
||||
<div v-if="seriesSequence && showSequence && !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>
|
||||
</div>
|
||||
|
||||
@@ -110,7 +110,6 @@ export default {
|
||||
default: 192
|
||||
},
|
||||
bookCoverAspectRatio: Number,
|
||||
showSequence: Boolean,
|
||||
bookshelfView: Number,
|
||||
bookMount: {
|
||||
// Book can be passed as prop or set with setEntity()
|
||||
@@ -176,7 +175,7 @@ export default {
|
||||
return this._libraryItem.id
|
||||
},
|
||||
series() {
|
||||
// Only included when filtering by series or collapse series
|
||||
// Only included when filtering by series or collapse series or Continue Series shelf on home page
|
||||
return this.mediaMetadata.series
|
||||
},
|
||||
seriesSequence() {
|
||||
|
||||
@@ -44,6 +44,14 @@ export default {
|
||||
this.$nextTick(this.init)
|
||||
}
|
||||
}
|
||||
},
|
||||
width: {
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
this.isInit = false
|
||||
this.$nextTick(this.init)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<modals-modal v-model="show" name="account" :width="800" :height="'unset'" :processing="processing">
|
||||
<modals-modal ref="modal" v-model="show" name="account" :width="800" :height="'unset'" :processing="processing">
|
||||
<template #outer>
|
||||
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
|
||||
<p class="font-book text-3xl text-white truncate">{{ title }}</p>
|
||||
@@ -8,20 +8,20 @@
|
||||
<form @submit.prevent="submitForm">
|
||||
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
|
||||
<div class="w-full p-8">
|
||||
<div class="flex py-2 -mx-2">
|
||||
<div class="flex py-2">
|
||||
<div class="w-1/2 px-2">
|
||||
<ui-text-input-with-label v-model="newUser.username" label="Username" class="mx-2" />
|
||||
<ui-text-input-with-label v-model="newUser.username" label="Username" />
|
||||
</div>
|
||||
<div class="w-1/2 px-2">
|
||||
<ui-text-input-with-label v-if="!isEditingRoot" v-model="newUser.password" :label="isNew ? 'Password' : 'Change Password'" type="password" class="mx-2" />
|
||||
<ui-text-input-with-label v-if="!isEditingRoot" v-model="newUser.password" :label="isNew ? 'Password' : 'Change Password'" type="password" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex py-2">
|
||||
<div class="px-2">
|
||||
<ui-input-dropdown v-model="newUser.type" label="Account Type" :disabled="isEditingRoot" :editable="false" :items="accountTypes" @input="userTypeUpdated" />
|
||||
<div v-show="!isEditingRoot" class="flex py-2">
|
||||
<div class="px-2 w-52">
|
||||
<ui-dropdown v-model="newUser.type" label="Account Type" :disabled="isEditingRoot" :items="accountTypes" @input="userTypeUpdated" />
|
||||
</div>
|
||||
<div class="flex-grow" />
|
||||
<div v-show="!isEditingRoot" class="flex items-center pt-4 px-2">
|
||||
<div class="flex items-center pt-4 px-2">
|
||||
<p class="px-3 font-semibold" :class="isEditingRoot ? 'text-gray-300' : ''">Is Active</p>
|
||||
<ui-toggle-switch v-model="newUser.isActive" :disabled="isEditingRoot" />
|
||||
</div>
|
||||
@@ -92,7 +92,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex pt-4">
|
||||
<div class="flex pt-4 px-2">
|
||||
<ui-btn v-if="isEditingRoot" to="/account">Change Root Password</ui-btn>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn color="success" type="submit">Submit</ui-btn>
|
||||
</div>
|
||||
@@ -116,7 +117,20 @@ export default {
|
||||
processing: false,
|
||||
newUser: {},
|
||||
isNew: true,
|
||||
accountTypes: ['guest', 'user', 'admin'],
|
||||
accountTypes: [
|
||||
{
|
||||
text: 'Guest',
|
||||
value: 'guest'
|
||||
},
|
||||
{
|
||||
text: 'User',
|
||||
value: 'user'
|
||||
},
|
||||
{
|
||||
text: 'Admin',
|
||||
value: 'admin'
|
||||
}
|
||||
],
|
||||
tags: [],
|
||||
loadingTags: false
|
||||
}
|
||||
@@ -124,6 +138,7 @@ export default {
|
||||
watch: {
|
||||
show: {
|
||||
handler(newVal) {
|
||||
console.log('accoutn modal show change', newVal)
|
||||
if (newVal) {
|
||||
this.init()
|
||||
}
|
||||
@@ -140,7 +155,7 @@ export default {
|
||||
}
|
||||
},
|
||||
title() {
|
||||
return this.isNew ? 'Add New Account' : `Update Account: ${(this.account || {}).username}`
|
||||
return this.isNew ? 'Add New Account' : `Update ${(this.account || {}).username}`
|
||||
},
|
||||
isEditingRoot() {
|
||||
return this.account && this.account.type === 'root'
|
||||
@@ -161,6 +176,10 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
close() {
|
||||
// Force close when navigating - used in UsersTable
|
||||
if (this.$refs.modal) this.$refs.modal.setHide()
|
||||
},
|
||||
accessAllTagsToggled(val) {
|
||||
if (!val && !this.newUser.itemTagsAccessible.length) {
|
||||
this.newUser.itemTagsAccessible = this.libraries.map((l) => l.id)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<div class="w-full h-full overflow-y-auto overflow-x-hidden px-4 py-6">
|
||||
<div class="w-full mb-4">
|
||||
<!-- <div class="flex items-center mb-4">
|
||||
<p v-if="autoDownloadEpisodes">Last new episode check {{ $formatDate(lastEpisodeCheck) }}</p>
|
||||
<div class="flex-grow" />
|
||||
<ui-btn :loading="checkingNewEpisodes" @click="checkForNewEpisodes">Check for new episodes</ui-btn>
|
||||
</div> -->
|
||||
<div v-if="userIsAdminOrUp" class="flex items-end justify-end mb-4">
|
||||
<!-- <p v-if="autoDownloadEpisodes">Last new episode check {{ $formatDate(lastEpisodeCheck) }}</p> -->
|
||||
<ui-text-input-with-label ref="lastCheckInput" v-model="lastEpisodeCheckInput" :disabled="checkingNewEpisodes" type="datetime-local" label="Look for new episodes after this date" class="max-w-xs mr-2" />
|
||||
<ui-btn :loading="checkingNewEpisodes" @click="checkForNewEpisodes">Check & Download New Episodes</ui-btn>
|
||||
</div>
|
||||
|
||||
<div v-if="episodes.length" class="w-full p-4 bg-primary">
|
||||
<p>Podcast Episodes</p>
|
||||
@@ -51,10 +51,23 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
checkingNewEpisodes: false
|
||||
checkingNewEpisodes: false,
|
||||
lastEpisodeCheckInput: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
lastEpisodeCheck: {
|
||||
handler(newVal) {
|
||||
if (newVal) {
|
||||
this.setLastEpisodeCheckInput()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userIsAdminOrUp() {
|
||||
return this.$store.getters['user/getIsAdminOrUp']
|
||||
},
|
||||
autoDownloadEpisodes() {
|
||||
return !!this.media.autoDownloadEpisodes
|
||||
},
|
||||
@@ -72,8 +85,22 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
checkForNewEpisodes() {
|
||||
async checkForNewEpisodes() {
|
||||
if (this.$refs.lastCheckInput) {
|
||||
this.$refs.lastCheckInput.blur()
|
||||
}
|
||||
this.checkingNewEpisodes = true
|
||||
const lastEpisodeCheck = new Date(this.lastEpisodeCheckInput).valueOf()
|
||||
|
||||
// If last episode check changed then update it first
|
||||
if (lastEpisodeCheck && lastEpisodeCheck !== this.lastEpisodeCheck) {
|
||||
var updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, { lastEpisodeCheck }).catch((error) => {
|
||||
console.error('Failed to update', error)
|
||||
return false
|
||||
})
|
||||
console.log('updateResult', updateResult)
|
||||
}
|
||||
|
||||
this.$axios
|
||||
.$get(`/api/podcasts/${this.libraryItemId}/checknew`)
|
||||
.then((response) => {
|
||||
@@ -91,7 +118,13 @@ export default {
|
||||
this.$toast.error(errorMsg)
|
||||
this.checkingNewEpisodes = false
|
||||
})
|
||||
},
|
||||
setLastEpisodeCheckInput() {
|
||||
this.lastEpisodeCheckInput = this.lastEpisodeCheck ? this.$formatDate(this.lastEpisodeCheck, "yyyy-MM-dd'T'HH:mm") : null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.setLastEpisodeCheckInput()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -58,7 +58,7 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<modals-account-modal v-model="showAccountModal" :account="selectedAccount" />
|
||||
<modals-account-modal ref="accountModal" v-model="showAccountModal" :account="selectedAccount" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -156,6 +156,10 @@ export default {
|
||||
this.init()
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.$refs.accountModal) {
|
||||
this.$refs.accountModal.close()
|
||||
}
|
||||
|
||||
if (this.$root.socket) {
|
||||
this.$root.socket.off('user_added', this.newUserAdded)
|
||||
this.$root.socket.off('user_updated', this.userUpdated)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="relative w-full" v-click-outside="clickOutsideObj">
|
||||
<p class="text-sm font-semibold" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
|
||||
<p class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
|
||||
<button type="button" :disabled="disabled" class="relative w-full border rounded shadow-sm pl-3 pr-8 py-2 text-left focus:outline-none sm:text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
|
||||
<span class="flex items-center">
|
||||
<span class="block truncate" :class="small ? 'text-sm' : ''">{{ selectedText }}</span>
|
||||
|
||||
@@ -106,12 +106,6 @@ export default {
|
||||
}
|
||||
}
|
||||
if (payload.serverSettings) {
|
||||
this.$store.commit('setServerSettings', payload.serverSettings)
|
||||
|
||||
if (payload.serverSettings.chromecastEnabled) {
|
||||
console.log('Chromecast enabled import script')
|
||||
require('@/plugins/chromecast.js').default(this)
|
||||
}
|
||||
}
|
||||
|
||||
// Start scans currently running
|
||||
@@ -167,8 +161,28 @@ export default {
|
||||
libraryUpdated(library) {
|
||||
this.$store.commit('libraries/addUpdate', library)
|
||||
},
|
||||
libraryRemoved(library) {
|
||||
async libraryRemoved(library) {
|
||||
this.$store.commit('libraries/remove', library)
|
||||
|
||||
// When removed currently selected library then set next accessible library
|
||||
const currLibraryId = this.$store.state.libraries.currentLibraryId
|
||||
if (currLibraryId === library.id) {
|
||||
var nextLibrary = this.$store.getters['libraries/getNextAccessibleLibrary']
|
||||
if (nextLibrary) {
|
||||
await this.$store.dispatch('libraries/fetch', nextLibrary.id)
|
||||
|
||||
if (this.$route.name.startsWith('config')) {
|
||||
// No need to refresh
|
||||
} else if (this.$route.name.startsWith('library')) {
|
||||
var newRoute = this.$route.path.replace(currLibraryId, nextLibrary.id)
|
||||
this.$router.push(newRoute)
|
||||
} else {
|
||||
this.$router.push(`/library/${nextLibrary.id}`)
|
||||
}
|
||||
} else {
|
||||
console.error('User has no accessible libraries')
|
||||
}
|
||||
}
|
||||
},
|
||||
libraryItemAdded(libraryItem) {
|
||||
// this.$store.commit('libraries/updateFilterDataWithAudiobook', libraryItem)
|
||||
|
||||
@@ -54,7 +54,7 @@ export default {
|
||||
bookCoverAspectRatio: this.bookCoverAspectRatio,
|
||||
bookshelfView: this.bookshelfView
|
||||
}
|
||||
if (this.entityName === 'series-books') props.showSequence = true
|
||||
|
||||
if (this.entityName === 'books') {
|
||||
props.filterBy = this.filterBy
|
||||
props.orderBy = this.orderBy
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-client",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.8",
|
||||
"description": "Audiobook manager and player",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
|
||||
<div class="w-full h-px bg-primary my-4" />
|
||||
|
||||
<p class="mb-4 text-lg">Change Password</p>
|
||||
<form @submit.prevent="submitChangePassword">
|
||||
<p v-if="!isGuest" class="mb-4 text-lg">Change Password</p>
|
||||
<form v-if="!isGuest" @submit.prevent="submitChangePassword">
|
||||
<ui-text-input-with-label v-model="password" :disabled="changingPassword" type="password" label="Password" class="my-2" />
|
||||
<ui-text-input-with-label v-model="newPassword" :disabled="changingPassword" type="password" label="New Password" class="my-2" />
|
||||
<ui-text-input-with-label v-model="confirmPassword" :disabled="changingPassword" type="password" label="Confirm Password" class="my-2" />
|
||||
@@ -60,6 +60,9 @@ export default {
|
||||
},
|
||||
isRoot() {
|
||||
return this.usertype === 'root'
|
||||
},
|
||||
isGuest() {
|
||||
return this.usertype === 'guest'
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -95,14 +95,16 @@
|
||||
<p class="ml-4">Book has no audio tracks but has valid ebook files. The e-reader is experimental and can be turned on in config.</p>
|
||||
</div>
|
||||
|
||||
<!-- Podcast episode downloads queue -->
|
||||
<div v-if="episodeDownloadsQueued.length" class="px-4 py-2 mt-4 bg-info bg-opacity-40 text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0">
|
||||
<div class="flex items-center">
|
||||
<p class="text-sm py-1">{{ episodeDownloadsQueued.length }} Episode{{ episodeDownloadsQueued.length === 1 ? '' : 's' }} queued for download</p>
|
||||
|
||||
<span class="material-icons hover:text-error text-xl ml-3 cursor-pointer" @click="clearDownloadQueue">close</span>
|
||||
<span v-if="userIsAdminOrUp" class="material-icons hover:text-error text-xl ml-3 cursor-pointer" @click="clearDownloadQueue">close</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Podcast episodes currently downloading -->
|
||||
<div v-if="episodesDownloading.length" class="px-4 py-2 mt-4 bg-success bg-opacity-20 text-sm font-semibold rounded-md text-gray-100 relative max-w-max mx-auto md:mx-0">
|
||||
<div v-for="episode in episodesDownloading" :key="episode.id" class="flex items-center">
|
||||
<widgets-loading-spinner />
|
||||
@@ -150,7 +152,8 @@
|
||||
<ui-icon-btn icon="collections_bookmark" class="mx-0.5" outlined @click="collectionsClick" />
|
||||
</ui-tooltip>
|
||||
|
||||
<ui-tooltip v-if="isPodcast" text="Find Episodes" direction="top">
|
||||
<!-- Only admin or root user can download new episodes -->
|
||||
<ui-tooltip v-if="isPodcast && userIsAdminOrUp" text="Find Episodes" direction="top">
|
||||
<ui-icon-btn icon="search" class="mx-0.5" :loading="fetchingRSSFeed" outlined @click="findEpisodesClick" />
|
||||
</ui-tooltip>
|
||||
</div>
|
||||
@@ -210,6 +213,9 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
userIsAdminOrUp() {
|
||||
return this.$store.getters['user/getIsAdminOrUp']
|
||||
},
|
||||
isFile() {
|
||||
return this.libraryItem.isFile
|
||||
},
|
||||
|
||||
@@ -48,8 +48,15 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setUser(user, defaultLibraryId) {
|
||||
this.$store.commit('libraries/setCurrentLibrary', defaultLibraryId)
|
||||
setUser({ user, userDefaultLibraryId, serverSettings }) {
|
||||
this.$store.commit('setServerSettings', serverSettings)
|
||||
|
||||
if (serverSettings.chromecastEnabled) {
|
||||
console.log('Chromecast enabled import script')
|
||||
require('@/plugins/chromecast.js').default(this)
|
||||
}
|
||||
|
||||
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
|
||||
this.$store.commit('user/setUser', user)
|
||||
},
|
||||
async submitForm() {
|
||||
@@ -69,7 +76,7 @@ export default {
|
||||
if (authRes && authRes.error) {
|
||||
this.error = authRes.error
|
||||
} else if (authRes) {
|
||||
this.setUser(authRes.user, authRes.userDefaultLibraryId)
|
||||
this.setUser(authRes)
|
||||
}
|
||||
this.processing = false
|
||||
},
|
||||
@@ -87,7 +94,7 @@ export default {
|
||||
}
|
||||
})
|
||||
.then((res) => {
|
||||
this.setUser(res.user, res.userDefaultLibraryId)
|
||||
this.setUser(res)
|
||||
this.processing = false
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
@@ -53,8 +53,8 @@
|
||||
</widgets-alert>
|
||||
|
||||
<!-- Item Upload cards -->
|
||||
<template v-for="(item, index) in items">
|
||||
<cards-item-upload-card :ref="`itemCard-${item.index}`" :key="index" :media-type="selectedLibraryMediaType" :item="item" :processing="processing" @remove="removeItem(item)" />
|
||||
<template v-for="item in items">
|
||||
<cards-item-upload-card :ref="`itemCard-${item.index}`" :key="item.index" :media-type="selectedLibraryMediaType" :item="item" :processing="processing" @remove="removeItem(item)" />
|
||||
</template>
|
||||
|
||||
<!-- Upload/Reset btns -->
|
||||
|
||||
@@ -29,6 +29,19 @@ export const getters = {
|
||||
var library = state.libraries.find(l => l.id === libraryId)
|
||||
if (!library) return null
|
||||
return library.provider
|
||||
},
|
||||
getNextAccessibleLibrary: (state, getters, rootState, rootGetters) => {
|
||||
var librariesSorted = getters['getSortedLibraries']()
|
||||
if (!librariesSorted.length) return null
|
||||
|
||||
var canAccessAllLibraries = rootGetters['user/getUserCanAccessAllLibraries']
|
||||
var userAccessibleLibraries = rootGetters['user/getLibrariesAccessible']
|
||||
if (canAccessAllLibraries) return librariesSorted[0]
|
||||
librariesSorted = librariesSorted.filter((lib) => {
|
||||
return userAccessibleLibraries.includes(lib.id)
|
||||
})
|
||||
if (!librariesSorted.length) return null
|
||||
return librariesSorted[0]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ export const state = () => ({
|
||||
|
||||
export const getters = {
|
||||
getIsRoot: (state) => state.user && state.user.type === 'root',
|
||||
getIsAdminOrUp: (state) => state.user && (state.user.type === 'admin' || state.user.type === 'root'),
|
||||
getToken: (state) => {
|
||||
return state.user ? state.user.token : null
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf",
|
||||
"version": "2.0.7",
|
||||
"version": "2.0.8",
|
||||
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
|
||||
@@ -100,6 +100,14 @@ class Auth {
|
||||
})
|
||||
}
|
||||
|
||||
getUserLoginResponsePayload(user) {
|
||||
return {
|
||||
user: user.toJSONForBrowser(),
|
||||
userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries),
|
||||
serverSettings: this.db.serverSettings.toJSON()
|
||||
}
|
||||
}
|
||||
|
||||
async login(req, res) {
|
||||
var username = (req.body.username || '').toLowerCase()
|
||||
var password = req.body.password || ''
|
||||
@@ -120,17 +128,14 @@ class Auth {
|
||||
if (password) {
|
||||
return res.status(401).send('Invalid root password (hint: there is none)')
|
||||
} else {
|
||||
return res.json({ user: user.toJSONForBrowser(), userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries) })
|
||||
return res.json(this.getUserLoginResponsePayload(user))
|
||||
}
|
||||
}
|
||||
|
||||
// Check password match
|
||||
var compare = await bcrypt.compare(password, user.pash)
|
||||
if (compare) {
|
||||
res.json({
|
||||
user: user.toJSONForBrowser(),
|
||||
userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries)
|
||||
})
|
||||
res.json(this.getUserLoginResponsePayload(user))
|
||||
} else {
|
||||
Logger.debug(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit}`)
|
||||
if (req.rateLimit.remaining <= 2) {
|
||||
|
||||
@@ -409,6 +409,7 @@ class Server {
|
||||
await this.db.updateEntity('user', user)
|
||||
|
||||
const initialPayload = {
|
||||
// TODO: this is sent with user auth now, update mobile app to use that then remove this
|
||||
serverSettings: this.db.serverSettings.toJSON(),
|
||||
audiobookPath: global.AudiobookPath,
|
||||
metadataPath: global.MetadataPath,
|
||||
|
||||
@@ -201,7 +201,6 @@ class LibraryController {
|
||||
libraryItems = naturalSort(libraryItems).by(sortArray)
|
||||
}
|
||||
|
||||
// TODO: Potentially implement collapse series again
|
||||
if (payload.collapseseries) {
|
||||
libraryItems = libraryHelpers.collapseBookSeries(libraryItems)
|
||||
payload.total = libraryItems.length
|
||||
@@ -319,102 +318,6 @@ class LibraryController {
|
||||
res.json(categories)
|
||||
}
|
||||
|
||||
// TODO: Remove old personalized function with all its helper functions
|
||||
// old personalized function looped through the library items many times
|
||||
// api/libraries/:id/personalized-old
|
||||
async getLibraryUserPersonalized(req, res) {
|
||||
var mediaType = req.library.mediaType
|
||||
var isPodcastLibrary = mediaType == 'podcast'
|
||||
var libraryItems = req.libraryItems
|
||||
var limitPerShelf = req.query.limit && !isNaN(req.query.limit) ? Number(req.query.limit) : 12
|
||||
var minified = req.query.minified == '1'
|
||||
|
||||
var itemsWithUserProgress = libraryHelpers.getMediaProgressWithItems(req.user, libraryItems)
|
||||
var categories = [
|
||||
{
|
||||
id: 'continue-listening',
|
||||
label: 'Continue Listening',
|
||||
type: isPodcastLibrary ? 'episode' : req.library.mediaType,
|
||||
entities: libraryHelpers.getItemsMostRecentlyListened(itemsWithUserProgress, limitPerShelf, minified)
|
||||
},
|
||||
{
|
||||
id: 'recently-added',
|
||||
label: 'Recently Added',
|
||||
type: req.library.mediaType,
|
||||
entities: libraryHelpers.getItemsMostRecentlyAdded(libraryItems, limitPerShelf, minified)
|
||||
},
|
||||
{
|
||||
id: 'listen-again',
|
||||
label: 'Listen Again',
|
||||
type: isPodcastLibrary ? 'episode' : req.library.mediaType,
|
||||
entities: libraryHelpers.getItemsMostRecentlyFinished(itemsWithUserProgress, limitPerShelf, minified)
|
||||
}
|
||||
].filter(cats => { // Remove categories with no items
|
||||
return cats.entities.length
|
||||
})
|
||||
|
||||
// New Series section
|
||||
// TODO: optimize and move to libraryHelpers
|
||||
if (!isPodcastLibrary) {
|
||||
var series = this.db.series.map(se => {
|
||||
var books = libraryItems.filter(li => li.media.metadata.hasSeries(se.id))
|
||||
if (!books.length) return null
|
||||
books = books.map(b => {
|
||||
var json = b.toJSONMinified()
|
||||
json.sequence = b.media.metadata.getSeriesSequence(se.id)
|
||||
return json
|
||||
})
|
||||
books = naturalSort(books).asc(b => b.sequence)
|
||||
return {
|
||||
id: se.id,
|
||||
name: se.name,
|
||||
type: 'series',
|
||||
addedAt: se.addedAt,
|
||||
books
|
||||
}
|
||||
}).filter(se => se).sort((a, b) => a.addedAt - b.addedAt).slice(0, 5)
|
||||
|
||||
if (series.length) {
|
||||
categories.push({
|
||||
id: 'recent-series',
|
||||
label: 'Recent Series',
|
||||
type: 'series',
|
||||
entities: series
|
||||
})
|
||||
}
|
||||
|
||||
var authors = this.db.authors.map(author => {
|
||||
var books = libraryItems.filter(li => li.media.metadata.hasAuthor(author.id))
|
||||
if (!books.length) return null
|
||||
// books = books.map(b => b.toJSONMinified())
|
||||
return {
|
||||
...author.toJSON(),
|
||||
numBooks: books.length
|
||||
}
|
||||
}).filter(au => au).sort((a, b) => a.addedAt - b.addedAt).slice(0, 10)
|
||||
if (authors.length) {
|
||||
categories.push({
|
||||
id: 'newest-authors',
|
||||
label: 'Newest Authors',
|
||||
type: 'authors',
|
||||
entities: authors
|
||||
})
|
||||
}
|
||||
} else {
|
||||
var episodesRecentlyAdded = libraryHelpers.getEpisodesRecentlyAdded(libraryItems, limitPerShelf, minified)
|
||||
if (episodesRecentlyAdded.length) {
|
||||
categories.splice(1, 0, {
|
||||
id: 'episodes-recently-added',
|
||||
label: 'Newest Episodes',
|
||||
type: 'episode',
|
||||
entities: episodesRecentlyAdded
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
res.json(categories)
|
||||
}
|
||||
|
||||
// PATCH: Change the order of libraries
|
||||
async reorder(req, res) {
|
||||
if (!req.user.isRoot) {
|
||||
|
||||
@@ -133,6 +133,10 @@ class MeController {
|
||||
|
||||
// PATCH: api/me/password
|
||||
updatePassword(req, res) {
|
||||
if (req.user.isGuest) {
|
||||
Logger.error(`[MeController] Guest user attempted to change password`, req.user.username)
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
this.auth.userChangePassword(req, res)
|
||||
}
|
||||
|
||||
|
||||
@@ -230,7 +230,12 @@ class MiscController {
|
||||
Logger.error('Invalid user in authorize')
|
||||
return res.sendStatus(401)
|
||||
}
|
||||
res.json({ user: req.user, userDefaultLibraryId: req.user.getDefaultLibraryId(this.db.libraries) })
|
||||
const userResponse = {
|
||||
user: req.user,
|
||||
userDefaultLibraryId: req.user.getDefaultLibraryId(this.db.libraries),
|
||||
serverSettings: this.db.serverSettings.toJSON()
|
||||
}
|
||||
res.json(userResponse)
|
||||
}
|
||||
|
||||
getAllTags(req, res) {
|
||||
|
||||
@@ -9,8 +9,8 @@ const filePerms = require('../utils/filePerms')
|
||||
class PodcastController {
|
||||
|
||||
async create(req, res) {
|
||||
if (!req.user.isRoot) {
|
||||
Logger.error(`[PodcastController] Non-root user attempted to create podcast`, req.user)
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[PodcastController] Non-admin user attempted to create podcast`, req.user)
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
const payload = req.body
|
||||
@@ -115,24 +115,33 @@ class PodcastController {
|
||||
}
|
||||
|
||||
async checkNewEpisodes(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[PodcastController] Non-admin user attempted to check/download episodes`, req.user)
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
var libraryItem = this.db.getLibraryItem(req.params.id)
|
||||
if (!libraryItem || libraryItem.mediaType !== 'podcast') {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
if (!req.user.checkCanAccessLibrary(libraryItem.libraryId)) {
|
||||
Logger.error(`[PodcastController] User attempted to check/download episodes for a library without permission`, req.user)
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
if (!libraryItem.media.metadata.feedUrl) {
|
||||
Logger.error(`[PodcastController] checkNewEpisodes no feed url for item ${libraryItem.id}`)
|
||||
return res.status(500).send('Podcast has no rss feed url')
|
||||
}
|
||||
|
||||
var newEpisodes = await this.podcastManager.checkPodcastForNewEpisodes(libraryItem)
|
||||
var newEpisodes = await this.podcastManager.checkAndDownloadNewEpisodes(libraryItem)
|
||||
res.json({
|
||||
episodes: newEpisodes || []
|
||||
})
|
||||
}
|
||||
|
||||
clearEpisodeDownloadQueue(req, res) {
|
||||
if (!req.user.canUpdate) {
|
||||
Logger.error(`[PodcastController] User attempting to clear download queue without permission "${req.user.username}"`)
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[PodcastController] Non-admin user attempting to clear download queue "${req.user.username}"`)
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
this.podcastManager.clearDownloadQueue(req.params.id)
|
||||
@@ -151,11 +160,17 @@ class PodcastController {
|
||||
}
|
||||
|
||||
async downloadEpisodes(req, res) {
|
||||
if (!req.user.isAdminOrUp) {
|
||||
Logger.error(`[PodcastController] Non-admin user attempted to download episodes`, req.user)
|
||||
return res.sendStatus(500)
|
||||
}
|
||||
|
||||
var libraryItem = this.db.getLibraryItem(req.params.id)
|
||||
if (!libraryItem || libraryItem.mediaType !== 'podcast') {
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
if (!req.user.canUpload || !req.user.checkCanAccessLibrary(libraryItem.libraryId)) {
|
||||
if (!req.user.checkCanAccessLibrary(libraryItem.libraryId)) {
|
||||
Logger.error(`[PodcastController] User attempted to download episodes for library without permission`, req.user)
|
||||
return res.sendStatus(404)
|
||||
}
|
||||
|
||||
|
||||
@@ -208,8 +208,27 @@ class PodcastManager {
|
||||
}
|
||||
// Filter new and not already has
|
||||
var newEpisodes = feed.episodes.filter(ep => ep.publishedAt > podcastLibraryItem.media.lastEpisodeCheck && !podcastLibraryItem.media.checkHasEpisodeByFeedUrl(ep.enclosure.url))
|
||||
// Max new episodes for safety = 2
|
||||
newEpisodes = newEpisodes.slice(0, 2)
|
||||
// Max new episodes for safety = 3
|
||||
newEpisodes = newEpisodes.slice(0, 3)
|
||||
return newEpisodes
|
||||
}
|
||||
|
||||
async checkAndDownloadNewEpisodes(libraryItem) {
|
||||
const lastEpisodeCheckDate = new Date(libraryItem.media.lastEpisodeCheck || 0)
|
||||
Logger.info(`[PodcastManager] checkAndDownloadNewEpisodes for "${libraryItem.media.metadata.title}" - Last episode check: ${lastEpisodeCheckDate}`)
|
||||
var newEpisodes = await this.checkPodcastForNewEpisodes(libraryItem)
|
||||
if (newEpisodes.length) {
|
||||
Logger.info(`[PodcastManager] Found ${newEpisodes.length} new episodes for podcast "${libraryItem.media.metadata.title}" - starting download`)
|
||||
this.downloadPodcastEpisodes(libraryItem, newEpisodes)
|
||||
} else {
|
||||
Logger.info(`[PodcastManager] No new episodes found for podcast "${libraryItem.media.metadata.title}"`)
|
||||
}
|
||||
|
||||
libraryItem.media.lastEpisodeCheck = Date.now()
|
||||
libraryItem.updatedAt = Date.now()
|
||||
await this.db.updateLibraryItem(libraryItem)
|
||||
this.emitter('item_updated', libraryItem.toJSONExpanded())
|
||||
|
||||
return newEpisodes
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ class ServerSettings {
|
||||
|
||||
this.backupSchedule = settings.backupSchedule || false
|
||||
this.backupsToKeep = settings.backupsToKeep || 2
|
||||
this.maxBackupSize = settings.maxBackupSize || 1
|
||||
this.maxBackupSize = settings.maxBackupSize || 1
|
||||
this.backupMetadataCovers = settings.backupMetadataCovers !== false
|
||||
|
||||
this.loggerDailyLogsToKeep = settings.loggerDailyLogsToKeep || 7
|
||||
|
||||
@@ -30,6 +30,15 @@ class User {
|
||||
get isRoot() {
|
||||
return this.type === 'root'
|
||||
}
|
||||
get isAdmin() {
|
||||
return this.type === 'admin'
|
||||
}
|
||||
get isGuest() {
|
||||
return this.type === 'guest'
|
||||
}
|
||||
get isAdminOrUp() {
|
||||
return this.isAdmin || this.isRoot
|
||||
}
|
||||
get canDelete() {
|
||||
return !!this.permissions.delete && this.isActive
|
||||
}
|
||||
@@ -186,6 +195,7 @@ class User {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// And update permissions
|
||||
if (payload.permissions) {
|
||||
for (const key in payload.permissions) {
|
||||
@@ -195,8 +205,15 @@ class User {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update accessible libraries
|
||||
if (payload.librariesAccessible !== undefined) {
|
||||
if (this.permissions.accessAllLibraries) {
|
||||
// Access all libraries
|
||||
if (this.librariesAccessible.length) {
|
||||
this.librariesAccessible = []
|
||||
hasUpdates = true
|
||||
}
|
||||
} else if (payload.librariesAccessible !== undefined) {
|
||||
if (payload.librariesAccessible.length) {
|
||||
if (payload.librariesAccessible.join(',') !== this.librariesAccessible.join(',')) {
|
||||
hasUpdates = true
|
||||
@@ -208,8 +225,14 @@ class User {
|
||||
}
|
||||
}
|
||||
|
||||
// Update accessible libraries
|
||||
if (payload.itemTagsAccessible !== undefined) {
|
||||
// Update accessible tags
|
||||
if (this.permissions.accessAllTags) {
|
||||
// Access all tags
|
||||
if (this.itemTagsAccessible.length) {
|
||||
this.itemTagsAccessible = []
|
||||
hasUpdates = true
|
||||
}
|
||||
} else if (payload.itemTagsAccessible !== undefined) {
|
||||
if (payload.itemTagsAccessible.length) {
|
||||
if (payload.itemTagsAccessible.join(',') !== this.itemTagsAccessible.join(',')) {
|
||||
hasUpdates = true
|
||||
|
||||
@@ -61,7 +61,6 @@ class ApiRouter {
|
||||
this.router.delete('/libraries/:id/issues', LibraryController.middleware.bind(this), LibraryController.removeLibraryItemsWithIssues.bind(this))
|
||||
this.router.get('/libraries/:id/series', LibraryController.middleware.bind(this), LibraryController.getAllSeriesForLibrary.bind(this))
|
||||
this.router.get('/libraries/:id/collections', LibraryController.middleware.bind(this), LibraryController.getCollectionsForLibrary.bind(this))
|
||||
this.router.get('/libraries/:id/personalized-old', LibraryController.middleware.bind(this), LibraryController.getLibraryUserPersonalized.bind(this))
|
||||
this.router.get('/libraries/:id/personalized', LibraryController.middleware.bind(this), LibraryController.getLibraryUserPersonalizedOptimal.bind(this))
|
||||
this.router.get('/libraries/:id/filterdata', LibraryController.middleware.bind(this), LibraryController.getLibraryFilterData.bind(this))
|
||||
this.router.get('/libraries/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this))
|
||||
|
||||
@@ -136,80 +136,6 @@ module.exports = {
|
||||
})
|
||||
},
|
||||
|
||||
getSeriesWithProgressFromBooks(user, books) {
|
||||
return []
|
||||
// var _series = {}
|
||||
// books.forEach((audiobook) => {
|
||||
// if (audiobook.book.series) {
|
||||
// var bookWithUserAb = { userAudiobook: user.getMediaProgress(audiobook.id), book: audiobook }
|
||||
// if (!_series[audiobook.book.series]) {
|
||||
// _series[audiobook.book.series] = {
|
||||
// id: audiobook.book.series,
|
||||
// name: audiobook.book.series,
|
||||
// type: 'series',
|
||||
// books: [bookWithUserAb]
|
||||
// }
|
||||
// } else {
|
||||
// _series[audiobook.book.series].books.push(bookWithUserAb)
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
// return Object.values(_series).map((series) => {
|
||||
// series.books = naturalSort(series.books).asc(ab => ab.book.book.volumeNumber)
|
||||
// return series
|
||||
// }).filter((series) => series.books.some((book) => book.userAudiobook && book.userAudiobook.isRead))
|
||||
},
|
||||
|
||||
sortSeriesBooks(books, seriesId, minified = false) {
|
||||
return naturalSort(books).asc(li => {
|
||||
if (!li.media.metadata.series) return null
|
||||
var series = li.media.metadata.series.find(se => se.id === seriesId)
|
||||
if (!series) return null
|
||||
return series.sequence
|
||||
}).map(li => {
|
||||
if (minified) return li.toJSONMinified()
|
||||
return li.toJSONExpanded()
|
||||
})
|
||||
},
|
||||
|
||||
getMediaProgressWithItems(user, libraryItems) {
|
||||
var mediaProgress = []
|
||||
libraryItems.forEach(li => {
|
||||
var itemProgress = user.getAllMediaProgressForLibraryItem(li.id).map(mp => {
|
||||
var episode = null
|
||||
if (mp.episodeId) {
|
||||
episode = li.media.getEpisode(mp.episodeId)
|
||||
if (!episode) {
|
||||
// Episode not found for library item
|
||||
return null
|
||||
}
|
||||
}
|
||||
return {
|
||||
userProgress: mp.toJSON(),
|
||||
libraryItem: li,
|
||||
episode
|
||||
}
|
||||
}).filter(mp => !!mp)
|
||||
|
||||
mediaProgress = mediaProgress.concat(itemProgress)
|
||||
})
|
||||
return mediaProgress
|
||||
},
|
||||
|
||||
getItemsMostRecentlyListened(itemsWithUserProgress, limit, minified = false) {
|
||||
var itemsInProgress = itemsWithUserProgress.filter((data) => data.userProgress && data.userProgress.progress > 0 && !data.userProgress.isFinished)
|
||||
itemsInProgress.sort((a, b) => {
|
||||
return b.userProgress.lastUpdate - a.userProgress.lastUpdate
|
||||
})
|
||||
return itemsInProgress.map(b => {
|
||||
var libjson = minified ? b.libraryItem.toJSONMinified() : b.libraryItem.toJSONExpanded()
|
||||
if (b.episode) {
|
||||
libjson.recentEpisode = b.episode
|
||||
}
|
||||
return libjson
|
||||
}).slice(0, limit)
|
||||
},
|
||||
|
||||
getBooksNextInSeries(seriesWithUserAb, limit, minified = false) {
|
||||
var incompleteSeires = seriesWithUserAb.filter((series) => series.books.some((book) => !book.userAudiobook || (!book.userAudiobook.isRead && book.userAudiobook.progress == 0)))
|
||||
var booksNextInSeries = []
|
||||
@@ -222,49 +148,6 @@ module.exports = {
|
||||
return booksNextInSeries.sort((a, b) => { return b.DateLastReadSeries - a.DateLastReadSeries }).map(b => minified ? b.book.toJSONMinified() : b.book.toJSONExpanded()).slice(0, limit)
|
||||
},
|
||||
|
||||
getItemsMostRecentlyFinished(itemsWithUserProgress, limit, minified = false) {
|
||||
var itemsFinished = itemsWithUserProgress.filter((data) => data.userProgress && data.userProgress.isFinished)
|
||||
itemsFinished.sort((a, b) => {
|
||||
return b.userProgress.finishedAt - a.userProgress.finishedAt
|
||||
})
|
||||
return itemsFinished.map(i => {
|
||||
var libjson = minified ? i.libraryItem.toJSONMinified() : i.libraryItem.toJSONExpanded()
|
||||
if (i.episode) {
|
||||
libjson.recentEpisode = i.episode
|
||||
}
|
||||
return libjson
|
||||
}).slice(0, limit)
|
||||
},
|
||||
|
||||
getItemsMostRecentlyAdded(libraryItems, limit, minified = false) {
|
||||
var itemsSortedByAddedAt = sort(libraryItems).desc(li => li.addedAt)
|
||||
return itemsSortedByAddedAt.map(b => minified ? b.toJSONMinified() : b.toJSONExpanded()).slice(0, limit)
|
||||
},
|
||||
|
||||
getEpisodesRecentlyAdded(libraryItems, limit, minified = false) {
|
||||
var libraryItemsWithEpisode = []
|
||||
libraryItems.forEach((li) => {
|
||||
if (li.mediaType !== 'podcast' || !li.media.hasMediaEntities) return
|
||||
var libjson = minified ? li.toJSONMinified() : li.toJSONExpanded()
|
||||
var episodes = sort(li.media.episodes || []).desc(ep => ep.addedAt)
|
||||
episodes.forEach((ep) => {
|
||||
var lie = { ...libjson }
|
||||
lie.recentEpisode = ep
|
||||
libraryItemsWithEpisode.push(lie)
|
||||
})
|
||||
})
|
||||
libraryItemsWithEpisode = sort(libraryItemsWithEpisode).desc(lie => lie.recentEpisode.addedAt)
|
||||
return libraryItemsWithEpisode.slice(0, limit)
|
||||
},
|
||||
|
||||
getSeriesMostRecentlyAdded(series, limit) {
|
||||
var seriesSortedByAddedAt = sort(series).desc(_series => {
|
||||
var booksSortedByMostRecent = sort(_series.books).desc(b => b.addedAt)
|
||||
return booksSortedByMostRecent[0].addedAt
|
||||
})
|
||||
return seriesSortedByAddedAt.slice(0, limit)
|
||||
},
|
||||
|
||||
getGenresWithCount(libraryItems) {
|
||||
var genresMap = {}
|
||||
libraryItems.forEach((li) => {
|
||||
@@ -354,7 +237,6 @@ module.exports = {
|
||||
buildPersonalizedShelves(user, libraryItems, mediaType, allSeries, allAuthors, maxEntitiesPerShelf = 10) {
|
||||
const isPodcastLibrary = mediaType === 'podcast'
|
||||
|
||||
|
||||
const shelves = [
|
||||
{
|
||||
id: 'continue-listening',
|
||||
@@ -363,6 +245,13 @@ module.exports = {
|
||||
entities: [],
|
||||
category: 'recentlyListened'
|
||||
},
|
||||
{
|
||||
id: 'continue-series',
|
||||
label: 'Continue Series',
|
||||
type: mediaType,
|
||||
entities: [],
|
||||
category: 'continueSeries'
|
||||
},
|
||||
{
|
||||
id: 'recently-added',
|
||||
label: 'Recently Added',
|
||||
@@ -400,7 +289,7 @@ module.exports = {
|
||||
}
|
||||
]
|
||||
|
||||
const categories = ['recentlyListened', 'newestEpisodes', 'newestItems', 'newestSeries', 'recentlyFinished', 'newestAuthors']
|
||||
const categories = ['recentlyListened', 'continueSeries', 'newestEpisodes', 'newestItems', 'newestSeries', 'recentlyFinished', 'newestAuthors']
|
||||
const categoryMap = {}
|
||||
categories.forEach((cat) => {
|
||||
categoryMap[cat] = {
|
||||
@@ -516,20 +405,24 @@ module.exports = {
|
||||
// Newest series
|
||||
if (libraryItem.media.metadata.series.length) {
|
||||
for (const librarySeries of libraryItem.media.metadata.series) {
|
||||
const mediaProgress = allItemProgress.length ? allItemProgress[0] : null
|
||||
const bookInProgress = mediaProgress && mediaProgress.inProgress
|
||||
const libraryItemJson = libraryItem.toJSONMinified()
|
||||
libraryItemJson.seriesSequence = librarySeries.sequence
|
||||
|
||||
if (!seriesMap[librarySeries.id]) {
|
||||
const seriesObj = allSeries.find(se => se.id === librarySeries.id)
|
||||
if (seriesObj) {
|
||||
var series = {
|
||||
...seriesObj.toJSON(),
|
||||
books: []
|
||||
books: [libraryItemJson],
|
||||
inProgress: bookInProgress,
|
||||
bookInProgressLastUpdate: bookInProgress ? mediaProgress.lastUpdate : null,
|
||||
sequenceInProgress: bookInProgress ? libraryItemJson.seriesSequence : null
|
||||
}
|
||||
seriesMap[librarySeries.id] = series
|
||||
|
||||
if (series.addedAt > categoryMap.newestSeries.smallest) {
|
||||
const libraryItemJson = libraryItem.toJSONMinified()
|
||||
libraryItemJson.seriesSequence = librarySeries.sequence
|
||||
series.books.push(libraryItemJson)
|
||||
|
||||
var indexToPut = categoryMap.newestSeries.items.findIndex(i => series.addedAt > i.addedAt)
|
||||
if (indexToPut >= 0) {
|
||||
categoryMap.newestSeries.items.splice(indexToPut, 0, series)
|
||||
@@ -544,15 +437,19 @@ module.exports = {
|
||||
}
|
||||
|
||||
categoryMap.newestSeries.biggest = categoryMap.newestSeries.items[0].addedAt
|
||||
|
||||
seriesMap[librarySeries.id] = series
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// series already in map - add book
|
||||
const libraryItemJson = libraryItem.toJSONMinified()
|
||||
libraryItemJson.seriesSequence = librarySeries.sequence
|
||||
seriesMap[librarySeries.id].books.push(libraryItemJson)
|
||||
|
||||
if (bookInProgress) { // Update if this series is in progress
|
||||
seriesMap[librarySeries.id].inProgress = true
|
||||
if (!seriesMap[librarySeries.id].sequenceInProgress) {
|
||||
seriesMap[librarySeries.id].sequenceInProgress = librarySeries.sequence
|
||||
seriesMap[librarySeries.id].bookInProgressLastUpdate = mediaProgress.lastUpdate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -643,6 +540,38 @@ module.exports = {
|
||||
}
|
||||
}
|
||||
|
||||
// For Continue Series - Find next book in series for series that are in progress
|
||||
for (const seriesId in seriesMap) {
|
||||
if (seriesMap[seriesId].inProgress) {
|
||||
seriesMap[seriesId].books = naturalSort(seriesMap[seriesId].books).asc(li => li.seriesSequence)
|
||||
|
||||
const nextBookInSeries = seriesMap[seriesId].books.find(li => {
|
||||
if (!seriesMap[seriesId].sequenceInProgress) return true
|
||||
// True if book series sequence is greater than the current book sequence in progress
|
||||
return String(li.seriesSequence).localeCompare(String(seriesMap[seriesId].sequenceInProgress), undefined, { sensitivity: 'base', numeric: true }) > 0
|
||||
})
|
||||
|
||||
if (nextBookInSeries) {
|
||||
const bookForContinueSeries = {
|
||||
...nextBookInSeries,
|
||||
prevBookInProgressLastUpdate: seriesMap[seriesId].bookInProgressLastUpdate
|
||||
}
|
||||
bookForContinueSeries.media.metadata.series = {
|
||||
id: seriesId,
|
||||
name: seriesMap[seriesId].name,
|
||||
sequence: nextBookInSeries.seriesSequence
|
||||
}
|
||||
|
||||
const indexToPut = categoryMap.continueSeries.items.findIndex(i => i.prevBookInProgressLastUpdate < bookForContinueSeries.prevBookInProgressLastUpdate)
|
||||
if (indexToPut >= 0) {
|
||||
categoryMap.continueSeries.items.splice(indexToPut, 0, bookForContinueSeries)
|
||||
} else if (categoryMap.continueSeries.items.length < 10) { // Max 10 books
|
||||
categoryMap.continueSeries.items.push(bookForContinueSeries)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort series books by sequence
|
||||
if (categoryMap.newestSeries.items.length) {
|
||||
for (const seriesItem of categoryMap.newestSeries.items) {
|
||||
|
||||
Reference in New Issue
Block a user