Compare commits

...
Author SHA1 Message Date
advplyr db1d6da491 Version bump 0.9.52-beta 2022-07-15 19:59:51 -05:00
advplyr 329a51f886 Example of adding custom action icon for android auto 2022-07-15 19:48:36 -05:00
advplyr 0c0badbae4 Fix:Android Auto use saved playback speed #229 2022-07-15 18:40:20 -05:00
advplyr 2da272c270 Fix:Android auto starting items from continue listening #167 2022-07-15 18:13:04 -05:00
advplyr 25ec5b3dcb Fix:ANR with starting foreground service before starting session 2022-07-15 17:46:33 -05:00
advplyr d9cdc16ff9 Fix:Catch api handler invalid json response body 2022-07-15 16:22:44 -05:00
advplyr 066b72d54d Fix:Media progress sync wait for api request to return before stopping 2022-07-15 16:10:42 -05:00
advplyr e7c913643a Add:Last local media progress sync with server widget, Update:Remove local media progress with bad id 2022-07-13 19:17:34 -05:00
advplyr 9847ed9fcb Fix network connection indicator 2022-07-13 17:13:15 -05:00
advplyr 721f812df0 Update:Library widget UI 2022-07-13 17:10:47 -05:00
advplyr a58965306a Fix misleading plugin function name 2022-07-13 16:44:02 -05:00
advplyr 2b5377db04 Merge pull request #278 from Jnewbon/master
Added ability to Give write permission to the SD card
2022-07-11 14:57:38 -05:00
Jnewbon 7fe3849705 Re Added read Permission check as i probably shouldn't have removed it 2022-07-11 00:43:36 +01:00
Jnewbon f998deb725 Added Storage Media type Write access 2022-07-11 00:26:53 +01:00
advplyr cb2aaede67 Fix:Socket reconnection on disconnect, Add:Connection indicator icon showing socket/cellular 2022-07-07 17:24:26 -05:00
advplyr e07e7f70d6 Fix:Buffered progress bar for chapter track #250 2022-07-04 18:42:43 -05:00
32 changed files with 372 additions and 130 deletions
+3 -3
View File
@@ -29,8 +29,8 @@ android {
applicationId "com.audiobookshelf.app" applicationId "com.audiobookshelf.app"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 82 versionCode 83
versionName "0.9.51-beta" versionName "0.9.52-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions { aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
@@ -111,7 +111,7 @@ dependencies {
implementation 'io.github.pilgr:paperdb:2.7.2' implementation 'io.github.pilgr:paperdb:2.7.2'
// Simple Storage // Simple Storage
implementation "com.anggrayudi:storage:0.13.0" implementation "com.anggrayudi:storage:1.3.0"
// OK HTTP // OK HTTP
implementation 'com.squareup.okhttp3:okhttp:4.9.2' implementation 'com.squareup.okhttp3:okhttp:4.9.2'
@@ -19,7 +19,6 @@ import com.audiobookshelf.app.plugins.AbsAudioPlayer
import com.audiobookshelf.app.plugins.AbsDownloader import com.audiobookshelf.app.plugins.AbsDownloader
import com.audiobookshelf.app.plugins.AbsFileSystem import com.audiobookshelf.app.plugins.AbsFileSystem
import com.getcapacitor.BridgeActivity import com.getcapacitor.BridgeActivity
import io.paperdb.Paper
class MainActivity : BridgeActivity() { class MainActivity : BridgeActivity() {
@@ -58,10 +57,6 @@ class MainActivity : BridgeActivity() {
DbManager.initialize(applicationContext) DbManager.initialize(applicationContext)
// Grant full storage access for testing
// var ss = SimpleStorage(this)
// ss.requestFullStorageAccess()
val permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) val permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
if (permission != PackageManager.PERMISSION_GRANTED) { if (permission != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, ActivityCompat.requestPermissions(this,
@@ -97,9 +92,7 @@ class MainActivity : BridgeActivity() {
foregroundService = mLocalBinder.getService() foregroundService = mLocalBinder.getService()
// Let NativeAudio know foreground service is ready and setup event listener // Let NativeAudio know foreground service is ready and setup event listener
if (pluginCallback != null) { pluginCallback()
pluginCallback()
}
} }
} }
@@ -45,7 +45,7 @@ class DbManager {
} }
} }
fun getLocalLibraryItemByLLId(libraryItemId:String):LocalLibraryItem? { fun getLocalLibraryItemByLId(libraryItemId:String):LocalLibraryItem? {
return getLocalLibraryItems().find { it.libraryItemId == libraryItemId } return getLocalLibraryItems().find { it.libraryItemId == libraryItemId }
} }
@@ -213,7 +213,11 @@ class DbManager {
val localLibraryItems = getLocalLibraryItems() val localLibraryItems = getLocalLibraryItems()
localMediaProgress.forEach { localMediaProgress.forEach {
val matchingLLI = localLibraryItems.find { lli -> lli.id == it.localLibraryItemId } val matchingLLI = localLibraryItems.find { lli -> lli.id == it.localLibraryItemId }
if (matchingLLI == null) { if (!it.id.startsWith("local")) {
// A bug on the server when syncing local media progress was replacing the media progress id causing duplicate progress. Remove them.
Log.d(tag, "cleanLocalMediaProgress: Invalid local media progress does not start with 'local' (fixed on server 2.0.24)")
Paper.book("localMediaProgress").delete(it.id)
} else if (matchingLLI == null) {
Log.d(tag, "cleanLocalMediaProgress: No matching local library item for local media progress ${it.id} - removing") Log.d(tag, "cleanLocalMediaProgress: No matching local library item for local media progress ${it.id} - removing")
Paper.book("localMediaProgress").delete(it.id) Paper.book("localMediaProgress").delete(it.id)
} else if (matchingLLI.isPodcast) { } else if (matchingLLI.isPodcast) {
@@ -2,6 +2,7 @@ package com.audiobookshelf.app.data
import com.fasterxml.jackson.annotation.JsonIgnore import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import kotlin.math.roundToInt
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class LocalMediaProgress( data class LocalMediaProgress(
@@ -22,6 +23,9 @@ data class LocalMediaProgress(
var libraryItemId:String?, var libraryItemId:String?,
var episodeId:String? var episodeId:String?
) { ) {
@get:JsonIgnore
val progressPercent get() = if (progress.isNaN()) 0 else (progress * 100).roundToInt()
@JsonIgnore @JsonIgnore
fun updateIsFinished(finished:Boolean) { fun updateIsFinished(finished:Boolean) {
if (isFinished != finished) { // If finished changed then set progress if (isFinished != finished) { // If finished changed then set progress
@@ -1,21 +1,24 @@
package com.audiobookshelf.app.media package com.audiobookshelf.app.media
import android.app.Activity
import android.content.Context import android.content.Context
import android.support.v4.media.MediaBrowserCompat import android.support.v4.media.MediaBrowserCompat
import android.util.Log import android.util.Log
import com.audiobookshelf.app.data.* import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.server.ApiHandler import com.audiobookshelf.app.server.ApiHandler
import com.getcapacitor.JSObject
import java.util.* import java.util.*
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.json.JSONException
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
class MediaManager(var apiHandler: ApiHandler, var ctx: Context) { class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
val tag = "MediaManager" val tag = "MediaManager"
var serverLibraryItems = listOf<LibraryItem>() var serverLibraryItems = mutableListOf<LibraryItem>()
var selectedLibraryId = "" var selectedLibraryId = ""
var selectedLibraryItemWrapper:LibraryItemWrapper? = null var selectedLibraryItemWrapper:LibraryItemWrapper? = null
@@ -26,10 +29,35 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
var serverLibraries = listOf<Library>() var serverLibraries = listOf<Library>()
var serverConfigIdUsed:String? = null var serverConfigIdUsed:String? = null
var userSettingsPlaybackRate:Float? = null
fun getIsLibrary(id:String) : Boolean { fun getIsLibrary(id:String) : Boolean {
return serverLibraries.find { it.id == id } != null return serverLibraries.find { it.id == id } != null
} }
fun getSavedPlaybackRate():Float {
if (userSettingsPlaybackRate != null) {
return userSettingsPlaybackRate ?: 1f
}
val sharedPrefs = ctx.getSharedPreferences("CapacitorStorage", Activity.MODE_PRIVATE)
if (sharedPrefs != null) {
val userSettingsPref = sharedPrefs.getString("userSettings", null)
if (userSettingsPref != null) {
try {
val userSettings = JSObject(userSettingsPref)
if (userSettings.has("playbackRate")) {
userSettingsPlaybackRate = userSettings.getDouble("playbackRate").toFloat()
return userSettingsPlaybackRate ?: 1f
}
} catch(je:JSONException) {
Log.e(tag, "Failed to parse userSettings JSON ${je.localizedMessage}")
}
}
}
return 1f
}
fun checkResetServerItems() { fun checkResetServerItems() {
// When opening android auto need to check if still connected to server // When opening android auto need to check if still connected to server
// and reset any server data already set // and reset any server data already set
@@ -39,7 +67,7 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
serverPodcastEpisodes = listOf() serverPodcastEpisodes = listOf()
serverLibraryCategories = listOf() serverLibraryCategories = listOf()
serverLibraries = listOf() serverLibraries = listOf()
serverLibraryItems = listOf() serverLibraryItems = mutableListOf()
selectedLibraryId = "" selectedLibraryId = ""
} }
} }
@@ -63,7 +91,11 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() } val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
if (libraryItemsWithAudio.isNotEmpty()) selectedLibraryId = libraryId if (libraryItemsWithAudio.isNotEmpty()) selectedLibraryId = libraryId
serverLibraryItems = libraryItemsWithAudio libraryItemsWithAudio.forEach { libraryItem ->
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
}
cb(libraryItemsWithAudio) cb(libraryItemsWithAudio)
} }
} }
@@ -211,6 +243,17 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
// Only using book or podcast library categories for now // Only using book or podcast library categories for now
libraryCategories.forEach { libraryCategories.forEach {
// Add items in continue listening to serverLibraryItems
if (it.id == "continue-listening") {
it.entities.forEach { libraryItemWrapper ->
val libraryItem = libraryItemWrapper as LibraryItem
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
}
}
// Log.d(tag, "Found library category ${it.label} with type ${it.type}") // Log.d(tag, "Found library category ${it.label} with type ${it.type}")
if (it.type == library.mediaType) { if (it.type == library.mediaType) {
// Log.d(tag, "Using library category ${it.id}") // Log.d(tag, "Using library category ${it.id}")
@@ -265,19 +308,18 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
} }
fun play(libraryItemWrapper:LibraryItemWrapper, episode:PodcastEpisode?, playItemRequestPayload:PlayItemRequestPayload, cb: (PlaybackSession?) -> Unit) { fun play(libraryItemWrapper:LibraryItemWrapper, episode:PodcastEpisode?, playItemRequestPayload:PlayItemRequestPayload, cb: (PlaybackSession?) -> Unit) {
if (libraryItemWrapper is LocalLibraryItem) { if (libraryItemWrapper is LocalLibraryItem) {
val localLibraryItem = libraryItemWrapper as LocalLibraryItem cb(libraryItemWrapper.getPlaybackSession(episode))
cb(localLibraryItem.getPlaybackSession(episode)) } else {
} else { val libraryItem = libraryItemWrapper as LibraryItem
val libraryItem = libraryItemWrapper as LibraryItem apiHandler.playLibraryItem(libraryItem.id,episode?.id ?: "", playItemRequestPayload) {
apiHandler.playLibraryItem(libraryItem.id,episode?.id ?: "",playItemRequestPayload) { if (it == null) {
if (it == null) { cb(null)
cb(null) } else {
} else { cb(it)
cb(it) }
} }
} }
}
} }
private fun levenshtein(lhs : CharSequence, rhs : CharSequence) : Int { private fun levenshtein(lhs : CharSequence, rhs : CharSequence) : Int {
@@ -4,7 +4,6 @@ import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import android.support.v4.media.MediaMetadataCompat import android.support.v4.media.MediaMetadataCompat
import android.util.Log
import androidx.annotation.AnyRes import androidx.annotation.AnyRes
import com.audiobookshelf.app.R import com.audiobookshelf.app.R
import com.audiobookshelf.app.data.Library import com.audiobookshelf.app.data.Library
@@ -10,7 +10,6 @@ import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.server.ApiHandler import com.audiobookshelf.app.server.ApiHandler
import java.util.* import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
import kotlin.math.roundToInt
data class MediaProgressSyncData( data class MediaProgressSyncData(
var timeListened:Long, // seconds var timeListened:Long, // seconds
@@ -56,7 +55,9 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
if (playerNotificationService.currentPlayer.isPlaying) { if (playerNotificationService.currentPlayer.isPlaying) {
val currentTime = playerNotificationService.getCurrentTimeSeconds() val currentTime = playerNotificationService.getCurrentTimeSeconds()
sync(currentTime) sync(currentTime) {
Log.d(tag, "Sync complete")
}
} }
} }
} }
@@ -67,8 +68,9 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
Log.d(tag, "stop: Stopping listening for $currentDisplayTitle") Log.d(tag, "stop: Stopping listening for $currentDisplayTitle")
val currentTime = playerNotificationService.getCurrentTimeSeconds() val currentTime = playerNotificationService.getCurrentTimeSeconds()
sync(currentTime) sync(currentTime) {
reset() reset()
}
} }
fun syncFromServerProgress(mediaProgress: MediaProgress) { fun syncFromServerProgress(mediaProgress: MediaProgress) {
@@ -81,10 +83,10 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
} }
} }
fun sync(currentTime:Double) { fun sync(currentTime:Double, cb: () -> Unit) {
val diffSinceLastSync = System.currentTimeMillis() - lastSyncTime val diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
if (diffSinceLastSync < 1000L) { if (diffSinceLastSync < 1000L) {
return return cb()
} }
val listeningTimeToAdd = diffSinceLastSync / 1000L val listeningTimeToAdd = diffSinceLastSync / 1000L
lastSyncTime = System.currentTimeMillis() lastSyncTime = System.currentTimeMillis()
@@ -92,6 +94,12 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
val syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime) val syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime)
currentPlaybackSession?.syncData(syncData) currentPlaybackSession?.syncData(syncData)
if (currentPlaybackSession?.progress?.isNaN() == true) {
Log.e(tag, "Current Playback Session invalid progress ${currentPlaybackSession?.progress} | Current Time: ${currentPlaybackSession?.currentTime} | Duration: ${currentPlaybackSession?.getTotalDuration()}")
return cb()
}
if (currentIsLocal) { if (currentIsLocal) {
// Save local progress sync // Save local progress sync
currentPlaybackSession?.let { currentPlaybackSession?.let {
@@ -99,16 +107,17 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
saveLocalProgress(it) saveLocalProgress(it)
// Local library item is linked to a server library item // Local library item is linked to a server library item
if (!it.libraryItemId.isNullOrEmpty()) { // Send sync to server also if connected to this server and local item belongs to this server
// Send sync to server also if connected to this server and local item belongs to this server if (!it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
if (it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) { apiHandler.sendLocalProgressSync(it) {
apiHandler.sendLocalProgressSync(it) { Log.d(
Log.d( tag,
tag, "Local progress sync data sent to server $currentDisplayTitle for time $currentTime"
"Local progress sync data sent to server $currentDisplayTitle for time $currentTime" )
) cb()
}
} }
} else {
cb()
} }
} }
} else { } else {
@@ -124,6 +133,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
} }
Log.d(tag, "Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime") Log.d(tag, "Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
} }
cb()
} }
} }
} }
@@ -140,9 +150,14 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
currentLocalMediaProgress?.updateFromPlaybackSession(playbackSession) currentLocalMediaProgress?.updateFromPlaybackSession(playbackSession)
} }
currentLocalMediaProgress?.let { currentLocalMediaProgress?.let {
DeviceManager.dbManager.saveLocalMediaProgress(it) if (it.progress.isNaN()) {
playerNotificationService.clientEventEmitter?.onLocalMediaProgressUpdate(it) Log.e(tag, "Invalid progress on local media progress")
Log.d(tag, "Saved Local Progress Current Time: ID ${it.id} | ${it.currentTime} | Duration ${it.duration} | Progress ${(it.progress * 100).roundToInt()}%") } else {
DeviceManager.dbManager.saveLocalMediaProgress(it)
playerNotificationService.clientEventEmitter?.onLocalMediaProgressUpdate(it)
Log.d(tag, "Saved Local Progress Current Time: ID ${it.id} | ${it.currentTime} | Duration ${it.duration} | Progress ${it.progressPercent}%")
}
} }
} }
@@ -7,8 +7,10 @@ import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.Message import android.os.Message
import android.support.v4.media.session.MediaSessionCompat import android.support.v4.media.session.MediaSessionCompat
import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import com.audiobookshelf.app.R
import com.audiobookshelf.app.data.LibraryItemWrapper import com.audiobookshelf.app.data.LibraryItemWrapper
import com.audiobookshelf.app.data.PodcastEpisode import com.audiobookshelf.app.data.PodcastEpisode
import java.util.* import java.util.*
@@ -28,8 +30,9 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
if (it == null) { if (it == null) {
Log.e(tag, "Failed to play library item") Log.e(tag, "Failed to play library item")
} else { } else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,true,null) playerNotificationService.preparePlayer(it,true, playbackRate)
} }
} }
} }
@@ -53,8 +56,9 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
if (it == null) { if (it == null) {
Log.e(tag, "Failed to play library item") Log.e(tag, "Failed to play library item")
} else { } else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it, true, null) playerNotificationService.preparePlayer(it, true, playbackRate)
} }
} }
} }
@@ -114,8 +118,9 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
if (it == null) { if (it == null) {
Log.e(tag, "Failed to play library item") Log.e(tag, "Failed to play library item")
} else { } else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it, true, null) playerNotificationService.preparePlayer(it, true, playbackRate)
} }
} }
} }
@@ -214,4 +219,12 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
} }
} }
// Example Using a custom action in android auto
// override fun onCustomAction(action: String?, extras: Bundle?) {
// super.onCustomAction(action, extras)
//
// if ("com.audiobookshelf.app.PLAYBACK_RATE" == action) {
//
// }
// }
} }
@@ -34,8 +34,9 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
if (it == null) { if (it == null) {
Log.e(tag, "Failed to play library item") Log.e(tag, "Failed to play library item")
} else { } else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it, playWhenReady, null) playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
} }
} }
} }
@@ -61,8 +62,9 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
if (it == null) { if (it == null) {
Log.e(tag, "Failed to play library item") Log.e(tag, "Failed to play library item")
} else { } else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it, playWhenReady, null) playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
} }
} }
} }
@@ -76,8 +78,9 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
if (it == null) { if (it == null) {
Log.e(tag, "Failed to play library item") Log.e(tag, "Failed to play library item")
} else { } else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
Handler(Looper.getMainLooper()).post() { Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it, playWhenReady, null) playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
} }
} }
} }
@@ -15,9 +15,12 @@ import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.media.MediaBrowserServiceCompat import androidx.media.MediaBrowserServiceCompat
import androidx.media.utils.MediaConstants import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.BuildConfig import com.audiobookshelf.app.BuildConfig
import com.audiobookshelf.app.R
import com.audiobookshelf.app.data.* import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.data.DeviceInfo import com.audiobookshelf.app.data.DeviceInfo
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
@@ -65,7 +68,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
private lateinit var ctx:Context private lateinit var ctx:Context
private lateinit var mediaSessionConnector: MediaSessionConnector private lateinit var mediaSessionConnector: MediaSessionConnector
private lateinit var playerNotificationManager: PlayerNotificationManager private lateinit var playerNotificationManager: PlayerNotificationManager
private lateinit var mediaSession: MediaSessionCompat lateinit var mediaSession: MediaSessionCompat
private lateinit var transportControls:MediaControllerCompat.TransportControls private lateinit var transportControls:MediaControllerCompat.TransportControls
lateinit var mediaManager: MediaManager lateinit var mediaManager: MediaManager
@@ -261,6 +264,18 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
mediaSessionConnector.setQueueNavigator(queueNavigator) mediaSessionConnector.setQueueNavigator(queueNavigator)
mediaSessionConnector.setPlaybackPreparer(MediaSessionPlaybackPreparer(this)) mediaSessionConnector.setPlaybackPreparer(MediaSessionPlaybackPreparer(this))
// Example adding custom action with icon in android auto
// mediaSessionConnector.setCustomActionProviders(object : MediaSessionConnector.CustomActionProvider {
// override fun onCustomAction(player: Player, action: String, extras: Bundle?) {
// }
// override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? {
// var icon = R.drawable.exo_icon_rewind
// return PlaybackStateCompat.CustomAction.Builder(
// "com.audiobookshelf.app.PLAYBACK_RATE", "Playback Rate", icon)
// .build()
// }
// })
mediaSession.setCallback(MediaSessionCallback(this)) mediaSession.setCallback(MediaSessionCallback(this))
initializeMPlayer() initializeMPlayer()
@@ -300,6 +315,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
User callable methods User callable methods
*/ */
fun preparePlayer(playbackSession: PlaybackSession, playWhenReady:Boolean, playbackRate:Float?) { fun preparePlayer(playbackSession: PlaybackSession, playWhenReady:Boolean, playbackRate:Float?) {
if (!isStarted) {
Log.i(tag, "preparePlayer: foreground service not started - Starting service --")
Intent(ctx, PlayerNotificationService::class.java).also { intent ->
ContextCompat.startForegroundService(ctx, intent)
}
}
isClosed = false isClosed = false
val playbackRateToUse = playbackRate ?: initialPlaybackRate ?: 1f val playbackRateToUse = playbackRate ?: initialPlaybackRate ?: 1f
initialPlaybackRate = playbackRate initialPlaybackRate = playbackRate
@@ -660,6 +682,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
PlayerListener.lastPauseTime = 0 PlayerListener.lastPauseTime = 0
isClosed = true isClosed = true
stopForeground(true) stopForeground(true)
stopSelf()
} }
fun sendClientMetadata(playerState: PlayerState) { fun sendClientMetadata(playerState: PlayerState) {
@@ -1,10 +1,8 @@
package com.audiobookshelf.app.plugins package com.audiobookshelf.app.plugins
import android.content.Intent
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
import androidx.core.content.ContextCompat
import com.audiobookshelf.app.MainActivity import com.audiobookshelf.app.MainActivity
import com.audiobookshelf.app.data.* import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.DeviceManager
@@ -150,14 +148,6 @@ class AbsAudioPlayer : Plugin() {
@PluginMethod @PluginMethod
fun prepareLibraryItem(call: PluginCall) { fun prepareLibraryItem(call: PluginCall) {
// Need to make sure the player service has been started
if (!PlayerNotificationService.isStarted) {
Log.w(tag, "prepareLibraryItem: PlayerService not started - Starting foreground service --")
Intent(mainActivity, PlayerNotificationService::class.java).also { intent ->
ContextCompat.startForegroundService(mainActivity, intent)
}
}
val libraryItemId = call.getString("libraryItemId", "").toString() val libraryItemId = call.getString("libraryItemId", "").toString()
val episodeId = call.getString("episodeId", "").toString() val episodeId = call.getString("episodeId", "").toString()
val playWhenReady = call.getBoolean("playWhenReady") == true val playWhenReady = call.getBoolean("playWhenReady") == true
@@ -77,10 +77,10 @@ class AbsDatabase : Plugin() {
} }
@PluginMethod @PluginMethod
fun getLocalLibraryItemByLLId(call:PluginCall) { fun getLocalLibraryItemByLId(call:PluginCall) {
val libraryItemId = call.getString("libraryItemId", "").toString() val libraryItemId = call.getString("libraryItemId", "").toString()
GlobalScope.launch(Dispatchers.IO) { GlobalScope.launch(Dispatchers.IO) {
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLLId(libraryItemId) val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItemId)
if (localLibraryItem == null) { if (localLibraryItem == null) {
call.resolve() call.resolve()
} else { } else {
@@ -1,5 +1,6 @@
package com.audiobookshelf.app.plugins package com.audiobookshelf.app.plugins
import android.app.AlertDialog
import android.database.Cursor import android.database.Cursor
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@@ -65,39 +66,59 @@ class AbsFileSystem : Plugin() {
@PluginMethod @PluginMethod
fun selectFolder(call: PluginCall) { fun selectFolder(call: PluginCall) {
var mediaType = call.data.getString("mediaType", "book").toString() val mediaType = call.data.getString("mediaType", "book").toString()
val REQUEST_CODE_SELECT_FOLDER = 6
val REQUEST_CODE_SDCARD_ACCESS = 7
mainActivity.storage.folderPickerCallback = object : FolderPickerCallback { mainActivity.storage.folderPickerCallback = object : FolderPickerCallback {
override fun onFolderSelected(requestCode: Int, folder: DocumentFile) { override fun onFolderSelected(requestCode: Int, folder: DocumentFile) {
Log.d(TAG, "ON FOLDER SELECTED ${folder.uri} ${folder.name}") Log.d(TAG, "ON FOLDER SELECTED ${folder.uri} ${folder.name}")
var absolutePath = folder.getAbsolutePath(activity) val absolutePath = folder.getAbsolutePath(activity)
var storageType = folder.getStorageType(activity) val storageType = folder.getStorageType(activity)
var simplePath = folder.getSimplePath(activity) val simplePath = folder.getSimplePath(activity)
var basePath = folder.getBasePath(activity) val basePath = folder.getBasePath(activity)
var folderId = android.util.Base64.encodeToString(folder.id.toByteArray(), android.util.Base64.DEFAULT) val folderId = android.util.Base64.encodeToString(folder.id.toByteArray(), android.util.Base64.DEFAULT)
var localFolder = LocalFolder(folderId, folder.name ?: "", folder.uri.toString(),basePath,absolutePath, simplePath, storageType.toString(), mediaType) val localFolder = LocalFolder(folderId, folder.name ?: "", folder.uri.toString(),basePath,absolutePath, simplePath, storageType.toString(), mediaType)
DeviceManager.dbManager.saveLocalFolder(localFolder) DeviceManager.dbManager.saveLocalFolder(localFolder)
call.resolve(JSObject(jacksonMapper.writeValueAsString(localFolder))) call.resolve(JSObject(jacksonMapper.writeValueAsString(localFolder)))
} }
override fun onStorageAccessDenied(requestCode: Int, folder: DocumentFile?, storageType: StorageType) { override fun onStorageAccessDenied(requestCode: Int, folder: DocumentFile?, storageType: StorageType) {
Log.e(TAG, "STORAGE ACCESS DENIED") val jsobj = JSObject()
var jsobj = JSObject() if (requestCode == REQUEST_CODE_SELECT_FOLDER) {
jsobj.put("error", "Access Denied")
call.resolve(jsobj) val builder: AlertDialog.Builder = AlertDialog.Builder(mainActivity)
builder.setMessage(
"You have no write access to this storage, thus selecting this folder is useless." +
"\nWould you like to grant access to this folder?")
builder.setNegativeButton("Dont Allow") { _, _ ->
run {
jsobj.put("error", "User Canceled, Access Denied")
call.resolve(jsobj)
}
}
builder.setPositiveButton("Allow.") { _, _ -> mainActivity.storageHelper.requestStorageAccess(REQUEST_CODE_SDCARD_ACCESS, storageType) }
builder.show()
} else {
Log.d(TAG, "STORAGE ACCESS DENIED $requestCode")
jsobj.put("error", "Access Denied")
call.resolve(jsobj)
}
} }
override fun onStoragePermissionDenied(requestCode: Int) { override fun onStoragePermissionDenied(requestCode: Int) {
Log.d(TAG, "STORAGE PERMISSION DENIED $requestCode") Log.d(TAG, "STORAGE PERMISSION DENIED $requestCode")
var jsobj = JSObject() val jsobj = JSObject()
jsobj.put("error", "Permission Denied") jsobj.put("error", "Permission Denied")
call.resolve(jsobj) call.resolve(jsobj)
} }
} }
mainActivity.storage.openFolderPicker(6) mainActivity.storage.openFolderPicker(REQUEST_CODE_SELECT_FOLDER)
} }
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
@@ -1,7 +1,6 @@
package com.audiobookshelf.app.server package com.audiobookshelf.app.server
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.util.Log import android.util.Log
@@ -17,6 +16,7 @@ import com.getcapacitor.JSObject
import okhttp3.* import okhttp3.*
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -28,8 +28,6 @@ class ApiHandler(var ctx:Context) {
private var pingClient = OkHttpClient.Builder().callTimeout(3, TimeUnit.SECONDS).build() private var pingClient = OkHttpClient.Builder().callTimeout(3, TimeUnit.SECONDS).build()
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
var storageSharedPreferences: SharedPreferences? = null
data class LocalMediaProgressSyncPayload(val localMediaProgress:List<LocalMediaProgress>) data class LocalMediaProgressSyncPayload(val localMediaProgress:List<LocalMediaProgress>)
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
data class MediaProgressSyncResponsePayload(val numServerProgressUpdates:Int, val localProgressUpdates:List<LocalMediaProgress>) data class MediaProgressSyncResponsePayload(val numServerProgressUpdates:Int, val localProgressUpdates:List<LocalMediaProgress>)
@@ -106,14 +104,21 @@ class ApiHandler(var ctx:Context) {
if (bodyString == "OK") { if (bodyString == "OK") {
cb(JSObject()) cb(JSObject())
} else { } else {
var jsonObj = JSObject() try {
if (bodyString.startsWith("[")) { var jsonObj = JSObject()
val array = JSArray(bodyString) if (bodyString.startsWith("[")) {
jsonObj.put("value", array) val array = JSArray(bodyString)
} else { jsonObj.put("value", array)
jsonObj = JSObject(bodyString) } else {
jsonObj = JSObject(bodyString)
}
cb(jsonObj)
} catch(je:JSONException) {
Log.e(tag, "Invalid JSON response ${je.localizedMessage} from body $bodyString")
val jsobj = JSObject()
jsobj.put("error", "Invalid response body")
cb(jsobj)
} }
cb(jsonObj)
} }
} }
} }
+8 -5
View File
@@ -8,25 +8,28 @@
<span class="material-icons text-3xl text-white">arrow_back</span> <span class="material-icons text-3xl text-white">arrow_back</span>
</a> </a>
<div v-if="user && currentLibrary"> <div v-if="user && currentLibrary">
<div class="pl-3 pr-4 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal"> <div class="pl-1.5 pr-2.5 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
<widgets-library-icon :icon="currentLibraryIcon" :size="4" /> <widgets-library-icon :icon="currentLibraryIcon" :size="4" />
<p class="text-base font-book leading-4 ml-2 mt-0.5">{{ currentLibraryName }}</p> <p class="text-sm font-book leading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
</div> </div>
</div> </div>
<widgets-connection-indicator />
<div class="flex-grow" /> <div class="flex-grow" />
<widgets-download-progress-indicator /> <widgets-download-progress-indicator />
<!-- Must be connected to a server to cast, only supports media items on server --> <!-- Must be connected to a server to cast, only supports media items on server -->
<div v-show="isCastAvailable && user" class="mx-2 cursor-pointer mt-1.5"> <div v-show="isCastAvailable && user" class="mx-2 cursor-pointer mt-1.5">
<span class="material-icons" :class="isCasting ? 'text-success' : ''" style="font-size: 1.6rem" @click="castClick">cast</span> <span class="material-icons text-2xl" :class="isCasting ? 'text-success' : ''" @click="castClick">cast</span>
</div> </div>
<nuxt-link v-if="user" class="h-7 mx-2" to="/search"> <nuxt-link v-if="user" class="h-7 mx-1.5" to="/search">
<span class="material-icons" style="font-size: 1.75rem">search</span> <span class="material-icons" style="font-size: 1.75rem">search</span>
</nuxt-link> </nuxt-link>
<div class="h-7 mx-2"> <div class="h-7 mx-1.5">
<span class="material-icons" style="font-size: 1.75rem" @click="clickShowSideDrawer">menu</span> <span class="material-icons" style="font-size: 1.75rem" @click="clickShowSideDrawer">menu</span>
</div> </div>
</div> </div>
+1 -2
View File
@@ -447,7 +447,7 @@ export default {
if (this.useChapterTrack && this.currentChapter) { if (this.useChapterTrack && this.currentChapter) {
var currChapTime = this.currentTime - this.currentChapter.start var currChapTime = this.currentTime - this.currentChapter.start
percentDone = currChapTime / this.currentChapterDuration percentDone = currChapTime / this.currentChapterDuration
bufferedPercent = (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration bufferedPercent = Math.max(0, Math.min(1, (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration))
} }
var ptWidth = Math.round(percentDone * this.trackWidth) var ptWidth = Math.round(percentDone * this.trackWidth)
this.$refs.playedTrack.style.width = ptWidth + 'px' this.$refs.playedTrack.style.width = ptWidth + 'px'
@@ -523,7 +523,6 @@ export default {
var data = await AbsAudioPlayer.getCurrentTime() var data = await AbsAudioPlayer.getCurrentTime()
this.currentTime = Number(data.value.toFixed(2)) this.currentTime = Number(data.value.toFixed(2))
this.bufferedTime = Number(data.bufferedTime.toFixed(2)) this.bufferedTime = Number(data.bufferedTime.toFixed(2))
console.log('[AudioPlayer] Got Current Time', this.currentTime)
this.timeupdate() this.timeupdate()
}, 1000) }, 1000)
}, },
+1 -1
View File
@@ -19,7 +19,7 @@
</div> </div>
<div class="absolute bottom-0 left-0 w-full py-6 px-6 text-gray-300"> <div class="absolute bottom-0 left-0 w-full py-6 px-6 text-gray-300">
<div v-if="serverConnectionConfig" class="mb-4 flex justify-center"> <div v-if="serverConnectionConfig" class="mb-4 flex justify-center">
<p class="text-xs" style="word-break: break-word">{{ serverConnectionConfig.address }} (v{{ serverSettings.version }})</p> <p class="text-xs text-gray-400" style="word-break: break-word">{{ serverConnectionConfig.address }} (v{{ serverSettings.version }})</p>
</div> </div>
<div class="flex items-center"> <div class="flex items-center">
<p class="text-xs">{{ $config.version }}</p> <p class="text-xs">{{ $config.version }}</p>
@@ -0,0 +1,71 @@
<template>
<div v-if="icon" class="flex h-full items-center px-2">
<span class="material-icons-outlined text-lg" :class="iconClass" @click="showAlertDialog">{{ icon }}</span>
</div>
</template>
<script>
import { Dialog } from '@capacitor/dialog'
export default {
data() {
return {}
},
computed: {
user() {
return this.$store.state.user.user
},
socketConnected() {
return this.$store.state.socketConnected
},
networkConnected() {
return this.$store.state.networkConnected
},
networkConnectionType() {
return this.$store.state.networkConnectionType
},
isCellular() {
return this.networkConnectionType === 'cellular'
},
icon() {
if (!this.user) return null // hide when not connected to server
if (!this.networkConnected) {
return 'wifi_off'
} else if (!this.socketConnected) {
return 'cloud_off'
} else if (this.isCellular) {
return 'signal_cellular_alt'
} else {
return 'cloud_done'
}
},
iconClass() {
if (!this.networkConnected) return 'text-error'
else if (!this.socketConnected) return 'text-warning'
else if (this.isCellular) return 'text-gray-200'
else return 'text-success'
}
},
methods: {
showAlertDialog() {
var msg = ''
if (!this.networkConnected) {
msg = 'No internet'
} else if (!this.socketConnected) {
msg = 'Socket not connected'
} else if (this.isCellular) {
msg = 'Socket connected over cellular'
} else {
msg = 'Socket connected over wifi'
}
Dialog.alert({
title: 'Connection Status',
message: msg
})
}
},
mounted() {},
beforeDestroy() {}
}
</script>
+1 -1
View File
@@ -17,7 +17,7 @@ CAP_PLUGIN(AbsDatabase, "AbsDatabase",
CAP_PLUGIN_METHOD(getLocalLibraryItems, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(getLocalLibraryItems, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getLocalLibraryItem, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(getLocalLibraryItem, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getLocalLibraryItemByLLId, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(getLocalLibraryItemByLId, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getLocalLibraryItemsInFolder, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(getLocalLibraryItemsInFolder, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(updateDeviceSettings, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(updateDeviceSettings, CAPPluginReturnPromise);
) )
+1 -1
View File
@@ -80,7 +80,7 @@ public class AbsDatabase: CAPPlugin {
@objc func getLocalLibraryItem(_ call: CAPPluginCall) { @objc func getLocalLibraryItem(_ call: CAPPluginCall) {
call.resolve() call.resolve()
} }
@objc func getLocalLibraryItemByLLId(_ call: CAPPluginCall) { @objc func getLocalLibraryItemByLId(_ call: CAPPluginCall) {
call.resolve() call.resolve()
} }
@objc func getLocalLibraryItemsInFolder(_ call: CAPPluginCall) { @objc func getLocalLibraryItemsInFolder(_ call: CAPPluginCall) {
+6 -6
View File
@@ -9,12 +9,12 @@ install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods def capacitor_pods
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app' pod 'CapacitorApp', :path => '..\..\node_modules\@capacitor\app'
pod 'CapacitorDialog', :path => '../../node_modules/@capacitor/dialog' pod 'CapacitorDialog', :path => '..\..\node_modules\@capacitor\dialog'
pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics' pod 'CapacitorHaptics', :path => '..\..\node_modules\@capacitor\haptics'
pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network' pod 'CapacitorNetwork', :path => '..\..\node_modules\@capacitor\network'
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar' pod 'CapacitorStatusBar', :path => '..\..\node_modules\@capacitor\status-bar'
pod 'CapacitorStorage', :path => '../../node_modules/@capacitor/storage' pod 'CapacitorStorage', :path => '..\..\node_modules\@capacitor\storage'
end end
target 'App' do target 'App' do
+10 -10
View File
@@ -66,9 +66,6 @@ export default {
}, },
currentLibraryId() { currentLibraryId() {
return this.$store.state.libraries.currentLibraryId return this.$store.state.libraries.currentLibraryId
},
isSocketConnected() {
return this.$store.state.socketConnected
} }
}, },
methods: { methods: {
@@ -154,9 +151,6 @@ export default {
// Only cancels stream if streamining not playing downloaded // Only cancels stream if streamining not playing downloaded
this.$eventBus.$emit('close-stream') this.$eventBus.$emit('close-stream')
}, },
socketConnectionUpdate(isConnected) {
console.log('Socket connection update', isConnected)
},
socketConnectionFailed(err) { socketConnectionFailed(err) {
this.$toast.error('Socket connection error: ' + err.message) this.$toast.error('Socket connection error: ' + err.message)
}, },
@@ -177,6 +171,7 @@ export default {
async syncLocalMediaProgress() { async syncLocalMediaProgress() {
if (!this.user) { if (!this.user) {
console.log('[default] No need to sync local media progress - not connected to server') console.log('[default] No need to sync local media progress - not connected to server')
this.$store.commit('setLastLocalMediaSyncResults', null)
return return
} }
@@ -184,10 +179,15 @@ export default {
var response = await this.$db.syncLocalMediaProgressWithServer() var response = await this.$db.syncLocalMediaProgressWithServer()
if (!response) { if (!response) {
if (this.$platform != 'web') this.$toast.error('Failed to sync local media with server') if (this.$platform != 'web') this.$toast.error('Failed to sync local media with server')
this.$store.commit('setLastLocalMediaSyncResults', null)
return return
} }
const { numLocalMediaProgressForServer, numServerProgressUpdates, numLocalProgressUpdates } = response const { numLocalMediaProgressForServer, numServerProgressUpdates, numLocalProgressUpdates } = response
if (numLocalMediaProgressForServer > 0) { if (numLocalMediaProgressForServer > 0) {
response.syncedAt = Date.now()
response.serverConfigName = this.$store.getters['user/getServerConfigName']
this.$store.commit('setLastLocalMediaSyncResults', response)
if (numServerProgressUpdates > 0 || numLocalProgressUpdates > 0) { if (numServerProgressUpdates > 0 || numLocalProgressUpdates > 0) {
console.log(`[default] ${numServerProgressUpdates} Server progress updates | ${numLocalProgressUpdates} Local progress updates`) console.log(`[default] ${numServerProgressUpdates} Server progress updates | ${numLocalProgressUpdates} Local progress updates`)
} else { } else {
@@ -195,6 +195,7 @@ export default {
} }
} else { } else {
console.log('[default] syncLocalMediaProgress No local media progress to sync') console.log('[default] syncLocalMediaProgress No local media progress to sync')
this.$store.commit('setLastLocalMediaSyncResults', null)
} }
}, },
async userUpdated(user) { async userUpdated(user) {
@@ -216,9 +217,10 @@ export default {
mediaProgress: prog mediaProgress: prog
} }
newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress(payload) newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress(payload)
} else { } else if (!localProg) {
// Check if local library item exists // Check if local library item exists
var localLibraryItem = await this.$db.getLocalLibraryItemByLLId(prog.libraryItemId) // local media progress may not exist yet if it hasn't been played
var localLibraryItem = await this.$db.getLocalLibraryItemByLId(prog.libraryItemId)
if (localLibraryItem) { if (localLibraryItem) {
if (prog.episodeId) { if (prog.episodeId) {
// If episode check if local episode exists // If episode check if local episode exists
@@ -253,7 +255,6 @@ export default {
} }
}, },
async mounted() { async mounted() {
this.$socket.on('connection-update', this.socketConnectionUpdate)
this.$socket.on('initialized', this.socketInit) this.$socket.on('initialized', this.socketInit)
this.$socket.on('user_updated', this.userUpdated) this.$socket.on('user_updated', this.userUpdated)
this.$socket.on('user_media_progress_updated', this.userMediaProgressUpdated) this.$socket.on('user_media_progress_updated', this.userMediaProgressUpdated)
@@ -282,7 +283,6 @@ export default {
} }
}, },
beforeDestroy() { beforeDestroy() {
this.$socket.off('connection-update', this.socketConnectionUpdate)
this.$socket.off('initialized', this.socketInit) this.$socket.off('initialized', this.socketInit)
this.$socket.off('user_updated', this.userUpdated) this.$socket.off('user_updated', this.userUpdated)
this.$socket.off('user_media_progress_updated', this.userMediaProgressUpdated) this.$socket.off('user_media_progress_updated', this.userMediaProgressUpdated)
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-app", "name": "audiobookshelf-app",
"version": "0.9.51-beta", "version": "0.9.52-beta",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-app", "name": "audiobookshelf-app",
"version": "0.9.51-beta", "version": "0.9.52-beta",
"author": "advplyr", "author": "advplyr",
"scripts": { "scripts": {
"dev": "nuxt --hostname 0.0.0.0 --port 1337", "dev": "nuxt --hostname 0.0.0.0 --port 1337",
+1 -1
View File
@@ -136,7 +136,7 @@ export default {
}) })
// Check if // Check if
if (libraryItem) { if (libraryItem) {
var localLibraryItem = await app.$db.getLocalLibraryItemByLLId(libraryItemId) var localLibraryItem = await app.$db.getLocalLibraryItemByLId(libraryItemId)
if (localLibraryItem) { if (localLibraryItem) {
console.log('Library item has local library item also', localLibraryItem.id) console.log('Library item has local library item also', localLibraryItem.id)
libraryItem.localLibraryItem = localLibraryItem libraryItem.localLibraryItem = localLibraryItem
+45 -2
View File
@@ -1,5 +1,28 @@
<template> <template>
<div class="w-full h-full py-6"> <div class="w-full h-full py-6">
<div v-if="lastLocalMediaSyncResults" class="px-2 mb-4">
<div class="w-full pl-2 pr-2 py-2 bg-black bg-opacity-25 rounded-lg relative">
<div class="flex items-center mb-1">
<span class="material-icons text-success text-xl">sync</span>
<p class="text-sm text-gray-300 pl-2">Local media progress synced with server</p>
</div>
<div class="flex justify-between mb-1.5">
<p class="text-xs text-gray-400 font-semibold">{{ syncedServerConfigName }}</p>
<p class="text-xs text-gray-400 italic">{{ $dateDistanceFromNow(syncedAt) }}</p>
</div>
<div v-if="!numLocalProgressUpdates && !numServerProgressUpdates">
<p class="text-sm text-gray-300">Local media progress was up-to-date with server ({{ numLocalMediaSynced }} item{{ numLocalMediaSynced == 1 ? '' : 's' }})</p>
</div>
<div v-else>
<p v-if="numServerProgressUpdates" class="text-sm text-gray-300">- {{ numServerProgressUpdates }} local media item{{ numServerProgressUpdates === 1 ? '' : 's' }} progress was updated on the server (local more recent).</p>
<p v-else class="text-sm text-gray-300">- No local media progress had to be synced on the server.</p>
<p v-if="numLocalProgressUpdates" class="text-sm text-gray-300">- {{ numLocalProgressUpdates }} local media item{{ numLocalProgressUpdates === 1 ? '' : 's' }} progress was updated to match the server (server more recent).</p>
<p v-else class="text-sm text-gray-300">- No server progress had to be synced with local media progress.</p>
</div>
</div>
</div>
<h1 class="text-base font-semibold px-3 mb-2">Local Folders</h1> <h1 class="text-base font-semibold px-3 mb-2">Local Folders</h1>
<div v-if="!isIos" class="w-full max-w-full px-3 py-2"> <div v-if="!isIos" class="w-full max-w-full px-3 py-2">
@@ -49,8 +72,28 @@ export default {
isIos() { isIos() {
return this.$platform === 'ios' return this.$platform === 'ios'
}, },
isSocketConnected() { lastLocalMediaSyncResults() {
return this.$store.state.socketConnected return this.$store.state.lastLocalMediaSyncResults
},
numLocalMediaSynced() {
if (!this.lastLocalMediaSyncResults) return 0
return this.lastLocalMediaSyncResults.numLocalMediaProgressForServer || 0
},
syncedAt() {
if (!this.lastLocalMediaSyncResults) return 0
return this.lastLocalMediaSyncResults.syncedAt || 0
},
syncedServerConfigName() {
if (!this.lastLocalMediaSyncResults) return ''
return this.lastLocalMediaSyncResults.serverConfigName
},
numLocalProgressUpdates() {
if (!this.lastLocalMediaSyncResults) return 0
return this.lastLocalMediaSyncResults.numLocalProgressUpdates || 0
},
numServerProgressUpdates() {
if (!this.lastLocalMediaSyncResults) return 0
return this.lastLocalMediaSyncResults.numServerProgressUpdates || 0
} }
}, },
methods: { methods: {
+1 -1
View File
@@ -164,7 +164,7 @@ class AbsDatabaseWeb extends WebPlugin {
async getLocalLibraryItem({ id }) { async getLocalLibraryItem({ id }) {
return this.getLocalLibraryItems().then((data) => data.value[0]) return this.getLocalLibraryItems().then((data) => data.value[0])
} }
async getLocalLibraryItemByLLId({ libraryItemId }) { async getLocalLibraryItemByLId({ libraryItemId }) {
return this.getLocalLibraryItems().then((data) => data.value.find(lli => lli.libraryItemId == libraryItemId)) return this.getLocalLibraryItems().then((data) => data.value.find(lli => lli.libraryItemId == libraryItemId))
} }
async getAllLocalMediaProgress() { async getAllLocalMediaProgress() {
+2 -2
View File
@@ -54,8 +54,8 @@ class DbService {
return AbsDatabase.getLocalLibraryItem({ id }) return AbsDatabase.getLocalLibraryItem({ id })
} }
getLocalLibraryItemByLLId(libraryItemId) { getLocalLibraryItemByLId(libraryItemId) {
return AbsDatabase.getLocalLibraryItemByLLId({ libraryItemId }) return AbsDatabase.getLocalLibraryItemByLId({ libraryItemId })
} }
getAllLocalMediaProgress() { getAllLocalMediaProgress() {
+9 -5
View File
@@ -39,6 +39,7 @@ class ServerSocket extends EventEmitter {
logout() { logout() {
if (this.socket) this.socket.disconnect() if (this.socket) this.socket.disconnect()
this.removeListeners()
} }
setSocketListeners() { setSocketListeners() {
@@ -54,6 +55,14 @@ class ServerSocket extends EventEmitter {
// }) // })
} }
removeListeners() {
if (!this.socket) return
this.socket.removeAllListeners()
if (this.socket.io && this.socket.io.removeAllListeners) {
this.socket.io.removeAllListeners()
}
}
onConnect() { onConnect() {
console.log('[SOCKET] Socket Connected ' + this.socket.id) console.log('[SOCKET] Socket Connected ' + this.socket.id)
this.connected = true this.connected = true
@@ -67,11 +76,6 @@ class ServerSocket extends EventEmitter {
this.connected = false this.connected = false
this.$store.commit('setSocketConnected', false) this.$store.commit('setSocketConnected', false)
this.emit('connection-update', false) this.emit('connection-update', false)
this.socket.removeAllListeners()
if (this.socket.io && this.socket.io.removeAllListeners) {
this.socket.io.removeAllListeners()
}
} }
onInit(data) { onInit(data) {
+5 -1
View File
@@ -19,7 +19,8 @@ export const state = () => ({
showSideDrawer: false, showSideDrawer: false,
isNetworkListenerInit: false, isNetworkListenerInit: false,
serverSettings: null, serverSettings: null,
lastBookshelfScrollData: {} lastBookshelfScrollData: {},
lastLocalMediaSyncResults: null
}) })
export const getters = { export const getters = {
@@ -126,5 +127,8 @@ export const mutations = {
setServerSettings(state, val) { setServerSettings(state, val) {
state.serverSettings = val state.serverSettings = val
this.$localStore.setServerSettings(state.serverSettings) this.$localStore.setServerSettings(state.serverSettings)
},
setLastLocalMediaSyncResults(state, val) {
state.lastLocalMediaSyncResults = val
} }
} }
+3
View File
@@ -22,6 +22,9 @@ export const getters = {
getServerAddress: (state) => { getServerAddress: (state) => {
return state.serverConnectionConfig ? state.serverConnectionConfig.address : null return state.serverConnectionConfig ? state.serverConnectionConfig.address : null
}, },
getServerConfigName: (state) => {
return state.serverConnectionConfig ? state.serverConnectionConfig.name : null
},
getCustomHeaders: (state) => { getCustomHeaders: (state) => {
return state.serverConnectionConfig ? state.serverConnectionConfig.customHeaders : null return state.serverConnectionConfig ? state.serverConnectionConfig.customHeaders : null
}, },
+3
View File
@@ -36,6 +36,9 @@ module.exports = {
}, },
fontSize: { fontSize: {
xxs: '0.625rem' xxs: '0.625rem'
},
maxWidth: {
'24': '6rem'
} }
} }
}, },