diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt index 65be49a0..4f97b373 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt @@ -42,6 +42,15 @@ data class LibraryItem( return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}") } + @JsonIgnore + fun checkHasTracks():Boolean { + return if (mediaType == "podcast") { + ((media as Podcast).numEpisodes ?: 0) > 0 + } else { + ((media as Book).numTracks ?: 0) > 0 + } + } + @JsonIgnore fun getMediaMetadata(): MediaMetadataCompat { return MediaMetadataCompat.Builder().apply { @@ -74,6 +83,7 @@ open class MediaType(var metadata:MediaTypeMetadata, var coverPath:String?) { open fun removeAudioTrack(localFileId:String) { } @JsonIgnore open fun getLocalCopy():MediaType { return MediaType(MediaTypeMetadata(""),null) } + } @JsonIgnoreProperties(ignoreUnknown = true) @@ -82,7 +92,8 @@ class Podcast( coverPath:String?, var tags:MutableList, var episodes:MutableList?, - var autoDownloadEpisodes:Boolean + var autoDownloadEpisodes:Boolean, + var numEpisodes:Int? ) : MediaType(metadata, coverPath) { @JsonIgnore override fun getAudioTracks():List { @@ -99,7 +110,7 @@ class Podcast( // Add new episodes audioTracks.forEach { at -> if (episodes?.find{ it.audioTrack?.localFileId == at.localFileId } == null) { - val newEpisode = PodcastEpisode("local_" + at.localFileId,episodes?.size ?: 0 + 1,null,null,at.title,null,null,null,at,at.duration,0, null) + val newEpisode = PodcastEpisode("local_ep_" + at.localFileId,episodes?.size ?: 0 + 1,null,null,at.title,null,null,null,at,at.duration,0, null) episodes?.add(newEpisode) } } @@ -147,7 +158,7 @@ class Podcast( // Used for FolderScanner local podcast item to get copy of Podcast excluding episodes @JsonIgnore override fun getLocalCopy(): Podcast { - return Podcast(metadata as PodcastMetadata,coverPath,tags, mutableListOf(),autoDownloadEpisodes) + return Podcast(metadata as PodcastMetadata,coverPath,tags, mutableListOf(),autoDownloadEpisodes, 0) } } @@ -160,7 +171,8 @@ class Book( var chapters:List?, var tracks:MutableList?, var size:Long?, - var duration:Double? + var duration:Double?, + var numTracks:Int? ) : MediaType(metadata, coverPath) { @JsonIgnore override fun getAudioTracks():List { @@ -209,7 +221,7 @@ class Book( @JsonIgnore override fun getLocalCopy(): Book { - return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(),null,null) + return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(),null,null, 0) } } @@ -281,7 +293,29 @@ data class PodcastEpisode( var duration:Double?, var size:Long?, var serverEpisodeId:String? // For local podcasts to match with server podcasts -) +) { + @JsonIgnore + fun getMediaMetadata(libraryItem:LibraryItemWrapper): MediaMetadataCompat { + var coverUri:Uri = Uri.EMPTY + val podcast = if(libraryItem is LocalLibraryItem) { + coverUri = libraryItem.getCoverUri() + libraryItem.media as Podcast + } else { + coverUri = (libraryItem as LibraryItem).getCoverUri() + (libraryItem as LibraryItem).media as Podcast + } + + return MediaMetadataCompat.Builder().apply { + putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id) + putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title) + putString(MediaMetadataCompat.METADATA_KEY_TITLE, title) + putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, podcast.metadata.getAuthorDisplayName()) + putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, podcast.metadata.getAuthorDisplayName()) + putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString()) + + }.build() + } +} @JsonIgnoreProperties(ignoreUnknown = true) data class LibraryFile( @@ -312,7 +346,16 @@ data class Library( var folders:MutableList, var icon:String, var mediaType:String -) +) { + @JsonIgnore + fun getMediaMetadata(): MediaMetadataCompat { + return MediaMetadataCompat.Builder().apply { + putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id) + putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, name) + putString(MediaMetadataCompat.METADATA_KEY_TITLE, name) + }.build() + } +} @JsonIgnoreProperties(ignoreUnknown = true) data class Folder( @@ -371,3 +414,9 @@ data class MediaProgress( var startedAt:Long, var finishedAt:Long? ) + +// Helper class +data class LibraryItemWithEpisode( + var libraryItemWrapper:LibraryItemWrapper, + var episode:PodcastEpisode +) diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt index 9c2c39ab..b6471220 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt @@ -42,6 +42,20 @@ class DbManager { return Paper.book("localLibraryItems").read(localLibraryItemId) } + fun getLocalLibraryItemWithEpisode(podcastEpisodeId:String):LibraryItemWithEpisode? { + var podcastEpisode:PodcastEpisode? = null + val localLibraryItem = getLocalLibraryItems("podcast").find { localLibraryItem -> + val podcast = localLibraryItem.media as Podcast + podcastEpisode = podcast.episodes?.find { it.id == podcastEpisodeId } + podcastEpisode != null + } + return if (localLibraryItem != null) { + LibraryItemWithEpisode(localLibraryItem, podcastEpisode!!) + } else { + null + } + } + fun removeLocalLibraryItem(localLibraryItemId:String) { Paper.book("localLibraryItems").delete(localLibraryItemId) } diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt b/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt index 1536ac4f..d200dd74 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt @@ -1,13 +1,20 @@ package com.audiobookshelf.app.data +import android.content.ContentResolver import android.content.Context +import android.content.Intent +import android.graphics.Bitmap import android.net.Uri import android.support.v4.media.MediaMetadataCompat import android.util.Log import com.audiobookshelf.app.R import com.audiobookshelf.app.device.DeviceManager +import com.audiobookshelf.app.player.NOTIFICATION_LARGE_ICON_SIZE +import com.bumptech.glide.Glide import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.util.* @JsonIgnoreProperties(ignoreUnknown = true) diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaItem.kt b/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaItem.kt index e7038800..fb9008f7 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaItem.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaItem.kt @@ -58,13 +58,13 @@ data class LocalMediaItem( @JsonIgnore fun getLocalLibraryItem():LocalLibraryItem { - var mediaMetadata = getMediaMetadata() + val mediaMetadata = getMediaMetadata() if (mediaType == "book") { - var chapters = getAudiobookChapters() - var book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration()) + val chapters = getAudiobookChapters() + val book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration(),audioTracks.size) return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true,null,null,null,null) } else { - var podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false) + val podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false, 0) podcast.setAudioTracks(audioTracks) // Builds episodes from audio tracks return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false, mediaType, podcast,localFiles,coverContentUrl, coverAbsolutePath, true, null,null,null,null) } diff --git a/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt b/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt index 6142b625..8e2e4321 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt @@ -288,7 +288,7 @@ class FolderScanner(var ctx: Context) { val audioProbeResult = probeAudioFile(localFile.absolutePath) // Create new audio track - val track = AudioTrack(audioTrackFromServer?.index ?: -1, audioTrackFromServer?.startOffset ?: 0.0, audioProbeResult.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, audioTrackFromServer?.index ?: -1) + val track = AudioTrack(audioTrackFromServer.index, audioTrackFromServer.startOffset, audioProbeResult.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, audioTrackFromServer?.index ?: -1) audioTracks.add(track) Log.d(tag, "scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}") @@ -296,7 +296,7 @@ class FolderScanner(var ctx: Context) { // Add podcast episodes to library itemPart.episode?.let { podcastEpisode -> val podcast = localLibraryItem.media as Podcast - var newEpisode = podcast.addEpisode(track, podcastEpisode) + val newEpisode = podcast.addEpisode(track, podcastEpisode) localEpisodeId = newEpisode.id Log.d(tag, "scanDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}") } @@ -366,7 +366,7 @@ class FolderScanner(var ctx: Context) { } fun scanLocalLibraryItem(localLibraryItem:LocalLibraryItem, forceAudioProbe:Boolean):LocalLibraryItemScanResult? { - var df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(localLibraryItem.contentUrl)) + val df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(localLibraryItem.contentUrl)) if (df == null) { Log.e(tag, "Item Folder Doc File Invalid ${localLibraryItem.absolutePath}") @@ -377,7 +377,7 @@ class FolderScanner(var ctx: Context) { var wasUpdated = false // Search for files in media item folder - var filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4")) + val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4")) Log.d(tag, "scanLocalLibraryItem ${filesFound.size} files found in ${localLibraryItem.absolutePath}") filesFound.forEach { @@ -388,10 +388,10 @@ class FolderScanner(var ctx: Context) { } } - var existingAudioTracks = localLibraryItem.media.getAudioTracks() + val existingAudioTracks = localLibraryItem.media.getAudioTracks() // Remove any files no longer found in library item folder - var existingLocalFileIds = localLibraryItem.localFiles.map { it.id } + val existingLocalFileIds = localLibraryItem.localFiles.map { it.id } existingLocalFileIds.forEach { localFileId -> Log.d(tag, "Checking local file id is there $localFileId") if (filesFound.find { DeviceManager.getBase64Id(it.id) == localFileId } == null) { @@ -407,12 +407,12 @@ class FolderScanner(var ctx: Context) { } filesFound.forEach { docFile -> - var localFileId = DeviceManager.getBase64Id(docFile.id) - var existingLocalFile = localLibraryItem.localFiles.find { it.id == localFileId } + val localFileId = DeviceManager.getBase64Id(docFile.id) + val existingLocalFile = localLibraryItem.localFiles.find { it.id == localFileId } if (existingLocalFile == null || (existingLocalFile.isAudioFile() && forceAudioProbe)) { - var localFile = existingLocalFile ?: LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx), docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length()) + val localFile = existingLocalFile ?: LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx), docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length()) if (existingLocalFile == null) { localLibraryItem.localFiles.add(localFile) Log.d(tag, "scanLocalLibraryItem new file found ${localFile.filename}") @@ -420,22 +420,26 @@ class FolderScanner(var ctx: Context) { if (localFile.isAudioFile()) { // TODO: Make asynchronous - var audioProbeResult = probeAudioFile(localFile.absolutePath) + val audioProbeResult = probeAudioFile(localFile.absolutePath) - var existingTrack = existingAudioTracks.find { audioTrack -> + val existingTrack = existingAudioTracks.find { audioTrack -> audioTrack.localFileId == localFile.id } if (existingTrack == null) { // Create new audio track - var lastTrack = existingAudioTracks.lastOrNull() - var startOffset = (lastTrack?.startOffset ?: 0.0) + (lastTrack?.duration ?: 0.0) - var track = AudioTrack(existingAudioTracks.size, startOffset, audioProbeResult.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, null) + val lastTrack = existingAudioTracks.lastOrNull() + val startOffset = (lastTrack?.startOffset ?: 0.0) + (lastTrack?.duration ?: 0.0) + val track = AudioTrack(existingAudioTracks.size, startOffset, audioProbeResult.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, null) localLibraryItem.media.addAudioTrack(track) + Log.d(tag, "Added New Audio Track ${track.title}") wasUpdated = true } else { existingTrack.audioProbeResult = audioProbeResult // TODO: Update data found from probe + + Log.d(tag, "Updated Audio Track Probe Data ${existingTrack.title}") + wasUpdated = true } } else { // Check if cover is empty diff --git a/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt b/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt index b668fd8a..80bc92cb 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt @@ -2,6 +2,8 @@ package com.audiobookshelf.app.media import android.bluetooth.BluetoothClass import android.content.Context +import android.support.v4.media.MediaBrowserCompat +import android.support.v4.media.MediaMetadataCompat import android.util.Log import com.audiobookshelf.app.data.* import com.audiobookshelf.app.device.DeviceManager @@ -14,6 +16,12 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { val tag = "MediaManager" var serverLibraryItems = listOf() + var selectedLibraryId = "" + + var selectedLibraryItemWrapper:LibraryItemWrapper? = null + var selectedPodcast:Podcast? = null + var selectedLibraryItemId:String? = null + var serverPodcastEpisodes = listOf() var serverLibraryCategories = listOf() var serverLibraries = listOf() @@ -22,6 +30,10 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { Paper.init(ctx) } + fun getIsLibrary(id:String) : Boolean { + return serverLibraries.find { it.id == id } != null + } + fun loadLibraryCategories(libraryId:String, cb: (List) -> Unit) { if (serverLibraryCategories.isNotEmpty()) { cb(serverLibraryCategories) @@ -33,17 +45,75 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { } } - fun loadLibraryItems(libraryId:String, cb: (List) -> Unit) { - if (serverLibraryItems.isNotEmpty()) { + fun loadLibraryItemsWithAudio(libraryId:String, cb: (List) -> Unit) { + if (serverLibraryItems.isNotEmpty() && selectedLibraryId == libraryId) { cb(serverLibraryItems) } else { apiHandler.getLibraryItems(libraryId) { libraryItems -> - serverLibraryItems = libraryItems - cb(libraryItems) + val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() } + if (libraryItemsWithAudio.isNotEmpty()) selectedLibraryId = libraryId + + serverLibraryItems = libraryItemsWithAudio + cb(libraryItemsWithAudio) } } } + fun loadLibraryItem(libraryItemId:String, cb: (LibraryItemWrapper?) -> Unit) { + if (libraryItemId.startsWith("local")) { + cb(DeviceManager.dbManager.getLocalLibraryItem(libraryItemId)) + } else { + Log.d(tag, "loadLibraryItem: $libraryItemId") + apiHandler.getLibraryItem(libraryItemId) { libraryItem -> + Log.d(tag, "loadLibraryItem: Got library item $libraryItem") + cb(libraryItem) + } + } + } + + fun loadPodcastEpisodeMediaBrowserItems(libraryItemId:String, cb: (MutableList) -> Unit) { + loadLibraryItem(libraryItemId) { libraryItemWrapper -> + Log.d(tag, "Loaded Podcast library item $libraryItemWrapper") + + selectedLibraryItemWrapper = libraryItemWrapper + + libraryItemWrapper?.let { + if (libraryItemWrapper is LocalLibraryItem) { // Local podcast episodes + if (libraryItemWrapper.mediaType != "podcast" || libraryItemWrapper.media.getAudioTracks().isEmpty()) { + serverPodcastEpisodes = listOf() + cb(mutableListOf()) + } else { + val podcast = libraryItemWrapper.media as Podcast + serverPodcastEpisodes = podcast.episodes ?: listOf() + selectedLibraryItemId = libraryItemWrapper.id + selectedPodcast = podcast + + val children = podcast.episodes?.map { podcastEpisode -> + Log.d(tag, "Local Podcast Episode ${podcastEpisode.title} | ${podcastEpisode.id}") + MediaBrowserCompat.MediaItem(podcastEpisode.getMediaMetadata(libraryItemWrapper).description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) + } + children?.let { cb(children as MutableList) } ?: cb(mutableListOf()) + } + } else if (libraryItemWrapper is LibraryItem) { // Server podcast episodes + if (libraryItemWrapper.mediaType != "podcast" || libraryItemWrapper.media.getAudioTracks().isEmpty()) { + serverPodcastEpisodes = listOf() + cb(mutableListOf()) + } else { + val podcast = libraryItemWrapper.media as Podcast + serverPodcastEpisodes = podcast.episodes ?: listOf() + selectedLibraryItemId = libraryItemWrapper.id + selectedPodcast = podcast + + val children = podcast.episodes?.map { podcastEpisode -> + MediaBrowserCompat.MediaItem(podcastEpisode.getMediaMetadata(libraryItemWrapper).description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) + } + children?.let { cb(children as MutableList) } ?: cb(mutableListOf()) + } + } + } + } + } + fun loadLibraries(cb: (List) -> Unit) { if (serverLibraries.isNotEmpty()) { cb(serverLibraries) @@ -69,8 +139,8 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { return cats } - fun loadAndroidAutoItems(libraryId:String, cb: (List) -> Unit) { - Log.d(tag, "Load android auto items for library id $libraryId") + fun loadAndroidAutoItems(cb: (List) -> Unit) { + Log.d(tag, "Load android auto items") val cats = mutableListOf() val localCategories = loadLocalCategory() @@ -84,26 +154,21 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { } loadLibraries { libraries -> - val library = libraries.find { it.id == libraryId } ?: libraries[0] + val library = libraries[0] Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}") - loadLibraryCategories(libraryId) { libraryCategories -> + loadLibraryCategories(library.id) { libraryCategories -> // Only using book or podcast library categories for now libraryCategories.forEach { - Log.d(tag, "Found library category ${it.label} with type ${it.type}") + // Log.d(tag, "Found library category ${it.label} with type ${it.type}") if (it.type == library.mediaType) { - Log.d(tag, "Using library category ${it.id}") + // Log.d(tag, "Using library category ${it.id}") cats.add(it) } } - loadLibraryItems(libraryId) { libraryItems -> - val mainCat = LibraryCategory("library", "Library", library.mediaType, libraryItems, false) - cats.add(mainCat) - - cb(cats) - } + cb(cats) } } } else { // Not connected/no internet sent downloaded cats only @@ -120,6 +185,19 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { } } + fun getPodcastWithEpisodeByEpisodeId(id:String) : LibraryItemWithEpisode? { + if (id.startsWith("local")) { + return DeviceManager.dbManager.getLocalLibraryItemWithEpisode(id) + } else { + val podcastEpisode = serverPodcastEpisodes.find { it.id == id } + return if (podcastEpisode != null && selectedLibraryItemWrapper != null) { + LibraryItemWithEpisode(selectedLibraryItemWrapper!!, podcastEpisode) + } else { + null + } + } + } + fun getById(id:String) : LibraryItemWrapper? { if (id.startsWith("local")) { return DeviceManager.dbManager.getLocalLibraryItem(id) @@ -135,13 +213,13 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { } } - fun play(libraryItemWrapper:LibraryItemWrapper, mediaPlayer:String, cb: (PlaybackSession) -> Unit) { + fun play(libraryItemWrapper:LibraryItemWrapper, episode:PodcastEpisode?, mediaPlayer:String, cb: (PlaybackSession) -> Unit) { if (libraryItemWrapper is LocalLibraryItem) { val localLibraryItem = libraryItemWrapper as LocalLibraryItem - cb(localLibraryItem.getPlaybackSession(null)) + cb(localLibraryItem.getPlaybackSession(episode)) } else { val libraryItem = libraryItemWrapper as LibraryItem - apiHandler.playLibraryItem(libraryItem.id,"",false, mediaPlayer) { + apiHandler.playLibraryItem(libraryItem.id,episode?.id ?: "",false, mediaPlayer) { cb(it) } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/BrowseTree.kt b/android/app/src/main/java/com/audiobookshelf/app/player/BrowseTree.kt index a80a0b99..316abc51 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/BrowseTree.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/BrowseTree.kt @@ -7,14 +7,15 @@ import android.support.v4.media.MediaMetadataCompat import android.util.Log import androidx.annotation.AnyRes import com.audiobookshelf.app.R +import com.audiobookshelf.app.data.Library import com.audiobookshelf.app.data.LibraryCategory import com.audiobookshelf.app.data.LibraryItem import com.audiobookshelf.app.data.LocalLibraryItem - class BrowseTree( val context: Context, - libraryCategories: List + libraryCategories: List, + libraries: List ) { private val mediaIdToChildren = mutableMapOf>() @@ -41,18 +42,18 @@ class BrowseTree( putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_localaudio).toString()) }.build() - val allMetadata = MediaMetadataCompat.Builder().apply { - putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, ALL_ROOT) - putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Library Items") - putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_books).toString()) - }.build() - val downloadsMetadata = MediaMetadataCompat.Builder().apply { putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT) putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads") putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_downloaddone).toString()) }.build() + val librariesMetadata = MediaMetadataCompat.Builder().apply { + putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, LIBRARIES_ROOT) + putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Libraries") + putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.icon_library_folder).toString()) + }.build() + // Server continue Listening cat libraryCategories.find { it.id == "continue-listening" }?.let { continueListeningCategory -> val continueListeningMediaMetadata = continueListeningCategory.entities.map { liw -> @@ -69,30 +70,32 @@ class BrowseTree( } } - rootList += allMetadata - rootList += downloadsMetadata + if (libraries.isNotEmpty()) { + rootList += librariesMetadata - // Server library cat - libraryCategories.find { it.id == "library" }?.let { libraryCategory -> - val libraryMediaMetadata = libraryCategory.entities.map { libc -> - val libraryItem = libc as LibraryItem - libraryItem.getMediaMetadata() - } - libraryMediaMetadata.forEach { - val children = mediaIdToChildren[ALL_ROOT] ?: mutableListOf() - children += it - mediaIdToChildren[ALL_ROOT] = children + libraries.forEach { library -> + val libraryMediaMetadata = library.getMediaMetadata() + val children = mediaIdToChildren[LIBRARIES_ROOT] ?: mutableListOf() + children += libraryMediaMetadata + mediaIdToChildren[LIBRARIES_ROOT] = children } } + rootList += downloadsMetadata libraryCategories.find { it.id == "local-books" }?.let { localBooksCat -> - val localMediaMetadata = localBooksCat.entities.map { libc -> + localBooksCat.entities.forEach { libc -> val libraryItem = libc as LocalLibraryItem - libraryItem.getMediaMetadata(context) + val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf() + children += libraryItem.getMediaMetadata(context) + mediaIdToChildren[DOWNLOADS_ROOT] = children } - localMediaMetadata.forEach { + } + + libraryCategories.find { it.id == "local-podcasts" }?.let { localPodcastsCat -> + localPodcastsCat.entities.forEach { libc -> + val libraryItem = libc as LocalLibraryItem val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf() - children += it + children += libraryItem.getMediaMetadata(context) mediaIdToChildren[DOWNLOADS_ROOT] = children } } @@ -104,6 +107,6 @@ class BrowseTree( } const val AUTO_BROWSE_ROOT = "/" -const val ALL_ROOT = "__ALL__" const val CONTINUE_ROOT = "__CONTINUE__" const val DOWNLOADS_ROOT = "__DOWNLOADS__" +const val LIBRARIES_ROOT = "__LIBRARIES__" diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt b/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt index 512ad91a..c3609e38 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt @@ -11,6 +11,7 @@ import android.util.Log import android.view.KeyEvent import com.audiobookshelf.app.data.LibraryItem import com.audiobookshelf.app.data.LibraryItemWrapper +import com.audiobookshelf.app.data.PodcastEpisode import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -27,7 +28,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi override fun onPrepare() { Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT") playerNotificationService.mediaManager.getFirstItem()?.let { li -> - playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) { + playerNotificationService.mediaManager.play(li, null, playerNotificationService.getMediaPlayer()) { Log.d(tag, "About to prepare player with ${it.displayTitle}") Handler(Looper.getMainLooper()).post() { playerNotificationService.preparePlayer(it,true,null) @@ -49,7 +50,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi override fun onPlayFromSearch(query: String?, extras: Bundle?) { Log.d(tag, "ON PLAY FROM SEARCH $query") playerNotificationService.mediaManager.getFromSearch(query)?.let { li -> - playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) { + playerNotificationService.mediaManager.play(li, null, playerNotificationService.getMediaPlayer()) { Log.d(tag, "About to prepare player with ${it.displayTitle}") Handler(Looper.getMainLooper()).post() { playerNotificationService.preparePlayer(it,true,null) @@ -90,14 +91,20 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) { Log.d(tag, "ON PLAY FROM MEDIA ID $mediaId") var libraryItemWrapper: LibraryItemWrapper? = null + var podcastEpisode: PodcastEpisode? = null + if (mediaId.isNullOrEmpty()) { libraryItemWrapper = playerNotificationService.mediaManager.getFirstItem() + } else if (mediaId.startsWith("ep_") || mediaId.startsWith("local_ep_")) { // Playing podcast episode + val libraryItemWithEpisode = playerNotificationService.mediaManager.getPodcastWithEpisodeByEpisodeId(mediaId) + libraryItemWrapper = libraryItemWithEpisode?.libraryItemWrapper + podcastEpisode = libraryItemWithEpisode?.episode } else { libraryItemWrapper = playerNotificationService.mediaManager.getById(mediaId) } libraryItemWrapper?.let { li -> - playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) { + playerNotificationService.mediaManager.play(li, podcastEpisode, playerNotificationService.getMediaPlayer()) { Log.d(tag, "About to prepare player with ${it.displayTitle}") Handler(Looper.getMainLooper()).post() { playerNotificationService.preparePlayer(it,true,null) diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionPlaybackPreparer.kt b/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionPlaybackPreparer.kt index 109b755d..7c37f2c5 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionPlaybackPreparer.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionPlaybackPreparer.kt @@ -7,8 +7,8 @@ import android.os.Looper import android.os.ResultReceiver import android.support.v4.media.session.PlaybackStateCompat import android.util.Log -import com.audiobookshelf.app.data.LibraryItem import com.audiobookshelf.app.data.LibraryItemWrapper +import com.audiobookshelf.app.data.PodcastEpisode import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector @@ -30,7 +30,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat override fun onPrepare(playWhenReady: Boolean) { Log.d(tag, "ON PREPARE $playWhenReady") playerNotificationService.mediaManager.getFirstItem()?.let { li -> - playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) { + playerNotificationService.mediaManager.play(li, null, playerNotificationService.getMediaPlayer()) { Handler(Looper.getMainLooper()).post() { playerNotificationService.preparePlayer(it,playWhenReady,null) } @@ -41,9 +41,19 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) { Log.d(tag, "ON PREPARE FROM MEDIA ID $mediaId $playWhenReady") - var libraryItemWrapper: LibraryItemWrapper? = playerNotificationService.mediaManager.getById(mediaId) + var libraryItemWrapper: LibraryItemWrapper? = null + var podcastEpisode: PodcastEpisode? = null + + if (mediaId.startsWith("ep_") || mediaId.startsWith("local_ep_")) { // Playing podcast episode + val libraryItemWithEpisode = playerNotificationService.mediaManager.getPodcastWithEpisodeByEpisodeId(mediaId) + libraryItemWrapper = libraryItemWithEpisode?.libraryItemWrapper + podcastEpisode = libraryItemWithEpisode?.episode + } else { + libraryItemWrapper = playerNotificationService.mediaManager.getById(mediaId) + } + libraryItemWrapper?.let { li -> - playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) { + playerNotificationService.mediaManager.play(li, podcastEpisode, playerNotificationService.getMediaPlayer()) { Log.d(tag, "About to prepare player with ${it.displayTitle}") Handler(Looper.getMainLooper()).post() { playerNotificationService.preparePlayer(it,playWhenReady,null) @@ -55,7 +65,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) { Log.d(tag, "ON PREPARE FROM SEARCH $query") playerNotificationService.mediaManager.getFromSearch(query)?.let { li -> - playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) { + playerNotificationService.mediaManager.play(li, null, playerNotificationService.getMediaPlayer()) { Log.d(tag, "About to prepare player with ${it.displayTitle}") Handler(Looper.getMainLooper()).post() { playerNotificationService.preparePlayer(it,playWhenReady,null) diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt index 22f599b5..39310a9b 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt @@ -9,6 +9,7 @@ import android.hardware.SensorManager import android.os.* import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.MediaDescriptionCompat +import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.session.MediaControllerCompat import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.PlaybackStateCompat @@ -565,6 +566,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { private val AUTO_MEDIA_ROOT = "/" private val ALL_ROOT = "__ALL__" + private val LIBRARIES_ROOT = "__LIBRARIES__" private lateinit var browseTree:BrowseTree @@ -610,32 +612,66 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { override fun onLoadChildren(parentMediaId: String, result: Result>) { Log.d(tag, "ON LOAD CHILDREN $parentMediaId") - val flag = if (parentMediaId == AUTO_MEDIA_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + var flag = if (parentMediaId == AUTO_MEDIA_ROOT || parentMediaId == LIBRARIES_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE result.detach() - mediaManager.loadAndroidAutoItems("main") { libraryCategories -> - browseTree = BrowseTree(this, libraryCategories) - val children = browseTree[parentMediaId]?.map { item -> - MediaBrowserCompat.MediaItem(item.description, flag) + if (parentMediaId.startsWith("li_") || parentMediaId.startsWith("local_")) { // Show podcast episodes + Log.d(tag, "Loading podcast episodes") + mediaManager.loadPodcastEpisodeMediaBrowserItems(parentMediaId) { + result.sendResult(it) } - result.sendResult(children as MutableList?) - } + } else if (::browseTree.isInitialized && browseTree[parentMediaId] == null && mediaManager.getIsLibrary(parentMediaId)) { // Load library items for library - // TODO: For using sub menus. Check if this is the root menu: -// if (AUTO_MEDIA_ROOT == parentMediaId) { - // build the MediaItem objects for the top level, - // and put them in the mediaItems list -// } else { - // examine the passed parentMediaId to see which submenu we're at, - // and put the children of that menu in the mediaItems list -// } + mediaManager.loadLibraryItemsWithAudio(parentMediaId) { libraryItems -> + val children = libraryItems.map { libraryItem -> + val libraryItemMediaMetadata = libraryItem.getMediaMetadata() + + if (libraryItem.mediaType == "podcast") { // Podcasts are browseable + flag = MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + } + + MediaBrowserCompat.MediaItem(libraryItemMediaMetadata.description, flag) + } + result.sendResult(children as MutableList?) + } + } else if (parentMediaId == "__DOWNLOADS__") { // Load downloads + + val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book") + val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast") + val localBrowseItems:MutableList = mutableListOf() + + localBooks.forEach { localLibraryItem -> + val mediaMetadata = localLibraryItem.getMediaMetadata(ctx) + localBrowseItems += MediaBrowserCompat.MediaItem(mediaMetadata.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) + } + + localPodcasts.forEach { localLibraryItem -> + val mediaMetadata = localLibraryItem.getMediaMetadata(ctx) + localBrowseItems += MediaBrowserCompat.MediaItem(mediaMetadata.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) + } + + result.sendResult(localBrowseItems) + + } else { // Load categories + + mediaManager.loadAndroidAutoItems() { libraryCategories -> + browseTree = BrowseTree(this, libraryCategories, mediaManager.serverLibraries) + + val children = browseTree[parentMediaId]?.map { item -> + Log.d(tag, "Loading Browser Media Item ${item.description.title} $flag") + + MediaBrowserCompat.MediaItem(item.description, flag) + } + result.sendResult(children as MutableList?) + } + } } override fun onSearch(query: String, extras: Bundle?, result: Result>) { result.detach() - mediaManager.loadAndroidAutoItems("main") { libraryCategories -> - browseTree = BrowseTree(this, libraryCategories) + mediaManager.loadAndroidAutoItems() { libraryCategories -> + browseTree = BrowseTree(this, libraryCategories, mediaManager.serverLibraries) val children = browseTree[ALL_ROOT]?.map { item -> MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) } diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt index e90f3641..0b05ade7 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt @@ -277,7 +277,7 @@ class AbsDownloader : Plugin() { finalDestinationFile.delete() } - var downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,null) + var downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,episode) downloadItem.downloadItemParts.add(downloadItemPart) var dlRequest = downloadItemPart.getDownloadRequest() @@ -294,7 +294,7 @@ class AbsDownloader : Plugin() { if (finalDestinationFile.exists()) { Log.d(tag, "Podcast cover already exists - not downloading cover again") } else { - downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,null) + downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,null) downloadItem.downloadItemParts.add(downloadItemPart) dlRequest = downloadItemPart.getDownloadRequest() diff --git a/android/app/src/main/res/drawable-anydpi-v24/exo_icon_books.xml b/android/app/src/main/res/drawable-anydpi/icon_library_folder.xml similarity index 60% rename from android/app/src/main/res/drawable-anydpi-v24/exo_icon_books.xml rename to android/app/src/main/res/drawable-anydpi/icon_library_folder.xml index 0072c6e5..2eb0c3cd 100644 --- a/android/app/src/main/res/drawable-anydpi-v24/exo_icon_books.xml +++ b/android/app/src/main/res/drawable-anydpi/icon_library_folder.xml @@ -10,9 +10,6 @@ android:translateY="-1.5294118"> - + android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z"/> diff --git a/android/app/src/main/res/drawable-hdpi/exo_icon_books.png b/android/app/src/main/res/drawable-hdpi/exo_icon_books.png deleted file mode 100644 index 5e15a4a9..00000000 Binary files a/android/app/src/main/res/drawable-hdpi/exo_icon_books.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-hdpi/icon_library_folder.png b/android/app/src/main/res/drawable-hdpi/icon_library_folder.png new file mode 100644 index 00000000..eee3872a Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/icon_library_folder.png differ diff --git a/android/app/src/main/res/drawable-mdpi/exo_icon_books.png b/android/app/src/main/res/drawable-mdpi/exo_icon_books.png deleted file mode 100644 index 3dbd2199..00000000 Binary files a/android/app/src/main/res/drawable-mdpi/exo_icon_books.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-mdpi/icon_library_folder.png b/android/app/src/main/res/drawable-mdpi/icon_library_folder.png new file mode 100644 index 00000000..6a91eea7 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/icon_library_folder.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/exo_icon_books.png b/android/app/src/main/res/drawable-xhdpi/exo_icon_books.png deleted file mode 100644 index f432c030..00000000 Binary files a/android/app/src/main/res/drawable-xhdpi/exo_icon_books.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xhdpi/icon_library_folder.png b/android/app/src/main/res/drawable-xhdpi/icon_library_folder.png new file mode 100644 index 00000000..90335438 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/icon_library_folder.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/exo_icon_books.png b/android/app/src/main/res/drawable-xxhdpi/exo_icon_books.png deleted file mode 100644 index 213a80e8..00000000 Binary files a/android/app/src/main/res/drawable-xxhdpi/exo_icon_books.png and /dev/null differ diff --git a/android/app/src/main/res/drawable-xxhdpi/icon_library_folder.png b/android/app/src/main/res/drawable-xxhdpi/icon_library_folder.png new file mode 100644 index 00000000..fe9ee616 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/icon_library_folder.png differ diff --git a/components/app/Appbar.vue b/components/app/Appbar.vue index 737d43ef..21d17a4c 100644 --- a/components/app/Appbar.vue +++ b/components/app/Appbar.vue @@ -7,7 +7,7 @@ arrow_back -
+

{{ currentLibraryName }}

@@ -51,14 +51,11 @@ export default { this.$store.commit('setCastAvailable', val) } }, - socketConnected() { - return this.$store.state.socketConnected - }, currentLibrary() { return this.$store.getters['libraries/getCurrentLibrary'] }, currentLibraryName() { - return this.currentLibrary ? this.currentLibrary.name : 'Main' + return this.currentLibrary ? this.currentLibrary.name : '' }, currentLibraryIcon() { return this.currentLibrary ? this.currentLibrary.icon : 'database' diff --git a/ios/App/Podfile b/ios/App/Podfile index dcdae730..89125e80 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -9,12 +9,12 @@ install! 'cocoapods', :disable_input_output_paths => true def capacitor_pods pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' - pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' - pod 'CapacitorDialog', :path => '../../node_modules/@capacitor/dialog' - pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics' - pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network' - pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar' - pod 'CapacitorStorage', :path => '../../node_modules/@capacitor/storage' + pod 'CapacitorApp', :path => '..\..\node_modules\@capacitor\app' + pod 'CapacitorDialog', :path => '..\..\node_modules\@capacitor\dialog' + pod 'CapacitorHaptics', :path => '..\..\node_modules\@capacitor\haptics' + pod 'CapacitorNetwork', :path => '..\..\node_modules\@capacitor\network' + pod 'CapacitorStatusBar', :path => '..\..\node_modules\@capacitor\status-bar' + pod 'CapacitorStorage', :path => '..\..\node_modules\@capacitor\storage' end target 'App' do