mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2025-07-13 15:34:50 +02:00
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:
parent
77ef0c119b
commit
12de187b7a
22 changed files with 248 additions and 158 deletions
|
@ -71,7 +71,7 @@
|
||||||
<service
|
<service
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:name=".PlayerNotificationService">
|
android:name=".player.PlayerNotificationService">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.media.browse.MediaBrowserService"/>
|
<action android:name="android.media.browse.MediaBrowserService"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
|
@ -10,7 +10,6 @@ import androidx.core.app.ActivityCompat
|
||||||
import com.anggrayudi.storage.SimpleStorage
|
import com.anggrayudi.storage.SimpleStorage
|
||||||
import com.anggrayudi.storage.SimpleStorageHelper
|
import com.anggrayudi.storage.SimpleStorageHelper
|
||||||
import com.audiobookshelf.app.data.AbsDatabase
|
import com.audiobookshelf.app.data.AbsDatabase
|
||||||
import com.audiobookshelf.app.data.DbManager
|
|
||||||
import com.audiobookshelf.app.player.PlayerNotificationService
|
import com.audiobookshelf.app.player.PlayerNotificationService
|
||||||
import com.audiobookshelf.app.plugins.AbsDownloader
|
import com.audiobookshelf.app.plugins.AbsDownloader
|
||||||
import com.audiobookshelf.app.plugins.AbsAudioPlayer
|
import com.audiobookshelf.app.plugins.AbsAudioPlayer
|
||||||
|
@ -18,7 +17,6 @@ import com.audiobookshelf.app.plugins.AbsFileSystem
|
||||||
import com.getcapacitor.BridgeActivity
|
import com.getcapacitor.BridgeActivity
|
||||||
import io.paperdb.Paper
|
import io.paperdb.Paper
|
||||||
|
|
||||||
|
|
||||||
class MainActivity : BridgeActivity() {
|
class MainActivity : BridgeActivity() {
|
||||||
private val tag = "MainActivity"
|
private val tag = "MainActivity"
|
||||||
|
|
||||||
|
@ -87,6 +85,7 @@ class MainActivity : BridgeActivity() {
|
||||||
|
|
||||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||||
super.onPostCreate(savedInstanceState)
|
super.onPostCreate(savedInstanceState)
|
||||||
|
Log.d(tag, "onPostCreate MainActivity")
|
||||||
|
|
||||||
mConnection = object : ServiceConnection {
|
mConnection = object : ServiceConnection {
|
||||||
override fun onServiceDisconnected(name: ComponentName) {
|
override fun onServiceDisconnected(name: ComponentName) {
|
||||||
|
@ -97,7 +96,6 @@ class MainActivity : BridgeActivity() {
|
||||||
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
||||||
Log.d(tag, "Service Connected $name")
|
Log.d(tag, "Service Connected $name")
|
||||||
|
|
||||||
|
|
||||||
mBounded = true
|
mBounded = true
|
||||||
val mLocalBinder = service as PlayerNotificationService.LocalBinder
|
val mLocalBinder = service as PlayerNotificationService.LocalBinder
|
||||||
foregroundService = mLocalBinder.getService()
|
foregroundService = mLocalBinder.getService()
|
||||||
|
@ -109,8 +107,10 @@ class MainActivity : BridgeActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val startIntent = Intent(this, PlayerNotificationService::class.java)
|
Intent(this, PlayerNotificationService::class.java).also { intent ->
|
||||||
bindService(startIntent, mConnection as ServiceConnection, Context.BIND_AUTO_CREATE);
|
Log.d(tag, "Binding PlayerNotificationService")
|
||||||
|
bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -30,28 +30,68 @@ data class LibraryItem(
|
||||||
JsonSubTypes.Type(Book::class),
|
JsonSubTypes.Type(Book::class),
|
||||||
JsonSubTypes.Type(Podcast::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)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class Podcast(
|
class Podcast(
|
||||||
var metadata:PodcastMetadata,
|
metadata:PodcastMetadata,
|
||||||
var coverPath:String?,
|
coverPath:String?,
|
||||||
var tags:MutableList<String>,
|
var tags:MutableList<String>,
|
||||||
var episodes:MutableList<PodcastEpisode>,
|
var episodes:MutableList<PodcastEpisode>,
|
||||||
var autoDownloadEpisodes:Boolean
|
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)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class Book(
|
class Book(
|
||||||
var metadata:BookMetadata,
|
metadata:BookMetadata,
|
||||||
var coverPath:String?,
|
coverPath:String?,
|
||||||
var tags:List<String>,
|
var tags:List<String>,
|
||||||
var audioFiles:List<AudioFile>,
|
var audioFiles:List<AudioFile>,
|
||||||
var chapters:List<BookChapter>,
|
var chapters:List<BookChapter>,
|
||||||
var tracks:List<AudioTrack>?,
|
var tracks:List<AudioTrack>?,
|
||||||
var size:Long?,
|
var size:Long?,
|
||||||
var duration:Double?
|
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
|
// This auto-detects whether it is a Book or Podcast
|
||||||
@JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION)
|
@JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION)
|
||||||
|
@ -59,11 +99,11 @@ data class Book(
|
||||||
JsonSubTypes.Type(BookMetadata::class),
|
JsonSubTypes.Type(BookMetadata::class),
|
||||||
JsonSubTypes.Type(PodcastMetadata::class)
|
JsonSubTypes.Type(PodcastMetadata::class)
|
||||||
)
|
)
|
||||||
open class MediaTypeMetadata {}
|
open class MediaTypeMetadata(var title:String) {}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class BookMetadata(
|
class BookMetadata(
|
||||||
var title:String,
|
title:String,
|
||||||
var subtitle:String?,
|
var subtitle:String?,
|
||||||
var authors:MutableList<Author>,
|
var authors:MutableList<Author>,
|
||||||
var narrators:MutableList<String>,
|
var narrators:MutableList<String>,
|
||||||
|
@ -81,15 +121,15 @@ data class BookMetadata(
|
||||||
var authorNameLF:String?,
|
var authorNameLF:String?,
|
||||||
var narratorName:String?,
|
var narratorName:String?,
|
||||||
var seriesName:String?
|
var seriesName:String?
|
||||||
) : MediaTypeMetadata()
|
) : MediaTypeMetadata(title)
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class PodcastMetadata(
|
class PodcastMetadata(
|
||||||
var title:String,
|
title:String,
|
||||||
var author:String?,
|
var author:String?,
|
||||||
var feedUrl:String?,
|
var feedUrl:String?,
|
||||||
var genres:MutableList<String>
|
var genres:MutableList<String>
|
||||||
) : MediaTypeMetadata()
|
) : MediaTypeMetadata(title)
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class Author(
|
data class Author(
|
||||||
|
@ -107,7 +147,8 @@ data class PodcastEpisode(
|
||||||
var title:String?,
|
var title:String?,
|
||||||
var subtitle:String?,
|
var subtitle:String?,
|
||||||
var description:String?,
|
var description:String?,
|
||||||
var audioFile:AudioFile
|
var audioFile:AudioFile?,
|
||||||
|
var audioTrack:AudioTrack?
|
||||||
)
|
)
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
|
|
@ -14,11 +14,11 @@ class DbManager {
|
||||||
Paper.book("device").write("data", deviceData)
|
Paper.book("device").write("data", deviceData)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLocalMediaItems():MutableList<LocalMediaItem> {
|
fun getLocalLibraryItems():MutableList<LocalLibraryItem> {
|
||||||
var localMediaItems:MutableList<LocalMediaItem> = mutableListOf()
|
var localLibraryItems:MutableList<LocalLibraryItem> = mutableListOf()
|
||||||
Paper.book("localMediaItems").allKeys.forEach {
|
Paper.book("localLibraryItems").allKeys.forEach {
|
||||||
var localMediaItem:LocalMediaItem? = Paper.book("localMediaItems").read(it)
|
var localLibraryItem:LocalLibraryItem? = Paper.book("localLibraryItems").read(it)
|
||||||
if (localMediaItem != null) {
|
if (localLibraryItem != null) {
|
||||||
// TODO: Check to make sure all file paths exist
|
// TODO: Check to make sure all file paths exist
|
||||||
// if (localMediaItem.coverContentUrl != null) {
|
// if (localMediaItem.coverContentUrl != null) {
|
||||||
// var file = DocumentFile.fromSingleUri(ctx)
|
// var file = DocumentFile.fromSingleUri(ctx)
|
||||||
|
@ -29,31 +29,31 @@ class DbManager {
|
||||||
// localMediaItems.add(localMediaItem)
|
// localMediaItems.add(localMediaItem)
|
||||||
// }
|
// }
|
||||||
// } else {
|
// } else {
|
||||||
localMediaItems.add(localMediaItem)
|
localLibraryItems.add(localLibraryItem)
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return localMediaItems
|
return localLibraryItems
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLocalMediaItemsInFolder(folderId:String):List<LocalMediaItem> {
|
fun getLocalLibraryItemsInFolder(folderId:String):List<LocalLibraryItem> {
|
||||||
var localMediaItems = getLocalMediaItems()
|
var localLibraryItems = getLocalLibraryItems()
|
||||||
return localMediaItems.filter {
|
return localLibraryItems.filter {
|
||||||
it.folderId == folderId
|
it.folderId == folderId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLocalMediaItem(localMediaItemId:String):LocalMediaItem? {
|
fun getLocalLibraryItem(localLibraryItemId:String):LocalLibraryItem? {
|
||||||
return Paper.book("localMediaItems").read(localMediaItemId)
|
return Paper.book("localLibraryItems").read(localLibraryItemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeLocalMediaItem(localMediaItemId:String) {
|
fun removeLocalLibraryItem(localLibraryItemId:String) {
|
||||||
Paper.book("localMediaItems").delete(localMediaItemId)
|
Paper.book("localLibraryItems").delete(localLibraryItemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveLocalMediaItems(localMediaItems:List<LocalMediaItem>) {
|
fun saveLocalLibraryItems(localLibraryItems:List<LocalLibraryItem>) {
|
||||||
localMediaItems.map {
|
localLibraryItems.map {
|
||||||
Paper.book("localMediaItems").write(it.id, it)
|
Paper.book("localLibraryItems").write(it.id, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,9 +77,9 @@ class DbManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeLocalFolder(folderId:String) {
|
fun removeLocalFolder(folderId:String) {
|
||||||
var localMediaItems = getLocalMediaItemsInFolder(folderId)
|
var localLibraryItems = getLocalLibraryItemsInFolder(folderId)
|
||||||
localMediaItems.forEach {
|
localLibraryItems.forEach {
|
||||||
Paper.book("localMediaItems").delete(it.id)
|
Paper.book("localLibraryItems").delete(it.id)
|
||||||
}
|
}
|
||||||
Paper.book("localFolders").delete(folderId)
|
Paper.book("localFolders").delete(folderId)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,14 +22,54 @@ data class DeviceData(
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class LocalLibraryItem(
|
data class LocalLibraryItem(
|
||||||
var id:String,
|
var id:String,
|
||||||
|
var libraryItemId:String?,
|
||||||
var folderId:String,
|
var folderId:String,
|
||||||
var absolutePath:String,
|
var absolutePath:String,
|
||||||
var isInvalid:Boolean,
|
var isInvalid:Boolean,
|
||||||
var mediaType:String,
|
var mediaType:String,
|
||||||
var media:MediaType,
|
var media:MediaType,
|
||||||
var localFiles:MutableList<LocalFile>,
|
var localFiles:MutableList<LocalFile>,
|
||||||
|
var coverContentUrl:String?,
|
||||||
|
var coverAbsolutePath:String?,
|
||||||
var isLocal:Boolean
|
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)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class LocalMediaItem(
|
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
|
@JsonIgnore
|
||||||
fun getAudiobookChapters():List<BookChapter> {
|
fun getAudiobookChapters():List<BookChapter> {
|
||||||
if (mediaType != "book" || audioTracks.isEmpty()) return mutableListOf()
|
if (mediaType != "book" || audioTracks.isEmpty()) return mutableListOf()
|
||||||
|
@ -98,11 +124,11 @@ data class LocalMediaItem(
|
||||||
var mediaMetadata = getMediaMetadata()
|
var mediaMetadata = getMediaMetadata()
|
||||||
if (mediaType == "book") {
|
if (mediaType == "book") {
|
||||||
var chapters = getAudiobookChapters()
|
var chapters = getAudiobookChapters()
|
||||||
var book = Book(mediaMetadata as BookMetadata, coverContentUrl, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration())
|
var book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration())
|
||||||
return LocalLibraryItem(id, folderId, absolutePath, false,mediaType, book, localFiles, true)
|
return LocalLibraryItem(id, null, folderId, absolutePath, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true)
|
||||||
} else {
|
} else {
|
||||||
var podcast = Podcast(mediaMetadata as PodcastMetadata, coverContentUrl, mutableListOf(), mutableListOf(), false)
|
var podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false)
|
||||||
return LocalLibraryItem(id, folderId, absolutePath, false, mediaType, podcast,localFiles,true)
|
return LocalLibraryItem(id, null, folderId, absolutePath, false, mediaType, podcast,localFiles,coverContentUrl, coverAbsolutePath, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,5 +6,5 @@ data class FolderScanResult(
|
||||||
var itemsRemoved:Int,
|
var itemsRemoved:Int,
|
||||||
var itemsUpToDate:Int,
|
var itemsUpToDate:Int,
|
||||||
val localFolder:LocalFolder,
|
val localFolder:LocalFolder,
|
||||||
val localMediaItems:List<LocalMediaItem>,
|
val localLibraryItems:List<LocalLibraryItem>,
|
||||||
)
|
)
|
||||||
|
|
|
@ -32,7 +32,7 @@ class PlaybackSession(
|
||||||
var audioTracks:MutableList<AudioTrack>,
|
var audioTracks:MutableList<AudioTrack>,
|
||||||
var currentTime:Double,
|
var currentTime:Double,
|
||||||
var libraryItem:LibraryItem?,
|
var libraryItem:LibraryItem?,
|
||||||
var localMediaItem:LocalMediaItem?,
|
var localLibraryItem:LocalLibraryItem?,
|
||||||
var serverUrl:String?,
|
var serverUrl:String?,
|
||||||
var token:String?
|
var token:String?
|
||||||
) {
|
) {
|
||||||
|
@ -74,7 +74,7 @@ class PlaybackSession(
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getCoverUri(): Uri {
|
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)
|
if (coverPath == null) return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
||||||
return Uri.parse("$serverUrl/api/items/$libraryItemId/cover?token=$token")
|
return Uri.parse("$serverUrl/api/items/$libraryItemId/cover?token=$token")
|
||||||
|
|
|
@ -15,6 +15,10 @@ import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
class FolderScanner(var ctx: Context) {
|
class FolderScanner(var ctx: Context) {
|
||||||
private val tag = "FolderScanner"
|
private val tag = "FolderScanner"
|
||||||
|
|
||||||
|
private fun getLocalLibraryItemId(mediaItemId:String):String {
|
||||||
|
return "local_" + DeviceManager.getBase64Id(mediaItemId)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: CLEAN this monster! Divide into bite-size methods
|
// TODO: CLEAN this monster! Divide into bite-size methods
|
||||||
fun scanForMediaItems(localFolder:LocalFolder, forceAudioProbe:Boolean):FolderScanResult? {
|
fun scanForMediaItems(localFolder:LocalFolder, forceAudioProbe:Boolean):FolderScanResult? {
|
||||||
FFmpegKitConfig.enableLogCallback { log ->
|
FFmpegKitConfig.enableLogCallback { log ->
|
||||||
|
@ -38,32 +42,32 @@ class FolderScanner(var ctx: Context) {
|
||||||
// Search for files in media item folder
|
// Search for files in media item folder
|
||||||
var foldersFound = df.search(false, DocumentFileType.FOLDER)
|
var foldersFound = df.search(false, DocumentFileType.FOLDER)
|
||||||
|
|
||||||
// Match folders found with media items already saved in db
|
// Match folders found with local library items already saved in db
|
||||||
var existingMediaItems = DeviceManager.dbManager.getLocalMediaItemsInFolder(localFolder.id)
|
var existingLocalLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id)
|
||||||
|
|
||||||
// Remove existing items no longer there
|
// Remove existing items no longer there
|
||||||
existingMediaItems = existingMediaItems.filter { lmi ->
|
existingLocalLibraryItems = existingLocalLibraryItems.filter { lli ->
|
||||||
var fileFound = foldersFound.find { f -> lmi.id == DeviceManager.getBase64Id(f.id) }
|
var fileFound = foldersFound.find { f -> lli.id == getLocalLibraryItemId(f.id) }
|
||||||
if (fileFound == null) {
|
if (fileFound == null) {
|
||||||
Log.d(tag, "Existing media item is no longer in file system ${lmi.name}")
|
Log.d(tag, "Existing local library item is no longer in file system ${lli.media.metadata.title}")
|
||||||
DeviceManager.dbManager.removeLocalMediaItem(lmi.id)
|
DeviceManager.dbManager.removeLocalLibraryItem(lli.id)
|
||||||
mediaItemsRemoved++
|
mediaItemsRemoved++
|
||||||
}
|
}
|
||||||
fileFound != null
|
fileFound != null
|
||||||
}
|
}
|
||||||
|
|
||||||
var mediaItems = mutableListOf<LocalMediaItem>()
|
var localLibraryItems = mutableListOf<LocalLibraryItem>()
|
||||||
|
|
||||||
foldersFound.forEach {
|
foldersFound.forEach { itemFolder ->
|
||||||
Log.d(tag, "Iterating over Folder Found ${it.name} | ${it.getSimplePath(ctx)} | URI: ${it.uri}")
|
Log.d(tag, "Iterating over Folder Found ${itemFolder.name} | ${itemFolder.getSimplePath(ctx)} | URI: ${itemFolder.uri}")
|
||||||
|
|
||||||
var itemFolderName = it.name ?: ""
|
var itemFolderName = itemFolder.name ?: ""
|
||||||
var itemId = "local_" + DeviceManager.getBase64Id(it.id)
|
var itemId = getLocalLibraryItemId(itemFolder.id)
|
||||||
|
|
||||||
var existingMediaItem = existingMediaItems.find { emi -> emi.id == itemId }
|
var existingItem = existingLocalLibraryItems.find { emi -> emi.id == itemId }
|
||||||
var existingLocalFiles = existingMediaItem?.localFiles ?: mutableListOf()
|
var existingLocalFiles = existingItem?.localFiles ?: mutableListOf()
|
||||||
var existingAudioTracks = existingMediaItem?.audioTracks ?: mutableListOf()
|
var existingAudioTracks = existingItem?.media?.getAudioTracks() ?: mutableListOf()
|
||||||
var isNewOrUpdated = existingMediaItem == null
|
var isNewOrUpdated = existingItem == null
|
||||||
|
|
||||||
var audioTracks = mutableListOf<AudioTrack>()
|
var audioTracks = mutableListOf<AudioTrack>()
|
||||||
var localFiles = mutableListOf<LocalFile>()
|
var localFiles = mutableListOf<LocalFile>()
|
||||||
|
@ -72,13 +76,14 @@ class FolderScanner(var ctx: Context) {
|
||||||
var coverContentUrl:String? = null
|
var coverContentUrl:String? = null
|
||||||
var coverAbsolutePath: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 ->
|
var existingLocalFilesRemoved = existingLocalFiles.filter { elf ->
|
||||||
filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder
|
filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder
|
||||||
}
|
}
|
||||||
if (existingLocalFilesRemoved.isNotEmpty()) {
|
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
|
isNewOrUpdated = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,9 +152,12 @@ class FolderScanner(var ctx: Context) {
|
||||||
if (existingLocalFile == null) {
|
if (existingLocalFile == null) {
|
||||||
isNewOrUpdated = true
|
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
|
// Existing media item did not have a cover - cover found on scan
|
||||||
isNewOrUpdated = true
|
isNewOrUpdated = true
|
||||||
|
existingItem.coverAbsolutePath = localFile.absolutePath
|
||||||
|
existingItem.coverContentUrl = localFile.contentUrl
|
||||||
|
existingItem.media.coverPath = localFile.absolutePath
|
||||||
}
|
}
|
||||||
|
|
||||||
// First image file use as cover path
|
// First image file use as cover path
|
||||||
|
@ -160,30 +168,36 @@ class FolderScanner(var ctx: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingMediaItem != null && audioTracks.isEmpty()) {
|
if (existingItem != null && audioTracks.isEmpty()) {
|
||||||
Log.d(tag, "Local media item ${existingMediaItem.name} no longer has audio tracks - removing item")
|
Log.d(tag, "Local library item ${existingItem.media.metadata.title} no longer has audio tracks - removing item")
|
||||||
DeviceManager.dbManager.removeLocalMediaItem(existingMediaItem.id)
|
DeviceManager.dbManager.removeLocalLibraryItem(existingItem.id)
|
||||||
mediaItemsRemoved++
|
mediaItemsRemoved++
|
||||||
} else if (existingMediaItem != null && !isNewOrUpdated) {
|
} else if (existingItem != null && !isNewOrUpdated) {
|
||||||
Log.d(tag, "Local media item ${existingMediaItem.name} has no updates")
|
Log.d(tag, "Local library item ${existingItem.media.metadata.title} has no updates")
|
||||||
mediaItemsUpToDate++
|
mediaItemsUpToDate++
|
||||||
} else if (audioTracks.isNotEmpty()) {
|
} else if (existingItem != null) {
|
||||||
if (existingMediaItem != null) mediaItemsUpdated++
|
Log.d(tag, "Updating local library item ${existingItem.media.metadata.title}")
|
||||||
else mediaItemsAdded++
|
mediaItemsUpdated++
|
||||||
|
|
||||||
Log.d(tag, "Found local media item named $itemFolderName with ${audioTracks.size} tracks and ${localFiles.size} local files")
|
existingItem.updateFromScan(audioTracks,localFiles)
|
||||||
var localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, it.uri.toString(), it.getSimplePath(ctx), it.getAbsolutePath(ctx),audioTracks,localFiles,coverContentUrl,coverAbsolutePath)
|
localLibraryItems.add(existingItem)
|
||||||
mediaItems.add(localMediaItem)
|
} 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")
|
Log.d(tag, "Folder $${localFolder.name} scan Results: $mediaItemsAdded Added | $mediaItemsUpdated Updated | $mediaItemsRemoved Removed | $mediaItemsUpToDate Up-to-date")
|
||||||
|
|
||||||
return if (mediaItems.isNotEmpty()) {
|
return if (localLibraryItems.isNotEmpty()) {
|
||||||
DeviceManager.dbManager.saveLocalMediaItems(mediaItems)
|
DeviceManager.dbManager.saveLocalLibraryItems(localLibraryItems)
|
||||||
|
|
||||||
var folderMediaItems = DeviceManager.dbManager.getLocalMediaItemsInFolder(localFolder.id) // Get all local media items
|
var folderLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id) // Get all local media items
|
||||||
FolderScanResult(mediaItemsAdded, mediaItemsUpdated, mediaItemsRemoved, mediaItemsUpToDate, localFolder, folderMediaItems)
|
FolderScanResult(mediaItemsAdded, mediaItemsUpdated, mediaItemsRemoved, mediaItemsUpToDate, localFolder, folderLibraryItems)
|
||||||
} else {
|
} else {
|
||||||
Log.d(tag, "No Media Items to save")
|
Log.d(tag, "No Media Items to save")
|
||||||
FolderScanResult(mediaItemsAdded, mediaItemsUpdated, mediaItemsRemoved, mediaItemsUpToDate, localFolder, mutableListOf())
|
FolderScanResult(mediaItemsAdded, mediaItemsUpdated, mediaItemsRemoved, mediaItemsUpToDate, localFolder, mutableListOf())
|
||||||
|
|
|
@ -133,8 +133,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
Log.d(tag, "onStartCommand $startId")
|
|
||||||
isStarted = true
|
isStarted = true
|
||||||
|
Log.d(tag, "onStartCommand $startId")
|
||||||
|
|
||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package com.audiobookshelf.app.plugins
|
package com.audiobookshelf.app.plugins
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import com.audiobookshelf.app.MainActivity
|
import com.audiobookshelf.app.MainActivity
|
||||||
import com.audiobookshelf.app.data.PlaybackSession
|
import com.audiobookshelf.app.data.PlaybackSession
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
|
@ -75,11 +77,19 @@ class AbsAudioPlayer : Plugin() {
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun prepareLibraryItem(call: PluginCall) {
|
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 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
|
if (libraryItemId.startsWith("local")) { // Play local media item
|
||||||
DeviceManager.dbManager.getLocalMediaItem(libraryItemId)?.let {
|
DeviceManager.dbManager.getLocalLibraryItem(libraryItemId)?.let {
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
Log.d(tag, "Preparing Local Media item ${jacksonObjectMapper().writeValueAsString(it)}")
|
Log.d(tag, "Preparing Local Media item ${jacksonObjectMapper().writeValueAsString(it)}")
|
||||||
var playbackSession = it.getPlaybackSession()
|
var playbackSession = it.getPlaybackSession()
|
||||||
|
|
|
@ -48,23 +48,22 @@ class AbsDatabase : Plugin() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun getLocalMediaItemsInFolder(call:PluginCall) {
|
fun getLocalLibraryItem(call:PluginCall) {
|
||||||
var folderId = call.getString("folderId", "").toString()
|
var id = call.getString("id", "").toString()
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
var localMediaItems = DeviceManager.dbManager.getLocalMediaItemsInFolder(folderId)
|
var localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(id)
|
||||||
var mediaItemsArray = jacksonObjectMapper().writeValueAsString(localMediaItems)
|
if (localLibraryItem == null) {
|
||||||
var jsobj = JSObject()
|
call.resolve()
|
||||||
jsobj.put("localMediaItems", mediaItemsArray)
|
} else {
|
||||||
call.resolve(jsobj)
|
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(localLibraryItem)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun getLocalLibraryItems(call:PluginCall) {
|
fun getLocalLibraryItems(call:PluginCall) {
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
var localLibraryItems = DeviceManager.dbManager.getLocalMediaItems().map {
|
var localLibraryItems = DeviceManager.dbManager.getLocalLibraryItems()
|
||||||
it.getLocalLibraryItem()
|
|
||||||
}
|
|
||||||
var jsobj = JSObject()
|
var jsobj = JSObject()
|
||||||
jsobj.put("localLibraryItems", jacksonObjectMapper().writeValueAsString(localLibraryItems))
|
jsobj.put("localLibraryItems", jacksonObjectMapper().writeValueAsString(localLibraryItems))
|
||||||
call.resolve(jsobj)
|
call.resolve(jsobj)
|
||||||
|
@ -72,16 +71,14 @@ class AbsDatabase : Plugin() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun getLocalLibraryItem(call:PluginCall) {
|
fun getLocalLibraryItemsInFolder(call:PluginCall) {
|
||||||
var id = call.getString("id", "").toString()
|
var folderId = call.getString("folderId", "").toString()
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
var mediaItem = DeviceManager.dbManager.getLocalMediaItem(id)
|
var localMediaItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(folderId)
|
||||||
var localLibraryItem = mediaItem?.getLocalLibraryItem()
|
var mediaItemsArray = jacksonObjectMapper().writeValueAsString(localMediaItems)
|
||||||
if (localLibraryItem == null) {
|
var jsobj = JSObject()
|
||||||
call.resolve()
|
jsobj.put("localLibraryItems", mediaItemsArray)
|
||||||
} else {
|
call.resolve(jsobj)
|
||||||
call.resolve(JSObject(jacksonObjectMapper().writeValueAsString(localLibraryItem)))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -118,11 +118,11 @@ class AbsDownloader : Plugin() {
|
||||||
fun getAbMetadataText(libraryItem:LibraryItem):String {
|
fun getAbMetadataText(libraryItem:LibraryItem):String {
|
||||||
var bookMedia = libraryItem.media as com.audiobookshelf.app.data.Book
|
var bookMedia = libraryItem.media as com.audiobookshelf.app.data.Book
|
||||||
var fileString = ";ABMETADATA1\n"
|
var fileString = ";ABMETADATA1\n"
|
||||||
fileString += "#libraryItemId=${libraryItem.id}\n"
|
// fileString += "#libraryItemId=${libraryItem.id}\n"
|
||||||
fileString += "title=${bookMedia.metadata.title}\n"
|
// fileString += "title=${bookMedia.metadata.title}\n"
|
||||||
fileString += "author=${bookMedia.metadata.authorName}\n"
|
// fileString += "author=${bookMedia.metadata.authorName}\n"
|
||||||
fileString += "narrator=${bookMedia.metadata.narratorName}\n"
|
// fileString += "narrator=${bookMedia.metadata.narratorName}\n"
|
||||||
fileString += "series=${bookMedia.metadata.seriesName}\n"
|
// fileString += "series=${bookMedia.metadata.seriesName}\n"
|
||||||
return fileString
|
return fileString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<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 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">
|
<div class="top-2 left-4 absolute cursor-pointer">
|
||||||
<span class="material-icons text-5xl" @click="collapseFullscreen">expand_more</span>
|
<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-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">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -175,12 +175,12 @@ export default {
|
||||||
libraryItem() {
|
libraryItem() {
|
||||||
return this.playbackSession ? this.playbackSession.libraryItem || null : null
|
return this.playbackSession ? this.playbackSession.libraryItem || null : null
|
||||||
},
|
},
|
||||||
localMediaItem() {
|
localLibraryItem() {
|
||||||
return this.playbackSession ? this.playbackSession.localMediaItem || null : null
|
return this.playbackSession ? this.playbackSession.localLibraryItem || null : null
|
||||||
},
|
},
|
||||||
localMediaItemCoverSrc() {
|
localLibraryItemCoverSrc() {
|
||||||
var localMediaItemCover = this.localMediaItem ? this.localMediaItem.coverContentUrl : null
|
var localItemCover = this.localLibraryItem ? this.localLibraryItem.coverContentUrl : null
|
||||||
if (localMediaItemCover) return Capacitor.convertFileSrc(localMediaItemCover)
|
if (localItemCover) return Capacitor.convertFileSrc(localItemCover)
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
playMethod() {
|
playMethod() {
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<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" />
|
||||||
<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>
|
|
||||||
|
|
||||||
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-rate.sync="playbackSpeed" @update:playbackRate="updatePlaybackSpeed" @change="changePlaybackSpeed" />
|
<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" />
|
<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" />
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<transition name="menu">
|
<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">
|
<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)">
|
<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">
|
<div class="flex items-center">
|
||||||
|
|
|
@ -97,7 +97,7 @@ export default {
|
||||||
var localCategories = await this.getLocalMediaItemCategories()
|
var localCategories = await this.getLocalMediaItemCategories()
|
||||||
this.shelves = this.shelves.concat(localCategories)
|
this.shelves = this.shelves.concat(localCategories)
|
||||||
|
|
||||||
if (this.user || !this.currentLibraryId) {
|
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) => {
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="w-32">
|
<div class="w-32">
|
||||||
<div class="relative">
|
<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 class="absolute bottom-0 left-0 h-1.5 bg-yellow-400 shadow-sm z-10" :style="{ width: 128 * progressPercent + 'px' }"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex my-4">
|
<div class="flex my-4">
|
||||||
|
|
|
@ -3,23 +3,24 @@
|
||||||
<div class="flex items-center mb-4">
|
<div class="flex items-center mb-4">
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<ui-btn v-if="!removingFolder" :loading="isScanning" small @click="clickScan">Scan</ui-btn>
|
<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" />
|
<ui-icon-btn class="ml-2" bg-color="error" outlined :loading="removingFolder" icon="delete" @click="clickDeleteFolder" />
|
||||||
</div>
|
</div>
|
||||||
<p class="text-lg mb-0.5 text-white text-opacity-75">Folder: {{ folderName }}</p>
|
<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">
|
<div v-if="isScanning" class="w-full text-center p-4">
|
||||||
<p>Scanning...</p>
|
<p>Scanning...</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="w-full media-item-container overflow-y-auto">
|
<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 :key="mediaItem.id" class="flex my-1">
|
||||||
<div class="w-12 h-12 bg-primary">
|
<div class="w-12 h-12 bg-primary">
|
||||||
<img v-if="mediaItem.coverPathSrc" :src="mediaItem.coverPathSrc" class="w-full h-full object-contain" />
|
<img v-if="mediaItem.coverPathSrc" :src="mediaItem.coverPathSrc" class="w-full h-full object-contain" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-2">
|
<div class="flex-grow px-2">
|
||||||
<p>{{ mediaItem.name }}</p>
|
<p>{{ mediaItem.media.metadata.title }}</p>
|
||||||
<p>{{ mediaItem.audioTracks.length }} Tracks</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>
|
||||||
<div class="w-12 h-12 flex items-center justify-center">
|
<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)">
|
<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() {
|
data() {
|
||||||
return {
|
return {
|
||||||
localMediaItems: [],
|
localLibraryItems: [],
|
||||||
folder: null,
|
folder: null,
|
||||||
isScanning: false,
|
isScanning: false,
|
||||||
removingFolder: false
|
removingFolder: false
|
||||||
|
@ -66,8 +67,8 @@ export default {
|
||||||
},
|
},
|
||||||
async clickDeleteFolder() {
|
async clickDeleteFolder() {
|
||||||
var deleteMessage = 'Are you sure you want to remove this folder? (does not delete anything in your file system)'
|
var deleteMessage = 'Are you sure you want to remove this folder? (does not delete anything in your file system)'
|
||||||
if (this.localMediaItems.length) {
|
if (this.localLibraryItems.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)`
|
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({
|
const { value } = await Dialog.confirm({
|
||||||
title: 'Confirm',
|
title: 'Confirm',
|
||||||
|
@ -87,7 +88,7 @@ export default {
|
||||||
this.isScanning = true
|
this.isScanning = true
|
||||||
var response = await AbsFileSystem.scanFolder({ folderId: this.folderId, forceAudioProbe })
|
var response = await AbsFileSystem.scanFolder({ folderId: this.folderId, forceAudioProbe })
|
||||||
|
|
||||||
if (response && response.localMediaItems) {
|
if (response && response.localLibraryItems) {
|
||||||
var itemsAdded = response.itemsAdded
|
var itemsAdded = response.itemsAdded
|
||||||
var itemsUpdated = response.itemsUpdated
|
var itemsUpdated = response.itemsUpdated
|
||||||
var itemsRemoved = response.itemsRemoved
|
var itemsRemoved = response.itemsRemoved
|
||||||
|
@ -100,14 +101,14 @@ export default {
|
||||||
this.$toast.info(`Folder scan complete:\n${toastMessages.join(' | ')}`)
|
this.$toast.info(`Folder scan complete:\n${toastMessages.join(' | ')}`)
|
||||||
|
|
||||||
// When all items are up-to-date then local media items are not returned
|
// When all items are up-to-date then local media items are not returned
|
||||||
if (response.localMediaItems.length) {
|
if (response.localLibraryItems.length) {
|
||||||
this.localMediaItems = response.localMediaItems.map((mi) => {
|
this.localLibraryItems = response.localLibraryItems.map((mi) => {
|
||||||
if (mi.coverContentUrl) {
|
if (mi.coverContentUrl) {
|
||||||
mi.coverPathSrc = Capacitor.convertFileSrc(mi.coverContentUrl)
|
mi.coverPathSrc = Capacitor.convertFileSrc(mi.coverContentUrl)
|
||||||
}
|
}
|
||||||
return mi
|
return mi
|
||||||
})
|
})
|
||||||
console.log('Set Local Media Items', this.localMediaItems.length)
|
console.log('Set Local Media Items', this.localLibraryItems.length)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log('No Local media items found')
|
console.log('No Local media items found')
|
||||||
|
@ -118,9 +119,9 @@ export default {
|
||||||
var folder = await this.$db.getLocalFolder(this.folderId)
|
var folder = await this.$db.getLocalFolder(this.folderId)
|
||||||
this.folder = folder
|
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)
|
console.log('Init folder', this.folderId, items)
|
||||||
this.localMediaItems = items.map((lmi) => {
|
this.localLibraryItems = items.map((lmi) => {
|
||||||
return {
|
return {
|
||||||
...lmi,
|
...lmi,
|
||||||
coverPathSrc: lmi.coverContentUrl ? Capacitor.convertFileSrc(lmi.coverContentUrl) : null
|
coverPathSrc: lmi.coverContentUrl ? Capacitor.convertFileSrc(lmi.coverContentUrl) : null
|
||||||
|
|
|
@ -59,6 +59,7 @@ export default {
|
||||||
return this.$toast.error('Must select a media type')
|
return this.$toast.error('Must select a media type')
|
||||||
}
|
}
|
||||||
var folderObj = await AbsFileSystem.selectFolder({ mediaType: this.newFolderMediaType })
|
var folderObj = await AbsFileSystem.selectFolder({ mediaType: this.newFolderMediaType })
|
||||||
|
if (!folderObj) return
|
||||||
if (folderObj.error) {
|
if (folderObj.error) {
|
||||||
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
|
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ class AbsFileSystemWeb extends WebPlugin {
|
||||||
constructor() {
|
constructor() {
|
||||||
super()
|
super()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async selectFolder() { }
|
||||||
}
|
}
|
||||||
|
|
||||||
const AbsFileSystem = registerPlugin('AbsFileSystem', {
|
const AbsFileSystem = registerPlugin('AbsFileSystem', {
|
||||||
|
|
|
@ -73,14 +73,14 @@ class DbService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
getLocalMediaItemsInFolder(folderId) {
|
getLocalLibraryItemsInFolder(folderId) {
|
||||||
if (isWeb) return []
|
if (isWeb) return []
|
||||||
return AbsDatabase.getLocalMediaItemsInFolder({ folderId }).then((data) => {
|
return AbsDatabase.getLocalLibraryItemsInFolder({ folderId }).then((data) => {
|
||||||
console.log('Loaded local media items in folder', JSON.stringify(data))
|
console.log('Loaded local library items in folder', JSON.stringify(data))
|
||||||
if (data.localMediaItems && typeof data.localMediaItems == 'string') {
|
if (data.localLibraryItems && typeof data.localLibraryItems == 'string') {
|
||||||
return JSON.parse(data.localMediaItems)
|
return JSON.parse(data.localLibraryItems)
|
||||||
}
|
}
|
||||||
return data.localMediaItems
|
return data.localLibraryItems
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ export const getters = {
|
||||||
return state.serverSettings[key]
|
return state.serverSettings[key]
|
||||||
},
|
},
|
||||||
getBookCoverAspectRatio: state => {
|
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
|
return state.serverSettings.coverAspectRatio === 0 ? 1.6 : 1
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue