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 f77adbf6..086175d3 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 @@ -14,6 +14,7 @@ data class AudioTrack( var metadata: FileMetadata?, var isLocal: Boolean, var localFileId: String?, + // TODO: This should no longer be necessary var serverIndex: Int? // Need to know if server track index is different ) { diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt b/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt index 6358ea2c..76b8f4b4 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/DeviceClasses.kt @@ -32,11 +32,14 @@ enum class AndroidAutoBrowseSeriesSequenceOrderSetting { ASC, DESC } +@JsonIgnoreProperties(ignoreUnknown = true) data class ServerConnectionConfig( var id:String, var index:Int, var name:String, var address:String, + // version added after 0.9.81-beta + var version:String?, var userId:String, var username:String, var token:String, diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/LibraryItem.kt b/android/app/src/main/java/com/audiobookshelf/app/data/LibraryItem.kt index 7b2dcf70..0b08f1aa 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/LibraryItem.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LibraryItem.kt @@ -53,6 +53,11 @@ class LibraryItem( return Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon) } + // As of v2.17.0 token is not needed with cover image requests + if (DeviceManager.isServerVersionGreaterThanOrEqualTo("2.17.0")) { + return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover") + } + return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}") } 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 cde9318e..46ea3cdc 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 @@ -149,6 +149,16 @@ class PlaybackSession( return total } + @JsonIgnore + fun checkIsServerVersionGte(compareVersion: String): Boolean { + // Safety check this playback session is the same one currently connected (should always be) + if (DeviceManager.serverConnectionConfigId != serverConnectionConfigId) { + return false + } + + return DeviceManager.isServerVersionGreaterThanOrEqualTo(compareVersion) + } + @JsonIgnore fun getCoverUri(ctx: Context): Uri { if (localLibraryItem?.coverContentUrl != null) { @@ -168,12 +178,22 @@ class PlaybackSession( if (coverPath == null) return Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon) + + // As of v2.17.0 token is not needed with cover image requests + if (checkIsServerVersionGte("2.17.0")) { + return Uri.parse("$serverAddress/api/items/$libraryItemId/cover") + } return Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}") } @JsonIgnore fun getContentUri(audioTrack: AudioTrack): Uri { if (isLocal) return Uri.parse(audioTrack.contentUrl) // Local content url + // As of v2.22.0 tracks use a different endpoint + // See: https://github.com/advplyr/audiobookshelf/pull/4263 + if (checkIsServerVersionGte("2.22.0")) { + return Uri.parse("$serverAddress/public/session/$id/track/${audioTrack.index}") + } return Uri.parse("$serverAddress${audioTrack.contentUrl}?token=${DeviceManager.token}") } @@ -264,14 +284,16 @@ class PlaybackSession( com.google.android.gms.cast.MediaMetadata.MEDIA_TYPE_AUDIOBOOK_CHAPTER ) + // As of v2.17.0 token is not needed with cover image requests + val coverUri = if (checkIsServerVersionGte("2.17.0")) { + Uri.parse("$serverAddress/api/items/$libraryItemId/cover") + } else { + Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}") + } + + // Cast always uses server cover uri coverPath?.let { - castMetadata.addImage( - WebImage( - Uri.parse( - "$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}" - ) - ) - ) + castMetadata.addImage(WebImage(coverUri)) } castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_TITLE, displayTitle ?: "") diff --git a/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt b/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt index d31891ff..de931ca1 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/device/DeviceManager.kt @@ -41,6 +41,7 @@ object DeviceManager { get() = serverConnectionConfig?.userId ?: "" val token get() = serverConnectionConfig?.token ?: "" + val serverVersion get() = serverConnectionConfig?.version ?: "" val isConnectedToServer get() = serverConnectionConfig != null @@ -111,6 +112,41 @@ object DeviceManager { return id?.let { deviceData.serverConnectionConfigs.find { it.id == id } } } + /** + * Check if the currently connected server version is >= compareVersion + * Abs server only uses major.minor.patch + * Note: Version is returned in Abs auth payloads starting v2.6.0 + * Note: Version is saved with the server connection config starting after v0.9.81 + * + * @example + * serverVersion=2.25.1 + * isServerVersionGreaterThanOrEqualTo("2.26.0") = false + * + * serverVersion=2.26.1 + * isServerVersionGreaterThanOrEqualTo("2.26.0") = true + */ + fun isServerVersionGreaterThanOrEqualTo(compareVersion:String):Boolean { + if (serverVersion == "") return false + if (compareVersion == "") return true + + val serverVersionParts = serverVersion.split(".").map { it.toIntOrNull() ?: 0 } + val compareVersionParts = compareVersion.split(".").map { it.toIntOrNull() ?: 0 } + + // Compare major, minor, and patch components + for (i in 0 until maxOf(serverVersionParts.size, compareVersionParts.size)) { + val serverVersionComponent = serverVersionParts.getOrElse(i) { 0 } + val compareVersionComponent = compareVersionParts.getOrElse(i) { 0 } + + if (serverVersionComponent < compareVersionComponent) { + return false // Server version is less than compareVersion + } else if (serverVersionComponent > compareVersionComponent) { + return true // Server version is greater than compareVersion + } + } + + return true // versions are equal in major, minor, and patch + } + /** * Checks the network connectivity status. * @param ctx The context to use for checking connectivity. 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 861ae692..4c376dfc 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 @@ -23,14 +23,14 @@ class AbsDatabase : Plugin() { val tag = "AbsDatabase" private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) - lateinit var mainActivity: MainActivity - lateinit var apiHandler: ApiHandler - lateinit var secureStorage: SecureStorage + private lateinit var mainActivity: MainActivity + private lateinit var apiHandler: ApiHandler + private lateinit var secureStorage: SecureStorage data class LocalMediaProgressPayload(val value:List) data class LocalLibraryItemsPayload(val value:List) data class LocalFoldersPayload(val value:List) - data class ServerConnConfigPayload(val id:String?, val index:Int, val name:String?, val userId:String, val username:String, val token:String, val refreshToken:String?, val address:String?, val customHeaders:Map?) + data class ServerConnConfigPayload(val id:String?, val index:Int, val name:String?, val userId:String, val username:String, var version:String, val token:String, val refreshToken:String?, val address:String?, val customHeaders:Map?) override fun load() { mainActivity = (activity as MainActivity) @@ -125,6 +125,7 @@ class AbsDatabase : Plugin() { val userId = serverConfigPayload.userId val username = serverConfigPayload.username + val serverVersion = serverConfigPayload.version val accessToken = serverConfigPayload.token // New token val refreshToken = serverConfigPayload.refreshToken // Refresh only sent on first connection @@ -144,7 +145,7 @@ class AbsDatabase : Plugin() { } Log.d(tag, "Refresh token secured = $hasRefreshToken") - serverConnectionConfig = ServerConnectionConfig(sscId, sscIndex, "$serverAddress ($username)", serverAddress, userId, username, accessToken, serverConfigPayload.customHeaders) + serverConnectionConfig = ServerConnectionConfig(sscId, sscIndex, "$serverAddress ($username)", serverAddress, serverVersion, userId, username, accessToken, serverConfigPayload.customHeaders) // Add and save DeviceManager.deviceData.serverConnectionConfigs.add(serverConnectionConfig!!) @@ -152,10 +153,11 @@ class AbsDatabase : Plugin() { DeviceManager.dbManager.saveDeviceData(DeviceManager.deviceData) } else { var shouldSave = false - if (serverConnectionConfig?.username != username || serverConnectionConfig?.token != accessToken) { + if (serverConnectionConfig?.username != username || serverConnectionConfig?.token != accessToken || serverConnectionConfig?.version != serverVersion) { serverConnectionConfig?.userId = userId serverConnectionConfig?.username = username serverConnectionConfig?.name = "${serverConnectionConfig?.address} (${serverConnectionConfig?.username})" + serverConnectionConfig?.version = serverVersion serverConnectionConfig?.token = accessToken shouldSave = true } diff --git a/components/connection/ServerConnectForm.vue b/components/connection/ServerConnectForm.vue index 1408797f..35f8e680 100644 --- a/components/connection/ServerConnectForm.vue +++ b/components/connection/ServerConnectForm.vue @@ -837,7 +837,7 @@ export default { this.serverConfig.refreshToken = user.refreshToken } - delete this.serverConfig.version + this.serverConfig.version = serverSettings.version var serverConnectionConfig = await this.$db.setServerConnectionConfig(this.serverConfig) diff --git a/layouts/default.vue b/layouts/default.vue index 23eed383..e895d81e 100644 --- a/layouts/default.vue +++ b/layouts/default.vue @@ -158,6 +158,7 @@ export default { } else if (userDefaultLibraryId) { this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId) } + serverConfig.version = serverSettings.version const serverConnectionConfig = await this.$db.setServerConnectionConfig(serverConfig) this.$store.commit('user/setUser', user) diff --git a/plugins/capacitor/AbsDatabase.js b/plugins/capacitor/AbsDatabase.js index 059abe30..1c499251 100644 --- a/plugins/capacitor/AbsDatabase.js +++ b/plugins/capacitor/AbsDatabase.js @@ -1,5 +1,18 @@ import { registerPlugin, Capacitor, WebPlugin } from '@capacitor/core' +/** + * @typedef {Object} ServerConnectionConfig + * @property {string} id + * @property {number} index + * @property {string} name + * @property {string} address + * @property {string} version + * @property {string} userId + * @property {string} username + * @property {string} token + * @property {string} [refreshToken] - Only passed in when setting config, then stored in secure storage + */ + class AbsDatabaseWeb extends WebPlugin { constructor() { super() @@ -19,6 +32,11 @@ class AbsDatabaseWeb extends WebPlugin { return deviceData } + /** + * + * @param {ServerConnectionConfig} serverConnectionConfig + * @returns {Promise} + */ async setCurrentServerConnectionConfig(serverConnectionConfig) { var deviceData = await this.getDeviceData() @@ -29,6 +47,7 @@ class AbsDatabaseWeb extends WebPlugin { ssc.token = serverConnectionConfig.token ssc.userId = serverConnectionConfig.userId ssc.username = serverConnectionConfig.username + ssc.version = serverConnectionConfig.version ssc.customHeaders = serverConnectionConfig.customHeaders || {} if (serverConnectionConfig.refreshToken) { @@ -47,6 +66,7 @@ class AbsDatabaseWeb extends WebPlugin { username: serverConnectionConfig.username, address: serverConnectionConfig.address, token: serverConnectionConfig.token, + version: serverConnectionConfig.version, customHeaders: serverConnectionConfig.customHeaders || {} } diff --git a/plugins/nativeHttp.js b/plugins/nativeHttp.js index 68b8bc6c..f00b5837 100644 --- a/plugins/nativeHttp.js +++ b/plugins/nativeHttp.js @@ -55,7 +55,7 @@ export default function ({ store, $db }, inject) { * @param {*} data - Request data * @param {Object} headers - Request headers * @param {Object} options - Additional options - * @param {{ id: string, address: string }} serverConnectionConfig + * @param {{ id: string, address: string, version: string }} serverConnectionConfig * @returns {Promise} - Promise that resolves with the response data */ async handleTokenRefresh(method, url, data, headers, options, serverConnectionConfig) { @@ -161,7 +161,7 @@ export default function ({ store, $db }, inject) { /** * Updates the store and secure storage with new tokens * @param {Object} tokens - Object containing accessToken and refreshToken - * @param {{ id: string, address: string }} serverConnectionConfig + * @param {{ id: string, address: string, version: string }} serverConnectionConfig * @returns {Promise} - Promise that resolves when tokens are updated */ async updateTokens(tokens, serverConnectionConfig) {