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 3bf086d8..0dec5b59 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 @@ -16,6 +16,8 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.getcapacitor.* import com.getcapacitor.annotation.CapacitorPlugin import com.google.android.gms.cast.CastDevice +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability import org.json.JSONObject @CapacitorPlugin(name = "AbsAudioPlayer") @@ -25,7 +27,7 @@ class AbsAudioPlayer : Plugin() { private lateinit var mainActivity: MainActivity private lateinit var apiHandler:ApiHandler - lateinit var castManager:CastManager + var castManager:CastManager? = null lateinit var playerNotificationService: PlayerNotificationService @@ -95,6 +97,24 @@ class AbsAudioPlayer : Plugin() { } private fun initCastManager() { + val googleApi = GoogleApiAvailability.getInstance() + val statusCode = googleApi.isGooglePlayServicesAvailable(mainActivity) + + if (statusCode != ConnectionResult.SUCCESS) { + if (statusCode == ConnectionResult.SERVICE_MISSING) { + Log.w(tag, "initCastManager: Google Api Missing") + } else if (statusCode == ConnectionResult.SERVICE_DISABLED) { + Log.w(tag, "initCastManager: Google Api Disabled") + } else if (statusCode == ConnectionResult.SERVICE_INVALID) { + Log.w(tag, "initCastManager: Google Api Invalid") + } else if (statusCode == ConnectionResult.SERVICE_UPDATING) { + Log.w(tag, "initCastManager: Google Api Updating") + } else if (statusCode == ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED) { + Log.w(tag, "initCastManager: Google Api Update Required") + } + return + } + val connListener = object: CastManager.ChromecastListener() { override fun onReceiverAvailableUpdate(available: Boolean) { Log.d(tag, "ChromecastListener: CAST Receiver Update Available $available") @@ -128,7 +148,7 @@ class AbsAudioPlayer : Plugin() { } castManager = CastManager(mainActivity) - castManager.startRouteScan(connListener) + castManager?.startRouteScan(connListener) } @PluginMethod @@ -144,7 +164,7 @@ class AbsAudioPlayer : Plugin() { 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 + val playbackRate = call.getFloat("playbackRate",1f) ?: 1f if (libraryItemId.isEmpty()) { Log.e(tag, "Invalid call to play library item no library item id") @@ -322,7 +342,11 @@ class AbsAudioPlayer : Plugin() { // Need to make sure the player service has been started Log.d(tag, "CAST REQUEST SESSION PLUGIN") call.resolve() - castManager.requestSession(playerNotificationService, object : CastManager.RequestSessionCallback() { + if (castManager == null) { + Log.e(tag, "Cast Manager not initialized") + return + } + castManager?.requestSession(playerNotificationService, object : CastManager.RequestSessionCallback() { override fun onError(errorCode: Int) { Log.e(tag, "CAST REQUEST SESSION CALLBACK ERROR $errorCode") } 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 a6c26c0a..53ea45ee 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 @@ -206,45 +206,93 @@ class AbsDatabase : Plugin() { @PluginMethod fun updateLocalMediaProgressFinished(call:PluginCall) { - var localMediaProgressId = call.getString("localMediaProgressId", "").toString() - var isFinished = call.getBoolean("isFinished", false) == true + val localLibraryItemId = call.getString("localLibraryItemId", "").toString() + var localEpisodeId:String? = call.getString("localEpisodeId", "").toString() + if (localEpisodeId.isNullOrEmpty()) localEpisodeId = null + + val localMediaProgressId = if (localEpisodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId" + val isFinished = call.getBoolean("isFinished", false) == true + Log.d(tag, "updateLocalMediaProgressFinished $localMediaProgressId | Is Finished:$isFinished") var localMediaProgress = DeviceManager.dbManager.getLocalMediaProgress(localMediaProgressId) - if (localMediaProgress == null) { - Log.e(tag, "updateLocalMediaProgressFinished Local Media Progress not found $localMediaProgressId") - call.resolve(JSObject("{\"error\":\"Progress not found\"}")) + + if (localMediaProgress == null) { // Create new local media progress if does not exist + Log.d(tag, "updateLocalMediaProgressFinished Local Media Progress not found $localMediaProgressId - Creating new") + val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId) + + if (localLibraryItem == null) { + return call.resolve(JSObject("{\"error\":\"Library Item not found\"}")) + } + if (localLibraryItem.mediaType != "podcast" && !localEpisodeId.isNullOrEmpty()) { + return call.resolve(JSObject("{\"error\":\"Invalid library item not a podcast\"}")) + } + + var duration = 0.0 + var podcastEpisode:PodcastEpisode? = null + if (!localEpisodeId.isNullOrEmpty()) { + val podcast = localLibraryItem.media as Podcast + podcastEpisode = podcast.episodes?.find { episode -> + episode.id == localEpisodeId + } + if (podcastEpisode == null) { + return call.resolve(JSObject("{\"error\":\"Podcast episode not found\"}")) + } + duration = podcastEpisode.duration ?: 0.0 + } else { + val book = localLibraryItem.media as Book + duration = book.duration ?: 0.0 + } + + val currentTime = System.currentTimeMillis() + localMediaProgress = LocalMediaProgress( + id = localMediaProgressId, + localLibraryItemId = localLibraryItemId, + localEpisodeId = localEpisodeId, + duration = duration, + progress = if (isFinished) 1.0 else 0.0, + currentTime = 0.0, + isFinished = isFinished, + lastUpdate = currentTime, + startedAt = if (isFinished) currentTime else 0L, + finishedAt = if (isFinished) currentTime else null, + serverConnectionConfigId = localLibraryItem.serverConnectionConfigId, + serverAddress = localLibraryItem.serverAddress, + serverUserId = localLibraryItem.serverUserId, + libraryItemId = localLibraryItem.libraryItemId, + episodeId = podcastEpisode?.serverEpisodeId) } else { localMediaProgress.updateIsFinished(isFinished) + } - var lmpstring = jacksonMapper.writeValueAsString(localMediaProgress) - Log.d(tag, "updateLocalMediaProgressFinished: Local Media Progress String $lmpstring") + // Save local media progress locally + DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress) - // Send update to server media progress is linked to a server and user is logged into that server - localMediaProgress.serverConnectionConfigId?.let { configId -> - if (DeviceManager.serverConnectionConfigId == configId) { - var libraryItemId = localMediaProgress.libraryItemId ?: "" - var episodeId = localMediaProgress.episodeId ?: "" - var updatePayload = JSObject() - updatePayload.put("isFinished", isFinished) - apiHandler.updateMediaProgress(libraryItemId,episodeId,updatePayload) { - Log.d(tag, "updateLocalMediaProgressFinished: Updated media progress isFinished on server") - var jsobj = JSObject() - jsobj.put("local", true) - jsobj.put("server", true) - jsobj.put("localMediaProgress", JSObject(lmpstring)) - call.resolve(jsobj) -// call.resolve(JSObject("{\"local\":true,\"server\":true,\"localMediaProgress\":$lmpstring}")) - } + val lmpstring = jacksonMapper.writeValueAsString(localMediaProgress) + Log.d(tag, "updateLocalMediaProgressFinished: Local Media Progress String $lmpstring") + + // Send update to server media progress is linked to a server and user is logged into that server + localMediaProgress.serverConnectionConfigId?.let { configId -> + if (DeviceManager.serverConnectionConfigId == configId) { + var libraryItemId = localMediaProgress.libraryItemId ?: "" + var episodeId = localMediaProgress.episodeId ?: "" + var updatePayload = JSObject() + updatePayload.put("isFinished", isFinished) + apiHandler.updateMediaProgress(libraryItemId,episodeId,updatePayload) { + Log.d(tag, "updateLocalMediaProgressFinished: Updated media progress isFinished on server") + var jsobj = JSObject() + jsobj.put("local", true) + jsobj.put("server", true) + jsobj.put("localMediaProgress", JSObject(lmpstring)) + call.resolve(jsobj) } } - if (localMediaProgress.serverConnectionConfigId == null || DeviceManager.serverConnectionConfigId != localMediaProgress.serverConnectionConfigId) { -// call.resolve(JSObject("{\"local\":true,\"localMediaProgress\":$lmpstring}}")) - var jsobj = JSObject() - jsobj.put("local", true) - jsobj.put("server", false) - jsobj.put("localMediaProgress", JSObject(lmpstring)) - call.resolve(jsobj) - } + } + if (localMediaProgress.serverConnectionConfigId == null || DeviceManager.serverConnectionConfigId != localMediaProgress.serverConnectionConfigId) { + var jsobj = JSObject() + jsobj.put("local", true) + jsobj.put("server", false) + jsobj.put("localMediaProgress", JSObject(lmpstring)) + 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 4e3e18be..b242e2a5 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 @@ -180,15 +180,30 @@ class AbsDownloader : Plugin() { // 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("/", "_") + var cleanedRelPath = relPath.replace("\\", "_").replace("/", "_") + cleanedRelPath = cleanStringForFileSystem(cleanedRelPath) return if (cleanedRelPath.startsWith("_")) cleanedRelPath.substring(1) else cleanedRelPath } + // Replace characters that cant be used in the file system + // Reserved characters: ?:\"*|/\\<> + private fun cleanStringForFileSystem(str:String):String { + val reservedCharacters = listOf("?", "\"", "*", "|", "/", "\\", "<", ">") + var newTitle = str + newTitle = newTitle.replace(":", " -") // Special case replace : with - + + reservedCharacters.forEach { + newTitle = newTitle.replace(it, "") + } + return newTitle + } + private fun startLibraryItemDownload(libraryItem: LibraryItem, localFolder: LocalFolder, episode:PodcastEpisode?) { val tempFolderPath = mainActivity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) if (libraryItem.mediaType == "book") { - val bookTitle = libraryItem.media.metadata.title + val bookTitle = cleanStringForFileSystem(libraryItem.media.metadata.title) + val tracks = libraryItem.media.getAudioTracks() Log.d(tag, "Starting library item download with ${tracks.size} tracks") val itemFolderPath = localFolder.absolutePath + "/" + bookTitle @@ -243,8 +258,8 @@ class AbsDownloader : Plugin() { } } else { // Podcast episode download + val podcastTitle = cleanStringForFileSystem(libraryItem.media.metadata.title) - val podcastTitle = libraryItem.media.metadata.title val audioTrack = episode?.audioTrack Log.d(tag, "Starting podcast episode download") val itemFolderPath = localFolder.absolutePath + "/" + podcastTitle diff --git a/components/bookshelf/LazyBookshelf.vue b/components/bookshelf/LazyBookshelf.vue index 386b5fe4..6ca0c1fa 100644 --- a/components/bookshelf/LazyBookshelf.vue +++ b/components/bookshelf/LazyBookshelf.vue @@ -3,6 +3,7 @@ diff --git a/components/cards/LazyListBookCard.vue b/components/cards/LazyListBookCard.vue index cdd0fcb3..cc7355cb 100644 --- a/components/cards/LazyListBookCard.vue +++ b/components/cards/LazyListBookCard.vue @@ -20,6 +20,8 @@

by {{ displayAuthor }}

{{ displaySortLine }}

+

{{ $elapsedPretty(duration) }}

+

{{ episodes }}

@@ -99,9 +101,23 @@ export default { mediaType() { return this._libraryItem.mediaType }, + duration() { + return this.media.duration || null + }, isPodcast() { return this.mediaType === 'podcast' }, + episodes() { + if (this.isPodcast) { + if (this.media.numEpisodes==1) { + return "1 episode" + } else { + return this.media.numEpisodes + ' episodes' + } + } else { + return null + } + }, placeholderUrl() { return '/book_placeholder.jpg' }, diff --git a/components/tables/podcast/EpisodeRow.vue b/components/tables/podcast/EpisodeRow.vue index 984afee8..4551dcad 100644 --- a/components/tables/podcast/EpisodeRow.vue +++ b/components/tables/podcast/EpisodeRow.vue @@ -27,9 +27,11 @@ - audio_file - {{ downloadItem ? 'downloading' : 'download' }} - download_done +
+ audio_file + {{ downloadItem ? 'downloading' : 'download' }} + download_done +
@@ -61,6 +63,9 @@ export default { } }, computed: { + isIos() { + return this.$platform === 'ios' + }, mediaType() { return 'podcast' }, @@ -204,9 +209,7 @@ export default { var isFinished = !this.userIsFinished var localLibraryItemId = this.isLocal ? this.libraryItemId : this.localLibraryItemId var localEpisodeId = this.isLocal ? this.episode.id : this.localEpisode.id - var localMediaProgressId = `${localLibraryItemId}-${localEpisodeId}` - console.log('toggleFinished local media progress id', localMediaProgressId, isFinished) - var payload = await this.$db.updateLocalMediaProgressFinished({ localMediaProgressId, isFinished }) + var payload = await this.$db.updateLocalMediaProgressFinished({ localLibraryItemId, localEpisodeId, isFinished }) console.log('toggleFinished payload', JSON.stringify(payload)) if (!payload || payload.error) { var errorMsg = payload ? payload.error : 'Unknown error' diff --git a/components/ui/ReadIconBtn.vue b/components/ui/ReadIconBtn.vue index 934b0c6b..c26c7167 100644 --- a/components/ui/ReadIconBtn.vue +++ b/components/ui/ReadIconBtn.vue @@ -1,5 +1,5 @@