diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/AudioProbeResult.kt b/android/app/src/main/java/com/audiobookshelf/app/data/AudioProbeResult.kt deleted file mode 100644 index 7e3ed63b..00000000 --- a/android/app/src/main/java/com/audiobookshelf/app/data/AudioProbeResult.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.audiobookshelf.app.data - -import com.fasterxml.jackson.annotation.JsonIgnore -import com.fasterxml.jackson.annotation.JsonIgnoreProperties - -@JsonIgnoreProperties(ignoreUnknown = true) -data class AudioProbeStream( - val index:Int, - val codec_name:String, - val codec_long_name:String, - val channels:Int, - val channel_layout:String, - val duration:Double, - val bit_rate:Double -) - -@JsonIgnoreProperties(ignoreUnknown = true) -data class AudioProbeChapterTags( - val title:String -) - -@JsonIgnoreProperties(ignoreUnknown = true) -data class AudioProbeChapter( - val id:Int, - val start:Long, - val end:Long, - val tags:AudioProbeChapterTags? -) { - @JsonIgnore - fun getBookChapter():BookChapter { - val startS = start / 1000.0 - val endS = end / 1000.0 - val title = tags?.title ?: "Chapter $id" - return BookChapter(id, startS, endS, title) - } -} - -@JsonIgnoreProperties(ignoreUnknown = true) -data class AudioProbeFormatTags( - val artist:String?, - val album:String?, - val comment:String?, - val date:String?, - val genre:String?, - val title:String? -) - -@JsonIgnoreProperties(ignoreUnknown = true) -data class AudioProbeFormat( - val filename:String, - val format_name:String, - val duration:Double, - val size:Long, - val bit_rate:Double, - val tags:AudioProbeFormatTags? -) - -@JsonIgnoreProperties(ignoreUnknown = true) -class AudioProbeResult ( - val streams:MutableList, - val chapters:MutableList, - val format:AudioProbeFormat) { - - val duration get() = format.duration - val size get() = format.size - val title get() = format.tags?.title ?: format.filename.split("/").last() - val artist get() = format.tags?.artist ?: "" - - @JsonIgnore - fun getBookChapters(): List { - if (chapters.isEmpty()) return mutableListOf() - return chapters.map { it.getBookChapter() } - } -} diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/AudioTrack.kt b/android/app/src/main/java/com/audiobookshelf/app/data/AudioTrack.kt index ca83aeac..f77adbf6 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/AudioTrack.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/AudioTrack.kt @@ -5,30 +5,33 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties @JsonIgnoreProperties(ignoreUnknown = true) data class AudioTrack( - var index:Int, - var startOffset:Double, - var duration:Double, - var title:String, - var contentUrl:String, - var mimeType:String, - var metadata:FileMetadata?, - var isLocal:Boolean, - var localFileId:String?, - var audioProbeResult:AudioProbeResult?, - var serverIndex:Int? // Need to know if server track index is different + var index: Int, + var startOffset: Double, + var duration: Double, + var title: String, + var contentUrl: String, + var mimeType: String, + var metadata: FileMetadata?, + var isLocal: Boolean, + var localFileId: String?, + var serverIndex: Int? // Need to know if server track index is different ) { @get:JsonIgnore - val startOffsetMs get() = (startOffset * 1000L).toLong() + val startOffsetMs + get() = (startOffset * 1000L).toLong() @get:JsonIgnore - val durationMs get() = (duration * 1000L).toLong() + val durationMs + get() = (duration * 1000L).toLong() @get:JsonIgnore - val endOffsetMs get() = startOffsetMs + durationMs + val endOffsetMs + get() = startOffsetMs + durationMs @get:JsonIgnore - val relPath get() = metadata?.relPath ?: "" + val relPath + get() = metadata?.relPath ?: "" @JsonIgnore - fun getBookChapter():BookChapter { - return BookChapter(index + 1,startOffset, startOffset + duration, title) + fun getBookChapter(): BookChapter { + return BookChapter(index + 1, startOffset, startOffset + duration, title) } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaItem.kt b/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaItem.kt index aa838046..987e5c88 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaItem.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LocalMediaItem.kt @@ -4,70 +4,65 @@ import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnoreProperties /* - Used as a helper class to generate LocalLibraryItem from scan results - */ + Used as a helper class to generate LocalLibraryItem from scan results +*/ @JsonIgnoreProperties(ignoreUnknown = true) data class LocalMediaItem( - var id:String, - var name: String, - var mediaType:String, - var folderId:String, - var contentUrl:String, - var simplePath: String, - var basePath:String, - var absolutePath:String, - var audioTracks:MutableList, - var ebookFile:EBookFile?, - var localFiles:MutableList, - var coverContentUrl:String?, - var coverAbsolutePath:String? + var id: String, + var name: String, + var mediaType: String, + var folderId: String, + var contentUrl: String, + var simplePath: String, + var basePath: String, + var absolutePath: String, + var audioTracks: MutableList, + var ebookFile: EBookFile?, + var localFiles: MutableList, + var coverContentUrl: String?, + var coverAbsolutePath: String? ) { @JsonIgnore - fun getDuration():Double { + fun getDuration(): Double { var total = 0.0 - audioTracks.forEach{ total += it.duration } + audioTracks.forEach { total += it.duration } return total } @JsonIgnore - fun getTotalSize():Long { + fun getTotalSize(): Long { var total = 0L localFiles.forEach { total += it.size } return total } @JsonIgnore - fun getMediaMetadata():MediaTypeMetadata { + 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, 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) - } - } - - @JsonIgnore - fun getAudiobookChapters():List { - if (mediaType != "book" || audioTracks.isEmpty()) return mutableListOf() - if (audioTracks.size == 1) { // Single track audiobook look for chapters from ffprobe - return audioTracks[0].audioProbeResult?.getBookChapters() ?: mutableListOf() - } - // Multi-track make chapters from tracks - return audioTracks.map { it.getBookChapter() } - } - - @JsonIgnore - fun getLocalLibraryItem():LocalLibraryItem { - val mediaMetadata = getMediaMetadata() - if (mediaType == "book") { - val chapters = getAudiobookChapters() - val book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,ebookFile,getTotalSize(),getDuration(),audioTracks.size) - return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true,null,null,null,null) - } else { - val podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false, 0) - podcast.setAudioTracks(audioTracks) // Builds episodes from audio tracks - return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false, mediaType, podcast,localFiles,coverContentUrl, coverAbsolutePath, true, null,null,null,null) + PodcastMetadata(name, null, null, mutableListOf(), false) } } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/MediaItemHistory.kt b/android/app/src/main/java/com/audiobookshelf/app/data/MediaItemHistory.kt index 5968cb91..7b1d094b 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/MediaItemHistory.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/MediaItemHistory.kt @@ -4,14 +4,14 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties @JsonIgnoreProperties(ignoreUnknown = true) class MediaItemHistory( - var id: String, // media id - var mediaDisplayTitle: String, - var libraryItemId: String, - var episodeId: String?, - var isLocal:Boolean, - var serverConnectionConfigId:String?, - var serverAddress:String?, - var serverUserId:String?, - var createdAt: Long, - var events:MutableList, + var id: String, // media id + var mediaDisplayTitle: String, + var libraryItemId: String, + var episodeId: String?, + var isLocal: Boolean, + var serverConnectionConfigId: String?, + var serverAddress: String?, + var serverUserId: String?, + var createdAt: Long, + var events: MutableList, ) 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 c7c2a120..cde9318e 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 @@ -12,6 +12,7 @@ import com.audiobookshelf.app.BuildConfig import com.audiobookshelf.app.R import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.media.MediaProgressSyncData +import com.audiobookshelf.app.player.* import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.google.android.exoplayer2.MediaItem @@ -19,61 +20,70 @@ import com.google.android.exoplayer2.MediaMetadata import com.google.android.gms.cast.MediaInfo import com.google.android.gms.cast.MediaQueueItem import com.google.android.gms.common.images.WebImage -import com.audiobookshelf.app.player.* @JsonIgnoreProperties(ignoreUnknown = true) class PlaybackSession( - var id:String, - var userId:String?, - var libraryItemId:String?, - var episodeId:String?, - var mediaType:String, - var mediaMetadata:MediaTypeMetadata, - var deviceInfo:DeviceInfo, - var chapters:List, - var displayTitle: String?, - var displayAuthor: String?, - var coverPath:String?, - var duration:Double, - var playMethod:Int, - var startedAt:Long, - var updatedAt:Long, - var timeListening:Long, - var audioTracks:MutableList, - var currentTime:Double, - var libraryItem:LibraryItem?, - var localLibraryItem:LocalLibraryItem?, - var localEpisodeId:String?, - var serverConnectionConfigId:String?, - var serverAddress:String?, - var mediaPlayer:String? + var id: String, + var userId: String?, + var libraryItemId: String?, + var episodeId: String?, + var mediaType: String, + var mediaMetadata: MediaTypeMetadata, + var deviceInfo: DeviceInfo, + var chapters: List, + var displayTitle: String?, + var displayAuthor: String?, + var coverPath: String?, + var duration: Double, + var playMethod: Int, + var startedAt: Long, + var updatedAt: Long, + var timeListening: Long, + var audioTracks: MutableList, + var currentTime: Double, + var libraryItem: LibraryItem?, + var localLibraryItem: LocalLibraryItem?, + var localEpisodeId: String?, + var serverConnectionConfigId: String?, + var serverAddress: String?, + var mediaPlayer: String? ) { @get:JsonIgnore - val isHLS get() = playMethod == PLAYMETHOD_TRANSCODE + val isHLS + get() = playMethod == PLAYMETHOD_TRANSCODE @get:JsonIgnore - val isDirectPlay get() = playMethod == PLAYMETHOD_DIRECTPLAY + val isDirectPlay + get() = playMethod == PLAYMETHOD_DIRECTPLAY @get:JsonIgnore - val isLocal get() = playMethod == PLAYMETHOD_LOCAL + val isLocal + get() = playMethod == PLAYMETHOD_LOCAL @get:JsonIgnore - val isPodcastEpisode get() = mediaType == "podcast" + val isPodcastEpisode + get() = mediaType == "podcast" @get:JsonIgnore - val currentTimeMs get() = (currentTime * 1000L).toLong() + val currentTimeMs + get() = (currentTime * 1000L).toLong() @get:JsonIgnore - val totalDurationMs get() = (getTotalDuration() * 1000L).toLong() + val totalDurationMs + get() = (getTotalDuration() * 1000L).toLong() @get:JsonIgnore - val localLibraryItemId get() = localLibraryItem?.id ?: "" + val localLibraryItemId + get() = localLibraryItem?.id ?: "" @get:JsonIgnore - val localMediaProgressId get() = if (localEpisodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId" + val localMediaProgressId + get() = + if (localEpisodeId.isNullOrEmpty()) localLibraryItemId + else "$localLibraryItemId-$localEpisodeId" @get:JsonIgnore - val progress get() = currentTime / getTotalDuration() + val progress + get() = currentTime / getTotalDuration() @get:JsonIgnore - val isLocalLibraryItemOnly get() = localLibraryItemId != "" && libraryItemId == null - @get:JsonIgnore - val mediaItemId get() = if (isLocalLibraryItemOnly) localMediaProgressId else if (episodeId.isNullOrEmpty()) libraryItemId ?: "" else "$libraryItemId-$episodeId" + val mediaItemId + get() = if (episodeId.isNullOrEmpty()) libraryItemId ?: "" else "$libraryItemId-$episodeId" @JsonIgnore - fun getCurrentTrackIndex():Int { + fun getCurrentTrackIndex(): Int { for (i in 0 until audioTracks.size) { val track = audioTracks[i] if (currentTimeMs >= track.startOffsetMs && (track.endOffsetMs > currentTimeMs)) { @@ -84,7 +94,7 @@ class PlaybackSession( } @JsonIgnore - fun getNextTrackIndex():Int { + fun getNextTrackIndex(): Int { for (i in 0 until audioTracks.size) { val track = audioTracks[i] if (currentTimeMs < track.startOffsetMs) { @@ -95,67 +105,74 @@ class PlaybackSession( } @JsonIgnore - fun getChapterForTime(time:Long):BookChapter? { + fun getChapterForTime(time: Long): BookChapter? { if (chapters.isEmpty()) return null - return chapters.find { time >= it.startMs && it.endMs > time} + return chapters.find { time >= it.startMs && it.endMs > time } } @JsonIgnore - fun getCurrentTrackEndTime():Long { + fun getCurrentTrackEndTime(): Long { val currentTrack = audioTracks[this.getCurrentTrackIndex()] return currentTrack.startOffsetMs + currentTrack.durationMs } @JsonIgnore - fun getNextChapterForTime(time:Long):BookChapter? { + fun getNextChapterForTime(time: Long): BookChapter? { if (chapters.isEmpty()) return null return chapters.find { time < it.startMs } // First chapter where start time is > then time } @JsonIgnore - fun getNextTrackEndTime():Long { + fun getNextTrackEndTime(): Long { val currentTrack = audioTracks[this.getNextTrackIndex()] return currentTrack.startOffsetMs + currentTrack.durationMs } @JsonIgnore - fun getCurrentTrackTimeMs():Long { + fun getCurrentTrackTimeMs(): Long { val currentTrack = audioTracks[this.getCurrentTrackIndex()] val time = currentTime - currentTrack.startOffset return (time * 1000L).toLong() } @JsonIgnore - fun getTrackStartOffsetMs(index:Int):Long { + fun getTrackStartOffsetMs(index: Int): Long { if (index < 0 || index >= audioTracks.size) return 0L val currentTrack = audioTracks[index] return (currentTrack.startOffset * 1000L).toLong() } @JsonIgnore - fun getTotalDuration():Double { + fun getTotalDuration(): Double { var total = 0.0 audioTracks.forEach { total += it.duration } return total } @JsonIgnore - fun getCoverUri(ctx:Context): Uri { + fun getCoverUri(ctx: Context): Uri { if (localLibraryItem?.coverContentUrl != null) { var coverUri = Uri.parse(localLibraryItem?.coverContentUrl.toString()) 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}") } @JsonIgnore - fun getContentUri(audioTrack:AudioTrack): Uri { + fun getContentUri(audioTrack: AudioTrack): Uri { if (isLocal) return Uri.parse(audioTrack.contentUrl) // Local content url return Uri.parse("$serverAddress${audioTrack.contentUrl}?token=${DeviceManager.token}") } @@ -164,28 +181,34 @@ class PlaybackSession( fun getMediaMetadataCompat(ctx: Context): MediaMetadataCompat { val coverUri = getCoverUri(ctx) - val metadataBuilder = MediaMetadataCompat.Builder() - .putString(MediaMetadataCompat.METADATA_KEY_TITLE, displayTitle) - .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, displayTitle) - .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, displayAuthor) - .putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, displayAuthor) - .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, displayAuthor) - .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, displayAuthor) - .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, displayAuthor) - .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, displayAuthor) - .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id) - .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, coverUri.toString()) - .putString(MediaMetadataCompat.METADATA_KEY_ART_URI, coverUri.toString()) - .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString()) + val metadataBuilder = + MediaMetadataCompat.Builder() + .putString(MediaMetadataCompat.METADATA_KEY_TITLE, displayTitle) + .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, displayTitle) + .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, displayAuthor) + .putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, displayAuthor) + .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, displayAuthor) + .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, displayAuthor) + .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, displayAuthor) + .putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, displayAuthor) + .putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id) + .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, coverUri.toString()) + .putString(MediaMetadataCompat.METADATA_KEY_ART_URI, coverUri.toString()) + .putString( + MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, + coverUri.toString() + ) // Local covers get bitmap if (localLibraryItem?.coverContentUrl != null) { - val bitmap = if (Build.VERSION.SDK_INT < 28) { - MediaStore.Images.Media.getBitmap(ctx.contentResolver, coverUri) - } else { - val source: ImageDecoder.Source = ImageDecoder.createSource(ctx.contentResolver, coverUri) - ImageDecoder.decodeBitmap(source) - } + val bitmap = + if (Build.VERSION.SDK_INT < 28) { + MediaStore.Images.Media.getBitmap(ctx.contentResolver, coverUri) + } else { + val source: ImageDecoder.Source = + ImageDecoder.createSource(ctx.contentResolver, coverUri) + ImageDecoder.decodeBitmap(source) + } metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap) metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, bitmap) } @@ -194,26 +217,27 @@ class PlaybackSession( } @JsonIgnore - fun getExoMediaMetadata(ctx:Context): MediaMetadata { + fun getExoMediaMetadata(ctx: Context): MediaMetadata { val coverUri = getCoverUri(ctx) - val metadataBuilder = MediaMetadata.Builder() - .setTitle(displayTitle) - .setDisplayTitle(displayTitle) - .setArtist(displayAuthor) - .setAlbumArtist(displayAuthor) - .setSubtitle(displayAuthor) - .setAlbumTitle(displayAuthor) - .setDescription(displayAuthor) - .setArtworkUri(coverUri) - .setMediaType(MediaMetadata.MEDIA_TYPE_AUDIO_BOOK) + val metadataBuilder = + MediaMetadata.Builder() + .setTitle(displayTitle) + .setDisplayTitle(displayTitle) + .setArtist(displayAuthor) + .setAlbumArtist(displayAuthor) + .setSubtitle(displayAuthor) + .setAlbumTitle(displayAuthor) + .setDescription(displayAuthor) + .setArtworkUri(coverUri) + .setMediaType(MediaMetadata.MEDIA_TYPE_AUDIO_BOOK) return metadataBuilder.build() } @JsonIgnore - fun getMediaItems(ctx:Context):List { - val mediaItems:MutableList = mutableListOf() + fun getMediaItems(ctx: Context): List { + val mediaItems: MutableList = mutableListOf() for (audioTrack in audioTracks) { val mediaMetadata = this.getExoMediaMetadata(ctx) @@ -221,50 +245,105 @@ class PlaybackSession( val mimeType = audioTrack.mimeType 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) } return mediaItems } @JsonIgnore - 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) + 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 + ) 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_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_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) + castMetadata.putInt( + com.google.android.gms.cast.MediaMetadata.KEY_TRACK_NUMBER, + audioTrack.index + ) return castMetadata } @JsonIgnore - fun getQueueItem(audioTrack:AudioTrack):MediaQueueItem { + fun getQueueItem(audioTrack: AudioTrack): MediaQueueItem { val castMetadata = getCastMediaMetadata(audioTrack) val mediaUri = getContentUri(audioTrack) - val mediaInfo = MediaInfo.Builder(mediaUri.toString()).apply { - setContentUrl(mediaUri.toString()) - setContentType(audioTrack.mimeType) - setMetadata(castMetadata) - setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) - }.build() + val mediaInfo = + MediaInfo.Builder(mediaUri.toString()) + .apply { + setContentUrl(mediaUri.toString()) + setContentType(audioTrack.mimeType) + setMetadata(castMetadata) + setStreamType(MediaInfo.STREAM_TYPE_BUFFERED) + } + .build() - return MediaQueueItem.Builder(mediaInfo).apply { - setPlaybackDuration(audioTrack.duration) - }.build() + return MediaQueueItem.Builder(mediaInfo) + .apply { setPlaybackDuration(audioTrack.duration) } + .build() } @JsonIgnore - 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) + 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 + ) } @JsonIgnore @@ -275,7 +354,25 @@ class PlaybackSession( } @JsonIgnore - fun getNewLocalMediaProgress():LocalMediaProgress { - return LocalMediaProgress(localMediaProgressId,localLibraryItemId,localEpisodeId,getTotalDuration(),progress,currentTime,false,null,null,updatedAt,startedAt,null,serverConnectionConfigId,serverAddress,userId,libraryItemId,episodeId) + fun getNewLocalMediaProgress(): LocalMediaProgress { + return LocalMediaProgress( + localMediaProgressId, + localLibraryItemId, + localEpisodeId, + getTotalDuration(), + progress, + currentTime, + false, + null, + null, + updatedAt, + startedAt, + null, + serverConnectionConfigId, + serverAddress, + userId, + libraryItemId, + episodeId + ) } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt b/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt index be45e425..951b6a3b 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt @@ -13,83 +13,144 @@ import java.io.File class FolderScanner(var ctx: Context) { private val tag = "FolderScanner" - private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) + private var jacksonMapper = + jacksonObjectMapper() + .enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) - data class DownloadItemScanResult(val localLibraryItem:LocalLibraryItem, var localMediaProgress:LocalMediaProgress?) + data class DownloadItemScanResult( + val localLibraryItem: LocalLibraryItem, + var localMediaProgress: LocalMediaProgress? + ) - private fun getLocalLibraryItemId(mediaItemId:String):String { + private fun getLocalLibraryItemId(mediaItemId: String): String { return "local_" + DeviceManager.getBase64Id(mediaItemId) } - private fun scanInternalDownloadItem(downloadItem:DownloadItem, cb: (DownloadItemScanResult?) -> Unit) { + private fun scanInternalDownloadItem( + downloadItem: DownloadItem, + cb: (DownloadItemScanResult?) -> Unit + ) { val localLibraryItemId = "local_${downloadItem.libraryItemId}" - var localEpisodeId:String? = null - var localLibraryItem:LocalLibraryItem? + var localEpisodeId: String? = null + var localLibraryItem: LocalLibraryItem? if (downloadItem.mediaType == "book") { - localLibraryItem = LocalLibraryItem(localLibraryItemId, downloadItem.localFolder.id, downloadItem.itemFolderPath, downloadItem.itemFolderPath, "", false, downloadItem.mediaType, downloadItem.media.getLocalCopy(), mutableListOf(), null, null, true, downloadItem.serverConnectionConfigId, downloadItem.serverAddress, downloadItem.serverUserId, downloadItem.libraryItemId) + localLibraryItem = + LocalLibraryItem( + localLibraryItemId, + downloadItem.localFolder.id, + downloadItem.itemFolderPath, + downloadItem.itemFolderPath, + "", + false, + downloadItem.mediaType, + downloadItem.media.getLocalCopy(), + mutableListOf(), + null, + null, + true, + downloadItem.serverConnectionConfigId, + downloadItem.serverAddress, + downloadItem.serverUserId, + downloadItem.libraryItemId + ) } else { // Lookup or create podcast local library item localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId) if (localLibraryItem == null) { - Log.d(tag, "[FolderScanner] Podcast local library item not created yet for ${downloadItem.media.metadata.title}") - localLibraryItem = LocalLibraryItem(localLibraryItemId, downloadItem.localFolder.id, downloadItem.itemFolderPath, downloadItem.itemFolderPath, "", false, downloadItem.mediaType, downloadItem.media.getLocalCopy(), mutableListOf(), null, null, true,downloadItem.serverConnectionConfigId,downloadItem.serverAddress,downloadItem.serverUserId,downloadItem.libraryItemId) + Log.d( + tag, + "[FolderScanner] Podcast local library item not created yet for ${downloadItem.media.metadata.title}" + ) + localLibraryItem = + LocalLibraryItem( + localLibraryItemId, + downloadItem.localFolder.id, + downloadItem.itemFolderPath, + downloadItem.itemFolderPath, + "", + false, + downloadItem.mediaType, + downloadItem.media.getLocalCopy(), + mutableListOf(), + null, + null, + true, + downloadItem.serverConnectionConfigId, + downloadItem.serverAddress, + downloadItem.serverUserId, + downloadItem.libraryItemId + ) } } - val audioTracks:MutableList = mutableListOf() + val audioTracks: MutableList = mutableListOf() var foundEBookFile = false downloadItem.downloadItemParts.forEach { downloadItemPart -> - Log.d(tag, "Scan internal storage item with finalDestinationUri=${downloadItemPart.finalDestinationUri}") + Log.d( + tag, + "Scan internal storage item with finalDestinationUri=${downloadItemPart.finalDestinationUri}" + ) val file = File(downloadItemPart.finalDestinationPath) Log.d(tag, "Scan internal storage item created file ${file.name}") if (file == null) { - Log.e(tag, "scanInternalDownloadItem: Null docFile for path ${downloadItemPart.finalDestinationPath}") + Log.e( + tag, + "scanInternalDownloadItem: Null docFile for path ${downloadItemPart.finalDestinationPath}" + ) } else { if (downloadItemPart.audioTrack != null) { val audioTrackFromServer = downloadItemPart.audioTrack Log.d( - tag, - "scanInternalDownloadItem: Audio Track from Server index = ${audioTrackFromServer.index}" + tag, + "scanInternalDownloadItem: Audio Track from Server index = ${audioTrackFromServer.index}" ) val localFileId = DeviceManager.getBase64Id(file.name) Log.d(tag, "Scan internal file localFileId=$localFileId") - val localFile = LocalFile( - localFileId, - file.name, - downloadItemPart.finalDestinationUri.toString(), - file.getBasePath(ctx), - file.absolutePath, - file.getSimplePath(ctx), - file.mimeType, - file.length() - ) + val localFile = + LocalFile( + localFileId, + file.name, + downloadItemPart.finalDestinationUri.toString(), + file.getBasePath(ctx), + file.absolutePath, + file.getSimplePath(ctx), + file.mimeType, + file.length() + ) localLibraryItem.localFiles.add(localFile) - val trackFileMetadata = FileMetadata(file.name, file.extension, file.absolutePath, file.getBasePath(ctx), file.length()) + val trackFileMetadata = + FileMetadata( + file.name, + file.extension, + file.absolutePath, + file.getBasePath(ctx), + file.length() + ) // Create new audio track - val track = AudioTrack( - audioTrackFromServer.index, - audioTrackFromServer.startOffset, - audioTrackFromServer.duration, - localFile.filename ?: "", - localFile.contentUrl, - localFile.mimeType ?: "", - trackFileMetadata, - true, - localFileId, - null, - audioTrackFromServer.index - ) + val track = + AudioTrack( + audioTrackFromServer.index, + audioTrackFromServer.startOffset, + audioTrackFromServer.duration, + localFile.filename ?: "", + localFile.contentUrl, + localFile.mimeType ?: "", + trackFileMetadata, + true, + localFileId, + audioTrackFromServer.index + ) audioTracks.add(track) Log.d( - tag, - "scanInternalDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}" + tag, + "scanInternalDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}" ) // Add podcast episodes to library @@ -98,40 +159,51 @@ class FolderScanner(var ctx: Context) { val newEpisode = podcast.addEpisode(track, podcastEpisode) localEpisodeId = newEpisode.id Log.d( - tag, - "scanInternalDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}" + tag, + "scanInternalDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}" ) } - } else if (downloadItemPart.ebookFile != null) { foundEBookFile = true Log.d(tag, "scanInternalDownloadItem: Ebook file found with mimetype=${file.mimeType}") val localFileId = DeviceManager.getBase64Id(file.name) - val localFile = LocalFile( - localFileId, - file.name, - Uri.fromFile(file).toString(), - file.getBasePath(ctx), - file.absolutePath, - file.getSimplePath(ctx), - file.mimeType, - file.length() - ) + val localFile = + LocalFile( + localFileId, + file.name, + Uri.fromFile(file).toString(), + file.getBasePath(ctx), + file.absolutePath, + file.getSimplePath(ctx), + file.mimeType, + file.length() + ) localLibraryItem.localFiles.add(localFile) - val ebookFile = EBookFile( - downloadItemPart.ebookFile.ino, - downloadItemPart.ebookFile.metadata, - downloadItemPart.ebookFile.ebookFormat, - true, - localFileId, - localFile.contentUrl - ) + val ebookFile = + EBookFile( + downloadItemPart.ebookFile.ino, + downloadItemPart.ebookFile.metadata, + downloadItemPart.ebookFile.ebookFormat, + true, + localFileId, + localFile.contentUrl + ) (localLibraryItem.media as Book).ebookFile = ebookFile Log.d(tag, "scanInternalDownloadItem: Ebook file added to lli ${localFile.contentUrl}") } else { val localFileId = DeviceManager.getBase64Id(file.name) - val localFile = LocalFile(localFileId,file.name,Uri.fromFile(file).toString(),file.getBasePath(ctx),file.absolutePath,file.getSimplePath(ctx),file.mimeType,file.length()) + val localFile = + LocalFile( + localFileId, + file.name, + Uri.fromFile(file).toString(), + file.getBasePath(ctx), + file.absolutePath, + file.getSimplePath(ctx), + file.mimeType, + file.length() + ) localLibraryItem.coverAbsolutePath = localFile.absolutePath localLibraryItem.coverContentUrl = localFile.contentUrl @@ -141,7 +213,10 @@ class FolderScanner(var ctx: Context) { } if (audioTracks.isEmpty() && !foundEBookFile) { - Log.d(tag, "scanDownloadItem did not find any audio tracks or ebook file in folder for ${downloadItem.itemFolderPath}") + Log.d( + tag, + "scanDownloadItem did not find any audio tracks or ebook file in folder for ${downloadItem.itemFolderPath}" + ) return cb(null) } @@ -163,30 +238,37 @@ class FolderScanner(var ctx: Context) { localLibraryItem.media.setAudioTracks(audioTracks) } - val downloadItemScanResult = DownloadItemScanResult(localLibraryItem,null) + val downloadItemScanResult = DownloadItemScanResult(localLibraryItem, null) // If library item had media progress then make local media progress and save downloadItem.userMediaProgress?.let { mediaProgress -> - val localMediaProgressId = if (downloadItem.episodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId" - val newLocalMediaProgress = LocalMediaProgress( - id = localMediaProgressId, - localLibraryItemId = localLibraryItemId, - localEpisodeId = localEpisodeId, - duration = mediaProgress.duration, - progress = mediaProgress.progress, - currentTime = mediaProgress.currentTime, - isFinished = mediaProgress.isFinished, - ebookLocation = mediaProgress.ebookLocation, - ebookProgress = mediaProgress.ebookProgress, - lastUpdate = mediaProgress.lastUpdate, - startedAt = mediaProgress.startedAt, - finishedAt = mediaProgress.finishedAt, - serverConnectionConfigId = downloadItem.serverConnectionConfigId, - serverAddress = downloadItem.serverAddress, - serverUserId = downloadItem.serverUserId, - libraryItemId = downloadItem.libraryItemId, - episodeId = downloadItem.episodeId) - Log.d(tag, "scanLibraryItemFolder: Saving local media progress ${newLocalMediaProgress.id} at progress ${newLocalMediaProgress.progress}") + val localMediaProgressId = + if (downloadItem.episodeId.isNullOrEmpty()) localLibraryItemId + else "$localLibraryItemId-$localEpisodeId" + val newLocalMediaProgress = + LocalMediaProgress( + id = localMediaProgressId, + localLibraryItemId = localLibraryItemId, + localEpisodeId = localEpisodeId, + duration = mediaProgress.duration, + progress = mediaProgress.progress, + currentTime = mediaProgress.currentTime, + isFinished = mediaProgress.isFinished, + ebookLocation = mediaProgress.ebookLocation, + ebookProgress = mediaProgress.ebookProgress, + lastUpdate = mediaProgress.lastUpdate, + startedAt = mediaProgress.startedAt, + finishedAt = mediaProgress.finishedAt, + serverConnectionConfigId = downloadItem.serverConnectionConfigId, + serverAddress = downloadItem.serverAddress, + serverUserId = downloadItem.serverUserId, + libraryItemId = downloadItem.libraryItemId, + episodeId = downloadItem.episodeId + ) + Log.d( + tag, + "scanLibraryItemFolder: Saving local media progress ${newLocalMediaProgress.id} at progress ${newLocalMediaProgress.progress}" + ) DeviceManager.dbManager.saveLocalMediaProgress(newLocalMediaProgress) downloadItemScanResult.localMediaProgress = newLocalMediaProgress @@ -206,7 +288,7 @@ class FolderScanner(var ctx: Context) { } val folderDf = DocumentFileCompat.fromUri(ctx, Uri.parse(downloadItem.localFolder.contentUrl)) - val foldersFound = folderDf?.search(true, DocumentFileType.FOLDER) ?: mutableListOf() + val foldersFound = folderDf?.search(true, DocumentFileType.FOLDER) ?: mutableListOf() var itemFolderId = "" var itemFolderUrl = "" @@ -235,72 +317,188 @@ class FolderScanner(var ctx: Context) { } val localLibraryItemId = getLocalLibraryItemId(itemFolderId) - Log.d(tag, "scanDownloadItem starting for ${downloadItem.itemFolderPath} | ${df.uri} | Item Folder Id:$itemFolderId | LLI Id:$localLibraryItemId") + Log.d( + tag, + "scanDownloadItem starting for ${downloadItem.itemFolderPath} | ${df.uri} | Item Folder Id:$itemFolderId | LLI Id:$localLibraryItemId" + ) // Search for files in media item folder // m4b files showing as mimeType application/octet-stream on Android 10 and earlier see #154 - val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/*")) + val filesFound = + df.search( + false, + DocumentFileType.FILE, + arrayOf("audio/*", "image/*", "video/mp4", "application/*") + ) Log.d(tag, "scanDownloadItem ${filesFound.size} files found in ${downloadItem.itemFolderPath}") - var localEpisodeId:String? = null - var localLibraryItem:LocalLibraryItem? + var localEpisodeId: String? = null + var localLibraryItem: LocalLibraryItem? if (downloadItem.mediaType == "book") { - localLibraryItem = LocalLibraryItem(localLibraryItemId, downloadItem.localFolder.id, itemFolderBasePath, itemFolderAbsolutePath, itemFolderUrl, false, downloadItem.mediaType, downloadItem.media.getLocalCopy(), mutableListOf(), null, null, true, downloadItem.serverConnectionConfigId, downloadItem.serverAddress, downloadItem.serverUserId, downloadItem.libraryItemId) + localLibraryItem = + LocalLibraryItem( + localLibraryItemId, + downloadItem.localFolder.id, + itemFolderBasePath, + itemFolderAbsolutePath, + itemFolderUrl, + false, + downloadItem.mediaType, + downloadItem.media.getLocalCopy(), + mutableListOf(), + null, + null, + true, + downloadItem.serverConnectionConfigId, + downloadItem.serverAddress, + downloadItem.serverUserId, + downloadItem.libraryItemId + ) } else { // Lookup or create podcast local library item localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId) if (localLibraryItem == null) { - Log.d(tag, "[FolderScanner] Podcast local library item not created yet for ${downloadItem.media.metadata.title}") - localLibraryItem = LocalLibraryItem(localLibraryItemId, downloadItem.localFolder.id, itemFolderBasePath, itemFolderAbsolutePath, itemFolderUrl, false, downloadItem.mediaType, downloadItem.media.getLocalCopy(), mutableListOf(), null, null, true,downloadItem.serverConnectionConfigId,downloadItem.serverAddress,downloadItem.serverUserId,downloadItem.libraryItemId) + Log.d( + tag, + "[FolderScanner] Podcast local library item not created yet for ${downloadItem.media.metadata.title}" + ) + localLibraryItem = + LocalLibraryItem( + localLibraryItemId, + downloadItem.localFolder.id, + itemFolderBasePath, + itemFolderAbsolutePath, + itemFolderUrl, + false, + downloadItem.mediaType, + downloadItem.media.getLocalCopy(), + mutableListOf(), + null, + null, + true, + downloadItem.serverConnectionConfigId, + downloadItem.serverAddress, + downloadItem.serverUserId, + downloadItem.libraryItemId + ) } } - val audioTracks:MutableList = mutableListOf() + val audioTracks: MutableList = mutableListOf() var foundEBookFile = false filesFound.forEach { docFile -> - val itemPart = downloadItem.downloadItemParts.find { itemPart -> - itemPart.filename == docFile.name - } + val itemPart = + downloadItem.downloadItemParts.find { itemPart -> itemPart.filename == docFile.name } if (itemPart == null) { - if (downloadItem.mediaType == "book") { // for books every download item should be a file found - Log.e(tag, "scanDownloadItem: Item part not found for doc file ${docFile.name} | ${docFile.getAbsolutePath(ctx)} | ${docFile.uri}") + if (downloadItem.mediaType == "book" + ) { // for books every download item should be a file found + Log.e( + tag, + "scanDownloadItem: Item part not found for doc file ${docFile.name} | ${docFile.getAbsolutePath(ctx)} | ${docFile.uri}" + ) } } else if (itemPart.audioTrack != null) { // Is audio track val audioTrackFromServer = itemPart.audioTrack - Log.d(tag, "scanDownloadItem: Audio Track from Server index = ${audioTrackFromServer.index}") + Log.d( + tag, + "scanDownloadItem: Audio Track from Server index = ${audioTrackFromServer.index}" + ) val localFileId = DeviceManager.getBase64Id(docFile.id) - val localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length()) + val localFile = + LocalFile( + localFileId, + docFile.name, + docFile.uri.toString(), + docFile.getBasePath(ctx), + docFile.getAbsolutePath(ctx), + docFile.getSimplePath(ctx), + docFile.mimeType, + docFile.length() + ) localLibraryItem.localFiles.add(localFile) // Create new audio track - val trackFileMetadata = FileMetadata(docFile.name ?: "", docFile.extension ?: "", docFile.getAbsolutePath(ctx), docFile.getBasePath(ctx), docFile.length()) - val track = AudioTrack(audioTrackFromServer.index, audioTrackFromServer.startOffset, audioTrackFromServer.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", trackFileMetadata, true, localFileId, null, audioTrackFromServer.index) + val trackFileMetadata = + FileMetadata( + docFile.name ?: "", + docFile.extension ?: "", + docFile.getAbsolutePath(ctx), + docFile.getBasePath(ctx), + docFile.length() + ) + val track = + AudioTrack( + audioTrackFromServer.index, + audioTrackFromServer.startOffset, + audioTrackFromServer.duration, + localFile.filename ?: "", + localFile.contentUrl, + localFile.mimeType ?: "", + trackFileMetadata, + true, + localFileId, + audioTrackFromServer.index + ) audioTracks.add(track) - Log.d(tag, "scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}") + Log.d( + tag, + "scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}" + ) // Add podcast episodes to library itemPart.episode?.let { podcastEpisode -> val podcast = localLibraryItem.media as Podcast val newEpisode = podcast.addEpisode(track, podcastEpisode) localEpisodeId = newEpisode.id - Log.d(tag, "scanDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}") + Log.d( + tag, + "scanDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}" + ) } } else if (itemPart.ebookFile != null) { // Ebook foundEBookFile = true Log.d(tag, "scanDownloadItem: Ebook file found with mimetype=${docFile.mimeType}") val localFileId = DeviceManager.getBase64Id(docFile.id) - val localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length()) + val localFile = + LocalFile( + localFileId, + docFile.name, + docFile.uri.toString(), + docFile.getBasePath(ctx), + docFile.getAbsolutePath(ctx), + docFile.getSimplePath(ctx), + docFile.mimeType, + docFile.length() + ) localLibraryItem.localFiles.add(localFile) - val ebookFile = EBookFile(itemPart.ebookFile.ino, itemPart.ebookFile.metadata, itemPart.ebookFile.ebookFormat, true, localFileId, localFile.contentUrl) + val ebookFile = + EBookFile( + itemPart.ebookFile.ino, + itemPart.ebookFile.metadata, + itemPart.ebookFile.ebookFormat, + true, + localFileId, + localFile.contentUrl + ) (localLibraryItem.media as Book).ebookFile = ebookFile Log.d(tag, "scanDownloadItem: Ebook file added to lli ${localFile.contentUrl}") } else { // Cover image val localFileId = DeviceManager.getBase64Id(docFile.id) - val localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length()) + val localFile = + LocalFile( + localFileId, + docFile.name, + docFile.uri.toString(), + docFile.getBasePath(ctx), + docFile.getAbsolutePath(ctx), + docFile.getSimplePath(ctx), + docFile.mimeType, + docFile.length() + ) localLibraryItem.coverAbsolutePath = localFile.absolutePath localLibraryItem.coverContentUrl = localFile.contentUrl @@ -309,7 +507,10 @@ class FolderScanner(var ctx: Context) { } if (audioTracks.isEmpty() && !foundEBookFile) { - Log.d(tag, "scanDownloadItem did not find any audio tracks or ebook file in folder for ${downloadItem.itemFolderPath}") + Log.d( + tag, + "scanDownloadItem did not find any audio tracks or ebook file in folder for ${downloadItem.itemFolderPath}" + ) return cb(null) } @@ -331,30 +532,37 @@ class FolderScanner(var ctx: Context) { localLibraryItem.media.setAudioTracks(audioTracks) } - val downloadItemScanResult = DownloadItemScanResult(localLibraryItem,null) + val downloadItemScanResult = DownloadItemScanResult(localLibraryItem, null) // If library item had media progress then make local media progress and save downloadItem.userMediaProgress?.let { mediaProgress -> - val localMediaProgressId = if (downloadItem.episodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId" - val newLocalMediaProgress = LocalMediaProgress( - id = localMediaProgressId, - localLibraryItemId = localLibraryItemId, - localEpisodeId = localEpisodeId, - duration = mediaProgress.duration, - progress = mediaProgress.progress, - currentTime = mediaProgress.currentTime, - isFinished = mediaProgress.isFinished, - ebookLocation = mediaProgress.ebookLocation, - ebookProgress = mediaProgress.ebookProgress, - lastUpdate = mediaProgress.lastUpdate, - startedAt = mediaProgress.startedAt, - finishedAt = mediaProgress.finishedAt, - serverConnectionConfigId = downloadItem.serverConnectionConfigId, - serverAddress = downloadItem.serverAddress, - serverUserId = downloadItem.serverUserId, - libraryItemId = downloadItem.libraryItemId, - episodeId = downloadItem.episodeId) - Log.d(tag, "scanLibraryItemFolder: Saving local media progress ${newLocalMediaProgress.id} at progress ${newLocalMediaProgress.progress}") + val localMediaProgressId = + if (downloadItem.episodeId.isNullOrEmpty()) localLibraryItemId + else "$localLibraryItemId-$localEpisodeId" + val newLocalMediaProgress = + LocalMediaProgress( + id = localMediaProgressId, + localLibraryItemId = localLibraryItemId, + localEpisodeId = localEpisodeId, + duration = mediaProgress.duration, + progress = mediaProgress.progress, + currentTime = mediaProgress.currentTime, + isFinished = mediaProgress.isFinished, + ebookLocation = mediaProgress.ebookLocation, + ebookProgress = mediaProgress.ebookProgress, + lastUpdate = mediaProgress.lastUpdate, + startedAt = mediaProgress.startedAt, + finishedAt = mediaProgress.finishedAt, + serverConnectionConfigId = downloadItem.serverConnectionConfigId, + serverAddress = downloadItem.serverAddress, + serverUserId = downloadItem.serverUserId, + libraryItemId = downloadItem.libraryItemId, + episodeId = downloadItem.episodeId + ) + Log.d( + tag, + "scanLibraryItemFolder: Saving local media progress ${newLocalMediaProgress.id} at progress ${newLocalMediaProgress.progress}" + ) DeviceManager.dbManager.saveLocalMediaProgress(newLocalMediaProgress) diff --git a/android/app/src/main/java/com/audiobookshelf/app/managers/DbManager.kt b/android/app/src/main/java/com/audiobookshelf/app/managers/DbManager.kt index e681eb91..4b5da5af 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/managers/DbManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/managers/DbManager.kt @@ -22,45 +22,47 @@ class DbManager { } 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) { Paper.book("device").write("data", deviceData) } - fun getLocalLibraryItems(mediaType:String? = null):MutableList { - val localLibraryItems:MutableList = mutableListOf() + fun getLocalLibraryItems(mediaType: String? = null): MutableList { + val localLibraryItems: MutableList = mutableListOf() Paper.book("localLibraryItems").allKeys.forEach { 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) } } return localLibraryItems } - fun getLocalLibraryItemsInFolder(folderId:String):List { + fun getLocalLibraryItemsInFolder(folderId: String): List { val localLibraryItems = getLocalLibraryItems() - return localLibraryItems.filter { - it.folderId == folderId - } + return localLibraryItems.filter { it.folderId == folderId } } - fun getLocalLibraryItemByLId(libraryItemId:String): LocalLibraryItem? { + fun getLocalLibraryItemByLId(libraryItemId: String): LocalLibraryItem? { return getLocalLibraryItems().find { it.libraryItemId == libraryItemId } } - fun getLocalLibraryItem(localLibraryItemId:String): LocalLibraryItem? { + fun getLocalLibraryItem(localLibraryItemId: String): LocalLibraryItem? { return Paper.book("localLibraryItems").read(localLibraryItemId) } - fun getLocalLibraryItemWithEpisode(podcastEpisodeId:String): LibraryItemWithEpisode? { + fun getLocalLibraryItemWithEpisode(podcastEpisodeId: String): LibraryItemWithEpisode? { var podcastEpisode: PodcastEpisode? = null - val localLibraryItem = getLocalLibraryItems("podcast").find { localLibraryItem -> - val podcast = localLibraryItem.media as Podcast - podcastEpisode = podcast.episodes?.find { it.id == podcastEpisodeId } - podcastEpisode != null - } + val localLibraryItem = + getLocalLibraryItems("podcast").find { localLibraryItem -> + val podcast = localLibraryItem.media as Podcast + podcastEpisode = podcast.episodes?.find { it.id == podcastEpisodeId } + podcastEpisode != null + } return if (localLibraryItem != null) { LibraryItemWithEpisode(localLibraryItem, podcastEpisode!!) } else { @@ -68,14 +70,12 @@ class DbManager { } } - fun removeLocalLibraryItem(localLibraryItemId:String) { + fun removeLocalLibraryItem(localLibraryItemId: String) { Paper.book("localLibraryItems").delete(localLibraryItemId) } - fun saveLocalLibraryItems(localLibraryItems:List) { - localLibraryItems.map { - Paper.book("localLibraryItems").write(it.id, it) - } + fun saveLocalLibraryItems(localLibraryItems: List) { + localLibraryItems.map { Paper.book("localLibraryItems").write(it.id, it) } } fun saveLocalLibraryItem(localLibraryItem: LocalLibraryItem) { @@ -83,28 +83,24 @@ class DbManager { } 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) } - fun getAllLocalFolders():List { - val localFolders:MutableList = mutableListOf() + fun getAllLocalFolders(): List { + val localFolders: MutableList = mutableListOf() Paper.book("localFolders").allKeys.forEach { localFolderId -> - Paper.book("localFolders").read(localFolderId)?.let { - localFolders.add(it) - } + Paper.book("localFolders").read(localFolderId)?.let { localFolders.add(it) } } return localFolders } - fun removeLocalFolder(folderId:String) { + fun removeLocalFolder(folderId: String) { val localLibraryItems = getLocalLibraryItemsInFolder(folderId) - localLibraryItems.forEach { - Paper.book("localLibraryItems").delete(it.id) - } + localLibraryItems.forEach { Paper.book("localLibraryItems").delete(it.id) } Paper.book("localFolders").delete(folderId) } @@ -112,29 +108,28 @@ class DbManager { Paper.book("downloadItems").write(downloadItem.id, downloadItem) } - fun removeDownloadItem(downloadItemId:String) { + fun removeDownloadItem(downloadItemId: String) { Paper.book("downloadItems").delete(downloadItemId) } - fun getDownloadItems():List { - val downloadItems:MutableList = mutableListOf() + fun getDownloadItems(): List { + val downloadItems: MutableList = mutableListOf() Paper.book("downloadItems").allKeys.forEach { downloadItemId -> - Paper.book("downloadItems").read(downloadItemId)?.let { - downloadItems.add(it) - } + Paper.book("downloadItems").read(downloadItemId)?.let { downloadItems.add(it) } } return downloadItems } 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}" - fun getLocalMediaProgress(localMediaProgressId:String): LocalMediaProgress? { + // For books this will just be the localLibraryItemId for podcast episodes this will be + // "{localLibraryItemId}-{episodeId}" + fun getLocalMediaProgress(localMediaProgressId: String): LocalMediaProgress? { return Paper.book("localMediaProgress").read(localMediaProgressId) } - fun getAllLocalMediaProgress():List { - val mediaProgress:MutableList = mutableListOf() + fun getAllLocalMediaProgress(): List { + val mediaProgress: MutableList = mutableListOf() Paper.book("localMediaProgress").allKeys.forEach { localMediaProgressId -> Paper.book("localMediaProgress").read(localMediaProgressId)?.let { mediaProgress.add(it) @@ -142,7 +137,7 @@ class DbManager { } return mediaProgress } - fun removeLocalMediaProgress(localMediaProgressId:String) { + fun removeLocalMediaProgress(localMediaProgressId: String) { Paper.book("localMediaProgress").delete(localMediaProgressId) } @@ -158,35 +153,50 @@ class DbManager { var hasUpdates = false // Check local files - lli.localFiles = lli.localFiles.filter { localFile -> - - val file = File(localFile.absolutePath) - if (!file.exists()) { - Log.d(tag, "cleanLocalLibraryItems: Local file ${localFile.absolutePath} was removed from library item ${lli.media.metadata.title}") - hasUpdates = true - } - file.exists() - } as MutableList + lli.localFiles = + lli.localFiles.filter { localFile -> + val file = File(localFile.absolutePath) + if (!file.exists()) { + Log.d( + tag, + "cleanLocalLibraryItems: Local file ${localFile.absolutePath} was removed from library item ${lli.media.metadata.title}" + ) + hasUpdates = true + } + file.exists() + } as + MutableList // Check audio tracks and episodes if (lli.isPodcast) { val podcast = lli.media as Podcast - podcast.episodes = podcast.episodes?.filter { ep -> - 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}") - hasUpdates = true - } - ep.audioTrack != null && lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } != null - } as MutableList + podcast.episodes = + podcast.episodes?.filter { ep -> + 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}" + ) + hasUpdates = true + } + ep.audioTrack != null && + lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } != null + } as + MutableList } else { val book = lli.media as Book - book.tracks = book.tracks?.filter { track -> - 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}") - hasUpdates = true - } - lli.localFiles.find { lf -> lf.id == track.localFileId } != null - } as MutableList + book.tracks = + book.tracks?.filter { track -> + 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}" + ) + hasUpdates = true + } + lli.localFiles.find { lf -> lf.id == track.localFileId } != null + } as + MutableList } // Check cover still there @@ -194,14 +204,22 @@ class DbManager { val coverFile = File(it) 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.coverContentUrl = null hasUpdates = true } } - if (hasUpdates) { + if (lli.serverConnectionConfigId == null) { + // Local-only item support was removed in app version 0.9.67, remove any remaining local + // only items beginning in 0.9.80 + Log.d(tag, "cleanLocalLibraryItems: Local only item ${lli.id} - removing from ABS") + Paper.book("localLibraryItems").delete(lli.id) + } else if (hasUpdates) { Log.d(tag, "cleanLocalLibraryItems: Saving local library item ${lli.id}") Paper.book("localLibraryItems").write(lli.id, lli) } @@ -215,11 +233,18 @@ class DbManager { localMediaProgress.forEach { val matchingLLI = localLibraryItems.find { lli -> lli.id == it.localLibraryItemId } 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. - Log.d(tag, "cleanLocalMediaProgress: Invalid local media progress does not start with 'local' (fixed on server 2.0.24)") + // A bug on the server when syncing local media progress was replacing the media progress id + // 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) } 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) } else if (matchingLLI.isPodcast) { if (it.localEpisodeId.isNullOrEmpty()) { @@ -229,7 +254,10 @@ class DbManager { val podcast = matchingLLI.media as Podcast val matchingLEp = podcast.episodes?.find { ep -> ep.id == it.localEpisodeId } 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) } } @@ -238,20 +266,20 @@ class DbManager { } 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) } 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) } - fun getPlaybackSessions():List { - val sessions:MutableList = mutableListOf() + fun getPlaybackSessions(): List { + val sessions: MutableList = mutableListOf() Paper.book("playbackSession").allKeys.forEach { playbackSessionId -> Paper.book("playbackSession").read(playbackSessionId)?.let { sessions.add(it) diff --git a/android/app/src/main/java/com/audiobookshelf/app/media/MediaEventManager.kt b/android/app/src/main/java/com/audiobookshelf/app/media/MediaEventManager.kt index 80d3b406..82bf3f42 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/media/MediaEventManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/media/MediaEventManager.kt @@ -36,76 +36,97 @@ object MediaEventManager { } 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) } 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) } - private fun addSyncEvent(eventName:String, mediaProgress:MediaProgressWrapper, description: String) { + private fun addSyncEvent( + eventName: String, + mediaProgress: MediaProgressWrapper, + description: String + ) { val mediaItemHistory = getMediaItemHistoryMediaItem(mediaProgress.mediaItemId) 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 } - val mediaItemEvent = MediaItemEvent( - name = eventName, - type = "Sync", - description = description, - currentTime = mediaProgress.currentTime, - serverSyncAttempted = false, - serverSyncSuccess = null, - serverSyncMessage = null, - timestamp = System.currentTimeMillis() - ) + val mediaItemEvent = + MediaItemEvent( + name = eventName, + type = "Sync", + description = description, + currentTime = mediaProgress.currentTime, + serverSyncAttempted = false, + serverSyncSuccess = null, + serverSyncMessage = null, + timestamp = System.currentTimeMillis() + ) mediaItemHistory.events.add(mediaItemEvent) DeviceManager.dbManager.saveMediaItemHistory(mediaItemHistory) clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory) } - private fun addPlaybackEvent(eventName:String, playbackSession:PlaybackSession, syncResult: SyncResult?) { - val mediaItemHistory = getMediaItemHistoryMediaItem(playbackSession.mediaItemId) ?: createMediaItemHistoryForSession(playbackSession) + private fun addPlaybackEvent( + eventName: String, + playbackSession: PlaybackSession, + syncResult: SyncResult? + ) { + val mediaItemHistory = + getMediaItemHistoryMediaItem(playbackSession.mediaItemId) + ?: createMediaItemHistoryForSession(playbackSession) - val mediaItemEvent = MediaItemEvent( - name = eventName, - type = "Playback", - description = "", - currentTime = playbackSession.currentTime, - serverSyncAttempted = syncResult?.serverSyncAttempted ?: false, - serverSyncSuccess = syncResult?.serverSyncSuccess, - serverSyncMessage = syncResult?.serverSyncMessage, - timestamp = System.currentTimeMillis() - ) + val mediaItemEvent = + MediaItemEvent( + name = eventName, + type = "Playback", + description = "", + currentTime = playbackSession.currentTime, + serverSyncAttempted = syncResult?.serverSyncAttempted ?: false, + serverSyncSuccess = syncResult?.serverSyncSuccess, + serverSyncMessage = syncResult?.serverSyncMessage, + timestamp = System.currentTimeMillis() + ) mediaItemHistory.events.add(mediaItemEvent) DeviceManager.dbManager.saveMediaItemHistory(mediaItemHistory) clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory) } - private fun getMediaItemHistoryMediaItem(mediaItemId: String) : MediaItemHistory? { + private fun getMediaItemHistoryMediaItem(mediaItemId: String): MediaItemHistory? { 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}\"") - val isLocalOnly = playbackSession.isLocalLibraryItemOnly - val libraryItemId = if (isLocalOnly) playbackSession.localLibraryItemId else playbackSession.libraryItemId ?: "" - val episodeId:String? = if (isLocalOnly && playbackSession.localEpisodeId != null) playbackSession.localEpisodeId else playbackSession.episodeId + val libraryItemId = playbackSession.libraryItemId ?: "" + val episodeId: String? = playbackSession.episodeId return MediaItemHistory( - id = playbackSession.mediaItemId, - mediaDisplayTitle = playbackSession.displayTitle ?: "Unset", - libraryItemId, - episodeId, - isLocalOnly, - playbackSession.serverConnectionConfigId, - playbackSession.serverAddress, - playbackSession.userId, - createdAt = System.currentTimeMillis(), - events = mutableListOf()) + id = playbackSession.mediaItemId, + mediaDisplayTitle = playbackSession.displayTitle ?: "Unset", + libraryItemId, + episodeId, + false, // local-only items are not supported + playbackSession.serverConnectionConfigId, + playbackSession.serverAddress, + playbackSession.userId, + createdAt = System.currentTimeMillis(), + events = mutableListOf() + ) } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt b/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt index 369fd18c..ae110498 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/media/MediaProgressSyncer.kt @@ -13,36 +13,43 @@ import java.util.* import kotlin.concurrent.schedule data class MediaProgressSyncData( - var timeListened:Long, // seconds - var duration:Double, // seconds - var currentTime:Double // seconds + var timeListened: Long, // seconds + var duration: Double, // seconds + var currentTime: Double // seconds ) data class SyncResult( - var serverSyncAttempted:Boolean, - var serverSyncSuccess:Boolean?, - var serverSyncMessage:String? + var serverSyncAttempted: Boolean, + var serverSyncSuccess: Boolean?, + 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 METERED_CONNECTION_SYNC_INTERVAL = 60000 private var listeningTimerTask: TimerTask? = null - var listeningTimerRunning:Boolean = false + var listeningTimerRunning: Boolean = false - private var lastSyncTime:Long = 0 - private var failedSyncs:Int = 0 + private var lastSyncTime: Long = 0 + private var failedSyncs: Int = 0 var currentPlaybackSession: PlaybackSession? = null // copy of pb session currently syncing var currentLocalMediaProgress: LocalMediaProgress? = null - private val currentDisplayTitle get() = currentPlaybackSession?.displayTitle ?: "Unset" - val currentIsLocal get() = currentPlaybackSession?.isLocal == true - val currentSessionId get() = currentPlaybackSession?.id ?: "" - private val currentPlaybackDuration get() = currentPlaybackSession?.duration ?: 0.0 + private val currentDisplayTitle + get() = currentPlaybackSession?.displayTitle ?: "Unset" + val currentIsLocal + 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) { Log.d(tag, "start: Timer already running for $currentDisplayTitle") if (playbackSession.id != currentSessionId) { @@ -62,40 +69,48 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi listeningTimerRunning = true lastSyncTime = System.currentTimeMillis() 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) { - Handler(Looper.getMainLooper()).post() { - if (playerNotificationService.currentPlayer.isPlaying) { - // Set auto sleep timer if enabled and within start/end time - playerNotificationService.sleepTimerManager.checkAutoSleepTimer() + listeningTimerTask = + Timer("ListeningTimer", false).schedule(15000L, 15000L) { + Handler(Looper.getMainLooper()).post() { + if (playerNotificationService.currentPlayer.isPlaying) { + // Set auto sleep timer if enabled and within start/end time + playerNotificationService.sleepTimerManager.checkAutoSleepTimer() - // Only sync with server on unmetered connection every 15s OR sync with server if last sync time is >= 60s - val shouldSyncServer = PlayerNotificationService.isUnmeteredNetwork || System.currentTimeMillis() - lastSyncTime >= METERED_CONNECTION_SYNC_INTERVAL + // Only sync with server on unmetered connection every 15s OR sync with server if + // last sync time is >= 60s + val shouldSyncServer = + PlayerNotificationService.isUnmeteredNetwork || + System.currentTimeMillis() - lastSyncTime >= + METERED_CONNECTION_SYNC_INTERVAL - val currentTime = playerNotificationService.getCurrentTimeSeconds() - if (currentTime > 0) { - sync(shouldSyncServer, currentTime) { syncResult -> - Log.d(tag, "Sync complete") + val currentTime = playerNotificationService.getCurrentTimeSeconds() + if (currentTime > 0) { + sync(shouldSyncServer, currentTime) { syncResult -> + Log.d(tag, "Sync complete") - currentPlaybackSession?.let { playbackSession -> - MediaEventManager.saveEvent(playbackSession, syncResult) + currentPlaybackSession?.let { playbackSession -> + MediaEventManager.saveEvent(playbackSession, syncResult) + } + } + } + } } } - } - } - } - } } - fun play(playbackSession:PlaybackSession) { + fun play(playbackSession: PlaybackSession) { Log.d(tag, "play ${playbackSession.displayTitle}") MediaEventManager.playEvent(playbackSession) start(playbackSession) } - fun stop(shouldSync:Boolean? = true, cb: () -> Unit) { + fun stop(shouldSync: Boolean? = true, cb: () -> Unit) { if (!listeningTimerRunning) { reset() return cb() @@ -106,7 +121,8 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi listeningTimerRunning = false 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 sync(true, currentTime) { syncResult -> currentPlaybackSession?.let { playbackSession -> @@ -197,12 +213,15 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi it.updatedAt = mediaProgress.lastUpdate 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) } } - fun sync(shouldSyncServer:Boolean, currentTime:Double, cb: (SyncResult?) -> Unit) { + fun sync(shouldSyncServer: Boolean, currentTime: Double, cb: (SyncResult?) -> Unit) { if (lastSyncTime <= 0) { Log.e(tag, "Last sync time is not set $lastSyncTime") return cb(null) @@ -214,11 +233,14 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi } val listeningTimeToAdd = diffSinceLastSync / 1000L - val syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime) + val syncData = MediaProgressSyncData(listeningTimeToAdd, currentPlaybackDuration, currentTime) currentPlaybackSession?.syncData(syncData) 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) } @@ -226,11 +248,7 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi // Save playback session to db (server linked sessions only) // Sessions are removed once successfully synced with the server - currentPlaybackSession?.let { - if (!it.isLocalLibraryItemOnly) { - DeviceManager.dbManager.savePlaybackSession(it) - } - } + currentPlaybackSession?.let { DeviceManager.dbManager.savePlaybackSession(it) } if (currentIsLocal) { // Save local progress sync @@ -238,11 +256,20 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi saveLocalProgress(it) 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 - // Send sync to server also if connected to this server and local item belongs to this server - if (hasNetworkConnection && shouldSyncServer && !it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) { + // Send sync to server also if connected to this server and local item belongs to this + // server + if (hasNetworkConnection && + shouldSyncServer && + !it.libraryItemId.isNullOrEmpty() && + it.serverConnectionConfigId != null && + DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId + ) { apiHandler.sendLocalProgressSync(it) { syncSuccess, errorMsg -> if (syncSuccess) { failedSyncs = 0 @@ -254,7 +281,10 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi playerNotificationService.alertSyncFailing() // Show alert in client 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)) @@ -278,7 +308,10 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi playerNotificationService.alertSyncFailing() // Show alert in client 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)) } @@ -287,9 +320,10 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi } } - private fun saveLocalProgress(playbackSession:PlaybackSession) { + private fun saveLocalProgress(playbackSession: PlaybackSession) { if (currentLocalMediaProgress == null) { - val mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(playbackSession.localMediaProgressId) + val mediaProgress = + DeviceManager.dbManager.getLocalMediaProgress(playbackSession.localMediaProgressId) if (mediaProgress == null) { currentLocalMediaProgress = playbackSession.getNewLocalMediaProgress() } else { @@ -306,12 +340,14 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi } else { DeviceManager.dbManager.saveLocalMediaProgress(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() { currentPlaybackSession = null currentLocalMediaProgress = null 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 4f42ce58..e9e792b7 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 @@ -25,6 +25,7 @@ import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import androidx.media.MediaBrowserServiceCompat import androidx.media.utils.MediaConstants +import com.audiobookshelf.app.BuildConfig import com.audiobookshelf.app.R import com.audiobookshelf.app.data.* import com.audiobookshelf.app.data.DeviceInfo @@ -33,10 +34,9 @@ import com.audiobookshelf.app.managers.DbManager import com.audiobookshelf.app.managers.SleepTimerManager 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.audiobookshelf.app.server.ApiHandler import com.google.android.exoplayer2.* import com.google.android.exoplayer2.audio.AudioAttributes import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector @@ -49,16 +49,15 @@ 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 - +import kotlinx.coroutines.runBlocking const val SLEEP_TIMER_WAKE_UP_EXPIRATION = 120000L // 2m const val PLAYER_CAST = "cast-player" const val PLAYER_EXO = "exo-player" -class PlayerNotificationService : MediaBrowserServiceCompat() { +class PlayerNotificationService : MediaBrowserServiceCompat() { companion object { var isStarted = false @@ -71,80 +70,80 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { private val tag = "PlayerNotificationServ" interface ClientEventEmitter { - fun onPlaybackSession(playbackSession:PlaybackSession) + fun onPlaybackSession(playbackSession: PlaybackSession) fun onPlaybackClosed() fun onPlayingUpdate(isPlaying: Boolean) fun onMetadata(metadata: PlaybackMetadata) fun onSleepTimerEnded(currentPosition: Long) - fun onSleepTimerSet(sleepTimeRemaining: Int, isAutoSleepTimer:Boolean) + fun onSleepTimerSet(sleepTimeRemaining: Int, isAutoSleepTimer: Boolean) fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress) - fun onPlaybackFailed(errorMessage:String) - fun onMediaPlayerChanged(mediaPlayer:String) + fun onPlaybackFailed(errorMessage: String) + fun onMediaPlayerChanged(mediaPlayer: String) fun onProgressSyncFailing() fun onProgressSyncSuccess() - fun onNetworkMeteredChanged(isUnmetered:Boolean) - fun onMediaItemHistoryUpdated(mediaItemHistory:MediaItemHistory) - fun onPlaybackSpeedChanged(playbackSpeed:Float) + fun onNetworkMeteredChanged(isUnmetered: Boolean) + fun onMediaItemHistoryUpdated(mediaItemHistory: MediaItemHistory) + fun onPlaybackSpeedChanged(playbackSpeed: Float) } private val binder = LocalBinder() - var clientEventEmitter:ClientEventEmitter? = null + var clientEventEmitter: ClientEventEmitter? = null - private lateinit var ctx:Context + private lateinit var ctx: Context private lateinit var mediaSessionConnector: MediaSessionConnector private lateinit var playerNotificationManager: PlayerNotificationManager lateinit var mediaSession: MediaSessionCompat - private lateinit var transportControls:MediaControllerCompat.TransportControls + private lateinit var transportControls: MediaControllerCompat.TransportControls lateinit var mediaManager: MediaManager lateinit var apiHandler: ApiHandler lateinit var mPlayer: ExoPlayer - lateinit var currentPlayer:Player - var castPlayer:CastPlayer? = null + lateinit var currentPlayer: Player + var castPlayer: CastPlayer? = null - lateinit var sleepTimerManager:SleepTimerManager + lateinit var sleepTimerManager: SleepTimerManager lateinit var mediaProgressSyncer: MediaProgressSyncer private var notificationId = 10 private var channelId = "audiobookshelf_channel" private var channelName = "Audiobookshelf Channel" - var currentPlaybackSession:PlaybackSession? = null - private var initialPlaybackRate:Float? = null + var currentPlaybackSession: PlaybackSession? = null + private var initialPlaybackRate: Float? = null private var isAndroidAuto = false // The following are used for the shake detection - private var isShakeSensorRegistered:Boolean = false + private var isShakeSensorRegistered: Boolean = false private var mSensorManager: SensorManager? = null private var mAccelerometer: Sensor? = null private var mShakeDetector: ShakeDetector? = null - private var shakeSensorUnregisterTask:TimerTask? = null + private var shakeSensorUnregisterTask: TimerTask? = null // These are used to trigger reloading if - private var forceReloadingAndroidAuto:Boolean = false - private var firstLoadDone:Boolean = false + private var forceReloadingAndroidAuto: Boolean = false + private var firstLoadDone: Boolean = false - fun isBrowseTreeInitialized() : Boolean { + 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 = mutableListOf() + private var cachedSearch: String = "" + private var cachedSearchResults: MutableList = mutableListOf() - /* - Service related stuff - */ + /* + Service related stuff + */ override fun onBind(intent: Intent): IBinder? { Log.d(tag, "onBind") - // Android Auto Media Browser Service - if (SERVICE_INTERFACE == intent.action) { - Log.d(tag, "Is Media Browser Service") - return super.onBind(intent) - } + // Android Auto Media Browser Service + if (SERVICE_INTERFACE == intent.action) { + Log.d(tag, "Is Media Browser Service") + return super.onBind(intent) + } return binder } @@ -167,8 +166,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { @RequiresApi(Build.VERSION_CODES.O) private fun createNotificationChannel(channelId: String, channelName: String): String { - val chan = NotificationChannel(channelId, - channelName, NotificationManager.IMPORTANCE_LOW) + val chan = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_LOW) chan.lightColor = Color.DKGRAY chan.lockscreenVisibility = Notification.VISIBILITY_PUBLIC val service = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -179,9 +177,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { // detach player override fun onDestroy() { try { - val connectivityManager = getSystemService(ConnectivityManager::class.java) as ConnectivityManager + val connectivityManager = + getSystemService(ConnectivityManager::class.java) as ConnectivityManager connectivityManager.unregisterNetworkCallback(networkCallback) - } catch(error:Exception) { + } catch (error: Exception) { Log.e(tag, "Error unregistering network listening callback $error") } @@ -196,11 +195,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { mediaSession.release() mediaProgressSyncer.reset() - super.onDestroy() } - //removing service when user swipe out our app + // removing service when user swipe out our app override fun onTaskRemoved(rootIntent: Intent?) { super.onTaskRemoved(rootIntent) Log.d(tag, "onTaskRemoved") @@ -220,12 +218,14 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { DeviceManager.initializeWidgetUpdater(ctx) // To listen for network change from metered to unmetered - val networkRequest = NetworkRequest.Builder() - .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) - .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) - .build() - val connectivityManager = getSystemService(ConnectivityManager::class.java) as ConnectivityManager + val networkRequest = + NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) + .addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) + .build() + val connectivityManager = + getSystemService(ConnectivityManager::class.java) as ConnectivityManager connectivityManager.registerNetworkCallback(networkRequest, networkCallback) DbManager.initialize(ctx) @@ -246,30 +246,28 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { // Initialize media manager mediaManager = MediaManager(apiHandler, ctx) - channelId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - createNotificationChannel(channelId, channelName) - } else "" + channelId = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + createNotificationChannel(channelId, channelName) + } else "" val sessionActivityPendingIntent = - packageManager?.getLaunchIntentForPackage(packageName)?.let { sessionIntent -> - PendingIntent.getActivity(this, 0, sessionIntent, PendingIntent.FLAG_IMMUTABLE) - } + packageManager?.getLaunchIntentForPackage(packageName)?.let { sessionIntent -> + PendingIntent.getActivity(this, 0, sessionIntent, PendingIntent.FLAG_IMMUTABLE) + } - mediaSession = MediaSessionCompat(this, tag) - .apply { - setSessionActivity(sessionActivityPendingIntent) - isActive = true - } + mediaSession = + MediaSessionCompat(this, tag).apply { + setSessionActivity(sessionActivityPendingIntent) + isActive = true + } val mediaController = MediaControllerCompat(ctx, mediaSession.sessionToken) // This is for Media Browser sessionToken = mediaSession.sessionToken - val builder = PlayerNotificationManager.Builder( - ctx, - notificationId, - channelId) + val builder = PlayerNotificationManager.Builder(ctx, notificationId, channelId) builder.setMediaDescriptionAdapter(AbMediaDescriptionAdapter(mediaController, this)) builder.setNotificationListener(PlayerNotificationListener(this)) @@ -293,56 +291,67 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { transportControls = mediaController.transportControls mediaSessionConnector = MediaSessionConnector(mediaSession) - val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) { - override fun getSupportedQueueNavigatorActions(player: Player): Long { - return PlaybackStateCompat.ACTION_PLAY_PAUSE or PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PAUSE - } + val queueNavigator: TimelineQueueNavigator = + object : TimelineQueueNavigator(mediaSession) { + override fun getSupportedQueueNavigatorActions(player: Player): Long { + return PlaybackStateCompat.ACTION_PLAY_PAUSE or + PlaybackStateCompat.ACTION_PLAY or + PlaybackStateCompat.ACTION_PAUSE + } - override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat { - if (currentPlaybackSession == null) { - Log.e(tag,"Playback session is not set - returning blank MediaDescriptionCompat") - return MediaDescriptionCompat.Builder().build() - } + override fun getMediaDescription( + player: Player, + windowIndex: Int + ): MediaDescriptionCompat { + if (currentPlaybackSession == null) { + Log.e(tag, "Playback session is not set - returning blank MediaDescriptionCompat") + return MediaDescriptionCompat.Builder().build() + } - val coverUri = currentPlaybackSession!!.getCoverUri(ctx) + val coverUri = currentPlaybackSession!!.getCoverUri(ctx) - var bitmap: Bitmap? = null -// Local covers get bitmap - if (currentPlaybackSession!!.localLibraryItem?.coverContentUrl != null) { - bitmap = if (Build.VERSION.SDK_INT < 28) { - MediaStore.Images.Media.getBitmap(ctx.contentResolver, coverUri) - } else { - val source: ImageDecoder.Source = ImageDecoder.createSource(ctx.contentResolver, coverUri) - ImageDecoder.decodeBitmap(source) - } - } + var bitmap: Bitmap? = null + // Local covers get bitmap + if (currentPlaybackSession!!.localLibraryItem?.coverContentUrl != null) { + bitmap = + if (Build.VERSION.SDK_INT < 28) { + MediaStore.Images.Media.getBitmap(ctx.contentResolver, coverUri) + } else { + val source: ImageDecoder.Source = + ImageDecoder.createSource(ctx.contentResolver, coverUri) + ImageDecoder.decodeBitmap(source) + } + } - // Fix for local images crashing on Android 11 for specific devices - // https://stackoverflow.com/questions/64186578/android-11-mediastyle-notification-crash/64232958#64232958 - try { - ctx.grantUriPermission( - "com.android.systemui", - coverUri, - Intent.FLAG_GRANT_READ_URI_PERMISSION - ) - } catch(error:Exception) { - Log.e(tag, "Grant uri permission error $error") - } + // Fix for local images crashing on Android 11 for specific devices + // https://stackoverflow.com/questions/64186578/android-11-mediastyle-notification-crash/64232958#64232958 + try { + ctx.grantUriPermission( + "com.android.systemui", + coverUri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + } catch (error: Exception) { + Log.e(tag, "Grant uri permission error $error") + } - val extra = Bundle() - extra.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, currentPlaybackSession!!.displayAuthor) + val extra = Bundle() + extra.putString( + MediaMetadataCompat.METADATA_KEY_ARTIST, + currentPlaybackSession!!.displayAuthor + ) - val mediaDescriptionBuilder = MediaDescriptionCompat.Builder() - .setExtras(extra) - .setTitle(currentPlaybackSession!!.displayTitle) + val mediaDescriptionBuilder = + MediaDescriptionCompat.Builder() + .setExtras(extra) + .setTitle(currentPlaybackSession!!.displayTitle) - bitmap?.let { - mediaDescriptionBuilder.setIconBitmap(it) - } ?: mediaDescriptionBuilder.setIconUri(coverUri) + bitmap?.let { mediaDescriptionBuilder.setIconBitmap(it) } + ?: mediaDescriptionBuilder.setIconUri(coverUri) - return mediaDescriptionBuilder.build() - } - } + return mediaDescriptionBuilder.build() + } + } setMediaSessionConnectorPlaybackActions() mediaSessionConnector.setQueueNavigator(queueNavigator) @@ -355,24 +364,32 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } private fun initializeMPlayer() { - val customLoadControl:LoadControl = DefaultLoadControl.Builder().setBufferDurationsMs( - 1000 * 20, // 20s min buffer - 1000 * 45, // 45s max buffer - 1000 * 5, // 5s playback start - 1000 * 20 // 20s playback rebuffer - ).build() + val customLoadControl: LoadControl = + DefaultLoadControl.Builder() + .setBufferDurationsMs( + 1000 * 20, // 20s min buffer + 1000 * 45, // 45s max buffer + 1000 * 5, // 5s playback start + 1000 * 20 // 20s playback rebuffer + ) + .build() - mPlayer = ExoPlayer.Builder(this) - .setLoadControl(customLoadControl) - .setSeekBackIncrementMs(deviceSettings.jumpBackwardsTimeMs) - .setSeekForwardIncrementMs(deviceSettings.jumpForwardTimeMs) - .build() + mPlayer = + ExoPlayer.Builder(this) + .setLoadControl(customLoadControl) + .setSeekBackIncrementMs(deviceSettings.jumpBackwardsTimeMs) + .setSeekForwardIncrementMs(deviceSettings.jumpForwardTimeMs) + .build() mPlayer.setHandleAudioBecomingNoisy(true) mPlayer.addListener(PlayerListener(this)) - val audioAttributes:AudioAttributes = AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).setContentType(C.AUDIO_CONTENT_TYPE_SPEECH).build() + val audioAttributes: AudioAttributes = + AudioAttributes.Builder() + .setUsage(C.USAGE_MEDIA) + .setContentType(C.AUDIO_CONTENT_TYPE_SPEECH) + .build() mPlayer.setAudioAttributes(audioAttributes, true) - //attach player to playerNotificationManager + // attach player to playerNotificationManager playerNotificationManager.setPlayer(mPlayer) mediaSessionConnector.setPlayer(mPlayer) @@ -381,7 +398,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { /* User callable methods */ - fun preparePlayer(playbackSession: PlaybackSession, playWhenReady:Boolean, playbackRate:Float?) { + fun preparePlayer( + playbackSession: PlaybackSession, + playWhenReady: Boolean, + playbackRate: Float? + ) { if (!isStarted) { Log.i(tag, "preparePlayer: foreground service not started - Starting service --") Intent(ctx, PlayerNotificationService::class.java).also { intent -> @@ -391,7 +412,10 @@ 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}") + 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 @@ -424,7 +448,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } currentPlaybackSession = playbackSession - DeviceManager.setLastPlaybackSession(playbackSession) // Save playback session to use when app is closed + DeviceManager.setLastPlaybackSession( + playbackSession + ) // Save playback session to use when app is closed Log.d(tag, "Set CurrentPlaybackSession MediaPlayer ${currentPlaybackSession?.mediaPlayer}") // Notify client @@ -440,7 +466,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } if (mPlayer == currentPlayer) { - val mediaSource:MediaSource + val mediaSource: MediaSource if (playbackSession.isLocal) { Log.d(tag, "Playing Local Item") @@ -448,33 +474,40 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { val extractorsFactory = DefaultExtractorsFactory() if (DeviceManager.deviceData.deviceSettings?.enableMp3IndexSeeking == true) { - // @see https://exoplayer.dev/troubleshooting.html#why-is-seeking-inaccurate-in-some-mp3-files + // @see + // https://exoplayer.dev/troubleshooting.html#why-is-seeking-inaccurate-in-some-mp3-files extractorsFactory.setMp3ExtractorFlags(Mp3Extractor.FLAG_ENABLE_INDEX_SEEKING) } - mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory).createMediaSource(mediaItems[0]) + mediaSource = + ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory) + .createMediaSource(mediaItems[0]) } else if (!playbackSession.isHLS) { Log.d(tag, "Direct Playing Item") val dataSourceFactory = DefaultHttpDataSource.Factory() val extractorsFactory = DefaultExtractorsFactory() if (DeviceManager.deviceData.deviceSettings?.enableMp3IndexSeeking == true) { - // @see https://exoplayer.dev/troubleshooting.html#why-is-seeking-inaccurate-in-some-mp3-files + // @see + // https://exoplayer.dev/troubleshooting.html#why-is-seeking-inaccurate-in-some-mp3-files extractorsFactory.setMp3ExtractorFlags(Mp3Extractor.FLAG_ENABLE_INDEX_SEEKING) } dataSourceFactory.setUserAgent(channelId) - mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory).createMediaSource(mediaItems[0]) + mediaSource = + ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory) + .createMediaSource(mediaItems[0]) } else { Log.d(tag, "Playing HLS Item") val dataSourceFactory = DefaultHttpDataSource.Factory() dataSourceFactory.setUserAgent(channelId) - dataSourceFactory.setDefaultRequestProperties(hashMapOf("Authorization" to "Bearer ${DeviceManager.token}")) + dataSourceFactory.setDefaultRequestProperties( + hashMapOf("Authorization" to "Bearer ${DeviceManager.token}") + ) mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItems[0]) } mPlayer.setMediaSource(mediaSource) - // Add remaining media items if multi-track if (mediaItems.size > 1) { currentPlayer.addMediaItems(mediaItems.subList(1, mediaItems.size)) @@ -482,13 +515,19 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { val currentTrackIndex = playbackSession.getCurrentTrackIndex() val currentTrackTime = playbackSession.getCurrentTrackTimeMs() - Log.d(tag, "currentPlayer current track index $currentTrackIndex & current track time $currentTrackTime") + Log.d( + tag, + "currentPlayer current track index $currentTrackIndex & current track time $currentTrackTime" + ) currentPlayer.seekTo(currentTrackIndex, currentTrackTime) } else { currentPlayer.seekTo(playbackSession.currentTimeMs) } - Log.d(tag, "Prepare complete for session ${currentPlaybackSession?.displayTitle} | ${currentPlayer.mediaItemCount}") + Log.d( + tag, + "Prepare complete for session ${currentPlaybackSession?.displayTitle} | ${currentPlayer.mediaItemCount}" + ) currentPlayer.playWhenReady = playWhenReady currentPlayer.setPlaybackSpeed(playbackRateToUse) @@ -499,33 +538,44 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { val mediaType = playbackSession.mediaType Log.d(tag, "Loading cast player $currentTrackIndex $currentTrackTime $mediaType") - castPlayer?.load(mediaItems, currentTrackIndex, currentTrackTime, playWhenReady, playbackRateToUse, mediaType) + castPlayer?.load( + mediaItems, + currentTrackIndex, + currentTrackTime, + playWhenReady, + playbackRateToUse, + mediaType + ) } } - private fun setMediaSessionConnectorCustomActions(playbackSession:PlaybackSession) { + private fun setMediaSessionConnectorCustomActions(playbackSession: PlaybackSession) { val mediaItems = playbackSession.getMediaItems(ctx) - val customActionProviders = mutableListOf( - JumpBackwardCustomActionProvider(), - JumpForwardCustomActionProvider(), - ChangePlaybackSpeedCustomActionProvider() // Will be pushed to far left - ) + val customActionProviders = + mutableListOf( + JumpBackwardCustomActionProvider(), + JumpForwardCustomActionProvider(), + ChangePlaybackSpeedCustomActionProvider() // Will be pushed to far left + ) if (playbackSession.mediaPlayer != PLAYER_CAST && mediaItems.size > 1) { - customActionProviders.addAll(listOf( - SkipBackwardCustomActionProvider(), - SkipForwardCustomActionProvider(), - )) + customActionProviders.addAll( + listOf( + SkipBackwardCustomActionProvider(), + SkipForwardCustomActionProvider(), + ) + ) } mediaSessionConnector.setCustomActionProviders(*customActionProviders.toTypedArray()) } fun setMediaSessionConnectorPlaybackActions() { - var playbackActions = PlaybackStateCompat.ACTION_PLAY_PAUSE or - PlaybackStateCompat.ACTION_PLAY or - PlaybackStateCompat.ACTION_PAUSE or - PlaybackStateCompat.ACTION_FAST_FORWARD or - PlaybackStateCompat.ACTION_REWIND or - PlaybackStateCompat.ACTION_STOP + var playbackActions = + PlaybackStateCompat.ACTION_PLAY_PAUSE or + PlaybackStateCompat.ACTION_PLAY or + PlaybackStateCompat.ACTION_PAUSE or + PlaybackStateCompat.ACTION_FAST_FORWARD or + PlaybackStateCompat.ACTION_REWIND or + PlaybackStateCompat.ACTION_STOP if (deviceSettings.allowSeekingOnMediaControls) { playbackActions = playbackActions or PlaybackStateCompat.ACTION_SEEK_TO @@ -533,7 +583,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { mediaSessionConnector.setEnabledPlaybackActions(playbackActions) } - fun handlePlayerPlaybackError(errorMessage:String) { + fun handlePlayerPlaybackError(errorMessage: String) { // On error and was attempting to direct play - fallback to transcode currentPlaybackSession?.let { playbackSession -> if (playbackSession.isDirectPlay) { @@ -548,13 +598,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { clientEventEmitter?.onPlaybackFailed(errorMessage) closePlayback(true) } else { - Handler(Looper.getMainLooper()).post { - preparePlayer(it, true, null) - } + Handler(Looper.getMainLooper()).post { preparePlayer(it, true, null) } } } } - } else { clientEventEmitter?.onPlaybackFailed(errorMessage) closePlayback(true) @@ -581,9 +628,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { Log.e(tag, "Failed to play library item") } else { val playbackRate = mediaManager.getSavedPlaybackRate() - Handler(Looper.getMainLooper()).post { - preparePlayer(it,true, playbackRate) - } + Handler(Looper.getMainLooper()).post { preparePlayer(it, true, playbackRate) } } } } @@ -606,10 +651,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { if (it == null) { Log.e(tag, "Failed to start new playback session") } else { - Log.d(tag, "New playback session response from server with session id ${it.id} for \"${it.displayTitle}\"") - Handler(Looper.getMainLooper()).post { - preparePlayer(it, true, null) - } + Log.d( + tag, + "New playback session response from server with session id ${it.id} for \"${it.displayTitle}\"" + ) + Handler(Looper.getMainLooper()).post { preparePlayer(it, true, null) } } } } @@ -642,23 +688,26 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { isSwitchingPlayer = true } - // Playback session in progress syncer is a copy that is up-to-date so replace current here with that - // TODO: bad design here implemented to prevent the session in MediaProgressSyncer from changing while syncing + // Playback session in progress syncer is a copy that is up-to-date so replace current here with + // that + // TODO: bad design here implemented to prevent the session in MediaProgressSyncer from + // changing while syncing if (mediaProgressSyncer.currentPlaybackSession != null) { currentPlaybackSession = mediaProgressSyncer.currentPlaybackSession?.clone() } - currentPlayer = if (useCastPlayer) { - Log.d(tag, "switchToPlayer: Using Cast Player " + castPlayer?.deviceInfo) - mediaSessionConnector.setPlayer(castPlayer) - playerNotificationManager.setPlayer(castPlayer) - castPlayer as CastPlayer - } else { - Log.d(tag, "switchToPlayer: Using ExoPlayer") - mediaSessionConnector.setPlayer(mPlayer) - playerNotificationManager.setPlayer(mPlayer) - mPlayer - } + currentPlayer = + if (useCastPlayer) { + Log.d(tag, "switchToPlayer: Using Cast Player " + castPlayer?.deviceInfo) + mediaSessionConnector.setPlayer(castPlayer) + playerNotificationManager.setPlayer(castPlayer) + castPlayer as CastPlayer + } else { + Log.d(tag, "switchToPlayer: Using ExoPlayer") + mediaSessionConnector.setPlayer(mPlayer) + playerNotificationManager.setPlayer(mPlayer) + mPlayer + } clientEventEmitter?.onMediaPlayerChanged(getMediaPlayer()) @@ -673,7 +722,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } } - fun getCurrentTrackStartOffsetMs() : Long { + fun getCurrentTrackStartOffsetMs(): Long { return if (currentPlayer.mediaItemCount > 1) { val windowIndex = currentPlayer.currentMediaItemIndex val currentTrackStartOffset = currentPlaybackSession?.getTrackStartOffsetMs(windowIndex) ?: 0L @@ -683,15 +732,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } } - fun getCurrentTime() : Long { + fun getCurrentTime(): Long { return currentPlayer.currentPosition + getCurrentTrackStartOffsetMs() } - fun getCurrentTimeSeconds() : Double { + fun getCurrentTimeSeconds(): Double { return getCurrentTime() / 1000.0 } - private fun getBufferedTime() : Long { + private fun getBufferedTime(): Long { return if (currentPlayer.mediaItemCount > 1) { val windowIndex = currentPlayer.currentMediaItemIndex val currentTrackStartOffset = currentPlaybackSession?.getTrackStartOffsetMs(windowIndex) ?: 0L @@ -701,62 +750,79 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } } - fun getBufferedTimeSeconds() : Double { + fun getBufferedTimeSeconds(): Double { return getBufferedTime() / 1000.0 } - fun getDuration() : Long { + fun getDuration(): Long { return currentPlaybackSession?.totalDurationMs ?: 0L } - fun getCurrentPlaybackSessionCopy() :PlaybackSession? { + fun getCurrentPlaybackSessionCopy(): PlaybackSession? { return currentPlaybackSession?.clone() } - fun getCurrentBookChapter():BookChapter? { + fun getCurrentBookChapter(): BookChapter? { return currentPlaybackSession?.getChapterForTime(this.getCurrentTime()) } - fun getEndTimeOfChapterOrTrack():Long? { + fun getEndTimeOfChapterOrTrack(): Long? { return getCurrentBookChapter()?.endMs ?: currentPlaybackSession?.getCurrentTrackEndTime() } - private fun getNextBookChapter():BookChapter? { + private fun getNextBookChapter(): BookChapter? { return currentPlaybackSession?.getNextChapterForTime(this.getCurrentTime()) } - fun getEndTimeOfNextChapterOrTrack():Long? { + fun getEndTimeOfNextChapterOrTrack(): Long? { return getNextBookChapter()?.endMs ?: currentPlaybackSession?.getNextTrackEndTime() } // Called from PlayerListener play event // check with server if progress has updated since last play and sync progress update - fun checkCurrentSessionProgress(seekBackTime:Long):Boolean { + fun checkCurrentSessionProgress(seekBackTime: Long): Boolean { if (currentPlaybackSession == null) return true mediaProgressSyncer.currentPlaybackSession?.let { playbackSession -> - if (!DeviceManager.checkConnectivity(ctx) || playbackSession.isLocalLibraryItemOnly) { + if (!DeviceManager.checkConnectivity(ctx)) { return true // carry on } if (playbackSession.isLocal) { // Make sure this connection config exists - val serverConnectionConfig = DeviceManager.getServerConnectionConfig(playbackSession.serverConnectionConfigId) + val serverConnectionConfig = + DeviceManager.getServerConnectionConfig(playbackSession.serverConnectionConfigId) if (serverConnectionConfig == null) { - Log.d(tag, "checkCurrentSessionProgress: Local library item server connection config is not saved ${playbackSession.serverConnectionConfigId}") + Log.d( + tag, + "checkCurrentSessionProgress: Local library item server connection config is not saved ${playbackSession.serverConnectionConfigId}" + ) return true // carry on } // Local playback session check if server has updated media progress - Log.d(tag, "checkCurrentSessionProgress: Checking if local media progress was updated on server") - apiHandler.getMediaProgress(playbackSession.libraryItemId!!, playbackSession.episodeId, serverConnectionConfig) { mediaProgress -> - - if (mediaProgress != null && mediaProgress.lastUpdate > playbackSession.updatedAt && mediaProgress.currentTime != playbackSession.currentTime) { - Log.d(tag, "checkCurrentSessionProgress: Media progress was updated since last play time updating from ${playbackSession.currentTime} to ${mediaProgress.currentTime}") + Log.d( + tag, + "checkCurrentSessionProgress: Checking if local media progress was updated on server" + ) + apiHandler.getMediaProgress( + playbackSession.libraryItemId!!, + playbackSession.episodeId, + serverConnectionConfig + ) { mediaProgress -> + if (mediaProgress != null && + mediaProgress.lastUpdate > playbackSession.updatedAt && + mediaProgress.currentTime != playbackSession.currentTime + ) { + Log.d( + tag, + "checkCurrentSessionProgress: Media progress was updated since last play time updating from ${playbackSession.currentTime} to ${mediaProgress.currentTime}" + ) mediaProgressSyncer.syncFromServerProgress(mediaProgress) - // Update current playback session stored in PNS since MediaProgressSyncer version is a copy + // Update current playback session stored in PNS since MediaProgressSyncer version is a + // copy mediaProgressSyncer.currentPlaybackSession?.let { updatedPlaybackSession -> currentPlaybackSession = updatedPlaybackSession } @@ -786,14 +852,14 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } else { // Streaming from server so check if playback session still exists on server Log.d( - tag, - "checkCurrentSessionProgress: Checking if playback session ${playbackSession.id} for server stream is still available" + tag, + "checkCurrentSessionProgress: Checking if playback session ${playbackSession.id} for server stream is still available" ) apiHandler.getPlaybackSession(playbackSession.id) { if (it == null) { Log.d( - tag, - "checkCurrentSessionProgress: Playback session does not exist on server - start new playback session" + tag, + "checkCurrentSessionProgress: Playback session does not exist on server - start new playback session" ) Handler(Looper.getMainLooper()).post { @@ -801,19 +867,19 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { startNewPlaybackSession() } } else { - Log.d(tag, "checkCurrentSessionProgress: Playback session still available on server") - Handler(Looper.getMainLooper()).post { - if (seekBackTime > 0L) { - seekBackward(seekBackTime) - } - - currentPlayer.volume = 1F // Volume on sleep timer might have decreased this - mediaProgressSyncer.currentPlaybackSession?.let { playbackSession -> - mediaProgressSyncer.play(playbackSession) - } - - clientEventEmitter?.onPlayingUpdate(true) + Log.d(tag, "checkCurrentSessionProgress: Playback session still available on server") + Handler(Looper.getMainLooper()).post { + if (seekBackTime > 0L) { + seekBackward(seekBackTime) } + + currentPlayer.volume = 1F // Volume on sleep timer might have decreased this + mediaProgressSyncer.currentPlaybackSession?.let { playbackSession -> + mediaProgressSyncer.play(playbackSession) + } + + clientEventEmitter?.onPlayingUpdate(true) + } } } } @@ -834,7 +900,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { currentPlayer.pause() } - fun playPause():Boolean { + fun playPause(): Boolean { return if (currentPlayer.isPlaying) { pause() false @@ -883,7 +949,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } fun seekForward(amount: Long) { - seekPlayer(getCurrentTime() + amount) + seekPlayer(getCurrentTime() + amount) } fun seekBackward(amount: Long) { @@ -895,12 +961,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { currentPlayer.setPlaybackSpeed(speed) // Refresh Android Auto actions - mediaProgressSyncer.currentPlaybackSession?.let { - setMediaSessionConnectorCustomActions(it) - } + mediaProgressSyncer.currentPlaybackSession?.let { setMediaSessionConnectorCustomActions(it) } } - fun closePlayback(calledOnError:Boolean? = false) { + fun closePlayback(calledOnError: Boolean? = false) { Log.d(tag, "closePlayback") val config = DeviceManager.serverConnectionConfig @@ -909,7 +973,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { if (mediaProgressSyncer.listeningTimerRunning) { Log.i(tag, "About to close playback so stopping media progress syncer first") - mediaProgressSyncer.stop(calledOnError == false) { // If closing on error then do not sync progress (causes exception) + mediaProgressSyncer.stop( + calledOnError == false + ) { // If closing on error then do not sync progress (causes exception) Log.d(tag, "Media Progress syncer stopped") // If not local session then close on server if (!isLocal && currentSessionId != "") { @@ -930,7 +996,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { try { currentPlayer.stop() currentPlayer.clearMediaItems() - } catch(e:Exception) { + } catch (e: Exception) { Log.e(tag, "Exception clearing exoplayer $e") } @@ -950,30 +1016,42 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { clientEventEmitter?.onMetadata(PlaybackMetadata(duration, getCurrentTimeSeconds(), playerState)) } - fun getMediaPlayer():String { - return if(currentPlayer == castPlayer) PLAYER_CAST else PLAYER_EXO + fun getMediaPlayer(): String { + return if (currentPlayer == castPlayer) PLAYER_CAST else PLAYER_EXO } @SuppressLint("HardwareIds") fun getDeviceInfo(): DeviceInfo { /* EXAMPLE - manufacturer: Google - model: Pixel 6 - brand: google - sdkVersion: 32 - appVersion: 0.9.46-beta - */ + manufacturer: Google + model: Pixel 6 + brand: google + sdkVersion: 32 + appVersion: 0.9.46-beta + */ val deviceId = Settings.Secure.getString(ctx.contentResolver, Settings.Secure.ANDROID_ID) - return DeviceInfo(deviceId, Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME) + return DeviceInfo( + deviceId, + Build.MANUFACTURER, + Build.MODEL, + Build.VERSION.SDK_INT, + BuildConfig.VERSION_NAME + ) } - private val deviceSettings get() = DeviceManager.deviceData.deviceSettings ?: DeviceSettings.default() + private val deviceSettings + get() = DeviceManager.deviceData.deviceSettings ?: DeviceSettings.default() - fun getPlayItemRequestPayload(forceTranscode:Boolean):PlayItemRequestPayload { - return PlayItemRequestPayload(getMediaPlayer(), !forceTranscode, forceTranscode, getDeviceInfo()) + fun getPlayItemRequestPayload(forceTranscode: Boolean): PlayItemRequestPayload { + return PlayItemRequestPayload( + getMediaPlayer(), + !forceTranscode, + forceTranscode, + getDeviceInfo() + ) } - fun getContext():Context { + fun getContext(): Context { return ctx } @@ -988,19 +1066,27 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { // // MEDIA BROWSER STUFF (ANDROID AUTO) // - private val VALID_MEDIA_BROWSERS = mutableListOf("com.audiobookshelf.app", "com.audiobookshelf.app.debug", ANDROID_AUTO_PKG_NAME, ANDROID_AUTO_SIMULATOR_PKG_NAME, ANDROID_WEARABLE_PKG_NAME, ANDROID_GSEARCH_PKG_NAME, ANDROID_AUTOMOTIVE_PKG_NAME) + private val VALID_MEDIA_BROWSERS = + mutableListOf( + "com.audiobookshelf.app", + "com.audiobookshelf.app.debug", + ANDROID_AUTO_PKG_NAME, + ANDROID_AUTO_SIMULATOR_PKG_NAME, + ANDROID_WEARABLE_PKG_NAME, + ANDROID_GSEARCH_PKG_NAME, + ANDROID_AUTOMOTIVE_PKG_NAME + ) 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 - + private lateinit var browseTree: BrowseTree // Only allowing android auto or similar to access media browser service // normal loading of audiobooks is handled in webview (not natively) - private fun isValid(packageName: String, uid: Int) : Boolean { + private fun isValid(packageName: String, uid: Int): Boolean { Log.d(tag, "onGetRoot: Checking package $packageName with uid $uid") if (!VALID_MEDIA_BROWSERS.contains(packageName)) { Log.d(tag, "onGetRoot: package $packageName not valid for the media browser service") @@ -1009,7 +1095,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { return true } - override fun onGetRoot(clientPackageName: String, clientUid: Int, rootHints: Bundle?): BrowserRoot? { + override fun onGetRoot( + clientPackageName: String, + clientUid: Int, + rootHints: Bundle? + ): BrowserRoot? { // Verify that the specified package is allowed to access your content return if (!isValid(clientPackageName, clientUid)) { // No further calls will be made to other media browsing methods. @@ -1026,29 +1116,30 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { isAndroidAuto = true val extras = Bundle() - extras.putBoolean( - MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true + extras.putBoolean(MediaConstants.BROWSER_SERVICE_EXTRAS_KEY_SEARCH_SUPPORTED, true) + extras.putInt( + MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM ) extras.putInt( - MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_BROWSABLE, - MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM - ) - extras.putInt( - MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE, - MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM + MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_PLAYABLE, + MediaConstants.DESCRIPTION_EXTRAS_VALUE_CONTENT_STYLE_LIST_ITEM ) BrowserRoot(AUTO_MEDIA_ROOT, extras) } } - override fun onLoadChildren(parentMediaId: String, result: Result>) { + override fun onLoadChildren( + parentMediaId: String, + result: Result> + ) { Log.d(tag, "ON LOAD CHILDREN $parentMediaId") result.detach() // Prevent crashing if app is restarted while browsing - if ((parentMediaId != DOWNLOADS_ROOT && parentMediaId != AUTO_MEDIA_ROOT) && !firstLoadDone){ + if ((parentMediaId != DOWNLOADS_ROOT && parentMediaId != AUTO_MEDIA_ROOT) && !firstLoadDone) { result.sendResult(null) return } @@ -1064,23 +1155,24 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { 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 = mutableListOf() mediaManager.serverItemsInProgress.forEach { itemInProgress -> @@ -1088,31 +1180,62 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { val mediaDescription: MediaDescriptionCompat if (itemInProgress.episode != null) { if (itemInProgress.isLocal) { - progress = DeviceManager.dbManager.getLocalMediaProgress("${itemInProgress.libraryItemWrapper.id}-${itemInProgress.episode.id}") + progress = + DeviceManager.dbManager.getLocalMediaProgress( + "${itemInProgress.libraryItemWrapper.id}-${itemInProgress.episode.id}" + ) } else { - progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == itemInProgress.libraryItemWrapper.id && it.episodeId == itemInProgress.episode.id } + progress = + mediaManager.serverUserMediaProgress.find { + it.libraryItemId == itemInProgress.libraryItemWrapper.id && + it.episodeId == itemInProgress.episode.id + } // to show download icon - val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(itemInProgress.libraryItemWrapper.id) + val localLibraryItem = + DeviceManager.dbManager.getLocalLibraryItemByLId( + itemInProgress.libraryItemWrapper.id + ) localLibraryItem?.let { lli -> - val localEpisode = (lli.media as Podcast).episodes?.find { it.serverEpisodeId == itemInProgress.episode.id } + val localEpisode = + (lli.media as Podcast).episodes?.find { + it.serverEpisodeId == itemInProgress.episode.id + } itemInProgress.episode.localEpisodeId = localEpisode?.id } - } - mediaDescription = itemInProgress.episode.getMediaDescription(itemInProgress.libraryItemWrapper, progress, ctx) + mediaDescription = + itemInProgress.episode.getMediaDescription( + itemInProgress.libraryItemWrapper, + progress, + ctx + ) } else { if (itemInProgress.isLocal) { - progress = DeviceManager.dbManager.getLocalMediaProgress(itemInProgress.libraryItemWrapper.id) + progress = + DeviceManager.dbManager.getLocalMediaProgress( + itemInProgress.libraryItemWrapper.id + ) } else { - progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == itemInProgress.libraryItemWrapper.id } + progress = + mediaManager.serverUserMediaProgress.find { + it.libraryItemId == itemInProgress.libraryItemWrapper.id + } - val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(itemInProgress.libraryItemWrapper.id) - (itemInProgress.libraryItemWrapper as LibraryItem).localLibraryItemId = localLibraryItem?.id // To show downloaded icon + val localLibraryItem = + DeviceManager.dbManager.getLocalLibraryItemByLId( + itemInProgress.libraryItemWrapper.id + ) + (itemInProgress.libraryItemWrapper as LibraryItem).localLibraryItemId = + localLibraryItem?.id // To show downloaded icon } mediaDescription = itemInProgress.libraryItemWrapper.getMediaDescription(progress, ctx) } - localBrowseItems += MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE) + localBrowseItems += + MediaBrowserCompat.MediaItem( + mediaDescription, + MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + ) } result.sendResult(localBrowseItems) } else if (parentMediaId == AUTO_MEDIA_ROOT) { @@ -1121,33 +1244,49 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { 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) - } + 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?) firstLoadDone = true if (mediaManager.serverLibraries.isNotEmpty()) { Log.d(tag, "Starting personalization fetch") - mediaManager.populatePersonalizedDataForAllLibraries { - notifyChildrenChanged("/") - } + mediaManager.populatePersonalizedDataForAllLibraries { notifyChildrenChanged("/") } Log.d(tag, "Initialize inprogress items") - mediaManager.initializeInProgressItems { - notifyChildrenChanged("/") - } + 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, "Found top menu item: ${item.description.title}") - MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) - } + 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?) } @@ -1159,62 +1298,73 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } // 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) - } + 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?) } 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 - ) - } + val children = + libraryItems?.map { libraryItem -> + val mediaDescription = libraryItem.getMediaDescription(null, ctx) + MediaBrowserCompat.MediaItem( + mediaDescription, + MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + ) + } result.sendResult(children as MutableList?) } } 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 - ) - ) + 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 - ) + 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?) @@ -1231,62 +1381,67 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { if (mediaIdParts.size == 3) { mediaManager.getLibraryRecentShelfs(mediaIdParts[2]) { availableShelfs -> Log.d(tag, "Found ${availableShelfs.size} shelfs") - val children : MutableList = mutableListOf() + val children: MutableList = 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 - ) + 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 - ) + 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 - ) + 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 - ) + 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 - ) + MediaBrowserCompat.MediaItem( + MediaDescriptionCompat.Builder() + .setTitle("Authors") + .setMediaId("${parentMediaId}__AUTHORS") + .setIconUri(getUriToAbsIconDrawable(ctx, "authors")) + .build(), + MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + ) ) } } @@ -1296,58 +1451,97 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { mediaManager.getLibraryRecentShelfByType(mediaIdParts[2], mediaIdParts[3]) { shelf -> if (shelf === null) { result.sendResult(mutableListOf()) - }else { + } 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) - } + 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?) } 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 } + 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 - } + // 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) - } + val description = + libraryItem.recentEpisode.getMediaDescription( + libraryItem, + progress, + ctx + ) + MediaBrowserCompat.MediaItem( + description, + MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + ) + } result.sendResult(children as MutableList?) } 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 - ) - } + 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?) } 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) - } + 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?) } 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) - } + 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?) } else { result.sendResult(mutableListOf()) } - } } } @@ -1355,17 +1549,17 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { 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 - */ + 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) @@ -1377,24 +1571,39 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { 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 - ) - } + 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?) } else { - val children = seriesItems.map { seriesItem -> - val description = seriesItem.getMediaDescription(null, ctx) - MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) - } + val children = + seriesItems.map { seriesItem -> + val description = seriesItem.getMediaDescription(null, ctx) + MediaBrowserCompat.MediaItem( + description, + MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + ) + } result.sendResult(children as MutableList?) } } @@ -1402,46 +1611,62 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { 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 - ) - } + 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?) } else { - val children = seriesItems.map { seriesItem -> - val description = seriesItem.getMediaDescription(null, ctx) - MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) - } + val children = + seriesItems.map { seriesItem -> + val description = seriesItem.getMediaDescription(null, ctx) + MediaBrowserCompat.MediaItem( + description, + MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + ) + } result.sendResult(children as MutableList?) } } } 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 -> + 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) { + 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) - } + 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?) } } else if (mediaIdParts[3] == "AUTHORS" && mediaIdParts.size == 5) { @@ -1449,24 +1674,39 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { 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 - ) - } + 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?) } else { - val children = authorItems.map { authorItem -> - val description = authorItem.getMediaDescription(null, ctx) - MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) - } + val children = + authorItems.map { authorItem -> + val description = authorItem.getMediaDescription(null, ctx) + MediaBrowserCompat.MediaItem( + description, + MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + ) + } result.sendResult(children as MutableList?) } } @@ -1474,96 +1714,155 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { 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 - ) - } + 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?) } else { - val children = authorItems.map { authorItem -> - val description = authorItem.getMediaDescription(null, ctx) - MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE) - } + val children = + authorItems.map { authorItem -> + val description = authorItem.getMediaDescription(null, ctx) + MediaBrowserCompat.MediaItem( + description, + MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + ) + } result.sendResult(children as MutableList?) } } } 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) - } - } + 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?) } } else if (mediaIdParts[3] == "AUTHOR_SERIES") { - mediaManager.loadAuthorSeriesBooksWithAudio(mediaIdParts[2], mediaIdParts[4], mediaIdParts[5]) { libraryItems -> + mediaManager.loadAuthorSeriesBooksWithAudio( + mediaIdParts[2], + mediaIdParts[4], + mediaIdParts[5] + ) { libraryItems -> var items = libraryItems - if (DeviceManager.deviceData.deviceSettings!!.androidAutoBrowseSeriesSequenceOrder === AndroidAutoBrowseSeriesSequenceOrderSetting.DESC) { + 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) - } - } + 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?) } } 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) - } + val children = + collectionItems.map { collectionItem -> + val description = collectionItem.getMediaDescription(null, ctx) + MediaBrowserCompat.MediaItem( + description, + MediaBrowserCompat.MediaItem.FLAG_BROWSABLE + ) + } result.sendResult(children as MutableList?) } } else if (mediaIdParts[3] == "COLLECTION") { Log.d(tag, "Loading collection ${mediaIdParts[4]} books from library ${mediaIdParts[2]}") - mediaManager.loadLibraryCollectionBooksWithAudio(mediaIdParts[2], mediaIdParts[4]) { libraryItems -> + 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) - } + 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?) } } 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) - } + 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?) } } else { @@ -1571,13 +1870,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } } else { Log.d(tag, "Loading podcast episodes for podcast $parentMediaId") - mediaManager.loadPodcastEpisodeMediaBrowserItems(parentMediaId, ctx) { - result.sendResult(it) - } + mediaManager.loadPodcastEpisodeMediaBrowserItems(parentMediaId, ctx) { result.sendResult(it) } } } - override fun onSearch(query: String, extras: Bundle?, result: Result>) { + override fun onSearch( + query: String, + extras: Bundle?, + result: Result> + ) { result.detach() if (cachedSearch != query) { Log.d(tag, "Search bundle: $extras") @@ -1619,12 +1920,14 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { mAccelerometer = mSensorManager!!.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) mShakeDetector = ShakeDetector() - mShakeDetector!!.setOnShakeListener(object : ShakeDetector.OnShakeListener { - override fun onShake(count: Int) { - Log.d(tag, "PHONE SHAKE! $count") - sleepTimerManager.handleShake() - } - }) + mShakeDetector!!.setOnShakeListener( + object : ShakeDetector.OnShakeListener { + override fun onShake(count: Int) { + Log.d(tag, "PHONE SHAKE! $count") + sleepTimerManager.handleShake() + } + } + ) } // Shake sensor used for sleep timer @@ -1636,11 +1939,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { shakeSensorUnregisterTask?.cancel() Log.d(tag, "Registering shake SENSOR ${mAccelerometer?.isWakeUpSensor}") - val success = mSensorManager!!.registerListener( - mShakeDetector, - mAccelerometer, - SensorManager.SENSOR_DELAY_UI - ) + val success = + mSensorManager!!.registerListener( + mShakeDetector, + mAccelerometer, + SensorManager.SENSOR_DELAY_UI + ) if (success) isShakeSensorRegistered = true } @@ -1649,37 +1953,54 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { // Unregister shake sensor after wake up expiration shakeSensorUnregisterTask?.cancel() - shakeSensorUnregisterTask = Timer("ShakeUnregisterTimer", false).schedule(SLEEP_TIMER_WAKE_UP_EXPIRATION) { - Handler(Looper.getMainLooper()).post { - Log.d(tag, "wake time expired: Unregistering shake sensor") - mSensorManager!!.unregisterListener(mShakeDetector) - isShakeSensorRegistered = false - } - } + shakeSensorUnregisterTask = + Timer("ShakeUnregisterTimer", false).schedule(SLEEP_TIMER_WAKE_UP_EXPIRATION) { + Handler(Looper.getMainLooper()).post { + Log.d(tag, "wake time expired: Unregistering shake sensor") + mSensorManager!!.unregisterListener(mShakeDetector) + isShakeSensorRegistered = false + } + } } - private val networkCallback = object : ConnectivityManager.NetworkCallback() { - // Network capabilities have changed for the network - override fun onCapabilitiesChanged( - network: Network, - networkCapabilities: NetworkCapabilities - ) { - super.onCapabilitiesChanged(network, networkCapabilities) + private val networkCallback = + object : ConnectivityManager.NetworkCallback() { + // Network capabilities have changed for the network + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities + ) { + super.onCapabilitiesChanged(network, networkCapabilities) - isUnmeteredNetwork = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED) - 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("/") - } - } - } - } + isUnmeteredNetwork = + networkCapabilities.hasCapability( + NetworkCapabilities.NET_CAPABILITY_NOT_METERED + ) + 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("/") + } + } + } + } inner class JumpBackwardCustomActionProvider : CustomActionProvider { override fun onCustomAction(player: Player, action: String, extras: Bundle?) { @@ -1691,10 +2012,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? { return PlaybackStateCompat.CustomAction.Builder( - CUSTOM_ACTION_JUMP_BACKWARD, - getContext().getString(R.string.action_jump_backward), - R.drawable.exo_icon_rewind - ).build() + CUSTOM_ACTION_JUMP_BACKWARD, + getContext().getString(R.string.action_jump_backward), + R.drawable.exo_icon_rewind + ) + .build() } } @@ -1708,10 +2030,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? { return PlaybackStateCompat.CustomAction.Builder( - CUSTOM_ACTION_JUMP_FORWARD, - getContext().getString(R.string.action_jump_forward), - R.drawable.exo_icon_fastforward - ).build() + CUSTOM_ACTION_JUMP_FORWARD, + getContext().getString(R.string.action_jump_forward), + R.drawable.exo_icon_fastforward + ) + .build() } } @@ -1725,10 +2048,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? { return PlaybackStateCompat.CustomAction.Builder( - CUSTOM_ACTION_SKIP_FORWARD, - getContext().getString(R.string.action_skip_forward), - R.drawable.skip_next_24 - ).build() + CUSTOM_ACTION_SKIP_FORWARD, + getContext().getString(R.string.action_skip_forward), + R.drawable.skip_next_24 + ) + .build() } } @@ -1742,10 +2066,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? { return PlaybackStateCompat.CustomAction.Builder( - CUSTOM_ACTION_SKIP_BACKWARD, - getContext().getString(R.string.action_skip_backward), - R.drawable.skip_previous_24 - ).build() + CUSTOM_ACTION_SKIP_BACKWARD, + getContext().getString(R.string.action_skip_backward), + R.drawable.skip_previous_24 + ) + .build() } } @@ -1760,27 +2085,28 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? { val playbackRate = mediaManager.getSavedPlaybackRate() - // Rounding values in the event a non preset value (.5, 1, 1.2, 1.5, 2, 3) is selected in the phone app - val drawable: Int = when (playbackRate) { - in 0.5f..0.7f -> R.drawable.ic_play_speed_0_5x - in 0.8f..1.0f -> R.drawable.ic_play_speed_1_0x - in 1.1f..1.3f -> R.drawable.ic_play_speed_1_2x - in 1.4f..1.7f -> R.drawable.ic_play_speed_1_5x - in 1.8f..2.4f -> R.drawable.ic_play_speed_2_0x - in 2.5f..3.0f -> R.drawable.ic_play_speed_3_0x - // anything set above 3 will be show the 3x to save from creating 100 icons - else -> R.drawable.ic_play_speed_3_0x - } + // Rounding values in the event a non preset value (.5, 1, 1.2, 1.5, 2, 3) is selected in the + // phone app + val drawable: Int = + when (playbackRate) { + in 0.5f..0.7f -> R.drawable.ic_play_speed_0_5x + in 0.8f..1.0f -> R.drawable.ic_play_speed_1_0x + in 1.1f..1.3f -> R.drawable.ic_play_speed_1_2x + in 1.4f..1.7f -> R.drawable.ic_play_speed_1_5x + in 1.8f..2.4f -> R.drawable.ic_play_speed_2_0x + in 2.5f..3.0f -> R.drawable.ic_play_speed_3_0x + // anything set above 3 will be show the 3x to save from creating 100 icons + else -> R.drawable.ic_play_speed_3_0x + } val customActionExtras = Bundle() customActionExtras.putFloat("speed", playbackRate) return PlaybackStateCompat.CustomAction.Builder( - CUSTOM_ACTION_CHANGE_SPEED, - getContext().getString(R.string.action_change_speed), - drawable - ) - .setExtras(customActionExtras) - .build() + CUSTOM_ACTION_CHANGE_SPEED, + getContext().getString(R.string.action_change_speed), + drawable + ) + .setExtras(customActionExtras) + .build() } } } - diff --git a/components/cards/LazyBookCard.vue b/components/cards/LazyBookCard.vue index e27452c8..b1d10c4b 100644 --- a/components/cards/LazyBookCard.vue +++ b/components/cards/LazyBookCard.vue @@ -63,7 +63,7 @@
- {{ isLocalOnly ? 'task' : 'download_done' }} + {{ 'download_done' }}
@@ -153,10 +153,6 @@ export default { isLocal() { return !!this._libraryItem.isLocal }, - isLocalOnly() { - // Local item with no server match - return this.isLocal && !this._libraryItem.libraryItemId - }, media() { return this._libraryItem.media || {} }, diff --git a/components/cards/LazyListBookCard.vue b/components/cards/LazyListBookCard.vue index aa38dbb0..fd670910 100644 --- a/components/cards/LazyListBookCard.vue +++ b/components/cards/LazyListBookCard.vue @@ -31,7 +31,7 @@
- {{ isLocalOnly ? 'task' : 'download_done' }} + {{ 'download_done' }}
@@ -93,10 +93,6 @@ export default { isLocal() { return !!this._libraryItem.isLocal }, - isLocalOnly() { - // Local item with no server match - return this.isLocal && !this._libraryItem.libraryItemId - }, media() { return this._libraryItem.media || {} }, @@ -363,4 +359,4 @@ export default { min-width: var(--list-card-cover-width); max-width: var(--list-card-cover-width); } - \ No newline at end of file + diff --git a/pages/item/_id/index.vue b/pages/item/_id/index.vue index 51c0a7a0..b6d76e10 100644 --- a/pages/item/_id/index.vue +++ b/pages/item/_id/index.vue @@ -36,10 +36,7 @@
-
-

{{ $strings.MessageMediaNotLinkedToServer }}

-
-
+

{{ $getString('MessageMediaLinkedToADifferentServer', [localLibraryItem.serverAddress]) }}

@@ -240,10 +237,6 @@ export default { isLocal() { return this.libraryItem.isLocal }, - isLocalOnly() { - // TODO: Remove the possibility to have local only on android - return this.isLocal && !this.libraryItem.libraryItemId - }, hasLocal() { // Server library item has matching local library item return this.isLocal || this.libraryItem.localLibraryItem @@ -282,21 +275,21 @@ export default { * User is currently connected to a server and this local library item has the same server address */ isLocalMatchingServerAddress() { - if (this.isLocalOnly || !this.localLibraryItem || !this.currentServerAddress) return false + if (!this.localLibraryItem || !this.currentServerAddress) return false return this.localLibraryItem.serverAddress === this.currentServerAddress }, /** * User is currently connected to a server and this local library item has the same user id */ isLocalMatchingUser() { - if (this.isLocalOnly || !this.localLibraryItem || !this.user) return false + if (!this.localLibraryItem || !this.user) return false return this.localLibraryItem.serverUserId === this.user.id || this.localLibraryItem.serverUserId === this.user.oldUserId }, /** * User is currently connected to a server and this local library item has the same connection config id */ isLocalMatchingConnectionConfig() { - if (this.isLocalOnly || !this.localLibraryItemServerConnectionConfigId || !this.currentServerConnectionConfigId) return false + if (!this.localLibraryItemServerConnectionConfigId || !this.currentServerConnectionConfigId) return false return this.localLibraryItemServerConnectionConfigId === this.currentServerConnectionConfigId }, bookCoverAspectRatio() { diff --git a/pages/media/_id/history.vue b/pages/media/_id/history.vue index ae1e7f4c..fb38c67a 100644 --- a/pages/media/_id/history.vue +++ b/pages/media/_id/history.vue @@ -51,9 +51,6 @@ export default { if (!this.mediaItemHistory) return [] return (this.mediaItemHistory.events || []).sort((a, b) => b.timestamp - a.timestamp) }, - mediaItemIsLocal() { - return this.mediaItemHistory && this.mediaItemHistory.isLocal - }, mediaItemLibraryItemId() { if (!this.mediaItemHistory) return null return this.mediaItemHistory.libraryItemId @@ -91,7 +88,7 @@ export default { // Collapse saves if (evt.name === 'Save') { - let saveName = evt.name + "-" + evt.serverSyncAttempted + "-" + evt.serverSyncSuccess + let saveName = evt.name + '-' + evt.serverSyncAttempted + '-' + evt.serverSyncSuccess if (lastSaveName === saveName && numSaves > 0 && !keyUpdated) { include = false const totalInGroup = groups[key].length @@ -140,19 +137,14 @@ export default { }, playAtTime(startTime) { this.$store.commit('setPlayerIsStartingPlayback', this.mediaItemEpisodeId || this.mediaItemLibraryItemId) - if (this.mediaItemIsLocal) { - // Local only - this.$eventBus.$emit('play-item', { libraryItemId: this.mediaItemLibraryItemId, episodeId: this.mediaItemEpisodeId, startTime }) + // Server may have local + const localProg = this.$store.getters['globals/getLocalMediaProgressByServerItemId'](this.mediaItemLibraryItemId, this.mediaItemEpisodeId) + if (localProg) { + // Has local copy so prefer + this.$eventBus.$emit('play-item', { libraryItemId: localProg.localLibraryItemId, episodeId: localProg.localEpisodeId, serverLibraryItemId: this.mediaItemLibraryItemId, serverEpisodeId: this.mediaItemEpisodeId, startTime }) } else { - // Server may have local - const localProg = this.$store.getters['globals/getLocalMediaProgressByServerItemId'](this.mediaItemLibraryItemId, this.mediaItemEpisodeId) - if (localProg) { - // Has local copy so prefer - this.$eventBus.$emit('play-item', { libraryItemId: localProg.localLibraryItemId, episodeId: localProg.localEpisodeId, serverLibraryItemId: this.mediaItemLibraryItemId, serverEpisodeId: this.mediaItemEpisodeId, startTime }) - } else { - // Only on server - this.$eventBus.$emit('play-item', { libraryItemId: this.mediaItemLibraryItemId, episodeId: this.mediaItemEpisodeId, startTime }) - } + // Only on server + this.$eventBus.$emit('play-item', { libraryItemId: this.mediaItemLibraryItemId, episodeId: this.mediaItemEpisodeId, startTime }) } }, getEventIcon(name) { @@ -211,4 +203,4 @@ export default { if (this.onMediaItemHistoryUpdatedListener) this.onMediaItemHistoryUpdatedListener.remove() } } - \ No newline at end of file +