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 @@
-