mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-16 00:44:51 +02:00
This patch implements a generic way of re-opening the player for the book you last listened to. This is especially handy when first opening the app and you can restart playback with a single click. The player is opened only if no other book is already open in the player. It will not overwrite or replace playbacks or player already in progress. The player is opened paused. No automatic playback since that could be really annoying. This closes #494
335 lines
No EOL
12 KiB
Vue
335 lines
No EOL
12 KiB
Vue
<template>
|
|
<div class="w-full h-full min-h-full relative">
|
|
<div v-if="!loading" class="w-full" :class="{ 'py-6': altViewEnabled }">
|
|
<template v-for="(shelf, index) in shelves">
|
|
<bookshelf-shelf :key="shelf.id" :label="shelf.label" :entities="shelf.entities" :type="shelf.type" :style="{ zIndex: shelves.length - index }" />
|
|
</template>
|
|
</div>
|
|
|
|
<div v-if="!shelves.length && !loading" class="absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
|
<div>
|
|
<p class="mb-4 text-center text-xl">
|
|
Bookshelf empty
|
|
<span v-show="user">
|
|
for library
|
|
<strong>{{ currentLibraryName }}</strong>
|
|
</span>
|
|
</p>
|
|
<div class="w-full" v-if="!user">
|
|
<div class="flex justify-center items-center mb-3">
|
|
<span class="material-icons text-error text-lg">cloud_off</span>
|
|
<p class="pl-2 text-error text-sm">Audiobookshelf server not connected.</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex justify-center">
|
|
<ui-btn v-if="!user" small @click="$router.push('/connect')" class="w-32">Connect</ui-btn>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="loading" class="absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
|
<ui-loading-indicator text="Loading Library..." />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
export default {
|
|
data() {
|
|
return {
|
|
shelves: [],
|
|
loading: false,
|
|
isFirstNetworkConnection: true,
|
|
lastServerFetch: 0,
|
|
lastServerFetchLibraryId: null,
|
|
lastLocalFetch: 0,
|
|
localLibraryItems: []
|
|
}
|
|
},
|
|
watch: {
|
|
networkConnected(newVal) {
|
|
// Update shelves when network connect status changes
|
|
console.log(`[categories] Network changed to ${newVal} - fetch categories. ${this.lastServerFetch}/${this.lastLocalFetch}`)
|
|
|
|
if (newVal) {
|
|
// Fetch right away the first time network connects
|
|
if (this.isFirstNetworkConnection) {
|
|
this.isFirstNetworkConnection = false
|
|
console.log(`[categories] networkConnected true first network connection. lastServerFetch=${this.lastServerFetch}`)
|
|
this.fetchCategories()
|
|
return
|
|
}
|
|
|
|
setTimeout(() => {
|
|
// Using timeout because making this fetch as soon as network gets connected will often fail on Android
|
|
console.log(`[categories] networkConnected true so fetching categories. lastServerFetch=${this.lastServerFetch}`)
|
|
this.fetchCategories()
|
|
}, 4000)
|
|
} else {
|
|
console.log(`[categories] networkConnected false so fetching categories`)
|
|
this.fetchCategories()
|
|
}
|
|
}
|
|
},
|
|
computed: {
|
|
user() {
|
|
return this.$store.state.user.user
|
|
},
|
|
networkConnected() {
|
|
return this.$store.state.networkConnected
|
|
},
|
|
currentLibraryName() {
|
|
return this.$store.getters['libraries/getCurrentLibraryName']
|
|
},
|
|
currentLibraryId() {
|
|
return this.$store.state.libraries.currentLibraryId
|
|
},
|
|
currentLibraryMediaType() {
|
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
|
},
|
|
currentLibraryIsPodcast() {
|
|
return this.currentLibraryMediaType === 'podcast'
|
|
},
|
|
altViewEnabled() {
|
|
return this.$store.getters['getAltViewEnabled']
|
|
},
|
|
localMediaProgress() {
|
|
return this.$store.state.globals.localMediaProgress
|
|
}
|
|
},
|
|
methods: {
|
|
async getLocalMediaItemCategories() {
|
|
const localMedia = await this.$db.getLocalLibraryItems()
|
|
if (!localMedia || !localMedia.length) return []
|
|
|
|
const categories = []
|
|
const books = []
|
|
const podcasts = []
|
|
const booksContinueListening = []
|
|
const podcastEpisodesContinueListening = []
|
|
localMedia.forEach((item) => {
|
|
if (item.mediaType == 'book') {
|
|
item.progress = this.$store.getters['globals/getLocalMediaProgressById'](item.id)
|
|
if (item.progress && !item.progress.isFinished && item.progress.progress > 0) booksContinueListening.push(item)
|
|
books.push(item)
|
|
} else if (item.mediaType == 'podcast') {
|
|
const podcastEpisodeItemCloner = { ...item }
|
|
item.media.episodes = item.media.episodes.map((ep) => {
|
|
ep.progress = this.$store.getters['globals/getLocalMediaProgressById'](item.id, ep.id)
|
|
if (ep.progress && !ep.progress.isFinished && ep.progress.progress > 0) {
|
|
podcastEpisodesContinueListening.push({
|
|
...podcastEpisodeItemCloner,
|
|
recentEpisode: ep
|
|
})
|
|
}
|
|
return ep
|
|
})
|
|
podcasts.push(item)
|
|
}
|
|
})
|
|
|
|
// Local continue listening shelves, only shown offline
|
|
if (booksContinueListening.length) {
|
|
categories.push({
|
|
id: 'local-books-continue',
|
|
label: 'Continue Books',
|
|
type: 'book',
|
|
localOnly: true,
|
|
entities: booksContinueListening.sort((a, b) => {
|
|
if (a.progress && b.progress) {
|
|
return b.progress.lastUpdate > a.progress.lastUpdate ? 1 : -1
|
|
}
|
|
return 0
|
|
})
|
|
})
|
|
}
|
|
if (podcastEpisodesContinueListening.length) {
|
|
categories.push({
|
|
id: 'local-episodes-continue',
|
|
label: 'Continue Episodes',
|
|
type: 'episode',
|
|
localOnly: true,
|
|
entities: podcastEpisodesContinueListening.sort((a, b) => {
|
|
if (a.recentEpisode.progress && b.recentEpisode.progress) {
|
|
return b.recentEpisode.progress.lastUpdate > a.recentEpisode.progress.lastUpdate ? 1 : -1
|
|
}
|
|
return 0
|
|
})
|
|
})
|
|
}
|
|
|
|
// Local books and local podcast shelves
|
|
if (books.length) {
|
|
categories.push({
|
|
id: 'local-books',
|
|
label: 'Local Books',
|
|
type: 'book',
|
|
entities: books.sort((a, b) => {
|
|
if (a.progress && a.progress.isFinished) return 1
|
|
else if (b.progress && b.progress.isFinished) return -1
|
|
else if (a.progress && b.progress) {
|
|
return b.progress.lastUpdate > a.progress.lastUpdate ? 1 : -1
|
|
}
|
|
return 0
|
|
})
|
|
})
|
|
}
|
|
if (podcasts.length) {
|
|
categories.push({
|
|
id: 'local-podcasts',
|
|
label: 'Local Podcasts',
|
|
type: 'podcast',
|
|
entities: podcasts
|
|
})
|
|
}
|
|
|
|
return categories
|
|
},
|
|
async fetchCategories() {
|
|
console.log(`[categories] fetchCategories networkConnected=${this.networkConnected}, lastServerFetch=${this.lastServerFetch}, lastLocalFetch=${this.lastLocalFetch}`)
|
|
|
|
// TODO: Find a better way to keep the shelf up-to-date with local vs server library because this is a disaster
|
|
if (this.user && this.currentLibraryId && this.networkConnected) {
|
|
if (this.lastServerFetch && Date.now() - this.lastServerFetch < 5000 && this.lastServerFetchLibraryId == this.currentLibraryId) {
|
|
console.log(`[categories] fetchCategories server fetch was ${Date.now() - this.lastServerFetch}ms ago so not doing it.`)
|
|
return
|
|
} else {
|
|
console.log(`[categories] fetchCategories fetching from server. Last was ${this.lastServerFetch ? Date.now() - this.lastServerFetch + 'ms' : 'Never'} ago. lastServerFetchLibraryId=${this.lastServerFetchLibraryId} and currentLibraryId=${this.currentLibraryId}`)
|
|
this.lastServerFetchLibraryId = this.currentLibraryId
|
|
this.lastServerFetch = Date.now()
|
|
this.lastLocalFetch = 0
|
|
}
|
|
} else {
|
|
if (this.lastLocalFetch && Date.now() - this.lastLocalFetch < 5000) {
|
|
console.log(`[categories] fetchCategories local fetch was ${Date.now() - this.lastLocalFetch}ms ago so not doing it.`)
|
|
return
|
|
} else {
|
|
console.log(`[categories] fetchCategories fetching from local. Last was ${this.lastLocalFetch ? Date.now() - this.lastLocalFetch + 'ms' : 'Never'} ago`)
|
|
this.lastServerFetchLibraryId = null
|
|
this.lastServerFetch = 0
|
|
this.lastLocalFetch = Date.now()
|
|
}
|
|
}
|
|
|
|
this.loading = true
|
|
this.shelves = []
|
|
|
|
if (this.user && this.currentLibraryId && this.networkConnected) {
|
|
this.localLibraryItems = await this.$db.getLocalLibraryItems()
|
|
const localCategories = await this.getLocalMediaItemCategories()
|
|
const categories = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/personalized?minified=1`).catch((error) => {
|
|
console.error('[categories] Failed to fetch categories', error)
|
|
return []
|
|
})
|
|
if (!categories.length) {
|
|
// Failed to load categories so use local shelves
|
|
console.warn(`[categories] Failed to get server categories so using local categories`)
|
|
this.shelves = localCategories
|
|
this.lastServerFetch = 0
|
|
this.lastLocalFetch = Date.now()
|
|
this.loading = false
|
|
console.log('[categories] Local shelves set from failure', this.shelves.length, this.lastLocalFetch)
|
|
return
|
|
}
|
|
|
|
this.shelves = categories.map((cat) => {
|
|
if (cat.type == 'book' || cat.type == 'podcast' || cat.type == 'episode') {
|
|
// Map localLibraryItem to entities
|
|
cat.entities = cat.entities.map((entity) => {
|
|
const localLibraryItem = this.localLibraryItems.find((lli) => {
|
|
return lli.libraryItemId == entity.id
|
|
})
|
|
if (localLibraryItem) {
|
|
entity.localLibraryItem = localLibraryItem
|
|
}
|
|
return entity
|
|
})
|
|
}
|
|
return cat
|
|
})
|
|
|
|
// If we don't already have a player open
|
|
// Try opening the first book from continue-listening without playing it
|
|
if (!this.$store.state.playerLibraryItemId) {
|
|
const lastListening = categories.filter((cat) => cat.id === 'continue-listening')[0]?.entities?.[0]?.id
|
|
if (lastListening) {
|
|
this.$eventBus.$emit('play-item', { libraryItemId: lastListening, paused: true })
|
|
}
|
|
}
|
|
|
|
// Only add the local shelf with the same media type
|
|
const localShelves = localCategories.filter((cat) => cat.type === this.currentLibraryMediaType && !cat.localOnly)
|
|
this.shelves.push(...localShelves)
|
|
console.log('[categories] Server shelves set', this.shelves.length, this.lastServerFetch)
|
|
} else {
|
|
// Offline only local
|
|
this.localLibraryItems = await this.$db.getLocalLibraryItems()
|
|
const localCategories = await this.getLocalMediaItemCategories()
|
|
this.shelves = localCategories
|
|
console.log('[categories] Local shelves set', this.shelves.length, this.lastLocalFetch)
|
|
}
|
|
|
|
this.loading = false
|
|
},
|
|
libraryChanged() {
|
|
if (this.currentLibraryId) {
|
|
console.log(`[categories] libraryChanged so fetching categories`)
|
|
this.fetchCategories()
|
|
}
|
|
},
|
|
audiobookAdded(audiobook) {
|
|
// TODO: Check if audiobook would be on this shelf
|
|
if (!this.search) {
|
|
this.fetchCategories()
|
|
}
|
|
},
|
|
audiobookUpdated(audiobook) {
|
|
this.shelves.forEach((shelf) => {
|
|
if (shelf.type === 'books') {
|
|
shelf.entities = shelf.entities.map((ent) => {
|
|
if (ent.id === audiobook.id) {
|
|
return audiobook
|
|
}
|
|
return ent
|
|
})
|
|
} else if (shelf.type === 'series') {
|
|
shelf.entities.forEach((ent) => {
|
|
ent.books = ent.books.map((book) => {
|
|
if (book.id === audiobook.id) return audiobook
|
|
return book
|
|
})
|
|
})
|
|
}
|
|
})
|
|
},
|
|
removeBookFromShelf(audiobook) {
|
|
this.shelves.forEach((shelf) => {
|
|
if (shelf.type === 'books') {
|
|
shelf.entities = shelf.entities.filter((ent) => {
|
|
return ent.id !== audiobook.id
|
|
})
|
|
} else if (shelf.type === 'series') {
|
|
shelf.entities.forEach((ent) => {
|
|
ent.books = ent.books.filter((book) => {
|
|
return book.id !== audiobook.id
|
|
})
|
|
})
|
|
}
|
|
})
|
|
},
|
|
initListeners() {
|
|
this.$eventBus.$on('library-changed', this.libraryChanged)
|
|
},
|
|
removeListeners() {
|
|
this.$eventBus.$off('library-changed', this.libraryChanged)
|
|
}
|
|
},
|
|
mounted() {
|
|
this.initListeners()
|
|
console.log(`[categories] mounted so fetching categories`)
|
|
this.fetchCategories()
|
|
},
|
|
beforeDestroy() {
|
|
this.removeListeners()
|
|
}
|
|
}
|
|
</script> |