diff --git a/android/app/build.gradle b/android/app/build.gradle index 366ad747..2d75e3eb 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -29,8 +29,8 @@ android { applicationId "com.audiobookshelf.app" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 70 - versionName "0.9.41-beta" + versionCode 71 + versionName "0.9.42-beta" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" aaptOptions { // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. 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 index da40ef16..ac3a9548 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/AudioProbeResult.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/AudioProbeResult.kt @@ -2,11 +2,6 @@ package com.audiobookshelf.app.data import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnoreProperties -import com.fasterxml.jackson.annotation.JsonInclude -import com.fasterxml.jackson.annotation.JsonProperty -import com.fasterxml.jackson.core.json.JsonReadFeature -import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import com.fasterxml.jackson.databind.jsonschema.JsonSerializableSchema @JsonIgnoreProperties(ignoreUnknown = true) data class AudioProbeStream( @@ -27,15 +22,15 @@ data class AudioProbeChapterTags( @JsonIgnoreProperties(ignoreUnknown = true) data class AudioProbeChapter( val id:Int, - val start:Int, - val end:Int, + val start:Long, + val end:Long, val tags:AudioProbeChapterTags? ) { @JsonIgnore fun getBookChapter():BookChapter { - var startS = start / 1000.0 - var endS = end / 1000.0 - var title = tags?.title ?: "Chapter $id" + val startS = start / 1000.0 + val endS = end / 1000.0 + val title = tags?.title ?: "Chapter $id" return BookChapter(id, startS, endS, title) } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt index 5fe635c4..b87fa0dd 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DataClasses.kt @@ -85,7 +85,7 @@ class Podcast( ) : MediaType(metadata, coverPath) { @JsonIgnore override fun getAudioTracks():List { - var tracks = episodes?.map { it.audioTrack } + val tracks = episodes?.map { it.audioTrack } return tracks?.filterNotNull() ?: mutableListOf() } @JsonIgnore @@ -98,7 +98,7 @@ class Podcast( // Add new episodes audioTracks.forEach { at -> if (episodes?.find{ it.audioTrack?.localFileId == at.localFileId } == null) { - var newEpisode = PodcastEpisode("local_" + at.localFileId,episodes?.size ?: 0 + 1,null,null,at.title,null,null,null,at,at.duration,0, null) + val newEpisode = PodcastEpisode("local_" + at.localFileId,episodes?.size ?: 0 + 1,null,null,at.title,null,null,null,at,at.duration,0, null) episodes?.add(newEpisode) } } @@ -111,7 +111,7 @@ class Podcast( } @JsonIgnore override fun addAudioTrack(audioTrack:AudioTrack) { - var newEpisode = PodcastEpisode("local_" + audioTrack.localFileId,episodes?.size ?: 0 + 1,null,null,audioTrack.title,null,null,null,audioTrack,audioTrack.duration,0, null) + val newEpisode = PodcastEpisode("local_" + audioTrack.localFileId,episodes?.size ?: 0 + 1,null,null,audioTrack.title,null,null,null,audioTrack,audioTrack.duration,0, null) episodes?.add(newEpisode) var index = 1 @@ -132,7 +132,7 @@ class Podcast( } @JsonIgnore fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode) { - var newEpisode = PodcastEpisode("local_" + episode.id,episodes?.size ?: 0 + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,audioTrack,audioTrack.duration,0, episode.id) + val newEpisode = PodcastEpisode("local_" + episode.id,episodes?.size ?: 0 + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,audioTrack,audioTrack.duration,0, episode.id) episodes?.add(newEpisode) var index = 1 diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt index bf0bf721..9c2c39ab 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DbManager.kt @@ -17,9 +17,9 @@ class DbManager { } fun getLocalLibraryItems(mediaType:String? = null):MutableList { - var localLibraryItems:MutableList = mutableListOf() + val localLibraryItems:MutableList = mutableListOf() Paper.book("localLibraryItems").allKeys.forEach { - var localLibraryItem:LocalLibraryItem? = Paper.book("localLibraryItems").read(it) + val localLibraryItem:LocalLibraryItem? = Paper.book("localLibraryItems").read(it) if (localLibraryItem != null && (mediaType.isNullOrEmpty() || mediaType == localLibraryItem.mediaType)) { localLibraryItems.add(localLibraryItem) } @@ -28,7 +28,7 @@ class DbManager { } fun getLocalLibraryItemsInFolder(folderId:String):List { - var localLibraryItems = getLocalLibraryItems() + val localLibraryItems = getLocalLibraryItems() return localLibraryItems.filter { it.folderId == folderId } @@ -65,7 +65,7 @@ class DbManager { } fun getAllLocalFolders():List { - var localFolders:MutableList = mutableListOf() + val localFolders:MutableList = mutableListOf() Paper.book("localFolders").allKeys.forEach { localFolderId -> Paper.book("localFolders").read(localFolderId)?.let { localFolders.add(it) @@ -75,7 +75,7 @@ class DbManager { } fun removeLocalFolder(folderId:String) { - var localLibraryItems = getLocalLibraryItemsInFolder(folderId) + val localLibraryItems = getLocalLibraryItemsInFolder(folderId) localLibraryItems.forEach { Paper.book("localLibraryItems").delete(it.id) } @@ -91,7 +91,7 @@ class DbManager { } fun getDownloadItems():List { - var downloadItems:MutableList = mutableListOf() + val downloadItems:MutableList = mutableListOf() Paper.book("downloadItems").allKeys.forEach { downloadItemId -> Paper.book("downloadItems").read(downloadItemId)?.let { downloadItems.add(it) @@ -108,7 +108,7 @@ class DbManager { return Paper.book("localMediaProgress").read(localMediaProgressId) } fun getAllLocalMediaProgress():List { - var mediaProgress:MutableList = mutableListOf() + val mediaProgress:MutableList = mutableListOf() Paper.book("localMediaProgress").allKeys.forEach { localMediaProgressId -> Paper.book("localMediaProgress").read(localMediaProgressId)?.let { mediaProgress.add(it) @@ -126,14 +126,14 @@ class DbManager { // Make sure all local file ids still exist fun cleanLocalLibraryItems() { - var localLibraryItems = getLocalLibraryItems() + val localLibraryItems = getLocalLibraryItems() localLibraryItems.forEach { lli -> var hasUpates = false // Check local files lli.localFiles = lli.localFiles.filter { localFile -> - var file = File(localFile.absolutePath) + 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}") hasUpates = true @@ -143,7 +143,7 @@ class DbManager { // Check audio tracks and episodes if (lli.isPodcast) { - var podcast = lli.media as Podcast + 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}") @@ -152,7 +152,7 @@ class DbManager { ep.audioTrack != null && lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } != null } as MutableList } else { - var book = lli.media as Book + 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}") @@ -164,7 +164,7 @@ class DbManager { // Check cover still there lli.coverAbsolutePath?.let { - var coverFile = File(it) + val coverFile = File(it) if (!coverFile.exists()) { Log.d(tag, "cleanLocalLibraryItems: Cover $it was removed from library item ${lli.media.metadata.title}") @@ -183,10 +183,10 @@ class DbManager { // Remove any local media progress where the local media item is not found fun cleanLocalMediaProgress() { - var localMediaProgress = getAllLocalMediaProgress() - var localLibraryItems = getLocalLibraryItems() + val localMediaProgress = getAllLocalMediaProgress() + val localLibraryItems = getLocalLibraryItems() localMediaProgress.forEach { - var matchingLLI = localLibraryItems.find { lli -> lli.id == it.localLibraryItemId } + val matchingLLI = localLibraryItems.find { lli -> lli.id == it.localLibraryItemId } if (matchingLLI == null) { Log.d(tag, "cleanLocalMediaProgress: No matching local library item for local media progress ${it.id} - removing") Paper.book("localMediaProgress").delete(it.id) @@ -195,8 +195,8 @@ class DbManager { Log.d(tag, "cleanLocalMediaProgress: Podcast media progress has no episode id - removing") Paper.book("localMediaProgress").delete(it.id) } else { - var podcast = matchingLLI.media as Podcast - var matchingLEp = podcast.episodes?.find { ep -> ep.id == it.localEpisodeId } + 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") Paper.book("localMediaProgress").delete(it.id) @@ -212,15 +212,4 @@ class DbManager { fun getLocalPlaybackSession(playbackSessionId:String):PlaybackSession? { return Paper.book("localPlaybackSession").read(playbackSessionId) } - - fun saveObject(db:String, key:String, value:JSONObject) { - Log.d(tag, "Saving Object $key ${value.toString()}") - Paper.book(db).write(key, value) - } - - fun loadObject(db:String, key:String):JSONObject? { - var json: JSONObject? = Paper.book(db).read(key) - Log.d(tag, "Loaded Object $key $json") - return json - } } 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 0737a536..ec159a43 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 @@ -251,7 +251,7 @@ class FolderScanner(var ctx: Context) { Log.d(tag, "scanDownloadItem starting for ${downloadItem.itemFolderPath} | ${df.uri} | Item Folder Id:$itemFolderId | LLI Id:$localLibraryItemId") // Search for files in media item folder - var filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*")) + var filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4")) Log.d(tag, "scanDownloadItem ${filesFound.size} files found in ${downloadItem.itemFolderPath}") var localLibraryItem:LocalLibraryItem? = null @@ -349,7 +349,7 @@ class FolderScanner(var ctx: Context) { var wasUpdated = false // Search for files in media item folder - var filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*")) + var filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4")) Log.d(tag, "scanLocalLibraryItem ${filesFound.size} files found in ${localLibraryItem.absolutePath}") filesFound.forEach { diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt b/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt index 9676fdfa..512ad91a 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionCallback.kt @@ -30,7 +30,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) { Log.d(tag, "About to prepare player with ${it.displayTitle}") Handler(Looper.getMainLooper()).post() { - playerNotificationService.preparePlayer(it,true) + playerNotificationService.preparePlayer(it,true,null) } } } @@ -52,7 +52,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) { Log.d(tag, "About to prepare player with ${it.displayTitle}") Handler(Looper.getMainLooper()).post() { - playerNotificationService.preparePlayer(it,true) + playerNotificationService.preparePlayer(it,true,null) } } } @@ -100,7 +100,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) { Log.d(tag, "About to prepare player with ${it.displayTitle}") Handler(Looper.getMainLooper()).post() { - playerNotificationService.preparePlayer(it,true) + playerNotificationService.preparePlayer(it,true,null) } } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionPlaybackPreparer.kt b/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionPlaybackPreparer.kt index a5ccd8c0..109b755d 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionPlaybackPreparer.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/player/MediaSessionPlaybackPreparer.kt @@ -32,7 +32,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat playerNotificationService.mediaManager.getFirstItem()?.let { li -> playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) { Handler(Looper.getMainLooper()).post() { - playerNotificationService.preparePlayer(it,playWhenReady) + playerNotificationService.preparePlayer(it,playWhenReady,null) } } } @@ -46,7 +46,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) { Log.d(tag, "About to prepare player with ${it.displayTitle}") Handler(Looper.getMainLooper()).post() { - playerNotificationService.preparePlayer(it,playWhenReady) + playerNotificationService.preparePlayer(it,playWhenReady,null) } } } @@ -58,7 +58,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) { Log.d(tag, "About to prepare player with ${it.displayTitle}") Handler(Looper.getMainLooper()).post() { - playerNotificationService.preparePlayer(it,playWhenReady) + playerNotificationService.preparePlayer(it,playWhenReady,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 6b7dff07..875ca690 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 @@ -9,7 +9,6 @@ import android.hardware.SensorManager import android.os.* import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.MediaDescriptionCompat -import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.session.MediaControllerCompat import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.PlaybackStateCompat @@ -31,7 +30,6 @@ 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 io.paperdb.Paper import java.util.* import kotlin.concurrent.schedule @@ -82,8 +80,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { private var channelName = "Audiobookshelf Channel" private var currentPlaybackSession:PlaybackSession? = null + private var initialPlaybackRate:Float? = null - var isAndroidAuto = false + private var isAndroidAuto = false // The following are used for the shake detection private var isShakeSensorRegistered:Boolean = false @@ -164,7 +163,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { ctx = this // Initialize player - var customLoadControl:LoadControl = DefaultLoadControl.Builder().setBufferDurationsMs( + val customLoadControl:LoadControl = DefaultLoadControl.Builder().setBufferDurationsMs( 1000 * 20, // 20s min buffer 1000 * 45, // 45s max buffer 1000 * 5, // 5s playback start @@ -178,7 +177,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { .build() mPlayer.setHandleAudioBecomingNoisy(true) mPlayer.addListener(PlayerListener(this)) - var audioAttributes:AudioAttributes = AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).setContentType(C.CONTENT_TYPE_SPEECH).build() + val audioAttributes:AudioAttributes = AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).setContentType(C.CONTENT_TYPE_SPEECH).build() mPlayer.setAudioAttributes(audioAttributes, true) currentPlayer = mPlayer @@ -205,7 +204,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { val sessionActivityPendingIntent = packageManager?.getLaunchIntentForPackage(packageName)?.let { sessionIntent -> - PendingIntent.getActivity(this, 0, sessionIntent, 0) + PendingIntent.getActivity(this, 0, sessionIntent, PendingIntent.FLAG_IMMUTABLE) } mediaSession = MediaSessionCompat(this, tag) @@ -275,7 +274,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { /* User callable methods */ - fun preparePlayer(playbackSession: PlaybackSession, playWhenReady:Boolean) { + fun preparePlayer(playbackSession: PlaybackSession, playWhenReady:Boolean, playbackRate:Float?) { + val playbackRateToUse = playbackRate ?: initialPlaybackRate ?: 1f + initialPlaybackRate = playbackRate + playbackSession.mediaPlayer = getMediaPlayer() if (playbackSession.mediaPlayer == "cast-player" && playbackSession.isLocal) { @@ -296,9 +298,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { clientEventEmitter?.onPlaybackSession(playbackSession) - var metadata = playbackSession.getMediaMetadataCompat() + val metadata = playbackSession.getMediaMetadataCompat() mediaSession.setMetadata(metadata) - var mediaItems = playbackSession.getMediaItems() + val mediaItems = playbackSession.getMediaItems() if (mediaItems.isEmpty()) { Log.e(tag, "Invalid playback session no media items to play") @@ -307,20 +309,20 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } if (mPlayer == currentPlayer) { - var mediaSource:MediaSource + val mediaSource:MediaSource if (playbackSession.isLocal) { Log.d(tag, "Playing Local Item") - var dataSourceFactory = DefaultDataSource.Factory(ctx) + val dataSourceFactory = DefaultDataSource.Factory(ctx) mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItems[0]) } else if (!playbackSession.isHLS) { Log.d(tag, "Direct Playing Item") - var dataSourceFactory = DefaultHttpDataSource.Factory() + val dataSourceFactory = DefaultHttpDataSource.Factory() dataSourceFactory.setUserAgent(channelId) mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItems[0]) } else { Log.d(tag, "Playing HLS Item") - var dataSourceFactory = DefaultHttpDataSource.Factory() + val dataSourceFactory = DefaultHttpDataSource.Factory() dataSourceFactory.setUserAgent(channelId) dataSourceFactory.setDefaultRequestProperties(hashMapOf("Authorization" to "Bearer ${DeviceManager.token}")) mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItems[0]) @@ -333,8 +335,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { currentPlayer.addMediaItems(mediaItems.subList(1, mediaItems.size)) Log.d(tag, "currentPlayer total media items ${currentPlayer.mediaItemCount}") - var currentTrackIndex = playbackSession.getCurrentTrackIndex() - var currentTrackTime = playbackSession.getCurrentTrackTimeMs() + val currentTrackIndex = playbackSession.getCurrentTrackIndex() + val currentTrackTime = playbackSession.getCurrentTrackTimeMs() Log.d(tag, "currentPlayer current track index $currentTrackIndex & current track time $currentTrackTime") currentPlayer.seekTo(currentTrackIndex, currentTrackTime) } else { @@ -343,16 +345,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { Log.d(tag, "Prepare complete for session ${currentPlaybackSession?.displayTitle} | ${currentPlayer.mediaItemCount}") currentPlayer.playWhenReady = playWhenReady - currentPlayer.setPlaybackSpeed(1f) // TODO: Playback speed should come from settings + currentPlayer.setPlaybackSpeed(playbackRateToUse) currentPlayer.prepare() - } else if (castPlayer != null) { - var currentTrackIndex = playbackSession.getCurrentTrackIndex() - var currentTrackTime = playbackSession.getCurrentTrackTimeMs() - var mediaType = playbackSession.mediaType + val currentTrackIndex = playbackSession.getCurrentTrackIndex() + val currentTrackTime = playbackSession.getCurrentTrackTimeMs() + val mediaType = playbackSession.mediaType Log.d(tag, "Loading cast player $currentTrackIndex $currentTrackTime $mediaType") - castPlayer?.load(mediaItems, currentTrackIndex, currentTrackTime, playWhenReady, 1f, mediaType) + castPlayer?.load(mediaItems, currentTrackIndex, currentTrackTime, playWhenReady, playbackRateToUse, mediaType) } } @@ -360,14 +361,14 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { // On error and was attempting to direct play - fallback to transcode currentPlaybackSession?.let { playbackSession -> if (playbackSession.isDirectPlay) { - var mediaPlayer = getMediaPlayer() + val mediaPlayer = getMediaPlayer() Log.d(tag, "Fallback to transcode $mediaPlayer") - var libraryItemId = playbackSession.libraryItemId ?: "" // Must be true since direct play - var episodeId = playbackSession.episodeId + val libraryItemId = playbackSession.libraryItemId ?: "" // Must be true since direct play + val episodeId = playbackSession.episodeId apiHandler.playLibraryItem(libraryItemId, episodeId, true, mediaPlayer) { Handler(Looper.getMainLooper()).post() { - preparePlayer(it, true) + preparePlayer(it, true, null) } } } else { @@ -378,7 +379,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } fun switchToPlayer(useCastPlayer: Boolean) { - var wasPlaying = currentPlayer.isPlaying + val wasPlaying = currentPlayer.isPlaying if (useCastPlayer) { if (currentPlayer == castPlayer) { Log.d(tag, "switchToPlayer: Already using Cast Player " + castPlayer?.deviceInfo) @@ -420,7 +421,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { if (wasPlaying) { // media is paused when switching players clientEventEmitter?.onPlayingUpdate(false) } - preparePlayer(it, false) + preparePlayer(it, false, null) } } @@ -499,8 +500,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { Log.d(tag, "seekPlayer mediaCount = ${currentPlayer.mediaItemCount} | $time") if (currentPlayer.mediaItemCount > 1) { currentPlaybackSession?.currentTime = time / 1000.0 - var newWindowIndex = currentPlaybackSession?.getCurrentTrackIndex() ?: 0 - var newTimeOffset = currentPlaybackSession?.getCurrentTrackTimeMs() ?: 0 + val newWindowIndex = currentPlaybackSession?.getCurrentTrackIndex() ?: 0 + val newTimeOffset = currentPlaybackSession?.getCurrentTrackTimeMs() ?: 0 currentPlayer.seekTo(newWindowIndex, newTimeOffset) } else { currentPlayer.seekTo(time) @@ -528,7 +529,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { } fun sendClientMetadata(playerState: PlayerState) { - var duration = currentPlaybackSession?.getTotalDuration() ?: 0.0 + val duration = currentPlaybackSession?.getTotalDuration() ?: 0.0 clientEventEmitter?.onMetadata(PlaybackMetadata(duration, getCurrentTimeSeconds(), playerState)) } @@ -593,7 +594,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { override fun onLoadChildren(parentMediaId: String, result: Result>) { Log.d(tag, "ON LOAD CHILDREN $parentMediaId") - var flag = if (parentMediaId == AUTO_MEDIA_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE + val flag = if (parentMediaId == AUTO_MEDIA_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE result.detach() @@ -652,7 +653,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() { shakeSensorUnregisterTask?.cancel() Log.d(tag, "Registering shake SENSOR ${mAccelerometer?.isWakeUpSensor}") - var success = mSensorManager!!.registerListener( + val success = mSensorManager!!.registerListener( mShakeDetector, mAccelerometer, SensorManager.SENSOR_DELAY_UI diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt index 2d62af52..3bf086d8 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsAudioPlayer.kt @@ -23,8 +23,8 @@ class AbsAudioPlayer : Plugin() { private val tag = "AbsAudioPlayer" var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) - lateinit var mainActivity: MainActivity - lateinit var apiHandler:ApiHandler + private lateinit var mainActivity: MainActivity + private lateinit var apiHandler:ApiHandler lateinit var castManager:CastManager lateinit var playerNotificationService: PlayerNotificationService @@ -37,7 +37,7 @@ class AbsAudioPlayer : Plugin() { initCastManager() - var foregroundServiceReady : () -> Unit = { + val foregroundServiceReady : () -> Unit = { playerNotificationService = mainActivity.foregroundService playerNotificationService.clientEventEmitter = (object : PlayerNotificationService.ClientEventEmitter { @@ -58,7 +58,7 @@ class AbsAudioPlayer : Plugin() { } override fun onPrepare(audiobookId: String, playWhenReady: Boolean) { - var jsobj = JSObject() + val jsobj = JSObject() jsobj.put("audiobookId", audiobookId) jsobj.put("playWhenReady", playWhenReady) notifyListeners("onPrepareMedia", jsobj) @@ -89,13 +89,13 @@ class AbsAudioPlayer : Plugin() { } fun emit(evtName: String, value: Any) { - var ret = JSObject() + val ret = JSObject() ret.put("value", value) notifyListeners(evtName, ret) } - fun initCastManager() { - var connListener = object: CastManager.ChromecastListener() { + private fun initCastManager() { + val connListener = object: CastManager.ChromecastListener() { override fun onReceiverAvailableUpdate(available: Boolean) { Log.d(tag, "ChromecastListener: CAST Receiver Update Available $available") isCastAvailable = available @@ -141,9 +141,10 @@ class AbsAudioPlayer : Plugin() { } } - var libraryItemId = call.getString("libraryItemId", "").toString() - var episodeId = call.getString("episodeId", "").toString() - var playWhenReady = call.getBoolean("playWhenReady") == true + val libraryItemId = call.getString("libraryItemId", "").toString() + val episodeId = call.getString("episodeId", "").toString() + val playWhenReady = call.getBoolean("playWhenReady") == true + var playbackRate = call.getFloat("playbackRate",1f) ?: 1f if (libraryItemId.isEmpty()) { Log.e(tag, "Invalid call to play library item no library item id") @@ -153,8 +154,8 @@ class AbsAudioPlayer : Plugin() { if (libraryItemId.startsWith("local")) { // Play local media item DeviceManager.dbManager.getLocalLibraryItem(libraryItemId)?.let { var episode: PodcastEpisode? = null - if (!episodeId.isNullOrEmpty()) { - var podcastMedia = it.media as Podcast + if (episodeId.isNotEmpty()) { + val podcastMedia = it.media as Podcast episode = podcastMedia.episodes?.find { ep -> ep.id == episodeId } if (episode == null) { Log.e(tag, "prepareLibraryItem: Podcast episode not found $episodeId") @@ -162,21 +163,21 @@ class AbsAudioPlayer : Plugin() { } } - Handler(Looper.getMainLooper()).post() { + Handler(Looper.getMainLooper()).post { Log.d(tag, "prepareLibraryItem: Preparing Local Media item ${jacksonMapper.writeValueAsString(it)}") - var playbackSession = it.getPlaybackSession(episode) - playerNotificationService.preparePlayer(playbackSession, playWhenReady) + val playbackSession = it.getPlaybackSession(episode) + playerNotificationService.preparePlayer(playbackSession, playWhenReady, playbackRate) } return call.resolve(JSObject()) } } else { // Play library item from server - var mediaPlayer = playerNotificationService.getMediaPlayer() + val mediaPlayer = playerNotificationService.getMediaPlayer() apiHandler.playLibraryItem(libraryItemId, episodeId, false, mediaPlayer) { - Handler(Looper.getMainLooper()).post() { + Handler(Looper.getMainLooper()).post { Log.d(tag, "Preparing Player TEST ${jacksonMapper.writeValueAsString(it)}") - playerNotificationService.preparePlayer(it, playWhenReady) + playerNotificationService.preparePlayer(it, playWhenReady, playbackRate) } call.resolve(JSObject(jacksonMapper.writeValueAsString(it))) @@ -186,9 +187,9 @@ class AbsAudioPlayer : Plugin() { @PluginMethod fun getCurrentTime(call: PluginCall) { - Handler(Looper.getMainLooper()).post() { - var currentTime = playerNotificationService.getCurrentTimeSeconds() - var bufferedTime = playerNotificationService.getBufferedTimeSeconds() + Handler(Looper.getMainLooper()).post { + val currentTime = playerNotificationService.getCurrentTimeSeconds() + val bufferedTime = playerNotificationService.getBufferedTimeSeconds() val ret = JSObject() ret.put("value", currentTime) ret.put("bufferedTime", bufferedTime) @@ -198,7 +199,7 @@ class AbsAudioPlayer : Plugin() { @PluginMethod fun pausePlayer(call: PluginCall) { - Handler(Looper.getMainLooper()).post() { + Handler(Looper.getMainLooper()).post { playerNotificationService.pause() call.resolve() } @@ -206,7 +207,7 @@ class AbsAudioPlayer : Plugin() { @PluginMethod fun playPlayer(call: PluginCall) { - Handler(Looper.getMainLooper()).post() { + Handler(Looper.getMainLooper()).post { playerNotificationService.play() call.resolve() } @@ -214,16 +215,16 @@ class AbsAudioPlayer : Plugin() { @PluginMethod fun playPause(call: PluginCall) { - Handler(Looper.getMainLooper()).post() { - var playing = playerNotificationService.playPause() + Handler(Looper.getMainLooper()).post { + val playing = playerNotificationService.playPause() call.resolve(JSObject("{\"playing\":$playing}")) } } @PluginMethod fun seek(call: PluginCall) { - var time:Int = call.getInt("value", 0) ?: 0 // Value in seconds - Handler(Looper.getMainLooper()).post() { + val time:Int = call.getInt("value", 0) ?: 0 // Value in seconds + Handler(Looper.getMainLooper()).post { playerNotificationService.seekPlayer(time * 1000L) // convert to ms call.resolve() } @@ -231,8 +232,8 @@ class AbsAudioPlayer : Plugin() { @PluginMethod fun seekForward(call: PluginCall) { - var amount:Int = call.getInt("value", 0) ?: 0 - Handler(Looper.getMainLooper()).post() { + val amount:Int = call.getInt("value", 0) ?: 0 + Handler(Looper.getMainLooper()).post { playerNotificationService.seekForward(amount * 1000L) // convert to ms call.resolve() } @@ -240,8 +241,8 @@ class AbsAudioPlayer : Plugin() { @PluginMethod fun seekBackward(call: PluginCall) { - var amount:Int = call.getInt("value", 0) ?: 0 // Value in seconds - Handler(Looper.getMainLooper()).post() { + val amount:Int = call.getInt("value", 0) ?: 0 // Value in seconds + Handler(Looper.getMainLooper()).post { playerNotificationService.seekBackward(amount * 1000L) // convert to ms call.resolve() } @@ -249,9 +250,9 @@ class AbsAudioPlayer : Plugin() { @PluginMethod fun setPlaybackSpeed(call: PluginCall) { - var playbackSpeed:Float = call.getFloat("value", 1.0f) ?: 1.0f + val playbackSpeed:Float = call.getFloat("value", 1.0f) ?: 1.0f - Handler(Looper.getMainLooper()).post() { + Handler(Looper.getMainLooper()).post { playerNotificationService.setPlaybackSpeed(playbackSpeed) call.resolve() } @@ -259,7 +260,7 @@ class AbsAudioPlayer : Plugin() { @PluginMethod fun closePlayback(call: PluginCall) { - Handler(Looper.getMainLooper()).post() { + Handler(Looper.getMainLooper()).post { playerNotificationService.closePlayback() call.resolve() } @@ -267,11 +268,11 @@ class AbsAudioPlayer : Plugin() { @PluginMethod fun setSleepTimer(call: PluginCall) { - var time:Long = call.getString("time", "360000")!!.toLong() - var isChapterTime:Boolean = call.getBoolean("isChapterTime", false) == true + val time:Long = call.getString("time", "360000")!!.toLong() + val isChapterTime:Boolean = call.getBoolean("isChapterTime", false) == true - Handler(Looper.getMainLooper()).post() { - var success:Boolean = playerNotificationService.sleepTimerManager.setSleepTimer(time, isChapterTime) + Handler(Looper.getMainLooper()).post { + val success:Boolean = playerNotificationService.sleepTimerManager.setSleepTimer(time, isChapterTime) val ret = JSObject() ret.put("success", success) call.resolve(ret) @@ -280,7 +281,7 @@ class AbsAudioPlayer : Plugin() { @PluginMethod fun getSleepTimerTime(call: PluginCall) { - var time = playerNotificationService.sleepTimerManager.getSleepTimerTime() + val time = playerNotificationService.sleepTimerManager.getSleepTimerTime() val ret = JSObject() ret.put("value", time) call.resolve(ret) @@ -288,9 +289,9 @@ class AbsAudioPlayer : Plugin() { @PluginMethod fun increaseSleepTime(call: PluginCall) { - var time:Long = call.getString("time", "300000")!!.toLong() + val time:Long = call.getString("time", "300000")!!.toLong() - Handler(Looper.getMainLooper()).post() { + Handler(Looper.getMainLooper()).post { playerNotificationService.sleepTimerManager.increaseSleepTime(time) val ret = JSObject() ret.put("success", true) @@ -300,9 +301,9 @@ class AbsAudioPlayer : Plugin() { @PluginMethod fun decreaseSleepTime(call: PluginCall) { - var time:Long = call.getString("time", "300000")!!.toLong() + val time:Long = call.getString("time", "300000")!!.toLong() - Handler(Looper.getMainLooper()).post() { + Handler(Looper.getMainLooper()).post { playerNotificationService.sleepTimerManager.decreaseSleepTime(time) val ret = JSObject() ret.put("success", true) @@ -338,7 +339,7 @@ class AbsAudioPlayer : Plugin() { @PluginMethod fun getIsCastAvailable(call: PluginCall) { - var jsobj = JSObject() + val jsobj = JSObject() jsobj.put("value", isCastAvailable) call.resolve(jsobj) } diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt index cb79822e..a6c26c0a 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDatabase.kt @@ -288,38 +288,4 @@ class AbsDatabase : Plugin() { call.resolve() } } - - // - // Generic Webview calls to db - // - @PluginMethod - fun saveFromWebview(call: PluginCall) { - var db = call.getString("db", "").toString() - var key = call.getString("key", "").toString() - var value = call.getObject("value") - - GlobalScope.launch(Dispatchers.IO) { - if (db == "" || key == "" || value == null) { - Log.d(tag, "saveFromWebview Invalid key/value") - } else { - var json = value as JSONObject - DeviceManager.dbManager.saveObject(db, key, json) - } - call.resolve() - } - } - - @PluginMethod - fun loadFromWebview(call:PluginCall) { - var db = call.getString("db", "").toString() - var key = call.getString("key", "").toString() - if (db == "" || key == "") { - Log.d(tag, "loadFromWebview Invalid Key") - call.resolve() - return - } - var json = DeviceManager.dbManager.loadObject(db, key) - var jsobj = JSObject.fromJSONObject(json) - call.resolve(jsobj) - } } diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt index e7b23d95..00ebaba9 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt @@ -4,7 +4,12 @@ import android.app.DownloadManager import android.content.Context import android.net.Uri import android.os.Build +import android.os.Environment import android.util.Log +import androidx.documentfile.provider.DocumentFile +import com.anggrayudi.storage.callback.FileCallback +import com.anggrayudi.storage.file.* +import com.anggrayudi.storage.media.FileDescription import com.audiobookshelf.app.MainActivity import com.audiobookshelf.app.data.* import com.audiobookshelf.app.device.DeviceManager @@ -37,19 +42,51 @@ class AbsDownloader : Plugin() { data class DownloadItemPart( val id: String, val filename: String, - val destinationPath:String, + val finalDestinationPath:String, val itemTitle: String, val serverPath: String, val localFolderName: String, + val localFolderUrl: String, val localFolderId: String, val audioTrack: AudioTrack?, val episode:PodcastEpisode?, var completed:Boolean, + var moved:Boolean, + var failed:Boolean, @JsonIgnore val uri: Uri, @JsonIgnore val destinationUri: Uri, + @JsonIgnore val finalDestinationUri: Uri, var downloadId: Long?, var progress: Long ) { + companion object { + fun make(filename:String, destinationFile:File, finalDestinationFile:File, itemTitle:String, serverPath:String, localFolder:LocalFolder, audioTrack:AudioTrack?, episode:PodcastEpisode?) :DownloadItemPart { + val destinationUri = Uri.fromFile(destinationFile) + val finalDestinationUri = Uri.fromFile(finalDestinationFile) + val downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}") + Log.d("DownloadItemPart", "Audio File Destination Uri: $destinationUri | Final Destination Uri: $finalDestinationUri | Download URI $downloadUri") + return DownloadItemPart( + id = DeviceManager.getBase64Id(finalDestinationFile.absolutePath), + filename = filename, finalDestinationFile.absolutePath, + itemTitle = itemTitle, + serverPath = serverPath, + localFolderName = localFolder.name, + localFolderUrl = localFolder.contentUrl, + localFolderId = localFolder.id, + audioTrack = audioTrack, + episode = episode, + completed = false, + moved = false, + failed = false, + uri = downloadUri, + destinationUri = destinationUri, + finalDestinationUri = finalDestinationUri, + downloadId = null, + progress = 0 + ) + } + } + @JsonIgnore fun getDownloadRequest(): DownloadManager.Request { val dlRequest = DownloadManager.Request(uri) @@ -92,10 +129,10 @@ class AbsDownloader : Plugin() { val libraryItemId = call.data.getString("libraryItemId").toString() var episodeId = call.data.getString("episodeId").toString() if (episodeId == "null") episodeId = "" - var localFolderId = call.data.getString("localFolderId").toString() + val localFolderId = call.data.getString("localFolderId").toString() Log.d(tag, "Download library item $libraryItemId to folder $localFolderId / episode: $episodeId") - var downloadId = if (episodeId.isNullOrEmpty()) libraryItemId else "$libraryItemId-$episodeId" + val downloadId = if (episodeId.isEmpty()) libraryItemId else "$libraryItemId-$episodeId" if (downloadQueue.find { it.id == downloadId } != null) { Log.d(tag, "Download already started for this media entity $downloadId") return call.resolve(JSObject("{\"error\":\"Download already started for this media entity\"}")) @@ -104,15 +141,15 @@ class AbsDownloader : Plugin() { apiHandler.getLibraryItem(libraryItemId) { libraryItem -> Log.d(tag, "Got library item from server ${libraryItem.id}") - var localFolder = DeviceManager.dbManager.getLocalFolder(localFolderId) + val localFolder = DeviceManager.dbManager.getLocalFolder(localFolderId) if (localFolder != null) { - if (!episodeId.isNullOrEmpty() && libraryItem.mediaType != "podcast") { + if (episodeId.isNotEmpty() && libraryItem.mediaType != "podcast") { Log.e(tag, "Library item is not a podcast but episode was requested") call.resolve(JSObject("{\"error\":\"Invalid library item not a podcast\"}")) - } else if (!episodeId.isNullOrEmpty()) { - var podcast = libraryItem.media as Podcast - var episode = podcast.episodes?.find { podcastEpisode -> + } else if (episodeId.isNotEmpty()) { + val podcast = libraryItem.media as Podcast + val episode = podcast.episodes?.find { podcastEpisode -> podcastEpisode.id == episodeId } if (episode == null) { @@ -132,77 +169,70 @@ class AbsDownloader : Plugin() { } // Clean folder path so it can be used in URL - fun cleanRelPath(relPath: String): String { - var cleanedRelPath = relPath.replace("\\", "/").replace("%", "%25").replace("#", "%23") + private fun cleanRelPath(relPath: String): String { + val cleanedRelPath = relPath.replace("\\", "/").replace("%", "%25").replace("#", "%23") return if (cleanedRelPath.startsWith("/")) cleanedRelPath.substring(1) else cleanedRelPath } - // Item filenames could be the same if they are in subfolders, this will make them unique - fun getFilenameFromRelPath(relPath: String): String { - var cleanedRelPath = relPath.replace("\\", "_").replace("/", "_") + // Item filenames could be the same if they are in sub-folders, this will make them unique + private fun getFilenameFromRelPath(relPath: String): String { + val cleanedRelPath = relPath.replace("\\", "_").replace("/", "_") return if (cleanedRelPath.startsWith("_")) cleanedRelPath.substring(1) else cleanedRelPath } - fun startLibraryItemDownload(libraryItem: LibraryItem, localFolder: LocalFolder, episode:PodcastEpisode?) { + private fun startLibraryItemDownload(libraryItem: LibraryItem, localFolder: LocalFolder, episode:PodcastEpisode?) { + val tempFolderPath = mainActivity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) + if (libraryItem.mediaType == "book") { - var bookTitle = libraryItem.media.metadata.title - var tracks = libraryItem.media.getAudioTracks() + val bookTitle = libraryItem.media.metadata.title + val tracks = libraryItem.media.getAudioTracks() Log.d(tag, "Starting library item download with ${tracks.size} tracks") - var itemFolderPath = localFolder.absolutePath + "/" + bookTitle - var downloadItem = DownloadItem(libraryItem.id, libraryItem.id, null,DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, libraryItem.media, mutableListOf()) + val itemFolderPath = localFolder.absolutePath + "/" + bookTitle + val downloadItem = DownloadItem(libraryItem.id, libraryItem.id, null,DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, libraryItem.media, mutableListOf()) // Create download item part for each audio track tracks.forEach { audioTrack -> - var serverPath = "/s/item/${libraryItem.id}/${cleanRelPath(audioTrack.relPath)}" - var destinationFilename = getFilenameFromRelPath(audioTrack.relPath) + val serverPath = "/s/item/${libraryItem.id}/${cleanRelPath(audioTrack.relPath)}" + val destinationFilename = getFilenameFromRelPath(audioTrack.relPath) Log.d(tag, "Audio File Server Path $serverPath | AF RelPath ${audioTrack.relPath} | LocalFolder Path ${localFolder.absolutePath} | DestName ${destinationFilename}") - var destinationFile = File("$itemFolderPath/$destinationFilename") - if (destinationFile.exists()) { - Log.d(tag, "Audio file already exists, removing it from ${destinationFile.absolutePath}") - destinationFile.delete() + val finalDestinationFile = File("$itemFolderPath/$destinationFilename") + val destinationFile = File("$tempFolderPath/$destinationFilename") + + if (finalDestinationFile.exists()) { + Log.d(tag, "Audio file already exists, removing it from ${finalDestinationFile.absolutePath}") + finalDestinationFile.delete() } - var destinationUri = Uri.fromFile(destinationFile) - var downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}") - Log.d(tag, "Audio File Destination Uri $destinationUri | Download URI $downloadUri") - var downloadItemPart = DownloadItemPart(DeviceManager.getBase64Id(destinationFile.absolutePath), destinationFilename, destinationFile.absolutePath, bookTitle, serverPath, localFolder.name, localFolder.id, audioTrack, null, false, downloadUri, destinationUri, null, 0) - + val downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,bookTitle,serverPath,localFolder,audioTrack,null) downloadItem.downloadItemParts.add(downloadItemPart) - var dlRequest = downloadItemPart.getDownloadRequest() - var downloadId = downloadManager.enqueue(dlRequest) + val dlRequest = downloadItemPart.getDownloadRequest() + val downloadId = downloadManager.enqueue(dlRequest) downloadItemPart.downloadId = downloadId } if (downloadItem.downloadItemParts.isNotEmpty()) { // Add cover download item if (libraryItem.media.coverPath != null && libraryItem.media.coverPath?.isNotEmpty() == true) { - var serverPath = "/api/items/${libraryItem.id}/cover?format=jpeg" - var destinationFilename = "cover.jpg" - var destinationFile = File("$itemFolderPath/$destinationFilename") + val serverPath = "/api/items/${libraryItem.id}/cover?format=jpeg" + val destinationFilename = "cover.jpg" + val destinationFile = File("$tempFolderPath/$destinationFilename") + val finalDestinationFile = File("$itemFolderPath/$destinationFilename") - if (destinationFile.exists()) { - Log.d(tag, "Cover already exists, removing it from ${destinationFile.absolutePath}") - destinationFile.delete() + if (finalDestinationFile.exists()) { + Log.d(tag, "Cover already exists, removing it from ${finalDestinationFile.absolutePath}") + finalDestinationFile.delete() } - var destinationUri = Uri.fromFile(destinationFile) - var downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}&token=${DeviceManager.token}") - var downloadItemPart = DownloadItemPart(DeviceManager.getBase64Id(destinationFile.absolutePath), destinationFilename, destinationFile.absolutePath, bookTitle, serverPath, localFolder.name, localFolder.id, null,null, false, downloadUri, destinationUri, null, 0) - + val downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,bookTitle,serverPath,localFolder,null,null) downloadItem.downloadItemParts.add(downloadItemPart) - var dlRequest = downloadItemPart.getDownloadRequest() - var downloadId = downloadManager.enqueue(dlRequest) + val dlRequest = downloadItemPart.getDownloadRequest() + val downloadId = downloadManager.enqueue(dlRequest) downloadItemPart.downloadId = downloadId } - // TODO: Cannot create new text file here but can download here... ?? -// var abmetadataFile = File(itemFolderPath, "abmetadata.abs") -// abmetadataFile.createNewFileIfPossible() -// abmetadataFile.writeText(getAbMetadataText(libraryItem)) - downloadQueue.add(downloadItem) startWatchingDownloads(downloadItem) DeviceManager.dbManager.saveDownloadItem(downloadItem) @@ -210,26 +240,25 @@ class AbsDownloader : Plugin() { } else { // Podcast episode download - var podcastTitle = libraryItem.media.metadata.title - var audioTrack = episode?.audioTrack + val podcastTitle = libraryItem.media.metadata.title + val audioTrack = episode?.audioTrack Log.d(tag, "Starting podcast episode download") - var itemFolderPath = localFolder.absolutePath + "/" + podcastTitle - var downloadItemId = "${libraryItem.id}-${episode?.id}" - var downloadItem = DownloadItem(downloadItemId, libraryItem.id, episode?.id, DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, podcastTitle, libraryItem.media, mutableListOf()) + val itemFolderPath = localFolder.absolutePath + "/" + podcastTitle + val downloadItemId = "${libraryItem.id}-${episode?.id}" + val downloadItem = DownloadItem(downloadItemId, libraryItem.id, episode?.id, DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, podcastTitle, libraryItem.media, mutableListOf()) var serverPath = "/s/item/${libraryItem.id}/${cleanRelPath(audioTrack?.relPath ?: "")}" var destinationFilename = getFilenameFromRelPath(audioTrack?.relPath ?: "") Log.d(tag, "Audio File Server Path $serverPath | AF RelPath ${audioTrack?.relPath} | LocalFolder Path ${localFolder.absolutePath} | DestName ${destinationFilename}") - var destinationFile = File("$itemFolderPath/$destinationFilename") - if (destinationFile.exists()) { - Log.d(tag, "Audio file already exists, removing it from ${destinationFile.absolutePath}") - destinationFile.delete() + + var destinationFile = File("$tempFolderPath/$destinationFilename") + var finalDestinationFile = File("$itemFolderPath/$destinationFilename") + if (finalDestinationFile.exists()) { + Log.d(tag, "Audio file already exists, removing it from ${finalDestinationFile.absolutePath}") + finalDestinationFile.delete() } - var destinationUri = Uri.fromFile(destinationFile) - var downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}") - Log.d(tag, "Audio File Destination Uri $destinationUri | Download URI $downloadUri") - var downloadItemPart = DownloadItemPart(DeviceManager.getBase64Id(destinationFile.absolutePath), destinationFilename, destinationFile.absolutePath, podcastTitle, serverPath, localFolder.name, localFolder.id, audioTrack, episode,false, downloadUri, destinationUri, null, 0) + var downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,null) downloadItem.downloadItemParts.add(downloadItemPart) var dlRequest = downloadItemPart.getDownloadRequest() @@ -239,15 +268,14 @@ class AbsDownloader : Plugin() { if (libraryItem.media.coverPath != null && libraryItem.media.coverPath?.isNotEmpty() == true) { serverPath = "/api/items/${libraryItem.id}/cover?format=jpeg" destinationFilename = "cover.jpg" - destinationFile = File("$itemFolderPath/$destinationFilename") - if (destinationFile.exists()) { + destinationFile = File("$tempFolderPath/$destinationFilename") + finalDestinationFile = File("$itemFolderPath/$destinationFilename") + + if (finalDestinationFile.exists()) { Log.d(tag, "Podcast cover already exists - not downloading cover again") } else { - destinationUri = Uri.fromFile(destinationFile) - downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}&token=${DeviceManager.token}") - downloadItemPart = DownloadItemPart(DeviceManager.getBase64Id(destinationFile.absolutePath), destinationFilename, destinationFile.absolutePath, podcastTitle, serverPath, localFolder.name, localFolder.id, null,null, false, downloadUri, destinationUri, null, 0) - + downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,null) downloadItem.downloadItemParts.add(downloadItemPart) dlRequest = downloadItemPart.getDownloadRequest() @@ -264,8 +292,8 @@ class AbsDownloader : Plugin() { fun startWatchingDownloads(downloadItem: DownloadItem) { GlobalScope.launch(Dispatchers.IO) { - while (downloadItem.downloadItemParts.find { !it.completed } != null) { // While some item is not completed - var numPartsBefore = downloadItem.downloadItemParts.size + while (downloadItem.downloadItemParts.find { !it.moved && !it.failed } != null) { // While some item is not completed + val numPartsBefore = downloadItem.downloadItemParts.size checkDownloads(downloadItem) // Keep database updated as item parts finish downloading @@ -278,13 +306,13 @@ class AbsDownloader : Plugin() { delay(500) } - var localLibraryItem = folderScanner.scanDownloadItem(downloadItem) + val localLibraryItem = folderScanner.scanDownloadItem(downloadItem) DeviceManager.dbManager.removeDownloadItem(downloadItem.id) downloadQueue.remove(downloadItem) Log.d(tag, "Item download complete ${downloadItem.itemTitle} | local library item id: ${localLibraryItem?.id} | Items remaining in Queue ${downloadQueue.size}") - var jsobj = JSObject() + val jsobj = JSObject() jsobj.put("libraryItemId", downloadItem.id) jsobj.put("localFolderId", downloadItem.localFolder.id) if (localLibraryItem != null) { @@ -295,26 +323,58 @@ class AbsDownloader : Plugin() { } fun checkDownloads(downloadItem: DownloadItem) { - var itemParts = downloadItem.downloadItemParts.map { it } + val itemParts = downloadItem.downloadItemParts.map { it } for (downloadItemPart in itemParts) { if (downloadItemPart.downloadId != null) { - var dlid = downloadItemPart.downloadId!! + val dlid = downloadItemPart.downloadId!! val query = DownloadManager.Query().setFilterById(dlid) downloadManager.query(query).use { if (it.moveToFirst()) { - val totalBytes = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)) - val downloadStatus = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_STATUS)) - val bytesDownloadedSoFar = it.getInt(it.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)) + val bytesColumnIndex = it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES) + val statusColumnIndex = it.getColumnIndex(DownloadManager.COLUMN_STATUS) + val bytesDownloadedColumnIndex = it.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR) + + val totalBytes = if (bytesColumnIndex >= 0) it.getInt(bytesColumnIndex) else 0 + val downloadStatus = if (statusColumnIndex >= 0) it.getInt(statusColumnIndex) else 0 + val bytesDownloadedSoFar = if (bytesDownloadedColumnIndex >= 0) it.getInt(bytesDownloadedColumnIndex) else 0 Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} bytes $totalBytes | bytes dled $bytesDownloadedSoFar | downloadStatus $downloadStatus") if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL) { - Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Done") -// downloadItem.downloadItemParts.remove(downloadItemPart) - downloadItemPart.completed = true + // Once file download is complete move the file to the final destination + if (!downloadItemPart.completed) { + Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Done") + downloadItemPart.completed = true + val file = DocumentFileCompat.fromUri(mainActivity, downloadItemPart.destinationUri) + Log.d(tag, "DOWNLOAD: Attempt move for file at destination ${downloadItemPart.destinationUri} | ${file?.getBasePath(mainActivity)}") + + val fcb = object : FileCallback() { + override fun onPrepare() { + Log.d(tag, "DOWNLOAD: PREPARING MOVE FILE") + } + override fun onFailed(errorCode:ErrorCode) { + Log.e(tag, "DOWNLOAD: FAILED TO MOVE FILE $errorCode") + downloadItemPart.failed = true + file?.delete() + } + override fun onCompleted(result:Any) { + Log.d(tag, "DOWNLOAD: FILE MOVE COMPLETED") + val resultDocFile = result as DocumentFile + Log.d(tag, "DOWNLOAD: COMPLETED FILE INFO ${resultDocFile.getAbsolutePath(mainActivity)}") + downloadItemPart.moved = true + } + } + + Log.d(tag, "DOWNLOAD: Move file to final destination path: ${downloadItemPart.finalDestinationPath}") + val localFolderFile = DocumentFileCompat.fromUri(mainActivity,Uri.parse(downloadItemPart.localFolderUrl)) + val mimetype = if (downloadItemPart.audioTrack != null) MimeType.AUDIO else MimeType.IMAGE + val fileDescription = FileDescription(downloadItemPart.filename, downloadItemPart.itemTitle, mimetype) + file?.moveFileTo(mainActivity,localFolderFile!!,fileDescription,fcb) + } else { + // Why is kotlin requiring an else here.. + } } else if (downloadStatus == DownloadManager.STATUS_FAILED) { Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Failed") downloadItem.downloadItemParts.remove(downloadItemPart) -// downloadItemPart.completed = true } else { //update progress val percentProgress = if (totalBytes > 0) ((bytesDownloadedSoFar * 100L) / totalBytes) else 0 diff --git a/components/app/AudioPlayer.vue b/components/app/AudioPlayer.vue index c9a63344..b9ac1976 100644 --- a/components/app/AudioPlayer.vue +++ b/components/app/AudioPlayer.vue @@ -43,7 +43,7 @@
- {{ bookmarks.length ? 'bookmark' : 'bookmark_border' }} + {{ bookmarks.length ? 'bookmark' : 'bookmark_border' }} bookmark diff --git a/components/app/AudioPlayerContainer.vue b/components/app/AudioPlayerContainer.vue index d6dd002a..584239fb 100644 --- a/components/app/AudioPlayerContainer.vue +++ b/components/app/AudioPlayerContainer.vue @@ -4,7 +4,7 @@ - +
@@ -31,7 +31,8 @@ export default { onSleepTimerSetListener: null, onMediaPlayerChangedListener: null, sleepInterval: null, - currentEndOfChapterTime: 0 + currentEndOfChapterTime: 0, + serverLibraryItemId: null } }, watch: { @@ -44,8 +45,8 @@ export default { }, computed: { bookmarks() { - // return this.$store.getters['user/getUserBookmarksForItem'](this.) - return [] + if (!this.serverLibraryItemId) return [] + return this.$store.getters['user/getUserBookmarksForItem'](this.serverLibraryItemId) }, socketConnected() { return this.$store.state.socketConnected @@ -181,10 +182,20 @@ export default { } } + this.serverLibraryItemId = null + + var playbackRate = 1 + if (this.$refs.audioPlayer) { + playbackRate = this.$refs.audioPlayer.currentPlaybackRate || 1 + } + console.log('Called playLibraryItem', libraryItemId) - AbsAudioPlayer.prepareLibraryItem({ libraryItemId, episodeId, playWhenReady: true }) + AbsAudioPlayer.prepareLibraryItem({ libraryItemId, episodeId, playWhenReady: true, playbackRate }) .then((data) => { console.log('Library item play response', JSON.stringify(data)) + if (!libraryItemId.startsWith('local')) { + this.serverLibraryItemId = libraryItemId + } }) .catch((error) => { console.error('Failed', error) diff --git a/components/bookshelf/Shelf.vue b/components/bookshelf/Shelf.vue index 6f6e7b3e..4c24ae5d 100644 --- a/components/bookshelf/Shelf.vue +++ b/components/bookshelf/Shelf.vue @@ -3,6 +3,7 @@
diff --git a/components/cards/LazyBookCard.vue b/components/cards/LazyBookCard.vue index 1febec32..da8207f2 100644 --- a/components/cards/LazyBookCard.vue +++ b/components/cards/LazyBookCard.vue @@ -34,8 +34,13 @@
+ +
+ {{ streamIsPlaying ? 'pause_circle' : 'play_circle_filled' }} +
+ -
+
{{ isLocalOnly ? 'task' : 'download_done' }} @@ -46,13 +51,18 @@ priority_high
- +

#{{ seriesSequence }}

+ +
+

Episode #{{ recentEpisodeNumber }}

+
+ -
+

{{ numEpisodes }}

@@ -196,6 +206,17 @@ export default { seriesSequence() { return this.series ? this.series.sequence : null }, + recentEpisode() { + // Only added to item when getting currently listening podcasts + return this._libraryItem.recentEpisode + }, + recentEpisodeNumber() { + if (!this.recentEpisode) return null + if (this.recentEpisode.episode) { + return this.recentEpisode.episode.replace(/^#/, '') + } + return this.recentEpisode.index + }, collapsedSeries() { // Only added to item object when collapseSeries is enabled return this._libraryItem.collapsedSeries @@ -222,7 +243,14 @@ export default { if (this.orderBy === 'size') return 'Size: ' + this.$bytesPretty(this._libraryItem.size) return null }, + episodeProgress() { + // Only used on home page currently listening podcast shelf + if (!this.recentEpisode) return null + if (this.isLocal) return this.store.getters['globals/getLocalMediaProgressById'](this.libraryItemId, this.recentEpisode.id) + return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.recentEpisode.id) + }, userProgress() { + if (this.episodeProgress) return this.episodeProgress if (this.isLocal) return this.store.getters['globals/getLocalMediaProgressById'](this.libraryItemId) return this.store.getters['user/getUserMediaProgress'](this.libraryItemId) }, @@ -233,19 +261,28 @@ export default { return this.userProgress ? !!this.userProgress.isFinished : false }, showError() { - return this.hasMissingParts || this.hasInvalidParts || this.isMissing || this.isInvalid + return this.numMissingParts || this.isMissing || this.isInvalid + }, + playerIsLocal() { + return !!this.$store.state.playerIsLocal + }, + localLibraryItemId() { + if (this.isLocal) return this.libraryItemId + return this.localLibraryItem ? this.localLibraryItem.id : null }, isStreaming() { - return this.store.getters['getlibraryItemIdStreaming'] === this.libraryItemId + if (this.isPodcast) { + if (this.playerIsLocal) { + // Check is streaming local version of this episode + return false // episode cards not implemented for local yet + } + return this.$store.getters['getIsEpisodeStreaming'](this.libraryItemId, this.recentEpisode.id) + } else { + return false // not yet necessary for books + } }, - showReadButton() { - return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook - }, - showPlayButton() { - return !this.isSelectionMode && !this.isMissing && !this.isInvalid && this.numTracks && !this.isStreaming - }, - showSmallEBookIcon() { - return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook + streamIsPlaying() { + return this.$store.state.playerIsPlaying && this.isStreaming }, isMissing() { return this._libraryItem.isMissing @@ -253,24 +290,9 @@ export default { isInvalid() { return this._libraryItem.isInvalid }, - hasMissingParts() { - return this._libraryItem.hasMissingParts - }, - hasInvalidParts() { - return this._libraryItem.hasInvalidParts - }, - errorText() { - if (this.isMissing) return 'Item directory is missing!' - else if (this.isInvalid) return 'Item has no media files' - var txt = '' - if (this.hasMissingParts) { - txt = `${this.hasMissingParts} missing parts.` - } - if (this.hasInvalidParts) { - if (this.hasMissingParts) txt += ' ' - txt += `${this.hasInvalidParts} invalid parts.` - } - return txt || 'Unknown Error' + numMissingParts() { + if (this.isPodcast) return 0 + return this.media.numMissingParts }, overlayWrapperClasslist() { var classes = [] @@ -343,6 +365,10 @@ export default { e.stopPropagation() e.preventDefault() this.selectBtnClick() + } else if (this.recentEpisode) { + var eventBus = this.$eventBus || this.$nuxt.$eventBus + if (this.streamIsPlaying) eventBus.$emit('pause-item') + else eventBus.$emit('play-item', { libraryItemId: this.libraryItemId, episodeId: this.recentEpisode.id }) } else { var router = this.$router || this.$nuxt.$router if (router) { diff --git a/components/covers/BookCover.vue b/components/covers/BookCover.vue index 62a91d05..8b530040 100644 --- a/components/covers/BookCover.vue +++ b/components/covers/BookCover.vue @@ -5,7 +5,7 @@
- +

{{ title }}

diff --git a/components/modals/BookmarksModal.vue b/components/modals/BookmarksModal.vue index 48db74d4..3ca9cf8b 100644 --- a/components/modals/BookmarksModal.vue +++ b/components/modals/BookmarksModal.vue @@ -45,6 +45,7 @@