Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
92f069d1e6 | ||
|
|
62c5042ecc | ||
|
|
604b086c0b | ||
|
|
0e0b356f6b | ||
|
|
573768e2b2 | ||
|
|
236fd09c94 | ||
|
|
cb6cb5f637 | ||
|
|
eb916a3ab0 | ||
|
|
61b8ca1510 | ||
|
|
5d2da97dc5 |
@@ -29,8 +29,8 @@ android {
|
||||
applicationId "com.audiobookshelf.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 74
|
||||
versionName "0.9.45-beta"
|
||||
versionCode 75
|
||||
versionName "0.9.46-beta"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -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<String>,
|
||||
var episodes:MutableList<PodcastEpisode>?,
|
||||
var autoDownloadEpisodes:Boolean
|
||||
var autoDownloadEpisodes:Boolean,
|
||||
var numEpisodes:Int?
|
||||
) : MediaType(metadata, coverPath) {
|
||||
@JsonIgnore
|
||||
override fun getAudioTracks():List<AudioTrack> {
|
||||
@@ -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<BookChapter>?,
|
||||
var tracks:MutableList<AudioTrack>?,
|
||||
var size:Long?,
|
||||
var duration:Double?
|
||||
var duration:Double?,
|
||||
var numTracks:Int?
|
||||
) : MediaType(metadata, coverPath) {
|
||||
@JsonIgnore
|
||||
override fun getAudioTracks():List<AudioTrack> {
|
||||
@@ -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<Folder>,
|
||||
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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ data class LocalFile(
|
||||
) {
|
||||
@JsonIgnore
|
||||
fun isAudioFile():Boolean {
|
||||
if (mimeType == "application/octet-stream") return true
|
||||
if (mimeType == "video/mp4") return true
|
||||
return mimeType?.startsWith("audio") == true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -248,9 +248,10 @@ class FolderScanner(var ctx: Context) {
|
||||
|
||||
val localLibraryItemId = getLocalLibraryItemId(itemFolderId)
|
||||
Log.d(tag, "scanDownloadItem starting for ${downloadItem.itemFolderPath} | ${df.uri} | Item Folder Id:$itemFolderId | LLI Id:$localLibraryItemId")
|
||||
|
||||
|
||||
// Search for files in media item folder
|
||||
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4"))
|
||||
// m4b files showing as mimeType application/octet-stream on Android 10 and earlier see #154
|
||||
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream"))
|
||||
Log.d(tag, "scanDownloadItem ${filesFound.size} files found in ${downloadItem.itemFolderPath}")
|
||||
|
||||
var localEpisodeId:String? = null
|
||||
@@ -288,7 +289,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 +297,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 +367,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 +378,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 +389,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 +408,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 +421,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
|
||||
|
||||
@@ -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<LibraryItem>()
|
||||
var selectedLibraryId = ""
|
||||
|
||||
var selectedLibraryItemWrapper:LibraryItemWrapper? = null
|
||||
var selectedPodcast:Podcast? = null
|
||||
var selectedLibraryItemId:String? = null
|
||||
var serverPodcastEpisodes = listOf<PodcastEpisode>()
|
||||
var serverLibraryCategories = listOf<LibraryCategory>()
|
||||
var serverLibraries = listOf<Library>()
|
||||
|
||||
@@ -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<LibraryCategory>) -> Unit) {
|
||||
if (serverLibraryCategories.isNotEmpty()) {
|
||||
cb(serverLibraryCategories)
|
||||
@@ -33,17 +45,75 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun loadLibraryItems(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
|
||||
if (serverLibraryItems.isNotEmpty()) {
|
||||
fun loadLibraryItemsWithAudio(libraryId:String, cb: (List<LibraryItem>) -> 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<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) {
|
||||
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<LibraryCategory>) -> Unit) {
|
||||
Log.d(tag, "Load android auto items for library id $libraryId")
|
||||
fun loadAndroidAutoItems(cb: (List<LibraryCategory>) -> Unit) {
|
||||
Log.d(tag, "Load android auto items")
|
||||
val cats = mutableListOf<LibraryCategory>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<LibraryCategory>
|
||||
libraryCategories: List<LibraryCategory>,
|
||||
libraries: List<Library>
|
||||
) {
|
||||
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())
|
||||
}.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__"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -15,7 +15,7 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
||||
private var onSeekBack: Boolean = false
|
||||
|
||||
override fun onPlayerError(error: PlaybackException) {
|
||||
var errorMessage = error.message ?: "Unknown Error"
|
||||
val errorMessage = error.message ?: "Unknown Error"
|
||||
Log.e(tag, "onPlayerError $errorMessage")
|
||||
playerNotificationService.handlePlayerPlaybackError(errorMessage) // If was direct playing session, fallback to transcode
|
||||
}
|
||||
@@ -90,6 +90,7 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
||||
// Start/stop progress sync interval
|
||||
Log.d(tag, "Playing ${playerNotificationService.getCurrentBookTitle()}")
|
||||
if (player.isPlaying) {
|
||||
player.volume = 1F // Volume on sleep timer might have decreased this
|
||||
playerNotificationService.mediaProgressSyncer.start()
|
||||
} else {
|
||||
playerNotificationService.mediaProgressSyncer.stop()
|
||||
|
||||
@@ -356,7 +356,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
Log.d(tag, "Prepare complete for session ${currentPlaybackSession?.displayTitle} | ${currentPlayer.mediaItemCount}")
|
||||
currentPlayer.playWhenReady = playWhenReady
|
||||
currentPlayer.setPlaybackSpeed(playbackRateToUse)
|
||||
|
||||
currentPlayer.prepare()
|
||||
|
||||
} else if (castPlayer != null) {
|
||||
val currentTrackIndex = playbackSession.getCurrentTrackIndex()
|
||||
val currentTrackTime = playbackSession.getCurrentTrackTimeMs()
|
||||
@@ -520,12 +522,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
|
||||
fun seekForward(amount: Long) {
|
||||
seekPlayer(getCurrentTime() + amount)
|
||||
// currentPlayer.seekTo(currentPlayer.currentPosition + amount)
|
||||
}
|
||||
|
||||
fun seekBackward(amount: Long) {
|
||||
seekPlayer(getCurrentTime() - amount)
|
||||
// currentPlayer.seekTo(currentPlayer.currentPosition - amount)
|
||||
}
|
||||
|
||||
fun setPlaybackSpeed(speed: Float) {
|
||||
@@ -565,6 +565,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 +611,66 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
override fun onLoadChildren(parentMediaId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||
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<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:
|
||||
// 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<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>>) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -10,9 +10,6 @@
|
||||
android:translateY="-1.5294118">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M4,6H2v14c0,1.1 0.9,2 2,2h14v-2H4V6z"/>
|
||||
<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"/>
|
||||
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"/>
|
||||
</group>
|
||||
</vector>
|
||||
|
Before Width: | Height: | Size: 276 B |
|
After Width: | Height: | Size: 216 B |
|
Before Width: | Height: | Size: 199 B |
|
After Width: | Height: | Size: 164 B |
|
Before Width: | Height: | Size: 309 B |
|
After Width: | Height: | Size: 254 B |
|
Before Width: | Height: | Size: 430 B |
|
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">
|
||||
<span class="material-icons text-3xl text-white">arrow_back</span>
|
||||
</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">
|
||||
<widgets-library-icon :icon="currentLibraryIcon" :size="4" />
|
||||
<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)
|
||||
}
|
||||
},
|
||||
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'
|
||||
|
||||
@@ -74,9 +74,6 @@ export default {
|
||||
username() {
|
||||
return this.user ? this.user.username : ''
|
||||
},
|
||||
socketConnected() {
|
||||
return this.$store.state.socketConnected
|
||||
},
|
||||
navItems() {
|
||||
var items = [
|
||||
{
|
||||
|
||||
@@ -25,6 +25,7 @@ export default {
|
||||
mixins: [bookshelfCardsHelpers],
|
||||
data() {
|
||||
return {
|
||||
routeFullPath: null,
|
||||
entitiesPerShelf: 2,
|
||||
bookshelfHeight: 0,
|
||||
bookshelfWidth: 0,
|
||||
@@ -72,6 +73,7 @@ export default {
|
||||
return this.page
|
||||
},
|
||||
hasFilter() {
|
||||
if (this.page === 'series' || this.page === 'collections') return false
|
||||
return this.filterBy !== 'all'
|
||||
},
|
||||
orderBy() {
|
||||
@@ -316,6 +318,15 @@ export default {
|
||||
await this.loadPage(0)
|
||||
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
||||
this.mountEntites(0, lastBookIndex)
|
||||
|
||||
// Set last scroll position for this bookshelf page
|
||||
if (this.$store.state.lastBookshelfScrollData[this.page] && window['bookshelf-wrapper']) {
|
||||
const { path, scrollTop } = this.$store.state.lastBookshelfScrollData[this.page]
|
||||
if (path === this.routeFullPath) {
|
||||
// Exact path match with query so use scroll position
|
||||
window['bookshelf-wrapper'].scrollTop = scrollTop
|
||||
}
|
||||
}
|
||||
},
|
||||
scroll(e) {
|
||||
if (!e || !e.target) return
|
||||
@@ -358,6 +369,8 @@ export default {
|
||||
if (newSearchParams !== this.currentSFQueryString || newSearchParams !== currentQueryString) {
|
||||
let newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + '?' + newSearchParams
|
||||
window.history.replaceState({ path: newurl }, '', newurl)
|
||||
|
||||
this.routeFullPath = window.location.pathname + (window.location.search || '') // Update for saving scroll position
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -452,12 +465,22 @@ export default {
|
||||
this.$socket.$off('items_added', this.libraryItemsAdded)
|
||||
}
|
||||
},
|
||||
updated() {
|
||||
this.routeFullPath = window.location.pathname + (window.location.search || '')
|
||||
},
|
||||
mounted() {
|
||||
this.routeFullPath = window.location.pathname + (window.location.search || '')
|
||||
|
||||
this.init()
|
||||
this.initListeners()
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.removeListeners()
|
||||
|
||||
// Set bookshelf scroll position for specific bookshelf page and query
|
||||
if (window['bookshelf-wrapper']) {
|
||||
this.$store.commit('setLastBookshelfScrollData', { scrollTop: window['bookshelf-wrapper'].scrollTop || 0, path: this.routeFullPath, name: this.page })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="absolute cover-bg" ref="coverBg" />
|
||||
</div>
|
||||
|
||||
<img v-if="fullCoverUrl" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? 1 : 0 }" :class="showCoverBg && !hasCover ? 'object-contain' : 'object-fill'" />
|
||||
<img v-if="fullCoverUrl" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? 1 : 0 }" :class="showCoverBg && hasCover ? 'object-contain' : 'object-fill'" />
|
||||
|
||||
<div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
||||
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
||||
@@ -121,7 +121,7 @@ export default {
|
||||
return this.media.coverPath || this.placeholderUrl
|
||||
},
|
||||
hasCover() {
|
||||
return !!this.media.coverPath || this.localCover || this.downloadCover
|
||||
return (!!this.media.coverPath && !this.isLocal) || this.localCover || this.downloadCover
|
||||
},
|
||||
sizeMultiplier() {
|
||||
var baseSize = this.squareAspectRatio ? 128 : 96
|
||||
|
||||
@@ -47,6 +47,10 @@ export default {
|
||||
text: 'Size',
|
||||
value: 'size'
|
||||
},
|
||||
{
|
||||
text: 'Duration',
|
||||
value: 'media.duration'
|
||||
},
|
||||
{
|
||||
text: 'File Birthtime',
|
||||
value: 'birthtimeMs'
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
|
||||
|
||||
<div v-if="!isIos">
|
||||
<div v-if="!isIos && userCanDownload">
|
||||
<span v-if="isLocal" class="material-icons-outlined px-2 text-success text-lg">audio_file</span>
|
||||
<span v-else-if="!localEpisode" class="material-icons mx-1 mt-2" :class="downloadItem ? 'animate-bounce text-warning text-opacity-75 text-xl' : 'text-gray-300 text-xl'" @click="downloadClick">{{ downloadItem ? 'downloading' : 'download' }}</span>
|
||||
<span v-else class="material-icons px-2 text-success text-xl">download_done</span>
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
<script>
|
||||
import { Dialog } from '@capacitor/dialog'
|
||||
import { AbsDownloader } from '@/plugins/capacitor'
|
||||
import { AbsFileSystem, AbsDownloader } from '@/plugins/capacitor'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -69,6 +69,9 @@ export default {
|
||||
mediaType() {
|
||||
return 'podcast'
|
||||
},
|
||||
userCanDownload() {
|
||||
return this.$store.getters['user/getUserCanDownload']
|
||||
},
|
||||
audioFile() {
|
||||
return this.episode.audioFile
|
||||
},
|
||||
@@ -132,8 +135,12 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
selectFolder() {
|
||||
this.$toast.error('Folder selector not implemented for podcasts yet')
|
||||
async selectFolder() {
|
||||
var folderObj = await AbsFileSystem.selectFolder({ mediaType: this.mediaType })
|
||||
if (folderObj.error) {
|
||||
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
|
||||
}
|
||||
return folderObj
|
||||
},
|
||||
downloadClick() {
|
||||
if (this.downloadItem) return
|
||||
|
||||
@@ -475,12 +475,12 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 7;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 0.9.44;
|
||||
MARKETING_VERSION = 0.9.46;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -499,12 +499,12 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 7;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 0.9.44;
|
||||
MARKETING_VERSION = 0.9.46;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-app",
|
||||
"version": "0.9.45-beta",
|
||||
"version": "0.9.46-beta",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-app",
|
||||
"version": "0.9.45-beta",
|
||||
"version": "0.9.46-beta",
|
||||
"author": "advplyr",
|
||||
"scripts": {
|
||||
"dev": "nuxt --hostname 0.0.0.0 --port 1337",
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
<span class="material-icons">auto_stories</span>
|
||||
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
|
||||
</ui-btn>
|
||||
<ui-btn v-if="user && showPlay && !hasLocal" :color="downloadItem ? 'warning' : 'primary'" class="flex items-center justify-center mr-2" :padding-x="2" @click="downloadClick">
|
||||
<ui-btn v-if="showDownload" :color="downloadItem ? 'warning' : 'primary'" class="flex items-center justify-center mr-2" :padding-x="2" @click="downloadClick">
|
||||
<span class="material-icons" :class="downloadItem ? 'animate-pulse' : ''">{{ downloadItem ? 'downloading' : 'download' }}</span>
|
||||
</ui-btn>
|
||||
<ui-read-icon-btn v-if="!isPodcast" :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="flex items-center justify-center" @click="toggleFinished" />
|
||||
@@ -117,6 +117,9 @@ export default {
|
||||
isIos() {
|
||||
return this.$platform === 'ios'
|
||||
},
|
||||
userCanDownload() {
|
||||
return this.$store.getters['user/getUserCanDownload']
|
||||
},
|
||||
isLocal() {
|
||||
return this.libraryItem.isLocal
|
||||
},
|
||||
@@ -235,6 +238,10 @@ export default {
|
||||
showRead() {
|
||||
return this.ebookFile && this.ebookFormat !== 'pdf'
|
||||
},
|
||||
showDownload() {
|
||||
if (this.isIos) return false
|
||||
return this.user && this.userCanDownload && this.showPlay && !this.hasLocal
|
||||
},
|
||||
ebookFile() {
|
||||
return this.media.ebookFile
|
||||
},
|
||||
@@ -242,9 +249,6 @@ export default {
|
||||
if (!this.ebookFile) return null
|
||||
return this.ebookFile.ebookFormat
|
||||
},
|
||||
hasStoragePermission() {
|
||||
return this.$store.state.hasStoragePermission
|
||||
},
|
||||
downloadItem() {
|
||||
return this.$store.getters['globals/getDownloadItem'](this.libraryItemId)
|
||||
},
|
||||
|
||||
@@ -16,7 +16,8 @@ export const state = () => ({
|
||||
showReader: false,
|
||||
showSideDrawer: false,
|
||||
isNetworkListenerInit: false,
|
||||
serverSettings: null
|
||||
serverSettings: null,
|
||||
lastBookshelfScrollData: {}
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
@@ -54,6 +55,9 @@ export const actions = {
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
setLastBookshelfScrollData(state, { scrollTop, path, name }) {
|
||||
state.lastBookshelfScrollData[name] = { scrollTop, path }
|
||||
},
|
||||
setPlayerItem(state, playbackSession) {
|
||||
state.playerIsLocal = playbackSession ? playbackSession.playMethod == this.$constants.PlayMethod.LOCAL : false
|
||||
|
||||
|
||||
@@ -35,6 +35,9 @@ export const getters = {
|
||||
},
|
||||
getUserSetting: (state) => (key) => {
|
||||
return state.settings ? state.settings[key] || null : null
|
||||
},
|
||||
getUserCanDownload: (state) => {
|
||||
return state.user && state.user.permissions ? !!state.user.permissions.download : false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||