Add local library items to bookshelf and landing page

This commit is contained in:
advplyr 2022-04-03 17:07:26 -05:00
parent 4f8b13b23d
commit 9fd3dc6978
15 changed files with 279 additions and 125 deletions

View file

@ -141,9 +141,9 @@ class AudioDownloader : Plugin() {
var downloadItem = DownloadItem(libraryItem.id, localFolder, bookTitle, mutableListOf()) var downloadItem = DownloadItem(libraryItem.id, localFolder, bookTitle, mutableListOf())
var itemFolderPath = localFolder.absolutePath + "/" + bookTitle var itemFolderPath = localFolder.absolutePath + "/" + bookTitle
tracks.forEach { audioFile -> tracks.forEach { audioFile ->
var serverPath = "/s/item/${libraryItem.id}/${cleanRelPath(audioFile.metadata.relPath)}" var serverPath = "/s/item/${libraryItem.id}/${cleanRelPath(audioFile.relPath)}"
var destinationFilename = getFilenameFromRelPath(audioFile.metadata.relPath) var destinationFilename = getFilenameFromRelPath(audioFile.relPath)
Log.d(tag, "Audio File Server Path $serverPath | AF RelPath ${audioFile.metadata.relPath} | LocalFolder Path ${localFolder.absolutePath} | DestName ${destinationFilename}") Log.d(tag, "Audio File Server Path $serverPath | AF RelPath ${audioFile.relPath} | LocalFolder Path ${localFolder.absolutePath} | DestName ${destinationFilename}")
var destinationFile = File("$itemFolderPath/$destinationFilename") var destinationFile = File("$itemFolderPath/$destinationFilename")
var destinationUri = Uri.fromFile(destinationFile) var destinationUri = Uri.fromFile(destinationFile)
var downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}") var downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}")

View file

@ -78,6 +78,16 @@ class MyNativeAudio : Plugin() {
var libraryItemId = call.getString("libraryItemId", "").toString() var libraryItemId = call.getString("libraryItemId", "").toString()
var playWhenReady = call.getBoolean("playWhenReady") == true var playWhenReady = call.getBoolean("playWhenReady") == true
if (libraryItemId.startsWith("local")) { // Play local media item
DeviceManager.dbManager.getLocalMediaItem(libraryItemId)?.let {
Handler(Looper.getMainLooper()).post() {
Log.d(tag, "Preparing Local Media item ${jacksonObjectMapper().writeValueAsString(it)}")
var playbackSession = it.getPlaybackSession()
playerNotificationService.preparePlayer(playbackSession, playWhenReady)
}
return call.resolve(JSObject())
}
} else { // Play library item from server
apiHandler.playLibraryItem(libraryItemId, false) { apiHandler.playLibraryItem(libraryItemId, false) {
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
@ -88,24 +98,6 @@ class MyNativeAudio : Plugin() {
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(it))) call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(it)))
} }
} }
@PluginMethod
fun playLocalLibraryItem(call:PluginCall) {
var localMediaItemId = call.getString("localMediaItemId", "").toString()
var playWhenReady = call.getBoolean("playWhenReady") == true
Log.d(tag, "playLocalLibraryItem $playWhenReady")
DeviceManager.dbManager.loadLocalMediaItem(localMediaItemId)?.let {
Handler(Looper.getMainLooper()).post() {
Log.d(tag, "Preparing Local Media item ${jacksonObjectMapper().writeValueAsString(it)}")
var playbackSession = it.getPlaybackSession()
playerNotificationService.preparePlayer(playbackSession, playWhenReady)
}
return call.resolve(JSObject())
}
var errObj = JSObject()
errObj.put("error", "Item Not Found")
call.resolve(errObj)
} }
@PluginMethod @PluginMethod

View file

