Autoformatting files

This commit is contained in:
Nicholas Wallace 2025-02-08 10:36:03 -07:00
parent 9492975a74
commit b2ebeafed5
6 changed files with 1411 additions and 893 deletions

View file

@ -8,10 +8,10 @@ class MediaItemHistory(
var mediaDisplayTitle: String, var mediaDisplayTitle: String,
var libraryItemId: String, var libraryItemId: String,
var episodeId: String?, var episodeId: String?,
var isLocal:Boolean, var isLocal: Boolean,
var serverConnectionConfigId:String?, var serverConnectionConfigId: String?,
var serverAddress:String?, var serverAddress: String?,
var serverUserId:String?, var serverUserId: String?,
var createdAt: Long, var createdAt: Long,
var events:MutableList<MediaItemEvent>, var events: MutableList<MediaItemEvent>,
) )

View file

@ -12,6 +12,7 @@ import com.audiobookshelf.app.BuildConfig
import com.audiobookshelf.app.R import com.audiobookshelf.app.R
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.media.MediaProgressSyncData import com.audiobookshelf.app.media.MediaProgressSyncData
import com.audiobookshelf.app.player.*
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.google.android.exoplayer2.MediaItem import com.google.android.exoplayer2.MediaItem
@ -19,61 +20,75 @@ import com.google.android.exoplayer2.MediaMetadata
import com.google.android.gms.cast.MediaInfo import com.google.android.gms.cast.MediaInfo
import com.google.android.gms.cast.MediaQueueItem import com.google.android.gms.cast.MediaQueueItem
import com.google.android.gms.common.images.WebImage import com.google.android.gms.common.images.WebImage
import com.audiobookshelf.app.player.*
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
class PlaybackSession( class PlaybackSession(
var id:String, var id: String,
var userId:String?, var userId: String?,
var libraryItemId:String?, var libraryItemId: String?,
var episodeId:String?, var episodeId: String?,
var mediaType:String, var mediaType: String,
var mediaMetadata:MediaTypeMetadata, var mediaMetadata: MediaTypeMetadata,
var deviceInfo:DeviceInfo, var deviceInfo: DeviceInfo,
var chapters:List<BookChapter>, var chapters: List<BookChapter>,
var displayTitle: String?, var displayTitle: String?,
var displayAuthor: String?, var displayAuthor: String?,
var coverPath:String?, var coverPath: String?,
var duration:Double, var duration: Double,
var playMethod:Int, var playMethod: Int,
var startedAt:Long, var startedAt: Long,
var updatedAt:Long, var updatedAt: Long,
var timeListening:Long, var timeListening: Long,
var audioTracks:MutableList<AudioTrack>, var audioTracks: MutableList<AudioTrack>,
var currentTime:Double, var currentTime: Double,
var libraryItem:LibraryItem?, var libraryItem: LibraryItem?,
var localLibraryItem:LocalLibraryItem?, var localLibraryItem: LocalLibraryItem?,
var localEpisodeId:String?, var localEpisodeId: String?,
var serverConnectionConfigId:String?, var serverConnectionConfigId: String?,
var serverAddress:String?, var serverAddress: String?,
var mediaPlayer:String? var mediaPlayer: String?
) { ) {
@get:JsonIgnore @get:JsonIgnore
val isHLS get() = playMethod == PLAYMETHOD_TRANSCODE val isHLS
get() = playMethod == PLAYMETHOD_TRANSCODE
@get:JsonIgnore @get:JsonIgnore
val isDirectPlay get() = playMethod == PLAYMETHOD_DIRECTPLAY val isDirectPlay
get() = playMethod == PLAYMETHOD_DIRECTPLAY
@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" val isPodcastEpisode
get() = mediaType == "podcast"
@get:JsonIgnore @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()
@get:JsonIgnore @get:JsonIgnore
val localLibraryItemId get() = localLibraryItem?.id ?: "" val localLibraryItemId
get() = localLibraryItem?.id ?: ""
@get:JsonIgnore @get:JsonIgnore
val localMediaProgressId get() = if (localEpisodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId" val localMediaProgressId
get() =
if (localEpisodeId.isNullOrEmpty()) localLibraryItemId
else "$localLibraryItemId-$localEpisodeId"
@get:JsonIgnore @get:JsonIgnore
val progress get() = currentTime / getTotalDuration() val progress
get() = currentTime / getTotalDuration()
@get:JsonIgnore @get:JsonIgnore
val isLocalLibraryItemOnly get() = localLibraryItemId != "" && libraryItemId == null val isLocalLibraryItemOnly
get() = localLibraryItemId != "" && libraryItemId == null
@get:JsonIgnore @get:JsonIgnore
val mediaItemId get() = if (isLocalLibraryItemOnly) localMediaProgressId else if (episodeId.isNullOrEmpty()) libraryItemId ?: "" else "$libraryItemId-$episodeId" val mediaItemId
get() =
if (isLocalLibraryItemOnly) localMediaProgressId
else if (episodeId.isNullOrEmpty()) libraryItemId ?: "" else "$libraryItemId-$episodeId"
@JsonIgnore @JsonIgnore
fun getCurrentTrackIndex():Int { fun getCurrentTrackIndex(): Int {
for (i in 0 until audioTracks.size) { for (i in 0 until audioTracks.size) {
val track = audioTracks[i] val track = audioTracks[i]
if (currentTimeMs >= track.startOffsetMs && (track.endOffsetMs > currentTimeMs)) { if (currentTimeMs >= track.startOffsetMs && (track.endOffsetMs > currentTimeMs)) {
@ -84,7 +99,7 @@ class PlaybackSession(
} }
@JsonIgnore @JsonIgnore
fun getNextTrackIndex():Int { fun getNextTrackIndex(): Int {
for (i in 0 until audioTracks.size) { for (i in 0 until audioTracks.size) {
val track = audioTracks[i] val track = audioTracks[i]
if (currentTimeMs < track.startOffsetMs) { if (currentTimeMs < track.startOffsetMs) {
@ -95,67 +110,74 @@ class PlaybackSession(
} }
@JsonIgnore @JsonIgnore
fun getChapterForTime(time:Long):BookChapter? { fun getChapterForTime(time: Long): BookChapter? {
if (chapters.isEmpty()) return null if (chapters.isEmpty()) return null
return chapters.find { time >= it.startMs && it.endMs > time} return chapters.find { time >= it.startMs && it.endMs > time }
} }
@JsonIgnore @JsonIgnore
fun getCurrentTrackEndTime():Long { fun getCurrentTrackEndTime(): Long {
val currentTrack = audioTracks[this.getCurrentTrackIndex()] val currentTrack = audioTracks[this.getCurrentTrackIndex()]
return currentTrack.startOffsetMs + currentTrack.durationMs return currentTrack.startOffsetMs + currentTrack.durationMs
} }
@JsonIgnore @JsonIgnore
fun getNextChapterForTime(time:Long):BookChapter? { fun getNextChapterForTime(time: Long): BookChapter? {
if (chapters.isEmpty()) return null if (chapters.isEmpty()) return null
return chapters.find { time < it.startMs } // First chapter where start time is > then time return chapters.find { time < it.startMs } // First chapter where start time is > then time
} }
@JsonIgnore @JsonIgnore
fun getNextTrackEndTime():Long { fun getNextTrackEndTime(): Long {
val currentTrack = audioTracks[this.getNextTrackIndex()] val currentTrack = audioTracks[this.getNextTrackIndex()]
return currentTrack.startOffsetMs + currentTrack.durationMs return currentTrack.startOffsetMs + currentTrack.durationMs
} }
@JsonIgnore @JsonIgnore
fun getCurrentTrackTimeMs():Long { fun getCurrentTrackTimeMs(): Long {
val currentTrack = audioTracks[this.getCurrentTrackIndex()] val currentTrack = audioTracks[this.getCurrentTrackIndex()]
val time = currentTime - currentTrack.startOffset val time = currentTime - currentTrack.startOffset
return (time * 1000L).toLong() return (time * 1000L).toLong()
} }
@JsonIgnore @JsonIgnore
fun getTrackStartOffsetMs(index:Int):Long { fun getTrackStartOffsetMs(index: Int): Long {
if (index < 0 || index >= audioTracks.size) return 0L if (index < 0 || index >= audioTracks.size) return 0L
val currentTrack = audioTracks[index] val currentTrack = audioTracks[index]
return (currentTrack.startOffset * 1000L).toLong() return (currentTrack.startOffset * 1000L).toLong()
} }
@JsonIgnore @JsonIgnore
fun getTotalDuration():Double { fun getTotalDuration(): Double {
var total = 0.0 var total = 0.0
audioTracks.forEach { total += it.duration } audioTracks.forEach { total += it.duration }
return total return total
} }
@JsonIgnore @JsonIgnore
fun getCoverUri(ctx:Context): Uri { fun getCoverUri(ctx: Context): Uri {
if (localLibraryItem?.coverContentUrl != null) { if (localLibraryItem?.coverContentUrl != null) {
var coverUri = Uri.parse(localLibraryItem?.coverContentUrl.toString()) var coverUri = Uri.parse(localLibraryItem?.coverContentUrl.toString())
if (coverUri.toString().startsWith("file:")) { if (coverUri.toString().startsWith("file:")) {
coverUri = FileProvider.getUriForFile(ctx, "${BuildConfig.APPLICATION_ID}.fileprovider", coverUri.toFile()) coverUri =
FileProvider.getUriForFile(
ctx,
"${BuildConfig.APPLICATION_ID}.fileprovider",
coverUri.toFile()
)
} }
return coverUri ?: Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon) return coverUri
?: Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)
} }
if (coverPath == null) return Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon) if (coverPath == null)
return Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)
return Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}") return Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}")
} }
@JsonIgnore @JsonIgnore
fun getContentUri(audioTrack:AudioTrack): Uri { fun getContentUri(audioTrack: AudioTrack): Uri {
if (isLocal) return Uri.parse(audioTrack.contentUrl) // Local content url if (isLocal) return Uri.parse(audioTrack.contentUrl) // Local content url
return Uri.parse("$serverAddress${audioTrack.contentUrl}?token=${DeviceManager.token}") return Uri.parse("$serverAddress${audioTrack.contentUrl}?token=${DeviceManager.token}")
} }
@ -164,7 +186,8 @@ class PlaybackSession(
fun getMediaMetadataCompat(ctx: Context): MediaMetadataCompat { fun getMediaMetadataCompat(ctx: Context): MediaMetadataCompat {
val coverUri = getCoverUri(ctx) val coverUri = getCoverUri(ctx)
val metadataBuilder = MediaMetadataCompat.Builder() val metadataBuilder =
MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, displayTitle) .putString(MediaMetadataCompat.METADATA_KEY_TITLE, displayTitle)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, displayTitle) .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, displayTitle)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, displayAuthor) .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, displayAuthor)
@ -176,14 +199,19 @@ class PlaybackSession(
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id) .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, coverUri.toString()) .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, coverUri.toString())
.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, coverUri.toString()) .putString(MediaMetadataCompat.METADATA_KEY_ART_URI, coverUri.toString())
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString()) .putString(
MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,
coverUri.toString()
)
// Local covers get bitmap // Local covers get bitmap
if (localLibraryItem?.coverContentUrl != null) { if (localLibraryItem?.coverContentUrl != null) {
val bitmap = if (Build.VERSION.SDK_INT < 28) { val bitmap =
if (Build.VERSION.SDK_INT < 28) {
MediaStore.Images.Media.getBitmap(ctx.contentResolver, coverUri) MediaStore.Images.Media.getBitmap(ctx.contentResolver, coverUri)
} else { } else {
val source: ImageDecoder.Source = ImageDecoder.createSource(ctx.contentResolver, coverUri) val source: ImageDecoder.Source =
ImageDecoder.createSource(ctx.contentResolver, coverUri)
ImageDecoder.decodeBitmap(source) ImageDecoder.decodeBitmap(source)
} }
metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap) metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap)
@ -194,10 +222,11 @@ class PlaybackSession(
} }
@JsonIgnore @JsonIgnore
fun getExoMediaMetadata(ctx:Context): MediaMetadata { fun getExoMediaMetadata(ctx: Context): MediaMetadata {
val coverUri = getCoverUri(ctx) val coverUri = getCoverUri(ctx)
val metadataBuilder = MediaMetadata.Builder() val metadataBuilder =
MediaMetadata.Builder()
.setTitle(displayTitle) .setTitle(displayTitle)
.setDisplayTitle(displayTitle) .setDisplayTitle(displayTitle)
.setArtist(displayAuthor) .setArtist(displayAuthor)
@ -212,8 +241,8 @@ class PlaybackSession(
} }
@JsonIgnore @JsonIgnore
fun getMediaItems(ctx:Context):List<MediaItem> { fun getMediaItems(ctx: Context): List<MediaItem> {
val mediaItems:MutableList<MediaItem> = mutableListOf() val mediaItems: MutableList<MediaItem> = mutableListOf()
for (audioTrack in audioTracks) { for (audioTrack in audioTracks) {
val mediaMetadata = this.getExoMediaMetadata(ctx) val mediaMetadata = this.getExoMediaMetadata(ctx)
@ -221,50 +250,105 @@ class PlaybackSession(
val mimeType = audioTrack.mimeType val mimeType = audioTrack.mimeType
val queueItem = getQueueItem(audioTrack) // Queue item used in exo player CastManager val queueItem = getQueueItem(audioTrack) // Queue item used in exo player CastManager
val mediaItem = MediaItem.Builder().setUri(mediaUri).setTag(queueItem).setMediaMetadata(mediaMetadata).setMimeType(mimeType).build() val mediaItem =
MediaItem.Builder()
.setUri(mediaUri)
.setTag(queueItem)
.setMediaMetadata(mediaMetadata)
.setMimeType(mimeType)
.build()
mediaItems.add(mediaItem) mediaItems.add(mediaItem)
} }
return mediaItems return mediaItems
} }
@JsonIgnore @JsonIgnore
fun getCastMediaMetadata(audioTrack:AudioTrack):com.google.android.gms.cast.MediaMetadata { fun getCastMediaMetadata(audioTrack: AudioTrack): com.google.android.gms.cast.MediaMetadata {
val castMetadata = com.google.android.gms.cast.MediaMetadata(com.google.android.gms.cast.MediaMetadata.MEDIA_TYPE_AUDIOBOOK_CHAPTER) val castMetadata =
com.google.android.gms.cast.MediaMetadata(
com.google.android.gms.cast.MediaMetadata.MEDIA_TYPE_AUDIOBOOK_CHAPTER
)
coverPath?.let { coverPath?.let {
castMetadata.addImage(WebImage(Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}"))) castMetadata.addImage(
WebImage(
Uri.parse(
"$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}"
)
)
)
} }
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(
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_ALBUM_TITLE, displayAuthor ?: "") com.google.android.gms.cast.MediaMetadata.KEY_ARTIST,
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_CHAPTER_TITLE, audioTrack.title) 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) castMetadata.putInt(
com.google.android.gms.cast.MediaMetadata.KEY_TRACK_NUMBER,
audioTrack.index
)
return castMetadata return castMetadata
} }
@JsonIgnore @JsonIgnore
fun getQueueItem(audioTrack:AudioTrack):MediaQueueItem { fun getQueueItem(audioTrack: AudioTrack): MediaQueueItem {
val castMetadata = getCastMediaMetadata(audioTrack) val castMetadata = getCastMediaMetadata(audioTrack)
val mediaUri = getContentUri(audioTrack) val mediaUri = getContentUri(audioTrack)
val mediaInfo = MediaInfo.Builder(mediaUri.toString()).apply { val mediaInfo =
MediaInfo.Builder(mediaUri.toString())
.apply {
setContentUrl(mediaUri.toString()) setContentUrl(mediaUri.toString())
setContentType(audioTrack.mimeType) setContentType(audioTrack.mimeType)
setMetadata(castMetadata) setMetadata(castMetadata)
setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
}.build() }
.build()
return MediaQueueItem.Builder(mediaInfo).apply { return MediaQueueItem.Builder(mediaInfo)
setPlaybackDuration(audioTrack.duration) .apply { setPlaybackDuration(audioTrack.duration) }
}.build() .build()
} }
@JsonIgnore @JsonIgnore
fun clone():PlaybackSession { fun clone(): PlaybackSession {
return PlaybackSession(id,userId,libraryItemId,episodeId,mediaType,mediaMetadata,deviceInfo,chapters,displayTitle,displayAuthor,coverPath,duration,playMethod,startedAt,updatedAt,timeListening,audioTracks,currentTime,libraryItem,localLibraryItem,localEpisodeId,serverConnectionConfigId,serverAddress, mediaPlayer) return PlaybackSession(
id,
userId,
libraryItemId,
episodeId,
mediaType,
mediaMetadata,
deviceInfo,
chapters,
displayTitle,
displayAuthor,
coverPath,
duration,
playMethod,
startedAt,
updatedAt,
timeListening,
audioTracks,
currentTime,
libraryItem,
localLibraryItem,
localEpisodeId,
serverConnectionConfigId,
serverAddress,
mediaPlayer
)
} }
@JsonIgnore @JsonIgnore
@ -275,7 +359,25 @@ class PlaybackSession(
} }
@JsonIgnore @JsonIgnore
fun getNewLocalMediaProgress():LocalMediaProgress { fun getNewLocalMediaProgress(): LocalMediaProgress {
return LocalMediaProgress(localMediaProgressId,localLibraryItemId,localEpisodeId,getTotalDuration(),progress,currentTime,false,null,null,updatedAt,startedAt,null,serverConnectionConfigId,serverAddress,userId,libraryItemId,episodeId) return LocalMediaProgress(
localMediaProgressId,
localLibraryItemId,
localEpisodeId,
getTotalDuration(),
progress,
currentTime,
false,
null,
null,
updatedAt,
startedAt,
null,
serverConnectionConfigId,
serverAddress,
userId,
libraryItemId,
episodeId
)
} }
} }

