Update:Android auto to show libraries and browseable podcasts, Fix:Download podcast set episodes and cover art correctly
|
@ -42,6 +42,15 @@ data class LibraryItem(
|
||||||
return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}")
|
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
|
@JsonIgnore
|
||||||
fun getMediaMetadata(): MediaMetadataCompat {
|
fun getMediaMetadata(): MediaMetadataCompat {
|
||||||
return MediaMetadataCompat.Builder().apply {
|
return MediaMetadataCompat.Builder().apply {
|
||||||
|
@ -74,6 +83,7 @@ open class MediaType(var metadata:MediaTypeMetadata, var coverPath:String?) {
|
||||||
open fun removeAudioTrack(localFileId:String) { }
|
open fun removeAudioTrack(localFileId:String) { }
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
open fun getLocalCopy():MediaType { return MediaType(MediaTypeMetadata(""),null) }
|
open fun getLocalCopy():MediaType { return MediaType(MediaTypeMetadata(""),null) }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
@ -82,7 +92,8 @@ class Podcast(
|
||||||
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,
|
||||||
|
var numEpisodes:Int?
|
||||||
) : MediaType(metadata, coverPath) {
|
) : MediaType(metadata, coverPath) {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getAudioTracks():List<AudioTrack> {
|
override fun getAudioTracks():List<AudioTrack> {
|
||||||
|
@ -99,7 +110,7 @@ class Podcast(
|
||||||
// Add new episodes
|
// Add new episodes
|
||||||
audioTracks.forEach { at ->
|
audioTracks.forEach { at ->
|
||||||
if (episodes?.find{ it.audioTrack?.localFileId == at.localFileId } == null) {
|
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)
|
episodes?.add(newEpisode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,7 +158,7 @@ class Podcast(
|
||||||
// Used for FolderScanner local podcast item to get copy of Podcast excluding episodes
|
// Used for FolderScanner local podcast item to get copy of Podcast excluding episodes
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getLocalCopy(): Podcast {
|
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<BookChapter>?,
|
var chapters:List<BookChapter>?,
|
||||||
var tracks:MutableList<AudioTrack>?,
|
var tracks:MutableList<AudioTrack>?,
|
||||||
var size:Long?,
|
var size:Long?,
|
||||||
var duration:Double?
|
var duration:Double?,
|
||||||
|
var numTracks:Int?
|
||||||
) : MediaType(metadata, coverPath) {
|
) : MediaType(metadata, coverPath) {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getAudioTracks():List<AudioTrack> {
|
override fun getAudioTracks():List<AudioTrack> {
|
||||||
|
@ -209,7 +221,7 @@ class Book(
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getLocalCopy(): Book {
|
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 duration:Double?,
|
||||||
var size:Long?,
|
var size:Long?,
|
||||||
var serverEpisodeId:String? // For local podcasts to match with server podcasts
|
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)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class LibraryFile(
|
data class LibraryFile(
|
||||||
|
@ -312,7 +346,16 @@ data class Library(
|
||||||
var folders:MutableList<Folder>,
|
var folders:MutableList<Folder>,
|
||||||
var icon:String,
|
var icon:String,
|
||||||
var mediaType: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)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class Folder(
|
data class Folder(
|
||||||
|
@ -371,3 +414,9 @@ data class MediaProgress(
|
||||||
var startedAt:Long,
|
var startedAt:Long,
|
||||||
var finishedAt:Long?
|
var finishedAt:Long?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Helper class
|
||||||
|
data class LibraryItemWithEpisode(
|
||||||
|
var libraryItemWrapper:LibraryItemWrapper,
|
||||||
|
var episode:PodcastEpisode
|
||||||
|
)
|
||||||
|
|
|
@ -42,6 +42,20 @@ class DbManager {
|
||||||
return Paper.book("localLibraryItems").read(localLibraryItemId)
|
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) {
|
fun removeLocalLibraryItem(localLibraryItemId:String) {
|
||||||
Paper.book("localLibraryItems").delete(localLibraryItemId)
|
Paper.book("localLibraryItems").delete(localLibraryItemId)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,20 @@
|
||||||
package com.audiobookshelf.app.data
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.R
|
import com.audiobookshelf.app.R
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
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.JsonIgnore
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
|
|
@ -58,13 +58,13 @@ data class LocalMediaItem(
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getLocalLibraryItem():LocalLibraryItem {
|
fun getLocalLibraryItem():LocalLibraryItem {
|
||||||
var mediaMetadata = getMediaMetadata()
|
val mediaMetadata = getMediaMetadata()
|
||||||
if (mediaType == "book") {
|
if (mediaType == "book") {
|
||||||
var chapters = getAudiobookChapters()
|
val chapters = getAudiobookChapters()
|
||||||
var book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration())
|
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)
|
return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true,null,null,null,null)
|
||||||
} else {
|
} 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
|
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)
|
return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false, mediaType, podcast,localFiles,coverContentUrl, coverAbsolutePath, true, null,null,null,null)
|
||||||
}
|
}
|
||||||
|
|
|
@ -288,7 +288,7 @@ class FolderScanner(var ctx: Context) {
|
||||||
val audioProbeResult = probeAudioFile(localFile.absolutePath)
|
val audioProbeResult = probeAudioFile(localFile.absolutePath)
|
||||||
|
|
||||||
// Create new audio track
|
// 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)
|
audioTracks.add(track)
|
||||||
|
|
||||||
Log.d(tag, "scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}")
|
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
|
// Add podcast episodes to library
|
||||||
itemPart.episode?.let { podcastEpisode ->
|
itemPart.episode?.let { podcastEpisode ->
|
||||||
val podcast = localLibraryItem.media as Podcast
|
val podcast = localLibraryItem.media as Podcast
|
||||||
var newEpisode = podcast.addEpisode(track, podcastEpisode)
|
val newEpisode = podcast.addEpisode(track, podcastEpisode)
|
||||||
localEpisodeId = newEpisode.id
|
localEpisodeId = newEpisode.id
|
||||||
Log.d(tag, "scanDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}")
|
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? {
|
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) {
|
if (df == null) {
|
||||||
Log.e(tag, "Item Folder Doc File Invalid ${localLibraryItem.absolutePath}")
|
Log.e(tag, "Item Folder Doc File Invalid ${localLibraryItem.absolutePath}")
|
||||||
|
@ -377,7 +377,7 @@ class FolderScanner(var ctx: Context) {
|
||||||
var wasUpdated = false
|
var wasUpdated = false
|
||||||
|
|
||||||
// Search for files in media item folder
|
// 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}")
|
Log.d(tag, "scanLocalLibraryItem ${filesFound.size} files found in ${localLibraryItem.absolutePath}")
|
||||||
|
|
||||||
filesFound.forEach {
|
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
|
// 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 ->
|
existingLocalFileIds.forEach { localFileId ->
|
||||||
Log.d(tag, "Checking local file id is there $localFileId")
|
Log.d(tag, "Checking local file id is there $localFileId")
|
||||||
if (filesFound.find { DeviceManager.getBase64Id(it.id) == localFileId } == null) {
|
if (filesFound.find { DeviceManager.getBase64Id(it.id) == localFileId } == null) {
|
||||||
|
@ -407,12 +407,12 @@ class FolderScanner(var ctx: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
filesFound.forEach { docFile ->
|
filesFound.forEach { docFile ->
|
||||||
var localFileId = DeviceManager.getBase64Id(docFile.id)
|
val localFileId = DeviceManager.getBase64Id(docFile.id)
|
||||||
var existingLocalFile = localLibraryItem.localFiles.find { it.id == localFileId }
|
val existingLocalFile = localLibraryItem.localFiles.find { it.id == localFileId }
|
||||||
|
|
||||||
if (existingLocalFile == null || (existingLocalFile.isAudioFile() && forceAudioProbe)) {
|
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) {
|
if (existingLocalFile == null) {
|
||||||
localLibraryItem.localFiles.add(localFile)
|
localLibraryItem.localFiles.add(localFile)
|
||||||
Log.d(tag, "scanLocalLibraryItem new file found ${localFile.filename}")
|
Log.d(tag, "scanLocalLibraryItem new file found ${localFile.filename}")
|
||||||
|
@ -420,22 +420,26 @@ class FolderScanner(var ctx: Context) {
|
||||||
|
|
||||||
if (localFile.isAudioFile()) {
|
if (localFile.isAudioFile()) {
|
||||||
// TODO: Make asynchronous
|
// 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
|
audioTrack.localFileId == localFile.id
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingTrack == null) {
|
if (existingTrack == null) {
|
||||||
// Create new audio track
|
// Create new audio track
|
||||||
var lastTrack = existingAudioTracks.lastOrNull()
|
val lastTrack = existingAudioTracks.lastOrNull()
|
||||||
var startOffset = (lastTrack?.startOffset ?: 0.0) + (lastTrack?.duration ?: 0.0)
|
val 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 track = AudioTrack(existingAudioTracks.size, startOffset, audioProbeResult.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, null)
|
||||||
localLibraryItem.media.addAudioTrack(track)
|
localLibraryItem.media.addAudioTrack(track)
|
||||||
|
Log.d(tag, "Added New Audio Track ${track.title}")
|
||||||
wasUpdated = true
|
wasUpdated = true
|
||||||
} else {
|
} else {
|
||||||
existingTrack.audioProbeResult = audioProbeResult
|
existingTrack.audioProbeResult = audioProbeResult
|
||||||
// TODO: Update data found from probe
|
// TODO: Update data found from probe
|
||||||
|
|
||||||
|
Log.d(tag, "Updated Audio Track Probe Data ${existingTrack.title}")
|
||||||
|
|
||||||
wasUpdated = true
|
wasUpdated = true
|
||||||
}
|
}
|
||||||
} else { // Check if cover is empty
|
} else { // Check if cover is empty
|
||||||
|
|
|
@ -2,6 +2,8 @@ package com.audiobookshelf.app.media
|
||||||
|
|
||||||
import android.bluetooth.BluetoothClass
|
import android.bluetooth.BluetoothClass
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.support.v4.media.MediaBrowserCompat
|
||||||
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.data.*
|
import com.audiobookshelf.app.data.*
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
|
@ -14,6 +16,12 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
||||||
val tag = "MediaManager"
|
val tag = "MediaManager"
|
||||||
|
|
||||||
var serverLibraryItems = listOf<LibraryItem>()
|
var serverLibraryItems = listOf<LibraryItem>()
|
||||||
|
var selectedLibraryId = ""
|
||||||
|
|
||||||
|
var selectedLibraryItemWrapper:LibraryItemWrapper? = null
|
||||||
|
var selectedPodcast:Podcast? = null
|
||||||
|
var selectedLibraryItemId:String? = null
|
||||||
|
var serverPodcastEpisodes = listOf<PodcastEpisode>()
|
||||||
var serverLibraryCategories = listOf<LibraryCategory>()
|
var serverLibraryCategories = listOf<LibraryCategory>()
|
||||||
var serverLibraries = listOf<Library>()
|
var serverLibraries = listOf<Library>()
|
||||||
|
|
||||||
|
@ -22,6 +30,10 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
||||||
Paper.init(ctx)
|
Paper.init(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getIsLibrary(id:String) : Boolean {
|
||||||
|
return serverLibraries.find { it.id == id } != null
|
||||||
|
}
|
||||||
|
|
||||||
fun loadLibraryCategories(libraryId:String, cb: (List<LibraryCategory>) -> Unit) {
|
fun loadLibraryCategories(libraryId:String, cb: (List<LibraryCategory>) -> Unit) {
|
||||||
if (serverLibraryCategories.isNotEmpty()) {
|
if (serverLibraryCategories.isNotEmpty()) {
|
||||||
cb(serverLibraryCategories)
|
cb(serverLibraryCategories)
|
||||||
|
@ -33,17 +45,75 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadLibraryItems(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
|
fun loadLibraryItemsWithAudio(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
|
||||||
if (serverLibraryItems.isNotEmpty()) {
|
if (serverLibraryItems.isNotEmpty() && selectedLibraryId == libraryId) {
|
||||||
cb(serverLibraryItems)
|
cb(serverLibraryItems)
|
||||||
} else {
|
} else {
|
||||||
apiHandler.getLibraryItems(libraryId) { libraryItems ->
|
apiHandler.getLibraryItems(libraryId) { libraryItems ->
|
||||||
serverLibraryItems = libraryItems
|
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
|
||||||
cb(libraryItems)
|
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<MediaBrowserCompat.MediaItem>) -> 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<Library>) -> Unit) {
|
fun loadLibraries(cb: (List<Library>) -> Unit) {
|
||||||
if (serverLibraries.isNotEmpty()) {
|
if (serverLibraries.isNotEmpty()) {
|
||||||
cb(serverLibraries)
|
cb(serverLibraries)
|
||||||
|
@ -69,8 +139,8 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
||||||
return cats
|
return cats
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadAndroidAutoItems(libraryId:String, cb: (List<LibraryCategory>) -> Unit) {
|
fun loadAndroidAutoItems(cb: (List<LibraryCategory>) -> Unit) {
|
||||||
Log.d(tag, "Load android auto items for library id $libraryId")
|
Log.d(tag, "Load android auto items")
|
||||||
val cats = mutableListOf<LibraryCategory>()
|
val cats = mutableListOf<LibraryCategory>()
|
||||||
|
|
||||||
val localCategories = loadLocalCategory()
|
val localCategories = loadLocalCategory()
|
||||||
|
@ -84,26 +154,21 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
loadLibraries { libraries ->
|
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}")
|
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
|
// Only using book or podcast library categories for now
|
||||||
libraryCategories.forEach {
|
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) {
|
if (it.type == library.mediaType) {
|
||||||
Log.d(tag, "Using library category ${it.id}")
|
// Log.d(tag, "Using library category ${it.id}")
|
||||||
cats.add(it)
|
cats.add(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadLibraryItems(libraryId) { libraryItems ->
|
cb(cats)
|
||||||
val mainCat = LibraryCategory("library", "Library", library.mediaType, libraryItems, false)
|
|
||||||
cats.add(mainCat)
|
|
||||||
|
|
||||||
cb(cats)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else { // Not connected/no internet sent downloaded cats only
|
} 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? {
|
fun getById(id:String) : LibraryItemWrapper? {
|
||||||
if (id.startsWith("local")) {
|
if (id.startsWith("local")) {
|
||||||
return DeviceManager.dbManager.getLocalLibraryItem(id)
|
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) {
|
if (libraryItemWrapper is LocalLibraryItem) {
|
||||||
val localLibraryItem = libraryItemWrapper as LocalLibraryItem
|
val localLibraryItem = libraryItemWrapper as LocalLibraryItem
|
||||||
cb(localLibraryItem.getPlaybackSession(null))
|
cb(localLibraryItem.getPlaybackSession(episode))
|
||||||
} else {
|
} else {
|
||||||
val libraryItem = libraryItemWrapper as LibraryItem
|
val libraryItem = libraryItemWrapper as LibraryItem
|
||||||
apiHandler.playLibraryItem(libraryItem.id,"",false, mediaPlayer) {
|
apiHandler.playLibraryItem(libraryItem.id,episode?.id ?: "",false, mediaPlayer) {
|
||||||
cb(it)
|
cb(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,14 +7,15 @@ import android.support.v4.media.MediaMetadataCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.AnyRes
|
import androidx.annotation.AnyRes
|
||||||
import com.audiobookshelf.app.R
|
import com.audiobookshelf.app.R
|
||||||
|
import com.audiobookshelf.app.data.Library
|
||||||
import com.audiobookshelf.app.data.LibraryCategory
|
import com.audiobookshelf.app.data.LibraryCategory
|
||||||
import com.audiobookshelf.app.data.LibraryItem
|
import com.audiobookshelf.app.data.LibraryItem
|
||||||
import com.audiobookshelf.app.data.LocalLibraryItem
|
import com.audiobookshelf.app.data.LocalLibraryItem
|
||||||
|
|
||||||
|
|
||||||
class BrowseTree(
|
class BrowseTree(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
libraryCategories: List<LibraryCategory>
|
libraryCategories: List<LibraryCategory>,
|
||||||
|
libraries: List<Library>
|
||||||
) {
|
) {
|
||||||
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()
|
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()
|
||||||
|
|
||||||
|
@ -41,18 +42,18 @@ class BrowseTree(
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_localaudio).toString())
|
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_localaudio).toString())
|
||||||
}.build()
|
}.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 {
|
val downloadsMetadata = MediaMetadataCompat.Builder().apply {
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT)
|
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT)
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads")
|
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads")
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_downloaddone).toString())
|
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_downloaddone).toString())
|
||||||
}.build()
|
}.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
|
// Server continue Listening cat
|
||||||
libraryCategories.find { it.id == "continue-listening" }?.let { continueListeningCategory ->
|
libraryCategories.find { it.id == "continue-listening" }?.let { continueListeningCategory ->
|
||||||
val continueListeningMediaMetadata = continueListeningCategory.entities.map { liw ->
|
val continueListeningMediaMetadata = continueListeningCategory.entities.map { liw ->
|
||||||
|
@ -69,30 +70,32 @@ class BrowseTree(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootList += allMetadata
|
if (libraries.isNotEmpty()) {
|
||||||
rootList += downloadsMetadata
|
rootList += librariesMetadata
|
||||||
|
|
||||||
// Server library cat
|
libraries.forEach { library ->
|
||||||
libraryCategories.find { it.id == "library" }?.let { libraryCategory ->
|
val libraryMediaMetadata = library.getMediaMetadata()
|
||||||
val libraryMediaMetadata = libraryCategory.entities.map { libc ->
|
val children = mediaIdToChildren[LIBRARIES_ROOT] ?: mutableListOf()
|
||||||
val libraryItem = libc as LibraryItem
|
children += libraryMediaMetadata
|
||||||
libraryItem.getMediaMetadata()
|
mediaIdToChildren[LIBRARIES_ROOT] = children
|
||||||
}
|
|
||||||
libraryMediaMetadata.forEach {
|
|
||||||
val children = mediaIdToChildren[ALL_ROOT] ?: mutableListOf()
|
|
||||||
children += it
|
|
||||||
mediaIdToChildren[ALL_ROOT] = children
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rootList += downloadsMetadata
|
||||||
libraryCategories.find { it.id == "local-books" }?.let { localBooksCat ->
|
libraryCategories.find { it.id == "local-books" }?.let { localBooksCat ->
|
||||||
val localMediaMetadata = localBooksCat.entities.map { libc ->
|
localBooksCat.entities.forEach { libc ->
|
||||||
val libraryItem = libc as LocalLibraryItem
|
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()
|
val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf()
|
||||||
children += it
|
children += libraryItem.getMediaMetadata(context)
|
||||||
mediaIdToChildren[DOWNLOADS_ROOT] = children
|
mediaIdToChildren[DOWNLOADS_ROOT] = children
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,6 +107,6 @@ class BrowseTree(
|
||||||
}
|
}
|
||||||
|
|
||||||
const val AUTO_BROWSE_ROOT = "/"
|
const val AUTO_BROWSE_ROOT = "/"
|
||||||
const val ALL_ROOT = "__ALL__"
|
|
||||||
const val CONTINUE_ROOT = "__CONTINUE__"
|
const val CONTINUE_ROOT = "__CONTINUE__"
|
||||||
const val DOWNLOADS_ROOT = "__DOWNLOADS__"
|
const val DOWNLOADS_ROOT = "__DOWNLOADS__"
|
||||||
|
const val LIBRARIES_ROOT = "__LIBRARIES__"
|
||||||
|
|
|
@ -11,6 +11,7 @@ import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import com.audiobookshelf.app.data.LibraryItem
|
import com.audiobookshelf.app.data.LibraryItem
|
||||||
import com.audiobookshelf.app.data.LibraryItemWrapper
|
import com.audiobookshelf.app.data.LibraryItemWrapper
|
||||||
|
import com.audiobookshelf.app.data.PodcastEpisode
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
@ -27,7 +28,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
||||||
override fun onPrepare() {
|
override fun onPrepare() {
|
||||||
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
|
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
|
||||||
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
|
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}")
|
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,true,null)
|
playerNotificationService.preparePlayer(it,true,null)
|
||||||
|
@ -49,7 +50,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
||||||
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
|
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
|
||||||
Log.d(tag, "ON PLAY FROM SEARCH $query")
|
Log.d(tag, "ON PLAY FROM SEARCH $query")
|
||||||
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
|
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}")
|
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,true,null)
|
playerNotificationService.preparePlayer(it,true,null)
|
||||||
|
@ -90,14 +91,20 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
||||||
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
||||||
Log.d(tag, "ON PLAY FROM MEDIA ID $mediaId")
|
Log.d(tag, "ON PLAY FROM MEDIA ID $mediaId")
|
||||||
var libraryItemWrapper: LibraryItemWrapper? = null
|
var libraryItemWrapper: LibraryItemWrapper? = null
|
||||||
|
var podcastEpisode: PodcastEpisode? = null
|
||||||
|
|
||||||
if (mediaId.isNullOrEmpty()) {
|
if (mediaId.isNullOrEmpty()) {
|
||||||
libraryItemWrapper = playerNotificationService.mediaManager.getFirstItem()
|
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 {
|
} else {
|
||||||
libraryItemWrapper = playerNotificationService.mediaManager.getById(mediaId)
|
libraryItemWrapper = playerNotificationService.mediaManager.getById(mediaId)
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryItemWrapper?.let { li ->
|
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}")
|
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,true,null)
|
playerNotificationService.preparePlayer(it,true,null)
|
||||||
|
|
|
@ -7,8 +7,8 @@ import android.os.Looper
|
||||||
import android.os.ResultReceiver
|
import android.os.ResultReceiver
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.data.LibraryItem
|
|
||||||
import com.audiobookshelf.app.data.LibraryItemWrapper
|
import com.audiobookshelf.app.data.LibraryItemWrapper
|
||||||
|
import com.audiobookshelf.app.data.PodcastEpisode
|
||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
||||||
override fun onPrepare(playWhenReady: Boolean) {
|
override fun onPrepare(playWhenReady: Boolean) {
|
||||||
Log.d(tag, "ON PREPARE $playWhenReady")
|
Log.d(tag, "ON PREPARE $playWhenReady")
|
||||||
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
|
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
|
||||||
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getMediaPlayer()) {
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,playWhenReady,null)
|
playerNotificationService.preparePlayer(it,playWhenReady,null)
|
||||||
}
|
}
|
||||||
|
@ -41,9 +41,19 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
||||||
override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
|
override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
|
||||||
Log.d(tag, "ON PREPARE FROM MEDIA ID $mediaId $playWhenReady")
|
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 ->
|
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}")
|
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,playWhenReady,null)
|
playerNotificationService.preparePlayer(it,playWhenReady,null)
|
||||||
|
@ -55,7 +65,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
||||||
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
|
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
|
||||||
Log.d(tag, "ON PREPARE FROM SEARCH $query")
|
Log.d(tag, "ON PREPARE FROM SEARCH $query")
|
||||||
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
|
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}")
|
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,playWhenReady,null)
|
playerNotificationService.preparePlayer(it,playWhenReady,null)
|
||||||
|
|
|
@ -9,6 +9,7 @@ import android.hardware.SensorManager
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import android.support.v4.media.MediaBrowserCompat
|
import android.support.v4.media.MediaBrowserCompat
|
||||||
import android.support.v4.media.MediaDescriptionCompat
|
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.MediaControllerCompat
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
|
@ -565,6 +566,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
|
|
||||||
private val AUTO_MEDIA_ROOT = "/"
|
private val AUTO_MEDIA_ROOT = "/"
|
||||||
private val ALL_ROOT = "__ALL__"
|
private val ALL_ROOT = "__ALL__"
|
||||||
|
private val LIBRARIES_ROOT = "__LIBRARIES__"
|
||||||
private lateinit var browseTree:BrowseTree
|
private lateinit var browseTree:BrowseTree
|
||||||
|
|
||||||
|
|
||||||
|
@ -610,32 +612,66 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
override fun onLoadChildren(parentMediaId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
override fun onLoadChildren(parentMediaId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
Log.d(tag, "ON LOAD CHILDREN $parentMediaId")
|
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()
|
result.detach()
|
||||||
|
|
||||||
mediaManager.loadAndroidAutoItems("main") { libraryCategories ->
|
if (parentMediaId.startsWith("li_") || parentMediaId.startsWith("local_")) { // Show podcast episodes
|
||||||
browseTree = BrowseTree(this, libraryCategories)
|
Log.d(tag, "Loading podcast episodes")
|
||||||
val children = browseTree[parentMediaId]?.map { item ->
|
mediaManager.loadPodcastEpisodeMediaBrowserItems(parentMediaId) {
|
||||||
MediaBrowserCompat.MediaItem(item.description, flag)
|
result.sendResult(it)
|
||||||
}
|
}
|
||||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
} 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:
|
mediaManager.loadLibraryItemsWithAudio(parentMediaId) { libraryItems ->
|
||||||
// if (AUTO_MEDIA_ROOT == parentMediaId) {
|
val children = libraryItems.map { libraryItem ->
|
||||||
// build the MediaItem objects for the top level,
|
val libraryItemMediaMetadata = libraryItem.getMediaMetadata()
|
||||||
// and put them in the mediaItems list
|
|
||||||
// } else {
|
if (libraryItem.mediaType == "podcast") { // Podcasts are browseable
|
||||||
// examine the passed parentMediaId to see which submenu we're at,
|
flag = MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||||
// and put the children of that menu in the mediaItems list
|
}
|
||||||
// }
|
|
||||||
|
MediaBrowserCompat.MediaItem(libraryItemMediaMetadata.description, flag)
|
||||||
|
}
|
||||||
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
|
}
|
||||||
|
} else if (parentMediaId == "__DOWNLOADS__") { // Load downloads
|
||||||
|
|
||||||
|
val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
|
||||||
|
val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast")
|
||||||
|
val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = 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<MediaBrowserCompat.MediaItem>?)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSearch(query: String, extras: Bundle?, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
override fun onSearch(query: String, extras: Bundle?, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
result.detach()
|
result.detach()
|
||||||
mediaManager.loadAndroidAutoItems("main") { libraryCategories ->
|
mediaManager.loadAndroidAutoItems() { libraryCategories ->
|
||||||
browseTree = BrowseTree(this, libraryCategories)
|
browseTree = BrowseTree(this, libraryCategories, mediaManager.serverLibraries)
|
||||||
val children = browseTree[ALL_ROOT]?.map { item ->
|
val children = browseTree[ALL_ROOT]?.map { item ->
|
||||||
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
}
|
}
|
||||||
|
|
|
@ -277,7 +277,7 @@ class AbsDownloader : Plugin() {
|
||||||
finalDestinationFile.delete()
|
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)
|
downloadItem.downloadItemParts.add(downloadItemPart)
|
||||||
|
|
||||||
var dlRequest = downloadItemPart.getDownloadRequest()
|
var dlRequest = downloadItemPart.getDownloadRequest()
|
||||||
|
@ -294,7 +294,7 @@ class AbsDownloader : Plugin() {
|
||||||
if (finalDestinationFile.exists()) {
|
if (finalDestinationFile.exists()) {
|
||||||
Log.d(tag, "Podcast cover already exists - not downloading cover again")
|
Log.d(tag, "Podcast cover already exists - not downloading cover again")
|
||||||
} else {
|
} 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)
|
downloadItem.downloadItemParts.add(downloadItemPart)
|
||||||
|
|
||||||
dlRequest = downloadItemPart.getDownloadRequest()
|
dlRequest = downloadItemPart.getDownloadRequest()
|
||||||
|
|
|
@ -10,9 +10,6 @@
|
||||||
android:translateY="-1.5294118">
|
android:translateY="-1.5294118">
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
android:fillColor="@android:color/white"
|
||||||
android:pathData="M4,6H2v14c0,1.1 0.9,2 2,2h14v-2H4V6z"/>
|
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"/>
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,12l-2.5,-1.5L15,12L15,4h5v8z"/>
|
|
||||||
</group>
|
</group>
|
||||||
</vector>
|
</vector>
|
Before Width: | Height: | Size: 276 B |
BIN
android/app/src/main/res/drawable-hdpi/icon_library_folder.png
Normal file
After Width: | Height: | Size: 216 B |
Before Width: | Height: | Size: 199 B |
BIN
android/app/src/main/res/drawable-mdpi/icon_library_folder.png
Normal file
After Width: | Height: | Size: 164 B |
Before Width: | Height: | Size: 309 B |
BIN
android/app/src/main/res/drawable-xhdpi/icon_library_folder.png
Normal file
After Width: | Height: | Size: 254 B |
Before Width: | Height: | Size: 430 B |
BIN
android/app/src/main/res/drawable-xxhdpi/icon_library_folder.png
Normal file
After Width: | Height: | Size: 466 B |
|
@ -7,7 +7,7 @@
|
||||||
<a v-if="showBack" @click="back" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 mr-2 cursor-pointer">
|
<a v-if="showBack" @click="back" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 mr-2 cursor-pointer">
|
||||||
<span class="material-icons text-3xl text-white">arrow_back</span>
|
<span class="material-icons text-3xl text-white">arrow_back</span>
|
||||||
</a>
|
</a>
|
||||||
<div v-if="user">
|
<div v-if="user && currentLibrary">
|
||||||
<div class="pl-3 pr-4 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
|
<div class="pl-3 pr-4 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
|
||||||
<widgets-library-icon :icon="currentLibraryIcon" :size="4" />
|
<widgets-library-icon :icon="currentLibraryIcon" :size="4" />
|
||||||
<p class="text-base font-book leading-4 ml-2 mt-0.5">{{ currentLibraryName }}</p>
|
<p class="text-base font-book leading-4 ml-2 mt-0.5">{{ currentLibraryName }}</p>
|
||||||
|
@ -51,14 +51,11 @@ export default {
|
||||||
this.$store.commit('setCastAvailable', val)
|
this.$store.commit('setCastAvailable', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
socketConnected() {
|
|
||||||
return this.$store.state.socketConnected
|
|
||||||
},
|
|
||||||
currentLibrary() {
|
currentLibrary() {
|
||||||
return this.$store.getters['libraries/getCurrentLibrary']
|
return this.$store.getters['libraries/getCurrentLibrary']
|
||||||
},
|
},
|
||||||
currentLibraryName() {
|
currentLibraryName() {
|
||||||
return this.currentLibrary ? this.currentLibrary.name : 'Main'
|
return this.currentLibrary ? this.currentLibrary.name : ''
|
||||||
},
|
},
|
||||||
currentLibraryIcon() {
|
currentLibraryIcon() {
|
||||||
return this.currentLibrary ? this.currentLibrary.icon : 'database'
|
return this.currentLibrary ? this.currentLibrary.icon : 'database'
|
||||||
|
|
|
@ -9,12 +9,12 @@ install! 'cocoapods', :disable_input_output_paths => true
|
||||||
def capacitor_pods
|
def capacitor_pods
|
||||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
pod 'CapacitorApp', :path => '..\..\node_modules\@capacitor\app'
|
||||||
pod 'CapacitorDialog', :path => '../../node_modules/@capacitor/dialog'
|
pod 'CapacitorDialog', :path => '..\..\node_modules\@capacitor\dialog'
|
||||||
pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics'
|
pod 'CapacitorHaptics', :path => '..\..\node_modules\@capacitor\haptics'
|
||||||
pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
|
pod 'CapacitorNetwork', :path => '..\..\node_modules\@capacitor\network'
|
||||||
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
pod 'CapacitorStatusBar', :path => '..\..\node_modules\@capacitor\status-bar'
|
||||||
pod 'CapacitorStorage', :path => '../../node_modules/@capacitor/storage'
|
pod 'CapacitorStorage', :path => '..\..\node_modules\@capacitor\storage'
|
||||||
end
|
end
|
||||||
|
|
||||||
target 'App' do
|
target 'App' do
|
||||||
|
|