Update folder scanner and db to store LocalLibraryItem objects instead of LocalMediaItem objects, some ui fixes and audio player service binding fix

This commit is contained in:
advplyr 2022-04-05 19:44:14 -05:00
parent 77ef0c119b
commit 12de187b7a
22 changed files with 248 additions and 158 deletions

View file

@ -71,7 +71,7 @@
<service
android:exported="true"
android:enabled="true"
android:name=".PlayerNotificationService">
android:name=".player.PlayerNotificationService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService"/>
</intent-filter>

View file

@ -10,7 +10,6 @@ import androidx.core.app.ActivityCompat
import com.anggrayudi.storage.SimpleStorage
import com.anggrayudi.storage.SimpleStorageHelper
import com.audiobookshelf.app.data.AbsDatabase
import com.audiobookshelf.app.data.DbManager
import com.audiobookshelf.app.player.PlayerNotificationService
import com.audiobookshelf.app.plugins.AbsDownloader
import com.audiobookshelf.app.plugins.AbsAudioPlayer
@ -18,7 +17,6 @@ import com.audiobookshelf.app.plugins.AbsFileSystem
import com.getcapacitor.BridgeActivity
import io.paperdb.Paper
class MainActivity : BridgeActivity() {
private val tag = "MainActivity"
@ -87,6 +85,7 @@ class MainActivity : BridgeActivity() {
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
Log.d(tag, "onPostCreate MainActivity")
mConnection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName) {
@ -97,7 +96,6 @@ class MainActivity : BridgeActivity() {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
Log.d(tag, "Service Connected $name")
mBounded = true
val mLocalBinder = service as PlayerNotificationService.LocalBinder
foregroundService = mLocalBinder.getService()
@ -109,8 +107,10 @@ class MainActivity : BridgeActivity() {
}
}
val startIntent = Intent(this, PlayerNotificationService::class.java)
bindService(startIntent, mConnection as ServiceConnection, Context.BIND_AUTO_CREATE);
Intent(this, PlayerNotificationService::class.java).also { intent ->
Log.d(tag, "Binding PlayerNotificationService")
bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
}
}

View file

@ -30,28 +30,68 @@ data class LibraryItem(
JsonSubTypes.Type(Book::class),
JsonSubTypes.Type(Podcast::class)
)
open class MediaType {}
open class MediaType(var metadata:MediaTypeMetadata, var coverPath:String?) {
@JsonIgnore
open fun getAudioTracks():List<AudioTrack> { return mutableListOf() }
@JsonIgnore
open fun setAudioTracks(audioTracks:List<AudioTrack>) { }
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class Podcast(
var metadata:PodcastMetadata,
var coverPath:String?,
class Podcast(
metadata:PodcastMetadata,
coverPath:String?,
var tags:MutableList<String>,
var episodes:MutableList<PodcastEpisode>,
var autoDownloadEpisodes:Boolean
) : MediaType()
) : MediaType(metadata, coverPath) {
@JsonIgnore
override fun getAudioTracks():List<AudioTrack> {
var tracks = episodes.map { it.audioTrack }
return tracks.filterNotNull()
}
@JsonIgnore
override fun setAudioTracks(audioTracks:List<AudioTrack>) {
// Remove episodes no longer there in tracks
episodes = episodes.filter { ep ->
audioTracks.find { it.localFileId == ep.audioTrack?.localFileId } != null
} as MutableList<PodcastEpisode>
// Add new episodes
audioTracks.forEach { at ->
if (episodes.find{ it.audioTrack?.localFileId == at.localFileId } == null) {
var newEpisode = PodcastEpisode("local_" + at.localFileId,episodes.size + 1,null,null,at.title,null,null,null,at)
episodes.add(newEpisode)
}
}
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class Book(
var metadata:BookMetadata,
var coverPath:String?,
class Book(
metadata:BookMetadata,
coverPath:String?,
var tags:List<String>,
var audioFiles:List<AudioFile>,
var chapters:List<BookChapter>,
var tracks:List<AudioTrack>?,
var size:Long?,
var duration:Double?
) : MediaType()
) : MediaType(metadata, coverPath) {
@JsonIgnore
override fun getAudioTracks():List<AudioTrack> {
return tracks ?: mutableListOf()
}
@JsonIgnore
override fun setAudioTracks(audioTracks:List<AudioTrack>) {
tracks = audioTracks
var totalDuration = 0.0
tracks?.forEach {
totalDuration += it.duration
}
duration = totalDuration
}
}
// This auto-detects whether it is a Book or Podcast
@JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION)
@ -59,11 +99,11 @@ data class Book(
JsonSubTypes.Type(BookMetadata::class),
JsonSubTypes.Type(PodcastMetadata::class)
)
open class MediaTypeMetadata {}
open class MediaTypeMetadata(var title:String) {}
@JsonIgnoreProperties(ignoreUnknown = true)
data class BookMetadata(
var title:String,
class BookMetadata(
title:String,
var subtitle:String?,
var authors:MutableList<Author>,
var narrators:MutableList<String>,
@ -81,15 +121,15 @@ data class BookMetadata(
var authorNameLF:String?,
var narratorName:String?,
var seriesName:String?
) : MediaTypeMetadata()
) : MediaTypeMetadata(title)
@JsonIgnoreProperties(ignoreUnknown = true)
data class PodcastMetadata(
var title:String,
class PodcastMetadata(
title:String,
var author:String?,
var feedUrl:String?,
var genres:MutableList<String>
) : MediaTypeMetadata()
) : MediaTypeMetadata(title)
@JsonIgnoreProperties(ignoreUnknown = true)
data class Author(
@ -107,7 +147,8 @@ data class PodcastEpisode(
var title:String?,
var subtitle:String?,
var description:String?,
var audioFile:AudioFile
var audioFile:AudioFile?,
var audioTrack:AudioTrack?
)
@JsonIgnoreProperties(ignoreUnknown = true)

View file

@ -14,11 +14,11 @@ class DbManager {
Paper.book("device").write("data", deviceData)
}
fun getLocalMediaItems():MutableList<LocalMediaItem> {
var localMediaItems:MutableList<LocalMediaItem> = mutableListOf()
Paper.book("localMediaItems").allKeys.forEach {
var localMediaItem:LocalMediaItem? = Paper.book("localMediaItems").read(it)
if (localMediaItem != null) {
fun getLocalLibraryItems():MutableList<LocalLibraryItem> {
var localLibraryItems:MutableList<LocalLibraryItem> = mutableListOf()
Paper.book("localLibraryItems").allKeys.forEach {
var localLibraryItem:LocalLibraryItem? = Paper.book("localLibraryItems").read(it)
if (localLibraryItem != null) {
// TODO: Check to make sure all file paths exist
// if (localMediaItem.coverContentUrl != null) {
// var file = DocumentFile.fromSingleUri(ctx)
@ -29,31 +29,31 @@ class DbManager {
// localMediaItems.add(localMediaItem)
// }
// } else {
localMediaItems.add(localMediaItem)
localLibraryItems.add(localLibraryItem)
// }
}
}
return localMediaItems
return localLibraryItems
}
fun getLocalMediaItemsInFolder(folderId:String):List<LocalMediaItem> {
var localMediaItems = getLocalMediaItems()
return localMediaItems.filter {
fun getLocalLibraryItemsInFolder(folderId:String):List<LocalLibraryItem> {
var localLibraryItems = getLocalLibraryItems()
return localLibraryItems.filter {
it.folderId == folderId
}
}
fun getLocalMediaItem(localMediaItemId:String):LocalMediaItem? {
return Paper.book("localMediaItems").read(localMediaItemId)
fun getLocalLibraryItem(localLibraryItemId:String):LocalLibraryItem? {
return Paper.book("localLibraryItems").read(localLibraryItemId)
}
fun removeLocalMediaItem(localMediaItemId:String) {
Paper.book("localMediaItems").delete(localMediaItemId)
fun removeLocalLibraryItem(localLibraryItemId:String) {
Paper.book("localLibraryItems").delete(localLibraryItemId)
}
fun saveLocalMediaItems(localMediaItems:List<LocalMediaItem>) {
localMediaItems.map {
Paper.book("localMediaItems").write(it.id, it)
fun saveLocalLibraryItems(localLibraryItems:List<LocalLibraryItem>) {
localLibraryItems.map {
Paper.book("localLibraryItems").write(it.id, it)
}
}
@ -77,9 +77,9 @@ class DbManager {
}
fun removeLocalFolder(folderId:String) {
var localMediaItems = getLocalMediaItemsInFolder(folderId)
localMediaItems.forEach {
Paper.book("localMediaItems").delete(it.id)
var localLibraryItems = getLocalLibraryItemsInFolder(folderId)
localLibraryItems.forEach {
Paper.book("localLibraryItems").delete(it.id)
}
Paper.book("localFolders").delete(folderId)
}

View file

@ -22,14 +22,54 @@ data class DeviceData(
@JsonIgnoreProperties(ignoreUnknown = true)
data class LocalLibraryItem(
var id:String,
var libraryItemId:String?,
var folderId:String,
var absolutePath:String,
var isInvalid:Boolean,
var mediaType:String,
var media:MediaType,
var localFiles:MutableList<LocalFile>,
var coverContentUrl:String?,
var coverAbsolutePath:String?,
var isLocal:Boolean
)
) {
@JsonIgnore
fun getDuration():Double {
var total = 0.0
var audioTracks = media.getAudioTracks()
audioTracks.forEach{ total += it.duration }
return total
}
@JsonIgnore
fun updateFromScan(audioTracks:List<AudioTrack>, _localFiles:MutableList<LocalFile>) {
media.setAudioTracks(audioTracks)
localFiles = _localFiles
if (coverContentUrl != null) {
if (localFiles.find { it.contentUrl == coverContentUrl } == null) {
// Cover was removed
coverContentUrl = null
coverAbsolutePath = null
media.coverPath = null
}
}
}
@JsonIgnore
fun getPlaybackSession():PlaybackSession {
var sessionId = "play-${UUID.randomUUID()}"
var mediaMetadata = media.metadata
var chapters = if (mediaType == "book") (media as Book).chapters else mutableListOf()
var authorName = "Unknown"
if (mediaType == "book") {
var bookMetadata = mediaMetadata as BookMetadata
authorName = bookMetadata?.authorName ?: "Unknown"
}
return PlaybackSession(sessionId,null,null,null, mediaType, mediaMetadata, chapters, mediaMetadata.title, authorName,null,getDuration(),PLAYMETHOD_LOCAL, media.getAudioTracks() as MutableList<AudioTrack>,0.0,null,this,null,null)
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class LocalMediaItem(
@ -69,20 +109,6 @@ data class LocalMediaItem(
}
}
@JsonIgnore
fun getPlaybackSession():PlaybackSession {
var sessionId = "play-${UUID.randomUUID()}"
var mediaMetadata = getMediaMetadata()
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()
@ -98,11 +124,11 @@ data class LocalMediaItem(
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)
var book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration())
return LocalLibraryItem(id, null, folderId, absolutePath, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true)
} else {
var podcast = Podcast(mediaMetadata as PodcastMetadata, coverContentUrl, mutableListOf(), mutableListOf(), false)
return LocalLibraryItem(id, folderId, absolutePath, false, mediaType, podcast,localFiles,true)
var podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false)
return LocalLibraryItem(id, null, folderId, absolutePath, false, mediaType, podcast,localFiles,coverContentUrl, coverAbsolutePath, true)
}
}
}

View file

@ -6,5 +6,5 @@ data class FolderScanResult(
var itemsRemoved:Int,
var itemsUpToDate:Int,
val localFolder:LocalFolder,
val localMediaItems:List<LocalMediaItem>,
val localLibraryItems:List<LocalLibraryItem>,
)

View file

@ -32,7 +32,7 @@ class PlaybackSession(
var audioTracks:MutableList<AudioTrack>,
var currentTime:Double,
var libraryItem:LibraryItem?,
var localMediaItem:LocalMediaItem?,
var localLibraryItem:LocalLibraryItem?,
var serverUrl:String?,
var token:String?
) {
@ -74,7 +74,7 @@ class PlaybackSession(
@JsonIgnore
fun getCoverUri(): Uri {
if (localMediaItem?.coverContentUrl != null) return Uri.parse(localMediaItem?.coverContentUrl) ?: Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
if (localLibraryItem?.coverContentUrl != null) return Uri.parse(localLibraryItem?.coverContentUrl) ?: Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
if (coverPath == null) return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
return Uri.parse("$serverUrl/api/items/$libraryItemId/cover?token=$token")

View file

@ -15,6 +15,10 @@ import com.fasterxml.jackson.module.kotlin.readValue
class FolderScanner(var ctx: Context) {
private val tag = "FolderScanner"
private fun getLocalLibraryItemId(mediaItemId:String):String {
return "local_" + DeviceManager.getBase64Id(mediaItemId)
}
// TODO: CLEAN this monster! Divide into bite-size methods
fun scanForMediaItems(localFolder:LocalFolder, forceAudioProbe:Boolean):FolderScanResult? {
FFmpegKitConfig.enableLogCallback { log ->
@ -38,32 +42,32 @@ class FolderScanner(var ctx: Context) {
// Search for files in media item folder
var foldersFound = df.search(false, DocumentFileType.FOLDER)
// Match folders found with media items already saved in db
var existingMediaItems = DeviceManager.dbManager.getLocalMediaItemsInFolder(localFolder.id)
// Match folders found with local library items already saved in db
var existingLocalLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id)
// Remove existing items no longer there
existingMediaItems = existingMediaItems.filter { lmi ->
var fileFound = foldersFound.find { f -> lmi.id == DeviceManager.getBase64Id(f.id) }
existingLocalLibraryItems = existingLocalLibraryItems.filter { lli ->
var fileFound = foldersFound.find { f -> lli.id == getLocalLibraryItemId(f.id) }
if (fileFound == null) {
Log.d(tag, "Existing media item is no longer in file system ${lmi.name}")
DeviceManager.dbManager.removeLocalMediaItem(lmi.id)
Log.d(tag, "Existing local library item is no longer in file system ${lli.media.metadata.title}")
DeviceManager.dbManager.removeLocalLibraryItem(lli.id)
mediaItemsRemoved++
}
fileFound != null
}
var mediaItems = mutableListOf<LocalMediaItem>()
var localLibraryItems = mutableListOf<LocalLibraryItem>()
foldersFound.forEach {
Log.d(tag, "Iterating over Folder Found ${it.name} | ${it.getSimplePath(ctx)} | URI: ${it.uri}")
foldersFound.forEach { itemFolder ->
Log.d(tag, "Iterating over Folder Found ${itemFolder.name} | ${itemFolder.getSimplePath(ctx)} | URI: ${itemFolder.uri}")
var itemFolderName = it.name ?: ""
var itemId = "local_" + DeviceManager.getBase64Id(it.id)
var itemFolderName = itemFolder.name ?: ""
var itemId = getLocalLibraryItemId(itemFolder.id)
var existingMediaItem = existingMediaItems.find { emi -> emi.id == itemId }
var existingLocalFiles = existingMediaItem?.localFiles ?: mutableListOf()
var existingAudioTracks = existingMediaItem?.audioTracks ?: mutableListOf()
var isNewOrUpdated = existingMediaItem == null
var existingItem = existingLocalLibraryItems.find { emi -> emi.id == itemId }
var existingLocalFiles = existingItem?.localFiles ?: mutableListOf()
var existingAudioTracks = existingItem?.media?.getAudioTracks() ?: mutableListOf()
var isNewOrUpdated = existingItem == null
var audioTracks = mutableListOf<AudioTrack>()
var localFiles = mutableListOf<LocalFile>()
@ -72,13 +76,14 @@ class FolderScanner(var ctx: Context) {
var coverContentUrl:String? = null
var coverAbsolutePath:String? = null
var filesInFolder = it.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
var filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*"))
var existingLocalFilesRemoved = existingLocalFiles.filter { elf ->
filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder
}
if (existingLocalFilesRemoved.isNotEmpty()) {
Log.d(tag, "${existingLocalFilesRemoved.size} Local files were removed from local media item ${existingMediaItem?.name}")
Log.d(tag, "${existingLocalFilesRemoved.size} Local files were removed from local media item ${existingItem?.media?.metadata?.title}")
isNewOrUpdated = true
}
@ -147,9 +152,12 @@ class FolderScanner(var ctx: Context) {
if (existingLocalFile == null) {
isNewOrUpdated = true
}
if (existingMediaItem != null && existingMediaItem.coverContentUrl == null) {
if (existingItem != null && existingItem.coverContentUrl == null) {
// Existing media item did not have a cover - cover found on scan
isNewOrUpdated = true
existingItem.coverAbsolutePath = localFile.absolutePath
existingItem.coverContentUrl = localFile.contentUrl
existingItem.media.coverPath = localFile.absolutePath
}
// First image file use as cover path
@ -160,30 +168,36 @@ class FolderScanner(var ctx: Context) {
}
}
if (existingMediaItem != null && audioTracks.isEmpty()) {
Log.d(tag, "Local media item ${existingMediaItem.name} no longer has audio tracks - removing item")
DeviceManager.dbManager.removeLocalMediaItem(existingMediaItem.id)
if (existingItem != null && audioTracks.isEmpty()) {
Log.d(tag, "Local library item ${existingItem.media.metadata.title} no longer has audio tracks - removing item")
DeviceManager.dbManager.removeLocalLibraryItem(existingItem.id)
mediaItemsRemoved++
} else if (existingMediaItem != null && !isNewOrUpdated) {
Log.d(tag, "Local media item ${existingMediaItem.name} has no updates")
} else if (existingItem != null && !isNewOrUpdated) {
Log.d(tag, "Local library item ${existingItem.media.metadata.title} has no updates")
mediaItemsUpToDate++
} else if (audioTracks.isNotEmpty()) {
if (existingMediaItem != null) mediaItemsUpdated++
else mediaItemsAdded++
} else if (existingItem != null) {
Log.d(tag, "Updating local library item ${existingItem.media.metadata.title}")
mediaItemsUpdated++
Log.d(tag, "Found local media item named $itemFolderName with ${audioTracks.size} tracks and ${localFiles.size} local files")
var localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, it.uri.toString(), it.getSimplePath(ctx), it.getAbsolutePath(ctx),audioTracks,localFiles,coverContentUrl,coverAbsolutePath)
mediaItems.add(localMediaItem)
existingItem.updateFromScan(audioTracks,localFiles)
localLibraryItems.add(existingItem)
} else if (audioTracks.isNotEmpty()) {
Log.d(tag, "Found local media item named $itemFolderName with ${audioTracks.size} tracks and ${localFiles.size} local files")
mediaItemsAdded++
var localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, itemFolder.uri.toString(), itemFolder.getSimplePath(ctx), itemFolder.getAbsolutePath(ctx),audioTracks,localFiles,coverContentUrl,coverAbsolutePath)
var localLibraryItem = localMediaItem.getLocalLibraryItem()
localLibraryItems.add(localLibraryItem)
}
}
Log.d(tag, "Folder $${localFolder.name} scan Results: $mediaItemsAdded Added | $mediaItemsUpdated Updated | $mediaItemsRemoved Removed | $mediaItemsUpToDate Up-to-date")
return if (mediaItems.isNotEmpty()) {
DeviceManager.dbManager.saveLocalMediaItems(mediaItems)
return if (localLibraryItems.isNotEmpty()) {
DeviceManager.dbManager.saveLocalLibraryItems(localLibraryItems)
var folderMediaItems = DeviceManager.dbManager.getLocalMediaItemsInFolder(localFolder.id) // Get all local media items
FolderScanResult(mediaItemsAdded, mediaItemsUpdated, mediaItemsRemoved, mediaItemsUpToDate, localFolder, folderMediaItems)
var folderLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id) // Get all local media items
FolderScanResult(mediaItemsAdded, mediaItemsUpdated, mediaItemsRemoved, mediaItemsUpToDate, localFolder, folderLibraryItems)
} else {
Log.d(tag, "No Media Items to save")
FolderScanResult(mediaItemsAdded, mediaItemsUpdated, mediaItemsRemoved, mediaItemsUpToDate, localFolder, mutableListOf())

View file

@ -133,8 +133,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.d(tag, "onStartCommand $startId")
isStarted = true
Log.d(tag, "onStartCommand $startId")
return START_STICKY
}

View file

@ -1,8 +1,10 @@
package com.audiobookshelf.app.plugins
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.content.ContextCompat
import com.audiobookshelf.app.MainActivity
import com.audiobookshelf.app.data.PlaybackSession
import com.audiobookshelf.app.device.DeviceManager
@ -75,11 +77,19 @@ class AbsAudioPlayer : Plugin() {
@PluginMethod
fun prepareLibraryItem(call: PluginCall) {
// Need to make sure the player service has been started
if (!PlayerNotificationService.isStarted) {
Log.w(tag, "prepareLibraryItem: PlayerService not started - Starting foreground service --")
Intent(mainActivity, PlayerNotificationService::class.java).also { intent ->
ContextCompat.startForegroundService(mainActivity, intent)
}
}
var libraryItemId = call.getString("libraryItemId", "").toString()
var playWhenReady = call.getBoolean("playWhenReady") == true
if (libraryItemId.startsWith("local")) { // Play local media item
DeviceManager.dbManager.getLocalMediaItem(libraryItemId)?.let {
DeviceManager.dbManager.getLocalLibraryItem(libraryItemId)?.let {
Handler(Looper.getMainLooper()).post() {
Log.d(tag, "Preparing Local Media item ${jacksonObjectMapper().writeValueAsString(it)}")
var playbackSession = it.getPlaybackSession()

View file

@ -48,23 +48,22 @@ class AbsDatabase : Plugin() {
}
@PluginMethod
fun getLocalMediaItemsInFolder(call:PluginCall) {
var folderId = call.getString("folderId", "").toString()
fun getLocalLibraryItem(call:PluginCall) {
var id = call.getString("id", "").toString()
GlobalScope.launch(Dispatchers.IO) {
var localMediaItems = DeviceManager.dbManager.getLocalMediaItemsInFolder(folderId)
var mediaItemsArray = jacksonObjectMapper().writeValueAsString(localMediaItems)
var jsobj = JSObject()
jsobj.put("localMediaItems", mediaItemsArray)
call.resolve(jsobj)
var localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(id)
if (localLibraryItem == null) {
call.resolve()
} else {
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(localLibraryItem)))
}
}
}
@PluginMethod
fun getLocalLibraryItems(call:PluginCall) {
GlobalScope.launch(Dispatchers.IO) {
var localLibraryItems = DeviceManager.dbManager.getLocalMediaItems().map {
it.getLocalLibraryItem()
}
var localLibraryItems = DeviceManager.dbManager.getLocalLibraryItems()
var jsobj = JSObject()
jsobj.put("localLibraryItems", jacksonObjectMapper().writeValueAsString(localLibraryItems))
call.resolve(jsobj)
@ -72,16 +71,14 @@ class AbsDatabase : Plugin() {
}
@PluginMethod
fun getLocalLibraryItem(call:PluginCall) {
var id = call.getString("id", "").toString()
fun getLocalLibraryItemsInFolder(call:PluginCall) {
var folderId = call.getString("folderId", "").toString()
GlobalScope.launch(Dispatchers.IO) {
var mediaItem = DeviceManager.dbManager.getLocalMediaItem(id)
var localLibraryItem = mediaItem?.getLocalLibraryItem()
if (localLibraryItem == null) {
call.resolve()
} else {
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(localLibraryItem)))
}
var localMediaItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(folderId)
var mediaItemsArray = jacksonObjectMapper().writeValueAsString(localMediaItems)
var jsobj = JSObject()
jsobj.put("localLibraryItems", mediaItemsArray)
call.resolve(jsobj)
}
}

View file

@ -118,11 +118,11 @@ class AbsDownloader : Plugin() {
fun getAbMetadataText(libraryItem:LibraryItem):String {
var bookMedia = libraryItem.media as com.audiobookshelf.app.data.Book
var fileString = ";ABMETADATA1\n"
fileString += "#libraryItemId=${libraryItem.id}\n"
fileString += "title=${bookMedia.metadata.title}\n"
fileString += "author=${bookMedia.metadata.authorName}\n"
fileString += "narrator=${bookMedia.metadata.narratorName}\n"
fileString += "series=${bookMedia.metadata.seriesName}\n"
// fileString += "#libraryItemId=${libraryItem.id}\n"
// fileString += "title=${bookMedia.metadata.title}\n"
// fileString += "author=${bookMedia.metadata.authorName}\n"
// fileString += "narrator=${bookMedia.metadata.narratorName}\n"
// fileString += "series=${bookMedia.metadata.seriesName}\n"
return fileString
}

View file

@ -1,5 +1,5 @@
<template>
<div v-if="playbackSession" class="fixed top-0 left-0 layout-wrapper right-0 z-50 pointer-events-none" :class="showFullscreen ? 'fullscreen' : ''">
<div v-if="playbackSession" id="streamContainer" class="fixed top-0 left-0 layout-wrapper right-0 z-50 pointer-events-none" :class="showFullscreen ? 'fullscreen' : ''">
<div v-if="showFullscreen" class="w-full h-full z-10 bg-bg absolute top-0 left-0 pointer-events-auto">
<div class="top-2 left-4 absolute cursor-pointer">
<span class="material-icons text-5xl" @click="collapseFullscreen">expand_more</span>
@ -31,7 +31,7 @@
<div class="cover-wrapper absolute z-30 pointer-events-auto" :class="bookCoverAspectRatio === 1 ? 'square-cover' : ''" @click="clickContainer">
<div class="cover-container bookCoverWrapper bg-black bg-opacity-75 w-full h-full">
<covers-book-cover v-if="libraryItem || localMediaItemCoverSrc" :library-item="libraryItem" :download-cover="localMediaItemCoverSrc" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<covers-book-cover v-if="libraryItem || localLibraryItemCoverSrc" :library-item="libraryItem" :download-cover="localLibraryItemCoverSrc" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</div>
</div>
@ -175,12 +175,12 @@ export default {
libraryItem() {
return this.playbackSession ? this.playbackSession.libraryItem || null : null
},
localMediaItem() {
return this.playbackSession ? this.playbackSession.localMediaItem || null : null
localLibraryItem() {
return this.playbackSession ? this.playbackSession.localLibraryItem || null : null
},
localMediaItemCoverSrc() {
var localMediaItemCover = this.localMediaItem ? this.localMediaItem.coverContentUrl : null
if (localMediaItemCover) return Capacitor.convertFileSrc(localMediaItemCover)
localLibraryItemCoverSrc() {
var localItemCover = this.localLibraryItem ? this.localLibraryItem.coverContentUrl : null
if (localItemCover) return Capacitor.convertFileSrc(localItemCover)
return null
},
playMethod() {

View file

@ -1,8 +1,6 @@
<template>
<div>
<div id="streamContainer">
<app-audio-player ref="audioPlayer" :playing.sync="isPlaying" :bookmarks="bookmarks" :sleep-timer-running="isSleepTimerRunning" :sleep-time-remaining="sleepTimeRemaining" @selectPlaybackSpeed="showPlaybackSpeedModal = true" @updateTime="(t) => (currentTime = t)" @showSleepTimer="showSleepTimer" @showBookmarks="showBookmarks" />
</div>
<app-audio-player ref="audioPlayer" :playing.sync="isPlaying" :bookmarks="bookmarks" :sleep-timer-running="isSleepTimerRunning" :sleep-time-remaining="sleepTimeRemaining" @selectPlaybackSpeed="showPlaybackSpeedModal = true" @updateTime="(t) => (currentTime = t)" @showSleepTimer="showSleepTimer" @showBookmarks="showBookmarks" />
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-rate.sync="playbackSpeed" @update:playbackRate="updatePlaybackSpeed" @change="changePlaybackSpeed" />
<modals-sleep-timer-modal v-model="showSleepTimerModal" :current-time="sleepTimeRemaining" :sleep-timer-running="isSleepTimerRunning" :current-end-of-chapter-time="currentEndOfChapterTime" @change="selectSleepTimeout" @cancel="cancelSleepTimer" @increase="increaseSleepTimer" @decrease="decreaseSleepTimer" />

View file

@ -11,7 +11,7 @@
</button>
<transition name="menu">
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-black-200 shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox">
<ul v-show="showMenu" class="absolute z-10 -mt-px w-full bg-primary border border-gray-600 shadow-lg max-h-56 rounded-b-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" tabindex="-1" role="listbox">
<template v-for="item in items">
<li :key="item.value" class="text-gray-100 select-none relative py-2 cursor-pointer hover:bg-black-400" id="listbox-option-0" role="option" @click="clickedOption(item.value)">
<div class="flex items-center">

View file

@ -97,7 +97,7 @@ export default {
var localCategories = await this.getLocalMediaItemCategories()
this.shelves = this.shelves.concat(localCategories)
if (this.user || !this.currentLibraryId) {
if (this.user && this.currentLibraryId) {
var categories = await this.$axios
.$get(`/api/libraries/${this.currentLibraryId}/personalized?minified=1`)
.then((data) => {

View file

@ -3,7 +3,7 @@
<div class="flex">
<div class="w-32">
<div class="relative">
<covers-book-cover :library-item="libraryItem" :download-cover="downloadedCover" :width="128" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<covers-book-cover :library-item="libraryItem" :width="128" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm z-10" :style="{ width: 128 * progressPercent + 'px' }"></div>
</div>
<div class="flex my-4">

View file

@ -3,23 +3,24 @@
<div class="flex items-center mb-4">
<div class="flex-grow" />
<ui-btn v-if="!removingFolder" :loading="isScanning" small @click="clickScan">Scan</ui-btn>
<ui-btn v-if="!removingFolder && localMediaItems.length" :loading="isScanning" small class="ml-2" color="warning" @click="clickForceRescan">Force Re-Scan</ui-btn>
<ui-btn v-if="!removingFolder && localLibraryItems.length" :loading="isScanning" small class="ml-2" color="warning" @click="clickForceRescan">Force Re-Scan</ui-btn>
<ui-icon-btn class="ml-2" bg-color="error" outlined :loading="removingFolder" icon="delete" @click="clickDeleteFolder" />
</div>
<p class="text-lg mb-0.5 text-white text-opacity-75">Folder: {{ folderName }}</p>
<p class="mb-4 text-xl">Local Media Items ({{ localMediaItems.length }})</p>
<p class="mb-4 text-xl">Local Library Items ({{ localLibraryItems.length }})</p>
<div v-if="isScanning" class="w-full text-center p-4">
<p>Scanning...</p>
</div>
<div v-else class="w-full media-item-container overflow-y-auto">
<template v-for="mediaItem in localMediaItems">
<template v-for="mediaItem in localLibraryItems">
<div :key="mediaItem.id" class="flex my-1">
<div class="w-12 h-12 bg-primary">
<img v-if="mediaItem.coverPathSrc" :src="mediaItem.coverPathSrc" class="w-full h-full object-contain" />
</div>
<div class="flex-grow px-2">
<p>{{ mediaItem.name }}</p>
<p>{{ mediaItem.audioTracks.length }} Tracks</p>
<p>{{ mediaItem.media.metadata.title }}</p>
<p v-if="mediaItem.type == 'book'">{{ mediaItem.media.tracks.length }} Tracks</p>
<p v-else-if="mediaItem.type == 'podcast'">{{ mediaItem.media.episodes.length }} Tracks</p>
</div>
<div class="w-12 h-12 flex items-center justify-center">
<button v-if="!isMissing" class="shadow-sm text-accent flex items-center justify-center rounded-full" @click.stop="play(mediaItem)">
@ -46,7 +47,7 @@ export default {
},
data() {
return {
localMediaItems: [],
localLibraryItems: [],
folder: null,
isScanning: false,
removingFolder: false
@ -66,8 +67,8 @@ export default {
},
async clickDeleteFolder() {
var deleteMessage = 'Are you sure you want to remove this folder? (does not delete anything in your file system)'
if (this.localMediaItems.length) {
deleteMessage = `Are you sure you want to remove this folder and ${this.localMediaItems.length} media items? (does not delete anything in your file system)`
if (this.localLibraryItems.length) {
deleteMessage = `Are you sure you want to remove this folder and ${this.localLibraryItems.length} items? (does not delete anything in your file system)`
}
const { value } = await Dialog.confirm({
title: 'Confirm',
@ -87,7 +88,7 @@ export default {
this.isScanning = true
var response = await AbsFileSystem.scanFolder({ folderId: this.folderId, forceAudioProbe })
if (response && response.localMediaItems) {
if (response && response.localLibraryItems) {
var itemsAdded = response.itemsAdded
var itemsUpdated = response.itemsUpdated
var itemsRemoved = response.itemsRemoved
@ -100,14 +101,14 @@ export default {
this.$toast.info(`Folder scan complete:\n${toastMessages.join(' | ')}`)
// When all items are up-to-date then local media items are not returned
if (response.localMediaItems.length) {
this.localMediaItems = response.localMediaItems.map((mi) => {
if (response.localLibraryItems.length) {
this.localLibraryItems = response.localLibraryItems.map((mi) => {
if (mi.coverContentUrl) {
mi.coverPathSrc = Capacitor.convertFileSrc(mi.coverContentUrl)
}
return mi
})
console.log('Set Local Media Items', this.localMediaItems.length)
console.log('Set Local Media Items', this.localLibraryItems.length)
}
} else {
console.log('No Local media items found')
@ -118,9 +119,9 @@ export default {
var folder = await this.$db.getLocalFolder(this.folderId)
this.folder = folder
var items = (await this.$db.getLocalMediaItemsInFolder(this.folderId)) || []
var items = (await this.$db.getLocalLibraryItemsInFolder(this.folderId)) || []
console.log('Init folder', this.folderId, items)
this.localMediaItems = items.map((lmi) => {
this.localLibraryItems = items.map((lmi) => {
return {
...lmi,
coverPathSrc: lmi.coverContentUrl ? Capacitor.convertFileSrc(lmi.coverContentUrl) : null

View file

@ -59,6 +59,7 @@ export default {
return this.$toast.error('Must select a media type')
}
var folderObj = await AbsFileSystem.selectFolder({ mediaType: this.newFolderMediaType })
if (!folderObj) return
if (folderObj.error) {
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
}

View file

@ -4,6 +4,8 @@ class AbsFileSystemWeb extends WebPlugin {
constructor() {
super()
}
async selectFolder() { }
}
const AbsFileSystem = registerPlugin('AbsFileSystem', {

View file

@ -73,14 +73,14 @@ class DbService {
})
}
getLocalMediaItemsInFolder(folderId) {
getLocalLibraryItemsInFolder(folderId) {
if (isWeb) return []
return AbsDatabase.getLocalMediaItemsInFolder({ folderId }).then((data) => {
console.log('Loaded local media items in folder', JSON.stringify(data))
if (data.localMediaItems && typeof data.localMediaItems == 'string') {
return JSON.parse(data.localMediaItems)
return AbsDatabase.getLocalLibraryItemsInFolder({ folderId }).then((data) => {
console.log('Loaded local library items in folder', JSON.stringify(data))
if (data.localLibraryItems && typeof data.localLibraryItems == 'string') {
return JSON.parse(data.localLibraryItems)
}
return data.localMediaItems
return data.localLibraryItems
})
}

View file

@ -38,7 +38,7 @@ export const getters = {
return state.serverSettings[key]
},
getBookCoverAspectRatio: state => {
if (!state.serverSettings || !state.serverSettings.coverAspectRatio) return 1.6
if (!state.serverSettings || !state.serverSettings.coverAspectRatio) return 1
return state.serverSettings.coverAspectRatio === 0 ? 1.6 : 1
},
}