mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-06 20:08:48 +02:00
Update:Android Auto play next podcast episode after finishing one
This commit is contained in:
@@ -5,6 +5,7 @@ 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.*
|
||||
|
||||
// This auto-detects whether it is a Book or Podcast
|
||||
@@ -51,7 +52,7 @@ class Podcast(
|
||||
// Add new episodes
|
||||
audioTracks.forEach { at ->
|
||||
if (episodes?.find{ it.audioTrack?.localFileId == at.localFileId } == null) {
|
||||
val newEpisode = PodcastEpisode("local_ep_" + at.localFileId,episodes?.size ?: 0 + 1,null,null,at.title,null,null,null,at,at.duration,0, null)
|
||||
val newEpisode = PodcastEpisode("local_ep_" + at.localFileId,(episodes?.size ?: 0) + 1,null,null,at.title,null,null,null, null, null, at,at.duration,0, null)
|
||||
episodes?.add(newEpisode)
|
||||
}
|
||||
}
|
||||
@@ -64,7 +65,7 @@ class Podcast(
|
||||
}
|
||||
@JsonIgnore
|
||||
override fun addAudioTrack(audioTrack:AudioTrack) {
|
||||
val newEpisode = PodcastEpisode("local_" + audioTrack.localFileId,(episodes?.size ?: 0) + 1,null,null,audioTrack.title,null,null,null,audioTrack,audioTrack.duration,0, null)
|
||||
val newEpisode = PodcastEpisode("local_ep_" + audioTrack.localFileId,(episodes?.size ?: 0) + 1,null,null,audioTrack.title,null,null,null, null, null,audioTrack,audioTrack.duration,0, null)
|
||||
episodes?.add(newEpisode)
|
||||
|
||||
var index = 1
|
||||
@@ -83,9 +84,16 @@ class Podcast(
|
||||
index++
|
||||
}
|
||||
}
|
||||
|
||||
// Used for FolderScanner local podcast item to get copy of Podcast excluding episodes
|
||||
@JsonIgnore
|
||||
override fun getLocalCopy(): Podcast {
|
||||
return Podcast(metadata as PodcastMetadata,coverPath,tags, mutableListOf(),autoDownloadEpisodes, 0)
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode):PodcastEpisode {
|
||||
val newEpisode = PodcastEpisode("local_" + episode.id,(episodes?.size ?: 0) + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,audioTrack,audioTrack.duration,0, episode.id)
|
||||
val newEpisode = PodcastEpisode("local_ep_" + episode.id,(episodes?.size ?: 0) + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,null,null,audioTrack,audioTrack.duration,0, episode.id)
|
||||
episodes?.add(newEpisode)
|
||||
|
||||
var index = 1
|
||||
@@ -96,10 +104,14 @@ class Podcast(
|
||||
return newEpisode
|
||||
}
|
||||
|
||||
// Used for FolderScanner local podcast item to get copy of Podcast excluding episodes
|
||||
@JsonIgnore
|
||||
override fun getLocalCopy(): Podcast {
|
||||
return Podcast(metadata as PodcastMetadata,coverPath,tags, mutableListOf(),autoDownloadEpisodes, 0)
|
||||
fun getNextUnfinishedEpisode(libraryItemId:String, mediaManager: MediaManager):PodcastEpisode? {
|
||||
val sortedEpisodes = episodes?.sortedByDescending { it.publishedAt }
|
||||
val podcastEpisode = sortedEpisodes?.find { episode ->
|
||||
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItemId && it.episodeId == episode.id }
|
||||
progress == null || !progress.isFinished
|
||||
}
|
||||
return podcastEpisode
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,6 +243,8 @@ data class PodcastEpisode(
|
||||
var title:String?,
|
||||
var subtitle:String?,
|
||||
var description:String?,
|
||||
var pubDate:String?,
|
||||
var publishedAt:Long?,
|
||||
var audioFile:AudioFile?,
|
||||
var audioTrack:AudioTrack?,
|
||||
var duration:Double?,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.audiobookshelf.app.data
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.ImageDecoder
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
@@ -58,6 +57,8 @@ class PlaybackSession(
|
||||
@get:JsonIgnore
|
||||
val isLocal get() = playMethod == PLAYMETHOD_LOCAL
|
||||
@get:JsonIgnore
|
||||
val isPodcastEpisode get() = mediaType == "podcast"
|
||||
@get:JsonIgnore
|
||||
val currentTimeMs get() = (currentTime * 1000L).toLong()
|
||||
@get:JsonIgnore
|
||||
val totalDurationMs get() = (getTotalDuration() * 1000L).toLong()
|
||||
@@ -136,6 +137,7 @@ class PlaybackSession(
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getCoverUri().toString())
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, getCoverUri().toString())
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, getCoverUri().toString())
|
||||
|
||||
// Local covers get bitmap
|
||||
if (localLibraryItem?.coverContentUrl != null) {
|
||||
@@ -196,7 +198,9 @@ class PlaybackSession(
|
||||
|
||||
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_TITLE, displayTitle ?: "")
|
||||
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_ARTIST, displayAuthor ?: "")
|
||||
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_ALBUM_TITLE, displayAuthor ?: "")
|
||||
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_CHAPTER_TITLE, audioTrack.title)
|
||||
|
||||
castMetadata.putInt(com.google.android.gms.cast.MediaMetadata.KEY_TRACK_NUMBER, audioTrack.index)
|
||||
return castMetadata
|
||||
}
|
||||
|
||||
@@ -266,6 +266,23 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
||||
|
||||
}
|
||||
|
||||
fun loadServerUserMediaProgress(cb: () -> Unit) {
|
||||
Log.d(tag, "Loading server media progress")
|
||||
if (DeviceManager.serverConnectionConfig == null) {
|
||||
return cb()
|
||||
}
|
||||
|
||||
DeviceManager.serverConnectionConfig?.let { config ->
|
||||
apiHandler.authorize(config) {
|
||||
Log.d(tag, "loadServerUserMediaProgress: Authorized server config ${config.address} result = $it")
|
||||
if (!it.isNullOrEmpty()) {
|
||||
serverUserMediaProgress = it
|
||||
}
|
||||
cb()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadAndroidAutoItems(cb: () -> Unit) {
|
||||
Log.d(tag, "Load android auto items")
|
||||
|
||||
|
||||
@@ -115,7 +115,19 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
||||
failedSyncs = 0
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
fun finished(cb: () -> Unit) {
|
||||
if (!listeningTimerRunning) return
|
||||
listeningTimerTask?.cancel()
|
||||
listeningTimerTask = null
|
||||
listeningTimerRunning = false
|
||||
Log.d(tag, "stop: Stopping listening for $currentDisplayTitle")
|
||||
|
||||
sync(true, currentPlaybackSession?.duration ?: 0.0) {
|
||||
reset()
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
fun syncFromServerProgress(mediaProgress: MediaProgress) {
|
||||
|
||||
@@ -52,6 +52,8 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
||||
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_ENDED) {
|
||||
Log.d(tag, "STATE_ENDED")
|
||||
playerNotificationService.sendClientMetadata(PlayerState.ENDED)
|
||||
|
||||
playerNotificationService.handlePlaybackEnded()
|
||||
}
|
||||
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_IDLE) {
|
||||
Log.d(tag, "STATE_IDLE")
|
||||
|
||||
+39
-1
@@ -367,6 +367,14 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: When an item isFinished the currentTime should be reset to 0
|
||||
// will reset the time if currentTime is within 5s of duration (for android auto)
|
||||
Log.d(tag, "Prepare Player Session Current Time=${playbackSession.currentTime}, Duration=${playbackSession.duration}")
|
||||
if (playbackSession.duration - playbackSession.currentTime < 5) {
|
||||
Log.d(tag, "Prepare Player Session is finished, so restart it")
|
||||
playbackSession.currentTime = 0.0
|
||||
}
|
||||
|
||||
isClosed = false
|
||||
val customActionProviders = mutableListOf(
|
||||
JumpBackwardCustomActionProvider(),
|
||||
@@ -488,6 +496,36 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
}
|
||||
}
|
||||
|
||||
fun handlePlaybackEnded() {
|
||||
Log.d(tag, "handlePlaybackEnded")
|
||||
if (isAndroidAuto && currentPlaybackSession?.isPodcastEpisode == true) {
|
||||
Log.d(tag, "Podcast playback ended on android auto")
|
||||
val libraryItem = currentPlaybackSession?.libraryItem ?: return
|
||||
|
||||
// Need to sync with server to set as finished
|
||||
mediaProgressSyncer.finished {
|
||||
// Need to reload media progress
|
||||
mediaManager.loadServerUserMediaProgress {
|
||||
val podcast = libraryItem.media as Podcast
|
||||
val nextEpisode = podcast.getNextUnfinishedEpisode(libraryItem.id, mediaManager)
|
||||
Log.d(tag, "handlePlaybackEnded nextEpisode=$nextEpisode")
|
||||
nextEpisode?.let { podcastEpisode ->
|
||||
mediaManager.play(libraryItem, podcastEpisode, getPlayItemRequestPayload(false)) {
|
||||
if (it == null) {
|
||||
Log.e(tag, "Failed to play library item")
|
||||
} else {
|
||||
val playbackRate = mediaManager.getSavedPlaybackRate()
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
preparePlayer(it,true, playbackRate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun startNewPlaybackSession() {
|
||||
currentPlaybackSession?.let { playbackSession ->
|
||||
val forceTranscode = playbackSession.isHLS // If already HLS then force
|
||||
@@ -663,7 +701,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
// Streaming from server so check if playback session still exists on server
|
||||
Log.d(
|
||||
tag,
|
||||
"checkCurrentSessionProgress: Checking if playback session for server stream is still available"
|
||||
"checkCurrentSessionProgress: Checking if playback session ${playbackSession.id} for server stream is still available"
|
||||
)
|
||||
apiHandler.getPlaybackSession(playbackSession.id) {
|
||||
if (it == null) {
|
||||
|
||||
@@ -12,7 +12,6 @@ import com.getcapacitor.annotation.CapacitorPlugin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.json.JSONObject
|
||||
|
||||
@CapacitorPlugin(name = "AbsDatabase")
|
||||
class AbsDatabase : Plugin() {
|
||||
|
||||
@@ -321,6 +321,7 @@ class ApiHandler(var ctx:Context) {
|
||||
}
|
||||
|
||||
fun getPlaybackSession(playbackSessionId:String, cb: (PlaybackSession?) -> Unit) {
|
||||
Log.d(tag, "getPlaybackSession for $playbackSessionId for server ${DeviceManager.serverAddress}")
|
||||
val endpoint = "/api/session/$playbackSessionId"
|
||||
getRequest(endpoint, null, null) {
|
||||
val err = it.getString("error")
|
||||
|
||||
Reference in New Issue
Block a user