mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-08-28 14:17:19 +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.MediaDescriptionCompat
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import androidx.media.utils.MediaConstants
|
import androidx.media.utils.MediaConstants
|
||||||
|
import com.audiobookshelf.app.media.MediaManager
|
||||||
import com.fasterxml.jackson.annotation.*
|
import com.fasterxml.jackson.annotation.*
|
||||||
|
|
||||||
// This auto-detects whether it is a Book or Podcast
|
// This auto-detects whether it is a Book or Podcast
|
||||||
@@ -51,7 +52,7 @@ class Podcast(
|
|||||||
// Add new episodes
|
// Add new episodes
|
||||||
audioTracks.forEach { at ->
|
audioTracks.forEach { at ->
|
||||||
if (episodes?.find{ it.audioTrack?.localFileId == at.localFileId } == null) {
|
if (episodes?.find{ it.audioTrack?.localFileId == at.localFileId } == null) {
|
||||||
val newEpisode = PodcastEpisode("local_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)
|
episodes?.add(newEpisode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +65,7 @@ class Podcast(
|
|||||||
}
|
}
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun addAudioTrack(audioTrack:AudioTrack) {
|
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)
|
episodes?.add(newEpisode)
|
||||||
|
|
||||||
var index = 1
|
var index = 1
|
||||||
@@ -83,9 +84,16 @@ class Podcast(
|
|||||||
index++
|
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
|
@JsonIgnore
|
||||||
fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode):PodcastEpisode {
|
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)
|
episodes?.add(newEpisode)
|
||||||
|
|
||||||
var index = 1
|
var index = 1
|
||||||
@@ -96,10 +104,14 @@ class Podcast(
|
|||||||
return newEpisode
|
return newEpisode
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used for FolderScanner local podcast item to get copy of Podcast excluding episodes
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getLocalCopy(): Podcast {
|
fun getNextUnfinishedEpisode(libraryItemId:String, mediaManager: MediaManager):PodcastEpisode? {
|
||||||
return Podcast(metadata as PodcastMetadata,coverPath,tags, mutableListOf(),autoDownloadEpisodes, 0)
|
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 title:String?,
|
||||||
var subtitle:String?,
|
var subtitle:String?,
|
||||||
var description:String?,
|
var description:String?,
|
||||||
|
var pubDate:String?,
|
||||||
|
var publishedAt:Long?,
|
||||||
var audioFile:AudioFile?,
|
var audioFile:AudioFile?,
|
||||||
var audioTrack:AudioTrack?,
|
var audioTrack:AudioTrack?,
|
||||||
var duration:Double?,
|
var duration:Double?,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.audiobookshelf.app.data
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.ImageDecoder
|
import android.graphics.ImageDecoder
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -58,6 +57,8 @@ class PlaybackSession(
|
|||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val isLocal get() = playMethod == PLAYMETHOD_LOCAL
|
val isLocal get() = playMethod == PLAYMETHOD_LOCAL
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
|
val isPodcastEpisode get() = mediaType == "podcast"
|
||||||
|
@get:JsonIgnore
|
||||||
val currentTimeMs get() = (currentTime * 1000L).toLong()
|
val currentTimeMs get() = (currentTime * 1000L).toLong()
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val totalDurationMs get() = (getTotalDuration() * 1000L).toLong()
|
val totalDurationMs get() = (getTotalDuration() * 1000L).toLong()
|
||||||
@@ -136,6 +137,7 @@ class PlaybackSession(
|
|||||||
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getCoverUri().toString())
|
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getCoverUri().toString())
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_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
|
// Local covers get bitmap
|
||||||
if (localLibraryItem?.coverContentUrl != null) {
|
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_TITLE, displayTitle ?: "")
|
||||||
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_ARTIST, displayAuthor ?: "")
|
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.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)
|
castMetadata.putInt(com.google.android.gms.cast.MediaMetadata.KEY_TRACK_NUMBER, audioTrack.index)
|
||||||
return castMetadata
|
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) {
|
fun loadAndroidAutoItems(cb: () -> Unit) {
|
||||||
Log.d(tag, "Load android auto items")
|
Log.d(tag, "Load android auto items")
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,19 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
failedSyncs = 0
|
failedSyncs = 0
|
||||||
cb()
|
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) {
|
fun syncFromServerProgress(mediaProgress: MediaProgress) {
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||||||
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_ENDED) {
|
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_ENDED) {
|
||||||
Log.d(tag, "STATE_ENDED")
|
Log.d(tag, "STATE_ENDED")
|
||||||
playerNotificationService.sendClientMetadata(PlayerState.ENDED)
|
playerNotificationService.sendClientMetadata(PlayerState.ENDED)
|
||||||
|
|
||||||
|
playerNotificationService.handlePlaybackEnded()
|
||||||
}
|
}
|
||||||
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_IDLE) {
|
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_IDLE) {
|
||||||
Log.d(tag, "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
|
isClosed = false
|
||||||
val customActionProviders = mutableListOf(
|
val customActionProviders = mutableListOf(
|
||||||
JumpBackwardCustomActionProvider(),
|
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() {
|
fun startNewPlaybackSession() {
|
||||||
currentPlaybackSession?.let { playbackSession ->
|
currentPlaybackSession?.let { playbackSession ->
|
||||||
val forceTranscode = playbackSession.isHLS // If already HLS then force
|
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
|
// Streaming from server so check if playback session still exists on server
|
||||||
Log.d(
|
Log.d(
|
||||||
tag,
|
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) {
|
apiHandler.getPlaybackSession(playbackSession.id) {
|
||||||
if (it == null) {
|
if (it == null) {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import com.getcapacitor.annotation.CapacitorPlugin
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.json.JSONObject
|
|
||||||
|
|
||||||
@CapacitorPlugin(name = "AbsDatabase")
|
@CapacitorPlugin(name = "AbsDatabase")
|
||||||
class AbsDatabase : Plugin() {
|
class AbsDatabase : Plugin() {
|
||||||
|
|||||||
@@ -321,6 +321,7 @@ class ApiHandler(var ctx:Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getPlaybackSession(playbackSessionId:String, cb: (PlaybackSession?) -> Unit) {
|
fun getPlaybackSession(playbackSessionId:String, cb: (PlaybackSession?) -> Unit) {
|
||||||
|
Log.d(tag, "getPlaybackSession for $playbackSessionId for server ${DeviceManager.serverAddress}")
|
||||||
val endpoint = "/api/session/$playbackSessionId"
|
val endpoint = "/api/session/$playbackSessionId"
|
||||||
getRequest(endpoint, null, null) {
|
getRequest(endpoint, null, null) {
|
||||||
val err = it.getString("error")
|
val err = it.getString("error")
|
||||||
|
|||||||
Reference in New Issue
Block a user