From 11f0af1ebd1c7caa4927fe5d6d5c240e6964ee7c Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 11 Dec 2022 16:09:00 -0600 Subject: [PATCH] Update:Android Auto play next podcast episode after finishing one --- .../audiobookshelf/app/data/DataClasses.kt | 26 +++++++++--- .../app/data/PlaybackSession.kt | 6 ++- .../audiobookshelf/app/media/MediaManager.kt | 17 ++++++++ .../app/player/MediaProgressSyncer.kt | 12 ++++++ .../app/player/PlayerListener.kt | 2 + .../app/player/PlayerNotificationService.kt | 40 ++++++++++++++++++- .../audiobookshelf/app/plugins/AbsDatabase.kt | 1 - .../audiobookshelf/app/server/ApiHandler.kt | 1 + 8 files changed, 96 insertions(+), 9 deletions(-) diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt index 55bf5607..e3d039ad 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt @@ -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?, diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt b/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt index c0765f12..22e54c04 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/PlaybackSession.kt @@ -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 } diff --git a/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt b/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt index d29695e1..b83d82f3 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/media/MediaManager.kt @@ -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") diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/MediaProgressSyncer.kt b/android/app/src/main/java/com/audiobookshelf/app/player/MediaProgressSyncer.kt index 99ee91b2..e766f303 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/MediaProgressSyncer.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/MediaProgressSyncer.kt @@ -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) { diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt index 239ebb97..fdb55812 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerListener.kt @@ -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") diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt index 63d3f826..443978ee 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt @@ -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) { diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt index 3076ae6e..e5585594 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt @@ -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() { diff --git a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt index 2d381ec8..6e76587e 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/server/ApiHandler.kt @@ -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")