mirror of
https://github.com/advplyr/audiobookshelf-app.git
synced 2026-07-26 14:38:39 +02:00
Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
910b3a2a17 | ||
|
|
916da91ccb | ||
|
|
d70a99254a | ||
|
|
dcf5bb61a2 | ||
|
|
aa65bb10c1 | ||
|
|
158448f8a2 | ||
|
|
f4be9b3e26 | ||
|
|
15d68ca285 | ||
|
|
ad12e6a19d | ||
|
|
1f1b2fe85a | ||
|
|
480df58ce4 | ||
|
|
2decf532b2 | ||
|
|
3ba87419ae | ||
|
|
1c78af37fa | ||
|
|
2b5373aedd | ||
|
|
99bf960b8a | ||
|
|
c4aca22c28 | ||
|
|
58bd0e0cee | ||
|
|
c1c56f8f52 | ||
|
|
f930ba1941 | ||
|
|
251116a5ce | ||
|
|
06739c0401 |
@@ -29,8 +29,8 @@ android {
|
||||
applicationId "com.audiobookshelf.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 75
|
||||
versionName "0.9.46-beta"
|
||||
versionCode 78
|
||||
versionName "0.9.48-beta"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
|
||||
@@ -66,3 +66,20 @@ data class LocalFolder(
|
||||
JsonSubTypes.Type(LocalLibraryItem::class)
|
||||
)
|
||||
open class LibraryItemWrapper()
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class DeviceInfo(
|
||||
var manufacturer:String,
|
||||
var model:String,
|
||||
var brand:String,
|
||||
var sdkVersion:Int,
|
||||
var clientVersion: String
|
||||
)
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
data class PlayItemRequestPayload(
|
||||
var mediaPlayer:String,
|
||||
var forceDirectPlay:Boolean,
|
||||
var forceTranscode:Boolean,
|
||||
var deviceInfo:DeviceInfo
|
||||
)
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
package com.audiobookshelf.app.data
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
import android.util.Log
|
||||
import com.audiobookshelf.app.R
|
||||
import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.audiobookshelf.app.player.NOTIFICATION_LARGE_ICON_SIZE
|
||||
import com.bumptech.glide.Glide
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.*
|
||||
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
|
||||
@@ -42,4 +42,15 @@ data class LocalMediaProgress(
|
||||
isFinished = playbackSession.progress >= 0.99
|
||||
finishedAt = if (isFinished) lastUpdate else null
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
fun updateFromServerMediaProgress(serverMediaProgress:MediaProgress) {
|
||||
isFinished = serverMediaProgress.isFinished
|
||||
progress = serverMediaProgress.progress
|
||||
currentTime = serverMediaProgress.currentTime
|
||||
duration = serverMediaProgress.duration
|
||||
lastUpdate = serverMediaProgress.lastUpdate
|
||||
finishedAt = serverMediaProgress.finishedAt
|
||||
startedAt = serverMediaProgress.startedAt
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,8 @@ class PlaybackSession(
|
||||
val localMediaProgressId get() = if (episodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
|
||||
@get:JsonIgnore
|
||||
val progress get() = currentTime / getTotalDuration()
|
||||
@get:JsonIgnore
|
||||
val isLocalLibraryItemOnly get() = localLibraryItemId != "" && libraryItemId == null
|
||||
|
||||
@JsonIgnore
|
||||
fun getCurrentTrackIndex():Int {
|
||||
|
||||
@@ -24,6 +24,6 @@ object DeviceManager {
|
||||
}
|
||||
|
||||
fun getBase64Id(id:String):String {
|
||||
return android.util.Base64.encodeToString(id.toByteArray(), android.util.Base64.NO_WRAP)
|
||||
return android.util.Base64.encodeToString(id.toByteArray(), android.util.Base64.URL_SAFE or android.util.Base64.NO_WRAP)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
package com.audiobookshelf.app.media
|
||||
|
||||
import android.bluetooth.BluetoothClass
|
||||
import android.content.Context
|
||||
import android.support.v4.media.MediaBrowserCompat
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
import android.util.Log
|
||||
import com.audiobookshelf.app.data.*
|
||||
import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.audiobookshelf.app.player.PlayerNotificationService
|
||||
import com.audiobookshelf.app.server.ApiHandler
|
||||
import java.util.*
|
||||
import io.paperdb.Paper
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
||||
val tag = "MediaManager"
|
||||
@@ -24,6 +25,7 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
||||
var serverPodcastEpisodes = listOf<PodcastEpisode>()
|
||||
var serverLibraryCategories = listOf<LibraryCategory>()
|
||||
var serverLibraries = listOf<Library>()
|
||||
var serverConfigIdUsed:String? = null
|
||||
|
||||
fun initializeAndroidAuto() {
|
||||
Log.d(tag, "Android Auto started when MainActivity was never started - initializing Paper")
|
||||
@@ -34,6 +36,20 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
||||
return serverLibraries.find { it.id == id } != null
|
||||
}
|
||||
|
||||
fun checkResetServerItems() {
|
||||
// When opening android auto need to check if still connected to server
|
||||
// and reset any server data already set
|
||||
val serverConnConfig = if (DeviceManager.isConnectedToServer) DeviceManager.serverConnectionConfig else DeviceManager.deviceData.getLastServerConnectionConfig()
|
||||
|
||||
if (!DeviceManager.isConnectedToServer || !apiHandler.isOnline() || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) {
|
||||
serverPodcastEpisodes = listOf()
|
||||
serverLibraryCategories = listOf()
|
||||
serverLibraries = listOf()
|
||||
serverLibraryItems = listOf()
|
||||
selectedLibraryId = ""
|
||||
}
|
||||
}
|
||||
|
||||
fun loadLibraryCategories(libraryId:String, cb: (List<LibraryCategory>) -> Unit) {
|
||||
if (serverLibraryCategories.isNotEmpty()) {
|
||||
cb(serverLibraryCategories)
|
||||
@@ -125,6 +141,48 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun checkServerConnection(config:ServerConnectionConfig) : Boolean {
|
||||
var successfulPing = false
|
||||
suspendCoroutine<Boolean> { cont ->
|
||||
apiHandler.pingServer(config) {
|
||||
Log.d(tag, "checkServerConnection: Checked server conn for ${config.address} result = $it")
|
||||
successfulPing = it
|
||||
cont.resume(it)
|
||||
}
|
||||
}
|
||||
return successfulPing
|
||||
}
|
||||
|
||||
fun checkSetValidServerConnectionConfig(cb: (Boolean) -> Unit) = runBlocking {
|
||||
if (!apiHandler.isOnline()) cb(false)
|
||||
else {
|
||||
coroutineScope {
|
||||
var hasValidConn = false
|
||||
|
||||
// First check if the current selected config is pingable
|
||||
DeviceManager.serverConnectionConfig?.let {
|
||||
hasValidConn = checkServerConnection(it)
|
||||
Log.d(tag, "checkSetValidServerConnectionConfig: Current config ${DeviceManager.serverAddress} is pingable? $hasValidConn")
|
||||
}
|
||||
|
||||
if (!hasValidConn) {
|
||||
// Loop through available configs and check if can connect
|
||||
for (config: ServerConnectionConfig in DeviceManager.deviceData.serverConnectionConfigs) {
|
||||
val result = checkServerConnection(config)
|
||||
|
||||
if (result) {
|
||||
hasValidConn = true
|
||||
DeviceManager.serverConnectionConfig = config
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cb(hasValidConn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Load currently listening category for local items
|
||||
fun loadLocalCategory():List<LibraryCategory> {
|
||||
val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
|
||||
@@ -146,33 +204,32 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
||||
val localCategories = loadLocalCategory()
|
||||
cats.addAll(localCategories)
|
||||
|
||||
// Connected to server and has internet - load other cats
|
||||
if (apiHandler.isOnline() && (DeviceManager.isConnectedToServer || DeviceManager.hasLastServerConnectionConfig)) {
|
||||
if (!DeviceManager.isConnectedToServer) {
|
||||
DeviceManager.serverConnectionConfig = DeviceManager.deviceData.getLastServerConnectionConfig()
|
||||
Log.d(tag, "Not connected to server, set last server \"${DeviceManager.serverAddress}\"")
|
||||
}
|
||||
// Check if any valid server connection if not use locally downloaded books
|
||||
checkSetValidServerConnectionConfig { isConnected ->
|
||||
if (isConnected) {
|
||||
serverConfigIdUsed = DeviceManager.serverConnectionConfigId
|
||||
|
||||
loadLibraries { libraries ->
|
||||
val library = libraries[0]
|
||||
Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}")
|
||||
loadLibraries { libraries ->
|
||||
val library = libraries[0]
|
||||
Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}")
|
||||
|
||||
loadLibraryCategories(library.id) { libraryCategories ->
|
||||
loadLibraryCategories(library.id) { libraryCategories ->
|
||||
|
||||
// Only using book or podcast library categories for now
|
||||
libraryCategories.forEach {
|
||||
// Log.d(tag, "Found library category ${it.label} with type ${it.type}")
|
||||
if (it.type == library.mediaType) {
|
||||
// Log.d(tag, "Using library category ${it.id}")
|
||||
cats.add(it)
|
||||
// Only using book or podcast library categories for now
|
||||
libraryCategories.forEach {
|
||||
// Log.d(tag, "Found library category ${it.label} with type ${it.type}")
|
||||
if (it.type == library.mediaType) {
|
||||
// Log.d(tag, "Using library category ${it.id}")
|
||||
cats.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cb(cats)
|
||||
cb(cats)
|
||||
}
|
||||
}
|
||||
} else { // Not connected/no internet sent downloaded cats only
|
||||
cb(cats)
|
||||
}
|
||||
} else { // Not connected/no internet sent downloaded cats only
|
||||
cb(cats)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,13 +270,13 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun play(libraryItemWrapper:LibraryItemWrapper, episode:PodcastEpisode?, mediaPlayer:String, cb: (PlaybackSession) -> Unit) {
|
||||
fun play(libraryItemWrapper:LibraryItemWrapper, episode:PodcastEpisode?, playItemRequestPayload:PlayItemRequestPayload, cb: (PlaybackSession) -> Unit) {
|
||||
if (libraryItemWrapper is LocalLibraryItem) {
|
||||
val localLibraryItem = libraryItemWrapper as LocalLibraryItem
|
||||
cb(localLibraryItem.getPlaybackSession(episode))
|
||||
} else {
|
||||
val libraryItem = libraryItemWrapper as LibraryItem
|
||||
apiHandler.playLibraryItem(libraryItem.id,episode?.id ?: "",false, mediaPlayer) {
|
||||
apiHandler.playLibraryItem(libraryItem.id,episode?.id ?: "",playItemRequestPayload) {
|
||||
cb(it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import com.audiobookshelf.app.data.LocalMediaProgress
|
||||
import com.audiobookshelf.app.data.MediaProgress
|
||||
import com.audiobookshelf.app.data.PlaybackSession
|
||||
import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.audiobookshelf.app.server.ApiHandler
|
||||
@@ -24,6 +25,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
||||
var listeningTimerRunning:Boolean = false
|
||||
|
||||
private var lastSyncTime:Long = 0
|
||||
private var failedSyncs:Int = 0
|
||||
|
||||
var currentPlaybackSession: PlaybackSession? = null // copy of pb session currently syncing
|
||||
var currentLocalMediaProgress: LocalMediaProgress? = null
|
||||
@@ -41,6 +43,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
||||
currentLocalMediaProgress = null
|
||||
listeningTimerTask?.cancel()
|
||||
lastSyncTime = 0L
|
||||
failedSyncs = 0
|
||||
} else {
|
||||
return
|
||||
}
|
||||
@@ -68,6 +71,16 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
||||
reset()
|
||||
}
|
||||
|
||||
fun syncFromServerProgress(mediaProgress: MediaProgress) {
|
||||
currentPlaybackSession?.let {
|
||||
it.updatedAt = mediaProgress.lastUpdate
|
||||
it.currentTime = mediaProgress.currentTime
|
||||
|
||||
DeviceManager.dbManager.saveLocalPlaybackSession(it)
|
||||
saveLocalProgress(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun sync(currentTime:Double) {
|
||||
val diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
|
||||
if (diffSinceLastSync < 1000L) {
|
||||
@@ -100,7 +113,17 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
||||
}
|
||||
} else {
|
||||
apiHandler.sendProgressSync(currentSessionId, syncData) {
|
||||
Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime")
|
||||
if (it) {
|
||||
Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime")
|
||||
failedSyncs = 0
|
||||
} else {
|
||||
failedSyncs++
|
||||
if (failedSyncs == 2) {
|
||||
playerNotificationService.alertSyncFailing() // Show alert in client
|
||||
failedSyncs = 0
|
||||
}
|
||||
Log.d(tag, "Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -130,5 +153,6 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
||||
currentPlaybackSession = null
|
||||
currentLocalMediaProgress = null
|
||||
lastSyncTime = 0L
|
||||
failedSyncs = 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,8 @@ import android.os.Message
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import com.audiobookshelf.app.data.LibraryItem
|
||||
import com.audiobookshelf.app.data.LibraryItemWrapper
|
||||
import com.audiobookshelf.app.data.PodcastEpisode
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
import kotlin.concurrent.schedule
|
||||
|
||||
@@ -28,7 +24,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
||||
override fun onPrepare() {
|
||||
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
|
||||
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
|
||||
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getMediaPlayer()) {
|
||||
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getPlayItemRequestPayload(false)) {
|
||||
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
||||
Handler(Looper.getMainLooper()).post() {
|
||||
playerNotificationService.preparePlayer(it,true,null)
|
||||
@@ -50,7 +46,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
||||
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
|
||||
Log.d(tag, "ON PLAY FROM SEARCH $query")
|
||||
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
|
||||
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getMediaPlayer()) {
|
||||
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getPlayItemRequestPayload(false)) {
|
||||
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
||||
Handler(Looper.getMainLooper()).post() {
|
||||
playerNotificationService.preparePlayer(it,true,null)
|
||||
@@ -101,10 +97,14 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
||||
podcastEpisode = libraryItemWithEpisode?.episode
|
||||
} else {
|
||||
libraryItemWrapper = playerNotificationService.mediaManager.getById(mediaId)
|
||||
|
||||
if (libraryItemWrapper == null) {
|
||||
Log.e(tag, "onPlayFromMediaId: Media item not found $mediaId")
|
||||
}
|
||||
}
|
||||
|
||||
libraryItemWrapper?.let { li ->
|
||||
playerNotificationService.mediaManager.play(li, podcastEpisode, playerNotificationService.getMediaPlayer()) {
|
||||
playerNotificationService.mediaManager.play(li, podcastEpisode, playerNotificationService.getPlayItemRequestPayload(false)) {
|
||||
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
||||
Handler(Looper.getMainLooper()).post() {
|
||||
playerNotificationService.preparePlayer(it,true,null)
|
||||
@@ -119,10 +119,13 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
||||
|
||||
fun handleCallMediaButton(intent: Intent): Boolean {
|
||||
if(Intent.ACTION_MEDIA_BUTTON == intent.action) {
|
||||
var keyEvent = intent.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT)
|
||||
if (keyEvent?.getAction() == KeyEvent.ACTION_UP) {
|
||||
when (keyEvent?.getKeyCode()) {
|
||||
val keyEvent = intent.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT)
|
||||
|
||||
if (keyEvent?.action == KeyEvent.ACTION_UP) {
|
||||
Log.d(tag, "handleCallMediaButton: key action_up for ${keyEvent.keyCode}")
|
||||
when (keyEvent.keyCode) {
|
||||
KeyEvent.KEYCODE_HEADSETHOOK -> {
|
||||
Log.d(tag, "handleCallMediaButton: Headset Hook")
|
||||
if (0 == mediaButtonClickCount) {
|
||||
if (playerNotificationService.mPlayer.isPlaying)
|
||||
playerNotificationService.pause()
|
||||
@@ -132,6 +135,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
||||
handleMediaButtonClickCount()
|
||||
}
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY -> {
|
||||
Log.d(tag, "handleCallMediaButton: Media Play")
|
||||
if (0 == mediaButtonClickCount) {
|
||||
playerNotificationService.play()
|
||||
playerNotificationService.sleepTimerManager.checkShouldExtendSleepTimer()
|
||||
@@ -139,6 +143,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
||||
handleMediaButtonClickCount()
|
||||
}
|
||||
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
|
||||
Log.d(tag, "handleCallMediaButton: Media Pause")
|
||||
if (0 == mediaButtonClickCount) playerNotificationService.pause()
|
||||
handleMediaButtonClickCount()
|
||||
}
|
||||
@@ -152,6 +157,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
||||
playerNotificationService.closePlayback()
|
||||
}
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
|
||||
Log.d(tag, "handleCallMediaButton: Media Play/Pause")
|
||||
if (playerNotificationService.mPlayer.isPlaying) {
|
||||
if (0 == mediaButtonClickCount) playerNotificationService.pause()
|
||||
handleMediaButtonClickCount()
|
||||
@@ -164,7 +170,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
Log.d(tag, "KeyCode:${keyEvent.getKeyCode()}")
|
||||
Log.d(tag, "KeyCode:${keyEvent.keyCode}")
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -173,7 +179,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
||||
return true
|
||||
}
|
||||
|
||||
fun handleMediaButtonClickCount() {
|
||||
private fun handleMediaButtonClickCount() {
|
||||
mediaButtonClickCount++
|
||||
if (1 == mediaButtonClickCount) {
|
||||
Timer().schedule(mediaButtonClickTimeout) {
|
||||
|
||||
+3
-3
@@ -30,7 +30,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
||||
override fun onPrepare(playWhenReady: Boolean) {
|
||||
Log.d(tag, "ON PREPARE $playWhenReady")
|
||||
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
|
||||
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getMediaPlayer()) {
|
||||
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getPlayItemRequestPayload(false)) {
|
||||
Handler(Looper.getMainLooper()).post() {
|
||||
playerNotificationService.preparePlayer(it,playWhenReady,null)
|
||||
}
|
||||
@@ -53,7 +53,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
||||
}
|
||||
|
||||
libraryItemWrapper?.let { li ->
|
||||
playerNotificationService.mediaManager.play(li, podcastEpisode, playerNotificationService.getMediaPlayer()) {
|
||||
playerNotificationService.mediaManager.play(li, podcastEpisode, playerNotificationService.getPlayItemRequestPayload(false)) {
|
||||
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
||||
Handler(Looper.getMainLooper()).post() {
|
||||
playerNotificationService.preparePlayer(it,playWhenReady,null)
|
||||
@@ -65,7 +65,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
||||
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
|
||||
Log.d(tag, "ON PREPARE FROM SEARCH $query")
|
||||
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
|
||||
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getMediaPlayer()) {
|
||||
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getPlayItemRequestPayload(false)) {
|
||||
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
||||
Handler(Looper.getMainLooper()).post() {
|
||||
playerNotificationService.preparePlayer(it,playWhenReady,null)
|
||||
|
||||
@@ -5,6 +5,8 @@ import com.audiobookshelf.app.data.PlayerState
|
||||
import com.google.android.exoplayer2.PlaybackException
|
||||
import com.google.android.exoplayer2.Player
|
||||
|
||||
const val PAUSE_LEN_BEFORE_RECHECK = 60000 // 1 minute
|
||||
|
||||
class PlayerListener(var playerNotificationService:PlayerNotificationService) : Player.Listener {
|
||||
var tag = "PlayerListener"
|
||||
|
||||
@@ -81,6 +83,12 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
||||
Log.d(tag, "SeekBackTime: back time is 0")
|
||||
}
|
||||
}
|
||||
|
||||
// Check if playback session still exists or sync media progress if updated
|
||||
if (lastPauseTime > PAUSE_LEN_BEFORE_RECHECK) {
|
||||
val shouldCarryOn = playerNotificationService.checkCurrentSessionProgress()
|
||||
if (!shouldCarryOn) return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(tag, "SeekBackTime: Player not playing set last pause time")
|
||||
@@ -102,8 +110,8 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
||||
|
||||
private fun calcPauseSeekBackTime() : Long {
|
||||
if (lastPauseTime <= 0) return 0
|
||||
var time: Long = System.currentTimeMillis() - lastPauseTime
|
||||
var seekback: Long
|
||||
val time: Long = System.currentTimeMillis() - lastPauseTime
|
||||
val seekback: Long
|
||||
if (time < 3000) seekback = 0
|
||||
else if (time < 300000) seekback = 10000 // 3s to 5m = jump back 10s
|
||||
else if (time < 1800000) seekback = 20000 // 5m to 30m = jump back 20s
|
||||
|
||||
+103
-5
@@ -17,7 +17,9 @@ import androidx.annotation.RequiresApi
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.media.MediaBrowserServiceCompat
|
||||
import androidx.media.utils.MediaConstants
|
||||
import com.audiobookshelf.app.BuildConfig
|
||||
import com.audiobookshelf.app.data.*
|
||||
import com.audiobookshelf.app.data.DeviceInfo
|
||||
import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.audiobookshelf.app.media.MediaManager
|
||||
import com.audiobookshelf.app.server.ApiHandler
|
||||
@@ -46,12 +48,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
fun onPlaybackClosed()
|
||||
fun onPlayingUpdate(isPlaying: Boolean)
|
||||
fun onMetadata(metadata: PlaybackMetadata)
|
||||
fun onPrepare(audiobookId: String, playWhenReady: Boolean)
|
||||
fun onSleepTimerEnded(currentPosition: Long)
|
||||
fun onSleepTimerSet(sleepTimeRemaining: Int)
|
||||
fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress)
|
||||
fun onPlaybackFailed(errorMessage:String)
|
||||
fun onMediaPlayerChanged(mediaPlayer:String)
|
||||
fun onProgressSyncFailing()
|
||||
}
|
||||
|
||||
private val tag = "PlayerService"
|
||||
@@ -66,7 +68,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
private lateinit var transportControls:MediaControllerCompat.TransportControls
|
||||
|
||||
lateinit var mediaManager: MediaManager
|
||||
private lateinit var apiHandler: ApiHandler
|
||||
lateinit var apiHandler: ApiHandler
|
||||
|
||||
lateinit var mPlayer: ExoPlayer
|
||||
lateinit var currentPlayer:Player
|
||||
@@ -373,12 +375,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
// On error and was attempting to direct play - fallback to transcode
|
||||
currentPlaybackSession?.let { playbackSession ->
|
||||
if (playbackSession.isDirectPlay) {
|
||||
val mediaPlayer = getMediaPlayer()
|
||||
Log.d(tag, "Fallback to transcode $mediaPlayer")
|
||||
val playItemRequestPayload = getPlayItemRequestPayload(true)
|
||||
Log.d(tag, "Fallback to transcode $playItemRequestPayload.mediaPlayer")
|
||||
|
||||
val libraryItemId = playbackSession.libraryItemId ?: "" // Must be true since direct play
|
||||
val episodeId = playbackSession.episodeId
|
||||
apiHandler.playLibraryItem(libraryItemId, episodeId, true, mediaPlayer) {
|
||||
apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
preparePlayer(it, true, null)
|
||||
}
|
||||
@@ -390,6 +392,21 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
}
|
||||
}
|
||||
|
||||
fun startNewPlaybackSession() {
|
||||
currentPlaybackSession?.let { playbackSession ->
|
||||
val forceTranscode = playbackSession.isHLS // If already HLS then force
|
||||
val playItemRequestPayload = getPlayItemRequestPayload(forceTranscode)
|
||||
|
||||
val libraryItemId = playbackSession.libraryItemId ?: "" // Must be true since direct play
|
||||
val episodeId = playbackSession.episodeId
|
||||
apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) {
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
preparePlayer(it, true, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun switchToPlayer(useCastPlayer: Boolean) {
|
||||
val wasPlaying = currentPlayer.isPlaying
|
||||
if (useCastPlayer) {
|
||||
@@ -481,6 +498,67 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
return currentPlaybackSession?.id
|
||||
}
|
||||
|
||||
// Called from PlayerListener play event
|
||||
// check with server if progress has updated since last play and sync progress update
|
||||
fun checkCurrentSessionProgress():Boolean {
|
||||
if (currentPlaybackSession == null) return true
|
||||
|
||||
currentPlaybackSession?.let { playbackSession ->
|
||||
if (!apiHandler.isOnline() || playbackSession.isLocalLibraryItemOnly) {
|
||||
return true // carry on
|
||||
}
|
||||
|
||||
if (playbackSession.isLocal) {
|
||||
// Local playback session check if server has updated media progress
|
||||
Log.d(tag, "checkCurrentSessionProgress: Checking if local media progress was updated on server")
|
||||
apiHandler.getMediaProgress(playbackSession.libraryItemId!!, playbackSession.episodeId) { mediaProgress ->
|
||||
if (mediaProgress.lastUpdate > playbackSession.updatedAt && mediaProgress.currentTime != playbackSession.currentTime) {
|
||||
Log.d(tag, "checkCurrentSessionProgress: Media progress was updated since last play time updating from ${playbackSession.currentTime} to ${mediaProgress.currentTime}")
|
||||
mediaProgressSyncer.syncFromServerProgress(mediaProgress)
|
||||
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
seekPlayer(playbackSession.currentTimeMs)
|
||||
}
|
||||
}
|
||||
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
// Should already be playing
|
||||
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
||||
mediaProgressSyncer.start()
|
||||
clientEventEmitter?.onPlayingUpdate(true)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Streaming from server so check if playback session still exists on server
|
||||
Log.d(
|
||||
tag,
|
||||
"checkCurrentSessionProgress: Checking if playback session for server stream is still available"
|
||||
)
|
||||
apiHandler.getPlaybackSession(playbackSession.id) {
|
||||
if (it == null) {
|
||||
Log.d(
|
||||
tag,
|
||||
"checkCurrentSessionProgress: Playback session does not exist on server - start new playback session"
|
||||
)
|
||||
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
currentPlayer.pause()
|
||||
startNewPlaybackSession()
|
||||
}
|
||||
} else {
|
||||
Log.d(tag, "checkCurrentSessionProgress: Playback session still available on server")
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
||||
mediaProgressSyncer.start()
|
||||
clientEventEmitter?.onPlayingUpdate(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun play() {
|
||||
if (currentPlayer.isPlaying) {
|
||||
Log.d(tag, "Already playing")
|
||||
@@ -549,10 +627,29 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
return if(currentPlayer == castPlayer) "cast-player" else "exo-player"
|
||||
}
|
||||
|
||||
fun getDeviceInfo(): DeviceInfo {
|
||||
/* EXAMPLE
|
||||
manufacturer: Google
|
||||
model: Pixel 6
|
||||
brand: google
|
||||
sdkVersion: 32
|
||||
appVersion: 0.9.46-beta
|
||||
*/
|
||||
return DeviceInfo(Build.MANUFACTURER, Build.MODEL, Build.BRAND, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
|
||||
}
|
||||
|
||||
fun getPlayItemRequestPayload(forceTranscode:Boolean):PlayItemRequestPayload {
|
||||
return PlayItemRequestPayload(getMediaPlayer(), !forceTranscode, forceTranscode, getDeviceInfo())
|
||||
}
|
||||
|
||||
fun getContext():Context {
|
||||
return ctx
|
||||
}
|
||||
|
||||
fun alertSyncFailing() {
|
||||
clientEventEmitter?.onProgressSyncFailing()
|
||||
}
|
||||
|
||||
//
|
||||
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
||||
//
|
||||
@@ -591,6 +688,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||
mediaManager.initializeAndroidAuto()
|
||||
isStarted = true
|
||||
}
|
||||
mediaManager.checkResetServerItems() // Reset any server items if no longer connected to server
|
||||
|
||||
isAndroidAuto = true
|
||||
|
||||
|
||||
@@ -59,13 +59,6 @@ class AbsAudioPlayer : Plugin() {
|
||||
notifyListeners("onMetadata", JSObject(jacksonMapper.writeValueAsString(metadata)))
|
||||
}
|
||||
|
||||
override fun onPrepare(audiobookId: String, playWhenReady: Boolean) {
|
||||
val jsobj = JSObject()
|
||||
jsobj.put("audiobookId", audiobookId)
|
||||
jsobj.put("playWhenReady", playWhenReady)
|
||||
notifyListeners("onPrepareMedia", jsobj)
|
||||
}
|
||||
|
||||
override fun onSleepTimerEnded(currentPosition: Long) {
|
||||
emit("onSleepTimerEnded", currentPosition)
|
||||
}
|
||||
@@ -85,6 +78,10 @@ class AbsAudioPlayer : Plugin() {
|
||||
override fun onMediaPlayerChanged(mediaPlayer:String) {
|
||||
emit("onMediaPlayerChanged", mediaPlayer)
|
||||
}
|
||||
|
||||
override fun onProgressSyncFailing() {
|
||||
emit("onProgressSyncFailing", "")
|
||||
}
|
||||
})
|
||||
}
|
||||
mainActivity.pluginCallback = foregroundServiceReady
|
||||
@@ -191,9 +188,9 @@ class AbsAudioPlayer : Plugin() {
|
||||
return call.resolve(JSObject())
|
||||
}
|
||||
} else { // Play library item from server
|
||||
val mediaPlayer = playerNotificationService.getMediaPlayer()
|
||||
val playItemRequestPayload = playerNotificationService.getPlayItemRequestPayload(false)
|
||||
|
||||
apiHandler.playLibraryItem(libraryItemId, episodeId, false, mediaPlayer) {
|
||||
apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) {
|
||||
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
Log.d(tag, "Preparing Player TEST ${jacksonMapper.writeValueAsString(it)}")
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.audiobookshelf.app.device.DeviceManager
|
||||
import com.audiobookshelf.app.server.ApiHandler
|
||||
import com.fasterxml.jackson.core.json.JsonReadFeature
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.getcapacitor.*
|
||||
import com.getcapacitor.annotation.CapacitorPlugin
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -188,7 +189,7 @@ class AbsDatabase : Plugin() {
|
||||
|
||||
@PluginMethod
|
||||
fun removeLocalMediaProgress(call:PluginCall) {
|
||||
var localMediaProgressId = call.getString("localMediaProgressId", "").toString()
|
||||
val localMediaProgressId = call.getString("localMediaProgressId", "").toString()
|
||||
DeviceManager.dbManager.removeLocalMediaProgress(localMediaProgressId)
|
||||
call.resolve()
|
||||
}
|
||||
@@ -204,6 +205,63 @@ class AbsDatabase : Plugin() {
|
||||
}
|
||||
}
|
||||
|
||||
// Updates received via web socket
|
||||
// This function doesn't need to sync with the server also because this data is coming from the server
|
||||
// If sending the localMediaProgressId then update existing media progress
|
||||
// If sending localLibraryItemId then save new local media progress
|
||||
@PluginMethod
|
||||
fun syncServerMediaProgressWithLocalMediaProgress(call:PluginCall) {
|
||||
val serverMediaProgress = call.getObject("mediaProgress").toString()
|
||||
val localLibraryItemId = call.getString("localLibraryItemId", "").toString()
|
||||
var localEpisodeId:String? = call.getString("localEpisodeId", "").toString()
|
||||
if (localEpisodeId.isNullOrEmpty()) localEpisodeId = null
|
||||
var localMediaProgressId = call.getString("localMediaProgressId") ?: ""
|
||||
|
||||
val mediaProgress = jacksonMapper.readValue<MediaProgress>(serverMediaProgress)
|
||||
|
||||
if (localMediaProgressId == "") {
|
||||
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
|
||||
if (localLibraryItem != null) {
|
||||
localMediaProgressId = if (localEpisodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
|
||||
|
||||
val localMediaProgress = LocalMediaProgress(
|
||||
id = localMediaProgressId,
|
||||
localLibraryItemId = localLibraryItemId,
|
||||
localEpisodeId = localEpisodeId,
|
||||
duration = mediaProgress.duration,
|
||||
progress = mediaProgress.progress,
|
||||
currentTime = mediaProgress.currentTime,
|
||||
isFinished = mediaProgress.isFinished,
|
||||
lastUpdate = mediaProgress.lastUpdate,
|
||||
startedAt = mediaProgress.startedAt,
|
||||
finishedAt = mediaProgress.finishedAt,
|
||||
serverConnectionConfigId = localLibraryItem.serverConnectionConfigId,
|
||||
serverAddress = localLibraryItem.serverAddress,
|
||||
serverUserId = localLibraryItem.serverUserId,
|
||||
libraryItemId = localLibraryItem.libraryItemId,
|
||||
episodeId = mediaProgress.episodeId)
|
||||
|
||||
Log.d(tag, "syncServerMediaProgressWithLocalMediaProgress: Saving new local media progress $localMediaProgress")
|
||||
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
|
||||
call.resolve(JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
|
||||
} else {
|
||||
Log.e(tag, "syncServerMediaProgressWithLocalMediaProgress: Local library item not found")
|
||||
}
|
||||
} else {
|
||||
Log.d(tag, "syncServerMediaProgressWithLocalMediaProgress $localMediaProgressId")
|
||||
val localMediaProgress = DeviceManager.dbManager.getLocalMediaProgress(localMediaProgressId)
|
||||
|
||||
if (localMediaProgress == null) {
|
||||
Log.w(tag, "syncServerMediaProgressWithLocalMediaProgress Local media progress not found $localMediaProgressId")
|
||||
call.resolve()
|
||||
} else {
|
||||
localMediaProgress.updateFromServerMediaProgress(mediaProgress)
|
||||
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
|
||||
call.resolve(JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
fun updateLocalMediaProgressFinished(call:PluginCall) {
|
||||
val localLibraryItemId = call.getString("localLibraryItemId", "").toString()
|
||||
|
||||
@@ -19,11 +19,13 @@ import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.json.JSONObject
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ApiHandler(var ctx:Context) {
|
||||
val tag = "ApiHandler"
|
||||
|
||||
private var client = OkHttpClient()
|
||||
private var defaultClient = OkHttpClient()
|
||||
private var pingClient = OkHttpClient.Builder().callTimeout(3, TimeUnit.SECONDS).build()
|
||||
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||
|
||||
var storageSharedPreferences: SharedPreferences? = null
|
||||
@@ -33,11 +35,14 @@ class ApiHandler(var ctx:Context) {
|
||||
data class MediaProgressSyncResponsePayload(val numServerProgressUpdates:Int, val localProgressUpdates:List<LocalMediaProgress>)
|
||||
data class LocalMediaProgressSyncResultsPayload(var numLocalMediaProgressForServer:Int, var numServerProgressUpdates:Int, var numLocalProgressUpdates:Int)
|
||||
|
||||
fun getRequest(endpoint:String, cb: (JSObject) -> Unit) {
|
||||
fun getRequest(endpoint:String, httpClient:OkHttpClient?, config:ServerConnectionConfig?, cb: (JSObject) -> Unit) {
|
||||
val address = config?.address ?: DeviceManager.serverAddress
|
||||
val token = config?.token ?: DeviceManager.token
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("${DeviceManager.serverAddress}$endpoint").addHeader("Authorization", "Bearer ${DeviceManager.token}")
|
||||
.url("${address}$endpoint").addHeader("Authorization", "Bearer $token")
|
||||
.build()
|
||||
makeRequest(request, cb)
|
||||
makeRequest(request, httpClient, cb)
|
||||
}
|
||||
|
||||
fun postRequest(endpoint:String, payload: JSObject, cb: (JSObject) -> Unit) {
|
||||
@@ -46,7 +51,7 @@ class ApiHandler(var ctx:Context) {
|
||||
val request = Request.Builder().post(requestBody)
|
||||
.url("${DeviceManager.serverAddress}$endpoint").addHeader("Authorization", "Bearer ${DeviceManager.token}")
|
||||
.build()
|
||||
makeRequest(request, cb)
|
||||
makeRequest(request, null, cb)
|
||||
}
|
||||
|
||||
fun patchRequest(endpoint:String, payload: JSObject, cb: (JSObject) -> Unit) {
|
||||
@@ -55,7 +60,7 @@ class ApiHandler(var ctx:Context) {
|
||||
val request = Request.Builder().patch(requestBody)
|
||||
.url("${DeviceManager.serverAddress}$endpoint").addHeader("Authorization", "Bearer ${DeviceManager.token}")
|
||||
.build()
|
||||
makeRequest(request, cb)
|
||||
makeRequest(request, null, cb)
|
||||
}
|
||||
|
||||
fun isOnline(): Boolean {
|
||||
@@ -76,7 +81,8 @@ class ApiHandler(var ctx:Context) {
|
||||
return false
|
||||
}
|
||||
|
||||
fun makeRequest(request:Request, cb: (JSObject) -> Unit) {
|
||||
fun makeRequest(request:Request, httpClient:OkHttpClient?, cb: (JSObject) -> Unit) {
|
||||
val client = httpClient ?: defaultClient
|
||||
client.newCall(request).enqueue(object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
Log.d(tag, "FAILURE TO CONNECT")
|
||||
@@ -86,9 +92,15 @@ class ApiHandler(var ctx:Context) {
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
response.use {
|
||||
if (!it.isSuccessful) throw IOException("Unexpected code $response")
|
||||
if (!it.isSuccessful) {
|
||||
// throw IOException("Unexpected code $response")
|
||||
val jsobj = JSObject()
|
||||
jsobj.put("error", "Unexpected code $response")
|
||||
cb(jsobj)
|
||||
return
|
||||
}
|
||||
|
||||
val bodyString = it.body!!.string()
|
||||
val bodyString = it.body!!.string()
|
||||
if (bodyString == "OK") {
|
||||
cb(JSObject())
|
||||
} else {
|
||||
@@ -108,7 +120,7 @@ class ApiHandler(var ctx:Context) {
|
||||
|
||||
fun getLibraries(cb: (List<Library>) -> Unit) {
|
||||
val mapper = jacksonMapper
|
||||
getRequest("/api/libraries") {
|
||||
getRequest("/api/libraries", null,null) {
|
||||
val libraries = mutableListOf<Library>()
|
||||
if (it.has("value")) {
|
||||
val array = it.getJSONArray("value")
|
||||
@@ -122,7 +134,7 @@ class ApiHandler(var ctx:Context) {
|
||||
}
|
||||
|
||||
fun getLibraryItem(libraryItemId:String, cb: (LibraryItem) -> Unit) {
|
||||
getRequest("/api/items/$libraryItemId?expanded=1") {
|
||||
getRequest("/api/items/$libraryItemId?expanded=1", null, null) {
|
||||
val libraryItem = jacksonMapper.readValue<LibraryItem>(it.toString())
|
||||
cb(libraryItem)
|
||||
}
|
||||
@@ -131,14 +143,14 @@ class ApiHandler(var ctx:Context) {
|
||||
fun getLibraryItemWithProgress(libraryItemId:String, episodeId:String?, cb: (LibraryItem) -> Unit) {
|
||||
var requestUrl = "/api/items/$libraryItemId?expanded=1&include=progress"
|
||||
if (!episodeId.isNullOrEmpty()) requestUrl += "&episode=$episodeId"
|
||||
getRequest(requestUrl) {
|
||||
getRequest(requestUrl, null, null) {
|
||||
val libraryItem = jacksonMapper.readValue<LibraryItem>(it.toString())
|
||||
cb(libraryItem)
|
||||
}
|
||||
}
|
||||
|
||||
fun getLibraryItems(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
|
||||
getRequest("/api/libraries/$libraryId/items?limit=100&minified=1") {
|
||||
getRequest("/api/libraries/$libraryId/items?limit=100&minified=1", null, null) {
|
||||
val items = mutableListOf<LibraryItem>()
|
||||
if (it.has("results")) {
|
||||
val array = it.getJSONArray("results")
|
||||
@@ -152,7 +164,7 @@ class ApiHandler(var ctx:Context) {
|
||||
}
|
||||
|
||||
fun getLibraryCategories(libraryId:String, cb: (List<LibraryCategory>) -> Unit) {
|
||||
getRequest("/api/libraries/$libraryId/personalized") {
|
||||
getRequest("/api/libraries/$libraryId/personalized", null, null) {
|
||||
val items = mutableListOf<LibraryCategory>()
|
||||
if (it.has("value")) {
|
||||
val array = it.getJSONArray("value")
|
||||
@@ -172,13 +184,8 @@ class ApiHandler(var ctx:Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun playLibraryItem(libraryItemId:String, episodeId:String?, forceTranscode:Boolean, mediaPlayer:String, cb: (PlaybackSession) -> Unit) {
|
||||
val payload = JSObject()
|
||||
payload.put("mediaPlayer", mediaPlayer)
|
||||
|
||||
// Only if direct play fails do we force transcode
|
||||
if (!forceTranscode) payload.put("forceDirectPlay", true)
|
||||
else payload.put("forceTranscode", true)
|
||||
fun playLibraryItem(libraryItemId:String, episodeId:String?, playItemRequestPayload:PlayItemRequestPayload, cb: (PlaybackSession) -> Unit) {
|
||||
val payload = JSObject(jacksonMapper.writeValueAsString(playItemRequestPayload))
|
||||
|
||||
val endpoint = if (episodeId.isNullOrEmpty()) "/api/items/$libraryItemId/play" else "/api/items/$libraryItemId/play/$episodeId"
|
||||
postRequest(endpoint, payload) {
|
||||
@@ -189,11 +196,15 @@ class ApiHandler(var ctx:Context) {
|
||||
}
|
||||
}
|
||||
|
||||
fun sendProgressSync(sessionId:String, syncData: MediaProgressSyncData, cb: () -> Unit) {
|
||||
fun sendProgressSync(sessionId:String, syncData: MediaProgressSyncData, cb: (Boolean) -> Unit) {
|
||||
val payload = JSObject(jacksonMapper.writeValueAsString(syncData))
|
||||
|
||||
postRequest("/api/session/$sessionId/sync", payload) {
|
||||
cb()
|
||||
if (!it.getString("error").isNullOrEmpty()) {
|
||||
cb(false)
|
||||
} else {
|
||||
cb(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,4 +268,38 @@ class ApiHandler(var ctx:Context) {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
|
||||
fun getMediaProgress(libraryItemId:String, episodeId:String?, cb: (MediaProgress) -> Unit) {
|
||||
val endpoint = if(episodeId.isNullOrEmpty()) "/api/me/progress/$libraryItemId" else "/api/me/progress/$libraryItemId/$episodeId"
|
||||
getRequest(endpoint, null, null) {
|
||||
val progress = jacksonMapper.readValue<MediaProgress>(it.toString())
|
||||
cb(progress)
|
||||
}
|
||||
}
|
||||
|
||||
fun getPlaybackSession(playbackSessionId:String, cb: (PlaybackSession?) -> Unit) {
|
||||
val endpoint = "/api/session/$playbackSessionId"
|
||||
getRequest(endpoint, null, null) {
|
||||
val err = it.getString("error")
|
||||
if (!err.isNullOrEmpty()) {
|
||||
cb(null)
|
||||
} else {
|
||||
cb(jacksonMapper.readValue<PlaybackSession>(it.toString()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun pingServer(config:ServerConnectionConfig, cb: (Boolean) -> Unit) {
|
||||
Log.d(tag, "pingServer: Pinging ${config.address}")
|
||||
getRequest("/ping", pingClient, config) {
|
||||
val success = it.getString("success")
|
||||
if (success.isNullOrEmpty()) {
|
||||
Log.d(tag, "pingServer: Ping ${config.address} Failed")
|
||||
cb(false)
|
||||
} else {
|
||||
Log.d(tag, "pingServer: Ping ${config.address} Successful")
|
||||
cb(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,10 +74,11 @@
|
||||
</div>
|
||||
|
||||
<div id="playerTrack" class="absolute bottom-0 left-0 w-full px-3">
|
||||
<div ref="track" class="h-2 w-full bg-gray-500 bg-opacity-50 relative" :class="isLoading ? 'animate-pulse' : ''" @click="clickTrack">
|
||||
<div ref="track" class="h-2 w-full bg-gray-500 bg-opacity-50 relative" :class="isLoading ? 'animate-pulse' : ''" @touchstart="touchstartTrack" @click="clickTrack">
|
||||
<div ref="readyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none" />
|
||||
<div ref="bufferedTrack" class="h-full bg-gray-500 absolute top-0 left-0 pointer-events-none" />
|
||||
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
|
||||
<div ref="draggingTrack" class="h-full bg-warning bg-opacity-25 absolute top-0 left-0 pointer-events-none" />
|
||||
</div>
|
||||
<div id="timestamp-row" class="flex pt-0.5">
|
||||
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem" ref="currentTimestamp">0:00</p>
|
||||
@@ -132,7 +133,9 @@ export default {
|
||||
touchStartTime: 0,
|
||||
touchEndY: 0,
|
||||
useChapterTrack: false,
|
||||
isLoading: false
|
||||
isLoading: false,
|
||||
touchTrackStart: false,
|
||||
dragPercent: 0
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -268,6 +271,10 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
touchstartTrack(e) {
|
||||
if (!e || !e.touches || !this.$refs.track || !this.showFullscreen) return
|
||||
this.touchTrackStart = true
|
||||
},
|
||||
selectChapter(chapter) {
|
||||
this.seek(chapter.start)
|
||||
this.showChapterModal = false
|
||||
@@ -517,15 +524,60 @@ export default {
|
||||
this.touchStartTime = Date.now()
|
||||
},
|
||||
touchend(e) {
|
||||
if (!this.showFullscreen || !e.changedTouches) return
|
||||
if (!e.changedTouches) return
|
||||
|
||||
this.touchEndY = e.changedTouches[0].screenY
|
||||
var touchDuration = Date.now() - this.touchStartTime
|
||||
if (touchDuration > 1200) {
|
||||
// console.log('touch too long', touchDuration)
|
||||
return
|
||||
if (this.touchTrackStart) {
|
||||
var touch = e.changedTouches[0]
|
||||
const touchOnTrackPos = touch.pageX - 12
|
||||
const dragPercent = Math.max(0, Math.min(1, touchOnTrackPos / this.trackWidth))
|
||||
|
||||
var seekToTime = 0
|
||||
if (this.useChapterTrack && this.currentChapter) {
|
||||
const currChapTime = dragPercent * this.currentChapterDuration
|
||||
seekToTime = this.currentChapter.start + currChapTime
|
||||
} else {
|
||||
seekToTime = dragPercent * this.totalDuration
|
||||
}
|
||||
this.seek(seekToTime)
|
||||
|
||||
if (this.$refs.draggingTrack) {
|
||||
this.$refs.draggingTrack.style.width = '0px'
|
||||
}
|
||||
this.touchTrackStart = false
|
||||
} else if (this.showFullscreen) {
|
||||
this.touchEndY = e.changedTouches[0].screenY
|
||||
var touchDuration = Date.now() - this.touchStartTime
|
||||
if (touchDuration > 1200) {
|
||||
// console.log('touch too long', touchDuration)
|
||||
return
|
||||
}
|
||||
this.handleGesture()
|
||||
}
|
||||
},
|
||||
touchmove(e) {
|
||||
if (!this.touchTrackStart) return
|
||||
|
||||
var touch = e.touches[0]
|
||||
const touchOnTrackPos = touch.pageX - 12
|
||||
const dragPercent = Math.max(0, Math.min(1, touchOnTrackPos / this.trackWidth))
|
||||
this.dragPercent = dragPercent
|
||||
|
||||
if (this.$refs.draggingTrack) {
|
||||
this.$refs.draggingTrack.style.width = this.dragPercent * this.trackWidth + 'px'
|
||||
}
|
||||
|
||||
var ts = this.$refs.currentTimestamp
|
||||
if (ts) {
|
||||
var currTimeStr = ''
|
||||
if (this.useChapterTrack && this.currentChapter) {
|
||||
const currChapTime = dragPercent * this.currentChapterDuration
|
||||
currTimeStr = this.$secondsToTimestamp(currChapTime)
|
||||
} else {
|
||||
const dragTime = dragPercent * this.totalDuration
|
||||
currTimeStr = this.$secondsToTimestamp(dragTime)
|
||||
}
|
||||
ts.innerText = currTimeStr
|
||||
}
|
||||
this.handleGesture()
|
||||
},
|
||||
clickMenuAction(action) {
|
||||
if (action === 'chapter_track') {
|
||||
@@ -632,7 +684,7 @@ export default {
|
||||
mounted() {
|
||||
document.body.addEventListener('touchstart', this.touchstart)
|
||||
document.body.addEventListener('touchend', this.touchend)
|
||||
|
||||
document.body.addEventListener('touchmove', this.touchmove)
|
||||
this.$nextTick(this.init)
|
||||
},
|
||||
beforeDestroy() {
|
||||
@@ -644,6 +696,7 @@ export default {
|
||||
this.forceCloseDropdownMenu()
|
||||
document.body.removeEventListener('touchstart', this.touchstart)
|
||||
document.body.removeEventListener('touchend', this.touchend)
|
||||
document.body.removeEventListener('touchmove', this.touchmove)
|
||||
|
||||
if (this.onPlayingUpdateListener) this.onPlayingUpdateListener.remove()
|
||||
if (this.onMetadataListener) this.onMetadataListener.remove()
|
||||
|
||||
@@ -30,9 +30,11 @@ export default {
|
||||
onSleepTimerEndedListener: null,
|
||||
onSleepTimerSetListener: null,
|
||||
onMediaPlayerChangedListener: null,
|
||||
onProgressSyncFailing: null,
|
||||
sleepInterval: null,
|
||||
currentEndOfChapterTime: 0,
|
||||
serverLibraryItemId: null
|
||||
serverLibraryItemId: null,
|
||||
syncFailedToast: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -241,6 +243,10 @@ export default {
|
||||
onMediaPlayerChanged(data) {
|
||||
var mediaPlayer = data.value
|
||||
this.$store.commit('setMediaPlayer', mediaPlayer)
|
||||
},
|
||||
showProgressSyncIsFailing() {
|
||||
if (!isNaN(this.syncFailedToast)) this.$toast.dismiss(this.syncFailedToast)
|
||||
this.syncFailedToast = this.$toast('Progress is not being synced', { timeout: false, type: 'error' })
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
@@ -248,6 +254,7 @@ export default {
|
||||
this.onSleepTimerEndedListener = AbsAudioPlayer.addListener('onSleepTimerEnded', this.onSleepTimerEnded)
|
||||
this.onSleepTimerSetListener = AbsAudioPlayer.addListener('onSleepTimerSet', this.onSleepTimerSet)
|
||||
this.onMediaPlayerChangedListener = AbsAudioPlayer.addListener('onMediaPlayerChanged', this.onMediaPlayerChanged)
|
||||
this.onProgressSyncFailing = AbsAudioPlayer.addListener('onProgressSyncFailing', this.showProgressSyncIsFailing)
|
||||
|
||||
this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate')
|
||||
console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`)
|
||||
@@ -264,6 +271,7 @@ export default {
|
||||
if (this.onSleepTimerEndedListener) this.onSleepTimerEndedListener.remove()
|
||||
if (this.onSleepTimerSetListener) this.onSleepTimerSetListener.remove()
|
||||
if (this.onMediaPlayerChangedListener) this.onMediaPlayerChangedListener.remove()
|
||||
if (this.onProgressSyncFailing) this.onProgressSyncFailing.remove()
|
||||
|
||||
// if (this.$server.socket) {
|
||||
// this.$server.socket.off('stream_open', this.streamOpen)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div ref="container" class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20" style="max-height: 75%" @click.stop>
|
||||
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||
<template v-for="item in items">
|
||||
<li :key="item.value" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption(item.value)">
|
||||
<li :key="item.value" class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" :class="selected === item.value ? 'bg-success bg-opacity-10' : ''" role="option" @click="clickedOption(item.value)">
|
||||
<div class="relative flex items-center px-3">
|
||||
<p class="font-normal block truncate text-base text-white text-opacity-80">{{ item.text }}</p>
|
||||
</div>
|
||||
@@ -30,7 +30,8 @@ export default {
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
selected: String // optional
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
|
||||
@@ -166,7 +166,7 @@ export default {
|
||||
return this.filterData.narrators || []
|
||||
},
|
||||
progress() {
|
||||
return ['Finished', 'In Progress', 'Not Started']
|
||||
return ['Finished', 'In Progress', 'Not Started', 'Not Finished']
|
||||
},
|
||||
sublistItems() {
|
||||
return (this[this.sublist] || []).map((item) => {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<modals-modal v-model="show" :width="400" height="100%">
|
||||
<template #outer>
|
||||
<div class="absolute top-5 left-4 z-40">
|
||||
<p class="text-white text-2xl truncate">Details</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="w-full h-full overflow-hidden absolute top-0 left-0 flex items-center justify-center" @click="show = false">
|
||||
<div class="w-full overflow-x-hidden overflow-y-auto bg-primary rounded-lg border border-white border-opacity-20 p-2" style="max-height: 75%" @click.stop>
|
||||
<p class="mb-1">{{ mediaMetadata.title }}</p>
|
||||
<p class="mb-1 text-xs text-gray-200">ID: {{ _libraryItem.id }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</modals-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: Boolean,
|
||||
libraryItem: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('input', val)
|
||||
}
|
||||
},
|
||||
_libraryItem() {
|
||||
return this.libraryItem || {}
|
||||
},
|
||||
media() {
|
||||
return this._libraryItem.media || {}
|
||||
},
|
||||
mediaMetadata() {
|
||||
return this.media.metadata || {}
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -1,10 +1,28 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<p class="text-lg mb-1 font-semibold">Episodes ({{ episodes.length }})</p>
|
||||
<div class="flex items-center">
|
||||
<p class="text-lg mb-1 font-semibold">Episodes ({{ episodesFiltered.length }})</p>
|
||||
<div class="flex-grow" />
|
||||
<button class="outline:none mx-3 pt-0.5 relative" @click="showFilters">
|
||||
<span class="material-icons text-xl text-gray-200">filter_alt</span>
|
||||
<div v-show="filterKey !== 'all' && episodesAreFiltered" class="absolute top-0 right-0 w-1.5 h-1.5 rounded-full bg-success border border-green-300 shadow-sm z-10 pointer-events-none" />
|
||||
</button>
|
||||
|
||||
<template v-for="episode in episodes">
|
||||
<div class="flex items-center border border-white border-opacity-25 rounded px-2" @click="clickSort">
|
||||
<p class="text-sm text-gray-200">{{ sortText }}</p>
|
||||
<span class="material-icons ml-1 text-gray-200">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-for="episode in episodesSorted">
|
||||
<tables-podcast-episode-row :episode="episode" :local-episode="localEpisodeMap[episode.id]" :library-item-id="libraryItemId" :local-library-item-id="localLibraryItemId" :is-local="isLocal" :key="episode.id" />
|
||||
</template>
|
||||
|
||||
<!-- What in tarnation is going on here?
|
||||
Without anything below the template it will not re-render -->
|
||||
<p> </p>
|
||||
|
||||
<modals-dialog v-model="showFiltersModal" title="Episode Filter" :items="filterItems" :selected="filterKey" @action="setFilter" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -24,9 +42,86 @@ export default {
|
||||
isLocal: Boolean // If is local then episodes and libraryItemId are local, otherwise local is passed in localLibraryItemId and localEpisodes
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
return {
|
||||
episodesCopy: [],
|
||||
showFiltersModal: false,
|
||||
sortKey: 'publishedAt',
|
||||
sortDesc: false,
|
||||
filterKey: 'incomplete',
|
||||
episodeSortItems: [
|
||||
{
|
||||
text: 'Pub Date',
|
||||
value: 'publishedAt'
|
||||
},
|
||||
{
|
||||
text: 'Title',
|
||||
value: 'title'
|
||||
},
|
||||
{
|
||||
text: 'Season',
|
||||
value: 'season'
|
||||
},
|
||||
{
|
||||
text: 'Episode',
|
||||
value: 'episode'
|
||||
}
|
||||
],
|
||||
filterItems: [
|
||||
{
|
||||
text: 'Show All',
|
||||
value: 'all'
|
||||
},
|
||||
{
|
||||
text: 'Incomplete',
|
||||
value: 'incomplete'
|
||||
},
|
||||
{
|
||||
text: 'In Progress',
|
||||
value: 'inProgress'
|
||||
},
|
||||
{
|
||||
text: 'Complete',
|
||||
value: 'complete'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
episodes: {
|
||||
immediate: true,
|
||||
handler() {
|
||||
this.init()
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
episodesAreFiltered() {
|
||||
return this.episodesFiltered.length !== this.episodesCopy.length
|
||||
},
|
||||
episodesFiltered() {
|
||||
return this.episodesCopy.filter((ep) => {
|
||||
var mediaProgress = this.getEpisodeProgress(ep)
|
||||
if (this.filterKey === 'incomplete') {
|
||||
return !mediaProgress || !mediaProgress.isFinished
|
||||
} else if (this.filterKey === 'complete') {
|
||||
return mediaProgress && mediaProgress.isFinished
|
||||
} else if (this.filterKey === 'inProgress') {
|
||||
return mediaProgress && !mediaProgress.isFinished
|
||||
} else if (this.filterKey === 'all') {
|
||||
console.log('Filter key is all')
|
||||
return true
|
||||
}
|
||||
return true
|
||||
})
|
||||
},
|
||||
episodesSorted() {
|
||||
return this.episodesFiltered.sort((a, b) => {
|
||||
if (this.sortDesc) {
|
||||
return String(b[this.sortKey]).localeCompare(String(a[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
|
||||
}
|
||||
return String(a[this.sortKey]).localeCompare(String(b[this.sortKey]), undefined, { numeric: true, sensitivity: 'base' })
|
||||
})
|
||||
},
|
||||
// Map of local episodes where server episode id is key
|
||||
localEpisodeMap() {
|
||||
var epmap = {}
|
||||
@@ -36,9 +131,36 @@ export default {
|
||||
}
|
||||
})
|
||||
return epmap
|
||||
},
|
||||
sortText() {
|
||||
if (!this.sortKey) return ''
|
||||
var _sel = this.episodeSortItems.find((i) => i.value === this.sortKey)
|
||||
if (!_sel) return ''
|
||||
return _sel.text
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setFilter(filter) {
|
||||
this.filterKey = filter
|
||||
console.log('Set filter', this.filterKey)
|
||||
this.showFiltersModal = false
|
||||
},
|
||||
showFilters() {
|
||||
this.showFiltersModal = true
|
||||
},
|
||||
clickSort() {
|
||||
this.sortDesc = !this.sortDesc
|
||||
},
|
||||
getEpisodeProgress(episode) {
|
||||
if (this.isLocal) return this.$store.getters['globals/getLocalMediaProgressById'](this.libraryItemId, episode.id)
|
||||
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, episode.id)
|
||||
},
|
||||
init() {
|
||||
this.episodesCopy = this.episodes.map((ep) => {
|
||||
return { ...ep }
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {},
|
||||
mounted() {}
|
||||
}
|
||||
</script>
|
||||
@@ -475,12 +475,12 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 0.9.46;
|
||||
MARKETING_VERSION = 0.9.47;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
@@ -499,12 +499,12 @@
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = Icons;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 8;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
DEVELOPMENT_TEAM = 7UFJ7D8V6A;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 0.9.46;
|
||||
MARKETING_VERSION = 0.9.47;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
|
||||
+6
-6
@@ -9,12 +9,12 @@ install! 'cocoapods', :disable_input_output_paths => true
|
||||
def capacitor_pods
|
||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
||||
pod 'CapacitorDialog', :path => '../../node_modules/@capacitor/dialog'
|
||||
pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics'
|
||||
pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
|
||||
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
||||
pod 'CapacitorStorage', :path => '../../node_modules/@capacitor/storage'
|
||||
pod 'CapacitorApp', :path => '..\..\node_modules\@capacitor\app'
|
||||
pod 'CapacitorDialog', :path => '..\..\node_modules\@capacitor\dialog'
|
||||
pod 'CapacitorHaptics', :path => '..\..\node_modules\@capacitor\haptics'
|
||||
pod 'CapacitorNetwork', :path => '..\..\node_modules\@capacitor\network'
|
||||
pod 'CapacitorStatusBar', :path => '..\..\node_modules\@capacitor\status-bar'
|
||||
pod 'CapacitorStorage', :path => '..\..\node_modules\@capacitor\storage'
|
||||
end
|
||||
|
||||
target 'App' do
|
||||
|
||||
@@ -45,6 +45,7 @@ class AudioPlayer: NSObject {
|
||||
self.playWhenReady = playWhenReady
|
||||
self.initialPlaybackRate = playbackRate
|
||||
self.audioPlayer = AVQueuePlayer()
|
||||
self.audioPlayer.automaticallyWaitsToMinimizeStalling = false
|
||||
self.playbackSession = playbackSession
|
||||
self.status = -1
|
||||
self.rate = 0.0
|
||||
|
||||
@@ -17,7 +17,7 @@ class ApiClient {
|
||||
}).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: Any], decodable: T.Type = T.self, callback: ((_ param: T) -> Void)?) {
|
||||
if (Store.serverConfig == nil) {
|
||||
NSLog("Server config not set")
|
||||
return
|
||||
@@ -27,7 +27,7 @@ class ApiClient {
|
||||
"Authorization": "Bearer \(Store.serverConfig!.token)"
|
||||
]
|
||||
|
||||
AF.request("\(Store.serverConfig!.address)/\(endpoint)", method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, headers: headers).responseDecodable(of: decodable) { response in
|
||||
AF.request("\(Store.serverConfig!.address)/\(endpoint)", method: .post, parameters: parameters, encoding: JSONEncoding.default, headers: headers).responseDecodable(of: decodable) { response in
|
||||
switch response.result {
|
||||
case .success(let obj):
|
||||
callback?(obj)
|
||||
@@ -88,10 +88,23 @@ class ApiClient {
|
||||
endpoint += "/\(episodeId!)"
|
||||
}
|
||||
|
||||
var systemInfo = utsname()
|
||||
uname(&systemInfo)
|
||||
let modelCode = withUnsafePointer(to: &systemInfo.machine) {
|
||||
$0.withMemoryRebound(to: CChar.self, capacity: 1) {
|
||||
ptr in String.init(validatingUTF8: ptr)
|
||||
}
|
||||
}
|
||||
|
||||
ApiClient.postResource(endpoint: endpoint, parameters: [
|
||||
"forceDirectPlay": !forceTranscode ? "1" : "",
|
||||
"forceTranscode": forceTranscode ? "1" : "",
|
||||
"mediaPlayer": "AVPlayer",
|
||||
"deviceInfo": [
|
||||
"manufacturer": "Apple",
|
||||
"model": modelCode,
|
||||
"clientVersion": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
||||
]
|
||||
], decodable: PlaybackSession.self) { obj in
|
||||
var session = obj
|
||||
|
||||
|
||||
+52
-1
@@ -197,16 +197,66 @@ export default {
|
||||
console.log('[default] syncLocalMediaProgress No local media progress to sync')
|
||||
}
|
||||
},
|
||||
userUpdated(user) {
|
||||
async userUpdated(user) {
|
||||
if (this.user && this.user.id == user.id) {
|
||||
this.$store.commit('user/setUser', user)
|
||||
}
|
||||
},
|
||||
async userMediaProgressUpdated(prog) {
|
||||
console.log(`[default] userMediaProgressUpdate checking for local media progress ${prog.id}`)
|
||||
|
||||
// Update local media progress if exists
|
||||
var localProg = this.$store.getters['globals/getLocalMediaProgressByServerItemId'](prog.libraryItemId, prog.episodeId)
|
||||
var newLocalMediaProgress = null
|
||||
if (localProg && localProg.lastUpdate < prog.lastUpdate) {
|
||||
// Server progress is more up-to-date
|
||||
console.log(`[default] syncing progress from server with local item for "${prog.libraryItemId}" ${prog.episodeId ? `episode ${prog.episodeId}` : ''}`)
|
||||
const payload = {
|
||||
localMediaProgressId: localProg.id,
|
||||
mediaProgress: prog
|
||||
}
|
||||
newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress(payload)
|
||||
} else {
|
||||
// Check if local library item exists
|
||||
var localLibraryItem = await this.$db.getLocalLibraryItemByLLId(prog.libraryItemId)
|
||||
if (localLibraryItem) {
|
||||
if (prog.episodeId) {
|
||||
// If episode check if local episode exists
|
||||
var lliEpisodes = localLibraryItem.media.episodes || []
|
||||
var localEpisode = lliEpisodes.find((ep) => ep.serverEpisodeId === prog.episodeId)
|
||||
if (localEpisode) {
|
||||
// Add new local media progress
|
||||
const payload = {
|
||||
localLibraryItemId: localLibraryItem.id,
|
||||
localEpisodeId: localEpisode.id,
|
||||
mediaProgress: prog
|
||||
}
|
||||
newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress(payload)
|
||||
}
|
||||
} else {
|
||||
// Add new local media progress
|
||||
const payload = {
|
||||
localLibraryItemId: localLibraryItem.id,
|
||||
mediaProgress: prog
|
||||
}
|
||||
newLocalMediaProgress = await this.$db.syncServerMediaProgressWithLocalMediaProgress(payload)
|
||||
}
|
||||
} else {
|
||||
console.log(`[default] userMediaProgressUpdate no local media progress or lli found for this server item ${prog.id}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (newLocalMediaProgress && newLocalMediaProgress.id) {
|
||||
console.log(`[default] local media progress updated for ${newLocalMediaProgress.id}`)
|
||||
this.$store.commit('globals/updateLocalMediaProgress', newLocalMediaProgress)
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
this.$socket.on('connection-update', this.socketConnectionUpdate)
|
||||
this.$socket.on('initialized', this.socketInit)
|
||||
this.$socket.on('user_updated', this.userUpdated)
|
||||
this.$socket.on('user_media_progress_updated', this.userMediaProgressUpdated)
|
||||
|
||||
if (this.$store.state.isFirstLoad) {
|
||||
this.$store.commit('setIsFirstLoad', false)
|
||||
@@ -231,6 +281,7 @@ export default {
|
||||
this.$socket.off('connection-update', this.socketConnectionUpdate)
|
||||
this.$socket.off('initialized', this.socketInit)
|
||||
this.$socket.off('user_updated', this.userUpdated)
|
||||
this.$socket.off('user_media_progress_updated', this.userMediaProgressUpdated)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-app",
|
||||
"version": "0.9.46-beta",
|
||||
"version": "0.9.48-beta",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "audiobookshelf-app",
|
||||
"version": "0.9.46-beta",
|
||||
"version": "0.9.48-beta",
|
||||
"author": "advplyr",
|
||||
"scripts": {
|
||||
"dev": "nuxt --hostname 0.0.0.0 --port 1337",
|
||||
|
||||
+160
-56
@@ -1,60 +1,100 @@
|
||||
<template>
|
||||
<div class="w-full h-full px-3 py-4 overflow-y-auto">
|
||||
<div class="flex">
|
||||
<div class="w-32">
|
||||
<div class="w-16">
|
||||
<div class="relative">
|
||||
<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>
|
||||
<covers-book-cover :library-item="libraryItem" :width="64" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
||||
<div v-if="!isPodcast" class="absolute bottom-0 left-0 h-1 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 64 * progressPercent + 'px' }"></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 class="flex-grow px-3">
|
||||
<h1 class="text-lg">{{ title }}</h1>
|
||||
<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 v-if="numTracks" class="text-gray-300 text-sm my-1">
|
||||
{{ $elapsedPretty(duration) }}
|
||||
<span v-if="!isLocal" class="px-4">{{ $bytesPretty(size) }}</span>
|
||||
<div class="title-container flex-grow pl-2">
|
||||
<div class="flex relative pr-6">
|
||||
<h1 class="text-base">{{ title }}</h1>
|
||||
|
||||
<button class="absolute top-0 right-0 h-full px-1 outline-none" @click="moreButtonPress">
|
||||
<span class="material-icons text-xl">more_vert</span>
|
||||
</button>
|
||||
</div>
|
||||
<p v-if="seriesList && seriesList.length" class="text-sm text-gray-300 py-0.5">
|
||||
<template v-for="(series, index) in seriesList"
|
||||
><nuxt-link :key="series.id" :to="`/bookshelf/series/${series.id}`">{{ series.text }}</nuxt-link
|
||||
><span :key="`${series.id}-comma`" v-if="index < seriesList.length - 1">, </span></template
|
||||
>
|
||||
</p>
|
||||
<p v-if="numTracks" class="text-gray-300 text-sm my-1">{{ numTracks }} Tracks</p>
|
||||
<p v-if="podcastAuthor" class="text-sm text-gray-400 py-0.5">By {{ author }}</p>
|
||||
<p v-else-if="bookAuthors && bookAuthors.length" class="text-sm text-gray-400 py-0.5">
|
||||
By
|
||||
<template v-for="(author, index) in bookAuthors"
|
||||
><nuxt-link :key="author.id" :to="`/bookshelf/library?filter=authors.${$encode(author.id)}`">{{ author.name }}</nuxt-link
|
||||
><span :key="`${author.id}-comma`" v-if="index < bookAuthors.length - 1">, </span></template
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 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">
|
||||
<span class="material-icons text-sm">close</span>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="narrators && narrators.length" class="text-sm text-gray-400 py-0.5">
|
||||
Narrated By
|
||||
<template v-for="(narrator, index) in narrators"
|
||||
><nuxt-link :key="narrator" :to="`/bookshelf/library?filter=narrators.${$encode(narrator)}`">{{ narrator }}</nuxt-link
|
||||
><span :key="`${narrator}-comma`" v-if="index < narrators.length - 1">, </span></template
|
||||
>
|
||||
</p>
|
||||
|
||||
<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">
|
||||
<span v-show="!isPlaying" class="material-icons">play_arrow</span>
|
||||
<span class="px-1 text-sm">{{ isPlaying ? 'Playing' : 'Play' }}</span>
|
||||
</ui-btn>
|
||||
<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 v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
|
||||
</ui-btn>
|
||||
<ui-read-icon-btn v-if="!isPodcast" :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="flex items-center justify-center" @click="toggleFinished" />
|
||||
</div>
|
||||
<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">
|
||||
<span v-show="!isPlaying" class="material-icons">play_arrow</span>
|
||||
<span class="px-1 text-sm">{{ isPlaying ? (isStreaming ? 'Streaming' : 'Playing') : hasLocal ? 'Play' : 'Stream' }}</span>
|
||||
</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">
|
||||
<span class="material-icons">auto_stories</span>
|
||||
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
|
||||
</ui-btn>
|
||||
<ui-btn v-if="showDownload" :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>
|
||||
</ui-btn>
|
||||
<ui-read-icon-btn v-if="!isPodcast" :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="flex items-center justify-center" @click="toggleFinished" />
|
||||
<!-- 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 v-if="numTracks" class="flex text-gray-100 text-xs my-2 -mx-0.5">
|
||||
<div class="bg-primary bg-opacity-80 px-3 py-0.5 rounded-full mx-0.5">
|
||||
<p>{{ $elapsedPretty(duration) }}</p>
|
||||
</div>
|
||||
<!-- TODO: Local books dont save the size -->
|
||||
<div v-if="size" class="bg-primary bg-opacity-80 px-3 py-0.5 rounded-full mx-0.5">
|
||||
<p>{{ $bytesPretty(size) }}</p>
|
||||
</div>
|
||||
<div class="bg-primary bg-opacity-80 px-3 py-0.5 rounded-full mx-0.5">
|
||||
<p>{{ numTracks }} Track{{ numTracks > 1 ? 's' : '' }}</p>
|
||||
</div>
|
||||
<div v-if="numChapters" class="bg-primary bg-opacity-80 px-3 py-0.5 rounded-full mx-0.5">
|
||||
<p>{{ numChapters }} Chapter{{ numChapters > 1 ? 's' : '' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<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 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">
|
||||
<span class="material-icons text-sm">close</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<span v-show="!isPlaying" class="material-icons">play_arrow</span>
|
||||
<span class="px-1 text-sm">{{ isPlaying ? 'Playing' : 'Play' }}</span>
|
||||
</ui-btn>
|
||||
<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 v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
|
||||
</ui-btn>
|
||||
<ui-read-icon-btn v-if="!isPodcast" :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="flex items-center justify-center" @click="toggleFinished" />
|
||||
</div>
|
||||
<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">
|
||||
<span v-show="!isPlaying" class="material-icons">play_arrow</span>
|
||||
<span class="px-1 text-sm">{{ isPlaying ? (isStreaming ? 'Streaming' : 'Playing') : hasLocal ? 'Play' : 'Stream' }}</span>
|
||||
</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">
|
||||
<span class="material-icons">auto_stories</span>
|
||||
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
|
||||
</ui-btn>
|
||||
<ui-btn v-if="showDownload" :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>
|
||||
</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-if="downloadItem" class="py-3">
|
||||
@@ -68,6 +108,10 @@
|
||||
<tables-podcast-episodes-table v-if="isPodcast" :library-item-id="libraryItemId" :local-library-item-id="localLibraryItemId" :episodes="episodes" :local-episodes="localLibraryItemEpisodes" :is-local="isLocal" />
|
||||
|
||||
<modals-select-local-folder-modal v-model="showSelectLocalFolder" :media-type="mediaType" @select="selectedLocalFolder" />
|
||||
|
||||
<modals-dialog v-model="showMoreMenu" title="" :items="moreMenuItems" @action="moreMenuAction" />
|
||||
|
||||
<modals-item-details-modal v-model="showDetailsModal" :library-item="libraryItem" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -82,7 +126,7 @@ export default {
|
||||
console.log(libraryItemId)
|
||||
if (libraryItemId.startsWith('local')) {
|
||||
libraryItem = await app.$db.getLocalLibraryItem(libraryItemId)
|
||||
console.log('Got lli', libraryItem)
|
||||
console.log('Got lli', libraryItemId)
|
||||
} else if (store.state.user.serverConnectionConfig) {
|
||||
libraryItem = await app.$axios.$get(`/api/items/${libraryItemId}?expanded=1`).catch((error) => {
|
||||
console.error('Failed', error)
|
||||
@@ -110,7 +154,9 @@ export default {
|
||||
return {
|
||||
resettingProgress: false,
|
||||
isProcessingReadUpdate: false,
|
||||
showSelectLocalFolder: false
|
||||
showSelectLocalFolder: false,
|
||||
showMoreMenu: false,
|
||||
showDetailsModal: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -169,9 +215,17 @@ export default {
|
||||
title() {
|
||||
return this.mediaMetadata.title
|
||||
},
|
||||
author() {
|
||||
if (this.isPodcast) return this.mediaMetadata.author
|
||||
return this.mediaMetadata.authorName
|
||||
podcastAuthor() {
|
||||
if (!this.isPodcast) return null
|
||||
return this.mediaMetadata.author || ''
|
||||
},
|
||||
bookAuthors() {
|
||||
if (this.isPodcast) return null
|
||||
return this.mediaMetadata.authors || []
|
||||
},
|
||||
narrators() {
|
||||
if (this.isPodcast) return null
|
||||
return this.mediaMetadata.narrators || []
|
||||
},
|
||||
description() {
|
||||
return this.mediaMetadata.description || ''
|
||||
@@ -179,9 +233,16 @@ export default {
|
||||
series() {
|
||||
return this.mediaMetadata.series || []
|
||||
},
|
||||
seriesName() {
|
||||
// For books only on toJSONExpanded
|
||||
return this.mediaMetadata.seriesName || ''
|
||||
seriesList() {
|
||||
if (this.isPodcast) return null
|
||||
return this.series.map((se) => {
|
||||
var text = se.name
|
||||
if (se.sequence) text += ` #${se.sequence}`
|
||||
return {
|
||||
...se,
|
||||
text
|
||||
}
|
||||
})
|
||||
},
|
||||
duration() {
|
||||
return this.media.duration
|
||||
@@ -226,6 +287,10 @@ export default {
|
||||
if (!this.media.tracks) return 0
|
||||
return this.media.tracks.length || 0
|
||||
},
|
||||
numChapters() {
|
||||
if (!this.media.chapters) return 0
|
||||
return this.media.chapters.length || 0
|
||||
},
|
||||
isMissing() {
|
||||
return this.libraryItem.isMissing
|
||||
},
|
||||
@@ -257,9 +322,41 @@ export default {
|
||||
},
|
||||
isCasting() {
|
||||
return this.$store.state.isCasting
|
||||
},
|
||||
moreMenuItems() {
|
||||
if (this.isLocal) {
|
||||
return [
|
||||
{
|
||||
text: 'Manage Local Files',
|
||||
value: 'manageLocal'
|
||||
},
|
||||
{
|
||||
text: 'View Details',
|
||||
value: 'details'
|
||||
}
|
||||
]
|
||||
} else {
|
||||
return [
|
||||
{
|
||||
text: 'View Details',
|
||||
value: 'details'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
moreMenuAction(action) {
|
||||
this.showMoreMenu = false
|
||||
if (action === 'manageLocal') {
|
||||
this.$router.push(`/localMedia/item/${this.libraryItemId}`)
|
||||
} else if (action === 'details') {
|
||||
this.showDetailsModal = true
|
||||
}
|
||||
},
|
||||
moreButtonPress() {
|
||||
this.showMoreMenu = true
|
||||
},
|
||||
readBook() {
|
||||
this.$store.commit('openReader', this.libraryItem)
|
||||
},
|
||||
@@ -283,7 +380,7 @@ export default {
|
||||
if (this.isLocal) {
|
||||
// TODO: If connected to server also sync with server
|
||||
await this.$db.removeLocalMediaProgress(this.libraryItemId)
|
||||
this.$store.commit('globals/removeLocalMediaProgress', this.libraryItemId)
|
||||
this.$store.commit('globals/removeLocalMediaProgressForItem', this.libraryItemId)
|
||||
} else {
|
||||
var progressId = this.userItemProgress.id
|
||||
await this.$axios
|
||||
@@ -446,4 +543,11 @@ export default {
|
||||
this.$socket.off('item_updated', this.itemUpdated)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.title-container {
|
||||
width: calc(100% - 64px);
|
||||
max-width: calc(100% - 64px);
|
||||
}
|
||||
</style>
|
||||
@@ -189,7 +189,7 @@ export default {
|
||||
if (this.selectedAudioTrack || this.selectedEpisode) {
|
||||
return [
|
||||
{
|
||||
text: 'Hard Delete',
|
||||
text: 'Remove & Delete Files',
|
||||
value: 'track-delete'
|
||||
}
|
||||
]
|
||||
@@ -208,7 +208,7 @@ export default {
|
||||
value: 'remove'
|
||||
},
|
||||
{
|
||||
text: 'Hard Delete',
|
||||
text: 'Remove & Delete Files',
|
||||
value: 'delete'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -187,6 +187,7 @@ class AbsAudioPlayerWeb extends WebPlugin {
|
||||
return
|
||||
}
|
||||
this.player.currentTime = this.trackStartTime
|
||||
|
||||
this.sendPlaybackMetadata(PlayerState.READY)
|
||||
if (this.playWhenReady) {
|
||||
this.player.play()
|
||||
@@ -195,10 +196,9 @@ class AbsAudioPlayerWeb extends WebPlugin {
|
||||
evtTimeupdate() { }
|
||||
|
||||
sendPlaybackMetadata(playerState) {
|
||||
var currentTime = this.player ? this.player.currentTime || 0 : 0
|
||||
this.notifyListeners('onMetadata', {
|
||||
duration: this.totalDuration,
|
||||
currentTime,
|
||||
currentTime: this.overallCurrentTime,
|
||||
playerState
|
||||
})
|
||||
}
|
||||
|
||||
@@ -195,6 +195,10 @@ class AbsDatabaseWeb extends WebPlugin {
|
||||
return null
|
||||
}
|
||||
|
||||
async syncServerMediaProgressWithLocalMediaProgress(payload) {
|
||||
return null
|
||||
}
|
||||
|
||||
async updateLocalTrackOrder({ localLibraryItemId, tracks }) {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -70,6 +70,10 @@ class DbService {
|
||||
return AbsDatabase.syncLocalMediaProgressWithServer()
|
||||
}
|
||||
|
||||
syncServerMediaProgressWithLocalMediaProgress(payload) {
|
||||
return AbsDatabase.syncServerMediaProgressWithLocalMediaProgress(payload)
|
||||
}
|
||||
|
||||
updateLocalTrackOrder(payload) {
|
||||
return AbsDatabase.updateLocalTrackOrder(payload)
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ class ServerSocket extends EventEmitter {
|
||||
console.log('[SOCKET] User Item Progress Updated', JSON.stringify(data))
|
||||
var progress = data.data
|
||||
this.$store.commit('user/updateUserMediaProgress', progress)
|
||||
this.emit('user_media_progress_updated', progress)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,12 @@ export const getters = {
|
||||
if (episodeId != null && lmp.localEpisodeId != episodeId) return false
|
||||
return lmp.localLibraryItemId == localLibraryItemId
|
||||
})
|
||||
},
|
||||
getLocalMediaProgressByServerItemId: (state) => (libraryItemId, episodeId = null) => {
|
||||
return state.localMediaProgress.find(lmp => {
|
||||
if (episodeId != null && lmp.episodeId != episodeId) return false
|
||||
return lmp.libraryItemId == libraryItemId
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +90,9 @@ export const mutations = {
|
||||
removeLocalMediaProgress(state, id) {
|
||||
state.localMediaProgress = state.localMediaProgress.filter(lmp => lmp.id != id)
|
||||
},
|
||||
removeLocalMediaProgressForItem(state, llid) {
|
||||
state.localMediaProgress = state.localMediaProgress.filter(lmp => lmp.localLibraryItemId !== llid)
|
||||
},
|
||||
setLastSearch(state, val) {
|
||||
state.lastSearch = val
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user