@ -1,6 +1,6 @@
package com.audiobookshelf.app.data package com.audiobookshelf.app.data
import android.net.Uri import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
@ -24,8 +24,16 @@ data class AudioProbeChapter(
val id:Int, val id:Int,
val start:Int, val start:Int,
val end:Int, val end:Int,
val tags:AudioProbeChapterTags val tags:AudioProbeChapterTags?
) ) {
@JsonIgnore
fun getBookChapter():BookChapter {
var startS = start / 1000.0
var endS = end / 1000.0
var title = tags?.title ?: "Chapter $id"
return BookChapter(id, startS, endS, title)
}
}
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class AudioProbeFormatTags( data class AudioProbeFormatTags(
@ -57,4 +65,10 @@ class AudioProbeResult (
val size get() = format.size val size get() = format.size
val title get() = format.tags.title ?: format.filename.split("/").last() val title get() = format.tags.title ?: format.filename.split("/").last()
val artist get() = format.tags.artist ?: "" val artist get() = format.tags.artist ?: ""
@JsonIgnore
fun getBookChapters(): List<BookChapter> {
if (chapters.isEmpty()) return mutableListOf()
return chapters.map { it.getBookChapter() }
}
} }

View file

@ -45,10 +45,10 @@ data class Podcast(
data class Book( data class Book(
var metadata:BookMetadata, var metadata:BookMetadata,
var coverPath:String?, var coverPath:String?,
var tags:MutableList<String>, var tags:List<String>,
var audioFiles:MutableList<AudioFile>, var audioFiles:List<AudioFile>,
var chapters:MutableList<BookChapter>, var chapters:List<BookChapter>,
var tracks:MutableList<AudioFile>?, var tracks:List<AudioTrack>?,
var size:Long?, var size:Long?,
var duration:Double? var duration:Double?
) : MediaType() ) : MediaType()
@ -154,9 +154,10 @@ data class AudioTrack(
var title:String, var title:String,
var contentUrl:String, var contentUrl:String,
var mimeType:String, var mimeType:String,
var metadata:FileMetadata?,
var isLocal:Boolean, var isLocal:Boolean,
var localFileId:String?, var localFileId:String?,
var audioProbeResult:AudioProbeResult? var audioProbeResult:AudioProbeResult?,
) { ) {
@get:JsonIgnore @get:JsonIgnore
@ -165,6 +166,13 @@ data class AudioTrack(
val durationMs get() = (duration * 1000L).toLong() val durationMs get() = (duration * 1000L).toLong()
@get:JsonIgnore @get:JsonIgnore
val endOffsetMs get() = startOffsetMs + durationMs val endOffsetMs get() = startOffsetMs + durationMs
@get:JsonIgnore
val relPath get() = metadata?.relPath ?: ""
@JsonIgnore
fun getBookChapter():BookChapter {
return BookChapter(index + 1,startOffset, startOffset + duration, title)
}
} }
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)

View file

@ -56,7 +56,7 @@ class DbManager : Plugin() {
} }
} }
fun loadLocalMediaItem(localMediaItemId:String):LocalMediaItem? { fun getLocalMediaItem(localMediaItemId:String):LocalMediaItem? {
return Paper.book("localMediaItems").read(localMediaItemId) return Paper.book("localMediaItems").read(localMediaItemId)
} }
@ -153,6 +153,32 @@ class DbManager : Plugin() {
} }
} }
@PluginMethod
fun getLocalLibraryItems_WV(call:PluginCall) {
GlobalScope.launch(Dispatchers.IO) {
var localLibraryItems = getLocalMediaItems().map {
it.getLocalLibraryItem()
}
var jsobj = JSObject()
jsobj.put("localLibraryItems", jacksonObjectMapper().writeValueAsString(localLibraryItems))
call.resolve(jsobj)
}
}
@PluginMethod
fun getLocalLibraryItem_WV(call:PluginCall) {
var id = call.getString("id", "").toString()
GlobalScope.launch(Dispatchers.IO) {
var mediaItem = getLocalMediaItem(id)
var localLibraryItem = mediaItem?.getLocalLibraryItem()
if (localLibraryItem == null) {
call.resolve()
} else {
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(localLibraryItem)))
}
}
}
@PluginMethod @PluginMethod
fun setCurrentServerConnectionConfig_WV(call:PluginCall) { fun setCurrentServerConnectionConfig_WV(call:PluginCall) {
var serverConnectionConfigId = call.getString("id", "").toString() var serverConnectionConfigId = call.getString("id", "").toString()

View file

@ -18,6 +18,18 @@ data class DeviceData(
var lastServerConnectionConfigId:String? var lastServerConnectionConfigId:String?
) )
@JsonIgnoreProperties(ignoreUnknown = true)
data class LocalLibraryItem(
var id:String,
var folderId:String,
var absolutePath:String,
var isInvalid:Boolean,
var mediaType:String,
var media:MediaType,
var localFiles:MutableList<LocalFile>,
var isLocal:Boolean
)
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class LocalMediaItem( data class LocalMediaItem(
var id:String, var id:String,
@ -40,6 +52,13 @@ data class LocalMediaItem(
return total return total
} }
@JsonIgnore
fun getTotalSize():Long {
var total = 0L
localFiles.forEach { total += it.size }
return total
}
@JsonIgnore @JsonIgnore
fun getMediaMetadata():MediaTypeMetadata { fun getMediaMetadata():MediaTypeMetadata {
return if (mediaType == "book") { return if (mediaType == "book") {
@ -54,7 +73,36 @@ data class LocalMediaItem(
var sessionId = "play-${UUID.randomUUID()}" var sessionId = "play-${UUID.randomUUID()}"
var mediaMetadata = getMediaMetadata() var mediaMetadata = getMediaMetadata()
return PlaybackSession(sessionId,null,null,null, mediaType, mediaMetadata, mutableListOf(), name, "author name here",null,getDuration(),PLAYMETHOD_LOCAL,audioTracks,0.0,null,this,null,null) var chapters = getAudiobookChapters()
var authorName = "Unknown"
if (mediaType == "book") {
var bookMetadata = mediaMetadata as BookMetadata
authorName = bookMetadata?.authorName ?: "Unknown"
}
return PlaybackSession(sessionId,null,null,null, mediaType, mediaMetadata, chapters, name, authorName,null,getDuration(),PLAYMETHOD_LOCAL,audioTracks,0.0,null,this,null,null)
}
@JsonIgnore
fun getAudiobookChapters():List<BookChapter> {
if (mediaType != "book" || audioTracks.isEmpty()) return mutableListOf()
if (audioTracks.size == 1) { // Single track audiobook look for chapters from ffprobe
return audioTracks[0].audioProbeResult?.getBookChapters() ?: mutableListOf()
}
// Multi-track make chapters from tracks
return audioTracks.map { it.getBookChapter() }
}
@JsonIgnore
fun getLocalLibraryItem():LocalLibraryItem {
var mediaMetadata = getMediaMetadata()
if (mediaType == "book") {
var chapters = getAudiobookChapters()
var book = Book(mediaMetadata as BookMetadata, coverContentUrl, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration())
return LocalLibraryItem(id, folderId, absolutePath, false,mediaType, book, localFiles, true)
} else {
var podcast = Podcast(mediaMetadata as PodcastMetadata, coverContentUrl, mutableListOf(), mutableListOf(), false)
return LocalLibraryItem(id, folderId, absolutePath, false, mediaType, podcast,localFiles,true)
}
} }
} }

View file

@ -23,7 +23,7 @@ class PlaybackSession(
var episodeId:String?, var episodeId:String?,
var mediaType:String, var mediaType:String,
var mediaMetadata:MediaTypeMetadata, var mediaMetadata:MediaTypeMetadata,
var chapters:MutableList<BookChapter>, var chapters:List<BookChapter>,
var displayTitle: String?, var displayTitle: String?,
var displayAuthor: String?, var displayAuthor: String?,
var coverPath:String?, var coverPath:String?,

View file

@ -11,9 +11,6 @@ import com.arthenica.ffmpegkit.Level
import com.audiobookshelf.app.data.* import com.audiobookshelf.app.data.*
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue import com.fasterxml.jackson.module.kotlin.readValue
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
class FolderScanner(var ctx: Context) { class FolderScanner(var ctx: Context) {
private val tag = "FolderScanner" private val tag = "FolderScanner"
@ -61,7 +58,7 @@ class FolderScanner(var ctx: Context) {
Log.d(tag, "Iterating over Folder Found ${it.name} | ${it.getSimplePath(ctx)} | URI: ${it.uri}") Log.d(tag, "Iterating over Folder Found ${it.name} | ${it.getSimplePath(ctx)} | URI: ${it.uri}")
var itemFolderName = it.name ?: "" var itemFolderName = it.name ?: ""
var itemId = DeviceManager.getBase64Id(it.id) var itemId = "local_" + DeviceManager.getBase64Id(it.id)
var existingMediaItem = existingMediaItems.find { emi -> emi.id == itemId } var existingMediaItem = existingMediaItems.find { emi -> emi.id == itemId }
var existingLocalFiles = existingMediaItem?.localFiles ?: mutableListOf() var existingLocalFiles = existingMediaItem?.localFiles ?: mutableListOf()
@ -130,7 +127,7 @@ class FolderScanner(var ctx: Context) {
audioTrackToAdd = existingAudioTrack audioTrackToAdd = existingAudioTrack
} else { } else {
// Create new audio track // Create new audio track
var track = AudioTrack(index, startOffset, audioProbeResult.duration, filename, localFile.contentUrl, mimeType, true, localFileId, audioProbeResult) var track = AudioTrack(index, startOffset, audioProbeResult.duration, filename, localFile.contentUrl, mimeType, null, true, localFileId, audioProbeResult)
audioTrackToAdd = track audioTrackToAdd = track
} }

View file

@ -175,15 +175,6 @@ export default {
.catch((error) => { .catch((error) => {
console.error('TEST failed', error) console.error('TEST failed', error)
}) })
},
async playLocalItem(localMediaItemId) {
MyNativeAudio.playLocalLibraryItem({ localMediaItemId, playWhenReady: true })
.then((data) => {
console.log('TEST library item play response', JSON.stringify(data))
})
.catch((error) => {
console.error('TEST failed', error)
})
} }
}, },
mounted() { mounted() {
@ -195,7 +186,6 @@ export default {
this.setListeners() this.setListeners()
this.$eventBus.$on('play-item', this.playLibraryItem) this.$eventBus.$on('play-item', this.playLibraryItem)
this.$eventBus.$on('play-local-item', this.playLocalItem)
this.$eventBus.$on('close-stream', this.closeStreamOnly) this.$eventBus.$on('close-stream', this.closeStreamOnly)
this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated }) this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated })
}, },
@ -211,7 +201,6 @@ export default {
// this.$server.socket.off('stream_reset', this.streamReset) // this.$server.socket.off('stream_reset', this.streamReset)
// } // }
this.$eventBus.$off('play-item', this.playLibraryItem) this.$eventBus.$off('play-item', this.playLibraryItem)
this.$eventBus.$off('play-local-item', this.playLocalItem)
this.$eventBus.$off('close-stream', this.closeStreamOnly) this.$eventBus.$off('close-stream', this.closeStreamOnly)
this.$store.commit('user/removeSettingsListener', 'streamContainer') this.$store.commit('user/removeSettingsListener', 'streamContainer')
} }

