Merge pull request #1322 from ISO-B/feat_android_auto_browse
Added better library browsing for Android Auto
|
|
@ -0,0 +1,36 @@
|
|||
package com.audiobookshelf.app.data
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.v4.media.MediaDescriptionCompat
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
class CollapsedSeries(
|
||||
id:String,
|
||||
var libraryId:String?,
|
||||
var name:String,
|
||||
//var nameIgnorePrefix:String,
|
||||
var sequence:String?,
|
||||
var libraryItemIds:MutableList<String>
|
||||
) : LibraryItemWrapper(id) {
|
||||
@get:JsonIgnore
|
||||
val title get() = name
|
||||
@get:JsonIgnore
|
||||
val numBooks get() = libraryItemIds.size
|
||||
|
||||
@JsonIgnore
|
||||
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
|
||||
val extras = Bundle()
|
||||
|
||||
val mediaId = "__LIBRARY__${libraryId}__SERIE__${id}"
|
||||
return MediaDescriptionCompat.Builder()
|
||||
.setMediaId(mediaId)
|
||||
.setTitle(title)
|
||||
//.setIconUri(getCoverUri())
|
||||
.setSubtitle("${numBooks} books")
|
||||
.setExtras(extras)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,15 @@
|
|||
package com.audiobookshelf.app.data
|
||||
|
||||
import android.content.Context
|
||||
import android.icu.text.DateFormat
|
||||
import android.os.Bundle
|
||||
import android.support.v4.media.MediaDescriptionCompat
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
import androidx.media.utils.MediaConstants
|
||||
import com.audiobookshelf.app.media.MediaManager
|
||||
import com.fasterxml.jackson.annotation.*
|
||||
import com.audiobookshelf.app.media.getUriToAbsIconDrawable
|
||||
import java.util.Date
|
||||
|
||||
// This auto-detects whether it is a Book or Podcast
|
||||
@JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION)
|
||||
|
|
@ -25,7 +28,8 @@ open class MediaType(var metadata:MediaTypeMetadata, var coverPath:String?) {
|
|||
open fun removeAudioTrack(localFileId:String) { }
|
||||
@JsonIgnore
|
||||
open fun getLocalCopy():MediaType { return MediaType(MediaTypeMetadata("", false),null) }
|
||||
|
||||
@JsonIgnore
|
||||
open fun checkHasTracks():Boolean { return false }
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
|
|
@ -93,6 +97,11 @@ class Podcast(
|
|||
return Podcast(metadata as PodcastMetadata,coverPath,tags, mutableListOf(),autoDownloadEpisodes, 0)
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
override fun checkHasTracks():Boolean {
|
||||
return (episodes?.size ?: numEpisodes ?: 0) > 0
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode):PodcastEpisode {
|
||||
val localEpisodeId = "local_ep_" + episode.id
|
||||
|
|
@ -182,6 +191,11 @@ class Book(
|
|||
override fun getLocalCopy(): Book {
|
||||
return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(), ebookFile, null,null, 0)
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
override fun checkHasTracks():Boolean {
|
||||
return (tracks?.size ?: numTracks ?: 0) > 0
|
||||
}
|
||||
}
|
||||
|
||||
// This auto-detects whether it is a BookMetadata or PodcastMetadata
|
||||
|
|
@ -214,7 +228,9 @@ class BookMetadata(
|
|||
var authorName:String?,
|
||||
var authorNameLF:String?,
|
||||
var narratorName:String?,
|
||||
var seriesName:String?
|
||||
var seriesName:String?,
|
||||
@JsonFormat(with=[JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY])
|
||||
var series:List<SeriesType>?
|
||||
) : MediaTypeMetadata(title, explicit) {
|
||||
@JsonIgnore
|
||||
override fun getAuthorDisplayName():String { return authorName ?: "Unknown" }
|
||||
|
|
@ -299,11 +315,18 @@ data class PodcastEpisode(
|
|||
|
||||
val libraryItemDescription = libraryItem.getMediaDescription(null, ctx)
|
||||
val mediaId = localEpisodeId ?: id
|
||||
var subtitle = libraryItemDescription.title
|
||||
if (publishedAt !== null) {
|
||||
val sdf = DateFormat.getDateInstance()
|
||||
val publishedAtDT = Date(publishedAt!!)
|
||||
subtitle = "${sdf.format(publishedAtDT)} / $subtitle"
|
||||
}
|
||||
|
||||
val mediaDescriptionBuilder = MediaDescriptionCompat.Builder()
|
||||
.setMediaId(mediaId)
|
||||
.setTitle(title)
|
||||
.setIconUri(coverUri)
|
||||
.setSubtitle(libraryItemDescription.title)
|
||||
.setSubtitle(subtitle)
|
||||
.setExtras(extras)
|
||||
|
||||
libraryItemDescription.iconBitmap?.let {
|
||||
|
|
@ -342,18 +365,39 @@ data class Library(
|
|||
var name:String,
|
||||
var folders:MutableList<Folder>,
|
||||
var icon:String,
|
||||
var mediaType:String
|
||||
var mediaType:String,
|
||||
var stats: LibraryStats?
|
||||
) {
|
||||
@JsonIgnore
|
||||
fun getMediaMetadata(): MediaMetadataCompat {
|
||||
fun getMediaMetadata(context: Context, targetType: String? = null): MediaMetadataCompat {
|
||||
var mediaId = id
|
||||
if (targetType !== null) {
|
||||
mediaId = "__RECENTLY__$id"
|
||||
}
|
||||
return MediaMetadataCompat.Builder().apply {
|
||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId)
|
||||
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, name)
|
||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, name)
|
||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToAbsIconDrawable(context, icon).toString())
|
||||
}.build()
|
||||
}
|
||||
}
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class LibraryStats(
|
||||
var totalItems: Int,
|
||||
var totalSize: Long,
|
||||
var totalDuration: Double,
|
||||
var numAudioFiles: Int
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class SeriesType(
|
||||
var id: String,
|
||||
var name: String,
|
||||
var sequence: String?
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class Folder(
|
||||
var id:String,
|
||||
|
|
@ -387,3 +431,84 @@ data class LibraryItemWithEpisode(
|
|||
var libraryItemWrapper:LibraryItemWrapper,
|
||||
var episode:PodcastEpisode
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class LibraryItemSearchResultSeriesItemType(
|
||||
var series: LibrarySeriesItem,
|
||||
var books: List<LibraryItem>?
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class LibraryItemSearchResultLibraryItemType(
|
||||
val libraryItem: LibraryItem
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class LibraryItemSearchResultType(
|
||||
var book:List<LibraryItemSearchResultLibraryItemType>?,
|
||||
var podcast:List<LibraryItemSearchResultLibraryItemType>?,
|
||||
var series:List<LibraryItemSearchResultSeriesItemType>?,
|
||||
var authors:List<LibraryAuthorItem>?
|
||||
)
|
||||
|
||||
// For personalized shelves
|
||||
@JsonTypeInfo(
|
||||
use=JsonTypeInfo.Id.NAME,
|
||||
property = "type",
|
||||
include = JsonTypeInfo.As.PROPERTY,
|
||||
visible = true
|
||||
)
|
||||
@JsonSubTypes(
|
||||
JsonSubTypes.Type(LibraryShelfBookEntity::class, name = "book"),
|
||||
JsonSubTypes.Type(LibraryShelfSeriesEntity::class, name = "series"),
|
||||
JsonSubTypes.Type(LibraryShelfAuthorEntity::class, name = "authors"),
|
||||
JsonSubTypes.Type(LibraryShelfEpisodeEntity::class, name = "episode"),
|
||||
JsonSubTypes.Type(LibraryShelfPodcastEntity::class, name = "podcast")
|
||||
)
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
sealed class LibraryShelfType(
|
||||
open val id: String,
|
||||
open val label: String,
|
||||
open val total: Int,
|
||||
open val type: String,
|
||||
)
|
||||
|
||||
data class LibraryShelfBookEntity(
|
||||
override val id: String,
|
||||
override val label: String,
|
||||
override val total: Int,
|
||||
override val type: String,
|
||||
val entities: List<LibraryItem>?
|
||||
) : LibraryShelfType(id, label, total, type)
|
||||
|
||||
data class LibraryShelfSeriesEntity(
|
||||
override val id: String,
|
||||
override val label: String,
|
||||
override val total: Int,
|
||||
override val type: String,
|
||||
val entities: List<LibrarySeriesItem>?
|
||||
) : LibraryShelfType(id, label, total, type)
|
||||
|
||||
data class LibraryShelfAuthorEntity(
|
||||
override val id: String,
|
||||
override val label: String,
|
||||
override val total: Int,
|
||||
override val type: String,
|
||||
val entities: List<LibraryAuthorItem>?
|
||||
) : LibraryShelfType(id, label, total, type)
|
||||
|
||||
data class LibraryShelfEpisodeEntity(
|
||||
override val id: String,
|
||||
override val label: String,
|
||||
override val total: Int,
|
||||
override val type: String,
|
||||
val entities: List<LibraryItem>?
|
||||
) : LibraryShelfType(id, label, total, type)
|
||||
|
||||
data class LibraryShelfPodcastEntity(
|
||||
override val id: String,
|
||||
override val label: String,
|
||||
override val total: Int,
|
||||
override val type: String,
|
||||
val entities: List<LibraryItem>?
|
||||
) : LibraryShelfType(id, label, total, type)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ enum class StreamingUsingCellularSetting {
|
|||
ASK, ALWAYS, NEVER
|
||||
}
|
||||
|
||||
enum class AndroidAutoBrowseSeriesSequenceOrderSetting {
|
||||
ASC, DESC
|
||||
}
|
||||
|
||||
data class ServerConnectionConfig(
|
||||
var id:String,
|
||||
var index:Int,
|
||||
|
|
@ -133,7 +137,9 @@ data class DeviceSettings(
|
|||
var disableSleepTimerResetFeedback: Boolean,
|
||||
var languageCode: String,
|
||||
var downloadUsingCellular: DownloadUsingCellularSetting,
|
||||
var streamingUsingCellular: StreamingUsingCellularSetting
|
||||
var streamingUsingCellular: StreamingUsingCellularSetting,
|
||||
var androidAutoBrowseLimitForGrouping: Int,
|
||||
var androidAutoBrowseSeriesSequenceOrder: AndroidAutoBrowseSeriesSequenceOrderSetting
|
||||
) {
|
||||
companion object {
|
||||
// Static method to get default device settings
|
||||
|
|
@ -159,7 +165,9 @@ data class DeviceSettings(
|
|||
disableSleepTimerResetFeedback = false,
|
||||
languageCode = "en-us",
|
||||
downloadUsingCellular = DownloadUsingCellularSetting.ALWAYS,
|
||||
streamingUsingCellular = StreamingUsingCellularSetting.ALWAYS
|
||||
streamingUsingCellular = StreamingUsingCellularSetting.ALWAYS,
|
||||
androidAutoBrowseLimitForGrouping = 100,
|
||||
androidAutoBrowseSeriesSequenceOrder = AndroidAutoBrowseSeriesSequenceOrderSetting.ASC
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
package com.audiobookshelf.app.data
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.support.v4.media.MediaDescriptionCompat
|
||||
import androidx.media.utils.MediaConstants
|
||||
import com.audiobookshelf.app.BuildConfig
|
||||
import com.audiobookshelf.app.R
|
||||
import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
class LibraryAuthorItem(
|
||||
id:String,
|
||||
var libraryId:String,
|
||||
var name:String,
|
||||
var description:String?,
|
||||
var imagePath:String?,
|
||||
var addedAt:Long,
|
||||
var updatedAt:Long,
|
||||
var numBooks:Int?,
|
||||
var libraryItems:MutableList<LibraryItem>?,
|
||||
var series:MutableList<LibrarySeriesItem>?
|
||||
) : LibraryItemWrapper(id) {
|
||||
@get:JsonIgnore
|
||||
val title get() = name
|
||||
|
||||
@get:JsonIgnore
|
||||
val bookCount get() = if (numBooks != null) numBooks else libraryItems!!.size
|
||||
|
||||
@JsonIgnore
|
||||
fun getPortraitUri(): Uri {
|
||||
if (imagePath == null) {
|
||||
return Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.md_account_outline)
|
||||
}
|
||||
|
||||
return Uri.parse("${DeviceManager.serverAddress}/api/authors/$id/image?token=${DeviceManager.token}")
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, groupTitle: String?): MediaDescriptionCompat {
|
||||
val extras = Bundle()
|
||||
if (groupTitle !== null) {
|
||||
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, groupTitle)
|
||||
}
|
||||
|
||||
val mediaId = "__LIBRARY__${libraryId}__AUTHOR__${id}"
|
||||
return MediaDescriptionCompat.Builder()
|
||||
.setMediaId(mediaId)
|
||||
.setTitle(title)
|
||||
.setIconUri(getPortraitUri())
|
||||
.setSubtitle("${bookCount} books")
|
||||
.setExtras(extras)
|
||||
.build()
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
|
||||
return getMediaDescription(progress, ctx, null)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package com.audiobookshelf.app.data
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.v4.media.MediaDescriptionCompat
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
class LibraryCollection(
|
||||
id:String,
|
||||
var libraryId:String,
|
||||
var name:String,
|
||||
//var userId:String?,
|
||||
var description:String?,
|
||||
var books:MutableList<LibraryItem>?,
|
||||
) : LibraryItemWrapper(id) {
|
||||
@get:JsonIgnore
|
||||
val title get() = name
|
||||
|
||||
@get:JsonIgnore
|
||||
val bookCount get() = if (books != null) books!!.size else 0
|
||||
|
||||
@get:JsonIgnore
|
||||
val audiobookCount get() = books?.filter { book -> (book.media as Book).getAudioTracks().isNotEmpty() }?.size ?: 0
|
||||
|
||||
@JsonIgnore
|
||||
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
|
||||
val extras = Bundle()
|
||||
|
||||
val mediaId = "__LIBRARY__${libraryId}__COLLECTION__${id}"
|
||||
return MediaDescriptionCompat.Builder()
|
||||
.setMediaId(mediaId)
|
||||
.setTitle(title)
|
||||
//.setIconUri(getCoverUri())
|
||||
.setSubtitle("${bookCount} books")
|
||||
.setExtras(extras)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
|
@ -32,10 +32,18 @@ class LibraryItem(
|
|||
var media:MediaType,
|
||||
var libraryFiles:MutableList<LibraryFile>?,
|
||||
var userMediaProgress:MediaProgress?, // Only included when requesting library item with progress (for downloads)
|
||||
var localLibraryItemId:String? // For Android Auto
|
||||
var collapsedSeries: CollapsedSeries?,
|
||||
var localLibraryItemId:String?, // For Android Auto
|
||||
val recentEpisode: PodcastEpisode? // Podcast episode shelf uses this
|
||||
) : LibraryItemWrapper(id) {
|
||||
@get:JsonIgnore
|
||||
val title get() = media.metadata.title
|
||||
val title: String
|
||||
get() {
|
||||
if (collapsedSeries != null) {
|
||||
return collapsedSeries!!.title
|
||||
}
|
||||
return media.metadata.title
|
||||
}
|
||||
@get:JsonIgnore
|
||||
val authorName get() = media.metadata.getAuthorDisplayName()
|
||||
|
||||
|
|
@ -50,57 +58,116 @@ class LibraryItem(
|
|||
|
||||
@JsonIgnore
|
||||
fun checkHasTracks():Boolean {
|
||||
return if (mediaType == "podcast") {
|
||||
((media as Podcast).numEpisodes ?: 0) > 0
|
||||
} else {
|
||||
((media as Book).numTracks ?: 0) > 0
|
||||
return media.checkHasTracks()
|
||||
}
|
||||
|
||||
@get:JsonIgnore
|
||||
val seriesSequence: String
|
||||
get() {
|
||||
if (mediaType != "podcast") {
|
||||
return ((media as Book).metadata as BookMetadata).series?.get(0)?.sequence.orEmpty()
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
@get:JsonIgnore
|
||||
val seriesSequenceParts: List<String>
|
||||
get() {
|
||||
if (seriesSequence.isEmpty()) {
|
||||
return listOf("")
|
||||
}
|
||||
return seriesSequence.split(".", limit = 2)
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, authorId: String?, showSeriesNumber: Boolean?, groupTitle: String?): MediaDescriptionCompat {
|
||||
val extras = Bundle()
|
||||
|
||||
if (collapsedSeries == null) {
|
||||
if (localLibraryItemId != null) {
|
||||
extras.putLong(
|
||||
MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
|
||||
MediaDescriptionCompat.STATUS_DOWNLOADED
|
||||
)
|
||||
}
|
||||
|
||||
if (progress != null) {
|
||||
if (progress.isFinished) {
|
||||
extras.putInt(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
|
||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
|
||||
)
|
||||
} else {
|
||||
extras.putInt(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
|
||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
|
||||
)
|
||||
extras.putDouble(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress.progress
|
||||
)
|
||||
}
|
||||
} else if (mediaType != "podcast") {
|
||||
extras.putInt(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
|
||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
|
||||
)
|
||||
}
|
||||
|
||||
if (media.metadata.explicit) {
|
||||
extras.putLong(
|
||||
MediaConstants.METADATA_KEY_IS_EXPLICIT,
|
||||
MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
|
||||
)
|
||||
}
|
||||
}
|
||||
if (groupTitle !== null) {
|
||||
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, groupTitle)
|
||||
}
|
||||
|
||||
val mediaId = if (localLibraryItemId != null) {
|
||||
localLibraryItemId
|
||||
} else if (collapsedSeries != null) {
|
||||
if (authorId != null) {
|
||||
"__LIBRARY__${libraryId}__AUTHOR_SERIES__${authorId}__${collapsedSeries!!.id}"
|
||||
} else {
|
||||
"__LIBRARY__${libraryId}__SERIES__${collapsedSeries!!.id}"
|
||||
}
|
||||
} else {
|
||||
id
|
||||
}
|
||||
var subtitle = authorName
|
||||
if (collapsedSeries != null) {
|
||||
subtitle = "${collapsedSeries!!.numBooks} books"
|
||||
}
|
||||
var itemTitle = title
|
||||
if (showSeriesNumber == true && seriesSequence != "") {
|
||||
itemTitle = "$seriesSequence. $itemTitle"
|
||||
}
|
||||
return MediaDescriptionCompat.Builder()
|
||||
.setMediaId(mediaId)
|
||||
.setTitle(itemTitle)
|
||||
.setIconUri(getCoverUri())
|
||||
.setSubtitle(subtitle)
|
||||
.setExtras(extras)
|
||||
.build()
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, authorId: String?, showSeriesNumber: Boolean?): MediaDescriptionCompat {
|
||||
return getMediaDescription(progress, ctx, authorId, showSeriesNumber, null)
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, authorId: String?): MediaDescriptionCompat {
|
||||
return getMediaDescription(progress, ctx, authorId, null, null)
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
|
||||
val extras = Bundle()
|
||||
|
||||
if (localLibraryItemId != null) {
|
||||
extras.putLong(
|
||||
MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
|
||||
MediaDescriptionCompat.STATUS_DOWNLOADED
|
||||
)
|
||||
}
|
||||
|
||||
if (progress != null) {
|
||||
if (progress.isFinished) {
|
||||
extras.putInt(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
|
||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
|
||||
)
|
||||
} else {
|
||||
extras.putInt(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
|
||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
|
||||
)
|
||||
extras.putDouble(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress.progress
|
||||
)
|
||||
}
|
||||
} else if (mediaType != "podcast") {
|
||||
extras.putInt(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
|
||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
|
||||
)
|
||||
}
|
||||
|
||||
if (media.metadata.explicit) {
|
||||
extras.putLong(MediaConstants.METADATA_KEY_IS_EXPLICIT, MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT)
|
||||
}
|
||||
|
||||
val mediaId = localLibraryItemId ?: id
|
||||
return MediaDescriptionCompat.Builder()
|
||||
.setMediaId(mediaId)
|
||||
.setTitle(title)
|
||||
.setIconUri(getCoverUri())
|
||||
.setSubtitle(authorName)
|
||||
.setExtras(extras)
|
||||
.build()
|
||||
/*
|
||||
This is needed so Android auto library hierarchy for author series can be implemented
|
||||
*/
|
||||
return getMediaDescription(progress, ctx, null, null, null)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
package com.audiobookshelf.app.data
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.support.v4.media.MediaDescriptionCompat
|
||||
import androidx.media.utils.MediaConstants
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
class LibrarySeriesItem(
|
||||
id:String,
|
||||
var libraryId:String,
|
||||
var name:String,
|
||||
var description:String?,
|
||||
var addedAt:Long,
|
||||
var updatedAt:Long,
|
||||
var books:MutableList<LibraryItem>?,
|
||||
var localLibraryItemId:String? // For Android Auto
|
||||
) : LibraryItemWrapper(id) {
|
||||
@get:JsonIgnore
|
||||
val title get() = name
|
||||
|
||||
@get:JsonIgnore
|
||||
val audiobookCount: Int
|
||||
get() {
|
||||
if (books == null) return 0
|
||||
val booksWithAudio = books?.filter { b -> b.media.checkHasTracks() }
|
||||
return booksWithAudio?.size ?: 0
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, groupTitle: String?): MediaDescriptionCompat {
|
||||
val extras = Bundle()
|
||||
|
||||
if (localLibraryItemId != null) {
|
||||
extras.putLong(
|
||||
MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
|
||||
MediaDescriptionCompat.STATUS_DOWNLOADED
|
||||
)
|
||||
}
|
||||
if (groupTitle !== null) {
|
||||
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, groupTitle)
|
||||
}
|
||||
|
||||
val mediaId = "__LIBRARY__${libraryId}__SERIES__${id}"
|
||||
return MediaDescriptionCompat.Builder()
|
||||
.setMediaId(mediaId)
|
||||
.setTitle(title)
|
||||
//.setIconUri(getCoverUri())
|
||||
.setSubtitle("$audiobookCount books")
|
||||
.setExtras(extras)
|
||||
.build()
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
|
||||
return getMediaDescription(progress, ctx, null)
|
||||
}
|
||||
}
|
||||
|
|
@ -41,7 +41,7 @@ data class LocalMediaItem(
|
|||
@JsonIgnore
|
||||
fun getMediaMetadata():MediaTypeMetadata {
|
||||
return if (mediaType == "book") {
|
||||
BookMetadata(name,null, mutableListOf(), mutableListOf(), mutableListOf(),null,null,null,null,null,null,null,false,null,null,null,null)
|
||||
BookMetadata(name,null, mutableListOf(), mutableListOf(), mutableListOf(),null,null,null,null,null,null,null,false,null,null,null, null, null)
|
||||
} else {
|
||||
PodcastMetadata(name,null,null, mutableListOf(), false)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,6 +61,12 @@ object DeviceManager {
|
|||
if (deviceData.deviceSettings?.streamingUsingCellular == null) {
|
||||
deviceData.deviceSettings?.streamingUsingCellular = StreamingUsingCellularSetting.ALWAYS
|
||||
}
|
||||
if (deviceData.deviceSettings?.androidAutoBrowseLimitForGrouping == null) {
|
||||
deviceData.deviceSettings?.androidAutoBrowseLimitForGrouping = 100
|
||||
}
|
||||
if (deviceData.deviceSettings?.androidAutoBrowseSeriesSequenceOrder == null) {
|
||||
deviceData.deviceSettings?.androidAutoBrowseSeriesSequenceOrder = AndroidAutoBrowseSeriesSequenceOrderSetting.ASC
|
||||
}
|
||||
}
|
||||
|
||||
fun getBase64Id(id:String):String {
|
||||
|
|
|
|||
|
|
@ -20,13 +20,23 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||
val tag = "MediaManager"
|
||||
|
||||
private var serverLibraryItems = mutableListOf<LibraryItem>() // Store all items here
|
||||
private var selectedLibraryItems = mutableListOf<LibraryItem>()
|
||||
private var selectedLibraryId = ""
|
||||
|
||||
private var cachedLibraryAuthors : MutableMap<String, MutableMap<String, LibraryAuthorItem>> = hashMapOf()
|
||||
private var cachedLibraryAuthorItems : MutableMap<String, MutableMap<String, List<LibraryItem>>> = hashMapOf()
|
||||
private var cachedLibraryAuthorSeriesItems : MutableMap<String, MutableMap<String, List<LibraryItem>>> = hashMapOf()
|
||||
private var cachedLibrarySeries : MutableMap<String, List<LibrarySeriesItem>> = hashMapOf()
|
||||
private var cachedLibrarySeriesItem : MutableMap<String, MutableMap<String, List<LibraryItem>>> = hashMapOf()
|
||||
private var cachedLibraryCollections : MutableMap<String, MutableMap<String, LibraryCollection>> = hashMapOf()
|
||||
private var cachedLibraryRecentShelves : MutableMap<String, MutableList<LibraryShelfType>> = hashMapOf()
|
||||
private var cachedLibraryDiscovery : MutableMap<String, MutableList<LibraryItem>> = hashMapOf()
|
||||
private var cachedLibraryPodcasts : MutableMap<String, MutableMap<String, LibraryItem>> = hashMapOf()
|
||||
private var isLibraryPodcastsCached : MutableMap<String, Boolean> = hashMapOf()
|
||||
var allLibraryPersonalizationsDone : Boolean = false
|
||||
private var libraryPersonalizationsDone : Int = 0
|
||||
|
||||
private var selectedPodcast:Podcast? = null
|
||||
private var selectedLibraryItemId:String? = null
|
||||
private var podcastEpisodeLibraryItemMap = mutableMapOf<String, LibraryItemWithEpisode>()
|
||||
private var serverLibraryCategories = listOf<LibraryCategory>()
|
||||
private var serverConfigIdUsed:String? = null
|
||||
private var serverConfigLastPing:Long = 0L
|
||||
var serverUserMediaProgress:MutableList<MediaProgress> = mutableListOf()
|
||||
|
|
@ -39,6 +49,35 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||
return serverLibraries.find { it.id == id } != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is discovery shelf for [libraryId]
|
||||
* If personalized shelves are not yet populated for library then populate
|
||||
*
|
||||
*/
|
||||
fun getHasDiscovery(libraryId: String) : Boolean {
|
||||
if (cachedLibraryDiscovery.containsKey(libraryId)) {
|
||||
if (cachedLibraryDiscovery[libraryId]!!.isNotEmpty()) {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
populatePersonalizedDataForLibrary(libraryId){}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun getLibrary(id:String) : Library? {
|
||||
return serverLibraries.find { it.id == id }
|
||||
}
|
||||
|
||||
/**
|
||||
* Add [libraryItem] to [serverLibraryItems] if it is not already added
|
||||
*/
|
||||
private fun addServerLibrary(libraryItem: LibraryItem) {
|
||||
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||
serverLibraryItems.add(libraryItem)
|
||||
}
|
||||
}
|
||||
|
||||
fun getSavedPlaybackRate():Float {
|
||||
if (userSettingsPlaybackRate != null) {
|
||||
return userSettingsPlaybackRate ?: 1f
|
||||
|
|
@ -91,19 +130,32 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
fun checkResetServerItems() {
|
||||
fun checkResetServerItems():Boolean {
|
||||
// When opening android auto need to check if still connected to server
|
||||
// and reset any server data already set
|
||||
val serverConnConfig = if (DeviceManager.isConnectedToServer) DeviceManager.serverConnectionConfig else DeviceManager.deviceData.getLastServerConnectionConfig()
|
||||
|
||||
if (!DeviceManager.isConnectedToServer || !DeviceManager.checkConnectivity(ctx) || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) {
|
||||
Log.d(tag, "Reset caches")
|
||||
podcastEpisodeLibraryItemMap = mutableMapOf()
|
||||
serverLibraryCategories = listOf()
|
||||
serverLibraries = listOf()
|
||||
serverLibraryItems = mutableListOf()
|
||||
selectedLibraryItems = mutableListOf()
|
||||
selectedLibraryId = ""
|
||||
cachedLibraryAuthors = hashMapOf()
|
||||
cachedLibraryAuthorItems = hashMapOf()
|
||||
cachedLibraryAuthorSeriesItems = hashMapOf()
|
||||
cachedLibrarySeries = hashMapOf()
|
||||
cachedLibrarySeriesItem = hashMapOf()
|
||||
cachedLibraryCollections = hashMapOf()
|
||||
cachedLibraryRecentShelves = hashMapOf()
|
||||
cachedLibraryDiscovery = hashMapOf()
|
||||
cachedLibraryPodcasts = hashMapOf()
|
||||
isLibraryPodcastsCached = hashMapOf()
|
||||
serverItemsInProgress = listOf()
|
||||
allLibraryPersonalizationsDone = false
|
||||
libraryPersonalizationsDone = 0
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun loadItemsInProgressForAllLibraries(cb: (List<ItemInProgress>) -> Unit) {
|
||||
|
|
@ -120,28 +172,458 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
fun loadLibraryItemsWithAudio(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
|
||||
if (selectedLibraryItems.isNotEmpty() && selectedLibraryId == libraryId) {
|
||||
cb(selectedLibraryItems)
|
||||
/**
|
||||
* Load personalized shelves from server for all libraries.
|
||||
* [cb] resolves when all libraries are processed
|
||||
*/
|
||||
fun populatePersonalizedDataForAllLibraries(cb: () -> Unit ) {
|
||||
serverLibraries.forEach {
|
||||
libraryPersonalizationsDone++
|
||||
Log.d(tag, "Loading personalization for library ${it.name} - ${it.id} - ${it.mediaType}")
|
||||
populatePersonalizedDataForLibrary(it.id) {
|
||||
Log.d(tag, "Loaded personalization for library ${it.name} - ${it.id} - ${it.mediaType}")
|
||||
libraryPersonalizationsDone--
|
||||
}
|
||||
}
|
||||
|
||||
while (libraryPersonalizationsDone > 0) { }
|
||||
|
||||
Log.d(tag, "Finished loading all library personalization data")
|
||||
allLibraryPersonalizationsDone = true
|
||||
cb()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get personalized shelves from server for selected [libraryId].
|
||||
* Populates [cachedLibraryRecentShelves] and [cachedLibraryDiscovery].
|
||||
*/
|
||||
private fun populatePersonalizedDataForLibrary(libraryId: String, cb: () -> Unit) {
|
||||
apiHandler.getLibraryPersonalized(libraryId) { shelves ->
|
||||
Log.d(tag, "populatePersonalizedDataForLibrary $libraryId")
|
||||
if (shelves === null) return@getLibraryPersonalized
|
||||
shelves.map { shelf ->
|
||||
Log.d(tag, "$shelf")
|
||||
if (shelf.type == "book") {
|
||||
if (shelf.id == "continue-listening") return@map
|
||||
else if (shelf.id == "listen-again") return@map
|
||||
else if (shelf.id == "recently-added") {
|
||||
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
|
||||
cachedLibraryRecentShelves[libraryId] = mutableListOf()
|
||||
}
|
||||
if (cachedLibraryRecentShelves[libraryId]?.find { it.id == shelf.id } == null) {
|
||||
cachedLibraryRecentShelves[libraryId]!!.add(shelf)
|
||||
}
|
||||
}
|
||||
else if (shelf.id == "discover") {
|
||||
if (!cachedLibraryDiscovery.containsKey(libraryId)) {
|
||||
cachedLibraryDiscovery[libraryId] = mutableListOf()
|
||||
}
|
||||
(shelf as LibraryShelfBookEntity).entities?.map {
|
||||
cachedLibraryDiscovery[libraryId]!!.add(it)
|
||||
}
|
||||
}
|
||||
else if (shelf.id == "continue-reading") return@map
|
||||
else if (shelf.id == "continue-series") return@map
|
||||
shelf as LibraryShelfBookEntity
|
||||
} else if (shelf.type == "series") {
|
||||
if (shelf.id == "recent-series") {
|
||||
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
|
||||
cachedLibraryRecentShelves[libraryId] = mutableListOf()
|
||||
}
|
||||
if (cachedLibraryRecentShelves[libraryId]?.find { it.id == shelf.id } == null) {
|
||||
cachedLibraryRecentShelves[libraryId]!!.add(shelf)
|
||||
}
|
||||
}
|
||||
} else if (shelf.type == "episode") {
|
||||
if (shelf.id == "continue-listening") return@map
|
||||
else if (shelf.id == "listen-again") return@map
|
||||
else if (shelf.id == "newest-episodes") {
|
||||
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
|
||||
cachedLibraryRecentShelves[libraryId] = mutableListOf()
|
||||
}
|
||||
if (cachedLibraryRecentShelves[libraryId]?.find { it.id == shelf.id } == null) {
|
||||
cachedLibraryRecentShelves[libraryId]!!.add(shelf)
|
||||
}
|
||||
|
||||
val podcastLibraryItemIds = mutableListOf<String>()
|
||||
(shelf as LibraryShelfEpisodeEntity).entities?.forEach { libraryItem ->
|
||||
if (!podcastLibraryItemIds.contains(libraryItem.id)) {
|
||||
podcastLibraryItemIds.add(libraryItem.id)
|
||||
loadPodcastItem(libraryItem.libraryId, libraryItem.id) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (shelf.type == "podcast") {
|
||||
if (shelf.id == "recently-added"){
|
||||
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
|
||||
cachedLibraryRecentShelves[libraryId] = mutableListOf()
|
||||
}
|
||||
if (cachedLibraryRecentShelves[libraryId]?.find { it.id == shelf.id } == null) {
|
||||
cachedLibraryRecentShelves[libraryId]!!.add(shelf)
|
||||
}
|
||||
}
|
||||
else if (shelf.id == "discover"){
|
||||
return@map
|
||||
}
|
||||
} else if (shelf.type =="authors") {
|
||||
if (shelf.id == "newest-authors") {
|
||||
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
|
||||
cachedLibraryRecentShelves[libraryId] = mutableListOf()
|
||||
}
|
||||
if (cachedLibraryRecentShelves[libraryId]?.find { it.id == shelf.id } == null) {
|
||||
cachedLibraryRecentShelves[libraryId]!!.add(shelf)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Log.d(tag, "populatePersonalizedDataForLibrary $libraryId DONE")
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns podcasts for selected library.
|
||||
* If data is not found from local cache it is loaded from server
|
||||
*/
|
||||
fun loadLibraryPodcasts(libraryId:String, cb: (List<LibraryItem>?) -> Unit) {
|
||||
// Without this there is possibility that only recent podcasts get loaded
|
||||
// Loading recent podcasts will also create cachedLibraryPodcasts entry for library
|
||||
if (!isLibraryPodcastsCached.containsKey(libraryId)) {
|
||||
isLibraryPodcastsCached[libraryId] = false
|
||||
}
|
||||
// Ensure that there is map for library
|
||||
if (!cachedLibraryPodcasts.containsKey(libraryId)) {
|
||||
cachedLibraryPodcasts[libraryId] = mutableMapOf()
|
||||
}
|
||||
if (isLibraryPodcastsCached.getOrElse(libraryId) {false}) {
|
||||
Log.d(tag, "loadLibraryPodcasts: Found from cache: $libraryId")
|
||||
cb(cachedLibraryPodcasts[libraryId]?.values?.sortedBy { libraryItem -> (libraryItem.media as Podcast).metadata.title })
|
||||
} else {
|
||||
apiHandler.getLibraryItems(libraryId) { libraryItems ->
|
||||
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
|
||||
if (libraryItemsWithAudio.isNotEmpty()) {
|
||||
selectedLibraryId = libraryId
|
||||
}
|
||||
|
||||
selectedLibraryItems = mutableListOf()
|
||||
libraryItemsWithAudio.forEach { libraryItem ->
|
||||
selectedLibraryItems.add(libraryItem)
|
||||
cachedLibraryPodcasts[libraryId]?.set(libraryItem.id, libraryItem)
|
||||
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||
serverLibraryItems.add(libraryItem)
|
||||
}
|
||||
}
|
||||
isLibraryPodcastsCached[libraryId] = true
|
||||
Log.d(tag, "loadLibraryPodcasts: loaded from server: $libraryId")
|
||||
cb(libraryItemsWithAudio.sortedBy { libraryItem -> (libraryItem.media as Podcast).metadata.title })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns series with audio books from selected library.
|
||||
* If data is not found from local cache then it will be fetched from server
|
||||
*/
|
||||
fun loadLibrarySeriesWithAudio(libraryId:String, cb: (List<LibrarySeriesItem>) -> Unit) {
|
||||
// Check "cache" first
|
||||
if (cachedLibrarySeries.containsKey(libraryId)) {
|
||||
Log.d(tag, "Series with audio found from cache | Library $libraryId ")
|
||||
cb(cachedLibrarySeries[libraryId] as List<LibrarySeriesItem>)
|
||||
} else {
|
||||
apiHandler.getLibrarySeries(libraryId) { seriesItems ->
|
||||
Log.d(tag, "Series with audio loaded from server | Library $libraryId")
|
||||
val seriesItemsWithAudio = seriesItems.filter { si -> si.audiobookCount > 0 }
|
||||
|
||||
cachedLibrarySeries[libraryId] = seriesItemsWithAudio
|
||||
|
||||
cb(seriesItemsWithAudio)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns series with audiobooks from selected library using filter for paging.
|
||||
* If data is not found from local cache then it will be fetched from server
|
||||
*/
|
||||
fun loadLibrarySeriesWithAudio(libraryId:String, seriesFilter:String, cb: (List<LibrarySeriesItem>) -> Unit) {
|
||||
// Check "cache" first
|
||||
if (!cachedLibrarySeries.containsKey(libraryId)) {
|
||||
loadLibrarySeriesWithAudio(libraryId) {}
|
||||
} else {
|
||||
Log.d(tag, "Series with audio found from cache | Library $libraryId ")
|
||||
}
|
||||
val seriesWithBooks = cachedLibrarySeries[libraryId]!!.filter { ls -> ls.title.uppercase().startsWith(seriesFilter) }.toList()
|
||||
cb(seriesWithBooks)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorts books in series. Assumes that sequence is main.minor
|
||||
*/
|
||||
private fun sortSeriesBooks(seriesBooks: List<LibraryItem>) : List<LibraryItem> {
|
||||
val sortingLogic = compareBy<LibraryItem> { it.seriesSequenceParts[0].length }
|
||||
.thenBy { it.seriesSequenceParts[0].ifEmpty { "" } }
|
||||
.thenBy { it.seriesSequenceParts.getOrElse(1) { "" }.length }
|
||||
.thenBy { it.seriesSequenceParts.getOrElse(1) { "" } }
|
||||
return seriesBooks.sortedWith(sortingLogic)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns books for series from library.
|
||||
* If data is not found from local cache then it will be fetched from server
|
||||
*/
|
||||
fun loadLibrarySeriesItemsWithAudio(libraryId:String, seriesId:String, cb: (List<LibraryItem>) -> Unit) {
|
||||
// Check "cache" first
|
||||
if (!cachedLibrarySeriesItem.containsKey(libraryId)) {
|
||||
cachedLibrarySeriesItem[libraryId] = hashMapOf()
|
||||
}
|
||||
if (cachedLibrarySeriesItem[libraryId]!!.containsKey(seriesId)) {
|
||||
Log.d(tag, "Items for series $seriesId found from cache | Library $libraryId")
|
||||
cachedLibrarySeriesItem[libraryId]!![seriesId]?.let { cb(it) }
|
||||
} else {
|
||||
apiHandler.getLibrarySeriesItems(libraryId, seriesId) { libraryItems ->
|
||||
Log.d(tag, "Items for series $seriesId loaded from server | Library $libraryId")
|
||||
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
|
||||
|
||||
val sortedLibraryItemsWithAudio = sortSeriesBooks(libraryItemsWithAudio)
|
||||
cachedLibrarySeriesItem[libraryId]!![seriesId] = sortedLibraryItemsWithAudio
|
||||
|
||||
sortedLibraryItemsWithAudio.forEach { libraryItem ->
|
||||
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||
serverLibraryItems.add(libraryItem)
|
||||
}
|
||||
}
|
||||
cb(sortedLibraryItemsWithAudio)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns authors with books from library.
|
||||
* If data is not found from local cache then it will be fetched from server
|
||||
*/
|
||||
fun loadAuthorsWithBooks(libraryId:String, cb: (List<LibraryAuthorItem>) -> Unit) {
|
||||
// Check "cache" first
|
||||
if (cachedLibraryAuthors.containsKey(libraryId)) {
|
||||
Log.d(tag, "Authors with books found from cache | Library $libraryId ")
|
||||
cb(cachedLibraryAuthors[libraryId]!!.values.toList())
|
||||
} else {
|
||||
// Fetch data from server and add it to local "cache"
|
||||
apiHandler.getLibraryAuthors(libraryId) { authorItems ->
|
||||
Log.d(tag, "Authors with books loaded from server | Library $libraryId ")
|
||||
// TO-DO: This check won't ensure that there is audiobooks. Current API won't offer ability to do so
|
||||
var authorItemsWithBooks = authorItems.filter { li -> li.bookCount != null && li.bookCount!! > 0 }
|
||||
authorItemsWithBooks = authorItemsWithBooks.sortedBy { it.name }
|
||||
// Ensure that there is map for library
|
||||
cachedLibraryAuthors[libraryId] = mutableMapOf()
|
||||
// Cache authors
|
||||
authorItemsWithBooks.forEach {
|
||||
if (!cachedLibraryAuthors[libraryId]!!.containsKey(it.id)) {
|
||||
cachedLibraryAuthors[libraryId]!![it.id] = it
|
||||
}
|
||||
}
|
||||
cb(authorItemsWithBooks)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns authors with books from selected library using filter for paging.
|
||||
* If data is not found from local cache then it will be fetched from server
|
||||
*/
|
||||
fun loadAuthorsWithBooks(libraryId:String, authorFilter: String, cb: (List<LibraryAuthorItem>) -> Unit) {
|
||||
// Check "cache" first
|
||||
if (cachedLibraryAuthors.containsKey(libraryId)) {
|
||||
Log.d(tag, "Authors with books found from cache | Library $libraryId ")
|
||||
} else {
|
||||
loadAuthorsWithBooks(libraryId) {}
|
||||
}
|
||||
val authorsWithBooks = cachedLibraryAuthors[libraryId]!!.values.filter { lai -> lai.name.uppercase().startsWith(authorFilter) }.toList()
|
||||
cb(authorsWithBooks)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns audiobooks for author from library
|
||||
* If data is not found from local cache then it will be fetched from server
|
||||
*/
|
||||
fun loadAuthorBooksWithAudio(libraryId:String, authorId:String, cb: (List<LibraryItem>) -> Unit) {
|
||||
// Ensure that there is map for library
|
||||
if (!cachedLibraryAuthorItems.containsKey(libraryId)) {
|
||||
cachedLibraryAuthorItems[libraryId] = mutableMapOf()
|
||||
}
|
||||
// Check "cache" first
|
||||
if (cachedLibraryAuthorItems[libraryId]!!.containsKey(authorId)) {
|
||||
Log.d(tag, "Items for author $authorId found from cache | Library $libraryId")
|
||||
cachedLibraryAuthorItems[libraryId]!![authorId]?.let { cb(it) }
|
||||
} else {
|
||||
apiHandler.getLibraryItemsFromAuthor(libraryId, authorId) { libraryItems ->
|
||||
Log.d(tag, "Items for author $authorId loaded from server | Library $libraryId")
|
||||
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
|
||||
|
||||
cachedLibraryAuthorItems[libraryId]!![authorId] = libraryItemsWithAudio
|
||||
|
||||
libraryItemsWithAudio.forEach { libraryItem ->
|
||||
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||
serverLibraryItems.add(libraryItem)
|
||||
}
|
||||
}
|
||||
|
||||
cb(libraryItemsWithAudio)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns audiobooks for author from specified series within library
|
||||
* If data is not found from local cache then it will be fetched from server
|
||||
*/
|
||||
fun loadAuthorSeriesBooksWithAudio(libraryId:String, authorId:String, seriesId: String, cb: (List<LibraryItem>) -> Unit) {
|
||||
val authorSeriesKey = "$authorId|$seriesId"
|
||||
// Ensure that there is map for library
|
||||
if (!cachedLibraryAuthorSeriesItems.containsKey(libraryId)) {
|
||||
cachedLibraryAuthorSeriesItems[libraryId] = mutableMapOf()
|
||||
}
|
||||
// Check "cache" first
|
||||
if (cachedLibraryAuthorSeriesItems[libraryId]!!.containsKey(authorSeriesKey)) {
|
||||
Log.d(tag, "Items for series $seriesId with author $authorId found from cache | Library $libraryId")
|
||||
cachedLibraryAuthorSeriesItems[libraryId]!![authorSeriesKey]?.let { cb(it) }
|
||||
} else {
|
||||
apiHandler.getLibrarySeriesItems(libraryId, seriesId) { libraryItems ->
|
||||
Log.d(tag, "Items for series $seriesId with author $authorId loaded from server | Library $libraryId")
|
||||
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
|
||||
if (!cachedLibraryAuthors[libraryId]!!.containsKey(authorId)) {
|
||||
Log.d(tag, "Author data is missing")
|
||||
}
|
||||
val authorName = cachedLibraryAuthors[libraryId]!![authorId]?.name ?: ""
|
||||
Log.d(tag, "Using author name: $authorName")
|
||||
val libraryItemsFromAuthorWithAudio = libraryItemsWithAudio.filter { li -> li.authorName.indexOf(authorName, ignoreCase = true) >= 0 }
|
||||
|
||||
val sortedLibraryItemsWithAudio = sortSeriesBooks(libraryItemsFromAuthorWithAudio)
|
||||
cachedLibraryAuthorSeriesItems[libraryId]!![authorId] = sortedLibraryItemsWithAudio
|
||||
|
||||
sortedLibraryItemsWithAudio.forEach { libraryItem ->
|
||||
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||
serverLibraryItems.add(libraryItem)
|
||||
}
|
||||
}
|
||||
|
||||
cb(sortedLibraryItemsWithAudio)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns collections with audiobooks from library
|
||||
* If data is not found from local cache then it will be fetched from server
|
||||
*/
|
||||
fun loadLibraryCollectionsWithAudio(libraryId:String, cb: (List<LibraryCollection>) -> Unit) {
|
||||
if (cachedLibraryCollections.containsKey(libraryId)) {
|
||||
Log.d(tag, "Collections with books found from cache | Library $libraryId ")
|
||||
cb(cachedLibraryCollections[libraryId]!!.values.toList())
|
||||
} else {
|
||||
apiHandler.getLibraryCollections(libraryId) { libraryCollections ->
|
||||
Log.d(tag, "Collections with books loaded from server | Library $libraryId ")
|
||||
val libraryCollectionsWithAudio = libraryCollections.filter { lc -> lc.audiobookCount > 0 }
|
||||
|
||||
// Cache collections
|
||||
cachedLibraryCollections[libraryId] = hashMapOf()
|
||||
libraryCollectionsWithAudio.forEach {
|
||||
if (!cachedLibraryCollections[libraryId]!!.containsKey(it.id)) {
|
||||
cachedLibraryCollections[libraryId]!![it.id] = it
|
||||
}
|
||||
}
|
||||
cb(libraryCollectionsWithAudio)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns audiobooks for collection from library
|
||||
* If data is not found from local cache then it will be fetched from server
|
||||
*/
|
||||
fun loadLibraryCollectionBooksWithAudio(libraryId: String, collectionId: String, cb: (List<LibraryItem>) -> Unit) {
|
||||
if (!cachedLibraryCollections.containsKey(libraryId)) {
|
||||
loadLibraryCollectionsWithAudio(libraryId) {}
|
||||
}
|
||||
Log.d(tag, "Trying to find collection $collectionId items from from cache | Library $libraryId ")
|
||||
if ( cachedLibraryCollections[libraryId]!!.containsKey(collectionId)) {
|
||||
val libraryCollectionBookswithAudio = cachedLibraryCollections[libraryId]!![collectionId]?.books
|
||||
libraryCollectionBookswithAudio?.forEach { libraryItem ->
|
||||
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||
serverLibraryItems.add(libraryItem)
|
||||
}
|
||||
}
|
||||
cb(libraryCollectionBookswithAudio as List<LibraryItem>)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns audiobooks from discovery shelf for [libraryId]
|
||||
* If data is not found from local cache then it will be fetched from server
|
||||
*/
|
||||
fun loadLibraryDiscoveryBooksWithAudio(libraryId: String, cb: (List<LibraryItem>) -> Unit) {
|
||||
if (!cachedLibraryDiscovery.containsKey(libraryId)) {
|
||||
cb(listOf())
|
||||
}
|
||||
val libraryItemsWithAudio = cachedLibraryDiscovery[libraryId]?.filter { li -> li.checkHasTracks() }
|
||||
libraryItemsWithAudio?.forEach { libraryItem -> addServerLibrary(libraryItem) }
|
||||
cb(libraryItemsWithAudio as List<LibraryItem>)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns recent shelves for [libraryId]
|
||||
* If data is not shelves are found returns empty list
|
||||
*/
|
||||
fun getLibraryRecentShelfs(libraryId: String, cb: (List<LibraryShelfType>) -> Unit) {
|
||||
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
|
||||
Log.d(tag, "getLibraryRecentShelfs: No shelves $libraryId")
|
||||
cb(listOf())
|
||||
return
|
||||
}
|
||||
cb(cachedLibraryRecentShelves[libraryId] as List<LibraryShelfType>)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns recent shelf by [type] for [libraryId]
|
||||
* If shelf is not found returns null
|
||||
*/
|
||||
fun getLibraryRecentShelfByType(libraryId: String, type:String, cb: (LibraryShelfType?) -> Unit) {
|
||||
Log.d(tag, "getLibraryRecentShelfByType: $libraryId | $type")
|
||||
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
|
||||
cb(null)
|
||||
return
|
||||
}
|
||||
for (shelf in cachedLibraryRecentShelves[libraryId]!!) {
|
||||
if (shelf.type == type.lowercase()) {
|
||||
cb(shelf)
|
||||
return
|
||||
}
|
||||
}
|
||||
cb(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads podcasts for newest episodes shelf
|
||||
*/
|
||||
private fun loadPodcastItem(libraryId: String, libraryItemId: String, cb: (LibraryItem?) -> Unit) {
|
||||
// Ensure that there is map for library
|
||||
if (!cachedLibraryPodcasts.containsKey(libraryId)) {
|
||||
cachedLibraryPodcasts[libraryId] = mutableMapOf()
|
||||
}
|
||||
if (cachedLibraryPodcasts[libraryId]!!.containsKey(libraryItemId)) {
|
||||
Log.d(tag, "loadPodcastItem: Podcast found from cache | Library $libraryItemId ")
|
||||
cb(cachedLibraryPodcasts[libraryId]?.get(libraryItemId))
|
||||
} else {
|
||||
Log.d(tag, "loadPodcastItem: Calling getLibraryItem $libraryItemId")
|
||||
apiHandler.getLibraryItem(libraryItemId) { libraryItem ->
|
||||
if (libraryItem !== null) {
|
||||
Log.d(tag, "loadPodcastItem: Got library item ${libraryItem.id} ${libraryItem.media.metadata.title}")
|
||||
val podcast = libraryItem.media as Podcast
|
||||
podcast.episodes?.forEach { podcastEpisode ->
|
||||
podcastEpisodeLibraryItemMap[podcastEpisode.id] = LibraryItemWithEpisode(libraryItem, podcastEpisode)
|
||||
}
|
||||
cachedLibraryPodcasts[libraryId]?.set(libraryItemId, libraryItem)
|
||||
cb(libraryItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadLibraryItem(libraryItemId:String, cb: (LibraryItemWrapper?) -> Unit) {
|
||||
if (libraryItemId.startsWith("local")) {
|
||||
cb(DeviceManager.dbManager.getLocalLibraryItem(libraryItemId))
|
||||
|
|
@ -187,8 +669,8 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||
}
|
||||
selectedLibraryItemId = libraryItemWrapper.id
|
||||
selectedPodcast = podcast
|
||||
|
||||
val children = podcast.episodes?.map { podcastEpisode ->
|
||||
val episodes = podcast.episodes?.sortedByDescending { it.publishedAt }
|
||||
val children = episodes?.map { podcastEpisode ->
|
||||
|
||||
val progress = serverUserMediaProgress.find { it.libraryItemId == libraryItemWrapper.id && it.episodeId == podcastEpisode.id }
|
||||
|
||||
|
|
@ -209,13 +691,16 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads libraries for selected server with stats
|
||||
*/
|
||||
private fun loadLibraries(cb: (List<Library>) -> Unit) {
|
||||
if (serverLibraries.isNotEmpty()) {
|
||||
cb(serverLibraries)
|
||||
} else {
|
||||
apiHandler.getLibraries {
|
||||
serverLibraries = it
|
||||
cb(it)
|
||||
apiHandler.getLibraries { loadedLibraries ->
|
||||
serverLibraries = loadedLibraries
|
||||
cb(serverLibraries)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -329,6 +814,25 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
fun initializeInProgressItems(cb: () -> Unit) {
|
||||
Log.d(tag, "Initializing inprogress items")
|
||||
|
||||
loadItemsInProgressForAllLibraries { itemsInProgress ->
|
||||
itemsInProgress.forEach {
|
||||
val libraryItem = it.libraryItemWrapper as LibraryItem
|
||||
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||
serverLibraryItems.add(libraryItem)
|
||||
}
|
||||
|
||||
if (it.episode != null) {
|
||||
podcastEpisodeLibraryItemMap[it.episode.id] = LibraryItemWithEpisode(it.libraryItemWrapper, it.episode)
|
||||
}
|
||||
}
|
||||
Log.d(tag, "Initializing inprogress items done")
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
fun loadAndroidAutoItems(cb: () -> Unit) {
|
||||
Log.d(tag, "Load android auto items")
|
||||
|
||||
|
|
@ -343,23 +847,7 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||
Log.w(tag, "No libraries returned from server request")
|
||||
cb()
|
||||
} else {
|
||||
val library = libraries[0]
|
||||
Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}")
|
||||
|
||||
loadItemsInProgressForAllLibraries { itemsInProgress ->
|
||||
itemsInProgress.forEach {
|
||||
val libraryItem = it.libraryItemWrapper as LibraryItem
|
||||
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||
serverLibraryItems.add(libraryItem)
|
||||
}
|
||||
|
||||
if (it.episode != null) {
|
||||
podcastEpisodeLibraryItemMap[it.episode.id] = LibraryItemWithEpisode(it.libraryItemWrapper, it.episode)
|
||||
}
|
||||
}
|
||||
|
||||
cb() // Fully loaded
|
||||
}
|
||||
cb() // Fully loaded
|
||||
}
|
||||
}
|
||||
} else { // Not connected to server
|
||||
|
|
@ -369,6 +857,65 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles search requests.
|
||||
* Searches from books, series and authors
|
||||
*/
|
||||
suspend fun doSearch(libraryId: String, queryString: String) : Map<String, List<MediaBrowserCompat.MediaItem>> {
|
||||
return suspendCoroutine {
|
||||
apiHandler.getSearchResults(libraryId, queryString) { searchResult ->
|
||||
Log.d(tag, "searchLocalCache: $searchResult")
|
||||
// Nothing found from server
|
||||
if (searchResult === null) {
|
||||
it.resume(mapOf())
|
||||
return@getSearchResults
|
||||
}
|
||||
|
||||
val foundItems: MutableMap<String, List<MediaBrowserCompat.MediaItem>> = mutableMapOf()
|
||||
|
||||
val serverLibrary = serverLibraries.find { sl -> sl.id == libraryId }
|
||||
|
||||
// Books
|
||||
if (searchResult.book !== null && searchResult.book!!.isNotEmpty()) {
|
||||
Log.d(tag, "searchLocalCache: found ${searchResult.book!!.size} books")
|
||||
val children = searchResult.book!!.filter { it.libraryItem.checkHasTracks() }.map { bookResult ->
|
||||
val libraryItem = bookResult.libraryItem
|
||||
|
||||
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
|
||||
serverLibraryItems.add(libraryItem)
|
||||
}
|
||||
val progress = serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
|
||||
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
|
||||
libraryItem.localLibraryItemId = localLibraryItem?.id
|
||||
val description = libraryItem.getMediaDescription(progress, ctx, null, null, "Books (${serverLibrary?.name})")
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||
}
|
||||
foundItems["book"] = children
|
||||
}
|
||||
if (searchResult.series !== null && searchResult.series!!.isNotEmpty()) {
|
||||
Log.d(tag, "onSearch: found ${searchResult.series!!.size} series")
|
||||
val children = searchResult.series!!.map { seriesResult ->
|
||||
val seriesItem = seriesResult.series
|
||||
seriesItem.books = seriesResult.books as MutableList<LibraryItem>
|
||||
val description = seriesItem.getMediaDescription(null, ctx, "Series (${serverLibrary?.name})")
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
foundItems["series"] = children
|
||||
}
|
||||
if (searchResult.authors !== null && searchResult.authors!!.isNotEmpty()) {
|
||||
Log.d(tag, "onSearch: found ${searchResult.authors!!.size} authors")
|
||||
val children = searchResult.authors!!.map { authorItem ->
|
||||
val description = authorItem.getMediaDescription(null, ctx, "Authors (${serverLibrary?.name})")
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
foundItems["authors"] = children
|
||||
}
|
||||
|
||||
it.resume(foundItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getFirstItem() : LibraryItemWrapper? {
|
||||
if (serverLibraryItems.isNotEmpty()) {
|
||||
return serverLibraryItems[0]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
package com.audiobookshelf.app.media
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.annotation.AnyRes
|
||||
import com.audiobookshelf.app.R
|
||||
|
||||
/**
|
||||
* get uri to drawable or any other resource type if u wish
|
||||
* @param drawableId - drawable res id
|
||||
* @return - uri
|
||||
*/
|
||||
fun getUriToDrawable(context: Context, @AnyRes drawableId: Int): Uri {
|
||||
return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE
|
||||
+ "://" + context.resources.getResourcePackageName(drawableId)
|
||||
+ '/' + context.resources.getResourceTypeName(drawableId)
|
||||
+ '/' + context.resources.getResourceEntryName(drawableId))
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* get uri to drawable or any other resource type if u wish
|
||||
* @param drawableId - drawable res id
|
||||
* @return - uri
|
||||
*/
|
||||
fun getUriToAbsIconDrawable(context: Context, absIconName: String): Uri {
|
||||
val drawableId = when(absIconName) {
|
||||
"audiobookshelf" -> R.drawable.abs_audiobookshelf
|
||||
"authors" -> R.drawable.abs_authors
|
||||
"book-1" -> R.drawable.abs_book_1
|
||||
"books-1" -> R.drawable.abs_books_1
|
||||
"books-2" -> R.drawable.abs_books_2
|
||||
"columns" -> R.drawable.abs_columns
|
||||
"database" -> R.drawable.abs_database
|
||||
"file-picture" -> R.drawable.abs_file_picture
|
||||
"headphones" -> R.drawable.abs_headphones
|
||||
"heart" -> R.drawable.abs_heart
|
||||
"microphone_1" -> R.drawable.abs_microphone_1
|
||||
"microphone_2" -> R.drawable.abs_microphone_2
|
||||
"microphone_3" -> R.drawable.abs_microphone_3
|
||||
"music" -> R.drawable.abs_music
|
||||
"podcast" -> R.drawable.abs_podcast
|
||||
"radio" -> R.drawable.abs_radio
|
||||
"rocket" -> R.drawable.abs_rocket
|
||||
"rss" -> R.drawable.abs_rss
|
||||
"star" -> R.drawable.abs_star
|
||||
else -> R.drawable.icon_library_folder
|
||||
}
|
||||
return Uri.parse(
|
||||
ContentResolver.SCHEME_ANDROID_RESOURCE
|
||||
+ "://" + context.resources.getResourcePackageName(drawableId)
|
||||
+ '/' + context.resources.getResourceTypeName(drawableId)
|
||||
+ '/' + context.resources.getResourceEntryName(drawableId))
|
||||
}
|
||||
|
|
@ -1,51 +1,45 @@
|
|||
package com.audiobookshelf.app.player
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
import androidx.annotation.AnyRes
|
||||
import android.util.Log
|
||||
import com.audiobookshelf.app.R
|
||||
import com.audiobookshelf.app.data.*
|
||||
import com.audiobookshelf.app.media.getUriToDrawable
|
||||
|
||||
class BrowseTree(
|
||||
val context: Context,
|
||||
itemsInProgress: List<ItemInProgress>,
|
||||
libraries: List<Library>
|
||||
libraries: List<Library>,
|
||||
recentsLoaded: Boolean
|
||||
) {
|
||||
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()
|
||||
|
||||
/**
|
||||
* get uri to drawable or any other resource type if u wish
|
||||
* @param drawableId - drawable res id
|
||||
* @return - uri
|
||||
*/
|
||||
private fun getUriToDrawable(@AnyRes drawableId: Int): Uri {
|
||||
return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE
|
||||
+ "://" + context.resources.getResourcePackageName(drawableId)
|
||||
+ '/' + context.resources.getResourceTypeName(drawableId)
|
||||
+ '/' + context.resources.getResourceEntryName(drawableId))
|
||||
}
|
||||
|
||||
init {
|
||||
val rootList = mediaIdToChildren[AUTO_BROWSE_ROOT] ?: mutableListOf()
|
||||
|
||||
val continueListeningMetadata = MediaMetadataCompat.Builder().apply {
|
||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, CONTINUE_ROOT)
|
||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Listening")
|
||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.exo_icon_localaudio).toString())
|
||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Continue")
|
||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_localaudio).toString())
|
||||
}.build()
|
||||
|
||||
val recentMetadata = MediaMetadataCompat.Builder().apply {
|
||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, RECENTLY_ROOT)
|
||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Recent")
|
||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.md_clock_outline).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(R.drawable.exo_icon_downloaddone).toString())
|
||||
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(R.drawable.icon_library_folder).toString())
|
||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.icon_library_folder).toString())
|
||||
}.build()
|
||||
|
||||
if (itemsInProgress.isNotEmpty()) {
|
||||
|
|
@ -53,13 +47,28 @@ class BrowseTree(
|
|||
}
|
||||
|
||||
if (libraries.isNotEmpty()) {
|
||||
if (recentsLoaded) {
|
||||
rootList += recentMetadata
|
||||
}
|
||||
rootList += librariesMetadata
|
||||
|
||||
libraries.forEach { library ->
|
||||
val libraryMediaMetadata = library.getMediaMetadata()
|
||||
// Skip libraries without audio content
|
||||
if (library.stats?.numAudioFiles == 0) return@forEach
|
||||
Log.d("BrowseTree", "Library $library | ${library.icon}")
|
||||
// Generate library list items for Libraries menu
|
||||
val libraryMediaMetadata = library.getMediaMetadata(context)
|
||||
val children = mediaIdToChildren[LIBRARIES_ROOT] ?: mutableListOf()
|
||||
children += libraryMediaMetadata
|
||||
mediaIdToChildren[LIBRARIES_ROOT] = children
|
||||
|
||||
if (recentsLoaded) {
|
||||
// Generate library list items for Recent menu
|
||||
val recentlyMediaMetadata = library.getMediaMetadata(context,"recently")
|
||||
val childrenRecently = mediaIdToChildren[RECENTLY_ROOT] ?: mutableListOf()
|
||||
childrenRecently += recentlyMediaMetadata
|
||||
mediaIdToChildren[RECENTLY_ROOT] = childrenRecently
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -75,3 +84,4 @@ const val AUTO_BROWSE_ROOT = "/"
|
|||
const val CONTINUE_ROOT = "__CONTINUE__"
|
||||
const val DOWNLOADS_ROOT = "__DOWNLOADS__"
|
||||
const val LIBRARIES_ROOT = "__LIBRARIES__"
|
||||
const val RECENTLY_ROOT = "__RECENTLY__"
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ import com.audiobookshelf.app.media.MediaManager
|
|||
import com.audiobookshelf.app.media.MediaProgressSyncer
|
||||
import com.audiobookshelf.app.server.ApiHandler
|
||||
import com.audiobookshelf.app.BuildConfig
|
||||
import com.audiobookshelf.app.media.getUriToAbsIconDrawable
|
||||
import com.audiobookshelf.app.media.getUriToDrawable
|
||||
import com.google.android.exoplayer2.*
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||
|
|
@ -47,6 +49,7 @@ import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
|||
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
||||
import com.google.android.exoplayer2.upstream.*
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.util.*
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
|
|
@ -119,6 +122,18 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
private var mShakeDetector: ShakeDetector? = null
|
||||
private var shakeSensorUnregisterTask:TimerTask? = null
|
||||
|
||||
// These are used to trigger reloading if
|
||||
private var forceReloadingAndroidAuto:Boolean = false
|
||||
private var firstLoadDone:Boolean = false
|
||||
|
||||
fun isBrowseTreeInitialized() : Boolean {
|
||||
return this::browseTree.isInitialized
|
||||
}
|
||||
|
||||
// Cache latest search so it wont trigger again when returning from series for example
|
||||
private var cachedSearch : String = ""
|
||||
private var cachedSearchResults : MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||
|
||||
/*
|
||||
Service related stuff
|
||||
*/
|
||||
|
|
@ -977,6 +992,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
|
||||
private val AUTO_MEDIA_ROOT = "/"
|
||||
private val LIBRARIES_ROOT = "__LIBRARIES__"
|
||||
private val RECENTLY_ROOT = "__RECENTLY__"
|
||||
private val DOWNLOADS_ROOT = "__DOWNLOADS__"
|
||||
private val CONTINUE_ROOT = "__CONTINUE__"
|
||||
private lateinit var browseTree:BrowseTree
|
||||
|
|
@ -999,8 +1015,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
// No further calls will be made to other media browsing methods.
|
||||
null
|
||||
} else {
|
||||
Log.d(tag, "Android Auto starting")
|
||||
isStarted = true
|
||||
mediaManager.checkResetServerItems() // Reset any server items if no longer connected to server
|
||||
|
||||
// Reset cache if no longer connected to server or server changed
|
||||
if (mediaManager.checkResetServerItems()) {
|
||||
forceReloadingAndroidAuto = true
|
||||
}
|
||||
|
||||
isAndroidAuto = true
|
||||
|
||||
|
|
@ -1026,33 +1047,45 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
|
||||
result.detach()
|
||||
|
||||
// Prevent crashing if app is restarted while browsing
|
||||
if ((parentMediaId != DOWNLOADS_ROOT && parentMediaId != AUTO_MEDIA_ROOT) && !firstLoadDone){
|
||||
result.sendResult(null)
|
||||
return
|
||||
}
|
||||
|
||||
if (parentMediaId == DOWNLOADS_ROOT) { // Load downloads
|
||||
|
||||
val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
|
||||
val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast")
|
||||
val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||
val localBrowseItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||
|
||||
localBooks.forEach { localLibraryItem ->
|
||||
if (localLibraryItem.media.getAudioTracks().isNotEmpty()) {
|
||||
val progress = DeviceManager.dbManager.getLocalMediaProgress(localLibraryItem.id)
|
||||
val description = localLibraryItem.getMediaDescription(progress, ctx)
|
||||
|
||||
localBrowseItems += MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||
localBrowseItems += MediaBrowserCompat.MediaItem(
|
||||
description,
|
||||
MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
localPodcasts.forEach { localLibraryItem ->
|
||||
val mediaDescription = localLibraryItem.getMediaDescription(null, ctx)
|
||||
localBrowseItems += MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
localBrowseItems += MediaBrowserCompat.MediaItem(
|
||||
mediaDescription,
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
}
|
||||
|
||||
result.sendResult(localBrowseItems)
|
||||
|
||||
} else if (parentMediaId == CONTINUE_ROOT) {
|
||||
val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||
val localBrowseItems: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||
mediaManager.serverItemsInProgress.forEach { itemInProgress ->
|
||||
val progress: MediaProgressWrapper?
|
||||
val mediaDescription:MediaDescriptionCompat
|
||||
val mediaDescription: MediaDescriptionCompat
|
||||
if (itemInProgress.episode != null) {
|
||||
if (itemInProgress.isLocal) {
|
||||
progress = DeviceManager.dbManager.getLocalMediaProgress("${itemInProgress.libraryItemWrapper.id}-${itemInProgress.episode.id}")
|
||||
|
|
@ -1082,32 +1115,459 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
localBrowseItems += MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||
}
|
||||
result.sendResult(localBrowseItems)
|
||||
} else if (parentMediaId == LIBRARIES_ROOT || parentMediaId == AUTO_MEDIA_ROOT) {
|
||||
mediaManager.loadAndroidAutoItems {
|
||||
browseTree = BrowseTree(this, mediaManager.serverItemsInProgress, mediaManager.serverLibraries)
|
||||
} else if (parentMediaId == AUTO_MEDIA_ROOT) {
|
||||
Log.d(tag, "Trying to initialize browseTree.")
|
||||
if (!this::browseTree.isInitialized || forceReloadingAndroidAuto) {
|
||||
forceReloadingAndroidAuto = false
|
||||
mediaManager.loadAndroidAutoItems {
|
||||
Log.d(tag, "android auto loaded. Starting browseTree initialize")
|
||||
browseTree = BrowseTree(this, mediaManager.serverItemsInProgress, mediaManager.serverLibraries, mediaManager.allLibraryPersonalizationsDone)
|
||||
val children = browseTree[parentMediaId]?.map { item ->
|
||||
Log.d(tag, "Found top menu item: ${item.description.title}")
|
||||
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
Log.d(tag, "browseTree initialize and android auto loaded")
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
firstLoadDone = true
|
||||
if (mediaManager.serverLibraries.isNotEmpty()) {
|
||||
Log.d(tag, "Starting personalization fetch")
|
||||
mediaManager.populatePersonalizedDataForAllLibraries {
|
||||
notifyChildrenChanged("/")
|
||||
}
|
||||
|
||||
Log.d(tag, "Initialize inprogress items")
|
||||
mediaManager.initializeInProgressItems {
|
||||
notifyChildrenChanged("/")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(tag, "Starting browseTree refresh")
|
||||
browseTree = BrowseTree(this, mediaManager.serverItemsInProgress, mediaManager.serverLibraries, mediaManager.allLibraryPersonalizationsDone)
|
||||
val children = browseTree[parentMediaId]?.map { item ->
|
||||
Log.d(tag, "Loading Browser Media Item ${item.description.title}")
|
||||
Log.d(tag, "Found top menu item: ${item.description.title}")
|
||||
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
Log.d(tag, "browseTree initialize and android auto loaded")
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
} else if (parentMediaId == LIBRARIES_ROOT || parentMediaId == RECENTLY_ROOT) {
|
||||
Log.d(tag, "First load done: $firstLoadDone")
|
||||
if (!firstLoadDone) {
|
||||
result.sendResult(null)
|
||||
return
|
||||
}
|
||||
// Wait until top-menu is initialized
|
||||
while (!this::browseTree.isInitialized) {}
|
||||
val children = browseTree[parentMediaId]?.map { item ->
|
||||
Log.d(tag, "[MENU: $parentMediaId] Showing list item ${item.description.title}")
|
||||
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
} else if (mediaManager.getIsLibrary(parentMediaId)) { // Load library items for library
|
||||
Log.d(tag, "Loading items for library $parentMediaId")
|
||||
val selectedLibrary = mediaManager.getLibrary(parentMediaId)
|
||||
if (selectedLibrary?.mediaType == "podcast") { // Podcasts are browseable
|
||||
mediaManager.loadLibraryPodcasts(parentMediaId) { libraryItems ->
|
||||
val children = libraryItems?.map { libraryItem ->
|
||||
val mediaDescription = libraryItem.getMediaDescription(null, ctx)
|
||||
MediaBrowserCompat.MediaItem(
|
||||
mediaDescription,
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
} else {
|
||||
val children = mutableListOf(
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle("Authors")
|
||||
.setMediaId("__LIBRARY__${parentMediaId}__AUTHORS")
|
||||
.setIconUri(getUriToAbsIconDrawable(ctx, "authors"))
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
),
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle("Series")
|
||||
.setMediaId("__LIBRARY__${parentMediaId}__SERIES_LIST")
|
||||
.setIconUri(getUriToAbsIconDrawable(ctx, "columns"))
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
),
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle("Collections")
|
||||
.setMediaId("__LIBRARY__${parentMediaId}__COLLECTIONS")
|
||||
.setIconUri(getUriToDrawable(ctx, R.drawable.md_book_multiple_outline))
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
)
|
||||
if (mediaManager.getHasDiscovery(parentMediaId)) {
|
||||
children.add(
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle("Discovery")
|
||||
.setMediaId("__LIBRARY__${parentMediaId}__DISCOVERY")
|
||||
.setIconUri(getUriToDrawable(ctx, R.drawable.md_telescope))
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
} else if (mediaManager.getIsLibrary(parentMediaId)) { // Load library items for library
|
||||
Log.d(tag, "Loading items for library $parentMediaId")
|
||||
mediaManager.loadLibraryItemsWithAudio(parentMediaId) { libraryItems ->
|
||||
val children = libraryItems.map { libraryItem ->
|
||||
if (libraryItem.mediaType == "podcast") { // Podcasts are browseable
|
||||
val mediaDescription = libraryItem.getMediaDescription(null, ctx)
|
||||
MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
} else if (parentMediaId.startsWith(RECENTLY_ROOT)) {
|
||||
Log.d(tag, "Browsing recently $parentMediaId")
|
||||
val mediaIdParts = parentMediaId.split("__")
|
||||
if (!mediaManager.getIsLibrary(mediaIdParts[2])) {
|
||||
Log.d(tag, "${mediaIdParts[2]} is not library")
|
||||
result.sendResult(null)
|
||||
return
|
||||
}
|
||||
Log.d(tag, "Mediaparts: ${mediaIdParts.size} | $mediaIdParts")
|
||||
if (mediaIdParts.size == 3) {
|
||||
mediaManager.getLibraryRecentShelfs(mediaIdParts[2]) { availableShelfs ->
|
||||
Log.d(tag, "Found ${availableShelfs.size} shelfs")
|
||||
val children : MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||
for (shelf in availableShelfs) {
|
||||
if (shelf.type == "book") {
|
||||
children.add(
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle("Books")
|
||||
.setMediaId("${parentMediaId}__BOOK")
|
||||
.setIconUri(getUriToDrawable(ctx, R.drawable.md_book_open_blank_variant_outline))
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
)
|
||||
} else if (shelf.type == "series") {
|
||||
children.add(
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle("Series")
|
||||
.setMediaId("${parentMediaId}__SERIES")
|
||||
.setIconUri(getUriToAbsIconDrawable(ctx, "columns"))
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
)
|
||||
} else if (shelf.type == "episode") {
|
||||
children.add(
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle("Episodes")
|
||||
.setMediaId("${parentMediaId}__EPISODE")
|
||||
.setIconUri(getUriToAbsIconDrawable(ctx, "microphone_2"))
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
)
|
||||
} else if (shelf.type == "podcast") {
|
||||
children.add(
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle("Podcast")
|
||||
.setMediaId("${parentMediaId}__PODCAST")
|
||||
.setIconUri(getUriToAbsIconDrawable(ctx, "podcast"))
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
)
|
||||
} else if (shelf.type == "authors") {
|
||||
children.add(
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle("Authors")
|
||||
.setMediaId("${parentMediaId}__AUTHORS")
|
||||
.setIconUri(getUriToAbsIconDrawable(ctx, "authors"))
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
} else if (mediaIdParts.size == 4) {
|
||||
mediaManager.getLibraryRecentShelfByType(mediaIdParts[2], mediaIdParts[3]) { shelf ->
|
||||
if (shelf === null) {
|
||||
result.sendResult(mutableListOf())
|
||||
}else {
|
||||
if (shelf.type == "book") {
|
||||
val children = (shelf as LibraryShelfBookEntity).entities?.map { libraryItem ->
|
||||
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
|
||||
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
|
||||
libraryItem.localLibraryItemId = localLibraryItem?.id
|
||||
val description = libraryItem.getMediaDescription(progress, ctx, null, false)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
} else if (shelf.type == "episode") {
|
||||
val episodesWithRecentEpisode = (shelf as LibraryShelfEpisodeEntity).entities?.filter { libraryItem -> libraryItem.recentEpisode !== null }
|
||||
val children = episodesWithRecentEpisode?.map { libraryItem ->
|
||||
val podcast = libraryItem.media as Podcast
|
||||
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.libraryId && it.episodeId == libraryItem.recentEpisode?.id }
|
||||
|
||||
// to show download icon
|
||||
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.recentEpisode!!.id)
|
||||
localLibraryItem?.let { lli ->
|
||||
val localEpisode = (lli.media as Podcast).episodes?.find { it.serverEpisodeId == libraryItem.recentEpisode.id }
|
||||
libraryItem.recentEpisode.localEpisodeId = localEpisode?.id
|
||||
}
|
||||
|
||||
val description = libraryItem.recentEpisode.getMediaDescription(libraryItem, progress, ctx)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
} else if (shelf.type == "podcast") {
|
||||
val children = (shelf as LibraryShelfPodcastEntity).entities?.map { libraryItem ->
|
||||
val mediaDescription = libraryItem.getMediaDescription(null, ctx)
|
||||
MediaBrowserCompat.MediaItem(
|
||||
mediaDescription,
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
} else if (shelf.type == "series") {
|
||||
val children = (shelf as LibraryShelfSeriesEntity).entities?.map { librarySeriesItem ->
|
||||
val description = librarySeriesItem.getMediaDescription(null, ctx)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
} else if (shelf.type == "authors") {
|
||||
val children = (shelf as LibraryShelfAuthorEntity).entities?.map { authorItem ->
|
||||
val description = authorItem.getMediaDescription(null, ctx)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
} else {
|
||||
result.sendResult(mutableListOf())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (parentMediaId.startsWith("__LIBRARY__")) {
|
||||
Log.d(tag, "Browsing library $parentMediaId")
|
||||
val mediaIdParts = parentMediaId.split("__")
|
||||
/*
|
||||
MediaIdParts for Library
|
||||
1: LIBRARY
|
||||
2: mediaId for library
|
||||
3: Browsing style (AUTHORS, AUTHOR, AUTHOR_SERIES, SERIES_LIST, SERIES, COLLECTION, COLLECTIONS, DISCOVERY)
|
||||
4:
|
||||
- Paging: SERIES_LIST, AUTHORS
|
||||
- SeriesId: SERIES
|
||||
- AuthorId: AUTHOR, AUTHOR_SERIES
|
||||
- CollectionId: COLLECTIONS
|
||||
5: SeriesId: AUTHOR_SERIES
|
||||
*/
|
||||
if (!mediaManager.getIsLibrary(mediaIdParts[2])) {
|
||||
Log.d(tag, "${mediaIdParts[2]} is not library")
|
||||
result.sendResult(null)
|
||||
return
|
||||
}
|
||||
Log.d(tag, "$mediaIdParts")
|
||||
if (mediaIdParts[3] == "SERIES_LIST" && mediaIdParts.size == 5) {
|
||||
Log.d(tag, "Loading series from library ${mediaIdParts[2]} with paging ${mediaIdParts[4]}")
|
||||
mediaManager.loadLibrarySeriesWithAudio(mediaIdParts[2], mediaIdParts[4]) { seriesItems ->
|
||||
Log.d(tag, "Received ${seriesItems.size} series")
|
||||
|
||||
val seriesLetters = seriesItems.groupingBy { iwb -> iwb.title.substring(0, mediaIdParts[4].length + 1).uppercase() }.eachCount()
|
||||
if (seriesItems.size > DeviceManager.deviceData.deviceSettings!!.androidAutoBrowseLimitForGrouping && seriesItems.size > 1 && seriesLetters.size > 1) {
|
||||
val children = seriesLetters.map { (seriesLetter, seriesCount) ->
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle(seriesLetter)
|
||||
.setMediaId("${parentMediaId}${seriesLetter.last()}")
|
||||
.setSubtitle("$seriesCount series")
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
} else {
|
||||
val children = seriesItems.map { seriesItem ->
|
||||
val description = seriesItem.getMediaDescription(null, ctx)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
}
|
||||
} else if (mediaIdParts[3] == "SERIES_LIST") {
|
||||
Log.d(tag, "Loading series from library ${mediaIdParts[2]}")
|
||||
mediaManager.loadLibrarySeriesWithAudio(mediaIdParts[2]) { seriesItems ->
|
||||
Log.d(tag, "Received ${seriesItems.size} series")
|
||||
if (seriesItems.size > DeviceManager.deviceData.deviceSettings!!.androidAutoBrowseLimitForGrouping && seriesItems.size > 1) {
|
||||
val seriesLetters = seriesItems.groupingBy { iwb -> iwb.title.first().uppercaseChar() }.eachCount()
|
||||
val children = seriesLetters.map { (seriesLetter, seriesCount) ->
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle(seriesLetter.toString())
|
||||
.setSubtitle("$seriesCount series")
|
||||
.setMediaId("${parentMediaId}__${seriesLetter}")
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
} else {
|
||||
val children = seriesItems.map { seriesItem ->
|
||||
val description = seriesItem.getMediaDescription(null, ctx)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
}
|
||||
} else if (mediaIdParts[3] == "SERIES") {
|
||||
Log.d(tag, "Loading items for serie ${mediaIdParts[4]} from library ${mediaIdParts[2]}")
|
||||
mediaManager.loadLibrarySeriesItemsWithAudio(
|
||||
mediaIdParts[2],
|
||||
mediaIdParts[4]
|
||||
) { libraryItems ->
|
||||
Log.d(tag, "Received ${libraryItems.size} library items")
|
||||
var items = libraryItems
|
||||
if (DeviceManager.deviceData.deviceSettings!!.androidAutoBrowseSeriesSequenceOrder === AndroidAutoBrowseSeriesSequenceOrderSetting.DESC) {
|
||||
items = libraryItems.reversed()
|
||||
}
|
||||
val children = items.map { libraryItem ->
|
||||
val progress =
|
||||
mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
|
||||
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
|
||||
libraryItem.localLibraryItemId = localLibraryItem?.id
|
||||
val description = libraryItem.getMediaDescription(progress, ctx, null, true)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
} else if (mediaIdParts[3] == "AUTHORS" && mediaIdParts.size == 5) {
|
||||
Log.d(tag, "Loading authors from library ${mediaIdParts[2]} with paging ${mediaIdParts[4]}")
|
||||
mediaManager.loadAuthorsWithBooks(mediaIdParts[2], mediaIdParts[4]) { authorItems ->
|
||||
Log.d(tag, "Received ${authorItems.size} authors")
|
||||
|
||||
val authorLetters = authorItems.groupingBy { iwb -> iwb.name.substring(0, mediaIdParts[4].length + 1).uppercase() }.eachCount()
|
||||
if (authorItems.size > DeviceManager.deviceData.deviceSettings!!.androidAutoBrowseLimitForGrouping && authorItems.size > 1 && authorLetters.size > 1) {
|
||||
val children = authorLetters.map { (authorLetter, authorCount) ->
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle(authorLetter)
|
||||
.setMediaId("${parentMediaId}${authorLetter.last()}")
|
||||
.setSubtitle("$authorCount authors")
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
} else {
|
||||
val children = authorItems.map { authorItem ->
|
||||
val description = authorItem.getMediaDescription(null, ctx)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
}
|
||||
} else if (mediaIdParts[3] == "AUTHORS") {
|
||||
Log.d(tag, "Loading authors from library ${mediaIdParts[2]}")
|
||||
mediaManager.loadAuthorsWithBooks(mediaIdParts[2]) { authorItems ->
|
||||
Log.d(tag, "Received ${authorItems.size} authors")
|
||||
if (authorItems.size > DeviceManager.deviceData.deviceSettings!!.androidAutoBrowseLimitForGrouping && authorItems.size > 1) {
|
||||
val authorLetters = authorItems.groupingBy { iwb -> iwb.name.first().uppercaseChar() }.eachCount()
|
||||
val children = authorLetters.map { (authorLetter, authorCount) ->
|
||||
MediaBrowserCompat.MediaItem(
|
||||
MediaDescriptionCompat.Builder()
|
||||
.setTitle(authorLetter.toString())
|
||||
.setSubtitle("$authorCount authors")
|
||||
.setMediaId("${parentMediaId}__${authorLetter}")
|
||||
.build(),
|
||||
MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||
)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
} else {
|
||||
val children = authorItems.map { authorItem ->
|
||||
val description = authorItem.getMediaDescription(null, ctx)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
}
|
||||
} else if (mediaIdParts[3] == "AUTHOR") {
|
||||
mediaManager.loadAuthorBooksWithAudio(mediaIdParts[2], mediaIdParts[4]) { libraryItems ->
|
||||
val children = libraryItems.map { libraryItem ->
|
||||
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
|
||||
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
|
||||
libraryItem.localLibraryItemId = localLibraryItem?.id
|
||||
if (libraryItem.collapsedSeries != null) {
|
||||
val description = libraryItem.getMediaDescription(progress, ctx, mediaIdParts[4])
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
} else {
|
||||
val description = libraryItem.getMediaDescription(progress, ctx)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||
}
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
} else if (mediaIdParts[3] == "AUTHOR_SERIES") {
|
||||
mediaManager.loadAuthorSeriesBooksWithAudio(mediaIdParts[2], mediaIdParts[4], mediaIdParts[5]) { libraryItems ->
|
||||
var items = libraryItems
|
||||
if (DeviceManager.deviceData.deviceSettings!!.androidAutoBrowseSeriesSequenceOrder === AndroidAutoBrowseSeriesSequenceOrderSetting.DESC) {
|
||||
items = libraryItems.reversed()
|
||||
}
|
||||
val children = items.map { libraryItem ->
|
||||
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
|
||||
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
|
||||
libraryItem.localLibraryItemId = localLibraryItem?.id
|
||||
val description = libraryItem.getMediaDescription(progress, ctx, null, true)
|
||||
if (libraryItem.collapsedSeries != null) {
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
} else {
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||
}
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
} else if (mediaIdParts[3] == "COLLECTIONS") {
|
||||
Log.d(tag, "Loading collections from library ${mediaIdParts[2]}")
|
||||
mediaManager.loadLibraryCollectionsWithAudio(mediaIdParts[2]) { collectionItems ->
|
||||
Log.d(tag, "Received ${collectionItems.size} collections")
|
||||
val children = collectionItems.map { collectionItem ->
|
||||
val description = collectionItem.getMediaDescription(null, ctx)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
} else if (mediaIdParts[3] == "COLLECTION") {
|
||||
Log.d(tag, "Loading collection ${mediaIdParts[4]} books from library ${mediaIdParts[2]}")
|
||||
mediaManager.loadLibraryCollectionBooksWithAudio(mediaIdParts[2], mediaIdParts[4]) { libraryItems ->
|
||||
Log.d(tag, "Received ${libraryItems.size} collections")
|
||||
val children = libraryItems.map { libraryItem ->
|
||||
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
|
||||
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
|
||||
libraryItem.localLibraryItemId = localLibraryItem?.id
|
||||
val description = libraryItem.getMediaDescription(progress, ctx)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
} else if (mediaIdParts[3] == "DISCOVERY") {
|
||||
Log.d(tag, "Loading discovery from library ${mediaIdParts[2]}")
|
||||
mediaManager.loadLibraryDiscoveryBooksWithAudio(mediaIdParts[2]) { libraryItems ->
|
||||
Log.d(tag, "Received ${libraryItems.size} libraryItems for discovery")
|
||||
val children = libraryItems.map { libraryItem ->
|
||||
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
|
||||
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
|
||||
libraryItem.localLibraryItemId = localLibraryItem?.id
|
||||
val description = libraryItem.getMediaDescription(progress, ctx)
|
||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
}
|
||||
} else {
|
||||
result.sendResult(null)
|
||||
}
|
||||
} else {
|
||||
Log.d(tag, "Loading podcast episodes for podcast $parentMediaId")
|
||||
|
|
@ -1119,13 +1579,35 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
|
||||
override fun onSearch(query: String, extras: Bundle?, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||
result.detach()
|
||||
mediaManager.loadAndroidAutoItems {
|
||||
browseTree = BrowseTree(this, mediaManager.serverItemsInProgress, mediaManager.serverLibraries)
|
||||
val children = browseTree[LIBRARIES_ROOT]?.map { item ->
|
||||
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||
if (cachedSearch != query) {
|
||||
Log.d(tag, "Search bundle: $extras")
|
||||
var foundBooks: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||
var foundPodcasts: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||
var foundSeries: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||
var foundAuthors: MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||
|
||||
mediaManager.serverLibraries.forEach { serverLibrary ->
|
||||
runBlocking {
|
||||
// Skip searching library if it doesn't have any audio files
|
||||
if (serverLibrary.stats?.numAudioFiles == 0) return@runBlocking
|
||||
val searchResult = mediaManager.doSearch(serverLibrary.id, query)
|
||||
for (resultData in searchResult.entries.iterator()) {
|
||||
when (resultData.key) {
|
||||
"book" -> foundBooks.addAll(resultData.value)
|
||||
"series" -> foundSeries.addAll(resultData.value)
|
||||
"authors" -> foundAuthors.addAll(resultData.value)
|
||||
"podcast" -> foundPodcasts.addAll(resultData.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||
foundBooks.addAll(foundSeries)
|
||||
foundBooks.addAll(foundAuthors)
|
||||
cachedSearchResults = foundBooks
|
||||
}
|
||||
result.sendResult(cachedSearchResults)
|
||||
cachedSearch = query
|
||||
Log.d(tag, "onSearch: Done")
|
||||
}
|
||||
|
||||
//
|
||||
|
|
@ -1188,6 +1670,14 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||
hasNetworkConnectivity = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
Log.i(tag, "Network capabilities changed. hasNetworkConnectivity=$hasNetworkConnectivity | isUnmeteredNetwork=$isUnmeteredNetwork")
|
||||
clientEventEmitter?.onNetworkMeteredChanged(isUnmeteredNetwork)
|
||||
if (hasNetworkConnectivity) {
|
||||
// Force android auto loading if libraries are empty.
|
||||
// Lack of network connectivity is most likely reason for libraries being empty
|
||||
if (isBrowseTreeInitialized() && firstLoadDone && mediaManager.serverLibraries.isEmpty()) {
|
||||
forceReloadingAndroidAuto = true
|
||||
notifyChildrenChanged("/")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
|||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.provider.Settings
|
||||
import android.util.Base64
|
||||
import android.util.Log
|
||||
import com.audiobookshelf.app.data.*
|
||||
import com.audiobookshelf.app.device.DeviceManager
|
||||
|
|
@ -154,7 +155,7 @@ class ApiHandler(var ctx:Context) {
|
|||
|
||||
fun getLibraries(cb: (List<Library>) -> Unit) {
|
||||
val mapper = jacksonMapper
|
||||
getRequest("/api/libraries", null,null) {
|
||||
getRequest("/api/libraries?include=stats", null,null) {
|
||||
val libraries = mutableListOf<Library>()
|
||||
|
||||
var array = JSONArray()
|
||||
|
|
@ -171,6 +172,23 @@ class ApiHandler(var ctx:Context) {
|
|||
}
|
||||
}
|
||||
|
||||
fun getLibraryPersonalized(libraryItemId:String, cb: (List<LibraryShelfType>?) -> Unit) {
|
||||
getRequest("/api/libraries/$libraryItemId/personalized", null, null) {
|
||||
if (it.has("error")) {
|
||||
Log.e(tag, it.getString("error") ?: "getLibraryStats Failed")
|
||||
cb(null)
|
||||
} else {
|
||||
val items = mutableListOf<LibraryShelfType>()
|
||||
val array = it.getJSONArray("value")
|
||||
for (i in 0 until array.length()) {
|
||||
val item = jacksonMapper.readValue<LibraryShelfType>(array.get(i).toString())
|
||||
items.add(item)
|
||||
}
|
||||
cb(items)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibraryItem(libraryItemId:String, cb: (LibraryItem?) -> Unit) {
|
||||
getRequest("/api/items/$libraryItemId?expanded=1", null, null) {
|
||||
if (it.has("error")) {
|
||||
|
|
@ -211,6 +229,103 @@ class ApiHandler(var ctx:Context) {
|
|||
}
|
||||
}
|
||||
|
||||
fun getLibrarySeries(libraryId:String, cb: (List<LibrarySeriesItem>) -> Unit) {
|
||||
Log.d(tag, "Getting series")
|
||||
getRequest("/api/libraries/$libraryId/series?minified=1&sort=name&limit=10000", null, null) {
|
||||
val items = mutableListOf<LibrarySeriesItem>()
|
||||
if (it.has("results")) {
|
||||
val array = it.getJSONArray("results")
|
||||
for (i in 0 until array.length()) {
|
||||
val item = jacksonMapper.readValue<LibrarySeriesItem>(array.get(i).toString())
|
||||
items.add(item)
|
||||
}
|
||||
}
|
||||
cb(items)
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibrarySeriesItems(libraryId:String, seriesId:String, cb: (List<LibraryItem>) -> Unit) {
|
||||
Log.d(tag, "Getting items for series")
|
||||
val seriesIdBase64 = Base64.encodeToString(seriesId.toByteArray(), Base64.DEFAULT)
|
||||
getRequest("/api/libraries/$libraryId/items?minified=1&sort=media.metadata.title&filter=series.${seriesIdBase64}&limit=1000", null, null) {
|
||||
val items = mutableListOf<LibraryItem>()
|
||||
if (it.has("results")) {
|
||||
val array = it.getJSONArray("results")
|
||||
for (i in 0 until array.length()) {
|
||||
val item = jacksonMapper.readValue<LibraryItem>(array.get(i).toString())
|
||||
items.add(item)
|
||||
}
|
||||
}
|
||||
cb(items)
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibraryAuthors(libraryId:String, cb: (List<LibraryAuthorItem>) -> Unit) {
|
||||
Log.d(tag, "Getting series")
|
||||
getRequest("/api/libraries/$libraryId/authors", null, null) {
|
||||
val items = mutableListOf<LibraryAuthorItem>()
|
||||
if (it.has("authors")) {
|
||||
val array = it.getJSONArray("authors")
|
||||
for (i in 0 until array.length()) {
|
||||
val item = jacksonMapper.readValue<LibraryAuthorItem>(array.get(i).toString())
|
||||
items.add(item)
|
||||
}
|
||||
}else{
|
||||
Log.e(tag, "No results")
|
||||
}
|
||||
cb(items)
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibraryItemsFromAuthor(libraryId:String, authorId:String, cb: (List<LibraryItem>) -> Unit) {
|
||||
Log.d(tag, "Getting author items")
|
||||
val authorIdBase64 = Base64.encodeToString(authorId.toByteArray(), Base64.DEFAULT)
|
||||
getRequest("/api/libraries/$libraryId/items?limit=1000&minified=1&filter=authors.${authorIdBase64}&sort=media.metadata.title&collapseseries=1", null, null) {
|
||||
val items = mutableListOf<LibraryItem>()
|
||||
if (it.has("results")) {
|
||||
val array = it.getJSONArray("results")
|
||||
for (i in 0 until array.length()) {
|
||||
val item = jacksonMapper.readValue<LibraryItem>(array.get(i).toString())
|
||||
if (item.collapsedSeries != null) {
|
||||
item.collapsedSeries?.libraryId = libraryId
|
||||
}
|
||||
items.add(item)
|
||||
}
|
||||
}else{
|
||||
Log.e(tag, "No results")
|
||||
}
|
||||
cb(items)
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibraryCollections(libraryId:String, cb: (List<LibraryCollection>) -> Unit) {
|
||||
Log.d(tag, "Getting collections")
|
||||
getRequest("/api/libraries/$libraryId/collections?minified=1&sort=name&limit=1000", null, null) {
|
||||
val items = mutableListOf<LibraryCollection>()
|
||||
if (it.has("results")) {
|
||||
val array = it.getJSONArray("results")
|
||||
for (i in 0 until array.length()) {
|
||||
val item = jacksonMapper.readValue<LibraryCollection>(array.get(i).toString())
|
||||
items.add(item)
|
||||
}
|
||||
}
|
||||
cb(items)
|
||||
}
|
||||
}
|
||||
|
||||
fun getSearchResults(libraryId:String, queryString:String, cb: (LibraryItemSearchResultType?) -> Unit) {
|
||||
Log.d(tag, "Doing search for library $libraryId")
|
||||
getRequest("/api/libraries/$libraryId/search?q=$queryString", null, null) {
|
||||
if (it.has("error")) {
|
||||
Log.e(tag, it.getString("error") ?: "getSearchResults Failed")
|
||||
cb(null)
|
||||
} else {
|
||||
val librarySearchResults = jacksonMapper.readValue<LibraryItemSearchResultType>(it.toString())
|
||||
cb(librarySearchResults)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getAllItemsInProgress(cb: (List<ItemInProgress>) -> Unit) {
|
||||
getRequest("/api/me/items-in-progress", null, null) {
|
||||
val items = mutableListOf<ItemInProgress>()
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
<!-- drawable/clock_outline.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z" /></vector>
|
||||
BIN
android/app/src/main/res/drawable-hdpi/md_account_outline.png
Normal file
|
After Width: | Height: | Size: 560 B |
BIN
android/app/src/main/res/drawable-hdpi/md_clock_outline.png
Normal file
|
After Width: | Height: | Size: 798 B |
BIN
android/app/src/main/res/drawable-mdpi/md_account_outline.png
Normal file
|
After Width: | Height: | Size: 387 B |
BIN
android/app/src/main/res/drawable-mdpi/md_clock_outline.png
Normal file
|
After Width: | Height: | Size: 532 B |
BIN
android/app/src/main/res/drawable-xhdpi/md_account_outline.png
Normal file
|
After Width: | Height: | Size: 707 B |
BIN
android/app/src/main/res/drawable-xhdpi/md_clock_outline.png
Normal file
|
After Width: | Height: | Size: 1 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/md_account_outline.png
Normal file
|
After Width: | Height: | Size: 1 KiB |
BIN
android/app/src/main/res/drawable-xxhdpi/md_clock_outline.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
9
android/app/src/main/res/drawable/abs_audiobookshelf.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="27dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="27"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M26.719,15.761c-0.165,-0.138 -0.422,-0.341 -0.77,-0.581v-2.703c0,-6.891 -5.586,-12.477 -12.478,-12.477v0c-6.891,0 -12.477,5.586 -12.477,12.477v2.703c-0.348,0.241 -0.605,0.443 -0.77,0.581 -0.137,0.114 -0.223,0.285 -0.223,0.476 0,0 0,0 0,0.001v-0,3.238c0,0 0,0.001 0,0.001 0,0.191 0.086,0.361 0.222,0.475l0.001,0.001c0.385,0.321 1.269,0.993 2.645,1.683v0.315c0,0.849 0.548,1.537 1.223,1.537v0c0.675,0 1.223,-0.688 1.223,-1.537v-7.767c0,-0.849 -0.548,-1.537 -1.223,-1.537v0c-0.647,0 -1.177,0.632 -1.22,1.431l-0.003,0.002v-1.601c0,-5.856 4.747,-10.602 10.603,-10.602v0c5.856,0 10.603,4.747 10.603,10.602v1.601l-0.003,-0.002c-0.043,-0.799 -0.573,-1.431 -1.22,-1.431v0c-0.675,0 -1.223,0.688 -1.223,1.537v7.766c0,0.849 0.548,1.537 1.223,1.537v0c0.676,0 1.223,-0.688 1.223,-1.537v-0.315c1.376,-0.69 2.26,-1.362 2.645,-1.683 0.137,-0.114 0.223,-0.285 0.223,-0.476 0,-0 0,-0.001 0,-0.001v0,-3.237c0,-0 0,-0 0,-0 0,-0.191 -0.086,-0.361 -0.222,-0.475l-0.001,-0.001zM9.12,29.262c0.816,0 1.477,-0.661 1.477,-1.477v0,-16.543c0,-0 0,-0 0,-0 0,-0.816 -0.661,-1.477 -1.477,-1.477h-1.526c-0.816,0 -1.478,0.662 -1.478,1.478v0,16.543c0,0.816 0.661,1.477 1.477,1.477 0,0 0,0 0,0v0zM6.673,13.731h3.368v0.352h-3.368zM14.234,29.262c0.816,0 1.477,-0.661 1.477,-1.477v0,-16.543c0,-0 0,-0 0,-0 0,-0.816 -0.661,-1.477 -1.477,-1.477h-1.526c-0.816,0 -1.478,0.662 -1.478,1.478v0,16.543c0,0.816 0.661,1.477 1.477,1.477 0,0 0,0 0,0v0zM11.787,13.731h3.367v0.352h-3.367zM19.348,29.262c0.816,0 1.477,-0.661 1.477,-1.477v0,-16.543c0,-0 0,-0 0,-0 0,-0.816 -0.661,-1.477 -1.477,-1.477h-1.526c-0.816,0 -1.478,0.662 -1.478,1.478v0,16.543c0,0.816 0.661,1.477 1.477,1.477 0,0 0,0 0,0v0zM16.901,13.731h3.367v0.352h-3.367zM3.566,29.773h19.81c0.615,0 1.113,0.498 1.113,1.113v0c0,0.615 -0.498,1.113 -1.113,1.113h-19.81c-0.615,0 -1.113,-0.498 -1.113,-1.113v-0c0,-0.615 0.498,-1.113 1.113,-1.113z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/abs_authors.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M16,7.333c2.577,0 4.667,2.089 4.667,4.667v0c0,2.577 -2.089,4.667 -4.667,4.667v0c-2.577,0 -4.667,-2.089 -4.667,-4.667v0c0,-2.577 2.089,-4.667 4.667,-4.667v0zM6.667,10.667c0.747,0 1.44,0.2 2.04,0.56 -0.2,1.907 0.36,3.8 1.507,5.28 -0.667,1.28 -2,2.16 -3.547,2.16 -2.209,0 -4,-1.791 -4,-4v0c0,-2.209 1.791,-4 4,-4v0zM25.333,10.667c2.209,0 4,1.791 4,4v0c0,2.209 -1.791,4 -4,4v0c-1.547,0 -2.88,-0.88 -3.547,-2.16 1.147,-1.48 1.707,-3.373 1.507,-5.28 0.6,-0.36 1.293,-0.56 2.04,-0.56zM7.333,24.333c0,-2.76 3.88,-5 8.667,-5s8.667,2.24 8.667,5v2.333h-17.333v-2.333zM0,26.667v-2c0,-1.853 2.52,-3.413 5.933,-3.867 -0.787,0.907 -1.267,2.16 -1.267,3.533v2.333h-4.667zM32,26.667h-4.667v-2.333c0,-1.373 -0.48,-2.627 -1.267,-3.533 3.413,0.453 5.933,2.013 5.933,3.867v2z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/abs_book_1.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M28,4v26h-21c-1.657,0 -3,-1.343 -3,-3s1.343,-3 3,-3h19v-24h-20c-2.2,0 -4,1.8 -4,4v24c0,2.2 1.8,4 4,4h24v-28h-2zM7.002,26v0c-0.001,0 -0.001,0 -0.002,0 -0.552,0 -1,0.448 -1,1s0.448,1 1,1c0.001,0 0.001,-0 0.002,-0v0h18.997v-2h-18.997z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/abs_books_1.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M12,4v20h4v-20h-4zM16,6.667l5.333,17.333 4,-1.333 -5.333,-17.333 -4,1.333zM6.667,6.667v17.333h4v-17.333h-4zM4,25.333v2.667h24v-2.667h-24z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/abs_books_2.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="36dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="36"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M7,4h-6c-0.55,0 -1,0.45 -1,1v22c0,0.55 0.45,1 1,1h6c0.55,0 1,-0.45 1,-1v-22c0,-0.55 -0.45,-1 -1,-1zM6,10h-4v-2h4v2zM17,4h-6c-0.55,0 -1,0.45 -1,1v22c0,0.55 0.45,1 1,1h6c0.55,0 1,-0.45 1,-1v-22c0,-0.55 -0.45,-1 -1,-1zM16,10h-4v-2h4v2zM23.909,5.546l-5.358,2.7c-0.491,0.247 -0.691,0.852 -0.443,1.343l8.999,17.861c0.247,0.491 0.852,0.691 1.343,0.443l5.358,-2.7c0.491,-0.247 0.691,-0.852 0.443,-1.343l-8.999,-17.861c-0.247,-0.491 -0.852,-0.691 -1.343,-0.443z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/abs_columns.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M19.2,6h-6.4v19.2h6.4v-19.2zM22.4,6v19.2h6.4v-19.2h-6.4zM9.6,6h-6.4v19.2h6.4v-19.2zM0,2.8h32v25.6h-32v-25.6z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/abs_database.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="27dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="27"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M13.68,0c7.518,0 13.612,2.854 13.612,6.37 0,3.518 -6.096,6.37 -13.612,6.37s-13.612,-2.854 -13.612,-6.37c0,-3.516 6.096,-6.37 13.612,-6.37v0zM0.068,21.31v4.891c2.422,8.602 26.349,6.94 27.227,-0.44v-4.885c-1.195,8.102 -25.313,8.685 -27.227,0.435v0,0zM0,8.578v4.776c2.422,8.401 26.482,7.266 27.362,0.06v-4.773c-1.198,7.914 -25.448,7.995 -27.362,-0.063v0zM0,14.75v4.891c2.422,8.602 26.482,7.44 27.362,0.06v-4.885c-1.198,8.102 -25.448,8.185 -27.362,-0.065v0z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/abs_file_picture.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M26,28h-20v-4l6,-10 8.219,10 5.781,-4v8zM26,15c0,1.657 -1.343,3 -3,3s-3,-1.343 -3,-3 1.343,-3 3,-3c1.657,0 3,1.343 3,3zM28.681,7.159c-0.694,-0.947 -1.662,-2.053 -2.724,-3.116s-2.169,-2.03 -3.116,-2.724c-1.612,-1.182 -2.393,-1.319 -2.841,-1.319h-15.5c-1.378,0 -2.5,1.121 -2.5,2.5v27c0,1.378 1.122,2.5 2.5,2.5h23c1.378,0 2.5,-1.122 2.5,-2.5v-19.5c0,-0.448 -0.137,-1.23 -1.319,-2.841zM24.543,5.457c0.959,0.959 1.712,1.825 2.268,2.543h-4.811v-4.811c0.718,0.556 1.584,1.309 2.543,2.268zM28,29.5c0,0.271 -0.229,0.5 -0.5,0.5h-23c-0.271,0 -0.5,-0.229 -0.5,-0.5v-27c0,-0.271 0.229,-0.5 0.5,-0.5 0,0 15.499,-0 15.5,0v7c0,0.552 0.448,1 1,1h7v19.5z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/abs_headphones.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M9,18h-2v14h2c0.55,0 1,-0.45 1,-1v-12c0,-0.55 -0.45,-1 -1,-1zM23,18c-0.55,0 -1,0.45 -1,1v12c0,0.55 0.45,1 1,1h2v-14h-2zM32,16c0,-8.837 -7.163,-16 -16,-16s-16,7.163 -16,16c0,1.919 0.338,3.759 0.958,5.464 -0.609,1.038 -0.958,2.246 -0.958,3.536 0,3.526 2.608,6.443 6,6.929v-13.857c-0.997,0.143 -1.927,0.495 -2.742,1.012 -0.168,-0.835 -0.258,-1.699 -0.258,-2.584 0,-7.18 5.82,-13 13,-13s13,5.82 13,13c0,0.885 -0.088,1.749 -0.257,2.584 -0.816,-0.517 -1.745,-0.87 -2.743,-1.013v13.858c3.392,-0.485 6,-3.402 6,-6.929 0,-1.29 -0.349,-2.498 -0.958,-3.536 0.62,-1.705 0.958,-3.545 0.958,-5.465z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/abs_heart.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M23.6,2c-3.363,0 -6.258,2.736 -7.599,5.594 -1.342,-2.858 -4.237,-5.594 -7.601,-5.594 -4.637,0 -8.4,3.764 -8.4,8.401 0,9.433 9.516,11.906 16.001,21.232 6.13,-9.268 15.999,-12.1 15.999,-21.232 0,-4.637 -3.763,-8.401 -8.4,-8.401z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/abs_microphone_1.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M6,6v10c0,3.313 2.688,6 6,6s6,-2.688 6,-6h-5c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h5v-2h-5c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h5v-2h-5c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h5c0,-3.313 -2.688,-6 -6,-6s-6,2.688 -6,6zM20,15v1c0,4.419 -3.581,8 -8,8s-8,-3.581 -8,-8v-2.5c0,-0.831 -0.669,-1.5 -1.5,-1.5s-1.5,0.669 -1.5,1.5v2.5c0,5.569 4.138,10.169 9.5,10.9v2.1h-3c-0.831,0 -1.5,0.669 -1.5,1.5s0.669,1.5 1.5,1.5h9c0.831,0 1.5,-0.669 1.5,-1.5s-0.669,-1.5 -1.5,-1.5h-3v-2.1c5.363,-0.731 9.5,-5.331 9.5,-10.9v-2.5c0,-0.831 -0.669,-1.5 -1.5,-1.5s-1.5,0.669 -1.5,1.5v1.5z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/abs_microphone_2.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="22dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="22"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M22.292,14.872c0,-0.836 -0.677,-1.51 -1.51,-1.51 -0.836,0 -1.51,0.677 -1.51,1.51 0,2.977 -0.714,5.435 -2.159,7.076 -1.31,1.484 -3.289,2.341 -5.964,2.341s-4.654,-0.854 -5.964,-2.339c-1.448,-1.641 -2.159,-4.099 -2.159,-7.078 0,-0.836 -0.677,-1.51 -1.51,-1.51 -0.836,0 -1.51,0.677 -1.51,1.51 0,3.711 0.961,6.857 2.914,9.073 1.438,1.63 3.375,2.734 5.815,3.164v2.479h-3.703c-0.661,0 -1.203,0.542 -1.203,1.203v1.206h14.646v-1.206c0,-0.661 -0.542,-1.203 -1.203,-1.203h-3.711v-2.479c2.44,-0.432 4.375,-1.534 5.815,-3.167 1.953,-2.214 2.917,-5.359 2.917,-9.07v0,0zM11.146,0c3.083,0 5.604,2.523 5.604,5.604v0.146h-3.013v3.57h3.016v1.818h-3.016v3.57h3.016v1.284c0,3.083 -2.523,5.604 -5.604,5.604 -3.083,0 -5.604,-2.523 -5.604,-5.604v-1.284h3.016v-3.57h-3.018v-1.818h3.016v-3.57h-3.016v-0.146c0,-3.081 2.521,-5.604 5.604,-5.604v0z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/abs_microphone_3.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M15,22c2.761,0 5,-2.239 5,-5v-12c0,-2.761 -2.239,-5 -5,-5s-5,2.239 -5,5v12c0,2.761 2.239,5 5,5zM22,14v3c0,3.866 -3.134,7 -7,7s-7,-3.134 -7,-7v-3h-2v3c0,4.632 3.5,8.447 8,8.944v4.056h-4v2h10v-2h-4v-4.056c4.5,-0.497 8,-4.312 8,-8.944v-3h-2z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/abs_music.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M30,0h2v23c0,2.761 -3.134,5 -7,5s-7,-2.239 -7,-5c0,-2.761 3.134,-5 7,-5 1.959,0 3.729,0.575 5,1.501v-11.501l-16,3.556v15.444c0,2.761 -3.134,5 -7,5s-7,-2.239 -7,-5c0,-2.761 3.134,-5 7,-5 1.959,0 3.729,0.575 5,1.501v-19.501l18,-4z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/abs_podcast.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="33dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="33"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M18.289,18.549v13.45h-3.973v-13.45c-1.305,-0.706 -2.191,-2.087 -2.191,-3.676 0,-2.307 1.871,-4.178 4.178,-4.178s4.178,1.871 4.178,4.178c0,1.589 -0.887,2.97 -2.192,3.676v0zM10.434,2.963c0.774,-0.382 1.091,-1.319 0.709,-2.091s-1.318,-1.091 -2.091,-0.71c-3.158,1.558 -5.613,4.134 -7.165,7.169 -1.146,2.241 -1.8,4.734 -1.879,7.251 -0.079,2.538 0.421,5.101 1.585,7.46 1.433,2.906 3.863,5.491 7.44,7.322 0.767,0.392 1.706,0.088 2.098,-0.679s0.088,-1.706 -0.679,-2.097c-2.929,-1.498 -4.905,-3.589 -6.059,-5.926 -0.93,-1.887 -1.33,-3.942 -1.267,-5.981 0.064,-2.058 0.599,-4.098 1.536,-5.93 1.256,-2.455 3.234,-4.536 5.771,-5.786v0zM23.554,0.161c-0.773,-0.382 -1.71,-0.064 -2.091,0.71s-0.064,1.71 0.71,2.091c2.537,1.251 4.515,3.332 5.771,5.787 0.937,1.832 1.472,3.871 1.536,5.93 0.064,2.038 -0.336,4.094 -1.267,5.981 -1.153,2.337 -3.13,4.428 -6.059,5.926 -0.767,0.392 -1.07,1.331 -0.679,2.097s1.331,1.071 2.098,0.679c3.577,-1.83 6.007,-4.415 7.44,-7.322 1.164,-2.359 1.664,-4.922 1.585,-7.46 -0.079,-2.516 -0.732,-5.01 -1.878,-7.251 -1.552,-3.034 -4.008,-5.611 -7.165,-7.169v0zM21.968,7.033c-0.582,-0.42 -1.395,-0.287 -1.814,0.295s-0.287,1.395 0.295,1.814c0.235,0.169 0.465,0.359 0.691,0.566 1.319,1.216 2.101,2.838 2.266,4.553 0.166,1.731 -0.295,3.566 -1.464,5.188 -0.198,0.276 -0.427,0.555 -0.686,0.836 -0.487,0.529 -0.453,1.353 0.076,1.839s1.353,0.452 1.84,-0.076c0.313,-0.339 0.607,-0.701 0.88,-1.081 1.555,-2.16 2.167,-4.617 1.943,-6.951 -0.225,-2.349 -1.293,-4.566 -3.091,-6.224 -0.283,-0.261 -0.595,-0.514 -0.935,-0.76v0zM12.156,9.143c0.582,-0.42 0.715,-1.232 0.295,-1.814s-1.232,-0.715 -1.814,-0.296c-0.341,0.246 -0.652,0.499 -0.935,0.76 -1.799,1.658 -2.866,3.875 -3.091,6.224 -0.224,2.334 0.387,4.792 1.943,6.952 0.274,0.379 0.567,0.741 0.88,1.08 0.487,0.529 1.311,0.563 1.839,0.076s0.563,-1.311 0.076,-1.839c-0.259,-0.281 -0.487,-0.56 -0.686,-0.836 -1.169,-1.623 -1.63,-3.457 -1.464,-5.188 0.164,-1.715 0.947,-3.337 2.266,-4.553 0.226,-0.207 0.456,-0.397 0.691,-0.566v0z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/abs_radio.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M30.925,2.938c0.794,-0.231 1.25,-1.069 1.019,-1.863s-1.069,-1.25 -1.869,-1.012l-26.844,7.869c-0.587,0.169 -1.119,0.456 -1.569,0.825 -1.006,0.725 -1.663,1.906 -1.663,3.244v16c0,2.206 1.794,4 4,4h24c2.206,0 4,-1.794 4,-4v-16c0,-2.206 -1.794,-4 -4,-4h-14.344l17.269,-5.063zM23,25c-2.762,0 -5,-2.238 -5,-5s2.238,-5 5,-5 5,2.238 5,5 -2.238,5 -5,5zM5,16c0,-0.55 0.45,-1 1,-1h6c0.55,0 1,0.45 1,1s-0.45,1 -1,1h-6c-0.55,0 -1,-0.45 -1,-1zM4,20c0,-0.55 0.45,-1 1,-1h8c0.55,0 1,0.45 1,1s-0.45,1 -1,1h-8c-0.55,0 -1,-0.45 -1,-1zM5,24c0,-0.55 0.45,-1 1,-1h6c0.55,0 1,0.45 1,1s-0.45,1 -1,1h-6c-0.55,0 -1,-0.45 -1,-1z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/abs_rocket.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M22,2l-10,10h-6l-6,8c0,0 6.357,-1.77 10.065,-0.94l-10.065,12.94 13.184,-10.255c1.839,4.208 -1.184,10.255 -1.184,10.255l8,-6v-6l10,-10 2,-10 -10,2z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/abs_rss.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M4.259,23.467c-2.35,0 -4.259,1.917 -4.259,4.252 0,2.349 1.909,4.244 4.259,4.244 2.358,0 4.265,-1.895 4.265,-4.244 -0,-2.336 -1.907,-4.252 -4.265,-4.252zM0.005,10.873v6.133c3.993,0 7.749,1.562 10.577,4.391 2.825,2.822 4.384,6.595 4.384,10.603h6.16c-0,-11.651 -9.478,-21.127 -21.121,-21.127zM0.012,0v6.136c14.243,0 25.836,11.604 25.836,25.864h6.152c0,-17.64 -14.352,-32 -31.988,-32z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
9
android/app/src/main/res/drawable/abs_star.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M32,12.408l-11.056,-1.607 -4.944,-10.018 -4.944,10.018 -11.056,1.607 8,7.798 -1.889,11.011 9.889,-5.199 9.889,5.199 -1.889,-11.011 8,-7.798z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
1
android/app/src/main/res/drawable/md_account_outline.xml
Normal file
|
|
@ -0,0 +1 @@
|
|||
<!-- drawable/account_outline.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" /></vector>
|
||||
|
|
@ -0,0 +1 @@
|
|||
<!-- drawable/book_multiple_outline.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M19 2A2 2 0 0 1 21 4V16A2 2 0 0 1 19 18H9A2 2 0 0 1 7 16V4A2 2 0 0 1 9 2H19M19 4H16V10L13.5 7.75L11 10V4H9V16H19M3 20A2 2 0 0 0 5 22H17V20H5V6H3Z" /></vector>
|
||||
|
|
@ -0,0 +1 @@
|
|||
<!-- drawable/book_open_blank_variant_outline.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M12 21.5C10.65 20.65 8.2 20 6.5 20C4.85 20 3.15 20.3 1.75 21.05C1.65 21.1 1.6 21.1 1.5 21.1C1.25 21.1 1 20.85 1 20.6V6C1.6 5.55 2.25 5.25 3 5C4.11 4.65 5.33 4.5 6.5 4.5C8.45 4.5 10.55 4.9 12 6C13.45 4.9 15.55 4.5 17.5 4.5C18.67 4.5 19.89 4.65 21 5C21.75 5.25 22.4 5.55 23 6V20.6C23 20.85 22.75 21.1 22.5 21.1C22.4 21.1 22.35 21.1 22.25 21.05C20.85 20.3 19.15 20 17.5 20C15.8 20 13.35 20.65 12 21.5M11 7.5C9.64 6.9 7.84 6.5 6.5 6.5C5.3 6.5 4.1 6.65 3 7V18.5C4.1 18.15 5.3 18 6.5 18C7.84 18 9.64 18.4 11 19V7.5M13 19C14.36 18.4 16.16 18 17.5 18C18.7 18 19.9 18.15 21 18.5V7C19.9 6.65 18.7 6.5 17.5 6.5C16.16 6.5 14.36 6.9 13 7.5V19Z" /></vector>
|
||||
1
android/app/src/main/res/drawable/md_clock_outline.xml
Normal file
|
|
@ -0,0 +1 @@
|
|||
<!-- drawable/clock_outline.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z" /></vector>
|
||||
1
android/app/src/main/res/drawable/md_telescope.xml
Normal file
|
|
@ -0,0 +1 @@
|
|||
<!-- drawable/telescope.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M21.9,8.9L20.2,9.9L16.2,3L17.9,2L21.9,8.9M9.8,7.9L12.8,13.1L18.9,9.6L15.9,4.4L9.8,7.9M11.4,12.7L9.4,9.2L5.1,11.7L7.1,15.2L11.4,12.7M2.1,14.6L3.1,16.3L5.7,14.8L4.7,13.1L2.1,14.6M12.1,14L11.8,13.6L7.5,16.1L7.8,16.5C8,16.8 8.3,17.1 8.6,17.3L7,22H9L10.4,17.7H10.5L12,22H14L12.1,16.4C12.6,15.7 12.6,14.8 12.1,14Z" /></vector>
|
||||
|
|
@ -150,6 +150,22 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Android Auto settings -->
|
||||
<template v-if="!isiOS">
|
||||
<p class="uppercase text-xs font-semibold text-fg-muted mb-2 mt-10">{{ $strings.HeaderAndroidAutoSettings }}</p>
|
||||
<div class="py-3 flex items-center">
|
||||
<p class="pr-4 w-36">{{ $strings.LabelAndroidAutoBrowseLimitForGrouping }}</p>
|
||||
<ui-text-input type="number" v-model="settings.androidAutoBrowseLimitForGrouping" style="width: 145px; max-width: 145px" @input="androidAutoBrowseLimitForGroupingUpdated" />
|
||||
<span class="material-icons-outlined ml-2" @click.stop="showInfo('androidAutoBrowseLimitForGrouping')">info</span>
|
||||
</div>
|
||||
<div class="py-3 flex items-center">
|
||||
<p class="pr-4 w-36">{{ $strings.LabelAndroidAutoBrowseSeriesSequenceOrder }}</p>
|
||||
<div @click.stop="showAndroidAutoBrowseSeriesSequenceOrderOptions">
|
||||
<ui-text-input :value="androidAutoBrowseSeriesSequenceOrderOption" readonly append-icon="expand_more" style="max-width: 200px" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-show="loading" class="w-full h-full absolute top-0 left-0 flex items-center justify-center z-10">
|
||||
<ui-loading-indicator />
|
||||
</div>
|
||||
|
|
@ -193,7 +209,9 @@ export default {
|
|||
autoSleepTimerAutoRewindTime: 300000, // 5 minutes
|
||||
languageCode: 'en-us',
|
||||
downloadUsingCellular: 'ALWAYS',
|
||||
streamingUsingCellular: 'ALWAYS'
|
||||
streamingUsingCellular: 'ALWAYS',
|
||||
androidAutoBrowseLimitForGrouping: 100,
|
||||
androidAutoBrowseSeriesSequenceOrder: 'ASC'
|
||||
},
|
||||
theme: 'dark',
|
||||
lockCurrentOrientation: false,
|
||||
|
|
@ -221,6 +239,10 @@ export default {
|
|||
enableMp3IndexSeeking: {
|
||||
name: this.$strings.LabelEnableMp3IndexSeeking,
|
||||
message: this.$strings.LabelEnableMp3IndexSeekingHelp
|
||||
},
|
||||
androidAutoBrowseLimitForGrouping: {
|
||||
name: this.$strings.LabelAndroidAutoBrowseLimitForGrouping,
|
||||
message: this.$strings.LabelAndroidAutoBrowseLimitForGroupingHelp
|
||||
}
|
||||
},
|
||||
hapticFeedbackItems: [
|
||||
|
|
@ -290,6 +312,16 @@ export default {
|
|||
text: this.$strings.LabelNever,
|
||||
value: 'NEVER'
|
||||
}
|
||||
],
|
||||
androidAutoBrowseSeriesSequenceOrderItems: [
|
||||
{
|
||||
text: this.$strings.LabelSequenceAscending,
|
||||
value: 'ASC'
|
||||
},
|
||||
{
|
||||
text: this.$strings.LabelSequenceDescending,
|
||||
value: 'DESC'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
@ -372,6 +404,10 @@ export default {
|
|||
const item = this.streamingUsingCellularItems.find((i) => i.value === this.settings.streamingUsingCellular)
|
||||
return item?.text || 'Error'
|
||||
},
|
||||
androidAutoBrowseSeriesSequenceOrderOption() {
|
||||
const item = this.androidAutoBrowseSeriesSequenceOrderItems.find((i) => i.value === this.settings.androidAutoBrowseSeriesSequenceOrder)
|
||||
return item?.text || 'Error'
|
||||
},
|
||||
moreMenuItems() {
|
||||
if (this.moreMenuSetting === 'shakeSensitivity') return this.shakeSensitivityItems
|
||||
else if (this.moreMenuSetting === 'hapticFeedback') return this.hapticFeedbackItems
|
||||
|
|
@ -379,6 +415,7 @@ export default {
|
|||
else if (this.moreMenuSetting === 'theme') return this.themeOptionItems
|
||||
else if (this.moreMenuSetting === 'downloadUsingCellular') return this.downloadUsingCellularItems
|
||||
else if (this.moreMenuSetting === 'streamingUsingCellular') return this.streamingUsingCellularItems
|
||||
else if (this.moreMenuSetting === 'androidAutoBrowseSeriesSequenceOrder') return this.androidAutoBrowseSeriesSequenceOrderItems
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
|
@ -421,6 +458,10 @@ export default {
|
|||
this.moreMenuSetting = 'streamingUsingCellular'
|
||||
this.showMoreMenuDialog = true
|
||||
},
|
||||
showAndroidAutoBrowseSeriesSequenceOrderOptions() {
|
||||
this.moreMenuSetting = 'androidAutoBrowseSeriesSequenceOrder'
|
||||
this.showMoreMenuDialog = true
|
||||
},
|
||||
clickMenuAction(action) {
|
||||
this.showMoreMenuDialog = false
|
||||
if (this.moreMenuSetting === 'shakeSensitivity') {
|
||||
|
|
@ -441,6 +482,9 @@ export default {
|
|||
} else if (this.moreMenuSetting === 'streamingUsingCellular') {
|
||||
this.settings.streamingUsingCellular = action
|
||||
this.saveSettings()
|
||||
} else if (this.moreMenuSetting === 'androidAutoBrowseSeriesSequenceOrder') {
|
||||
this.settings.androidAutoBrowseSeriesSequenceOrder = action
|
||||
this.saveSettings()
|
||||
}
|
||||
},
|
||||
saveTheme(theme) {
|
||||
|
|
@ -451,6 +495,12 @@ export default {
|
|||
if (!val) return // invalid times return falsy
|
||||
this.saveSettings()
|
||||
},
|
||||
androidAutoBrowseLimitForGroupingUpdated(val) {
|
||||
if (!val) return // invalid times return falsy
|
||||
if (val > 1000) val = 1000
|
||||
if (val < 30) val = 30
|
||||
this.saveSettings()
|
||||
},
|
||||
hapticFeedbackUpdated(val) {
|
||||
this.$store.commit('globals/setHapticFeedback', val)
|
||||
this.saveSettings()
|
||||
|
|
@ -576,6 +626,9 @@ export default {
|
|||
|
||||
this.settings.downloadUsingCellular = deviceSettings.downloadUsingCellular || 'ALWAYS'
|
||||
this.settings.streamingUsingCellular = deviceSettings.streamingUsingCellular || 'ALWAYS'
|
||||
|
||||
this.settings.androidAutoBrowseLimitForGrouping = deviceSettings.androidAutoBrowseLimitForGrouping
|
||||
this.settings.androidAutoBrowseSeriesSequenceOrder = deviceSettings.androidAutoBrowseSeriesSequenceOrder || 'ASC'
|
||||
},
|
||||
async init() {
|
||||
this.loading = true
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@
|
|||
"ButtonYes": "Yes",
|
||||
"HeaderAccount": "Account",
|
||||
"HeaderAdvanced": "Advanced",
|
||||
"HeaderAndroidAutoSettings": "Android Auto Settings",
|
||||
"HeaderAudioTracks": "Audio Tracks",
|
||||
"HeaderChapters": "Chapters",
|
||||
"HeaderCollection": "Collection",
|
||||
|
|
@ -92,6 +93,9 @@
|
|||
"LabelAll": "All",
|
||||
"LabelAllowSeekingOnMediaControls": "Allow position seeking on media notification controls",
|
||||
"LabelAlways": "Always",
|
||||
"LabelAndroidAutoBrowseLimitForGrouping": "Alphabetical drawdown limit",
|
||||
"LabelAndroidAutoBrowseLimitForGroupingHelp": "Don't use alphabetical drawdown when there is less than this amount of items to show",
|
||||
"LabelAndroidAutoBrowseSeriesSequenceOrder": "Series books order",
|
||||
"LabelAskConfirmation": "Ask for confirmation",
|
||||
"LabelAuthor": "Author",
|
||||
"LabelAuthorFirstLast": "Author (First Last)",
|
||||
|
|
@ -218,6 +222,8 @@
|
|||
"LabelScaleElapsedTimeBySpeed": "Scale Elapsed Time by Speed",
|
||||
"LabelSeason": "Season",
|
||||
"LabelSelectADevice": "Select a device",
|
||||
"LabelSequenceAscending": "Sequence Ascending",
|
||||
"LabelSequenceDescending": "Sequence Descending",
|
||||
"LabelSeries": "Series",
|
||||
"LabelServerAddress": "Server address",
|
||||
"LabelSetEbookAsPrimary": "Set as primary",
|
||||
|
|
|
|||