Compare commits

...
Author SHA1 Message Date
advplyr 0c49bcfe3f iOS version bump 0.9.44 2022-05-06 20:04:42 -05:00
advplyr aecb6a8dd2 Android version bump 0.9.44-beta 2022-05-06 20:01:01 -05:00
advplyr 718947522e Fix:Remove download notifications after download item finished #161 2022-05-06 19:45:07 -05:00
advplyr 2f8ca51447 Add:Vibrate when sleep timer is extended or reset by shaking #169 2022-05-06 19:28:07 -05:00
advplyr 4fa6dd2616 Fix:Cover crashing when using in media notification. Convert content uri for local image to File and pass main activity context instead of player service context #35 #164 2022-05-06 18:57:05 -05:00
advplyr 736e57fafd Add:Fallback to transcode when direct play fails, and send playback failed event to client 2022-05-06 18:17:45 -05:00
advplyr 30d86279a5 Update:Host fonts locally 2022-05-05 18:25:32 -05:00
advplyrandGitHub 8bbfcdeb82 Merge pull request #166 from rasmuslos/master
Added sleep timer
2022-05-05 07:49:44 -05:00
Rasmus Krämer d23cf62264 Fix sleep timer increase & decrease 2022-05-05 14:38:34 +02:00
advplyr 73d5b19d2b Update:Casting when playing local item if connected to server linked to local item #170, Update:Play button color when playing local item verse server item, Add:Show play method at the top of audio player (Direct, Local, Transcode) 2022-05-04 19:31:56 -05:00
Rasmus Krämer 1959351125 Removed unnecessary comment 2022-05-03 15:03:53 +02:00
Rasmus Krämer 0708133779 Fixed crash 2022-05-03 15:01:30 +02:00
Rasmus Krämer 9a81fc3688 Merge branch 'advplyr-master' 2022-05-03 14:42:20 +02:00
Rasmus Krämer ac71d39265 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app into advplyr-master 2022-05-03 14:41:46 +02:00
Rasmus Krämer 4203654ec8 Added sleep timer 2022-05-03 14:32:46 +02:00
Rasmus Krämer 9701c767b2 Small improvements 2022-05-03 12:55:13 +02:00
advplyrandGitHub 0223df4f9e Merge pull request #162 from benonymity/ListViewImprovements
Tweaks for bookshelf list view
2022-05-01 18:34:48 -05:00
benonymity 7549385404 Return null if not podcast 2022-05-01 18:27:02 -04:00
benonymity 3f2d0ed8b1 Display number of podcast episodes in list view 2022-05-01 18:03:37 -04:00
benonymity e4a5927e07 Tweaks for bookshelf list view 2022-05-01 16:49:56 -04:00
advplyr a3aac4da75 Update: iOS AudioPlayer direct play with multi-track using AVQueuePlayer 2022-05-01 12:19:31 -05:00
advplyr 1e9453e501 Fix:Check google services are available before initializing chromecast #158 2022-04-30 16:33:41 -05:00
advplyr bec3f5841e Fix:Setting local library item as finished when local media progress does not exist 2022-04-30 15:35:20 -05:00
advplyrandGitHub 068762912f Merge pull request #134 from benonymity/MarkAsRead
Add mark as read button
2022-04-30 14:58:45 -05:00
advplyr 01e85d0e91 Fix:Android replace reserved characters in filenames #154 2022-04-30 14:45:55 -05:00
benonymity a67c19f30f Shorten play button text 2022-04-30 15:01:27 -04:00
Rasmus Krämer 394363c8cb Send report when playback ends 2022-04-30 10:58:08 +02:00
benonymity 7fd51ebcc1 Fix button padding, final tests 2022-04-28 20:05:25 -04:00
benonymity ae4678cf24 Resize button, no podcast downloads on iOS 2022-04-28 17:13:21 -04:00
benonymity 68e565ebe2 Don't show if a podcast 2022-04-28 16:38:27 -04:00
benonymity d99f4406b7 Removed episode reference, smaller play button 2022-04-28 13:21:47 -04:00
BenandGitHub 2064cd8380 Merge branch 'advplyr:master' into MarkAsRead 2022-04-28 12:24:44 -04:00
benonymity 415ff65561 Display when local, add CSS classes 2022-04-27 15:19:29 -04:00
benonymity 1fed00ca81 Add localLIbraryProgress updates 2022-04-24 22:02:22 -04:00
BenandGitHub a63022a669 Merge branch 'advplyr:master' into MarkAsRead 2022-04-24 21:40:01 -04:00
benonymity 114dbd24bc Add button to mark book as read 2022-04-23 00:27:03 -04:00
38 changed files with 1240 additions and 323 deletions
+2 -2
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 72 versionCode 73
versionName "0.9.43-beta" versionName "0.9.44-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.
@@ -5,6 +5,9 @@ import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.support.v4.media.session.MediaControllerCompat import android.support.v4.media.session.MediaControllerCompat
import android.util.Log import android.util.Log
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.file.getAbsolutePath
import com.anggrayudi.storage.file.toRawFile
import com.audiobookshelf.app.R import com.audiobookshelf.app.R
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
@@ -60,10 +63,25 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
private suspend fun resolveUriAsBitmap(uri: Uri): Bitmap? { private suspend fun resolveUriAsBitmap(uri: Uri): Bitmap? {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
// Block on downloading artwork. // Block on downloading artwork.
val context = playerNotificationService.getContext()
// Fix attempt for #35 local cover crashing
// Convert content uri to a file and pass to Glide
var urival:Any = uri
if (uri.toString().startsWith("content:")) {
val imageDocFile = DocumentFile.fromSingleUri(context, uri)
Log.d(tag, "Converting local content url $uri to file with path ${imageDocFile?.getAbsolutePath(context)}")
val file = imageDocFile?.toRawFile(context)
file?.let {
Log.d(tag, "Using local file image instead of content uri ${it.absolutePath}")
urival = it
}
}
try { try {
Glide.with(playerNotificationService).applyDefaultRequestOptions(glideOptions) Glide.with(context).applyDefaultRequestOptions(glideOptions)
.asBitmap() .asBitmap()
.load(uri) .load(urival)
.placeholder(R.drawable.icon) .placeholder(R.drawable.icon)
.error(R.drawable.icon) .error(R.drawable.icon)
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE) .submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)
@@ -71,7 +89,7 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
Glide.with(playerNotificationService).applyDefaultRequestOptions(glideOptions) Glide.with(context).applyDefaultRequestOptions(glideOptions)
.asBitmap() .asBitmap()
.load(Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)) .load(Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon))
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE) .submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)
@@ -539,6 +539,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
return if(currentPlayer == castPlayer) "cast-player" else "exo-player" return if(currentPlayer == castPlayer) "cast-player" else "exo-player"
} }
fun getContext():Context {
return ctx
}
// //
// MEDIA BROWSER STUFF (ANDROID AUTO) // MEDIA BROWSER STUFF (ANDROID AUTO)
// //
@@ -1,7 +1,7 @@
package com.audiobookshelf.app.player package com.audiobookshelf.app.player
import android.os.Handler import android.content.Context
import android.os.Looper import android.os.*
import android.util.Log import android.util.Log
import java.util.* import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
@@ -9,9 +9,8 @@ import kotlin.math.roundToInt
const val SLEEP_EXTENSION_TIME = 900000L // 15m const val SLEEP_EXTENSION_TIME = 900000L // 15m
class SleepTimerManager constructor(playerNotificationService:PlayerNotificationService) { class SleepTimerManager constructor(val playerNotificationService:PlayerNotificationService) {
private val tag = "SleepTimerManager" private val tag = "SleepTimerManager"
private val playerNotificationService:PlayerNotificationService = playerNotificationService
private var sleepTimerTask:TimerTask? = null private var sleepTimerTask:TimerTask? = null
private var sleepTimerRunning:Boolean = false private var sleepTimerRunning:Boolean = false
@@ -64,7 +63,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
// Register shake sensor // Register shake sensor
playerNotificationService.registerSensor() playerNotificationService.registerSensor()
var currentTime = getCurrentTime() val currentTime = getCurrentTime()
if (isChapterTime) { if (isChapterTime) {
if (currentTime > time) { if (currentTime > time) {
Log.d(tag, "Invalid sleep timer - current time is already passed chapter time $time") Log.d(tag, "Invalid sleep timer - current time is already passed chapter time $time")
@@ -95,7 +94,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
if (getIsPlaying()) { if (getIsPlaying()) {
sleepTimerElapsed += 1000L sleepTimerElapsed += 1000L
var sleepTimeSecondsRemaining = getSleepTimerTimeRemainingSeconds() val sleepTimeSecondsRemaining = getSleepTimerTimeRemainingSeconds()
Log.d(tag, "Timer Elapsed $sleepTimerElapsed | Sleep TIMER time remaining $sleepTimeSecondsRemaining s") Log.d(tag, "Timer Elapsed $sleepTimerElapsed | Sleep TIMER time remaining $sleepTimeSecondsRemaining s")
if (sleepTimeSecondsRemaining > 0) { if (sleepTimeSecondsRemaining > 0) {
@@ -111,7 +110,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
sleepTimerFinishedAt = System.currentTimeMillis() sleepTimerFinishedAt = System.currentTimeMillis()
} else if (sleepTimeSecondsRemaining <= 30) { } else if (sleepTimeSecondsRemaining <= 30) {
// Start fading out audio // Start fading out audio
var volume = sleepTimeSecondsRemaining / 30F val volume = sleepTimeSecondsRemaining / 30F
Log.d(tag, "SLEEP VOLUME FADE $volume | ${sleepTimeSecondsRemaining}s remaining") Log.d(tag, "SLEEP VOLUME FADE $volume | ${sleepTimeSecondsRemaining}s remaining")
setVolume(volume) setVolume(volume)
} }
@@ -129,7 +128,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
playerNotificationService.unregisterSensor() playerNotificationService.unregisterSensor()
} }
fun getSleepTimerTime():Long? { fun getSleepTimerTime():Long {
return sleepTimerEndTime return sleepTimerEndTime
} }
@@ -139,6 +138,30 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
playerNotificationService.clientEventEmitter?.onSleepTimerSet(0) playerNotificationService.clientEventEmitter?.onSleepTimerSet(0)
} }
// Vibrate when extending sleep timer by shaking
private fun vibrate() {
val context = playerNotificationService.getContext()
val vibrator:Vibrator
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager =
context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
vibrator = vibratorManager.defaultVibrator
} else {
@Suppress("DEPRECATION")
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
}
vibrator.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val vibrationEffect = VibrationEffect.createWaveform(longArrayOf(0, 150, 150, 150),-1)
it.vibrate(vibrationEffect)
} else {
@Suppress("DEPRECATION")
it.vibrate(10)
}
}
}
private fun extendSleepTime() { private fun extendSleepTime() {
if (!sleepTimerRunning) return if (!sleepTimerRunning) return
setVolume(1F) setVolume(1F)
@@ -157,7 +180,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
if (!sleepTimerRunning) { if (!sleepTimerRunning) {
if (sleepTimerFinishedAt <= 0L) return if (sleepTimerFinishedAt <= 0L) return
var finishedAtDistance = System.currentTimeMillis() - sleepTimerFinishedAt val finishedAtDistance = System.currentTimeMillis() - sleepTimerFinishedAt
if (finishedAtDistance > SLEEP_TIMER_WAKE_UP_EXPIRATION) // 2 minutes if (finishedAtDistance > SLEEP_TIMER_WAKE_UP_EXPIRATION) // 2 minutes
{ {
Log.d(tag, "Sleep timer finished over 2 mins ago, clearing it") Log.d(tag, "Sleep timer finished over 2 mins ago, clearing it")
@@ -165,14 +188,18 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
return return
} }
var newSleepTime = if (sleepTimerExtensionTime >= 0) sleepTimerExtensionTime else SLEEP_EXTENSION_TIME val newSleepTime = if (sleepTimerExtensionTime >= 0) sleepTimerExtensionTime else SLEEP_EXTENSION_TIME
vibrate()
setSleepTimer(newSleepTime, false) setSleepTimer(newSleepTime, false)
play() play()
return return
} }
// Only extend if within 30 seconds of finishing // Only extend if within 30 seconds of finishing
var sleepTimeRemaining = getSleepTimerTimeRemainingSeconds() val sleepTimeRemaining = getSleepTimerTimeRemainingSeconds()
if (sleepTimeRemaining <= 30) extendSleepTime() if (sleepTimeRemaining <= 30) {
vibrate()
extendSleepTime()
}
} }
fun handleShake() { fun handleShake() {
@@ -188,7 +215,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
sleepTimerLength += time sleepTimerLength += time
if (sleepTimerLength + getCurrentTime() > getDuration()) sleepTimerLength = getDuration() - getCurrentTime() if (sleepTimerLength + getCurrentTime() > getDuration()) sleepTimerLength = getDuration() - getCurrentTime()
} else { } else {
var newSleepEndTime = sleepTimerEndTime + time val newSleepEndTime = sleepTimerEndTime + time
sleepTimerEndTime = if (newSleepEndTime >= getDuration()) { sleepTimerEndTime = if (newSleepEndTime >= getDuration()) {
getDuration() getDuration()
} else { } else {
@@ -209,7 +236,7 @@ class SleepTimerManager constructor(playerNotificationService:PlayerNotification
sleepTimerLength -= time sleepTimerLength -= time
if (sleepTimerLength <= 0) sleepTimerLength = 1000L if (sleepTimerLength <= 0) sleepTimerLength = 1000L
} else { } else {
var newSleepEndTime = sleepTimerEndTime - time val newSleepEndTime = sleepTimerEndTime - time
sleepTimerEndTime = if (newSleepEndTime <= 1000) { sleepTimerEndTime = if (newSleepEndTime <= 1000) {
// End sleep timer in 1 second // End sleep timer in 1 second
getCurrentTime() + 1000 getCurrentTime() + 1000
@@ -16,6 +16,8 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.* import com.getcapacitor.*
import com.getcapacitor.annotation.CapacitorPlugin import com.getcapacitor.annotation.CapacitorPlugin
import com.google.android.gms.cast.CastDevice 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 import org.json.JSONObject
@CapacitorPlugin(name = "AbsAudioPlayer") @CapacitorPlugin(name = "AbsAudioPlayer")
@@ -25,7 +27,7 @@ class AbsAudioPlayer : Plugin() {
private lateinit var mainActivity: MainActivity private lateinit var mainActivity: MainActivity
private lateinit var apiHandler:ApiHandler private lateinit var apiHandler:ApiHandler
lateinit var castManager:CastManager var castManager:CastManager? = null
lateinit var playerNotificationService: PlayerNotificationService lateinit var playerNotificationService: PlayerNotificationService
@@ -95,6 +97,24 @@ class AbsAudioPlayer : Plugin() {
} }
private fun initCastManager() { 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() { val connListener = object: CastManager.ChromecastListener() {
override fun onReceiverAvailableUpdate(available: Boolean) { override fun onReceiverAvailableUpdate(available: Boolean) {
Log.d(tag, "ChromecastListener: CAST Receiver Update Available $available") Log.d(tag, "ChromecastListener: CAST Receiver Update Available $available")
@@ -128,7 +148,7 @@ class AbsAudioPlayer : Plugin() {
} }
castManager = CastManager(mainActivity) castManager = CastManager(mainActivity)
castManager.startRouteScan(connListener) castManager?.startRouteScan(connListener)
} }
@PluginMethod @PluginMethod
@@ -144,7 +164,7 @@ class AbsAudioPlayer : Plugin() {
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
var playbackRate = call.getFloat("playbackRate",1f) ?: 1f val playbackRate = call.getFloat("playbackRate",1f) ?: 1f
if (libraryItemId.isEmpty()) { if (libraryItemId.isEmpty()) {
Log.e(tag, "Invalid call to play library item no library item id") 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 // Need to make sure the player service has been started
Log.d(tag, "CAST REQUEST SESSION PLUGIN") Log.d(tag, "CAST REQUEST SESSION PLUGIN")
call.resolve() 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) { override fun onError(errorCode: Int) {
Log.e(tag, "CAST REQUEST SESSION CALLBACK ERROR $errorCode") Log.e(tag, "CAST REQUEST SESSION CALLBACK ERROR $errorCode")
} }
@@ -206,45 +206,93 @@ class AbsDatabase : Plugin() {
@PluginMethod @PluginMethod
fun updateLocalMediaProgressFinished(call:PluginCall) { fun updateLocalMediaProgressFinished(call:PluginCall) {
var localMediaProgressId = call.getString("localMediaProgressId", "").toString() val localLibraryItemId = call.getString("localLibraryItemId", "").toString()
var isFinished = call.getBoolean("isFinished", false) == true 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") Log.d(tag, "updateLocalMediaProgressFinished $localMediaProgressId | Is Finished:$isFinished")
var localMediaProgress = DeviceManager.dbManager.getLocalMediaProgress(localMediaProgressId) var localMediaProgress = DeviceManager.dbManager.getLocalMediaProgress(localMediaProgressId)
if (localMediaProgress == null) {
Log.e(tag, "updateLocalMediaProgressFinished Local Media Progress not found $localMediaProgressId") if (localMediaProgress == null) { // Create new local media progress if does not exist
call.resolve(JSObject("{\"error\":\"Progress not found\"}")) 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 { } else {
localMediaProgress.updateIsFinished(isFinished) localMediaProgress.updateIsFinished(isFinished)
}
var lmpstring = jacksonMapper.writeValueAsString(localMediaProgress) // Save local media progress locally
Log.d(tag, "updateLocalMediaProgressFinished: Local Media Progress String $lmpstring") DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
// Send update to server media progress is linked to a server and user is logged into that server val lmpstring = jacksonMapper.writeValueAsString(localMediaProgress)
localMediaProgress.serverConnectionConfigId?.let { configId -> Log.d(tag, "updateLocalMediaProgressFinished: Local Media Progress String $lmpstring")
if (DeviceManager.serverConnectionConfigId == configId) {
var libraryItemId = localMediaProgress.libraryItemId ?: "" // Send update to server media progress is linked to a server and user is logged into that server
var episodeId = localMediaProgress.episodeId ?: "" localMediaProgress.serverConnectionConfigId?.let { configId ->
var updatePayload = JSObject() if (DeviceManager.serverConnectionConfigId == configId) {
updatePayload.put("isFinished", isFinished) var libraryItemId = localMediaProgress.libraryItemId ?: ""
apiHandler.updateMediaProgress(libraryItemId,episodeId,updatePayload) { var episodeId = localMediaProgress.episodeId ?: ""
Log.d(tag, "updateLocalMediaProgressFinished: Updated media progress isFinished on server") var updatePayload = JSObject()
var jsobj = JSObject() updatePayload.put("isFinished", isFinished)
jsobj.put("local", true) apiHandler.updateMediaProgress(libraryItemId,episodeId,updatePayload) {
jsobj.put("server", true) Log.d(tag, "updateLocalMediaProgressFinished: Updated media progress isFinished on server")
jsobj.put("localMediaProgress", JSObject(lmpstring)) var jsobj = JSObject()
call.resolve(jsobj) jsobj.put("local", true)
// call.resolve(JSObject("{\"local\":true,\"server\":true,\"localMediaProgress\":$lmpstring}")) 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}}")) if (localMediaProgress.serverConnectionConfigId == null || DeviceManager.serverConnectionConfigId != localMediaProgress.serverConnectionConfigId) {
var jsobj = JSObject() var jsobj = JSObject()
jsobj.put("local", true) jsobj.put("local", true)
jsobj.put("server", false) jsobj.put("server", false)
jsobj.put("localMediaProgress", JSObject(lmpstring)) jsobj.put("localMediaProgress", JSObject(lmpstring))
call.resolve(jsobj) call.resolve(jsobj)
}
} }
} }
@@ -180,15 +180,30 @@ class AbsDownloader : Plugin() {
// Item filenames could be the same if they are in sub-folders, this will make them unique // Item filenames could be the same if they are in sub-folders, this will make them unique
private fun getFilenameFromRelPath(relPath: String): String { 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 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?) { private fun startLibraryItemDownload(libraryItem: LibraryItem, localFolder: LocalFolder, episode:PodcastEpisode?) {
val tempFolderPath = mainActivity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) val tempFolderPath = mainActivity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
if (libraryItem.mediaType == "book") { if (libraryItem.mediaType == "book") {
val bookTitle = libraryItem.media.metadata.title val bookTitle = cleanStringForFileSystem(libraryItem.media.metadata.title)
val tracks = libraryItem.media.getAudioTracks() val tracks = libraryItem.media.getAudioTracks()
Log.d(tag, "Starting library item download with ${tracks.size} tracks") Log.d(tag, "Starting library item download with ${tracks.size} tracks")
val itemFolderPath = localFolder.absolutePath + "/" + bookTitle val itemFolderPath = localFolder.absolutePath + "/" + bookTitle
@@ -243,8 +258,8 @@ class AbsDownloader : Plugin() {
} }
} else { } else {
// Podcast episode download // Podcast episode download
val podcastTitle = cleanStringForFileSystem(libraryItem.media.metadata.title)
val podcastTitle = libraryItem.media.metadata.title
val audioTrack = episode?.audioTrack val audioTrack = episode?.audioTrack
Log.d(tag, "Starting podcast episode download") Log.d(tag, "Starting podcast episode download")
val itemFolderPath = localFolder.absolutePath + "/" + podcastTitle val itemFolderPath = localFolder.absolutePath + "/" + podcastTitle
@@ -310,6 +325,13 @@ class AbsDownloader : Plugin() {
delay(500) delay(500)
} }
// Remove download notifications
downloadItem.downloadItemParts.forEach { downloadItemPart ->
downloadItemPart.downloadId?.let {
downloadManager.remove(it)
}
}
val downloadItemScanResult = folderScanner.scanDownloadItem(downloadItem) val downloadItemScanResult = folderScanner.scanDownloadItem(downloadItem)
DeviceManager.dbManager.removeDownloadItem(downloadItem.id) DeviceManager.dbManager.removeDownloadItem(downloadItem.id)
downloadQueue.remove(downloadItem) downloadQueue.remove(downloadItem)
@@ -352,6 +374,7 @@ class AbsDownloader : Plugin() {
if (!downloadItemPart.completed) { if (!downloadItemPart.completed) {
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Done") Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Done")
downloadItemPart.completed = true downloadItemPart.completed = true
val file = DocumentFileCompat.fromUri(mainActivity, downloadItemPart.destinationUri) val file = DocumentFileCompat.fromUri(mainActivity, downloadItemPart.destinationUri)
Log.d(tag, "DOWNLOAD: Attempt move for file at destination ${downloadItemPart.destinationUri} | ${file?.getBasePath(mainActivity)}") Log.d(tag, "DOWNLOAD: Attempt move for file at destination ${downloadItemPart.destinationUri} | ${file?.getBasePath(mainActivity)}")
+275 -40
View File
@@ -1,10 +1,10 @@
@font-face { @font-face {
font-family: 'Material Icons'; font-family: 'Material Icons';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: url(/fonts/MaterialIcons.woff2) format('woff2'); src: url(/fonts/MaterialIcons.woff2) format('woff2');
} }
@font-face { @font-face {
font-family: 'Material Icons Outlined'; font-family: 'Material Icons Outlined';
font-style: normal; font-style: normal;
@@ -12,43 +12,6 @@
src: url(/fonts/MaterialIconsOutlined.woff2) format('woff2'); src: url(/fonts/MaterialIconsOutlined.woff2) format('woff2');
} }
/* .material-icons {
font-family: 'Material Icons';
font-weight: normal;
font-style: normal;
line-height: 1;
font-size: 1.5rem;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased;
}
.material-icons.text-icon {
font-size: 1.15rem;
}
.material-icons.text-lg {
font-size: 1.25rem;
}
.material-icons.text-2xl {
font-size: 1.5rem;
}
.material-icons.text-3xl {
font-size: 1.875rem;
}
.material-icons.text-4xl {
font-size: 2.25rem;
}
.material-icons.text-5xl {
font-size: 3rem;
}
.material-icons.text-base {
font-size: 1rem;
} */
.material-icons { .material-icons {
font-family: 'Material Icons'; font-family: 'Material Icons';
font-weight: normal; font-weight: normal;
@@ -60,9 +23,9 @@
white-space: nowrap; white-space: nowrap;
word-wrap: normal; word-wrap: normal;
direction: ltr; direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
.material-icons:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl):not(.text-7xl):not(.text-8xl) { .material-icons:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl):not(.text-7xl):not(.text-8xl) {
font-size: 1.5rem; font-size: 1.5rem;
} }
@@ -78,9 +41,9 @@
white-space: nowrap; white-space: nowrap;
word-wrap: normal; word-wrap: normal;
direction: ltr; direction: ltr;
-webkit-font-feature-settings: 'liga';
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
} }
.material-icons-outlined:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl):not(.text-7xl):not(.text-8xl) { .material-icons-outlined:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl):not(.text-7xl):not(.text-8xl) {
font-size: 1.5rem; font-size: 1.5rem;
} }
@@ -93,6 +56,7 @@
src: url(/fonts/GentiumBookBasic.woff2) format('woff2'); src: url(/fonts/GentiumBookBasic.woff2) format('woff2');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
} }
/* latin */ /* latin */
@font-face { @font-face {
font-family: 'Gentium Book Basic'; font-family: 'Gentium Book Basic';
@@ -101,4 +65,275 @@
font-display: swap; font-display: swap;
src: url(/fonts/GentiumBookBasic.woff2) format('woff2'); src: url(/fonts/GentiumBookBasic.woff2) format('woff2');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Light.ttf) format('ttf');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-Regular.ttf) format('ttf');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
unicode-range: U+0370-03FF;
}
/* vietnamese */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+1EA0-1EF9, U+20AB;
}
/* latin-ext */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Source Sans Pro';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url(/fonts/Source_Sans_Pro/SourceSansPro-SemiBold.ttf) format('ttf');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* cyrillic-ext */
@font-face {
font-family: 'Ubuntu Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
/* cyrillic */
@font-face {
font-family: 'Ubuntu Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
/* greek-ext */
@font-face {
font-family: 'Ubuntu Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
unicode-range: U+1F00-1FFF;
}
/* greek */
@font-face {
font-family: 'Ubuntu Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
unicode-range: U+0370-03FF;
}
/* latin-ext */
@font-face {
font-family: 'Ubuntu Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* latin */
@font-face {
font-family: 'Ubuntu Mono';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
} }
+1 -3
View File
@@ -80,11 +80,9 @@ export default {
methods: { methods: {
castClick() { castClick() {
if (this.$store.state.playerIsLocal) { if (this.$store.state.playerIsLocal) {
this.$toast.warn('Cannot cast downloaded media item') this.$eventBus.$emit('cast-local-item')
return return
} }
console.log('Cast Btn Click')
AbsAudioPlayer.requestSession() AbsAudioPlayer.requestSession()
}, },
clickShowSideDrawer() { clickShowSideDrawer() {
+8 -6
View File
@@ -4,7 +4,7 @@
<div class="top-2 left-4 absolute cursor-pointer"> <div class="top-2 left-4 absolute cursor-pointer">
<span class="material-icons text-5xl" @click="collapseFullscreen">expand_more</span> <span class="material-icons text-5xl" @click="collapseFullscreen">expand_more</span>
</div> </div>
<div v-show="showCastBtn" class="top-3.5 right-20 absolute cursor-pointer"> <div v-show="showCastBtn" class="top-4 right-16 absolute cursor-pointer">
<span class="material-icons text-3xl" :class="isCasting ? 'text-success' : ''" @click="castClick">cast</span> <span class="material-icons text-3xl" :class="isCasting ? 'text-success' : ''" @click="castClick">cast</span>
</div> </div>
<div class="top-4 right-4 absolute cursor-pointer"> <div class="top-4 right-4 absolute cursor-pointer">
@@ -12,6 +12,7 @@
<span class="material-icons text-3xl">more_vert</span> <span class="material-icons text-3xl">more_vert</span>
</ui-dropdown-menu> </ui-dropdown-menu>
</div> </div>
<p class="top-2 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75" style="font-size: 10px" :class="{ 'text-success': isLocalPlayMethod, 'text-accent': !isLocalPlayMethod }">{{ isDirectPlayMethod ? 'Direct' : isLocalPlayMethod ? 'Local' : 'Transcode' }}</p>
</div> </div>
<div v-if="useChapterTrack && showFullscreen" class="absolute total-track w-full px-3 z-30"> <div v-if="useChapterTrack && showFullscreen" class="absolute total-track w-full px-3 z-30">
@@ -63,7 +64,7 @@
<div class="flex items-center justify-center"> <div class="flex items-center justify-center">
<span v-show="showFullscreen" class="material-icons next-icon text-white text-opacity-75 cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span> <span v-show="showFullscreen" class="material-icons next-icon text-white text-opacity-75 cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
<span class="material-icons jump-icon text-white cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="backward10">replay_10</span> <span class="material-icons jump-icon text-white cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="backward10">replay_10</span>
<div class="play-btn cursor-pointer shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick"> <div class="play-btn cursor-pointer shadow-sm flex items-center justify-center rounded-full text-primary mx-4" :class="{ 'animate-spin': seekLoading, 'bg-accent': !isLocalPlayMethod, 'bg-success': isLocalPlayMethod }" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
<span v-if="!isLoading" class="material-icons">{{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}</span> <span v-if="!isLoading" class="material-icons">{{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}</span>
<widgets-spinner-icon v-else class="h-8 w-8" /> <widgets-spinner-icon v-else class="h-8 w-8" />
</div> </div>
@@ -159,7 +160,7 @@ export default {
return this.showFullscreen ? 200 : 60 return this.showFullscreen ? 200 : 60
}, },
showCastBtn() { showCastBtn() {
return this.$store.state.isCastAvailable && !this.isLocalPlayMethod return this.$store.state.isCastAvailable
}, },
isCasting() { isCasting() {
return this.mediaPlayer === 'cast-player' return this.mediaPlayer === 'cast-player'
@@ -193,6 +194,9 @@ export default {
isLocalPlayMethod() { isLocalPlayMethod() {
return this.playMethod == this.$constants.PlayMethod.LOCAL return this.playMethod == this.$constants.PlayMethod.LOCAL
}, },
isDirectPlayMethod() {
return this.playMethod == this.$constants.PlayMethod.DIRECTPLAY
},
title() { title() {
if (this.playbackSession) return this.playbackSession.displayTitle if (this.playbackSession) return this.playbackSession.displayTitle
return this.mediaMetadata ? this.mediaMetadata.title : 'Title' return this.mediaMetadata ? this.mediaMetadata.title : 'Title'
@@ -269,12 +273,10 @@ export default {
this.showChapterModal = false this.showChapterModal = false
}, },
castClick() { castClick() {
console.log('Cast Btn Click')
if (this.isLocalPlayMethod) { if (this.isLocalPlayMethod) {
this.$toast.warn('Cannot cast downloaded media items') this.$eventBus.$emit('cast-local-item')
return return
} }
AbsAudioPlayer.requestSession() AbsAudioPlayer.requestSession()
}, },
clickContainer() { clickContainer() {
+30
View File
@@ -168,10 +168,36 @@ export default {
this.$refs.audioPlayer.closePlayback() this.$refs.audioPlayer.closePlayback()
} }
}, },
castLocalItem() {
if (!this.serverLibraryItemId) {
this.$toast.error(`Cannot cast locally downloaded media`)
} else {
// Change to server library item
this.playServerLibraryItemAndCast(this.serverLibraryItemId)
}
},
playServerLibraryItemAndCast(libraryItemId) {
var playbackRate = 1
if (this.$refs.audioPlayer) {
playbackRate = this.$refs.audioPlayer.currentPlaybackRate || 1
}
AbsAudioPlayer.prepareLibraryItem({ libraryItemId, episodeId: null, playWhenReady: false, playbackRate })
.then((data) => {
console.log('Library item play response', JSON.stringify(data))
AbsAudioPlayer.requestSession()
})
.catch((error) => {
console.error('Failed', error)
})
},
async playLibraryItem(payload) { async playLibraryItem(payload) {
var libraryItemId = payload.libraryItemId var libraryItemId = payload.libraryItemId
var episodeId = payload.episodeId var episodeId = payload.episodeId
// When playing local library item and can also play this item from the server
// then store the server library item id so it can be used if a cast is made
var serverLibraryItemId = payload.serverLibraryItemId || null
if (libraryItemId.startsWith('local') && this.$store.state.isCasting) { if (libraryItemId.startsWith('local') && this.$store.state.isCasting) {
const { value } = await Dialog.confirm({ const { value } = await Dialog.confirm({
title: 'Warning', title: 'Warning',
@@ -195,6 +221,8 @@ export default {
console.log('Library item play response', JSON.stringify(data)) console.log('Library item play response', JSON.stringify(data))
if (!libraryItemId.startsWith('local')) { if (!libraryItemId.startsWith('local')) {
this.serverLibraryItemId = libraryItemId this.serverLibraryItemId = libraryItemId
} else {
this.serverLibraryItemId = serverLibraryItemId
} }
}) })
.catch((error) => { .catch((error) => {
@@ -228,6 +256,7 @@ export default {
this.$eventBus.$on('play-item', this.playLibraryItem) this.$eventBus.$on('play-item', this.playLibraryItem)
this.$eventBus.$on('pause-item', this.pauseItem) this.$eventBus.$on('pause-item', this.pauseItem)
this.$eventBus.$on('close-stream', this.closeStreamOnly) this.$eventBus.$on('close-stream', this.closeStreamOnly)
this.$eventBus.$on('cast-local-item', this.castLocalItem)
this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated }) this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated })
}, },
beforeDestroy() { beforeDestroy() {
@@ -246,6 +275,7 @@ export default {
this.$eventBus.$off('play-item', this.playLibraryItem) this.$eventBus.$off('play-item', this.playLibraryItem)
this.$eventBus.$off('pause-item', this.pauseItem) this.$eventBus.$off('pause-item', this.pauseItem)
this.$eventBus.$off('close-stream', this.closeStreamOnly) this.$eventBus.$off('close-stream', this.closeStreamOnly)
this.$eventBus.$off('cast-local-item', this.castLocalItem)
this.$store.commit('user/removeSettingsListener', 'streamContainer') this.$store.commit('user/removeSettingsListener', 'streamContainer')
} }
} }
+1
View File
@@ -3,6 +3,7 @@
<template v-for="shelf in totalShelves"> <template v-for="shelf in totalShelves">
<div :key="shelf" class="w-full px-2 relative" :class="showBookshelfListView ? '' : 'bookshelfRow'" :id="`shelf-${shelf - 1}`" :style="{ height: shelfHeight + 'px' }"> <div :key="shelf" class="w-full px-2 relative" :class="showBookshelfListView ? '' : 'bookshelfRow'" :id="`shelf-${shelf - 1}`" :style="{ height: shelfHeight + 'px' }">
<div v-if="!showBookshelfListView" class="bookshelfDivider w-full absolute bottom-0 left-0 z-30" style="min-height: 16px" :class="`h-${shelfDividerHeightIndex}`" /> <div v-if="!showBookshelfListView" class="bookshelfDivider w-full absolute bottom-0 left-0 z-30" style="min-height: 16px" :class="`h-${shelfDividerHeightIndex}`" />
<div v-else class="flex border-t border-white border-opacity-10 my-3 py-1"/>
</div> </div>
</template> </template>
-4
View File
@@ -446,10 +446,6 @@ export default {
this.selected = !this.selected this.selected = !this.selected
this.$emit('select', this.libraryItem) this.$emit('select', this.libraryItem)
}, },
play() {
var eventBus = this.$eventBus || this.$nuxt.$eventBus
eventBus.$emit('play-item', { libraryItemId: this.libraryItemId })
},
destroy() { destroy() {
// destroy the vue listeners, etc // destroy the vue listeners, etc
this.$destroy() this.$destroy()
+16
View File
@@ -20,6 +20,8 @@
</p> </p>
<p class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">by {{ displayAuthor }}</p> <p class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">by {{ displayAuthor }}</p>
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p> <p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
<p v-if="duration" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ $elapsedPretty(duration) }}</p>
<p v-if="episodes" class="truncate text-gray-400" :style="{ fontSize: 0.7 * sizeMultiplier + 'rem' }">{{ episodes }}</p>
</div> </div>
<div v-if="localLibraryItem || isLocal" class="absolute top-0 right-0 z-20" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }"> <div v-if="localLibraryItem || isLocal" class="absolute top-0 right-0 z-20" :style="{ top: 0.375 * sizeMultiplier + 'rem', right: 0.375 * sizeMultiplier + 'rem', padding: `${0.1 * sizeMultiplier}rem ${0.25 * sizeMultiplier}rem` }">
@@ -99,9 +101,23 @@ export default {
mediaType() { mediaType() {
return this._libraryItem.mediaType return this._libraryItem.mediaType
}, },
duration() {
return this.media.duration || null
},
isPodcast() { isPodcast() {
return this.mediaType === 'podcast' 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() { placeholderUrl() {
return '/book_placeholder.jpg' return '/book_placeholder.jpg'
}, },
+9 -6
View File
@@ -27,9 +27,11 @@
<ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" /> <ui-read-icon-btn :disabled="isProcessingReadUpdate" :is-read="userIsFinished" borderless class="mx-1 mt-0.5" @click="toggleFinished" />
<span v-if="isLocal" class="material-icons-outlined px-2 text-success text-lg">audio_file</span> <div v-if="!isIos">
<span v-else-if="!localEpisode" class="material-icons px-2" :class="downloadItem ? 'animate-bounce text-warning text-opacity-75 text-xl' : 'text-gray-300 text-xl'" @click="downloadClick">{{ downloadItem ? 'downloading' : 'download' }}</span> <span v-if="isLocal" class="material-icons-outlined px-2 text-success text-lg">audio_file</span>
<span v-else class="material-icons px-2 text-success text-xl">download_done</span> <span v-else-if="!localEpisode" class="material-icons mx-1 mt-2" :class="downloadItem ? 'animate-bounce text-warning text-opacity-75 text-xl' : 'text-gray-300 text-xl'" @click="downloadClick">{{ downloadItem ? 'downloading' : 'download' }}</span>
<span v-else class="material-icons px-2 text-success text-xl">download_done</span>
</div>
</div> </div>
</div> </div>
@@ -61,6 +63,9 @@ export default {
} }
}, },
computed: { computed: {
isIos() {
return this.$platform === 'ios'
},
mediaType() { mediaType() {
return 'podcast' return 'podcast'
}, },
@@ -204,9 +209,7 @@ export default {
var isFinished = !this.userIsFinished var isFinished = !this.userIsFinished
var localLibraryItemId = this.isLocal ? this.libraryItemId : this.localLibraryItemId var localLibraryItemId = this.isLocal ? this.libraryItemId : this.localLibraryItemId
var localEpisodeId = this.isLocal ? this.episode.id : this.localEpisode.id var localEpisodeId = this.isLocal ? this.episode.id : this.localEpisode.id
var localMediaProgressId = `${localLibraryItemId}-${localEpisodeId}` var payload = await this.$db.updateLocalMediaProgressFinished({ localLibraryItemId, localEpisodeId, isFinished })
console.log('toggleFinished local media progress id', localMediaProgressId, isFinished)
var payload = await this.$db.updateLocalMediaProgressFinished({ localMediaProgressId, isFinished })
console.log('toggleFinished payload', JSON.stringify(payload)) console.log('toggleFinished payload', JSON.stringify(payload))
if (!payload || payload.error) { if (!payload || payload.error) {
var errorMsg = payload ? payload.error : 'Unknown error' var errorMsg = payload ? payload.error : 'Unknown error'
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<button class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" :class="borderless ? '' : 'bg-primary border border-gray-600'" @click="clickBtn"> <button class="icon-btn rounded-md flex items-center justify-center px-2 relative" :class="borderless ? '' : 'bg-primary border border-gray-600'" @click="clickBtn">
<div class="w-5 h-5 text-white relative"> <div class="w-5 h-5 text-white relative">
<svg v-if="isRead" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)"> <svg v-if="isRead" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" /> <path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
+6 -6
View File
@@ -459,14 +459,14 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons; ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 6; CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_TEAM = 7UFJ7D8V6A; DEVELOPMENT_TEAM = 7UFJ7D8V6A;
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 0.9.43; MARKETING_VERSION = 0.9.44;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.development; PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OBJC_BRIDGING_HEADER = "App/App-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "App/App-Bridging-Header.h";
@@ -483,12 +483,12 @@
ASSETCATALOG_COMPILER_APPICON_NAME = Icons; ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 6; CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_TEAM = 7UFJ7D8V6A; DEVELOPMENT_TEAM = "";
INFOPLIST_FILE = App/Info.plist; INFOPLIST_FILE = App/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 0.9.43; MARKETING_VERSION = 0.9.44;
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app; PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
+7 -1
View File
@@ -14,13 +14,19 @@ CAP_PLUGIN(AbsAudioPlayer, "AbsAudioPlayer",
CAP_PLUGIN_METHOD(setPlaybackSpeed, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(setPlaybackSpeed, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(playPause, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(playPlayer, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(playPlayer, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(pausePlayer, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(pausePlayer, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(playPause, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(seek, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(seek, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(seekForward, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(seekForward, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(seekBackward, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(seekBackward, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getCurrentTime, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(getCurrentTime, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(cancelSleepTimer, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(decreaseSleepTime, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(increaseSleepTime, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(getSleepTimerTime, CAPPluginReturnPromise);
CAP_PLUGIN_METHOD(setSleepTimer, CAPPluginReturnPromise);
) )
+104 -38
View File
@@ -10,12 +10,19 @@ import Capacitor
@objc(AbsAudioPlayer) @objc(AbsAudioPlayer)
public class AbsAudioPlayer: CAPPlugin { public class AbsAudioPlayer: CAPPlugin {
private var initialPlayWhenReady = false
private var initialPlaybackRate:Float = 1
override public func load() { override public func load() {
NotificationCenter.default.addObserver(self, selector: #selector(sendMetadata), name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(sendMetadata), name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(sendPlaybackClosedEvent), name: NSNotification.Name(PlayerEvents.closed.rawValue), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(sendPlaybackClosedEvent), name: NSNotification.Name(PlayerEvents.closed.rawValue), object: nil)
self.bridge?.webView?.allowsBackForwardNavigationGestures = true;
NotificationCenter.default.addObserver(self, selector: #selector(sendMetadata), name: UIApplication.didBecomeActiveNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(sendMetadata), name: UIApplication.didBecomeActiveNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(sendMetadata), name: UIApplication.willEnterForegroundNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(sendMetadata), name: UIApplication.willEnterForegroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(sendSleepTimerSet), name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(sendSleepTimerEnded), name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(onPlaybackFailed), name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
self.bridge?.webView?.allowsBackForwardNavigationGestures = true;
} }
@objc func prepareLibraryItem(_ call: CAPPluginCall) { @objc func prepareLibraryItem(_ call: CAPPluginCall) {
@@ -33,8 +40,11 @@ public class AbsAudioPlayer: CAPPlugin {
return call.resolve() return call.resolve()
} }
initialPlayWhenReady = playWhenReady
initialPlaybackRate = playbackRate
sendPrepareMetadataEvent(itemId: libraryItemId!, playWhenReady: playWhenReady) sendPrepareMetadataEvent(itemId: libraryItemId!, playWhenReady: playWhenReady)
ApiClient.startPlaybackSession(libraryItemId: libraryItemId!, episodeId: episodeId) { session in ApiClient.startPlaybackSession(libraryItemId: libraryItemId!, episodeId: episodeId, forceTranscode: false) { session in
PlayerHandler.startPlayback(session: session, playWhenReady: playWhenReady, playbackRate: playbackRate) PlayerHandler.startPlayback(session: session, playWhenReady: playWhenReady, playbackRate: playbackRate)
do { do {
@@ -43,7 +53,6 @@ public class AbsAudioPlayer: CAPPlugin {
} catch(let exception) { } catch(let exception) {
NSLog("failed to convert session to json") NSLog("failed to convert session to json")
debugPrint(exception) debugPrint(exception)
call.resolve([:]) call.resolve([:])
} }
@@ -68,18 +77,19 @@ public class AbsAudioPlayer: CAPPlugin {
call.resolve() call.resolve()
} }
@objc func playPause(_ call: CAPPluginCall) {
PlayerHandler.playPause()
call.resolve([ "playing": !PlayerHandler.paused() ])
}
@objc func playPlayer(_ call: CAPPluginCall) { @objc func playPlayer(_ call: CAPPluginCall) {
PlayerHandler.play() PlayerHandler.paused = false
call.resolve() call.resolve()
} }
@objc func pausePlayer(_ call: CAPPluginCall) { @objc func pausePlayer(_ call: CAPPluginCall) {
PlayerHandler.pause() PlayerHandler.paused = true
call.resolve() call.resolve()
} }
// I have no clue why but after i moved this block of code from above "playPlayer" to here the app stopped crashing. Move it back up if you want to
@objc func playPause(_ call: CAPPluginCall) {
PlayerHandler.paused = !PlayerHandler.paused
call.resolve([ "playing": !PlayerHandler.paused ])
}
@objc func seek(_ call: CAPPluginCall) { @objc func seek(_ call: CAPPluginCall) {
PlayerHandler.seek(amount: call.getDouble("value", 0.0)) PlayerHandler.seek(amount: call.getDouble("value", 0.0))
@@ -95,13 +105,97 @@ public class AbsAudioPlayer: CAPPlugin {
} }
@objc func sendMetadata() { @objc func sendMetadata() {
self.notifyListeners("onPlayingUpdate", data: [ "value": !PlayerHandler.paused() ]) self.notifyListeners("onPlayingUpdate", data: [ "value": !PlayerHandler.paused ])
self.notifyListeners("onMetadata", data: PlayerHandler.getMetdata()) self.notifyListeners("onMetadata", data: PlayerHandler.getMetdata())
} }
@objc func sendPlaybackClosedEvent() { @objc func sendPlaybackClosedEvent() {
self.notifyListeners("onPlaybackClosed", data: [ "value": true ]) self.notifyListeners("onPlaybackClosed", data: [ "value": true ])
} }
@objc func decreaseSleepTime(_ call: CAPPluginCall) {
guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) }
guard let time = Int(timeString) else { return call.resolve([ "success": false ]) }
guard let currentSleepTime = PlayerHandler.remainingSleepTime else { return call.resolve([ "success": false ]) }
PlayerHandler.remainingSleepTime = currentSleepTime - (time / 1000)
call.resolve()
}
@objc func increaseSleepTime(_ call: CAPPluginCall) {
guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) }
guard let time = Int(timeString) else { return call.resolve([ "success": false ]) }
guard let currentSleepTime = PlayerHandler.remainingSleepTime else { return call.resolve([ "success": false ]) }
PlayerHandler.remainingSleepTime = currentSleepTime + (time / 1000)
call.resolve()
}
@objc func setSleepTimer(_ call: CAPPluginCall) {
guard let timeString = call.getString("time") else { return call.resolve([ "success": false ]) }
guard let time = Int(timeString) else { return call.resolve([ "success": false ]) }
NSLog("chapter time: \(call.getBool("isChapterTime", false))")
if call.getBool("isChapterTime", false) {
let timeToPause = time / 1000 - Int(PlayerHandler.getCurrentTime() ?? 0)
if timeToPause < 0 { return call.resolve([ "success": false ]) }
NSLog("oof \(timeToPause)")
PlayerHandler.remainingSleepTime = timeToPause
return call.resolve([ "success": true ])
}
PlayerHandler.remainingSleepTime = time / 1000
call.resolve([ "success": true ])
}
@objc func cancelSleepTimer(_ call: CAPPluginCall) {
PlayerHandler.remainingSleepTime = nil
call.resolve()
}
@objc func getSleepTimerTime(_ call: CAPPluginCall) {
call.resolve([
"value": PlayerHandler.remainingSleepTime
])
}
@objc func sendSleepTimerEnded() {
self.notifyListeners("onSleepTimerEnded", data: [
"value": PlayerHandler.getCurrentTime()
])
}
@objc func sendSleepTimerSet() {
self.notifyListeners("onSleepTimerSet", data: [
"value": PlayerHandler.remainingSleepTime
])
}
@objc func onPlaybackFailed() {
if (PlayerHandler.getPlayMethod() == PlayMethod.directplay.rawValue) {
let playbackSession = PlayerHandler.getPlaybackSession()
let libraryItemId = playbackSession?.libraryItemId ?? ""
let episodeId = playbackSession?.episodeId ?? nil
NSLog("TEST: Forcing Transcode")
// If direct playing then fallback to transcode
ApiClient.startPlaybackSession(libraryItemId: libraryItemId, episodeId: episodeId, forceTranscode: true) { session in
PlayerHandler.startPlayback(session: session, playWhenReady: self.initialPlayWhenReady, playbackRate: self.initialPlaybackRate)
do {
self.sendPlaybackSession(session: try session.asDictionary())
} catch(let exception) {
NSLog("failed to convert session to json")
debugPrint(exception)
}
self.sendMetadata()
}
} else {
self.notifyListeners("onPlaybackFailed", data: [
"value": "Playback Error"
])
}
}
@objc func sendPrepareMetadataEvent(itemId: String, playWhenReady: Bool) { @objc func sendPrepareMetadataEvent(itemId: String, playWhenReady: Bool) {
self.notifyListeners("onPrepareMedia", data: [ self.notifyListeners("onPrepareMedia", data: [
"audiobookId": itemId, "audiobookId": itemId,
@@ -111,32 +205,4 @@ public class AbsAudioPlayer: CAPPlugin {
@objc func sendPlaybackSession(session: [String: Any]) { @objc func sendPlaybackSession(session: [String: Any]) {
self.notifyListeners("onPlaybackSession", data: session) self.notifyListeners("onPlaybackSession", data: session)
} }
/*
IMPLEMENTED:
cancelSleepTimer
decreaseSleepTime
increaseSleepTime
getSleepTimerTime
setSleepTimer
* closePlayback
* setPlaybackSpeed
* seekBackward
* seekForward
* seek
* playPause
* playPlayer
* pausePlayer
* getCurrentTime
* onPlaybackSession
* onPrepareMedia
* onPlaybackClosed
* onPlayingUpdate
* onMetadata
onSleepTimerEnded
onSleepTimerSet
*/
} }
+4 -5
View File
@@ -53,7 +53,7 @@ public class AbsDatabase: CAPPlugin {
} }
@objc func removeServerConnectionConfig(_ call: CAPPluginCall) { @objc func removeServerConnectionConfig(_ call: CAPPluginCall) {
let id = call.getString("serverConnectionConfigId", "") let id = call.getString("serverConnectionConfigId", "")
Database.deleteServerConnectionConfig(id: id) Database.shared.deleteServerConnectionConfig(id: id)
call.resolve() call.resolve()
} }
@@ -63,13 +63,12 @@ public class AbsDatabase: CAPPlugin {
} }
@objc func getDeviceData(_ call: CAPPluginCall) { @objc func getDeviceData(_ call: CAPPluginCall) {
let configs = Database.getServerConnectionConfigs() let configs = Database.shared.getServerConnectionConfigs()
let index = Database.getLastActiveConfigIndex() let index = Database.shared.getLastActiveConfigIndex()
call.resolve([ call.resolve([
"serverConnectionConfigs": configs.map { config in convertServerConnectionConfigToJSON(config: config) }, "serverConnectionConfigs": configs.map { config in convertServerConnectionConfigToJSON(config: config) },
"lastServerConnectionConfigId": configs.first { config in config.index == index }?.id, "lastServerConnectionConfigId": configs.first { config in config.index == index }?.id as Any,
// Luckily this isn't implemented yet
// "currentLocalPlaybackSession": nil, // "currentLocalPlaybackSession": nil,
]) ])
} }
+167 -64
View File
@@ -10,6 +10,13 @@ import AVFoundation
import UIKit import UIKit
import MediaPlayer import MediaPlayer
enum PlayMethod:Int {
case directplay = 0
case directstream = 1
case transcode = 2
case local = 3
}
class AudioPlayer: NSObject { class AudioPlayer: NSObject {
// enums and @objc are not compatible // enums and @objc are not compatible
@objc dynamic var status: Int @objc dynamic var status: Int
@@ -24,29 +31,25 @@ class AudioPlayer: NSObject {
private var playWhenReady: Bool private var playWhenReady: Bool
private var initialPlaybackRate: Float private var initialPlaybackRate: Float
private var audioPlayer: AVPlayer private var audioPlayer: AVQueuePlayer
private var playbackSession: PlaybackSession private var playbackSession: PlaybackSession
private var activeAudioTrack: AudioTrack
private var queueObserver:NSKeyValueObservation?
private var queueItemStatusObserver:NSKeyValueObservation?
private var currentTrackIndex = 0
private var allPlayerItems:[AVPlayerItem] = []
// MARK: - Constructor // MARK: - Constructor
init(playbackSession: PlaybackSession, playWhenReady: Bool = false, playbackRate: Float = 1) { init(playbackSession: PlaybackSession, playWhenReady: Bool = false, playbackRate: Float = 1) {
self.playWhenReady = playWhenReady self.playWhenReady = playWhenReady
self.initialPlaybackRate = playbackRate self.initialPlaybackRate = playbackRate
self.audioPlayer = AVPlayer() self.audioPlayer = AVQueuePlayer()
self.playbackSession = playbackSession self.playbackSession = playbackSession
self.status = -1 self.status = -1
self.rate = 0.0 self.rate = 0.0
self.tmpRate = playbackRate self.tmpRate = playbackRate
if playbackSession.audioTracks.count != 1 || playbackSession.audioTracks[0].mimeType != "application/vnd.apple.mpegurl" {
NSLog("The player only support HLS streams right now")
self.activeAudioTrack = AudioTrack(index: 0, startOffset: -1, duration: -1, title: "", contentUrl: nil, mimeType: "", metadata: nil, serverIndex: 0)
super.init()
return
}
self.activeAudioTrack = playbackSession.audioTracks[0]
super.init() super.init()
initAudioSession() initAudioSession()
@@ -56,19 +59,35 @@ class AudioPlayer: NSObject {
self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.rate), options: .new, context: &playerContext) self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.rate), options: .new, context: &playerContext)
self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem), options: .new, context: &playerContext) self.audioPlayer.addObserver(self, forKeyPath: #keyPath(AVPlayer.currentItem), options: .new, context: &playerContext)
let playerItem = AVPlayerItem(asset: createAsset()) for track in playbackSession.audioTracks {
playerItem.addObserver(self, forKeyPath: #keyPath(AVPlayerItem.status), options: .new, context: &playerItemContext) let playerItem = AVPlayerItem(asset: createAsset(itemId: playbackSession.libraryItemId!, track: track))
self.allPlayerItems.append(playerItem)
}
self.audioPlayer.replaceCurrentItem(with: playerItem) self.currentTrackIndex = getItemIndexForTime(time: playbackSession.currentTime)
NSLog("TEST: Starting track index \(self.currentTrackIndex) for start time \(playbackSession.currentTime)")
let playerItems = self.allPlayerItems[self.currentTrackIndex..<self.allPlayerItems.count]
NSLog("TEST: Setting player items \(playerItems.count)")
for item in Array(playerItems) {
self.audioPlayer.insert(item, after:self.audioPlayer.items().last)
}
setupQueueObserver()
setupQueueItemStatusObserver()
NSLog("Audioplayer ready") NSLog("Audioplayer ready")
} }
deinit { deinit {
self.queueObserver?.invalidate()
self.queueItemStatusObserver?.invalidate()
destroy() destroy()
} }
public func destroy() { public func destroy() {
// Pause is not synchronous causing this error on below lines: // Pause is not synchronous causing this error on below lines:
// AVAudioSession_iOS.mm:1206 Deactivating an audio session that has running I/O. All I/O should be stopped or paused prior to deactivating the audio session // AVAudioSession_iOS.mm:1206 Deactivating an audio session that has running I/O. All I/O should be stopped or paused prior to deactivating the audio session
// It is related to L79 `AVAudioSession.sharedInstance().setActive(false)`
pause() pause()
audioPlayer.replaceCurrentItem(with: nil) audioPlayer.replaceCurrentItem(with: nil)
@@ -79,14 +98,60 @@ class AudioPlayer: NSObject {
print(error) print(error)
} }
// Throws error Possibly related to the error above DispatchQueue.runOnMainQueue {
// DispatchQueue.main.sync { UIApplication.shared.endReceivingRemoteControlEvents()
// UIApplication.shared.endReceivingRemoteControlEvents() }
// }
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.closed.rawValue), object: nil) NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.closed.rawValue), object: nil)
} }
func getItemIndexForTime(time:Double) -> Int {
for index in 0..<self.allPlayerItems.count {
let startOffset = playbackSession.audioTracks[index].startOffset ?? 0.0
let duration = playbackSession.audioTracks[index].duration
let trackEnd = startOffset + duration
if (time < trackEnd.rounded(.down)) {
return index
}
}
return 0
}
func setupQueueObserver() {
self.queueObserver = self.audioPlayer.observe(\.currentItem, options: [.new]) {_,_ in
let prevTrackIndex = self.currentTrackIndex
self.audioPlayer.currentItem.map { item in
self.currentTrackIndex = self.allPlayerItems.firstIndex(of:item) ?? 0
if (self.currentTrackIndex != prevTrackIndex) {
NSLog("TEST: New Current track index \(self.currentTrackIndex)")
}
}
}
}
func setupQueueItemStatusObserver() {
self.queueItemStatusObserver?.invalidate()
self.queueItemStatusObserver = self.audioPlayer.currentItem?.observe(\.status, options: [.new, .old], changeHandler: { (playerItem, change) in
if (playerItem.status == .readyToPlay) {
NSLog("TEST: queueStatusObserver: Current Item Ready to play. PlayWhenReady: \(self.playWhenReady)")
self.updateNowPlaying()
let firstReady = self.status < 0
self.status = 0
if self.playWhenReady {
self.seek(self.playbackSession.currentTime, from: "queueItemStatusObserver")
self.playWhenReady = false
self.play()
} else if (firstReady) { // Only seek on first readyToPlay
self.seek(self.playbackSession.currentTime, from: "queueItemStatusObserver")
}
} else if (playerItem.status == .failed) {
NSLog("TEST: queueStatusObserver: FAILED \(playerItem.error?.localizedDescription ?? "")")
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.failed.rawValue), object: nil)
}
})
}
// MARK: - Methods // MARK: - Methods
public func play(allowSeekBack: Bool = false) { public func play(allowSeekBack: Bool = false) {
if allowSeekBack { if allowSeekBack {
@@ -110,11 +175,11 @@ class AudioPlayer: NSObject {
} }
if time != nil { if time != nil {
seek(getCurrentTime() - Double(time!)) seek(getCurrentTime() - Double(time!), from: "play")
} }
} }
lastPlayTime = Date.timeIntervalSinceReferenceDate lastPlayTime = Date.timeIntervalSinceReferenceDate
self.audioPlayer.play() self.audioPlayer.play()
self.status = 1 self.status = 1
self.rate = self.tmpRate self.rate = self.tmpRate
@@ -122,6 +187,7 @@ class AudioPlayer: NSObject {
updateNowPlaying() updateNowPlaying()
} }
public func pause() { public func pause() {
self.audioPlayer.pause() self.audioPlayer.pause()
self.status = 0 self.status = 0
@@ -130,24 +196,60 @@ class AudioPlayer: NSObject {
updateNowPlaying() updateNowPlaying()
lastPlayTime = Date.timeIntervalSinceReferenceDate lastPlayTime = Date.timeIntervalSinceReferenceDate
} }
public func seek(_ to: Double) {
let continuePlaing = rate > 0.0 public func seek(_ to: Double, from: String) {
let continuePlaying = rate > 0.0
pause() pause()
self.audioPlayer.seek(to: CMTime(seconds: to, preferredTimescale: 1000)) { completed in
if !completed { NSLog("TEST: Seek to \(to) from \(from)")
NSLog("WARNING: seeking not completed (to \(to)")
let currentTrack = self.playbackSession.audioTracks[self.currentTrackIndex]
let ctso = currentTrack.startOffset ?? 0.0
let trackEnd = ctso + currentTrack.duration
NSLog("TEST: Seek current track END = \(trackEnd)")
let indexOfSeek = getItemIndexForTime(time: to)
NSLog("TEST: Seek to index \(indexOfSeek) | Current index \(self.currentTrackIndex)")
// Reconstruct queue if seeking to a different track
if (self.currentTrackIndex != indexOfSeek) {
self.currentTrackIndex = indexOfSeek
self.playbackSession.currentTime = to
self.playWhenReady = continuePlaying // Only playWhenReady if already playing
self.status = -1
let playerItems = self.allPlayerItems[indexOfSeek..<self.allPlayerItems.count]
self.audioPlayer.removeAllItems()
for item in Array(playerItems) {
self.audioPlayer.insert(item, after:self.audioPlayer.items().last)
} }
if continuePlaing { setupQueueItemStatusObserver()
self.play() } else {
NSLog("TEST: Seeking in current item \(to)")
let currentTrackStartOffset = self.playbackSession.audioTracks[self.currentTrackIndex].startOffset ?? 0.0
let seekTime = to - currentTrackStartOffset
self.audioPlayer.seek(to: CMTime(seconds: seekTime, preferredTimescale: 1000)) { completed in
if !completed {
NSLog("WARNING: seeking not completed (to \(seekTime)")
}
if continuePlaying {
self.play()
}
self.updateNowPlaying()
} }
self.updateNowPlaying()
} }
} }
public func setPlaybackRate(_ rate: Float, observed: Bool = false) { public func setPlaybackRate(_ rate: Float, observed: Bool = false) {
if self.audioPlayer.rate != rate { if self.audioPlayer.rate != rate {
NSLog("TEST: setPlaybakRate rate changed from \(self.audioPlayer.rate) to \(rate)")
self.audioPlayer.rate = rate self.audioPlayer.rate = rate
} }
if rate > 0.0 && !(observed && rate == 1) { if rate > 0.0 && !(observed && rate == 1) {
@@ -155,25 +257,42 @@ class AudioPlayer: NSObject {
} }
self.rate = rate self.rate = rate
self.updateNowPlaying() self.updateNowPlaying()
} }
public func getCurrentTime() -> Double { public func getCurrentTime() -> Double {
self.audioPlayer.currentTime().seconds let currentTrackTime = self.audioPlayer.currentTime().seconds
let audioTrack = playbackSession.audioTracks[currentTrackIndex]
let startOffset = audioTrack.startOffset ?? 0.0
return startOffset + currentTrackTime
}
public func getPlayMethod() -> Int {
return self.playbackSession.playMethod
}
public func getPlaybackSession() -> PlaybackSession {
return self.playbackSession
} }
public func getDuration() -> Double { public func getDuration() -> Double {
self.audioPlayer.currentItem?.duration.seconds ?? 0 return playbackSession.duration
} }
// MARK: - Private // MARK: - Private
private func createAsset() -> AVAsset { private func createAsset(itemId:String, track:AudioTrack) -> AVAsset {
let headers: [String: String] = [ if (playbackSession.playMethod == PlayMethod.directplay.rawValue) {
"Authorization": "Bearer \(Store.serverConfig!.token)" // The only reason this is separate is because the filename needs to be encoded
] let filename = track.metadata?.filename ?? ""
let filenameEncoded = filename.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)
return AVURLAsset(url: URL(string: "\(Store.serverConfig!.address)\(activeAudioTrack.contentUrl ?? "")")!, options: ["AVURLAssetHTTPHeaderFieldsKey": headers]) let urlstr = "\(Store.serverConfig!.address)/s/item/\(itemId)/\(filenameEncoded ?? "")?token=\(Store.serverConfig!.token)"
let url = URL(string: urlstr)!
return AVURLAsset(url: url)
} else { // HLS Transcode
let headers: [String: String] = [
"Authorization": "Bearer \(Store.serverConfig!.token)"
]
return AVURLAsset(url: URL(string: "\(Store.serverConfig!.address)\(track.contentUrl ?? "")")!, options: ["AVURLAssetHTTPHeaderFieldsKey": headers])
}
} }
private func initAudioSession() { private func initAudioSession() {
do { do {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, options: [.allowAirPlay]) try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, options: [.allowAirPlay])
@@ -186,9 +305,9 @@ class AudioPlayer: NSObject {
// MARK: - Now playing // MARK: - Now playing
private func setupRemoteTransportControls() { private func setupRemoteTransportControls() {
// DispatchQueue.main.sync { DispatchQueue.runOnMainQueue {
UIApplication.shared.beginReceivingRemoteControlEvents() UIApplication.shared.beginReceivingRemoteControlEvents()
// } }
let commandCenter = MPRemoteCommandCenter.shared() let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.isEnabled = true commandCenter.playCommand.isEnabled = true
@@ -209,7 +328,7 @@ class AudioPlayer: NSObject {
return .noSuchContent return .noSuchContent
} }
seek(getCurrentTime() + command.preferredIntervals[0].doubleValue) seek(getCurrentTime() + command.preferredIntervals[0].doubleValue, from: "remote")
return .success return .success
} }
commandCenter.skipBackwardCommand.isEnabled = true commandCenter.skipBackwardCommand.isEnabled = true
@@ -219,7 +338,7 @@ class AudioPlayer: NSObject {
return .noSuchContent return .noSuchContent
} }
seek(getCurrentTime() - command.preferredIntervals[0].doubleValue) seek(getCurrentTime() - command.preferredIntervals[0].doubleValue, from: "remote")
return .success return .success
} }
@@ -229,7 +348,7 @@ class AudioPlayer: NSObject {
return .noSuchContent return .noSuchContent
} }
self.seek(event.positionTime) self.seek(event.positionTime, from: "remote")
return .success return .success
} }
@@ -246,33 +365,17 @@ class AudioPlayer: NSObject {
} }
private func updateNowPlaying() { private func updateNowPlaying() {
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil) NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
NowPlayingInfo.update(duration: getDuration(), currentTime: getCurrentTime(), rate: rate) NowPlayingInfo.shared.update(duration: getDuration(), currentTime: getCurrentTime(), rate: rate)
} }
// MARK: - Observer // MARK: - Observer
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if context == &playerItemContext { if context == &playerContext {
if keyPath == #keyPath(AVPlayer.status) {
guard let playerStatus = AVPlayerItem.Status(rawValue: (change?[.newKey] as? Int ?? -1)) else { return }
if playerStatus == .readyToPlay {
self.updateNowPlaying()
let firstReady = self.status < 0
self.status = 0
if self.playWhenReady {
seek(playbackSession.currentTime)
self.playWhenReady = false
self.play()
} else if (firstReady) { // Only seek on first readyToPlay
seek(playbackSession.currentTime)
}
}
}
} else if context == &playerContext {
if keyPath == #keyPath(AVPlayer.rate) { if keyPath == #keyPath(AVPlayer.rate) {
NSLog("TEST: playerContext observer player rate")
self.setPlaybackRate(change?[.newKey] as? Float ?? 1.0, observed: true) self.setPlaybackRate(change?[.newKey] as? Float ?? 1.0, observed: true)
} else if keyPath == #keyPath(AVPlayer.currentItem) { } else if keyPath == #keyPath(AVPlayer.currentItem) {
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.update.rawValue), object: nil)
NSLog("WARNING: Item ended") NSLog("WARNING: Item ended")
} }
} else { } else {
+73 -36
View File
@@ -12,7 +12,44 @@ class PlayerHandler {
private static var session: PlaybackSession? private static var session: PlaybackSession?
private static var timer: Timer? private static var timer: Timer?
private static var listeningTimePassedSinceLastSync = 0.0 private static var _remainingSleepTime: Int? = nil
public static var remainingSleepTime: Int? {
get {
return _remainingSleepTime
}
set(time) {
if time != nil && time! < 0 {
_remainingSleepTime = nil
} else {
_remainingSleepTime = time
}
if _remainingSleepTime == nil {
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepEnded.rawValue), object: _remainingSleepTime)
} else {
NotificationCenter.default.post(name: NSNotification.Name(PlayerEvents.sleepSet.rawValue), object: _remainingSleepTime)
}
}
}
private static var listeningTimePassedSinceLastSync: Double = 0.0
private static var lastSyncReport: PlaybackReport?
public static var paused: Bool {
get {
guard let player = player else {
return true
}
return player.rate == 0.0
}
set(paused) {
if paused {
self.player?.pause()
} else {
self.player?.play()
}
}
}
public static func startPlayback(session: PlaybackSession, playWhenReady: Bool, playbackRate: Float) { public static func startPlayback(session: PlaybackSession, playWhenReady: Bool, playbackRate: Float) {
if player != nil { if player != nil {
@@ -20,16 +57,16 @@ class PlayerHandler {
player = nil player = nil
} }
NowPlayingInfo.setSessionMetadata(metadata: NowPlayingMetadata(id: session.id, itemId: session.libraryItemId!, artworkUrl: session.coverPath, title: session.displayTitle ?? "Unknown title", author: session.displayAuthor, series: nil)) NowPlayingInfo.shared.setSessionMetadata(metadata: NowPlayingMetadata(id: session.id, itemId: session.libraryItemId!, artworkUrl: session.coverPath, title: session.displayTitle ?? "Unknown title", author: session.displayAuthor, series: nil))
self.session = session self.session = session
player = AudioPlayer(playbackSession: session, playWhenReady: playWhenReady, playbackRate: playbackRate) player = AudioPlayer(playbackSession: session, playWhenReady: playWhenReady, playbackRate: playbackRate)
// DispatchQueue.main.sync { DispatchQueue.runOnMainQueue {
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.tick() self.tick()
}
} }
// }
} }
public static func stopPlayback() { public static func stopPlayback() {
player?.destroy() player?.destroy()
@@ -38,7 +75,7 @@ class PlayerHandler {
timer?.invalidate() timer?.invalidate()
timer = nil timer = nil
NowPlayingInfo.reset() NowPlayingInfo.shared.reset()
} }
public static func getCurrentTime() -> Double? { public static func getCurrentTime() -> Double? {
@@ -47,45 +84,32 @@ class PlayerHandler {
public static func setPlaybackSpeed(speed: Float) { public static func setPlaybackSpeed(speed: Float) {
self.player?.setPlaybackRate(speed) self.player?.setPlaybackRate(speed)
} }
public static func getPlayMethod() -> Int? {
public static func play() { self.player?.getPlayMethod()
self.player?.play()
} }
public static func pause() { public static func getPlaybackSession() -> PlaybackSession? {
self.player?.play() self.player?.getPlaybackSession()
}
public static func playPause() {
if paused() {
self.player?.play()
} else {
self.player?.pause()
}
} }
public static func seekForward(amount: Double) { public static func seekForward(amount: Double) {
if player == nil { guard let player = player else {
return return
} }
let destinationTime = player!.getCurrentTime() + amount let destinationTime = player.getCurrentTime() + amount
player!.seek(destinationTime) player.seek(destinationTime, from: "handler")
} }
public static func seekBackward(amount: Double) { public static func seekBackward(amount: Double) {
if player == nil { guard let player = player else {
return return
} }
let destinationTime = player!.getCurrentTime() - amount let destinationTime = player.getCurrentTime() - amount
player!.seek(destinationTime) player.seek(destinationTime, from: "handler")
} }
public static func seek(amount: Double) { public static func seek(amount: Double) {
player?.seek(amount) player?.seek(amount, from: "handler")
} }
public static func paused() -> Bool {
player?.rate == 0.0
}
public static func getMetdata() -> [String: Any] { public static func getMetdata() -> [String: Any] {
DispatchQueue.main.async { DispatchQueue.main.async {
syncProgress() syncProgress()
@@ -94,29 +118,42 @@ class PlayerHandler {
return [ return [
"duration": player?.getDuration() ?? 0, "duration": player?.getDuration() ?? 0,
"currentTime": player?.getCurrentTime() ?? 0, "currentTime": player?.getCurrentTime() ?? 0,
"playerState": !paused(), "playerState": !paused,
"currentRate": player?.rate ?? 0, "currentRate": player?.rate ?? 0,
] ]
} }
private static func tick() { private static func tick() {
if !paused() { if !paused {
listeningTimePassedSinceLastSync += 1 listeningTimePassedSinceLastSync += 1
} }
if listeningTimePassedSinceLastSync > 3 { if listeningTimePassedSinceLastSync > 3 {
syncProgress() syncProgress()
} }
if remainingSleepTime != nil {
if remainingSleepTime! == 0 {
paused = true
}
remainingSleepTime! -= 1
}
} }
public static func syncProgress() { public static func syncProgress() {
if player == nil || session == nil { if session == nil { return }
guard let player = player else { return }
let playerCurrentTime = player.getCurrentTime()
if (lastSyncReport != nil && lastSyncReport?.currentTime == playerCurrentTime) {
// No need to syncProgress
return return
} }
let report = PlaybackReport(currentTime: player!.getCurrentTime(), duration: player!.getDuration(), timeListened: listeningTimePassedSinceLastSync) let report = PlaybackReport(currentTime: playerCurrentTime, duration: player.getDuration(), timeListened: listeningTimePassedSinceLastSync)
session!.currentTime = player!.getCurrentTime() session!.currentTime = playerCurrentTime
listeningTimePassedSinceLastSync = 0 listeningTimePassedSinceLastSync = 0
lastSyncReport = report
// TODO: check if online // TODO: check if online
NSLog("sending playback report") NSLog("sending playback report")
+11 -3
View File
@@ -9,6 +9,14 @@ import Foundation
import Alamofire import Alamofire
class ApiClient { class ApiClient {
public static func getData(from url: URL, completion: @escaping (UIImage?) -> Void) {
URLSession.shared.dataTask(with: url, completionHandler: {(data, response, error) in
if let data = data {
completion(UIImage(data:data))
}
}).resume()
}
public static func postResource<T: Decodable>(endpoint: String, parameters: [String: String], decodable: T.Type = T.self, callback: ((_ param: T) -> Void)?) { public static func postResource<T: Decodable>(endpoint: String, parameters: [String: String], decodable: T.Type = T.self, callback: ((_ param: T) -> Void)?) {
if (Store.serverConfig == nil) { if (Store.serverConfig == nil) {
NSLog("Server config not set") NSLog("Server config not set")
@@ -53,15 +61,15 @@ class ApiClient {
} }
} }
public static func startPlaybackSession(libraryItemId: String, episodeId: String?, callback: @escaping (_ param: PlaybackSession) -> Void) { public static func startPlaybackSession(libraryItemId: String, episodeId: String?, forceTranscode:Bool, callback: @escaping (_ param: PlaybackSession) -> Void) {
var endpoint = "api/items/\(libraryItemId)/play" var endpoint = "api/items/\(libraryItemId)/play"
if episodeId != nil { if episodeId != nil {
endpoint += "/\(episodeId!)" endpoint += "/\(episodeId!)"
} }
ApiClient.postResource(endpoint: endpoint, parameters: [ ApiClient.postResource(endpoint: endpoint, parameters: [
"forceTranscode": "true", // TODO: direct play "forceDirectPlay": !forceTranscode ? "1" : "",
"forceTranscode": forceTranscode ? "1" : "",
"mediaPlayer": "AVPlayer", "mediaPlayer": "AVPlayer",
], decodable: PlaybackSession.self) { obj in ], decodable: PlaybackSession.self) { obj in
var session = obj var session = obj
+22 -13
View File
@@ -10,16 +10,25 @@ import RealmSwift
class Database { class Database {
// All DB releated actions must be executed on "realm-queue" // All DB releated actions must be executed on "realm-queue"
public static let realmQueue = DispatchQueue(label: "realm-queue") public static let realmQueue: DispatchQueue = DispatchQueue(label: "realm-queue")
private static var instance: Realm = try! Realm(queue: realmQueue) public static var shared = {
realmQueue.sync {
return Database()
}
}()
private var instance: Realm
private init() {
self.instance = try! Realm(queue: Database.realmQueue)
}
public static func setServerConnectionConfig(config: ServerConnectionConfig) { public func setServerConnectionConfig(config: ServerConnectionConfig) {
var refrence: ThreadSafeReference<ServerConnectionConfig>? var refrence: ThreadSafeReference<ServerConnectionConfig>?
if config.realm != nil { if config.realm != nil {
refrence = ThreadSafeReference(to: config) refrence = ThreadSafeReference(to: config)
} }
realmQueue.sync { Database.realmQueue.sync {
let existing: ServerConnectionConfig? = instance.object(ofType: ServerConnectionConfig.self, forPrimaryKey: config.id) let existing: ServerConnectionConfig? = instance.object(ofType: ServerConnectionConfig.self, forPrimaryKey: config.id)
if config.index == 0 { if config.index == 0 {
@@ -55,8 +64,8 @@ class Database {
setLastActiveConfigIndex(index: config.index) setLastActiveConfigIndex(index: config.index)
} }
} }
public static func deleteServerConnectionConfig(id: String) { public func deleteServerConnectionConfig(id: String) {
realmQueue.sync { Database.realmQueue.sync {
let config = instance.object(ofType: ServerConnectionConfig.self, forPrimaryKey: id) let config = instance.object(ofType: ServerConnectionConfig.self, forPrimaryKey: id)
do { do {
@@ -71,10 +80,10 @@ class Database {
} }
} }
} }
public static func getServerConnectionConfigs() -> [ServerConnectionConfig] { public func getServerConnectionConfigs() -> [ServerConnectionConfig] {
var refrences: [ThreadSafeReference<ServerConnectionConfig>] = [] var refrences: [ThreadSafeReference<ServerConnectionConfig>] = []
realmQueue.sync { Database.realmQueue.sync {
let configs = instance.objects(ServerConnectionConfig.self) let configs = instance.objects(ServerConnectionConfig.self)
refrences = configs.map { config in refrences = configs.map { config in
return ThreadSafeReference(to: config) return ThreadSafeReference(to: config)
@@ -94,12 +103,12 @@ class Database {
} }
} }
public static func setLastActiveConfigIndexToNil() { public func setLastActiveConfigIndexToNil() {
realmQueue.sync { Database.realmQueue.sync {
setLastActiveConfigIndex(index: nil) setLastActiveConfigIndex(index: nil)
} }
} }
public static func setLastActiveConfigIndex(index: Int?) { public func setLastActiveConfigIndex(index: Int?) {
let existing = instance.objects(ServerConnectionConfigActiveIndex.self) let existing = instance.objects(ServerConnectionConfigActiveIndex.self)
let obj = ServerConnectionConfigActiveIndex() let obj = ServerConnectionConfigActiveIndex()
obj.index = index obj.index = index
@@ -114,8 +123,8 @@ class Database {
debugPrint(exception) debugPrint(exception)
} }
} }
public static func getLastActiveConfigIndex() -> Int? { public func getLastActiveConfigIndex() -> Int? {
return realmQueue.sync { return Database.realmQueue.sync {
return instance.objects(ServerConnectionConfigActiveIndex.self).first?.index ?? nil return instance.objects(ServerConnectionConfigActiveIndex.self).first?.index ?? nil
} }
} }
+11
View File
@@ -18,3 +18,14 @@ extension Encodable {
return dictionary return dictionary
} }
} }
extension DispatchQueue {
static func runOnMainQueue(callback: @escaping (() -> Void)) {
if Thread.isMainThread {
callback()
} else {
DispatchQueue.main.sync {
callback()
}
}
}
}
+14 -22
View File
@@ -8,14 +8,6 @@
import Foundation import Foundation
import MediaPlayer import MediaPlayer
func getData(from url: URL, completion: @escaping (UIImage?) -> Void) {
URLSession.shared.dataTask(with: url, completionHandler: {(data, response, error) in
if let data = data {
completion(UIImage(data:data))
}
}).resume()
}
struct NowPlayingMetadata { struct NowPlayingMetadata {
var id: String var id: String
var itemId: String var itemId: String
@@ -26,22 +18,22 @@ struct NowPlayingMetadata {
} }
class NowPlayingInfo { class NowPlayingInfo {
private static var nowPlayingInfo: [String: Any] = [:] static var shared = {
return NowPlayingInfo()
}()
public static func setSessionMetadata(metadata: NowPlayingMetadata) { private var nowPlayingInfo: [String: Any]
private init() {
self.nowPlayingInfo = [:]
}
public func setSessionMetadata(metadata: NowPlayingMetadata) {
setMetadata(artwork: nil, metadata: metadata) setMetadata(artwork: nil, metadata: metadata)
/*
if !shouldFetchCover(id: metadata.id) || metadata.artworkUrl == nil {
return
}
*/
guard let url = URL(string: "\(Store.serverConfig!.address)/api/items/\(metadata.itemId)/cover?token=\(Store.serverConfig!.token)") else { guard let url = URL(string: "\(Store.serverConfig!.address)/api/items/\(metadata.itemId)/cover?token=\(Store.serverConfig!.token)") else {
return return
} }
ApiClient.getData(from: url) { [self] image in
getData(from: url) { [self] image in
guard let downloadedImage = image else { guard let downloadedImage = image else {
return return
} }
@@ -52,7 +44,7 @@ class NowPlayingInfo {
self.setMetadata(artwork: artwork, metadata: metadata) self.setMetadata(artwork: artwork, metadata: metadata)
} }
} }
public static func update(duration: Double, currentTime: Double, rate: Float) { public func update(duration: Double, currentTime: Double, rate: Float) {
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = currentTime
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = rate nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = rate
@@ -60,12 +52,12 @@ class NowPlayingInfo {
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
} }
public static func reset() { public func reset() {
nowPlayingInfo = [:] nowPlayingInfo = [:]
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
} }
private static func setMetadata(artwork: MPMediaItemArtwork?, metadata: NowPlayingMetadata?) { private func setMetadata(artwork: MPMediaItemArtwork?, metadata: NowPlayingMetadata?) {
if metadata == nil { if metadata == nil {
return return
} }
@@ -84,7 +76,7 @@ class NowPlayingInfo {
nowPlayingInfo[MPMediaItemPropertyArtist] = metadata!.author ?? "unknown" nowPlayingInfo[MPMediaItemPropertyArtist] = metadata!.author ?? "unknown"
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = metadata!.series nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = metadata!.series
} }
private static func shouldFetchCover(id: String) -> Bool { private func shouldFetchCover(id: String) -> Bool {
nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] as? String != id || nowPlayingInfo[MPMediaItemPropertyArtwork] == nil nowPlayingInfo[MPNowPlayingInfoPropertyExternalContentIdentifier] as? String != id || nowPlayingInfo[MPMediaItemPropertyArtwork] == nil
} }
} }
+3
View File
@@ -10,4 +10,7 @@ import Foundation
enum PlayerEvents: String { enum PlayerEvents: String {
case update = "com.audiobookshelf.app.player.update" case update = "com.audiobookshelf.app.player.update"
case closed = "com.audiobookshelf.app.player.closed" case closed = "com.audiobookshelf.app.player.closed"
case sleepSet = "com.audiobookshelf.app.player.sleep.set"
case sleepEnded = "com.audiobookshelf.app.player.sleep.ended"
case failed = "com.audiobookshelf.app.player.failed"
} }
+2 -2
View File
@@ -16,9 +16,9 @@ class Store {
} }
set(updated) { set(updated) {
if updated != nil { if updated != nil {
Database.setServerConnectionConfig(config: updated!) Database.shared.setServerConnectionConfig(config: updated!)
} else { } else {
Database.setLastActiveConfigIndexToNil() Database.shared.setLastActiveConfigIndexToNil()
} }
Database.realmQueue.sync { Database.realmQueue.sync {
+1 -2
View File
@@ -30,8 +30,7 @@ export default {
} }
], ],
link: [ link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }, { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css2?family=Ubuntu+Mono&family=Source+Sans+Pro:wght@300;400;600' },
] ]
}, },
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "audiobookshelf-app", "name": "audiobookshelf-app",
"version": "0.9.43-beta", "version": "0.9.44-beta",
"author": "advplyr", "author": "advplyr",
"scripts": { "scripts": {
"dev": "nuxt --hostname localhost --port 1337", "dev": "nuxt --hostname localhost --port 1337",
+81 -12
View File
@@ -6,10 +6,13 @@
<covers-book-cover :library-item="libraryItem" :width="128" :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-book-cover :library-item="libraryItem" :width="128" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1.5 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 128 * progressPercent + 'px' }"></div> <div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1.5 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 128 * progressPercent + 'px' }"></div>
</div> </div>
<!-- Show an indicator for local library items whether they are linked to a server item and if that server item is connected -->
<p v-if="isLocal && serverLibraryItemId" style="font-size: 10px" class="text-success py-1 uppercase tracking-widest">connected</p>
<p v-else-if="isLocal && libraryItem.serverAddress" style="font-size: 10px" class="text-gray-400 py-1">{{ libraryItem.serverAddress }}</p>
</div> </div>
<div class="flex-grow px-3"> <div class="flex-grow px-3">
<h1 class="text-lg">{{ title }}</h1> <h1 class="text-lg">{{ title }}</h1>
<!-- <h3 v-if="series" class="font-book text-gray-300 text-lg leading-7">{{ seriesText }}</h3> --> <h3 v-if="seriesName" class="text-gray-300 text-sm leading-6">{{ seriesName }}</h3>
<p class="text-sm text-gray-400">by {{ author }}</p> <p class="text-sm text-gray-400">by {{ author }}</p>
<p v-if="numTracks" class="text-gray-300 text-sm my-1"> <p v-if="numTracks" class="text-gray-300 text-sm my-1">
{{ $elapsedPretty(duration) }} {{ $elapsedPretty(duration) }}
@@ -20,33 +23,36 @@
<div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 bg-primary text-sm font-semibold rounded-md text-gray-200 mt-4 relative" :class="resettingProgress ? 'opacity-25' : ''"> <div v-if="!isPodcast && progressPercent > 0" class="px-4 py-2 bg-primary text-sm font-semibold rounded-md text-gray-200 mt-4 relative" :class="resettingProgress ? 'opacity-25' : ''">
<p class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p> <p class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
<p v-if="progressPercent < 1" class="text-gray-400 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p> <p v-if="progressPercent < 1" class="text-gray-400 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
<p v-else class="text-gray-400 text-xs">Finished {{ $formatDate(userProgressFinishedAt) }}</p>
<div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick"> <div v-if="!resettingProgress" class="absolute -top-1.5 -right-1.5 p-1 w-5 h-5 rounded-full bg-bg hover:bg-error border border-primary flex items-center justify-center cursor-pointer" @click.stop="clearProgressClick">
<span class="material-icons text-sm">close</span> <span class="material-icons text-sm">close</span>
</div> </div>
</div> </div>
<div v-if="isLocal" class="flex mt-4 -mr-2"> <div v-if="isLocal" class="flex mt-4">
<ui-btn color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mr-2" :padding-x="4" @click="playClick"> <ui-btn color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mr-2" :padding-x="4" @click="playClick">
<span v-show="!isPlaying" class="material-icons">play_arrow</span> <span v-show="!isPlaying" class="material-icons">play_arrow</span>
<span class="px-1 text-sm">{{ isPlaying ? 'Playing' : 'Play Local' }}</span> <span class="px-1 text-sm">{{ isPlaying ? 'Playing' : 'Play' }}</span>
</ui-btn> </ui-btn>
<ui-btn v-if="showRead && isConnected" color="info" class="flex items-center justify-center mr-2" :class="showPlay ? '' : 'flex-grow'" :padding-x="2" @click="readBook"> <ui-btn v-if="showRead" color="info" class="flex items-center justify-center mr-2" :class="showPlay ? '' : 'flex-grow'" :padding-x="2" @click="readBook">
<span class="material-icons">auto_stories</span> <span class="material-icons">auto_stories</span>
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span> <span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
</ui-btn> </ui-btn>
<ui-read-icon-btn v-if="!isPodcast" :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="flex items-center justify-center" @click="toggleFinished" />
</div> </div>
<div v-else-if="(user && (showPlay || showRead)) || hasLocal" class="flex mt-4 -mr-2"> <div v-else-if="(user && (showPlay || showRead)) || hasLocal" class="flex mt-4">
<ui-btn v-if="showPlay" color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mr-2" :padding-x="4" @click="playClick"> <ui-btn v-if="showPlay" color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mr-2" :padding-x="4" @click="playClick">
<span v-show="!isPlaying" class="material-icons">play_arrow</span> <span v-show="!isPlaying" class="material-icons">play_arrow</span>
<span class="px-1 text-sm">{{ isPlaying ? (isStreaming ? 'Streaming' : 'Playing') : hasLocal ? 'Play Local' : 'Play Stream' }}</span> <span class="px-1 text-sm">{{ isPlaying ? (isStreaming ? 'Streaming' : 'Playing') : hasLocal ? 'Play' : 'Stream' }}</span>
</ui-btn> </ui-btn>
<ui-btn v-if="showRead && user" color="info" class="flex items-center justify-center mr-2" :class="showPlay ? '' : 'flex-grow'" :padding-x="2" @click="readBook"> <ui-btn v-if="showRead && user" color="info" class="flex items-center justify-center mr-2" :class="showPlay ? '' : 'flex-grow'" :padding-x="2" @click="readBook">
<span class="material-icons">auto_stories</span> <span class="material-icons">auto_stories</span>
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span> <span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
</ui-btn> </ui-btn>
<ui-btn v-if="user && showPlay && !isIos && !hasLocal" :color="downloadItem ? 'warning' : 'primary'" class="flex items-center justify-center" :padding-x="2" @click="downloadClick"> <ui-btn v-if="user && showPlay && !isIos && !hasLocal" :color="downloadItem ? 'warning' : 'primary'" class="flex items-center justify-center mr-2" :padding-x="2" @click="downloadClick">
<span class="material-icons" :class="downloadItem ? 'animate-pulse' : ''">{{ downloadItem ? 'downloading' : 'download' }}</span> <span class="material-icons" :class="downloadItem ? 'animate-pulse' : ''">{{ downloadItem ? 'downloading' : 'download' }}</span>
</ui-btn> </ui-btn>
<ui-read-icon-btn v-if="!isPodcast" :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="flex items-center justify-center" @click="toggleFinished" />
</div> </div>
</div> </div>
</div> </div>
@@ -103,6 +109,7 @@ export default {
data() { data() {
return { return {
resettingProgress: false, resettingProgress: false,
isProcessingReadUpdate: false,
showSelectLocalFolder: false showSelectLocalFolder: false
} }
}, },
@@ -129,8 +136,14 @@ export default {
var podcastMedia = this.localLibraryItem.media var podcastMedia = this.localLibraryItem.media
return podcastMedia ? podcastMedia.episodes || [] : [] return podcastMedia ? podcastMedia.episodes || [] : []
}, },
isConnected() { serverLibraryItemId() {
return this.$store.state.socketConnected if (!this.isLocal) return this.libraryItem.id
// Check if local library item is connected to the current server
if (!this.libraryItem.serverAddress || !this.libraryItem.libraryItemId) return null
if (this.$store.getters['user/getServerAddress'] === this.libraryItem.serverAddress) {
return this.libraryItem.libraryItemId
}
return null
}, },
bookCoverAspectRatio() { bookCoverAspectRatio() {
return this.$store.getters['getBookCoverAspectRatio'] return this.$store.getters['getBookCoverAspectRatio']
@@ -163,6 +176,10 @@ export default {
series() { series() {
return this.mediaMetadata.series || [] return this.mediaMetadata.series || []
}, },
seriesName() {
// For books only on toJSONExpanded
return this.mediaMetadata.seriesName || ''
},
duration() { duration() {
return this.media.duration return this.media.duration
}, },
@@ -233,6 +250,9 @@ export default {
}, },
episodes() { episodes() {
return this.media.episodes || [] return this.media.episodes || []
},
isCasting() {
return this.$store.state.isCasting
} }
}, },
methods: { methods: {
@@ -241,7 +261,12 @@ export default {
}, },
playClick() { playClick() {
// Todo: Allow playing local or streaming // Todo: Allow playing local or streaming
if (this.hasLocal) return this.$eventBus.$emit('play-item', { libraryItemId: this.localLibraryItem.id }) if (this.hasLocal && this.serverLibraryItemId && this.isCasting) {
// If casting and connected to server for local library item then send server library item id
this.$eventBus.$emit('play-item', { libraryItemId: this.serverLibraryItemId })
return
}
if (this.hasLocal) return this.$eventBus.$emit('play-item', { libraryItemId: this.localLibraryItem.id, serverLibraryItemId: this.serverLibraryItemId })
this.$eventBus.$emit('play-item', { libraryItemId: this.libraryItemId }) this.$eventBus.$emit('play-item', { libraryItemId: this.libraryItemId })
}, },
async clearProgressClick() { async clearProgressClick() {
@@ -352,15 +377,59 @@ export default {
console.log('New local library item', item.id) console.log('New local library item', item.id)
this.$set(this.libraryItem, 'localLibraryItem', item) this.$set(this.libraryItem, 'localLibraryItem', item)
} }
},
async toggleFinished() {
this.isProcessingReadUpdate = true
if (this.isLocal) {
var isFinished = !this.userIsFinished
var payload = await this.$db.updateLocalMediaProgressFinished({ localLibraryItemId: this.localLibraryItemId, isFinished })
console.log('toggleFinished payload', JSON.stringify(payload))
if (!payload || payload.error) {
var errorMsg = payload ? payload.error : 'Unknown error'
this.$toast.error(errorMsg)
} else {
var localMediaProgress = payload.localMediaProgress
console.log('toggleFinished localMediaProgress', JSON.stringify(localMediaProgress))
if (localMediaProgress) {
this.$store.commit('globals/updateLocalMediaProgress', localMediaProgress)
}
var lmp = this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId)
console.log('toggleFinished Check LMP', this.libraryItemId, JSON.stringify(lmp))
var serverUpdated = payload.server
if (serverUpdated) {
this.$toast.success(`Local & Server Item marked as ${isFinished ? 'Finished' : 'Not Finished'}`)
} else {
this.$toast.success(`Local Item marked as ${isFinished ? 'Finished' : 'Not Finished'}`)
}
}
this.isProcessingReadUpdate = false
} else {
var updatePayload = {
isFinished: !this.userIsFinished
}
this.$axios
.$patch(`/api/me/progress/${this.libraryItemId}`, updatePayload)
.then(() => {
this.isProcessingReadUpdate = false
this.$toast.success(`Item marked as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
})
.catch((error) => {
console.error('Failed', error)
this.isProcessingReadUpdate = false
this.$toast.error(`Failed to mark as ${updatePayload.isFinished ? 'Finished' : 'Not Finished'}`)
})
}
} }
}, },
mounted() { mounted() {
this.$eventBus.$on('new-local-library-item', this.newLocalLibraryItem) this.$eventBus.$on('new-local-library-item', this.newLocalLibraryItem)
// this.$server.socket.on('item_updated', this.itemUpdated) this.$socket.on('item_updated', this.itemUpdated)
}, },
beforeDestroy() { beforeDestroy() {
this.$eventBus.$off('new-local-library-item', this.newLocalLibraryItem) this.$eventBus.$off('new-local-library-item', this.newLocalLibraryItem)
// this.$server.socket.off('item_updated', this.itemUpdated) this.$socket.off('item_updated', this.itemUpdated)
} }
} }
</script> </script>
+2 -1
View File
@@ -199,7 +199,8 @@ class AbsDatabaseWeb extends WebPlugin {
return [] return []
} }
async updateLocalMediaProgressFinished({ localMediaProgressId, isFinished }) { async updateLocalMediaProgressFinished(payload) {
// { localLibraryItemId, localEpisodeId, isFinished }
return null return null
} }
} }
+93
View File
@@ -0,0 +1,93 @@
Copyright 2010, 2012, 2014 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name Source.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.
+96
View File
@@ -0,0 +1,96 @@
-------------------------------
UBUNTU FONT LICENCE Version 1.0
-------------------------------
PREAMBLE
This licence allows the licensed fonts to be used, studied, modified and
redistributed freely. The fonts, including any derivative works, can be
bundled, embedded, and redistributed provided the terms of this licence
are met. The fonts and derivatives, however, cannot be released under
any other licence. The requirement for fonts to remain under this
licence does not require any document created using the fonts or their
derivatives to be published under this licence, as long as the primary
purpose of the document is not to be a vehicle for the distribution of
the fonts.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this licence and clearly marked as such. This may
include source files, build scripts and documentation.
"Original Version" refers to the collection of Font Software components
as received under this licence.
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to
a new environment.
"Copyright Holder(s)" refers to all individuals and companies who have a
copyright ownership of the Font Software.
"Substantially Changed" refers to Modified Versions which can be easily
identified as dissimilar to the Font Software by users of the Font
Software comparing the Original Version with the Modified Version.
To "Propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification and with or without charging
a redistribution fee), making available to the public, and in some
countries other activities as well.
PERMISSION & CONDITIONS
This licence does not grant any rights under trademark law and all such
rights are reserved.
Permission is hereby granted, free of charge, to any person obtaining a
copy of the Font Software, to propagate the Font Software, subject to
the below conditions:
1) Each copy of the Font Software must contain the above copyright
notice and this licence. These can be included either as stand-alone
text files, human-readable headers or in the appropriate machine-
readable metadata fields within text or binary files as long as those
fields can be easily viewed by the user.
2) The font name complies with the following:
(a) The Original Version must retain its name, unmodified.
(b) Modified Versions which are Substantially Changed must be renamed to
avoid use of the name of the Original Version or similar names entirely.
(c) Modified Versions which are not Substantially Changed must be
renamed to both (i) retain the name of the Original Version and (ii) add
additional naming elements to distinguish the Modified Version from the
Original Version. The name of such Modified Versions must be the name of
the Original Version, with "derivative X" where X represents the name of
the new work, appended to that name.
3) The name(s) of the Copyright Holder(s) and any contributor to the
Font Software shall not be used to promote, endorse or advertise any
Modified Version, except (i) as required by this licence, (ii) to
acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
their explicit written permission.
4) The Font Software, modified or unmodified, in part or in whole, must
be distributed entirely under this licence, and must not be distributed
under any other licence. The requirement for fonts to remain under this
licence does not affect any document created using the Font Software,
except any version of the Font Software extracted from a document
created using the Font Software may only be distributed under this
licence.
TERMINATION
This licence becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
DEALINGS IN THE FONT SOFTWARE.
Binary file not shown.