View file

@ -17,11 +17,11 @@
<div v-if="booksInSeries" class="absolute z-20 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ booksInSeries }}</div> <div v-if="booksInSeries" class="absolute z-20 top-1.5 right-1.5 rounded-md leading-3 text-sm p-1 font-semibold text-white flex items-center justify-center" style="background-color: #cd9d49dd">{{ booksInSeries }}</div>
<div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10"> <div class="w-full h-full absolute top-0 left-0 rounded overflow-hidden z-10">
<div v-show="audiobook && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }"> <div v-show="libraryItem && !imageReady" class="absolute top-0 left-0 w-full h-full flex items-center justify-center" :style="{ padding: sizeMultiplier * 0.5 + 'rem' }">
<p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p> <p :style="{ fontSize: sizeMultiplier * 0.8 + 'rem' }" class="font-book text-gray-300 text-center">{{ title }}</p>
</div> </div>
<img v-show="audiobook" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" /> <img v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
<!-- Placeholder Cover Title & Author --> <!-- Placeholder Cover Title & Author -->
<div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }"> <div v-if="!hasCover" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex items-center justify-center" :style="{ padding: placeholderCoverPadding + 'rem' }">
@ -52,6 +52,8 @@
</template> </template>
<script> <script>
import { Capacitor } from '@capacitor/core'
export default { export default {
props: { props: {
index: Number, index: Number,
@ -78,7 +80,7 @@ export default {
data() { data() {
return { return {
isProcessingReadUpdate: false, isProcessingReadUpdate: false,
audiobook: null, libraryItem: null,
imageReady: false, imageReady: false,
rescanning: false, rescanning: false,
selected: false, selected: false,
@ -90,7 +92,7 @@ export default {
bookMount: { bookMount: {
handler(newVal) { handler(newVal) {
if (newVal) { if (newVal) {
this.audiobook = newVal this.libraryItem = newVal
} }
} }
} }
@ -100,7 +102,11 @@ export default {
return this.store.state.showExperimentalFeatures return this.store.state.showExperimentalFeatures
}, },
_libraryItem() { _libraryItem() {
return this.audiobook || {} return this.libraryItem || {}
},
isLocal() {
// Is local library item
return !!this._libraryItem.isLocal
}, },
media() { media() {
return this._libraryItem.media || {} return this._libraryItem.media || {}
@ -112,6 +118,10 @@ export default {
return '/book_placeholder.jpg' return '/book_placeholder.jpg'
}, },
bookCoverSrc() { bookCoverSrc() {
if (this.isLocal) {
if (this.media.coverPath) return Capacitor.convertFileSrc(this.media.coverPath)
return this.placeholderUrl
}
return this.store.getters['globals/getLibraryItemCoverSrc'](this._libraryItem, this.placeholderUrl) return this.store.getters['globals/getLibraryItemCoverSrc'](this._libraryItem, this.placeholderUrl)
}, },
libraryItemId() { libraryItemId() {
@ -225,8 +235,8 @@ export default {
return this._libraryItem.hasInvalidParts return this._libraryItem.hasInvalidParts
}, },
errorText() { errorText() {
if (this.isMissing) return 'Audiobook directory is missing!' if (this.isMissing) return 'Item directory is missing!'
else if (this.isInvalid) return 'Audiobook has no audio tracks & ebook' else if (this.isInvalid) return 'Item has no media files'
var txt = '' var txt = ''
if (this.hasMissingParts) { if (this.hasMissingParts) {
txt = `${this.hasMissingParts} missing parts.` txt = `${this.hasMissingParts} missing parts.`
@ -307,7 +317,7 @@ export default {
if (!val) this.selected = false if (!val) this.selected = false
}, },
setEntity(libraryItem) { setEntity(libraryItem) {
this.audiobook = libraryItem this.libraryItem = libraryItem
}, },
clickCard(e) { clickCard(e) {
if (this.isSelectionMode) { if (this.isSelectionMode) {
@ -323,7 +333,7 @@ export default {
} }
}, },
editClick() { editClick() {
this.$emit('edit', this.audiobook) this.$emit('edit', this.libraryItem)
}, },
toggleFinished() { toggleFinished() {
var updatePayload = { var updatePayload = {
@ -369,27 +379,27 @@ export default {
}, },
showEditModalTracks() { showEditModalTracks() {
// More menu func // More menu func
this.store.commit('showEditModalOnTab', { libraryItem: this.audiobook, tab: 'tracks' }) this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'tracks' })
}, },
showEditModalMatch() { showEditModalMatch() {
// More menu func // More menu func
this.store.commit('showEditModalOnTab', { libraryItem: this.audiobook, tab: 'match' }) this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'match' })
}, },
showEditModalDownload() { showEditModalDownload() {
// More menu func // More menu func
this.store.commit('showEditModalOnTab', { libraryItem: this.audiobook, tab: 'download' }) this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'download' })
}, },
openCollections() { openCollections() {
this.store.commit('setSelectedLibraryItem', this.audiobook) this.store.commit('setSelectedLibraryItem', this.libraryItem)
this.store.commit('globals/setShowUserCollectionsModal', true) this.store.commit('globals/setShowUserCollectionsModal', true)
}, },
clickReadEBook() { clickReadEBook() {
this.store.commit('showEReader', this.audiobook) this.store.commit('showEReader', this.libraryItem)
}, },
selectBtnClick() { selectBtnClick() {
if (this.processingBatch) return if (this.processingBatch) return
this.selected = !this.selected this.selected = !this.selected
this.$emit('select', this.audiobook) this.$emit('select', this.libraryItem)
}, },
play() { play() {
var eventBus = this.$eventBus || this.$nuxt.$eventBus var eventBus = this.$eventBus || this.$nuxt.$eventBus

View file

@ -33,6 +33,8 @@
</template> </template>
<script> <script>
import { Capacitor } from '@capacitor/core'
export default { export default {
props: { props: {
libraryItem: { libraryItem: {
@ -60,6 +62,10 @@ export default {
} }
}, },
computed: { computed: {
isLocal() {
if (!this.libraryItem) return false
return this.libraryItem.isLocal
},
squareAspectRatio() { squareAspectRatio() {
return this.bookCoverAspectRatio === 1 return this.bookCoverAspectRatio === 1
}, },
@ -98,6 +104,10 @@ export default {
return '/book_placeholder.jpg' return '/book_placeholder.jpg'
}, },
fullCoverUrl() { fullCoverUrl() {
if (this.isLocal) {
if (this.hasCover) return Capacitor.convertFileSrc(this.cover)
return this.placeholderUrl
}
if (this.downloadCover) return this.downloadCover if (this.downloadCover) return this.downloadCover
if (!this.libraryItem) return null if (!this.libraryItem) return null
var store = this.$store || this.$nuxt.$store var store = this.$store || this.$nuxt.$store

View file

@ -1,8 +1,10 @@
<template> <template>
<div class="w-full h-full min-h-full relative"> <div class="w-full h-full min-h-full relative">
<div v-if="!loading" class="w-full">
<template v-for="(shelf, index) in shelves"> <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 }" /> <bookshelf-shelf :key="shelf.id" :label="shelf.label" :entities="shelf.entities" :type="shelf.type" :style="{ zIndex: shelves.length - index }" />
</template> </template>
</div>
<div v-if="!shelves.length" class="absolute top-0 left-0 w-full h-full flex items-center justify-center"> <div v-if="!shelves.length" class="absolute top-0 left-0 w-full h-full flex items-center justify-center">
<div> <div>
@ -123,8 +125,48 @@ export default {
} }
}, },
methods: { methods: {
async getLocalMediaItemCategories() {
var localMedia = await this.$db.getLocalLibraryItems()
if (!localMedia || !localMedia.length) return []
console.log('Got local library items', localMedia.length)
var categories = []
var books = []
var podcasts = []
localMedia.forEach((item) => {
if (item.mediaType == 'book') {
books.push(item)
} else if (item.mediaType == 'podcast') {
podcasts.push(item)
}
})
if (books.length) {
categories.push({
id: 'local-books',
label: 'Local Books',
type: 'book',
entities: books
})
}
if (podcasts.length) {
categories.push({
id: 'local-podcasts',
label: 'Local Podcasts',
type: 'podcast',
entities: podcasts
})
}
return categories
},
async fetchCategories() { async fetchCategories() {
if (!this.currentLibraryId) return null this.loading = true
this.shelves = []
var localCategories = await this.getLocalMediaItemCategories()
console.log('Category shelves', localCategories.length)
this.shelves = this.shelves.concat(localCategories)
if (this.user || !this.currentLibraryId) {
var categories = await this.$axios var categories = await this.$axios
.$get(`/api/libraries/${this.currentLibraryId}/personalized?minified=1`) .$get(`/api/libraries/${this.currentLibraryId}/personalized?minified=1`)
.then((data) => { .then((data) => {
@ -134,8 +176,9 @@ export default {
console.error('Failed to fetch categories', error) console.error('Failed to fetch categories', error)
return [] return []
}) })
this.shelves = categories this.shelves = this.shelves.concat(categories)
console.log('Shelves', this.shelves) }
this.loading = false
}, },
// async socketInit(isConnected) { // async socketInit(isConnected) {
// if (isConnected && this.currentLibraryId) { // if (isConnected && this.currentLibraryId) {
@ -150,15 +193,13 @@ export default {
async libraryChanged(libid) { async libraryChanged(libid) {
if (this.isSocketConnected && this.currentLibraryId) { if (this.isSocketConnected && this.currentLibraryId) {
await this.fetchCategories() await this.fetchCategories()
} else {
this.shelves = this.downloadOnlyShelves
}
},
downloadsLoaded() {
if (!this.isSocketConnected) {
this.shelves = this.downloadOnlyShelves
} }
}, },
// downloadsLoaded() {
// if (!this.isSocketConnected) {
// this.shelves = this.downloadOnlyShelves
// }
// },
audiobookAdded(audiobook) { audiobookAdded(audiobook) {
console.log('Audiobook added', audiobook) console.log('Audiobook added', audiobook)
// TODO: Check if audiobook would be on this shelf // TODO: Check if audiobook would be on this shelf
@ -201,28 +242,15 @@ export default {
} }
}) })
}, },
audiobookRemoved(audiobook) {
this.removeBookFromShelf(audiobook)
},
audiobooksAdded(audiobooks) {
console.log('audiobooks added', audiobooks)
// TODO: Check if audiobook would be on this shelf
this.fetchCategories()
},
audiobooksUpdated(audiobooks) {
audiobooks.forEach((ab) => {
this.audiobookUpdated(ab)
})
},
initListeners() { initListeners() {
// this.$server.on('initialized', this.socketInit) // this.$server.on('initialized', this.socketInit)
this.$eventBus.$on('library-changed', this.libraryChanged) this.$eventBus.$on('library-changed', this.libraryChanged)
this.$eventBus.$on('downloads-loaded', this.downloadsLoaded) // this.$eventBus.$on('downloads-loaded', this.downloadsLoaded)
}, },
removeListeners() { removeListeners() {
// this.$server.off('initialized', this.socketInit) // this.$server.off('initialized', this.socketInit)
this.$eventBus.$off('library-changed', this.libraryChanged) this.$eventBus.$off('library-changed', this.libraryChanged)
this.$eventBus.$off('downloads-loaded', this.downloadsLoaded) // this.$eventBus.$off('downloads-loaded', this.downloadsLoaded)
} }
}, },
mounted() { mounted() {

View file

@ -27,7 +27,21 @@
</div> </div>
</div> </div>
<div v-if="(isConnected && (showPlay || showRead)) || isDownloadPlayable" class="flex mt-4 -mr-2"> <div v-if="isLocal" class="flex mt-4 -mr-2">
<ui-btn color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mr-2" :padding-x="4" @click="playClick">
<span v-show="!isPlaying" class="material-icons">play_arrow</span>
<span class="px-1 text-sm">{{ isPlaying ? 'Playing' : 'Play Local' }}</span>
</ui-btn>
<ui-btn v-if="showRead && isConnected" color="info" class="flex items-center justify-center mr-2" :class="showPlay ? '' : 'flex-grow'" :padding-x="2" @click="readBook">
<span class="material-icons">auto_stories</span>
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
</ui-btn>
<ui-btn v-if="isConnected && showPlay && !isIos" color="primary" class="flex items-center justify-center" :padding-x="2" @click="downloadClick">
<span class="material-icons">download</span>
<!-- <span class="material-icons" :class="downloadObj ? 'animate-pulse' : ''">{{ downloadObj ? (isDownloading || isDownloadPreparing ? 'downloading' : 'download_done') : 'download' }}</span> -->
</ui-btn>
</div>
<div v-else-if="(isConnected && (showPlay || showRead)) || isDownloadPlayable" class="flex mt-4 -mr-2">
<ui-btn v-if="showPlay" color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mr-2" :padding-x="4" @click="playClick"> <ui-btn v-if="showPlay" color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mr-2" :padding-x="4" @click="playClick">
<span v-show="!isPlaying" class="material-icons">play_arrow</span> <span v-show="!isPlaying" class="material-icons">play_arrow</span>
<span class="px-1 text-sm">{{ isPlaying ? (isStreaming ? 'Streaming' : 'Playing') : isDownloadPlayable ? 'Play local' : 'Play stream' }}</span> <span class="px-1 text-sm">{{ isPlaying ? (isStreaming ? 'Streaming' : 'Playing') : isDownloadPlayable ? 'Play local' : 'Play stream' }}</span>
@ -37,7 +51,8 @@
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span> <span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
</ui-btn> </ui-btn>
<ui-btn v-if="isConnected && showPlay && !isIos" color="primary" class="flex items-center justify-center" :padding-x="2" @click="downloadClick"> <ui-btn v-if="isConnected && showPlay && !isIos" color="primary" class="flex items-center justify-center" :padding-x="2" @click="downloadClick">
<span class="material-icons" :class="downloadObj ? 'animate-pulse' : ''">{{ downloadObj ? (isDownloading || isDownloadPreparing ? 'downloading' : 'download_done') : 'download' }}</span> <span class="material-icons">download</span>
<!-- <span class="material-icons" :class="downloadObj ? 'animate-pulse' : ''">{{ downloadObj ? (isDownloading || isDownloadPreparing ? 'downloading' : 'download_done') : 'download' }}</span> -->
</ui-btn> </ui-btn>
</div> </div>
</div> </div>
@ -61,16 +76,13 @@ export default {
var libraryItemId = params.id var libraryItemId = params.id
var libraryItem = null var libraryItem = null
if (store.state.user.serverConnectionConfig) { if (libraryItemId.startsWith('local')) {
libraryItem = await app.$db.getLocalLibraryItem(libraryItemId)
} else if (store.state.user.serverConnectionConfig) {
libraryItem = await app.$axios.$get(`/api/items/${libraryItemId}?expanded=1`).catch((error) => { libraryItem = await app.$axios.$get(`/api/items/${libraryItemId}?expanded=1`).catch((error) => {
console.error('Failed', error) console.error('Failed', error)
return false return false
}) })
} else {
var download = store.getters['downloads/getDownload'](libraryItemId)
if (download) {
libraryItem = download.libraryItem
}
} }
if (!libraryItem) { if (!libraryItem) {
@ -91,6 +103,9 @@ export default {
isIos() { isIos() {
return this.$platform === 'ios' return this.$platform === 'ios'
}, },
isLocal() {
return this.libraryItem.isLocal
},
isConnected() { isConnected() {
return this.$store.state.socketConnected return this.$store.state.socketConnected
}, },
@ -182,18 +197,19 @@ export default {
if (!this.ebookFile) return null if (!this.ebookFile) return null
return this.ebookFile.ebookFormat return this.ebookFile.ebookFormat
}, },
isDownloadPreparing() { // isDownloadPreparing() {
return this.downloadObj ? this.downloadObj.isPreparing : false // return this.downloadObj ? this.downloadObj.isPreparing : false
}, // },
isDownloadPlayable() { isDownloadPlayable() {
return this.downloadObj && !this.isDownloading && !this.isDownloadPreparing return false
}, // return this.downloadObj && !this.isDownloading && !this.isDownloadPreparing
downloadedCover() {
return this.downloadObj ? this.downloadObj.cover : null
},
downloadObj() {
return this.$store.getters['downloads/getDownload'](this.libraryItemId)
}, },
// downloadedCover() {
// return this.downloadObj ? this.downloadObj.cover : null
// },
// downloadObj() {
// return this.$store.getters['downloads/getDownload'](this.libraryItemId)
// },
hasStoragePermission() { hasStoragePermission() {
return this.$store.state.hasStoragePermission return this.$store.state.hasStoragePermission
} }
@ -264,7 +280,7 @@ export default {
this.download() this.download()
}, },
async download(selectedLocalFolder = null) { async download(selectedLocalFolder = null) {
if (!this.numTracks || this.downloadObj) { if (!this.numTracks) {
return return
} }

View file

@ -81,7 +81,7 @@ export default {
} }
}, },
play(mediaItem) { play(mediaItem) {
this.$eventBus.$emit('play-local-item', mediaItem.id) this.$eventBus.$emit('play-item', mediaItem.id)
}, },
async scanFolder(forceAudioProbe = false) { async scanFolder(forceAudioProbe = false) {
this.isScanning = true this.isScanning = true

View file

@ -87,6 +87,22 @@ class DbService {
return data.localMediaItems return data.localMediaItems
}) })
} }
getLocalLibraryItems() {
if (isWeb) return []
return DbManager.getLocalLibraryItems_WV().then((data) => {
console.log('Loaded all local media items', JSON.stringify(data))
if (data.localLibraryItems && typeof data.localLibraryItems == 'string') {
return JSON.parse(data.localLibraryItems)
}
return data.localLibraryItems
})
}
getLocalLibraryItem(id) {
if (isWeb) return null
return DbManager.getLocalLibraryItem_WV({ id })
}
} }
export default ({ app, store }, inject) => { export default ({ app, store }, inject) => {