View file

@ -22,41 +22,43 @@ class DbManager {
} }
fun getDeviceData(): DeviceData { fun getDeviceData(): DeviceData {
return Paper.book("device").read("data") ?: DeviceData(mutableListOf(), null, DeviceSettings.default(), null) return Paper.book("device").read("data")
?: DeviceData(mutableListOf(), null, DeviceSettings.default(), null)
} }
fun saveDeviceData(deviceData: DeviceData) { fun saveDeviceData(deviceData: DeviceData) {
Paper.book("device").write("data", deviceData) Paper.book("device").write("data", deviceData)
} }
fun getLocalLibraryItems(mediaType:String? = null):MutableList<LocalLibraryItem> { fun getLocalLibraryItems(mediaType: String? = null): MutableList<LocalLibraryItem> {
val localLibraryItems:MutableList<LocalLibraryItem> = mutableListOf() val localLibraryItems: MutableList<LocalLibraryItem> = mutableListOf()
Paper.book("localLibraryItems").allKeys.forEach { Paper.book("localLibraryItems").allKeys.forEach {
val localLibraryItem: LocalLibraryItem? = Paper.book("localLibraryItems").read(it) val localLibraryItem: LocalLibraryItem? = Paper.book("localLibraryItems").read(it)
if (localLibraryItem != null && (mediaType.isNullOrEmpty() || mediaType == localLibraryItem.mediaType)) { if (localLibraryItem != null &&
(mediaType.isNullOrEmpty() || mediaType == localLibraryItem.mediaType)
) {
localLibraryItems.add(localLibraryItem) localLibraryItems.add(localLibraryItem)
} }
} }
return localLibraryItems return localLibraryItems
} }
fun getLocalLibraryItemsInFolder(folderId:String):List<LocalLibraryItem> { fun getLocalLibraryItemsInFolder(folderId: String): List<LocalLibraryItem> {
val localLibraryItems = getLocalLibraryItems() val localLibraryItems = getLocalLibraryItems()
return localLibraryItems.filter { return localLibraryItems.filter { it.folderId == folderId }
it.folderId == folderId
}
} }
fun getLocalLibraryItemByLId(libraryItemId:String): LocalLibraryItem? { fun getLocalLibraryItemByLId(libraryItemId: String): LocalLibraryItem? {
return getLocalLibraryItems().find { it.libraryItemId == libraryItemId } return getLocalLibraryItems().find { it.libraryItemId == libraryItemId }
} }
fun getLocalLibraryItem(localLibraryItemId:String): LocalLibraryItem? { fun getLocalLibraryItem(localLibraryItemId: String): LocalLibraryItem? {
return Paper.book("localLibraryItems").read(localLibraryItemId) return Paper.book("localLibraryItems").read(localLibraryItemId)
} }
fun getLocalLibraryItemWithEpisode(podcastEpisodeId:String): LibraryItemWithEpisode? { fun getLocalLibraryItemWithEpisode(podcastEpisodeId: String): LibraryItemWithEpisode? {
var podcastEpisode: PodcastEpisode? = null var podcastEpisode: PodcastEpisode? = null
val localLibraryItem = getLocalLibraryItems("podcast").find { localLibraryItem -> val localLibraryItem =
getLocalLibraryItems("podcast").find { localLibraryItem ->
val podcast = localLibraryItem.media as Podcast val podcast = localLibraryItem.media as Podcast
podcastEpisode = podcast.episodes?.find { it.id == podcastEpisodeId } podcastEpisode = podcast.episodes?.find { it.id == podcastEpisodeId }
podcastEpisode != null podcastEpisode != null
@ -68,14 +70,12 @@ class DbManager {
} }
} }
fun removeLocalLibraryItem(localLibraryItemId:String) { fun removeLocalLibraryItem(localLibraryItemId: String) {
Paper.book("localLibraryItems").delete(localLibraryItemId) Paper.book("localLibraryItems").delete(localLibraryItemId)
} }
fun saveLocalLibraryItems(localLibraryItems:List<LocalLibraryItem>) { fun saveLocalLibraryItems(localLibraryItems: List<LocalLibraryItem>) {
localLibraryItems.map { localLibraryItems.map { Paper.book("localLibraryItems").write(it.id, it) }
Paper.book("localLibraryItems").write(it.id, it)
}
} }
fun saveLocalLibraryItem(localLibraryItem: LocalLibraryItem) { fun saveLocalLibraryItem(localLibraryItem: LocalLibraryItem) {
@ -83,28 +83,24 @@ class DbManager {
} }
fun saveLocalFolder(localFolder: LocalFolder) { fun saveLocalFolder(localFolder: LocalFolder) {
Paper.book("localFolders").write(localFolder.id,localFolder) Paper.book("localFolders").write(localFolder.id, localFolder)
} }
fun getLocalFolder(folderId:String): LocalFolder? { fun getLocalFolder(folderId: String): LocalFolder? {
return Paper.book("localFolders").read(folderId) return Paper.book("localFolders").read(folderId)
} }
fun getAllLocalFolders():List<LocalFolder> { fun getAllLocalFolders(): List<LocalFolder> {
val localFolders:MutableList<LocalFolder> = mutableListOf() val localFolders: MutableList<LocalFolder> = mutableListOf()
Paper.book("localFolders").allKeys.forEach { localFolderId -> Paper.book("localFolders").allKeys.forEach { localFolderId ->
Paper.book("localFolders").read<LocalFolder>(localFolderId)?.let { Paper.book("localFolders").read<LocalFolder>(localFolderId)?.let { localFolders.add(it) }
localFolders.add(it)
}
} }
return localFolders return localFolders
} }
fun removeLocalFolder(folderId:String) { fun removeLocalFolder(folderId: String) {
val localLibraryItems = getLocalLibraryItemsInFolder(folderId) val localLibraryItems = getLocalLibraryItemsInFolder(folderId)
localLibraryItems.forEach { localLibraryItems.forEach { Paper.book("localLibraryItems").delete(it.id) }
Paper.book("localLibraryItems").delete(it.id)
}
Paper.book("localFolders").delete(folderId) Paper.book("localFolders").delete(folderId)
} }
@ -112,29 +108,28 @@ class DbManager {
Paper.book("downloadItems").write(downloadItem.id, downloadItem) Paper.book("downloadItems").write(downloadItem.id, downloadItem)
} }
fun removeDownloadItem(downloadItemId:String) { fun removeDownloadItem(downloadItemId: String) {
Paper.book("downloadItems").delete(downloadItemId) Paper.book("downloadItems").delete(downloadItemId)
} }
fun getDownloadItems():List<DownloadItem> { fun getDownloadItems(): List<DownloadItem> {
val downloadItems:MutableList<DownloadItem> = mutableListOf() val downloadItems: MutableList<DownloadItem> = mutableListOf()
Paper.book("downloadItems").allKeys.forEach { downloadItemId -> Paper.book("downloadItems").allKeys.forEach { downloadItemId ->
Paper.book("downloadItems").read<DownloadItem>(downloadItemId)?.let { Paper.book("downloadItems").read<DownloadItem>(downloadItemId)?.let { downloadItems.add(it) }
downloadItems.add(it)
}
} }
return downloadItems return downloadItems
} }
fun saveLocalMediaProgress(mediaProgress: LocalMediaProgress) { fun saveLocalMediaProgress(mediaProgress: LocalMediaProgress) {
Paper.book("localMediaProgress").write(mediaProgress.id,mediaProgress) Paper.book("localMediaProgress").write(mediaProgress.id, mediaProgress)
} }
// For books this will just be the localLibraryItemId for podcast episodes this will be "{localLibraryItemId}-{episodeId}" // For books this will just be the localLibraryItemId for podcast episodes this will be
fun getLocalMediaProgress(localMediaProgressId:String): LocalMediaProgress? { // "{localLibraryItemId}-{episodeId}"
fun getLocalMediaProgress(localMediaProgressId: String): LocalMediaProgress? {
return Paper.book("localMediaProgress").read(localMediaProgressId) return Paper.book("localMediaProgress").read(localMediaProgressId)
} }
fun getAllLocalMediaProgress():List<LocalMediaProgress> { fun getAllLocalMediaProgress(): List<LocalMediaProgress> {
val mediaProgress:MutableList<LocalMediaProgress> = mutableListOf() val mediaProgress: MutableList<LocalMediaProgress> = mutableListOf()
Paper.book("localMediaProgress").allKeys.forEach { localMediaProgressId -> Paper.book("localMediaProgress").allKeys.forEach { localMediaProgressId ->
Paper.book("localMediaProgress").read<LocalMediaProgress>(localMediaProgressId)?.let { Paper.book("localMediaProgress").read<LocalMediaProgress>(localMediaProgressId)?.let {
mediaProgress.add(it) mediaProgress.add(it)
@ -142,7 +137,7 @@ class DbManager {
} }
return mediaProgress return mediaProgress
} }
fun removeLocalMediaProgress(localMediaProgressId:String) { fun removeLocalMediaProgress(localMediaProgressId: String) {
Paper.book("localMediaProgress").delete(localMediaProgressId) Paper.book("localMediaProgress").delete(localMediaProgressId)
} }
@ -158,35 +153,50 @@ class DbManager {
var hasUpdates = false var hasUpdates = false
// Check local files // Check local files
lli.localFiles = lli.localFiles.filter { localFile -> lli.localFiles =
lli.localFiles.filter { localFile ->
val file = File(localFile.absolutePath) val file = File(localFile.absolutePath)
if (!file.exists()) { if (!file.exists()) {
Log.d(tag, "cleanLocalLibraryItems: Local file ${localFile.absolutePath} was removed from library item ${lli.media.metadata.title}") Log.d(
tag,
"cleanLocalLibraryItems: Local file ${localFile.absolutePath} was removed from library item ${lli.media.metadata.title}"
)
hasUpdates = true hasUpdates = true
} }
file.exists() file.exists()
} as MutableList<LocalFile> } as
MutableList<LocalFile>
// Check audio tracks and episodes // Check audio tracks and episodes
if (lli.isPodcast) { if (lli.isPodcast) {
val podcast = lli.media as Podcast val podcast = lli.media as Podcast
podcast.episodes = podcast.episodes?.filter { ep -> podcast.episodes =
podcast.episodes?.filter { ep ->
if (lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } == null) { if (lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } == null) {
Log.d(tag, "cleanLocalLibraryItems: Podcast episode ${ep.title} was removed from library item ${lli.media.metadata.title}") Log.d(
tag,
"cleanLocalLibraryItems: Podcast episode ${ep.title} was removed from library item ${lli.media.metadata.title}"
)
hasUpdates = true hasUpdates = true
} }
ep.audioTrack != null && lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } != null ep.audioTrack != null &&
} as MutableList<PodcastEpisode> lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } != null
} as
MutableList<PodcastEpisode>
} else { } else {
val book = lli.media as Book val book = lli.media as Book
book.tracks = book.tracks?.filter { track -> book.tracks =
book.tracks?.filter { track ->
if (lli.localFiles.find { lf -> lf.id == track.localFileId } == null) { if (lli.localFiles.find { lf -> lf.id == track.localFileId } == null) {
Log.d(tag, "cleanLocalLibraryItems: Audio track ${track.title} was removed from library item ${lli.media.metadata.title}") Log.d(
tag,
"cleanLocalLibraryItems: Audio track ${track.title} was removed from library item ${lli.media.metadata.title}"
)
hasUpdates = true hasUpdates = true
} }
lli.localFiles.find { lf -> lf.id == track.localFileId } != null lli.localFiles.find { lf -> lf.id == track.localFileId } != null
} as MutableList<AudioTrack> } as
MutableList<AudioTrack>
} }
// Check cover still there // Check cover still there
@ -194,7 +204,10 @@ class DbManager {
val coverFile = File(it) val coverFile = File(it)
if (!coverFile.exists()) { if (!coverFile.exists()) {
Log.d(tag, "cleanLocalLibraryItems: Cover $it was removed from library item ${lli.media.metadata.title}") Log.d(
tag,
"cleanLocalLibraryItems: Cover $it was removed from library item ${lli.media.metadata.title}"
)
lli.coverAbsolutePath = null lli.coverAbsolutePath = null
lli.coverContentUrl = null lli.coverContentUrl = null
hasUpdates = true hasUpdates = true
@ -215,11 +228,18 @@ class DbManager {
localMediaProgress.forEach { localMediaProgress.forEach {
val matchingLLI = localLibraryItems.find { lli -> lli.id == it.localLibraryItemId } val matchingLLI = localLibraryItems.find { lli -> lli.id == it.localLibraryItemId }
if (!it.id.startsWith("local")) { if (!it.id.startsWith("local")) {
// A bug on the server when syncing local media progress was replacing the media progress id causing duplicate progress. Remove them. // A bug on the server when syncing local media progress was replacing the media progress id
Log.d(tag, "cleanLocalMediaProgress: Invalid local media progress does not start with 'local' (fixed on server 2.0.24)") // causing duplicate progress. Remove them.
Log.d(
tag,
"cleanLocalMediaProgress: Invalid local media progress does not start with 'local' (fixed on server 2.0.24)"
)
Paper.book("localMediaProgress").delete(it.id) Paper.book("localMediaProgress").delete(it.id)
} else if (matchingLLI == null) { } else if (matchingLLI == null) {
Log.d(tag, "cleanLocalMediaProgress: No matching local library item for local media progress ${it.id} - removing") Log.d(
tag,
"cleanLocalMediaProgress: No matching local library item for local media progress ${it.id} - removing"
)
Paper.book("localMediaProgress").delete(it.id) Paper.book("localMediaProgress").delete(it.id)
} else if (matchingLLI.isPodcast) { } else if (matchingLLI.isPodcast) {
if (it.localEpisodeId.isNullOrEmpty()) { if (it.localEpisodeId.isNullOrEmpty()) {
@ -229,7 +249,10 @@ class DbManager {
val podcast = matchingLLI.media as Podcast val podcast = matchingLLI.media as Podcast
val matchingLEp = podcast.episodes?.find { ep -> ep.id == it.localEpisodeId } val matchingLEp = podcast.episodes?.find { ep -> ep.id == it.localEpisodeId }
if (matchingLEp == null) { if (matchingLEp == null) {
Log.d(tag, "cleanLocalMediaProgress: Podcast media progress for episode ${it.localEpisodeId} not found - removing") Log.d(
tag,
"cleanLocalMediaProgress: Podcast media progress for episode ${it.localEpisodeId} not found - removing"
)
Paper.book("localMediaProgress").delete(it.id) Paper.book("localMediaProgress").delete(it.id)
} }
} }
@ -238,20 +261,20 @@ class DbManager {
} }
fun saveMediaItemHistory(mediaItemHistory: MediaItemHistory) { fun saveMediaItemHistory(mediaItemHistory: MediaItemHistory) {
Paper.book("mediaItemHistory").write(mediaItemHistory.id,mediaItemHistory) Paper.book("mediaItemHistory").write(mediaItemHistory.id, mediaItemHistory)
} }
fun getMediaItemHistory(id:String): MediaItemHistory? { fun getMediaItemHistory(id: String): MediaItemHistory? {
return Paper.book("mediaItemHistory").read(id) return Paper.book("mediaItemHistory").read(id)
} }
fun savePlaybackSession(playbackSession: PlaybackSession) { fun savePlaybackSession(playbackSession: PlaybackSession) {
Paper.book("playbackSession").write(playbackSession.id,playbackSession) Paper.book("playbackSession").write(playbackSession.id, playbackSession)
} }
fun removePlaybackSession(playbackSessionId:String) { fun removePlaybackSession(playbackSessionId: String) {
Paper.book("playbackSession").delete(playbackSessionId) Paper.book("playbackSession").delete(playbackSessionId)
} }
fun getPlaybackSessions():List<PlaybackSession> { fun getPlaybackSessions(): List<PlaybackSession> {
val sessions:MutableList<PlaybackSession> = mutableListOf() val sessions: MutableList<PlaybackSession> = mutableListOf()
Paper.book("playbackSession").allKeys.forEach { playbackSessionId -> Paper.book("playbackSession").allKeys.forEach { playbackSessionId ->
Paper.book("playbackSession").read<PlaybackSession>(playbackSessionId)?.let { Paper.book("playbackSession").read<PlaybackSession>(playbackSessionId)?.let {
sessions.add(it) sessions.add(it)

View file

@ -36,23 +36,37 @@ object MediaEventManager {
} }
fun seekEvent(playbackSession: PlaybackSession, syncResult: SyncResult?) { fun seekEvent(playbackSession: PlaybackSession, syncResult: SyncResult?) {
Log.i(tag, "Seek Event for media \"${playbackSession.displayTitle}\", currentTime=${playbackSession.currentTime}") Log.i(
tag,
"Seek Event for media \"${playbackSession.displayTitle}\", currentTime=${playbackSession.currentTime}"
)
addPlaybackEvent("Seek", playbackSession, syncResult) addPlaybackEvent("Seek", playbackSession, syncResult)
} }
fun syncEvent(mediaProgress: MediaProgressWrapper, description: String) { fun syncEvent(mediaProgress: MediaProgressWrapper, description: String) {
Log.i(tag, "Sync Event for media item id \"${mediaProgress.mediaItemId}\", currentTime=${mediaProgress.currentTime}") Log.i(
tag,
"Sync Event for media item id \"${mediaProgress.mediaItemId}\", currentTime=${mediaProgress.currentTime}"
)
addSyncEvent("Sync", mediaProgress, description) addSyncEvent("Sync", mediaProgress, description)
} }
private fun addSyncEvent(eventName:String, mediaProgress:MediaProgressWrapper, description: String) { private fun addSyncEvent(
eventName: String,
mediaProgress: MediaProgressWrapper,
description: String
) {
val mediaItemHistory = getMediaItemHistoryMediaItem(mediaProgress.mediaItemId) val mediaItemHistory = getMediaItemHistoryMediaItem(mediaProgress.mediaItemId)
if (mediaItemHistory == null) { if (mediaItemHistory == null) {
Log.w(tag, "addSyncEvent: Media Item History not created yet for media item id ${mediaProgress.mediaItemId}") Log.w(
tag,
"addSyncEvent: Media Item History not created yet for media item id ${mediaProgress.mediaItemId}"
)
return return
} }
val mediaItemEvent = MediaItemEvent( val mediaItemEvent =
MediaItemEvent(
name = eventName, name = eventName,
type = "Sync", type = "Sync",
description = description, description = description,
@ -68,10 +82,17 @@ object MediaEventManager {
clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory) clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory)
} }
private fun addPlaybackEvent(eventName:String, playbackSession:PlaybackSession, syncResult: SyncResult?) { private fun addPlaybackEvent(
val mediaItemHistory = getMediaItemHistoryMediaItem(playbackSession.mediaItemId) ?: createMediaItemHistoryForSession(playbackSession) eventName: String,
playbackSession: PlaybackSession,
syncResult: SyncResult?
) {
val mediaItemHistory =
getMediaItemHistoryMediaItem(playbackSession.mediaItemId)
?: createMediaItemHistoryForSession(playbackSession)
val mediaItemEvent = MediaItemEvent( val mediaItemEvent =
MediaItemEvent(
name = eventName, name = eventName,
type = "Playback", type = "Playback",
description = "", description = "",
@ -87,15 +108,20 @@ object MediaEventManager {
clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory) clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory)
} }
private fun getMediaItemHistoryMediaItem(mediaItemId: String) : MediaItemHistory? { private fun getMediaItemHistoryMediaItem(mediaItemId: String): MediaItemHistory? {
return DeviceManager.dbManager.getMediaItemHistory(mediaItemId) return DeviceManager.dbManager.getMediaItemHistory(mediaItemId)
} }
private fun createMediaItemHistoryForSession(playbackSession: PlaybackSession):MediaItemHistory { private fun createMediaItemHistoryForSession(playbackSession: PlaybackSession): MediaItemHistory {
Log.i(tag, "Creating new media item history for media \"${playbackSession.displayTitle}\"") Log.i(tag, "Creating new media item history for media \"${playbackSession.displayTitle}\"")
val isLocalOnly = playbackSession.isLocalLibraryItemOnly val isLocalOnly = playbackSession.isLocalLibraryItemOnly
val libraryItemId = if (isLocalOnly) playbackSession.localLibraryItemId else playbackSession.libraryItemId ?: "" val libraryItemId =
val episodeId:String? = if (isLocalOnly && playbackSession.localEpisodeId != null) playbackSession.localEpisodeId else playbackSession.episodeId if (isLocalOnly) playbackSession.localLibraryItemId
else playbackSession.libraryItemId ?: ""
val episodeId: String? =
if (isLocalOnly && playbackSession.localEpisodeId != null)
playbackSession.localEpisodeId
else playbackSession.episodeId
return MediaItemHistory( return MediaItemHistory(
id = playbackSession.mediaItemId, id = playbackSession.mediaItemId,
mediaDisplayTitle = playbackSession.displayTitle ?: "Unset", mediaDisplayTitle = playbackSession.displayTitle ?: "Unset",
@ -106,6 +132,7 @@ object MediaEventManager {
playbackSession.serverAddress, playbackSession.serverAddress,
playbackSession.userId, playbackSession.userId,
createdAt = System.currentTimeMillis(), createdAt = System.currentTimeMillis(),
events = mutableListOf()) events = mutableListOf()
)
} }
} }

View file

@ -13,36 +13,43 @@ import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
data class MediaProgressSyncData( data class MediaProgressSyncData(
var timeListened:Long, // seconds var timeListened: Long, // seconds
var duration:Double, // seconds var duration: Double, // seconds
var currentTime:Double // seconds var currentTime: Double // seconds
) )
data class SyncResult( data class SyncResult(
var serverSyncAttempted:Boolean, var serverSyncAttempted: Boolean,
var serverSyncSuccess:Boolean?, var serverSyncSuccess: Boolean?,
var serverSyncMessage:String? var serverSyncMessage: String?
) )
class MediaProgressSyncer(val playerNotificationService: PlayerNotificationService, private val apiHandler: ApiHandler) { class MediaProgressSyncer(
val playerNotificationService: PlayerNotificationService,
private val apiHandler: ApiHandler
) {
private val tag = "MediaProgressSync" private val tag = "MediaProgressSync"
private val METERED_CONNECTION_SYNC_INTERVAL = 60000 private val METERED_CONNECTION_SYNC_INTERVAL = 60000
private var listeningTimerTask: TimerTask? = null private var listeningTimerTask: TimerTask? = null
var listeningTimerRunning:Boolean = false var listeningTimerRunning: Boolean = false
private var lastSyncTime:Long = 0 private var lastSyncTime: Long = 0
private var failedSyncs:Int = 0 private var failedSyncs: Int = 0
var currentPlaybackSession: PlaybackSession? = null // copy of pb session currently syncing var currentPlaybackSession: PlaybackSession? = null // copy of pb session currently syncing
var currentLocalMediaProgress: LocalMediaProgress? = null var currentLocalMediaProgress: LocalMediaProgress? = null
private val currentDisplayTitle get() = currentPlaybackSession?.displayTitle ?: "Unset" private val currentDisplayTitle
val currentIsLocal get() = currentPlaybackSession?.isLocal == true get() = currentPlaybackSession?.displayTitle ?: "Unset"
val currentSessionId get() = currentPlaybackSession?.id ?: "" val currentIsLocal
private val currentPlaybackDuration get() = currentPlaybackSession?.duration ?: 0.0 get() = currentPlaybackSession?.isLocal == true
val currentSessionId
get() = currentPlaybackSession?.id ?: ""
private val currentPlaybackDuration
get() = currentPlaybackSession?.duration ?: 0.0
fun start(playbackSession:PlaybackSession) { fun start(playbackSession: PlaybackSession) {
if (listeningTimerRunning) { if (listeningTimerRunning) {
Log.d(tag, "start: Timer already running for $currentDisplayTitle") Log.d(tag, "start: Timer already running for $currentDisplayTitle")
if (playbackSession.id != currentSessionId) { if (playbackSession.id != currentSessionId) {
@ -62,16 +69,24 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi
listeningTimerRunning = true listeningTimerRunning = true
lastSyncTime = System.currentTimeMillis() lastSyncTime = System.currentTimeMillis()
currentPlaybackSession = playbackSession.clone() currentPlaybackSession = playbackSession.clone()
Log.d(tag, "start: init last sync time $lastSyncTime with playback session id=${currentPlaybackSession?.id}") Log.d(
tag,
"start: init last sync time $lastSyncTime with playback session id=${currentPlaybackSession?.id}"
)
listeningTimerTask = Timer("ListeningTimer", false).schedule(15000L, 15000L) { listeningTimerTask =
Timer("ListeningTimer", false).schedule(15000L, 15000L) {
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
if (playerNotificationService.currentPlayer.isPlaying) { if (playerNotificationService.currentPlayer.isPlaying) {
// Set auto sleep timer if enabled and within start/end time // Set auto sleep timer if enabled and within start/end time
playerNotificationService.sleepTimerManager.checkAutoSleepTimer() playerNotificationService.sleepTimerManager.checkAutoSleepTimer()
// Only sync with server on unmetered connection every 15s OR sync with server if last sync time is >= 60s // Only sync with server on unmetered connection every 15s OR sync with server if
val shouldSyncServer = PlayerNotificationService.isUnmeteredNetwork || System.currentTimeMillis() - lastSyncTime >= METERED_CONNECTION_SYNC_INTERVAL // last sync time is >= 60s
val shouldSyncServer =
PlayerNotificationService.isUnmeteredNetwork ||
System.currentTimeMillis() - lastSyncTime >=
METERED_CONNECTION_SYNC_INTERVAL
val currentTime = playerNotificationService.getCurrentTimeSeconds() val currentTime = playerNotificationService.getCurrentTimeSeconds()
if (currentTime > 0) { if (currentTime > 0) {
@ -88,14 +103,14 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi
} }
} }
fun play(playbackSession:PlaybackSession) { fun play(playbackSession: PlaybackSession) {
Log.d(tag, "play ${playbackSession.displayTitle}") Log.d(tag, "play ${playbackSession.displayTitle}")
MediaEventManager.playEvent(playbackSession) MediaEventManager.playEvent(playbackSession)
start(playbackSession) start(playbackSession)
} }
fun stop(shouldSync:Boolean? = true, cb: () -> Unit) { fun stop(shouldSync: Boolean? = true, cb: () -> Unit) {
if (!listeningTimerRunning) { if (!listeningTimerRunning) {
reset() reset()
return cb() return cb()
@ -106,7 +121,8 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi
listeningTimerRunning = false listeningTimerRunning = false
Log.d(tag, "stop: Stopping listening for $currentDisplayTitle") Log.d(tag, "stop: Stopping listening for $currentDisplayTitle")
val currentTime = if (shouldSync == true) playerNotificationService.getCurrentTimeSeconds() else 0.0 val currentTime =
if (shouldSync == true) playerNotificationService.getCurrentTimeSeconds() else 0.0
if (currentTime > 0) { // Current time should always be > 0 on stop if (currentTime > 0) { // Current time should always be > 0 on stop
sync(true, currentTime) { syncResult -> sync(true, currentTime) { syncResult ->
currentPlaybackSession?.let { playbackSession -> currentPlaybackSession?.let { playbackSession ->
@ -197,12 +213,15 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi
it.updatedAt = mediaProgress.lastUpdate it.updatedAt = mediaProgress.lastUpdate
it.currentTime = mediaProgress.currentTime it.currentTime = mediaProgress.currentTime
MediaEventManager.syncEvent(mediaProgress, "Received from server get media progress request while playback session open") MediaEventManager.syncEvent(
mediaProgress,
"Received from server get media progress request while playback session open"
)
saveLocalProgress(it) saveLocalProgress(it)
} }
} }
fun sync(shouldSyncServer:Boolean, currentTime:Double, cb: (SyncResult?) -> Unit) { fun sync(shouldSyncServer: Boolean, currentTime: Double, cb: (SyncResult?) -> Unit) {
if (lastSyncTime <= 0) { if (lastSyncTime <= 0) {
Log.e(tag, "Last sync time is not set $lastSyncTime") Log.e(tag, "Last sync time is not set $lastSyncTime")
return cb(null) return cb(null)
@ -214,11 +233,14 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi
} }
val listeningTimeToAdd = diffSinceLastSync / 1000L val listeningTimeToAdd = diffSinceLastSync / 1000L
val syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime) val syncData = MediaProgressSyncData(listeningTimeToAdd, currentPlaybackDuration, currentTime)
currentPlaybackSession?.syncData(syncData) currentPlaybackSession?.syncData(syncData)
if (currentPlaybackSession?.progress?.isNaN() == true) { if (currentPlaybackSession?.progress?.isNaN() == true) {
Log.e(tag, "Current Playback Session invalid progress ${currentPlaybackSession?.progress} | Current Time: ${currentPlaybackSession?.currentTime} | Duration: ${currentPlaybackSession?.getTotalDuration()}") Log.e(
tag,
"Current Playback Session invalid progress ${currentPlaybackSession?.progress} | Current Time: ${currentPlaybackSession?.currentTime} | Duration: ${currentPlaybackSession?.getTotalDuration()}"
)
return cb(null) return cb(null)
} }
@ -238,11 +260,20 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi
saveLocalProgress(it) saveLocalProgress(it)
lastSyncTime = System.currentTimeMillis() lastSyncTime = System.currentTimeMillis()
Log.d(tag, "Sync local device current serverConnectionConfigId=${DeviceManager.serverConnectionConfig?.id}") Log.d(
tag,
"Sync local device current serverConnectionConfigId=${DeviceManager.serverConnectionConfig?.id}"
)
// Local library item is linked to a server library item // Local library item is linked to a server library item
// Send sync to server also if connected to this server and local item belongs to this server // Send sync to server also if connected to this server and local item belongs to this
if (hasNetworkConnection && shouldSyncServer && !it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) { // server
if (hasNetworkConnection &&
shouldSyncServer &&
!it.libraryItemId.isNullOrEmpty() &&
it.serverConnectionConfigId != null &&
DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId
) {
apiHandler.sendLocalProgressSync(it) { syncSuccess, errorMsg -> apiHandler.sendLocalProgressSync(it) { syncSuccess, errorMsg ->
if (syncSuccess) { if (syncSuccess) {
failedSyncs = 0 failedSyncs = 0
@ -254,7 +285,10 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi
playerNotificationService.alertSyncFailing() // Show alert in client playerNotificationService.alertSyncFailing() // Show alert in client
failedSyncs = 0 failedSyncs = 0
} }
Log.e(tag, "Local Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime with session id=${it.id}") Log.e(
tag,
"Local Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime with session id=${it.id}"
)
} }
cb(SyncResult(true, syncSuccess, errorMsg)) cb(SyncResult(true, syncSuccess, errorMsg))
@ -278,7 +312,10 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi
playerNotificationService.alertSyncFailing() // Show alert in client playerNotificationService.alertSyncFailing() // Show alert in client
failedSyncs = 0 failedSyncs = 0
} }
Log.e(tag, "Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime with session id=${currentSessionId}") Log.e(
tag,
"Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime with session id=${currentSessionId}"
)
} }
cb(SyncResult(true, syncSuccess, errorMsg)) cb(SyncResult(true, syncSuccess, errorMsg))
} }
@ -287,9 +324,10 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi
} }
} }
private fun saveLocalProgress(playbackSession:PlaybackSession) { private fun saveLocalProgress(playbackSession: PlaybackSession) {
if (currentLocalMediaProgress == null) { if (currentLocalMediaProgress == null) {
val mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(playbackSession.localMediaProgressId) val mediaProgress =
DeviceManager.dbManager.getLocalMediaProgress(playbackSession.localMediaProgressId)
if (mediaProgress == null) { if (mediaProgress == null) {
currentLocalMediaProgress = playbackSession.getNewLocalMediaProgress() currentLocalMediaProgress = playbackSession.getNewLocalMediaProgress()
} else { } else {
@ -306,12 +344,14 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi
} else { } else {
DeviceManager.dbManager.saveLocalMediaProgress(it) DeviceManager.dbManager.saveLocalMediaProgress(it)
playerNotificationService.clientEventEmitter?.onLocalMediaProgressUpdate(it) playerNotificationService.clientEventEmitter?.onLocalMediaProgressUpdate(it)
Log.d(tag, "Saved Local Progress Current Time: ID ${it.id} | ${it.currentTime} | Duration ${it.duration} | Progress ${it.progressPercent}%") Log.d(
tag,
"Saved Local Progress Current Time: ID ${it.id} | ${it.currentTime} | Duration ${it.duration} | Progress ${it.progressPercent}%"
)
} }
} }
} }
fun reset() { fun reset() {
currentPlaybackSession = null currentPlaybackSession = null
currentLocalMediaProgress = null currentLocalMediaProgress = null