Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bad8d10a18 | ||
|
|
a4412da3ed | ||
|
|
870774b408 | ||
|
|
d7dcaa22a6 | ||
|
|
cc0b2943dd | ||
|
|
50ee0b2265 | ||
|
|
910b3a2a17 | ||
|
|
916da91ccb | ||
|
|
d70a99254a | ||
|
|
dcf5bb61a2 | ||
|
|
aa65bb10c1 | ||
|
|
158448f8a2 | ||
|
|
f4be9b3e26 | ||
|
|
15d68ca285 | ||
|
|
ad12e6a19d | ||
|
|
1f1b2fe85a | ||
|
|
480df58ce4 | ||
|
|
2decf532b2 | ||
|
|
3ba87419ae | ||
|
|
1c78af37fa | ||
|
|
2b5373aedd | ||
|
|
99bf960b8a | ||
|
|
c4aca22c28 | ||
|
|
58bd0e0cee | ||
|
|
c1c56f8f52 | ||
|
|
f930ba1941 | ||
|
|
251116a5ce | ||
|
|
06739c0401 | ||
|
|
92f069d1e6 | ||
|
|
62c5042ecc | ||
|
|
604b086c0b | ||
|
|
0e0b356f6b | ||
|
|
573768e2b2 | ||
|
|
236fd09c94 | ||
|
|
cb6cb5f637 | ||
|
|
eb916a3ab0 | ||
|
|
61b8ca1510 | ||
|
|
5d2da97dc5 | ||
|
|
d626686614 | ||
|
|
60ee33cb72 | ||
|
|
8d2498e96d | ||
|
|
1794484bed | ||
|
|
f2b6331843 | ||
|
|
8d5f33245f | ||
|
|
1ee544b842 | ||
|
|
0fd9463c7c | ||
|
|
ad5edf3aee | ||
|
|
d5fafd8cab | ||
|
|
92256f2fed | ||
|
|
5fa8d4c989 | ||
|
|
6b75f79f00 | ||
|
|
b0c9f29d90 | ||
|
|
7721afc116 | ||
|
|
bc85e9bbfa | ||
|
|
e8b6602fe5 |
@@ -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 73
|
versionCode 80
|
||||||
versionName "0.9.44-beta"
|
versionName "0.9.49-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.
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ dependencies {
|
|||||||
implementation project(':capacitor-network')
|
implementation project(':capacitor-network')
|
||||||
implementation project(':capacitor-status-bar')
|
implementation project(':capacitor-status-bar')
|
||||||
implementation project(':capacitor-storage')
|
implementation project(':capacitor-storage')
|
||||||
implementation project(':robingenz-capacitor-app-update')
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:dist="http://schemas.android.com/apk/distribution"
|
xmlns:dist="http://schemas.android.com/apk/distribution"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:installLocation="preferExternal"
|
||||||
package="com.audiobookshelf.app">
|
package="com.audiobookshelf.app">
|
||||||
|
|
||||||
<!-- Permissions -->
|
<!-- Permissions -->
|
||||||
|
|||||||
@@ -22,9 +22,5 @@
|
|||||||
{
|
{
|
||||||
"pkg": "@capacitor/storage",
|
"pkg": "@capacitor/storage",
|
||||||
"classpath": "com.capacitorjs.plugins.storage.StoragePlugin"
|
"classpath": "com.capacitorjs.plugins.storage.StoragePlugin"
|
||||||
},
|
|
||||||
{
|
|
||||||
"pkg": "@robingenz/capacitor-app-update",
|
|
||||||
"classpath": "dev.robingenz.capacitor.appupdate.AppUpdatePlugin"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -42,6 +42,15 @@ data class LibraryItem(
|
|||||||
return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}")
|
return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun checkHasTracks():Boolean {
|
||||||
|
return if (mediaType == "podcast") {
|
||||||
|
((media as Podcast).numEpisodes ?: 0) > 0
|
||||||
|
} else {
|
||||||
|
((media as Book).numTracks ?: 0) > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getMediaMetadata(): MediaMetadataCompat {
|
fun getMediaMetadata(): MediaMetadataCompat {
|
||||||
return MediaMetadataCompat.Builder().apply {
|
return MediaMetadataCompat.Builder().apply {
|
||||||
@@ -74,6 +83,7 @@ open class MediaType(var metadata:MediaTypeMetadata, var coverPath:String?) {
|
|||||||
open fun removeAudioTrack(localFileId:String) { }
|
open fun removeAudioTrack(localFileId:String) { }
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
open fun getLocalCopy():MediaType { return MediaType(MediaTypeMetadata(""),null) }
|
open fun getLocalCopy():MediaType { return MediaType(MediaTypeMetadata(""),null) }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
@@ -82,7 +92,8 @@ class Podcast(
|
|||||||
coverPath:String?,
|
coverPath:String?,
|
||||||
var tags:MutableList<String>,
|
var tags:MutableList<String>,
|
||||||
var episodes:MutableList<PodcastEpisode>?,
|
var episodes:MutableList<PodcastEpisode>?,
|
||||||
var autoDownloadEpisodes:Boolean
|
var autoDownloadEpisodes:Boolean,
|
||||||
|
var numEpisodes:Int?
|
||||||
) : MediaType(metadata, coverPath) {
|
) : MediaType(metadata, coverPath) {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getAudioTracks():List<AudioTrack> {
|
override fun getAudioTracks():List<AudioTrack> {
|
||||||
@@ -99,7 +110,7 @@ class Podcast(
|
|||||||
// Add new episodes
|
// Add new episodes
|
||||||
audioTracks.forEach { at ->
|
audioTracks.forEach { at ->
|
||||||
if (episodes?.find{ it.audioTrack?.localFileId == at.localFileId } == null) {
|
if (episodes?.find{ it.audioTrack?.localFileId == at.localFileId } == null) {
|
||||||
val newEpisode = PodcastEpisode("local_" + at.localFileId,episodes?.size ?: 0 + 1,null,null,at.title,null,null,null,at,at.duration,0, null)
|
val newEpisode = PodcastEpisode("local_ep_" + at.localFileId,episodes?.size ?: 0 + 1,null,null,at.title,null,null,null,at,at.duration,0, null)
|
||||||
episodes?.add(newEpisode)
|
episodes?.add(newEpisode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -147,7 +158,7 @@ class Podcast(
|
|||||||
// Used for FolderScanner local podcast item to get copy of Podcast excluding episodes
|
// Used for FolderScanner local podcast item to get copy of Podcast excluding episodes
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getLocalCopy(): Podcast {
|
override fun getLocalCopy(): Podcast {
|
||||||
return Podcast(metadata as PodcastMetadata,coverPath,tags, mutableListOf(),autoDownloadEpisodes)
|
return Podcast(metadata as PodcastMetadata,coverPath,tags, mutableListOf(),autoDownloadEpisodes, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,7 +171,8 @@ class Book(
|
|||||||
var chapters:List<BookChapter>?,
|
var chapters:List<BookChapter>?,
|
||||||
var tracks:MutableList<AudioTrack>?,
|
var tracks:MutableList<AudioTrack>?,
|
||||||
var size:Long?,
|
var size:Long?,
|
||||||
var duration:Double?
|
var duration:Double?,
|
||||||
|
var numTracks:Int?
|
||||||
) : MediaType(metadata, coverPath) {
|
) : MediaType(metadata, coverPath) {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getAudioTracks():List<AudioTrack> {
|
override fun getAudioTracks():List<AudioTrack> {
|
||||||
@@ -209,7 +221,7 @@ class Book(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getLocalCopy(): Book {
|
override fun getLocalCopy(): Book {
|
||||||
return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(),null,null)
|
return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(),null,null, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,7 +293,29 @@ data class PodcastEpisode(
|
|||||||
var duration:Double?,
|
var duration:Double?,
|
||||||
var size:Long?,
|
var size:Long?,
|
||||||
var serverEpisodeId:String? // For local podcasts to match with server podcasts
|
var serverEpisodeId:String? // For local podcasts to match with server podcasts
|
||||||
)
|
) {
|
||||||
|
@JsonIgnore
|
||||||
|
fun getMediaMetadata(libraryItem:LibraryItemWrapper): MediaMetadataCompat {
|
||||||
|
var coverUri:Uri = Uri.EMPTY
|
||||||
|
val podcast = if(libraryItem is LocalLibraryItem) {
|
||||||
|
coverUri = libraryItem.getCoverUri()
|
||||||
|
libraryItem.media as Podcast
|
||||||
|
} else {
|
||||||
|
coverUri = (libraryItem as LibraryItem).getCoverUri()
|
||||||
|
(libraryItem as LibraryItem).media as Podcast
|
||||||
|
}
|
||||||
|
|
||||||
|
return MediaMetadataCompat.Builder().apply {
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, podcast.metadata.getAuthorDisplayName())
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, podcast.metadata.getAuthorDisplayName())
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString())
|
||||||
|
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class LibraryFile(
|
data class LibraryFile(
|
||||||
@@ -312,7 +346,16 @@ data class Library(
|
|||||||
var folders:MutableList<Folder>,
|
var folders:MutableList<Folder>,
|
||||||
var icon:String,
|
var icon:String,
|
||||||
var mediaType:String
|
var mediaType:String
|
||||||
)
|
) {
|
||||||
|
@JsonIgnore
|
||||||
|
fun getMediaMetadata(): MediaMetadataCompat {
|
||||||
|
return MediaMetadataCompat.Builder().apply {
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, name)
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_TITLE, name)
|
||||||
|
}.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class Folder(
|
data class Folder(
|
||||||
@@ -371,3 +414,9 @@ data class MediaProgress(
|
|||||||
var startedAt:Long,
|
var startedAt:Long,
|
||||||
var finishedAt:Long?
|
var finishedAt:Long?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Helper class
|
||||||
|
data class LibraryItemWithEpisode(
|
||||||
|
var libraryItemWrapper:LibraryItemWrapper,
|
||||||
|
var episode:PodcastEpisode
|
||||||
|
)
|
||||||
|
|||||||
@@ -42,6 +42,20 @@ class DbManager {
|
|||||||
return Paper.book("localLibraryItems").read(localLibraryItemId)
|
return Paper.book("localLibraryItems").read(localLibraryItemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getLocalLibraryItemWithEpisode(podcastEpisodeId:String):LibraryItemWithEpisode? {
|
||||||
|
var podcastEpisode:PodcastEpisode? = null
|
||||||
|
val localLibraryItem = getLocalLibraryItems("podcast").find { localLibraryItem ->
|
||||||
|
val podcast = localLibraryItem.media as Podcast
|
||||||
|
podcastEpisode = podcast.episodes?.find { it.id == podcastEpisodeId }
|
||||||
|
podcastEpisode != null
|
||||||
|
}
|
||||||
|
return if (localLibraryItem != null) {
|
||||||
|
LibraryItemWithEpisode(localLibraryItem, podcastEpisode!!)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun removeLocalLibraryItem(localLibraryItemId:String) {
|
fun removeLocalLibraryItem(localLibraryItemId:String) {
|
||||||
Paper.book("localLibraryItems").delete(localLibraryItemId)
|
Paper.book("localLibraryItems").delete(localLibraryItemId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ data class LocalFile(
|
|||||||
) {
|
) {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun isAudioFile():Boolean {
|
fun isAudioFile():Boolean {
|
||||||
|
if (mimeType == "application/octet-stream") return true
|
||||||
|
if (mimeType == "video/mp4") return true
|
||||||
return mimeType?.startsWith("audio") == true
|
return mimeType?.startsWith("audio") == true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,3 +66,20 @@ data class LocalFolder(
|
|||||||
JsonSubTypes.Type(LocalLibraryItem::class)
|
JsonSubTypes.Type(LocalLibraryItem::class)
|
||||||
)
|
)
|
||||||
open class LibraryItemWrapper()
|
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,5 +1,6 @@
|
|||||||
package com.audiobookshelf.app.data
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -44,7 +45,7 @@ data class LocalLibraryItem(
|
|||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getDuration():Double {
|
fun getDuration():Double {
|
||||||
var total = 0.0
|
var total = 0.0
|
||||||
var audioTracks = media.getAudioTracks()
|
val audioTracks = media.getAudioTracks()
|
||||||
audioTracks.forEach{ total += it.duration }
|
audioTracks.forEach{ total += it.duration }
|
||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
@@ -94,15 +95,17 @@ data class LocalLibraryItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getMediaMetadata(): MediaMetadataCompat {
|
fun getMediaMetadata(ctx: Context): MediaMetadataCompat {
|
||||||
|
val coverUri = getCoverUri()
|
||||||
|
|
||||||
return MediaMetadataCompat.Builder().apply {
|
return MediaMetadataCompat.Builder().apply {
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
|
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, authorName)
|
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, authorName)
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, getCoverUri().toString())
|
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString())
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getCoverUri().toString())
|
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, coverUri.toString())
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ART_URI, getCoverUri().toString())
|
putString(MediaMetadataCompat.METADATA_KEY_ART_URI, coverUri.toString())
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, authorName)
|
putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, authorName)
|
||||||
}.build()
|
}.build()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,13 +58,13 @@ data class LocalMediaItem(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getLocalLibraryItem():LocalLibraryItem {
|
fun getLocalLibraryItem():LocalLibraryItem {
|
||||||
var mediaMetadata = getMediaMetadata()
|
val mediaMetadata = getMediaMetadata()
|
||||||
if (mediaType == "book") {
|
if (mediaType == "book") {
|
||||||
var chapters = getAudiobookChapters()
|
val chapters = getAudiobookChapters()
|
||||||
var book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration())
|
val book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration(),audioTracks.size)
|
||||||
return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true,null,null,null,null)
|
return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true,null,null,null,null)
|
||||||
} else {
|
} else {
|
||||||
var podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false)
|
val podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false, 0)
|
||||||
podcast.setAudioTracks(audioTracks) // Builds episodes from audio tracks
|
podcast.setAudioTracks(audioTracks) // Builds episodes from audio tracks
|
||||||
return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false, mediaType, podcast,localFiles,coverContentUrl, coverAbsolutePath, true, null,null,null,null)
|
return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false, mediaType, podcast,localFiles,coverContentUrl, coverAbsolutePath, true, null,null,null,null)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,4 +42,15 @@ data class LocalMediaProgress(
|
|||||||
isFinished = playbackSession.progress >= 0.99
|
isFinished = playbackSession.progress >= 0.99
|
||||||
finishedAt = if (isFinished) lastUpdate else null
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,11 @@ package com.audiobookshelf.app.data
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import androidx.core.app.NotificationCompat
|
|
||||||
import com.audiobookshelf.app.R
|
import com.audiobookshelf.app.R
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import com.audiobookshelf.app.player.MediaProgressSyncData
|
import com.audiobookshelf.app.player.MediaProgressSyncData
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
import com.google.android.exoplayer2.C
|
|
||||||
import com.google.android.exoplayer2.MediaItem
|
import com.google.android.exoplayer2.MediaItem
|
||||||
import com.google.android.exoplayer2.MediaMetadata
|
import com.google.android.exoplayer2.MediaMetadata
|
||||||
import com.google.android.gms.cast.MediaInfo
|
import com.google.android.gms.cast.MediaInfo
|
||||||
@@ -64,11 +62,13 @@ class PlaybackSession(
|
|||||||
val localMediaProgressId get() = if (episodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
|
val localMediaProgressId get() = if (episodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val progress get() = currentTime / getTotalDuration()
|
val progress get() = currentTime / getTotalDuration()
|
||||||
|
@get:JsonIgnore
|
||||||
|
val isLocalLibraryItemOnly get() = localLibraryItemId != "" && libraryItemId == null
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getCurrentTrackIndex():Int {
|
fun getCurrentTrackIndex():Int {
|
||||||
for (i in 0..(audioTracks.size - 1)) {
|
for (i in 0..(audioTracks.size - 1)) {
|
||||||
var track = audioTracks[i]
|
val track = audioTracks[i]
|
||||||
if (currentTimeMs >= track.startOffsetMs && (track.endOffsetMs) > currentTimeMs) {
|
if (currentTimeMs >= track.startOffsetMs && (track.endOffsetMs) > currentTimeMs) {
|
||||||
return i
|
return i
|
||||||
}
|
}
|
||||||
@@ -78,14 +78,14 @@ class PlaybackSession(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getCurrentTrackTimeMs():Long {
|
fun getCurrentTrackTimeMs():Long {
|
||||||
var currentTrack = audioTracks[this.getCurrentTrackIndex()]
|
val currentTrack = audioTracks[this.getCurrentTrackIndex()]
|
||||||
var time = currentTime - currentTrack.startOffset
|
val time = currentTime - currentTrack.startOffset
|
||||||
return (time * 1000L).toLong()
|
return (time * 1000L).toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getTrackStartOffsetMs(index:Int):Long {
|
fun getTrackStartOffsetMs(index:Int):Long {
|
||||||
var currentTrack = audioTracks[index]
|
val currentTrack = audioTracks[index]
|
||||||
return (currentTrack.startOffset * 1000L).toLong()
|
return (currentTrack.startOffset * 1000L).toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ class PlaybackSession(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getMediaMetadataCompat(): MediaMetadataCompat {
|
fun getMediaMetadataCompat(): MediaMetadataCompat {
|
||||||
var metadataBuilder = MediaMetadataCompat.Builder()
|
val metadataBuilder = MediaMetadataCompat.Builder()
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, displayTitle)
|
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, displayTitle)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, displayTitle)
|
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, displayTitle)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, displayAuthor)
|
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, displayAuthor)
|
||||||
@@ -125,14 +125,14 @@ class PlaybackSession(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getExoMediaMetadata(audioTrack:AudioTrack): MediaMetadata {
|
fun getExoMediaMetadata(audioTrack:AudioTrack): MediaMetadata {
|
||||||
var metadataBuilder = MediaMetadata.Builder()
|
val metadataBuilder = MediaMetadata.Builder()
|
||||||
.setTitle(displayTitle)
|
.setTitle(displayTitle)
|
||||||
.setDisplayTitle(displayTitle)
|
.setDisplayTitle(displayTitle)
|
||||||
.setArtist(displayAuthor)
|
.setArtist(displayAuthor)
|
||||||
.setAlbumArtist(displayAuthor)
|
.setAlbumArtist(displayAuthor)
|
||||||
.setSubtitle(displayAuthor)
|
.setSubtitle(displayAuthor)
|
||||||
|
|
||||||
var contentUri = this.getContentUri(audioTrack)
|
val contentUri = this.getContentUri(audioTrack)
|
||||||
metadataBuilder.setMediaUri(contentUri)
|
metadataBuilder.setMediaUri(contentUri)
|
||||||
|
|
||||||
return metadataBuilder.build()
|
return metadataBuilder.build()
|
||||||
@@ -140,15 +140,15 @@ class PlaybackSession(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getMediaItems():List<MediaItem> {
|
fun getMediaItems():List<MediaItem> {
|
||||||
var mediaItems:MutableList<MediaItem> = mutableListOf()
|
val mediaItems:MutableList<MediaItem> = mutableListOf()
|
||||||
|
|
||||||
for (audioTrack in audioTracks) {
|
for (audioTrack in audioTracks) {
|
||||||
var mediaMetadata = this.getExoMediaMetadata(audioTrack)
|
val mediaMetadata = this.getExoMediaMetadata(audioTrack)
|
||||||
var mediaUri = this.getContentUri(audioTrack)
|
val mediaUri = this.getContentUri(audioTrack)
|
||||||
var mimeType = audioTrack.mimeType
|
val mimeType = audioTrack.mimeType
|
||||||
|
|
||||||
var queueItem = getQueueItem(audioTrack) // Queue item used in exo player CastManager
|
val queueItem = getQueueItem(audioTrack) // Queue item used in exo player CastManager
|
||||||
var mediaItem = MediaItem.Builder().setUri(mediaUri).setTag(queueItem).setMediaMetadata(mediaMetadata).setMimeType(mimeType).build()
|
val mediaItem = MediaItem.Builder().setUri(mediaUri).setTag(queueItem).setMediaMetadata(mediaMetadata).setMimeType(mimeType).build()
|
||||||
mediaItems.add(mediaItem)
|
mediaItems.add(mediaItem)
|
||||||
}
|
}
|
||||||
return mediaItems
|
return mediaItems
|
||||||
@@ -156,7 +156,7 @@ class PlaybackSession(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getCastMediaMetadata(audioTrack:AudioTrack):com.google.android.gms.cast.MediaMetadata {
|
fun getCastMediaMetadata(audioTrack:AudioTrack):com.google.android.gms.cast.MediaMetadata {
|
||||||
var castMetadata = com.google.android.gms.cast.MediaMetadata(com.google.android.gms.cast.MediaMetadata.MEDIA_TYPE_AUDIOBOOK_CHAPTER)
|
val castMetadata = com.google.android.gms.cast.MediaMetadata(com.google.android.gms.cast.MediaMetadata.MEDIA_TYPE_AUDIOBOOK_CHAPTER)
|
||||||
|
|
||||||
coverPath?.let {
|
coverPath?.let {
|
||||||
castMetadata.addImage(WebImage(Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}")))
|
castMetadata.addImage(WebImage(Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}")))
|
||||||
@@ -171,11 +171,11 @@ class PlaybackSession(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getQueueItem(audioTrack:AudioTrack):MediaQueueItem {
|
fun getQueueItem(audioTrack:AudioTrack):MediaQueueItem {
|
||||||
var castMetadata = getCastMediaMetadata(audioTrack)
|
val castMetadata = getCastMediaMetadata(audioTrack)
|
||||||
|
|
||||||
var mediaUri = getContentUri(audioTrack)
|
val mediaUri = getContentUri(audioTrack)
|
||||||
|
|
||||||
var mediaInfo = MediaInfo.Builder(mediaUri.toString()).apply {
|
val mediaInfo = MediaInfo.Builder(mediaUri.toString()).apply {
|
||||||
setContentUrl(mediaUri.toString())
|
setContentUrl(mediaUri.toString())
|
||||||
setContentType(audioTrack.mimeType)
|
setContentType(audioTrack.mimeType)
|
||||||
setMetadata(castMetadata)
|
setMetadata(castMetadata)
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ object DeviceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getBase64Id(id:String):String {
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getServerConnectionConfig(id:String?):ServerConnectionConfig? {
|
||||||
|
if (id == null) return null
|
||||||
|
return deviceData.serverConnectionConfigs.find { it.id == id }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
var coverContentUrl:String? = null
|
var coverContentUrl:String? = null
|
||||||
var coverAbsolutePath:String? = null
|
var coverAbsolutePath:String? = null
|
||||||
|
|
||||||
val filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4"))
|
val filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream"))
|
||||||
|
|
||||||
val existingLocalFilesRemoved = existingLocalFiles.filter { elf ->
|
val existingLocalFilesRemoved = existingLocalFiles.filter { elf ->
|
||||||
filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder
|
filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder
|
||||||
@@ -250,7 +250,8 @@ class FolderScanner(var ctx: Context) {
|
|||||||
Log.d(tag, "scanDownloadItem starting for ${downloadItem.itemFolderPath} | ${df.uri} | Item Folder Id:$itemFolderId | LLI Id:$localLibraryItemId")
|
Log.d(tag, "scanDownloadItem starting for ${downloadItem.itemFolderPath} | ${df.uri} | Item Folder Id:$itemFolderId | LLI Id:$localLibraryItemId")
|
||||||
|
|
||||||
// Search for files in media item folder
|
// Search for files in media item folder
|
||||||
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4"))
|
// m4b files showing as mimeType application/octet-stream on Android 10 and earlier see #154
|
||||||
|
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream"))
|
||||||
Log.d(tag, "scanDownloadItem ${filesFound.size} files found in ${downloadItem.itemFolderPath}")
|
Log.d(tag, "scanDownloadItem ${filesFound.size} files found in ${downloadItem.itemFolderPath}")
|
||||||
|
|
||||||
var localEpisodeId:String? = null
|
var localEpisodeId:String? = null
|
||||||
@@ -288,7 +289,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
val audioProbeResult = probeAudioFile(localFile.absolutePath)
|
val audioProbeResult = probeAudioFile(localFile.absolutePath)
|
||||||
|
|
||||||
// Create new audio track
|
// Create new audio track
|
||||||
val track = AudioTrack(audioTrackFromServer?.index ?: -1, audioTrackFromServer?.startOffset ?: 0.0, audioProbeResult.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, audioTrackFromServer?.index ?: -1)
|
val track = AudioTrack(audioTrackFromServer.index, audioTrackFromServer.startOffset, audioProbeResult.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, audioTrackFromServer?.index ?: -1)
|
||||||
audioTracks.add(track)
|
audioTracks.add(track)
|
||||||
|
|
||||||
Log.d(tag, "scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}")
|
Log.d(tag, "scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}")
|
||||||
@@ -296,7 +297,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
// Add podcast episodes to library
|
// Add podcast episodes to library
|
||||||
itemPart.episode?.let { podcastEpisode ->
|
itemPart.episode?.let { podcastEpisode ->
|
||||||
val podcast = localLibraryItem.media as Podcast
|
val podcast = localLibraryItem.media as Podcast
|
||||||
var newEpisode = podcast.addEpisode(track, podcastEpisode)
|
val newEpisode = podcast.addEpisode(track, podcastEpisode)
|
||||||
localEpisodeId = newEpisode.id
|
localEpisodeId = newEpisode.id
|
||||||
Log.d(tag, "scanDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}")
|
Log.d(tag, "scanDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}")
|
||||||
}
|
}
|
||||||
@@ -366,7 +367,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun scanLocalLibraryItem(localLibraryItem:LocalLibraryItem, forceAudioProbe:Boolean):LocalLibraryItemScanResult? {
|
fun scanLocalLibraryItem(localLibraryItem:LocalLibraryItem, forceAudioProbe:Boolean):LocalLibraryItemScanResult? {
|
||||||
var df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(localLibraryItem.contentUrl))
|
val df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(localLibraryItem.contentUrl))
|
||||||
|
|
||||||
if (df == null) {
|
if (df == null) {
|
||||||
Log.e(tag, "Item Folder Doc File Invalid ${localLibraryItem.absolutePath}")
|
Log.e(tag, "Item Folder Doc File Invalid ${localLibraryItem.absolutePath}")
|
||||||
@@ -377,7 +378,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
var wasUpdated = false
|
var wasUpdated = false
|
||||||
|
|
||||||
// Search for files in media item folder
|
// Search for files in media item folder
|
||||||
var filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4"))
|
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream"))
|
||||||
Log.d(tag, "scanLocalLibraryItem ${filesFound.size} files found in ${localLibraryItem.absolutePath}")
|
Log.d(tag, "scanLocalLibraryItem ${filesFound.size} files found in ${localLibraryItem.absolutePath}")
|
||||||
|
|
||||||
filesFound.forEach {
|
filesFound.forEach {
|
||||||
@@ -388,10 +389,10 @@ class FolderScanner(var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var existingAudioTracks = localLibraryItem.media.getAudioTracks()
|
val existingAudioTracks = localLibraryItem.media.getAudioTracks()
|
||||||
|
|
||||||
// Remove any files no longer found in library item folder
|
// Remove any files no longer found in library item folder
|
||||||
var existingLocalFileIds = localLibraryItem.localFiles.map { it.id }
|
val existingLocalFileIds = localLibraryItem.localFiles.map { it.id }
|
||||||
existingLocalFileIds.forEach { localFileId ->
|
existingLocalFileIds.forEach { localFileId ->
|
||||||
Log.d(tag, "Checking local file id is there $localFileId")
|
Log.d(tag, "Checking local file id is there $localFileId")
|
||||||
if (filesFound.find { DeviceManager.getBase64Id(it.id) == localFileId } == null) {
|
if (filesFound.find { DeviceManager.getBase64Id(it.id) == localFileId } == null) {
|
||||||
@@ -407,12 +408,12 @@ class FolderScanner(var ctx: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
filesFound.forEach { docFile ->
|
filesFound.forEach { docFile ->
|
||||||
var localFileId = DeviceManager.getBase64Id(docFile.id)
|
val localFileId = DeviceManager.getBase64Id(docFile.id)
|
||||||
var existingLocalFile = localLibraryItem.localFiles.find { it.id == localFileId }
|
val existingLocalFile = localLibraryItem.localFiles.find { it.id == localFileId }
|
||||||
|
|
||||||
if (existingLocalFile == null || (existingLocalFile.isAudioFile() && forceAudioProbe)) {
|
if (existingLocalFile == null || (existingLocalFile.isAudioFile() && forceAudioProbe)) {
|
||||||
|
|
||||||
var localFile = existingLocalFile ?: LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx), docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
|
val localFile = existingLocalFile ?: LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx), docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
|
||||||
if (existingLocalFile == null) {
|
if (existingLocalFile == null) {
|
||||||
localLibraryItem.localFiles.add(localFile)
|
localLibraryItem.localFiles.add(localFile)
|
||||||
Log.d(tag, "scanLocalLibraryItem new file found ${localFile.filename}")
|
Log.d(tag, "scanLocalLibraryItem new file found ${localFile.filename}")
|
||||||
@@ -420,22 +421,26 @@ class FolderScanner(var ctx: Context) {
|
|||||||
|
|
||||||
if (localFile.isAudioFile()) {
|
if (localFile.isAudioFile()) {
|
||||||
// TODO: Make asynchronous
|
// TODO: Make asynchronous
|
||||||
var audioProbeResult = probeAudioFile(localFile.absolutePath)
|
val audioProbeResult = probeAudioFile(localFile.absolutePath)
|
||||||
|
|
||||||
var existingTrack = existingAudioTracks.find { audioTrack ->
|
val existingTrack = existingAudioTracks.find { audioTrack ->
|
||||||
audioTrack.localFileId == localFile.id
|
audioTrack.localFileId == localFile.id
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingTrack == null) {
|
if (existingTrack == null) {
|
||||||
// Create new audio track
|
// Create new audio track
|
||||||
var lastTrack = existingAudioTracks.lastOrNull()
|
val lastTrack = existingAudioTracks.lastOrNull()
|
||||||
var startOffset = (lastTrack?.startOffset ?: 0.0) + (lastTrack?.duration ?: 0.0)
|
val startOffset = (lastTrack?.startOffset ?: 0.0) + (lastTrack?.duration ?: 0.0)
|
||||||
var track = AudioTrack(existingAudioTracks.size, startOffset, audioProbeResult.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, null)
|
val track = AudioTrack(existingAudioTracks.size, startOffset, audioProbeResult.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, null)
|
||||||
localLibraryItem.media.addAudioTrack(track)
|
localLibraryItem.media.addAudioTrack(track)
|
||||||
|
Log.d(tag, "Added New Audio Track ${track.title}")
|
||||||
wasUpdated = true
|
wasUpdated = true
|
||||||
} else {
|
} else {
|
||||||
existingTrack.audioProbeResult = audioProbeResult
|
existingTrack.audioProbeResult = audioProbeResult
|
||||||
// TODO: Update data found from probe
|
// TODO: Update data found from probe
|
||||||
|
|
||||||
|
Log.d(tag, "Updated Audio Track Probe Data ${existingTrack.title}")
|
||||||
|
|
||||||
wasUpdated = true
|
wasUpdated = true
|
||||||
}
|
}
|
||||||
} else { // Check if cover is empty
|
} else { // Check if cover is empty
|
||||||
|
|||||||
@@ -1,27 +1,55 @@
|
|||||||
package com.audiobookshelf.app.media
|
package com.audiobookshelf.app.media
|
||||||
|
|
||||||
import android.bluetooth.BluetoothClass
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.support.v4.media.MediaBrowserCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.data.*
|
import com.audiobookshelf.app.data.*
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import com.audiobookshelf.app.player.PlayerNotificationService
|
|
||||||
import com.audiobookshelf.app.server.ApiHandler
|
import com.audiobookshelf.app.server.ApiHandler
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import io.paperdb.Paper
|
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) {
|
class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
||||||
val tag = "MediaManager"
|
val tag = "MediaManager"
|
||||||
|
|
||||||
var serverLibraryItems = listOf<LibraryItem>()
|
var serverLibraryItems = listOf<LibraryItem>()
|
||||||
|
var selectedLibraryId = ""
|
||||||
|
|
||||||
|
var selectedLibraryItemWrapper:LibraryItemWrapper? = null
|
||||||
|
var selectedPodcast:Podcast? = null
|
||||||
|
var selectedLibraryItemId:String? = null
|
||||||
|
var serverPodcastEpisodes = listOf<PodcastEpisode>()
|
||||||
var serverLibraryCategories = listOf<LibraryCategory>()
|
var serverLibraryCategories = listOf<LibraryCategory>()
|
||||||
var serverLibraries = listOf<Library>()
|
var serverLibraries = listOf<Library>()
|
||||||
|
var serverConfigIdUsed:String? = null
|
||||||
|
|
||||||
fun initializeAndroidAuto() {
|
fun initializeAndroidAuto() {
|
||||||
Log.d(tag, "Android Auto started when MainActivity was never started - initializing Paper")
|
Log.d(tag, "Android Auto started when MainActivity was never started - initializing Paper")
|
||||||
Paper.init(ctx)
|
Paper.init(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getIsLibrary(id:String) : Boolean {
|
||||||
|
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) {
|
fun loadLibraryCategories(libraryId:String, cb: (List<LibraryCategory>) -> Unit) {
|
||||||
if (serverLibraryCategories.isNotEmpty()) {
|
if (serverLibraryCategories.isNotEmpty()) {
|
||||||
cb(serverLibraryCategories)
|
cb(serverLibraryCategories)
|
||||||
@@ -33,17 +61,75 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadLibraryItems(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
|
fun loadLibraryItemsWithAudio(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
|
||||||
if (serverLibraryItems.isNotEmpty()) {
|
if (serverLibraryItems.isNotEmpty() && selectedLibraryId == libraryId) {
|
||||||
cb(serverLibraryItems)
|
cb(serverLibraryItems)
|
||||||
} else {
|
} else {
|
||||||
apiHandler.getLibraryItems(libraryId) { libraryItems ->
|
apiHandler.getLibraryItems(libraryId) { libraryItems ->
|
||||||
serverLibraryItems = libraryItems
|
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
|
||||||
cb(libraryItems)
|
if (libraryItemsWithAudio.isNotEmpty()) selectedLibraryId = libraryId
|
||||||
|
|
||||||
|
serverLibraryItems = libraryItemsWithAudio
|
||||||
|
cb(libraryItemsWithAudio)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadLibraryItem(libraryItemId:String, cb: (LibraryItemWrapper?) -> Unit) {
|
||||||
|
if (libraryItemId.startsWith("local")) {
|
||||||
|
cb(DeviceManager.dbManager.getLocalLibraryItem(libraryItemId))
|
||||||
|
} else {
|
||||||
|
Log.d(tag, "loadLibraryItem: $libraryItemId")
|
||||||
|
apiHandler.getLibraryItem(libraryItemId) { libraryItem ->
|
||||||
|
Log.d(tag, "loadLibraryItem: Got library item $libraryItem")
|
||||||
|
cb(libraryItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadPodcastEpisodeMediaBrowserItems(libraryItemId:String, cb: (MutableList<MediaBrowserCompat.MediaItem>) -> Unit) {
|
||||||
|
loadLibraryItem(libraryItemId) { libraryItemWrapper ->
|
||||||
|
Log.d(tag, "Loaded Podcast library item $libraryItemWrapper")
|
||||||
|
|
||||||
|
selectedLibraryItemWrapper = libraryItemWrapper
|
||||||
|
|
||||||
|
libraryItemWrapper?.let {
|
||||||
|
if (libraryItemWrapper is LocalLibraryItem) { // Local podcast episodes
|
||||||
|
if (libraryItemWrapper.mediaType != "podcast" || libraryItemWrapper.media.getAudioTracks().isEmpty()) {
|
||||||
|
serverPodcastEpisodes = listOf()
|
||||||
|
cb(mutableListOf())
|
||||||
|
} else {
|
||||||
|
val podcast = libraryItemWrapper.media as Podcast
|
||||||
|
serverPodcastEpisodes = podcast.episodes ?: listOf()
|
||||||
|
selectedLibraryItemId = libraryItemWrapper.id
|
||||||
|
selectedPodcast = podcast
|
||||||
|
|
||||||
|
val children = podcast.episodes?.map { podcastEpisode ->
|
||||||
|
Log.d(tag, "Local Podcast Episode ${podcastEpisode.title} | ${podcastEpisode.id}")
|
||||||
|
MediaBrowserCompat.MediaItem(podcastEpisode.getMediaMetadata(libraryItemWrapper).description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
|
}
|
||||||
|
children?.let { cb(children as MutableList) } ?: cb(mutableListOf())
|
||||||
|
}
|
||||||
|
} else if (libraryItemWrapper is LibraryItem) { // Server podcast episodes
|
||||||
|
if (libraryItemWrapper.mediaType != "podcast" || libraryItemWrapper.media.getAudioTracks().isEmpty()) {
|
||||||
|
serverPodcastEpisodes = listOf()
|
||||||
|
cb(mutableListOf())
|
||||||
|
} else {
|
||||||
|
val podcast = libraryItemWrapper.media as Podcast
|
||||||
|
serverPodcastEpisodes = podcast.episodes ?: listOf()
|
||||||
|
selectedLibraryItemId = libraryItemWrapper.id
|
||||||
|
selectedPodcast = podcast
|
||||||
|
|
||||||
|
val children = podcast.episodes?.map { podcastEpisode ->
|
||||||
|
MediaBrowserCompat.MediaItem(podcastEpisode.getMediaMetadata(libraryItemWrapper).description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
|
}
|
||||||
|
children?.let { cb(children as MutableList) } ?: cb(mutableListOf())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun loadLibraries(cb: (List<Library>) -> Unit) {
|
fun loadLibraries(cb: (List<Library>) -> Unit) {
|
||||||
if (serverLibraries.isNotEmpty()) {
|
if (serverLibraries.isNotEmpty()) {
|
||||||
cb(serverLibraries)
|
cb(serverLibraries)
|
||||||
@@ -55,11 +141,53 @@ 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
|
// TODO: Load currently listening category for local items
|
||||||
fun loadLocalCategory():List<LibraryCategory> {
|
fun loadLocalCategory():List<LibraryCategory> {
|
||||||
var localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
|
val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
|
||||||
var localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast")
|
val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast")
|
||||||
var cats = mutableListOf<LibraryCategory>()
|
val cats = mutableListOf<LibraryCategory>()
|
||||||
if (localBooks.isNotEmpty()) {
|
if (localBooks.isNotEmpty()) {
|
||||||
cats.add(LibraryCategory("local-books", "Local Books", "book", localBooks, true))
|
cats.add(LibraryCategory("local-books", "Local Books", "book", localBooks, true))
|
||||||
}
|
}
|
||||||
@@ -69,45 +197,39 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
return cats
|
return cats
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadAndroidAutoItems(libraryId:String, cb: (List<LibraryCategory>) -> Unit) {
|
fun loadAndroidAutoItems(cb: (List<LibraryCategory>) -> Unit) {
|
||||||
Log.d(tag, "Load android auto items for library id $libraryId")
|
Log.d(tag, "Load android auto items")
|
||||||
var cats = mutableListOf<LibraryCategory>()
|
val cats = mutableListOf<LibraryCategory>()
|
||||||
|
|
||||||
var localCategories = loadLocalCategory()
|
val localCategories = loadLocalCategory()
|
||||||
cats.addAll(localCategories)
|
cats.addAll(localCategories)
|
||||||
|
|
||||||
// Connected to server and has internet - load other cats
|
// Check if any valid server connection if not use locally downloaded books
|
||||||
if (apiHandler.isOnline() && (DeviceManager.isConnectedToServer || DeviceManager.hasLastServerConnectionConfig)) {
|
checkSetValidServerConnectionConfig { isConnected ->
|
||||||
if (!DeviceManager.isConnectedToServer) {
|
if (isConnected) {
|
||||||
DeviceManager.serverConnectionConfig = DeviceManager.deviceData.getLastServerConnectionConfig()
|
serverConfigIdUsed = DeviceManager.serverConnectionConfigId
|
||||||
Log.d(tag, "Not connected to server, set last server \"${DeviceManager.serverAddress}\"")
|
|
||||||
}
|
|
||||||
|
|
||||||
loadLibraries { libraries ->
|
loadLibraries { libraries ->
|
||||||
var library = libraries.find { it.id == libraryId } ?: libraries[0]
|
val library = libraries[0]
|
||||||
Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}")
|
Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}")
|
||||||
|
|
||||||
loadLibraryCategories(libraryId) { libraryCategories ->
|
loadLibraryCategories(library.id) { libraryCategories ->
|
||||||
|
|
||||||
// Only using book or podcast library categories for now
|
// Only using book or podcast library categories for now
|
||||||
libraryCategories.forEach {
|
libraryCategories.forEach {
|
||||||
Log.d(tag, "Found library category ${it.label} with type ${it.type}")
|
// Log.d(tag, "Found library category ${it.label} with type ${it.type}")
|
||||||
if (it.type == library.mediaType) {
|
if (it.type == library.mediaType) {
|
||||||
Log.d(tag, "Using library category ${it.id}")
|
// Log.d(tag, "Using library category ${it.id}")
|
||||||
cats.add(it)
|
cats.add(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
loadLibraryItems(libraryId) { libraryItems ->
|
|
||||||
var mainCat = LibraryCategory("library", "Library", library.mediaType, libraryItems, false)
|
|
||||||
cats.add(mainCat)
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,11 +237,24 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
if (serverLibraryItems.isNotEmpty()) {
|
if (serverLibraryItems.isNotEmpty()) {
|
||||||
return serverLibraryItems[0]
|
return serverLibraryItems[0]
|
||||||
} else {
|
} else {
|
||||||
var localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
|
val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
|
||||||
return if (localBooks.isNotEmpty()) return localBooks[0] else null
|
return if (localBooks.isNotEmpty()) return localBooks[0] else null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getPodcastWithEpisodeByEpisodeId(id:String) : LibraryItemWithEpisode? {
|
||||||
|
if (id.startsWith("local")) {
|
||||||
|
return DeviceManager.dbManager.getLocalLibraryItemWithEpisode(id)
|
||||||
|
} else {
|
||||||
|
val podcastEpisode = serverPodcastEpisodes.find { it.id == id }
|
||||||
|
return if (podcastEpisode != null && selectedLibraryItemWrapper != null) {
|
||||||
|
LibraryItemWithEpisode(selectedLibraryItemWrapper!!, podcastEpisode)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getById(id:String) : LibraryItemWrapper? {
|
fun getById(id:String) : LibraryItemWrapper? {
|
||||||
if (id.startsWith("local")) {
|
if (id.startsWith("local")) {
|
||||||
return DeviceManager.dbManager.getLocalLibraryItem(id)
|
return DeviceManager.dbManager.getLocalLibraryItem(id)
|
||||||
@@ -135,13 +270,13 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun play(libraryItemWrapper:LibraryItemWrapper, mediaPlayer:String, cb: (PlaybackSession) -> Unit) {
|
fun play(libraryItemWrapper:LibraryItemWrapper, episode:PodcastEpisode?, playItemRequestPayload:PlayItemRequestPayload, cb: (PlaybackSession) -> Unit) {
|
||||||
if (libraryItemWrapper is LocalLibraryItem) {
|
if (libraryItemWrapper is LocalLibraryItem) {
|
||||||
var localLibraryItem = libraryItemWrapper as LocalLibraryItem
|
val localLibraryItem = libraryItemWrapper as LocalLibraryItem
|
||||||
cb(localLibraryItem.getPlaybackSession(null))
|
cb(localLibraryItem.getPlaybackSession(episode))
|
||||||
} else {
|
} else {
|
||||||
var libraryItem = libraryItemWrapper as LibraryItem
|
val libraryItem = libraryItemWrapper as LibraryItem
|
||||||
apiHandler.playLibraryItem(libraryItem.id,"",false, mediaPlayer) {
|
apiHandler.playLibraryItem(libraryItem.id,episode?.id ?: "",playItemRequestPayload) {
|
||||||
cb(it)
|
cb(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ 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
|
||||||
@@ -62,26 +59,10 @@ 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.
|
|
||||||
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(context).applyDefaultRequestOptions(glideOptions)
|
Glide.with(playerNotificationService)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.load(urival)
|
.load(uri)
|
||||||
.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)
|
||||||
@@ -89,7 +70,7 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
|
|
||||||
Glide.with(context).applyDefaultRequestOptions(glideOptions)
|
Glide.with(playerNotificationService)
|
||||||
.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)
|
||||||
|
|||||||
@@ -7,14 +7,15 @@ import android.support.v4.media.MediaMetadataCompat
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.AnyRes
|
import androidx.annotation.AnyRes
|
||||||
import com.audiobookshelf.app.R
|
import com.audiobookshelf.app.R
|
||||||
|
import com.audiobookshelf.app.data.Library
|
||||||
import com.audiobookshelf.app.data.LibraryCategory
|
import com.audiobookshelf.app.data.LibraryCategory
|
||||||
import com.audiobookshelf.app.data.LibraryItem
|
import com.audiobookshelf.app.data.LibraryItem
|
||||||
import com.audiobookshelf.app.data.LocalLibraryItem
|
import com.audiobookshelf.app.data.LocalLibraryItem
|
||||||
|
|
||||||
|
|
||||||
class BrowseTree(
|
class BrowseTree(
|
||||||
val context: Context,
|
val context: Context,
|
||||||
libraryCategories: List<LibraryCategory>
|
libraryCategories: List<LibraryCategory>,
|
||||||
|
libraries: List<Library>
|
||||||
) {
|
) {
|
||||||
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()
|
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()
|
||||||
|
|
||||||
@@ -41,22 +42,22 @@ class BrowseTree(
|
|||||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_localaudio).toString())
|
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_localaudio).toString())
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
val allMetadata = MediaMetadataCompat.Builder().apply {
|
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, ALL_ROOT)
|
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Library Items")
|
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_books).toString())
|
|
||||||
}.build()
|
|
||||||
|
|
||||||
val downloadsMetadata = MediaMetadataCompat.Builder().apply {
|
val downloadsMetadata = MediaMetadataCompat.Builder().apply {
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT)
|
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT)
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads")
|
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads")
|
||||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_downloaddone).toString())
|
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_downloaddone).toString())
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
|
val librariesMetadata = MediaMetadataCompat.Builder().apply {
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, LIBRARIES_ROOT)
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Libraries")
|
||||||
|
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.icon_library_folder).toString())
|
||||||
|
}.build()
|
||||||
|
|
||||||
// Server continue Listening cat
|
// Server continue Listening cat
|
||||||
libraryCategories.find { it.id == "continue-listening" }?.let { continueListeningCategory ->
|
libraryCategories.find { it.id == "continue-listening" }?.let { continueListeningCategory ->
|
||||||
var continueListeningMediaMetadata = continueListeningCategory.entities.map { liw ->
|
val continueListeningMediaMetadata = continueListeningCategory.entities.map { liw ->
|
||||||
var libraryItem = liw as LibraryItem
|
val libraryItem = liw as LibraryItem
|
||||||
libraryItem.getMediaMetadata()
|
libraryItem.getMediaMetadata()
|
||||||
}
|
}
|
||||||
if (continueListeningMediaMetadata.isNotEmpty()) {
|
if (continueListeningMediaMetadata.isNotEmpty()) {
|
||||||
@@ -69,30 +70,32 @@ class BrowseTree(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rootList += allMetadata
|
if (libraries.isNotEmpty()) {
|
||||||
rootList += downloadsMetadata
|
rootList += librariesMetadata
|
||||||
|
|
||||||
// Server library cat
|
libraries.forEach { library ->
|
||||||
libraryCategories.find { it.id == "library" }?.let { libraryCategory ->
|
val libraryMediaMetadata = library.getMediaMetadata()
|
||||||
var libraryMediaMetadata = libraryCategory.entities.map { libc ->
|
val children = mediaIdToChildren[LIBRARIES_ROOT] ?: mutableListOf()
|
||||||
var libraryItem = libc as LibraryItem
|
children += libraryMediaMetadata
|
||||||
libraryItem.getMediaMetadata()
|
mediaIdToChildren[LIBRARIES_ROOT] = children
|
||||||
}
|
|
||||||
libraryMediaMetadata.forEach {
|
|
||||||
val children = mediaIdToChildren[ALL_ROOT] ?: mutableListOf()
|
|
||||||
children += it
|
|
||||||
mediaIdToChildren[ALL_ROOT] = children
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rootList += downloadsMetadata
|
||||||
libraryCategories.find { it.id == "local-books" }?.let { localBooksCat ->
|
libraryCategories.find { it.id == "local-books" }?.let { localBooksCat ->
|
||||||
var localMediaMetadata = localBooksCat.entities.map { libc ->
|
localBooksCat.entities.forEach { libc ->
|
||||||
var libraryItem = libc as LocalLibraryItem
|
val libraryItem = libc as LocalLibraryItem
|
||||||
libraryItem.getMediaMetadata()
|
val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf()
|
||||||
|
children += libraryItem.getMediaMetadata(context)
|
||||||
|
mediaIdToChildren[DOWNLOADS_ROOT] = children
|
||||||
}
|
}
|
||||||
localMediaMetadata.forEach {
|
}
|
||||||
|
|
||||||
|
libraryCategories.find { it.id == "local-podcasts" }?.let { localPodcastsCat ->
|
||||||
|
localPodcastsCat.entities.forEach { libc ->
|
||||||
|
val libraryItem = libc as LocalLibraryItem
|
||||||
val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf()
|
val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf()
|
||||||
children += it
|
children += libraryItem.getMediaMetadata(context)
|
||||||
mediaIdToChildren[DOWNLOADS_ROOT] = children
|
mediaIdToChildren[DOWNLOADS_ROOT] = children
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,6 +107,6 @@ class BrowseTree(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const val AUTO_BROWSE_ROOT = "/"
|
const val AUTO_BROWSE_ROOT = "/"
|
||||||
const val ALL_ROOT = "__ALL__"
|
|
||||||
const val CONTINUE_ROOT = "__CONTINUE__"
|
const val CONTINUE_ROOT = "__CONTINUE__"
|
||||||
const val DOWNLOADS_ROOT = "__DOWNLOADS__"
|
const val DOWNLOADS_ROOT = "__DOWNLOADS__"
|
||||||
|
const val LIBRARIES_ROOT = "__LIBRARIES__"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.os.Handler
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.data.LocalMediaProgress
|
import com.audiobookshelf.app.data.LocalMediaProgress
|
||||||
|
import com.audiobookshelf.app.data.MediaProgress
|
||||||
import com.audiobookshelf.app.data.PlaybackSession
|
import com.audiobookshelf.app.data.PlaybackSession
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import com.audiobookshelf.app.server.ApiHandler
|
import com.audiobookshelf.app.server.ApiHandler
|
||||||
@@ -17,15 +18,14 @@ data class MediaProgressSyncData(
|
|||||||
var currentTime:Double // seconds
|
var currentTime:Double // seconds
|
||||||
)
|
)
|
||||||
|
|
||||||
class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, apiHandler: ApiHandler) {
|
class MediaProgressSyncer(val playerNotificationService:PlayerNotificationService, private val apiHandler: ApiHandler) {
|
||||||
private val tag = "MediaProgressSync"
|
private val tag = "MediaProgressSync"
|
||||||
private val playerNotificationService:PlayerNotificationService = playerNotificationService
|
|
||||||
private val apiHandler = apiHandler
|
|
||||||
|
|
||||||
private var listeningTimerTask: TimerTask? = null
|
private var listeningTimerTask: TimerTask? = null
|
||||||
var listeningTimerRunning:Boolean = false
|
var listeningTimerRunning:Boolean = false
|
||||||
|
|
||||||
private var lastSyncTime:Long = 0
|
private var lastSyncTime:Long = 0
|
||||||
|
private var failedSyncs:Int = 0
|
||||||
|
|
||||||
var currentPlaybackSession: PlaybackSession? = null // copy of pb session currently syncing
|
var currentPlaybackSession: PlaybackSession? = null // copy of pb session currently syncing
|
||||||
var currentLocalMediaProgress: LocalMediaProgress? = null
|
var currentLocalMediaProgress: LocalMediaProgress? = null
|
||||||
@@ -43,6 +43,7 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a
|
|||||||
currentLocalMediaProgress = null
|
currentLocalMediaProgress = null
|
||||||
listeningTimerTask?.cancel()
|
listeningTimerTask?.cancel()
|
||||||
lastSyncTime = 0L
|
lastSyncTime = 0L
|
||||||
|
failedSyncs = 0
|
||||||
} else {
|
} else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -54,7 +55,7 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a
|
|||||||
listeningTimerTask = Timer("ListeningTimer", false).schedule(0L, 5000L) {
|
listeningTimerTask = Timer("ListeningTimer", false).schedule(0L, 5000L) {
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
if (playerNotificationService.currentPlayer.isPlaying) {
|
if (playerNotificationService.currentPlayer.isPlaying) {
|
||||||
var currentTime = playerNotificationService.getCurrentTimeSeconds()
|
val currentTime = playerNotificationService.getCurrentTimeSeconds()
|
||||||
sync(currentTime)
|
sync(currentTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,20 +66,30 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a
|
|||||||
if (!listeningTimerRunning) return
|
if (!listeningTimerRunning) return
|
||||||
Log.d(tag, "stop: Stopping listening for $currentDisplayTitle")
|
Log.d(tag, "stop: Stopping listening for $currentDisplayTitle")
|
||||||
|
|
||||||
var currentTime = playerNotificationService.getCurrentTimeSeconds()
|
val currentTime = playerNotificationService.getCurrentTimeSeconds()
|
||||||
sync(currentTime)
|
sync(currentTime)
|
||||||
reset()
|
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) {
|
fun sync(currentTime:Double) {
|
||||||
var diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
|
val diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
|
||||||
if (diffSinceLastSync < 1000L) {
|
if (diffSinceLastSync < 1000L) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var listeningTimeToAdd = diffSinceLastSync / 1000L
|
val listeningTimeToAdd = diffSinceLastSync / 1000L
|
||||||
lastSyncTime = System.currentTimeMillis()
|
lastSyncTime = System.currentTimeMillis()
|
||||||
|
|
||||||
var syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime)
|
val syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime)
|
||||||
|
|
||||||
currentPlaybackSession?.syncData(syncData)
|
currentPlaybackSession?.syncData(syncData)
|
||||||
if (currentIsLocal) {
|
if (currentIsLocal) {
|
||||||
@@ -87,23 +98,39 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a
|
|||||||
DeviceManager.dbManager.saveLocalPlaybackSession(it)
|
DeviceManager.dbManager.saveLocalPlaybackSession(it)
|
||||||
saveLocalProgress(it)
|
saveLocalProgress(it)
|
||||||
|
|
||||||
// Send sync to server also if connected to this server and local item belongs to this server
|
// Local library item is linked to a server library item
|
||||||
if (it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
|
if (!it.libraryItemId.isNullOrEmpty()) {
|
||||||
apiHandler.sendLocalProgressSync(it) {
|
// Send sync to server also if connected to this server and local item belongs to this server
|
||||||
Log.d(tag, "Local progress sync data sent to server $currentDisplayTitle for time $currentTime")
|
if (it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
|
||||||
|
apiHandler.sendLocalProgressSync(it) {
|
||||||
|
Log.d(
|
||||||
|
tag,
|
||||||
|
"Local progress sync data sent to server $currentDisplayTitle for time $currentTime"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
apiHandler.sendProgressSync(currentSessionId, syncData) {
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveLocalProgress(playbackSession:PlaybackSession) {
|
private fun saveLocalProgress(playbackSession:PlaybackSession) {
|
||||||
if (currentLocalMediaProgress == null) {
|
if (currentLocalMediaProgress == null) {
|
||||||
var mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(playbackSession.localMediaProgressId)
|
val mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(playbackSession.localMediaProgressId)
|
||||||
if (mediaProgress == null) {
|
if (mediaProgress == null) {
|
||||||
currentLocalMediaProgress = playbackSession.getNewLocalMediaProgress()
|
currentLocalMediaProgress = playbackSession.getNewLocalMediaProgress()
|
||||||
} else {
|
} else {
|
||||||
@@ -126,5 +153,6 @@ class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, a
|
|||||||
currentPlaybackSession = null
|
currentPlaybackSession = null
|
||||||
currentLocalMediaProgress = null
|
currentLocalMediaProgress = null
|
||||||
lastSyncTime = 0L
|
lastSyncTime = 0L
|
||||||
|
failedSyncs = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,8 @@ import android.os.Message
|
|||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import com.audiobookshelf.app.data.LibraryItem
|
|
||||||
import com.audiobookshelf.app.data.LibraryItemWrapper
|
import com.audiobookshelf.app.data.LibraryItemWrapper
|
||||||
import kotlinx.coroutines.Dispatchers
|
import com.audiobookshelf.app.data.PodcastEpisode
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
|
|
||||||
@@ -27,7 +24,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
override fun onPrepare() {
|
override fun onPrepare() {
|
||||||
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
|
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
|
||||||
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
|
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
|
||||||
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getPlayItemRequestPayload(false)) {
|
||||||
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,true,null)
|
playerNotificationService.preparePlayer(it,true,null)
|
||||||
@@ -49,7 +46,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
|
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
|
||||||
Log.d(tag, "ON PLAY FROM SEARCH $query")
|
Log.d(tag, "ON PLAY FROM SEARCH $query")
|
||||||
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
|
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
|
||||||
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getPlayItemRequestPayload(false)) {
|
||||||
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,true,null)
|
playerNotificationService.preparePlayer(it,true,null)
|
||||||
@@ -90,14 +87,24 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
|
||||||
Log.d(tag, "ON PLAY FROM MEDIA ID $mediaId")
|
Log.d(tag, "ON PLAY FROM MEDIA ID $mediaId")
|
||||||
var libraryItemWrapper: LibraryItemWrapper? = null
|
var libraryItemWrapper: LibraryItemWrapper? = null
|
||||||
|
var podcastEpisode: PodcastEpisode? = null
|
||||||
|
|
||||||
if (mediaId.isNullOrEmpty()) {
|
if (mediaId.isNullOrEmpty()) {
|
||||||
libraryItemWrapper = playerNotificationService.mediaManager.getFirstItem()
|
libraryItemWrapper = playerNotificationService.mediaManager.getFirstItem()
|
||||||
|
} else if (mediaId.startsWith("ep_") || mediaId.startsWith("local_ep_")) { // Playing podcast episode
|
||||||
|
val libraryItemWithEpisode = playerNotificationService.mediaManager.getPodcastWithEpisodeByEpisodeId(mediaId)
|
||||||
|
libraryItemWrapper = libraryItemWithEpisode?.libraryItemWrapper
|
||||||
|
podcastEpisode = libraryItemWithEpisode?.episode
|
||||||
} else {
|
} else {
|
||||||
libraryItemWrapper = playerNotificationService.mediaManager.getById(mediaId)
|
libraryItemWrapper = playerNotificationService.mediaManager.getById(mediaId)
|
||||||
|
|
||||||
|
if (libraryItemWrapper == null) {
|
||||||
|
Log.e(tag, "onPlayFromMediaId: Media item not found $mediaId")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
libraryItemWrapper?.let { li ->
|
libraryItemWrapper?.let { li ->
|
||||||
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
playerNotificationService.mediaManager.play(li, podcastEpisode, playerNotificationService.getPlayItemRequestPayload(false)) {
|
||||||
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,true,null)
|
playerNotificationService.preparePlayer(it,true,null)
|
||||||
@@ -112,10 +119,13 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
|
|
||||||
fun handleCallMediaButton(intent: Intent): Boolean {
|
fun handleCallMediaButton(intent: Intent): Boolean {
|
||||||
if(Intent.ACTION_MEDIA_BUTTON == intent.action) {
|
if(Intent.ACTION_MEDIA_BUTTON == intent.action) {
|
||||||
var keyEvent = intent.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT)
|
val keyEvent = intent.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT)
|
||||||
if (keyEvent?.getAction() == KeyEvent.ACTION_UP) {
|
|
||||||
when (keyEvent?.getKeyCode()) {
|
if (keyEvent?.action == KeyEvent.ACTION_UP) {
|
||||||
|
Log.d(tag, "handleCallMediaButton: key action_up for ${keyEvent.keyCode}")
|
||||||
|
when (keyEvent.keyCode) {
|
||||||
KeyEvent.KEYCODE_HEADSETHOOK -> {
|
KeyEvent.KEYCODE_HEADSETHOOK -> {
|
||||||
|
Log.d(tag, "handleCallMediaButton: Headset Hook")
|
||||||
if (0 == mediaButtonClickCount) {
|
if (0 == mediaButtonClickCount) {
|
||||||
if (playerNotificationService.mPlayer.isPlaying)
|
if (playerNotificationService.mPlayer.isPlaying)
|
||||||
playerNotificationService.pause()
|
playerNotificationService.pause()
|
||||||
@@ -125,6 +135,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
handleMediaButtonClickCount()
|
handleMediaButtonClickCount()
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_MEDIA_PLAY -> {
|
KeyEvent.KEYCODE_MEDIA_PLAY -> {
|
||||||
|
Log.d(tag, "handleCallMediaButton: Media Play")
|
||||||
if (0 == mediaButtonClickCount) {
|
if (0 == mediaButtonClickCount) {
|
||||||
playerNotificationService.play()
|
playerNotificationService.play()
|
||||||
playerNotificationService.sleepTimerManager.checkShouldExtendSleepTimer()
|
playerNotificationService.sleepTimerManager.checkShouldExtendSleepTimer()
|
||||||
@@ -132,6 +143,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
handleMediaButtonClickCount()
|
handleMediaButtonClickCount()
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
|
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
|
||||||
|
Log.d(tag, "handleCallMediaButton: Media Pause")
|
||||||
if (0 == mediaButtonClickCount) playerNotificationService.pause()
|
if (0 == mediaButtonClickCount) playerNotificationService.pause()
|
||||||
handleMediaButtonClickCount()
|
handleMediaButtonClickCount()
|
||||||
}
|
}
|
||||||
@@ -145,6 +157,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
playerNotificationService.closePlayback()
|
playerNotificationService.closePlayback()
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
|
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
|
||||||
|
Log.d(tag, "handleCallMediaButton: Media Play/Pause")
|
||||||
if (playerNotificationService.mPlayer.isPlaying) {
|
if (playerNotificationService.mPlayer.isPlaying) {
|
||||||
if (0 == mediaButtonClickCount) playerNotificationService.pause()
|
if (0 == mediaButtonClickCount) playerNotificationService.pause()
|
||||||
handleMediaButtonClickCount()
|
handleMediaButtonClickCount()
|
||||||
@@ -157,7 +170,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Log.d(tag, "KeyCode:${keyEvent.getKeyCode()}")
|
Log.d(tag, "KeyCode:${keyEvent.keyCode}")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,7 +179,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleMediaButtonClickCount() {
|
private fun handleMediaButtonClickCount() {
|
||||||
mediaButtonClickCount++
|
mediaButtonClickCount++
|
||||||
if (1 == mediaButtonClickCount) {
|
if (1 == mediaButtonClickCount) {
|
||||||
Timer().schedule(mediaButtonClickTimeout) {
|
Timer().schedule(mediaButtonClickTimeout) {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import android.os.Looper
|
|||||||
import android.os.ResultReceiver
|
import android.os.ResultReceiver
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.data.LibraryItem
|
|
||||||
import com.audiobookshelf.app.data.LibraryItemWrapper
|
import com.audiobookshelf.app.data.LibraryItemWrapper
|
||||||
|
import com.audiobookshelf.app.data.PodcastEpisode
|
||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
|||||||
override fun onPrepare(playWhenReady: Boolean) {
|
override fun onPrepare(playWhenReady: Boolean) {
|
||||||
Log.d(tag, "ON PREPARE $playWhenReady")
|
Log.d(tag, "ON PREPARE $playWhenReady")
|
||||||
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
|
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
|
||||||
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getPlayItemRequestPayload(false)) {
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,playWhenReady,null)
|
playerNotificationService.preparePlayer(it,playWhenReady,null)
|
||||||
}
|
}
|
||||||
@@ -41,9 +41,19 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
|||||||
override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
|
override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
|
||||||
Log.d(tag, "ON PREPARE FROM MEDIA ID $mediaId $playWhenReady")
|
Log.d(tag, "ON PREPARE FROM MEDIA ID $mediaId $playWhenReady")
|
||||||
|
|
||||||
var libraryItemWrapper: LibraryItemWrapper? = playerNotificationService.mediaManager.getById(mediaId)
|
var libraryItemWrapper: LibraryItemWrapper? = null
|
||||||
|
var podcastEpisode: PodcastEpisode? = null
|
||||||
|
|
||||||
|
if (mediaId.startsWith("ep_") || mediaId.startsWith("local_ep_")) { // Playing podcast episode
|
||||||
|
val libraryItemWithEpisode = playerNotificationService.mediaManager.getPodcastWithEpisodeByEpisodeId(mediaId)
|
||||||
|
libraryItemWrapper = libraryItemWithEpisode?.libraryItemWrapper
|
||||||
|
podcastEpisode = libraryItemWithEpisode?.episode
|
||||||
|
} else {
|
||||||
|
libraryItemWrapper = playerNotificationService.mediaManager.getById(mediaId)
|
||||||
|
}
|
||||||
|
|
||||||
libraryItemWrapper?.let { li ->
|
libraryItemWrapper?.let { li ->
|
||||||
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
playerNotificationService.mediaManager.play(li, podcastEpisode, playerNotificationService.getPlayItemRequestPayload(false)) {
|
||||||
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,playWhenReady,null)
|
playerNotificationService.preparePlayer(it,playWhenReady,null)
|
||||||
@@ -55,7 +65,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
|||||||
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
|
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
|
||||||
Log.d(tag, "ON PREPARE FROM SEARCH $query")
|
Log.d(tag, "ON PREPARE FROM SEARCH $query")
|
||||||
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
|
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
|
||||||
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
|
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getPlayItemRequestPayload(false)) {
|
||||||
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
Log.d(tag, "About to prepare player with ${it.displayTitle}")
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
playerNotificationService.preparePlayer(it,playWhenReady,null)
|
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.PlaybackException
|
||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
|
|
||||||
|
const val PAUSE_LEN_BEFORE_RECHECK = 30000 // 30 seconds
|
||||||
|
|
||||||
class PlayerListener(var playerNotificationService:PlayerNotificationService) : Player.Listener {
|
class PlayerListener(var playerNotificationService:PlayerNotificationService) : Player.Listener {
|
||||||
var tag = "PlayerListener"
|
var tag = "PlayerListener"
|
||||||
|
|
||||||
@@ -15,7 +17,7 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||||||
private var onSeekBack: Boolean = false
|
private var onSeekBack: Boolean = false
|
||||||
|
|
||||||
override fun onPlayerError(error: PlaybackException) {
|
override fun onPlayerError(error: PlaybackException) {
|
||||||
var errorMessage = error.message ?: "Unknown Error"
|
val errorMessage = error.message ?: "Unknown Error"
|
||||||
Log.e(tag, "onPlayerError $errorMessage")
|
Log.e(tag, "onPlayerError $errorMessage")
|
||||||
playerNotificationService.handlePlayerPlaybackError(errorMessage) // If was direct playing session, fallback to transcode
|
playerNotificationService.handlePlayerPlaybackError(errorMessage) // If was direct playing session, fallback to transcode
|
||||||
}
|
}
|
||||||
@@ -81,6 +83,13 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||||||
Log.d(tag, "SeekBackTime: back time is 0")
|
Log.d(tag, "SeekBackTime: back time is 0")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if playback session still exists or sync media progress if updated
|
||||||
|
val pauseLength: Long = System.currentTimeMillis() - lastPauseTime
|
||||||
|
if (pauseLength > PAUSE_LEN_BEFORE_RECHECK) {
|
||||||
|
val shouldCarryOn = playerNotificationService.checkCurrentSessionProgress()
|
||||||
|
if (!shouldCarryOn) return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.d(tag, "SeekBackTime: Player not playing set last pause time")
|
Log.d(tag, "SeekBackTime: Player not playing set last pause time")
|
||||||
@@ -90,6 +99,7 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||||||
// Start/stop progress sync interval
|
// Start/stop progress sync interval
|
||||||
Log.d(tag, "Playing ${playerNotificationService.getCurrentBookTitle()}")
|
Log.d(tag, "Playing ${playerNotificationService.getCurrentBookTitle()}")
|
||||||
if (player.isPlaying) {
|
if (player.isPlaying) {
|
||||||
|
player.volume = 1F // Volume on sleep timer might have decreased this
|
||||||
playerNotificationService.mediaProgressSyncer.start()
|
playerNotificationService.mediaProgressSyncer.start()
|
||||||
} else {
|
} else {
|
||||||
playerNotificationService.mediaProgressSyncer.stop()
|
playerNotificationService.mediaProgressSyncer.stop()
|
||||||
@@ -101,8 +111,8 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||||||
|
|
||||||
private fun calcPauseSeekBackTime() : Long {
|
private fun calcPauseSeekBackTime() : Long {
|
||||||
if (lastPauseTime <= 0) return 0
|
if (lastPauseTime <= 0) return 0
|
||||||
var time: Long = System.currentTimeMillis() - lastPauseTime
|
val time: Long = System.currentTimeMillis() - lastPauseTime
|
||||||
var seekback: Long
|
val seekback: Long
|
||||||
if (time < 3000) seekback = 0
|
if (time < 3000) seekback = 0
|
||||||
else if (time < 300000) seekback = 10000 // 3s to 5m = jump back 10s
|
else if (time < 300000) seekback = 10000 // 3s to 5m = jump back 10s
|
||||||
else if (time < 1800000) seekback = 20000 // 5m to 30m = jump back 20s
|
else if (time < 1800000) seekback = 20000 // 5m to 30m = jump back 20s
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ import androidx.annotation.RequiresApi
|
|||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.media.MediaBrowserServiceCompat
|
import androidx.media.MediaBrowserServiceCompat
|
||||||
import androidx.media.utils.MediaConstants
|
import androidx.media.utils.MediaConstants
|
||||||
|
import com.audiobookshelf.app.BuildConfig
|
||||||
import com.audiobookshelf.app.data.*
|
import com.audiobookshelf.app.data.*
|
||||||
|
import com.audiobookshelf.app.data.DeviceInfo
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import com.audiobookshelf.app.media.MediaManager
|
import com.audiobookshelf.app.media.MediaManager
|
||||||
import com.audiobookshelf.app.server.ApiHandler
|
import com.audiobookshelf.app.server.ApiHandler
|
||||||
@@ -46,12 +48,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
fun onPlaybackClosed()
|
fun onPlaybackClosed()
|
||||||
fun onPlayingUpdate(isPlaying: Boolean)
|
fun onPlayingUpdate(isPlaying: Boolean)
|
||||||
fun onMetadata(metadata: PlaybackMetadata)
|
fun onMetadata(metadata: PlaybackMetadata)
|
||||||
fun onPrepare(audiobookId: String, playWhenReady: Boolean)
|
|
||||||
fun onSleepTimerEnded(currentPosition: Long)
|
fun onSleepTimerEnded(currentPosition: Long)
|
||||||
fun onSleepTimerSet(sleepTimeRemaining: Int)
|
fun onSleepTimerSet(sleepTimeRemaining: Int)
|
||||||
fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress)
|
fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress)
|
||||||
fun onPlaybackFailed(errorMessage:String)
|
fun onPlaybackFailed(errorMessage:String)
|
||||||
fun onMediaPlayerChanged(mediaPlayer:String)
|
fun onMediaPlayerChanged(mediaPlayer:String)
|
||||||
|
fun onProgressSyncFailing()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val tag = "PlayerService"
|
private val tag = "PlayerService"
|
||||||
@@ -75,7 +77,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
lateinit var sleepTimerManager:SleepTimerManager
|
lateinit var sleepTimerManager:SleepTimerManager
|
||||||
lateinit var mediaProgressSyncer:MediaProgressSyncer
|
lateinit var mediaProgressSyncer:MediaProgressSyncer
|
||||||
|
|
||||||
private var notificationId = 10;
|
private var notificationId = 10
|
||||||
private var channelId = "audiobookshelf_channel"
|
private var channelId = "audiobookshelf_channel"
|
||||||
private var channelName = "Audiobookshelf Channel"
|
private var channelName = "Audiobookshelf Channel"
|
||||||
|
|
||||||
@@ -100,7 +102,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
// Android Auto Media Browser Service
|
// Android Auto Media Browser Service
|
||||||
if (SERVICE_INTERFACE == intent.action) {
|
if (SERVICE_INTERFACE == intent.action) {
|
||||||
Log.d(tag, "Is Media Browser Service")
|
Log.d(tag, "Is Media Browser Service")
|
||||||
return super.onBind(intent);
|
return super.onBind(intent)
|
||||||
}
|
}
|
||||||
return binder
|
return binder
|
||||||
}
|
}
|
||||||
@@ -245,11 +247,21 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
mediaSessionConnector = MediaSessionConnector(mediaSession)
|
mediaSessionConnector = MediaSessionConnector(mediaSession)
|
||||||
val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) {
|
val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) {
|
||||||
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
|
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
|
||||||
|
val coverUri = currentPlaybackSession!!.getCoverUri()
|
||||||
|
|
||||||
|
// Fix for local images crashing on Android 11 for specific devices
|
||||||
|
// https://stackoverflow.com/questions/64186578/android-11-mediastyle-notification-crash/64232958#64232958
|
||||||
|
ctx.grantUriPermission(
|
||||||
|
"com.android.systemui",
|
||||||
|
coverUri,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
)
|
||||||
|
|
||||||
return MediaDescriptionCompat.Builder()
|
return MediaDescriptionCompat.Builder()
|
||||||
.setMediaId(currentPlaybackSession!!.id)
|
.setMediaId(currentPlaybackSession!!.id)
|
||||||
.setTitle(currentPlaybackSession!!.displayTitle)
|
.setTitle(currentPlaybackSession!!.displayTitle)
|
||||||
.setSubtitle(currentPlaybackSession!!.displayAuthor)
|
.setSubtitle(currentPlaybackSession!!.displayAuthor)
|
||||||
.setIconUri(currentPlaybackSession!!.getCoverUri()).build()
|
.setIconUri(coverUri).build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -346,7 +358,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
Log.d(tag, "Prepare complete for session ${currentPlaybackSession?.displayTitle} | ${currentPlayer.mediaItemCount}")
|
Log.d(tag, "Prepare complete for session ${currentPlaybackSession?.displayTitle} | ${currentPlayer.mediaItemCount}")
|
||||||
currentPlayer.playWhenReady = playWhenReady
|
currentPlayer.playWhenReady = playWhenReady
|
||||||
currentPlayer.setPlaybackSpeed(playbackRateToUse)
|
currentPlayer.setPlaybackSpeed(playbackRateToUse)
|
||||||
|
|
||||||
currentPlayer.prepare()
|
currentPlayer.prepare()
|
||||||
|
|
||||||
} else if (castPlayer != null) {
|
} else if (castPlayer != null) {
|
||||||
val currentTrackIndex = playbackSession.getCurrentTrackIndex()
|
val currentTrackIndex = playbackSession.getCurrentTrackIndex()
|
||||||
val currentTrackTime = playbackSession.getCurrentTrackTimeMs()
|
val currentTrackTime = playbackSession.getCurrentTrackTimeMs()
|
||||||
@@ -361,13 +375,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
// On error and was attempting to direct play - fallback to transcode
|
// On error and was attempting to direct play - fallback to transcode
|
||||||
currentPlaybackSession?.let { playbackSession ->
|
currentPlaybackSession?.let { playbackSession ->
|
||||||
if (playbackSession.isDirectPlay) {
|
if (playbackSession.isDirectPlay) {
|
||||||
val mediaPlayer = getMediaPlayer()
|
val playItemRequestPayload = getPlayItemRequestPayload(true)
|
||||||
Log.d(tag, "Fallback to transcode $mediaPlayer")
|
Log.d(tag, "Fallback to transcode $playItemRequestPayload.mediaPlayer")
|
||||||
|
|
||||||
val libraryItemId = playbackSession.libraryItemId ?: "" // Must be true since direct play
|
val libraryItemId = playbackSession.libraryItemId ?: "" // Must be true since direct play
|
||||||
val episodeId = playbackSession.episodeId
|
val episodeId = playbackSession.episodeId
|
||||||
apiHandler.playLibraryItem(libraryItemId, episodeId, true, mediaPlayer) {
|
apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) {
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post {
|
||||||
preparePlayer(it, true, null)
|
preparePlayer(it, true, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -378,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) {
|
fun switchToPlayer(useCastPlayer: Boolean) {
|
||||||
val wasPlaying = currentPlayer.isPlaying
|
val wasPlaying = currentPlayer.isPlaying
|
||||||
if (useCastPlayer) {
|
if (useCastPlayer) {
|
||||||
@@ -469,6 +498,76 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
return currentPlaybackSession?.id
|
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) {
|
||||||
|
|
||||||
|
// Make sure this connection config exists
|
||||||
|
val serverConnectionConfig = DeviceManager.getServerConnectionConfig(playbackSession.serverConnectionConfigId)
|
||||||
|
if (serverConnectionConfig == null) {
|
||||||
|
Log.d(tag, "checkCurrentSessionProgress: Local library item server connection config is not saved ${playbackSession.serverConnectionConfigId}")
|
||||||
|
return true // carry on
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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, serverConnectionConfig) { mediaProgress ->
|
||||||
|
|
||||||
|
if (mediaProgress != null && 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() {
|
fun play() {
|
||||||
if (currentPlayer.isPlaying) {
|
if (currentPlayer.isPlaying) {
|
||||||
Log.d(tag, "Already playing")
|
Log.d(tag, "Already playing")
|
||||||
@@ -510,12 +609,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
fun seekForward(amount: Long) {
|
fun seekForward(amount: Long) {
|
||||||
seekPlayer(getCurrentTime() + amount)
|
seekPlayer(getCurrentTime() + amount)
|
||||||
// currentPlayer.seekTo(currentPlayer.currentPosition + amount)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun seekBackward(amount: Long) {
|
fun seekBackward(amount: Long) {
|
||||||
seekPlayer(getCurrentTime() - amount)
|
seekPlayer(getCurrentTime() - amount)
|
||||||
// currentPlayer.seekTo(currentPlayer.currentPosition - amount)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setPlaybackSpeed(speed: Float) {
|
fun setPlaybackSpeed(speed: Float) {
|
||||||
@@ -539,10 +636,29 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
return if(currentPlayer == castPlayer) "cast-player" else "exo-player"
|
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 {
|
fun getContext():Context {
|
||||||
return ctx
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun alertSyncFailing() {
|
||||||
|
clientEventEmitter?.onProgressSyncFailing()
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
||||||
//
|
//
|
||||||
@@ -555,6 +671,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
private val AUTO_MEDIA_ROOT = "/"
|
private val AUTO_MEDIA_ROOT = "/"
|
||||||
private val ALL_ROOT = "__ALL__"
|
private val ALL_ROOT = "__ALL__"
|
||||||
|
private val LIBRARIES_ROOT = "__LIBRARIES__"
|
||||||
private lateinit var browseTree:BrowseTree
|
private lateinit var browseTree:BrowseTree
|
||||||
|
|
||||||
|
|
||||||
@@ -580,6 +697,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
mediaManager.initializeAndroidAuto()
|
mediaManager.initializeAndroidAuto()
|
||||||
isStarted = true
|
isStarted = true
|
||||||
}
|
}
|
||||||
|
mediaManager.checkResetServerItems() // Reset any server items if no longer connected to server
|
||||||
|
|
||||||
isAndroidAuto = true
|
isAndroidAuto = true
|
||||||
|
|
||||||
@@ -600,32 +718,66 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
override fun onLoadChildren(parentMediaId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
override fun onLoadChildren(parentMediaId: String, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
Log.d(tag, "ON LOAD CHILDREN $parentMediaId")
|
Log.d(tag, "ON LOAD CHILDREN $parentMediaId")
|
||||||
|
|
||||||
val flag = if (parentMediaId == AUTO_MEDIA_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
var flag = if (parentMediaId == AUTO_MEDIA_ROOT || parentMediaId == LIBRARIES_ROOT) MediaBrowserCompat.MediaItem.FLAG_BROWSABLE else MediaBrowserCompat.MediaItem.FLAG_PLAYABLE
|
||||||
|
|
||||||
result.detach()
|
result.detach()
|
||||||
|
|
||||||
mediaManager.loadAndroidAutoItems("main") { libraryCategories ->
|
if (parentMediaId.startsWith("li_") || parentMediaId.startsWith("local_")) { // Show podcast episodes
|
||||||
browseTree = BrowseTree(this, libraryCategories)
|
Log.d(tag, "Loading podcast episodes")
|
||||||
val children = browseTree[parentMediaId]?.map { item ->
|
mediaManager.loadPodcastEpisodeMediaBrowserItems(parentMediaId) {
|
||||||
MediaBrowserCompat.MediaItem(item.description, flag)
|
result.sendResult(it)
|
||||||
}
|
}
|
||||||
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
} else if (::browseTree.isInitialized && browseTree[parentMediaId] == null && mediaManager.getIsLibrary(parentMediaId)) { // Load library items for library
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: For using sub menus. Check if this is the root menu:
|
mediaManager.loadLibraryItemsWithAudio(parentMediaId) { libraryItems ->
|
||||||
// if (AUTO_MEDIA_ROOT == parentMediaId) {
|
val children = libraryItems.map { libraryItem ->
|
||||||
// build the MediaItem objects for the top level,
|
val libraryItemMediaMetadata = libraryItem.getMediaMetadata()
|
||||||
// and put them in the mediaItems list
|
|
||||||
// } else {
|
if (libraryItem.mediaType == "podcast") { // Podcasts are browseable
|
||||||
// examine the passed parentMediaId to see which submenu we're at,
|
flag = MediaBrowserCompat.MediaItem.FLAG_BROWSABLE
|
||||||
// and put the children of that menu in the mediaItems list
|
}
|
||||||
// }
|
|
||||||
|
MediaBrowserCompat.MediaItem(libraryItemMediaMetadata.description, flag)
|
||||||
|
}
|
||||||
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
|
}
|
||||||
|
} else if (parentMediaId == "__DOWNLOADS__") { // Load downloads
|
||||||
|
|
||||||
|
val localBooks = DeviceManager.dbManager.getLocalLibraryItems("book")
|
||||||
|
val localPodcasts = DeviceManager.dbManager.getLocalLibraryItems("podcast")
|
||||||
|
val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||||
|
|
||||||
|
localBooks.forEach { localLibraryItem ->
|
||||||
|
val mediaMetadata = localLibraryItem.getMediaMetadata(ctx)
|
||||||
|
localBrowseItems += MediaBrowserCompat.MediaItem(mediaMetadata.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
localPodcasts.forEach { localLibraryItem ->
|
||||||
|
val mediaMetadata = localLibraryItem.getMediaMetadata(ctx)
|
||||||
|
localBrowseItems += MediaBrowserCompat.MediaItem(mediaMetadata.description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||||
|
}
|
||||||
|
|
||||||
|
result.sendResult(localBrowseItems)
|
||||||
|
|
||||||
|
} else { // Load categories
|
||||||
|
|
||||||
|
mediaManager.loadAndroidAutoItems() { libraryCategories ->
|
||||||
|
browseTree = BrowseTree(this, libraryCategories, mediaManager.serverLibraries)
|
||||||
|
|
||||||
|
val children = browseTree[parentMediaId]?.map { item ->
|
||||||
|
Log.d(tag, "Loading Browser Media Item ${item.description.title} $flag")
|
||||||
|
|
||||||
|
MediaBrowserCompat.MediaItem(item.description, flag)
|
||||||
|
}
|
||||||
|
result.sendResult(children as MutableList<MediaBrowserCompat.MediaItem>?)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSearch(query: String, extras: Bundle?, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
override fun onSearch(query: String, extras: Bundle?, result: Result<MutableList<MediaBrowserCompat.MediaItem>>) {
|
||||||
result.detach()
|
result.detach()
|
||||||
mediaManager.loadAndroidAutoItems("main") { libraryCategories ->
|
mediaManager.loadAndroidAutoItems() { libraryCategories ->
|
||||||
browseTree = BrowseTree(this, libraryCategories)
|
browseTree = BrowseTree(this, libraryCategories, mediaManager.serverLibraries)
|
||||||
val children = browseTree[ALL_ROOT]?.map { item ->
|
val children = browseTree[ALL_ROOT]?.map { item ->
|
||||||
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
MediaBrowserCompat.MediaItem(item.description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,13 +59,6 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
notifyListeners("onMetadata", JSObject(jacksonMapper.writeValueAsString(metadata)))
|
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) {
|
override fun onSleepTimerEnded(currentPosition: Long) {
|
||||||
emit("onSleepTimerEnded", currentPosition)
|
emit("onSleepTimerEnded", currentPosition)
|
||||||
}
|
}
|
||||||
@@ -85,6 +78,10 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
override fun onMediaPlayerChanged(mediaPlayer:String) {
|
override fun onMediaPlayerChanged(mediaPlayer:String) {
|
||||||
emit("onMediaPlayerChanged", mediaPlayer)
|
emit("onMediaPlayerChanged", mediaPlayer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onProgressSyncFailing() {
|
||||||
|
emit("onProgressSyncFailing", "")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
mainActivity.pluginCallback = foregroundServiceReady
|
mainActivity.pluginCallback = foregroundServiceReady
|
||||||
@@ -191,9 +188,9 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
return call.resolve(JSObject())
|
return call.resolve(JSObject())
|
||||||
}
|
}
|
||||||
} else { // Play library item from server
|
} 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 {
|
Handler(Looper.getMainLooper()).post {
|
||||||
Log.d(tag, "Preparing Player TEST ${jacksonMapper.writeValueAsString(it)}")
|
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.audiobookshelf.app.server.ApiHandler
|
||||||
import com.fasterxml.jackson.core.json.JsonReadFeature
|
import com.fasterxml.jackson.core.json.JsonReadFeature
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
import com.getcapacitor.*
|
import com.getcapacitor.*
|
||||||
import com.getcapacitor.annotation.CapacitorPlugin
|
import com.getcapacitor.annotation.CapacitorPlugin
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -188,7 +189,7 @@ class AbsDatabase : Plugin() {
|
|||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun removeLocalMediaProgress(call:PluginCall) {
|
fun removeLocalMediaProgress(call:PluginCall) {
|
||||||
var localMediaProgressId = call.getString("localMediaProgressId", "").toString()
|
val localMediaProgressId = call.getString("localMediaProgressId", "").toString()
|
||||||
DeviceManager.dbManager.removeLocalMediaProgress(localMediaProgressId)
|
DeviceManager.dbManager.removeLocalMediaProgress(localMediaProgressId)
|
||||||
call.resolve()
|
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
|
@PluginMethod
|
||||||
fun updateLocalMediaProgressFinished(call:PluginCall) {
|
fun updateLocalMediaProgressFinished(call:PluginCall) {
|
||||||
val localLibraryItemId = call.getString("localLibraryItemId", "").toString()
|
val localLibraryItemId = call.getString("localLibraryItemId", "").toString()
|
||||||
|
|||||||
@@ -277,7 +277,7 @@ class AbsDownloader : Plugin() {
|
|||||||
finalDestinationFile.delete()
|
finalDestinationFile.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
var downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,null)
|
var downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,episode)
|
||||||
downloadItem.downloadItemParts.add(downloadItemPart)
|
downloadItem.downloadItemParts.add(downloadItemPart)
|
||||||
|
|
||||||
var dlRequest = downloadItemPart.getDownloadRequest()
|
var dlRequest = downloadItemPart.getDownloadRequest()
|
||||||
@@ -294,7 +294,7 @@ class AbsDownloader : Plugin() {
|
|||||||
if (finalDestinationFile.exists()) {
|
if (finalDestinationFile.exists()) {
|
||||||
Log.d(tag, "Podcast cover already exists - not downloading cover again")
|
Log.d(tag, "Podcast cover already exists - not downloading cover again")
|
||||||
} else {
|
} else {
|
||||||
downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,null)
|
downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,null)
|
||||||
downloadItem.downloadItemParts.add(downloadItemPart)
|
downloadItem.downloadItemParts.add(downloadItemPart)
|
||||||
|
|
||||||
dlRequest = downloadItemPart.getDownloadRequest()
|
dlRequest = downloadItemPart.getDownloadRequest()
|
||||||
|
|||||||
@@ -19,11 +19,13 @@ import okhttp3.MediaType.Companion.toMediaType
|
|||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class ApiHandler(var ctx:Context) {
|
class ApiHandler(var ctx:Context) {
|
||||||
val tag = "ApiHandler"
|
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 jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||||
|
|
||||||
var storageSharedPreferences: SharedPreferences? = null
|
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 MediaProgressSyncResponsePayload(val numServerProgressUpdates:Int, val localProgressUpdates:List<LocalMediaProgress>)
|
||||||
data class LocalMediaProgressSyncResultsPayload(var numLocalMediaProgressForServer:Int, var numServerProgressUpdates:Int, var numLocalProgressUpdates:Int)
|
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()
|
val request = Request.Builder()
|
||||||
.url("${DeviceManager.serverAddress}$endpoint").addHeader("Authorization", "Bearer ${DeviceManager.token}")
|
.url("${address}$endpoint").addHeader("Authorization", "Bearer $token")
|
||||||
.build()
|
.build()
|
||||||
makeRequest(request, cb)
|
makeRequest(request, httpClient, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun postRequest(endpoint:String, payload: JSObject, cb: (JSObject) -> Unit) {
|
fun postRequest(endpoint:String, payload: JSObject, cb: (JSObject) -> Unit) {
|
||||||
@@ -46,7 +51,7 @@ class ApiHandler(var ctx:Context) {
|
|||||||
val request = Request.Builder().post(requestBody)
|
val request = Request.Builder().post(requestBody)
|
||||||
.url("${DeviceManager.serverAddress}$endpoint").addHeader("Authorization", "Bearer ${DeviceManager.token}")
|
.url("${DeviceManager.serverAddress}$endpoint").addHeader("Authorization", "Bearer ${DeviceManager.token}")
|
||||||
.build()
|
.build()
|
||||||
makeRequest(request, cb)
|
makeRequest(request, null, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun patchRequest(endpoint:String, payload: JSObject, cb: (JSObject) -> Unit) {
|
fun patchRequest(endpoint:String, payload: JSObject, cb: (JSObject) -> Unit) {
|
||||||
@@ -55,7 +60,7 @@ class ApiHandler(var ctx:Context) {
|
|||||||
val request = Request.Builder().patch(requestBody)
|
val request = Request.Builder().patch(requestBody)
|
||||||
.url("${DeviceManager.serverAddress}$endpoint").addHeader("Authorization", "Bearer ${DeviceManager.token}")
|
.url("${DeviceManager.serverAddress}$endpoint").addHeader("Authorization", "Bearer ${DeviceManager.token}")
|
||||||
.build()
|
.build()
|
||||||
makeRequest(request, cb)
|
makeRequest(request, null, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isOnline(): Boolean {
|
fun isOnline(): Boolean {
|
||||||
@@ -76,19 +81,28 @@ class ApiHandler(var ctx:Context) {
|
|||||||
return false
|
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 {
|
client.newCall(request).enqueue(object : Callback {
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
Log.d(tag, "FAILURE TO CONNECT")
|
Log.d(tag, "FAILURE TO CONNECT")
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
cb(JSObject())
|
|
||||||
|
val jsobj = JSObject()
|
||||||
|
jsobj.put("error", "Failed to connect")
|
||||||
|
cb(jsobj)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResponse(call: Call, response: Response) {
|
override fun onResponse(call: Call, response: Response) {
|
||||||
response.use {
|
response.use {
|
||||||
if (!it.isSuccessful) throw IOException("Unexpected code $response")
|
if (!it.isSuccessful) {
|
||||||
|
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") {
|
if (bodyString == "OK") {
|
||||||
cb(JSObject())
|
cb(JSObject())
|
||||||
} else {
|
} else {
|
||||||
@@ -108,7 +122,7 @@ class ApiHandler(var ctx:Context) {
|
|||||||
|
|
||||||
fun getLibraries(cb: (List<Library>) -> Unit) {
|
fun getLibraries(cb: (List<Library>) -> Unit) {
|
||||||
val mapper = jacksonMapper
|
val mapper = jacksonMapper
|
||||||
getRequest("/api/libraries") {
|
getRequest("/api/libraries", null,null) {
|
||||||
val libraries = mutableListOf<Library>()
|
val libraries = mutableListOf<Library>()
|
||||||
if (it.has("value")) {
|
if (it.has("value")) {
|
||||||
val array = it.getJSONArray("value")
|
val array = it.getJSONArray("value")
|
||||||
@@ -122,7 +136,7 @@ class ApiHandler(var ctx:Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getLibraryItem(libraryItemId:String, cb: (LibraryItem) -> Unit) {
|
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())
|
val libraryItem = jacksonMapper.readValue<LibraryItem>(it.toString())
|
||||||
cb(libraryItem)
|
cb(libraryItem)
|
||||||
}
|
}
|
||||||
@@ -131,14 +145,14 @@ class ApiHandler(var ctx:Context) {
|
|||||||
fun getLibraryItemWithProgress(libraryItemId:String, episodeId:String?, cb: (LibraryItem) -> Unit) {
|
fun getLibraryItemWithProgress(libraryItemId:String, episodeId:String?, cb: (LibraryItem) -> Unit) {
|
||||||
var requestUrl = "/api/items/$libraryItemId?expanded=1&include=progress"
|
var requestUrl = "/api/items/$libraryItemId?expanded=1&include=progress"
|
||||||
if (!episodeId.isNullOrEmpty()) requestUrl += "&episode=$episodeId"
|
if (!episodeId.isNullOrEmpty()) requestUrl += "&episode=$episodeId"
|
||||||
getRequest(requestUrl) {
|
getRequest(requestUrl, null, null) {
|
||||||
val libraryItem = jacksonMapper.readValue<LibraryItem>(it.toString())
|
val libraryItem = jacksonMapper.readValue<LibraryItem>(it.toString())
|
||||||
cb(libraryItem)
|
cb(libraryItem)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLibraryItems(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
|
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>()
|
val items = mutableListOf<LibraryItem>()
|
||||||
if (it.has("results")) {
|
if (it.has("results")) {
|
||||||
val array = it.getJSONArray("results")
|
val array = it.getJSONArray("results")
|
||||||
@@ -152,7 +166,7 @@ class ApiHandler(var ctx:Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getLibraryCategories(libraryId:String, cb: (List<LibraryCategory>) -> Unit) {
|
fun getLibraryCategories(libraryId:String, cb: (List<LibraryCategory>) -> Unit) {
|
||||||
getRequest("/api/libraries/$libraryId/personalized") {
|
getRequest("/api/libraries/$libraryId/personalized", null, null) {
|
||||||
val items = mutableListOf<LibraryCategory>()
|
val items = mutableListOf<LibraryCategory>()
|
||||||
if (it.has("value")) {
|
if (it.has("value")) {
|
||||||
val array = it.getJSONArray("value")
|
val array = it.getJSONArray("value")
|
||||||
@@ -172,13 +186,8 @@ class ApiHandler(var ctx:Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun playLibraryItem(libraryItemId:String, episodeId:String?, forceTranscode:Boolean, mediaPlayer:String, cb: (PlaybackSession) -> Unit) {
|
fun playLibraryItem(libraryItemId:String, episodeId:String?, playItemRequestPayload:PlayItemRequestPayload, cb: (PlaybackSession) -> Unit) {
|
||||||
val payload = JSObject()
|
val payload = JSObject(jacksonMapper.writeValueAsString(playItemRequestPayload))
|
||||||
payload.put("mediaPlayer", mediaPlayer)
|
|
||||||
|
|
||||||
// Only if direct play fails do we force transcode
|
|
||||||
if (!forceTranscode) payload.put("forceDirectPlay", true)
|
|
||||||
else payload.put("forceTranscode", true)
|
|
||||||
|
|
||||||
val endpoint = if (episodeId.isNullOrEmpty()) "/api/items/$libraryItemId/play" else "/api/items/$libraryItemId/play/$episodeId"
|
val endpoint = if (episodeId.isNullOrEmpty()) "/api/items/$libraryItemId/play" else "/api/items/$libraryItemId/play/$episodeId"
|
||||||
postRequest(endpoint, payload) {
|
postRequest(endpoint, payload) {
|
||||||
@@ -189,11 +198,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))
|
val payload = JSObject(jacksonMapper.writeValueAsString(syncData))
|
||||||
|
|
||||||
postRequest("/api/session/$sessionId/sync", payload) {
|
postRequest("/api/session/$sessionId/sync", payload) {
|
||||||
cb()
|
if (!it.getString("error").isNullOrEmpty()) {
|
||||||
|
cb(false)
|
||||||
|
} else {
|
||||||
|
cb(true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,4 +270,45 @@ class ApiHandler(var ctx:Context) {
|
|||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getMediaProgress(libraryItemId:String, episodeId:String?, serverConnectionConfig:ServerConnectionConfig?, cb: (MediaProgress?) -> Unit) {
|
||||||
|
val endpoint = if(episodeId.isNullOrEmpty()) "/api/me/progress/$libraryItemId" else "/api/me/progress/$libraryItemId/$episodeId"
|
||||||
|
|
||||||
|
// TODO: Using ping client here allows for shorter timeout (3 seconds), maybe rename or make diff client for requests requiring quicker response
|
||||||
|
getRequest(endpoint, pingClient, serverConnectionConfig) {
|
||||||
|
if (it.has("error")) {
|
||||||
|
Log.e(tag, "getMediaProgress: Failed to get progress")
|
||||||
|
cb(null)
|
||||||
|
} else {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,6 @@
|
|||||||
android:translateY="-1.5294118">
|
android:translateY="-1.5294118">
|
||||||
<path
|
<path
|
||||||
android:fillColor="@android:color/white"
|
android:fillColor="@android:color/white"
|
||||||
android:pathData="M4,6H2v14c0,1.1 0.9,2 2,2h14v-2H4V6z"/>
|
android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z"/>
|
||||||
<path
|
|
||||||
android:fillColor="@android:color/white"
|
|
||||||
android:pathData="M20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM20,12l-2.5,-1.5L15,12L15,4h5v8z"/>
|
|
||||||
</group>
|
</group>
|
||||||
</vector>
|
</vector>
|
||||||
|
Before Width: | Height: | Size: 276 B |
|
After Width: | Height: | Size: 216 B |
|
Before Width: | Height: | Size: 199 B |
|
After Width: | Height: | Size: 164 B |
|
Before Width: | Height: | Size: 309 B |
|
After Width: | Height: | Size: 254 B |
|
Before Width: | Height: | Size: 430 B |
|
After Width: | Height: | Size: 466 B |
@@ -19,6 +19,3 @@ project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacit
|
|||||||
|
|
||||||
include ':capacitor-storage'
|
include ':capacitor-storage'
|
||||||
project(':capacitor-storage').projectDir = new File('../node_modules/@capacitor/storage/android')
|
project(':capacitor-storage').projectDir = new File('../node_modules/@capacitor/storage/android')
|
||||||
|
|
||||||
include ':robingenz-capacitor-app-update'
|
|
||||||
project(':robingenz-capacitor-app-update').projectDir = new File('../node_modules/@robingenz/capacitor-app-update/android')
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ body {
|
|||||||
min-height: calc(100% - 64px);
|
min-height: calc(100% - 64px);
|
||||||
max-height: calc(100% - 64px);
|
max-height: calc(100% - 64px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#content.playerOpen {
|
#content.playerOpen {
|
||||||
height: calc(100% - 164px);
|
height: calc(100% - 164px);
|
||||||
min-height: calc(100% - 164px);
|
min-height: calc(100% - 164px);
|
||||||
@@ -46,6 +47,7 @@ body {
|
|||||||
.box-shadow-book {
|
.box-shadow-book {
|
||||||
box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166;
|
box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shadow-height {
|
.shadow-height {
|
||||||
height: calc(100% - 4px);
|
height: calc(100% - 4px);
|
||||||
}
|
}
|
||||||
@@ -53,6 +55,7 @@ body {
|
|||||||
.bookshelfRow {
|
.bookshelfRow {
|
||||||
background-image: url(/wood_panels.jpg);
|
background-image: url(/wood_panels.jpg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bookshelfDivider {
|
.bookshelfDivider {
|
||||||
background: rgb(149, 119, 90);
|
background: rgb(149, 119, 90);
|
||||||
background: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%);
|
background: linear-gradient(180deg, rgba(149, 119, 90, 1) 0%, rgba(103, 70, 37, 1) 17%, rgba(103, 70, 37, 1) 88%, rgba(71, 48, 25, 1) 100%);
|
||||||
@@ -63,9 +66,9 @@ body {
|
|||||||
Bookshelf Label
|
Bookshelf Label
|
||||||
*/
|
*/
|
||||||
.categoryPlacard {
|
.categoryPlacard {
|
||||||
background-image: url(https://image.freepik.com/free-photo/brown-wooden-textured-flooring-background_53876-128537.jpg);
|
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shinyBlack {
|
.shinyBlack {
|
||||||
background-color: #2d3436;
|
background-color: #2d3436;
|
||||||
background-image: linear-gradient(315deg, #19191a 0%, rgb(15, 15, 15) 74%);
|
background-image: linear-gradient(315deg, #19191a 0%, rgb(15, 15, 15) 74%);
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<a v-if="showBack" @click="back" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 mr-2 cursor-pointer">
|
<a v-if="showBack" @click="back" class="rounded-full h-10 w-10 flex items-center justify-center hover:bg-white hover:bg-opacity-10 mr-2 cursor-pointer">
|
||||||
<span class="material-icons text-3xl text-white">arrow_back</span>
|
<span class="material-icons text-3xl text-white">arrow_back</span>
|
||||||
</a>
|
</a>
|
||||||
<div v-if="user">
|
<div v-if="user && currentLibrary">
|
||||||
<div class="pl-3 pr-4 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
|
<div class="pl-3 pr-4 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
|
||||||
<widgets-library-icon :icon="currentLibraryIcon" :size="4" />
|
<widgets-library-icon :icon="currentLibraryIcon" :size="4" />
|
||||||
<p class="text-base font-book leading-4 ml-2 mt-0.5">{{ currentLibraryName }}</p>
|
<p class="text-base font-book leading-4 ml-2 mt-0.5">{{ currentLibraryName }}</p>
|
||||||
@@ -51,14 +51,11 @@ export default {
|
|||||||
this.$store.commit('setCastAvailable', val)
|
this.$store.commit('setCastAvailable', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
socketConnected() {
|
|
||||||
return this.$store.state.socketConnected
|
|
||||||
},
|
|
||||||
currentLibrary() {
|
currentLibrary() {
|
||||||
return this.$store.getters['libraries/getCurrentLibrary']
|
return this.$store.getters['libraries/getCurrentLibrary']
|
||||||
},
|
},
|
||||||
currentLibraryName() {
|
currentLibraryName() {
|
||||||
return this.currentLibrary ? this.currentLibrary.name : 'Main'
|
return this.currentLibrary ? this.currentLibrary.name : ''
|
||||||
},
|
},
|
||||||
currentLibraryIcon() {
|
currentLibraryIcon() {
|
||||||
return this.currentLibrary ? this.currentLibrary.icon : 'database'
|
return this.currentLibrary ? this.currentLibrary.icon : 'database'
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="playbackSession" id="streamContainer" class="fixed top-0 left-0 layout-wrapper right-0 z-50 pointer-events-none" :class="showFullscreen ? 'fullscreen' : ''">
|
<div v-if="playbackSession" id="streamContainer" class="playerContainer fixed top-0 left-0 layout-wrapper right-0 z-50 pointer-events-none" :class="{ fullscreen: showFullscreen, 'ios-player': $platform === 'ios', 'web-player': $platform === 'web' }">
|
||||||
<div v-if="showFullscreen" class="w-full h-full z-10 bg-bg absolute top-0 left-0 pointer-events-auto">
|
<div v-if="showFullscreen" class="w-full h-full z-10 bg-bg absolute top-0 left-0 pointer-events-auto">
|
||||||
<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>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
<p class="author-text text-white text-opacity-75 truncate">by {{ authorName }}</p>
|
<p class="author-text text-white text-opacity-75 truncate">by {{ authorName }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="streamContainer" class="w-full z-20 bg-primary absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" @click="clickContainer">
|
<div id="playerContent" class="playerContainer w-full z-20 bg-primary absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" @click="clickContainer">
|
||||||
<div v-if="showFullscreen" class="absolute top-0 left-0 right-0 w-full py-3 mx-auto px-3" style="max-width: 380px">
|
<div v-if="showFullscreen" class="absolute top-0 left-0 right-0 w-full py-3 mx-auto px-3" style="max-width: 380px">
|
||||||
<div class="flex items-center justify-between pointer-events-auto">
|
<div class="flex items-center justify-between pointer-events-auto">
|
||||||
<span v-if="!isPodcast && !isLocalPlayMethod" class="material-icons text-3xl text-white text-opacity-75 cursor-pointer" @click="$emit('showBookmarks')">{{ bookmarks.length ? 'bookmark' : 'bookmark_border' }}</span>
|
<span v-if="!isPodcast && !isLocalPlayMethod" class="material-icons text-3xl text-white text-opacity-75 cursor-pointer" @click="$emit('showBookmarks')">{{ bookmarks.length ? 'bookmark' : 'bookmark_border' }}</span>
|
||||||
@@ -74,12 +74,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="playerTrack" class="absolute bottom-0 left-0 w-full px-3">
|
<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="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="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="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>
|
||||||
<div class="flex pt-0.5">
|
<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>
|
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem" ref="currentTimestamp">0:00</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p v-show="showFullscreen" class="text-sm truncate text-white text-opacity-75" style="max-width: 65%">{{ currentChapterTitle }}</p>
|
<p v-show="showFullscreen" class="text-sm truncate text-white text-opacity-75" style="max-width: 65%">{{ currentChapterTitle }}</p>
|
||||||
@@ -132,7 +133,9 @@ export default {
|
|||||||
touchStartTime: 0,
|
touchStartTime: 0,
|
||||||
touchEndY: 0,
|
touchEndY: 0,
|
||||||
useChapterTrack: false,
|
useChapterTrack: false,
|
||||||
isLoading: false
|
isLoading: false,
|
||||||
|
touchTrackStart: false,
|
||||||
|
dragPercent: 0
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -268,6 +271,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
touchstartTrack(e) {
|
||||||
|
if (!e || !e.touches || !this.$refs.track || !this.showFullscreen) return
|
||||||
|
this.touchTrackStart = true
|
||||||
|
},
|
||||||
selectChapter(chapter) {
|
selectChapter(chapter) {
|
||||||
this.seek(chapter.start)
|
this.seek(chapter.start)
|
||||||
this.showChapterModal = false
|
this.showChapterModal = false
|
||||||
@@ -517,15 +524,60 @@ export default {
|
|||||||
this.touchStartTime = Date.now()
|
this.touchStartTime = Date.now()
|
||||||
},
|
},
|
||||||
touchend(e) {
|
touchend(e) {
|
||||||
if (!this.showFullscreen || !e.changedTouches) return
|
if (!e.changedTouches) return
|
||||||
|
|
||||||
this.touchEndY = e.changedTouches[0].screenY
|
if (this.touchTrackStart) {
|
||||||
var touchDuration = Date.now() - this.touchStartTime
|
var touch = e.changedTouches[0]
|
||||||
if (touchDuration > 1200) {
|
const touchOnTrackPos = touch.pageX - 12
|
||||||
// console.log('touch too long', touchDuration)
|
const dragPercent = Math.max(0, Math.min(1, touchOnTrackPos / this.trackWidth))
|
||||||
return
|
|
||||||
|
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) {
|
clickMenuAction(action) {
|
||||||
if (action === 'chapter_track') {
|
if (action === 'chapter_track') {
|
||||||
@@ -632,7 +684,7 @@ export default {
|
|||||||
mounted() {
|
mounted() {
|
||||||
document.body.addEventListener('touchstart', this.touchstart)
|
document.body.addEventListener('touchstart', this.touchstart)
|
||||||
document.body.addEventListener('touchend', this.touchend)
|
document.body.addEventListener('touchend', this.touchend)
|
||||||
|
document.body.addEventListener('touchmove', this.touchmove)
|
||||||
this.$nextTick(this.init)
|
this.$nextTick(this.init)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
@@ -644,6 +696,7 @@ export default {
|
|||||||
this.forceCloseDropdownMenu()
|
this.forceCloseDropdownMenu()
|
||||||
document.body.removeEventListener('touchstart', this.touchstart)
|
document.body.removeEventListener('touchstart', this.touchstart)
|
||||||
document.body.removeEventListener('touchend', this.touchend)
|
document.body.removeEventListener('touchend', this.touchend)
|
||||||
|
document.body.removeEventListener('touchmove', this.touchmove)
|
||||||
|
|
||||||
if (this.onPlayingUpdateListener) this.onPlayingUpdateListener.remove()
|
if (this.onPlayingUpdateListener) this.onPlayingUpdateListener.remove()
|
||||||
if (this.onMetadataListener) this.onMetadataListener.remove()
|
if (this.onMetadataListener) this.onMetadataListener.remove()
|
||||||
@@ -659,13 +712,15 @@ export default {
|
|||||||
.bookCoverWrapper {
|
.bookCoverWrapper {
|
||||||
box-shadow: 3px -2px 5px #00000066;
|
box-shadow: 3px -2px 5px #00000066;
|
||||||
}
|
}
|
||||||
#streamContainer {
|
.playerContainer {
|
||||||
box-shadow: 0px -8px 8px #11111155;
|
|
||||||
height: 100px;
|
height: 100px;
|
||||||
}
|
}
|
||||||
.fullscreen #streamContainer {
|
.fullscreen .playerContainer {
|
||||||
height: 200px;
|
height: 200px;
|
||||||
}
|
}
|
||||||
|
#playerContent {
|
||||||
|
box-shadow: 0px -8px 8px #11111155;
|
||||||
|
}
|
||||||
|
|
||||||
#playerTrack {
|
#playerTrack {
|
||||||
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
transition: all 0.15s cubic-bezier(0.39, 0.575, 0.565, 1);
|
||||||
@@ -676,6 +731,11 @@ export default {
|
|||||||
margin-bottom: 18px;
|
margin-bottom: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ios-player #timestamp-row {
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-right: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.cover-wrapper {
|
.cover-wrapper {
|
||||||
bottom: 44px;
|
bottom: 44px;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
|
|||||||
@@ -30,9 +30,11 @@ export default {
|
|||||||
onSleepTimerEndedListener: null,
|
onSleepTimerEndedListener: null,
|
||||||
onSleepTimerSetListener: null,
|
onSleepTimerSetListener: null,
|
||||||
onMediaPlayerChangedListener: null,
|
onMediaPlayerChangedListener: null,
|
||||||
|
onProgressSyncFailing: null,
|
||||||
sleepInterval: null,
|
sleepInterval: null,
|
||||||
currentEndOfChapterTime: 0,
|
currentEndOfChapterTime: 0,
|
||||||
serverLibraryItemId: null
|
serverLibraryItemId: null,
|
||||||
|
syncFailedToast: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -241,6 +243,10 @@ export default {
|
|||||||
onMediaPlayerChanged(data) {
|
onMediaPlayerChanged(data) {
|
||||||
var mediaPlayer = data.value
|
var mediaPlayer = data.value
|
||||||
this.$store.commit('setMediaPlayer', mediaPlayer)
|
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() {
|
mounted() {
|
||||||
@@ -248,6 +254,7 @@ export default {
|
|||||||
this.onSleepTimerEndedListener = AbsAudioPlayer.addListener('onSleepTimerEnded', this.onSleepTimerEnded)
|
this.onSleepTimerEndedListener = AbsAudioPlayer.addListener('onSleepTimerEnded', this.onSleepTimerEnded)
|
||||||
this.onSleepTimerSetListener = AbsAudioPlayer.addListener('onSleepTimerSet', this.onSleepTimerSet)
|
this.onSleepTimerSetListener = AbsAudioPlayer.addListener('onSleepTimerSet', this.onSleepTimerSet)
|
||||||
this.onMediaPlayerChangedListener = AbsAudioPlayer.addListener('onMediaPlayerChanged', this.onMediaPlayerChanged)
|
this.onMediaPlayerChangedListener = AbsAudioPlayer.addListener('onMediaPlayerChanged', this.onMediaPlayerChanged)
|
||||||
|
this.onProgressSyncFailing = AbsAudioPlayer.addListener('onProgressSyncFailing', this.showProgressSyncIsFailing)
|
||||||
|
|
||||||
this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate')
|
this.playbackSpeed = this.$store.getters['user/getUserSetting']('playbackRate')
|
||||||
console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`)
|
console.log(`[AudioPlayerContainer] Init Playback Speed: ${this.playbackSpeed}`)
|
||||||
@@ -264,6 +271,7 @@ export default {
|
|||||||
if (this.onSleepTimerEndedListener) this.onSleepTimerEndedListener.remove()
|
if (this.onSleepTimerEndedListener) this.onSleepTimerEndedListener.remove()
|
||||||
if (this.onSleepTimerSetListener) this.onSleepTimerSetListener.remove()
|
if (this.onSleepTimerSetListener) this.onSleepTimerSetListener.remove()
|
||||||
if (this.onMediaPlayerChangedListener) this.onMediaPlayerChangedListener.remove()
|
if (this.onMediaPlayerChangedListener) this.onMediaPlayerChangedListener.remove()
|
||||||
|
if (this.onProgressSyncFailing) this.onProgressSyncFailing.remove()
|
||||||
|
|
||||||
// if (this.$server.socket) {
|
// if (this.$server.socket) {
|
||||||
// this.$server.socket.off('stream_open', this.streamOpen)
|
// this.$server.socket.off('stream_open', this.streamOpen)
|
||||||
|
|||||||
@@ -74,9 +74,6 @@ export default {
|
|||||||
username() {
|
username() {
|
||||||
return this.user ? this.user.username : ''
|
return this.user ? this.user.username : ''
|
||||||
},
|
},
|
||||||
socketConnected() {
|
|
||||||
return this.$store.state.socketConnected
|
|
||||||
},
|
|
||||||
navItems() {
|
navItems() {
|
||||||
var items = [
|
var items = [
|
||||||
{
|
{
|
||||||
@@ -125,11 +122,15 @@ export default {
|
|||||||
this.show = false
|
this.show = false
|
||||||
},
|
},
|
||||||
async logout() {
|
async logout() {
|
||||||
await this.$axios.$post('/logout').catch((error) => {
|
if (this.user) {
|
||||||
console.error(error)
|
await this.$axios.$post('/logout').catch((error) => {
|
||||||
})
|
console.error(error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
this.$socket.logout()
|
this.$socket.logout()
|
||||||
await this.$db.logout()
|
await this.$db.logout()
|
||||||
|
this.$localStore.removeLastLibraryId()
|
||||||
this.$store.commit('user/logout')
|
this.$store.commit('user/logout')
|
||||||
this.$router.push('/connect')
|
this.$router.push('/connect')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +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 v-else class="flex border-t border-white border-opacity-10" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -25,11 +25,12 @@ export default {
|
|||||||
mixins: [bookshelfCardsHelpers],
|
mixins: [bookshelfCardsHelpers],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
routeFullPath: null,
|
||||||
|
entitiesPerShelf: 2,
|
||||||
bookshelfHeight: 0,
|
bookshelfHeight: 0,
|
||||||
bookshelfWidth: 0,
|
bookshelfWidth: 0,
|
||||||
bookshelfMarginLeft: 0,
|
bookshelfMarginLeft: 0,
|
||||||
shelvesPerPage: 0,
|
shelvesPerPage: 0,
|
||||||
entitiesPerShelf: 2,
|
|
||||||
currentPage: 0,
|
currentPage: 0,
|
||||||
booksPerFetch: 20,
|
booksPerFetch: 20,
|
||||||
initialized: false,
|
initialized: false,
|
||||||
@@ -72,6 +73,7 @@ export default {
|
|||||||
return this.page
|
return this.page
|
||||||
},
|
},
|
||||||
hasFilter() {
|
hasFilter() {
|
||||||
|
if (this.page === 'series' || this.page === 'collections') return false
|
||||||
return this.filterBy !== 'all'
|
return this.filterBy !== 'all'
|
||||||
},
|
},
|
||||||
orderBy() {
|
orderBy() {
|
||||||
@@ -83,14 +85,11 @@ export default {
|
|||||||
filterBy() {
|
filterBy() {
|
||||||
return this.$store.getters['user/getUserSetting']('mobileFilterBy')
|
return this.$store.getters['user/getUserSetting']('mobileFilterBy')
|
||||||
},
|
},
|
||||||
coverAspectRatio() {
|
|
||||||
return this.$store.getters['getServerSetting']('coverAspectRatio')
|
|
||||||
},
|
|
||||||
isCoverSquareAspectRatio() {
|
isCoverSquareAspectRatio() {
|
||||||
return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE
|
return this.bookCoverAspectRatio === 1
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.isCoverSquareAspectRatio ? 1 : 1.6
|
return this.$store.getters['getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
bookWidth() {
|
bookWidth() {
|
||||||
var coverSize = 100
|
var coverSize = 100
|
||||||
@@ -119,7 +118,7 @@ export default {
|
|||||||
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||||
},
|
},
|
||||||
shelfHeight() {
|
shelfHeight() {
|
||||||
if (this.showBookshelfListView) return this.entityHeight
|
if (this.showBookshelfListView) return this.entityHeight + 16
|
||||||
return this.entityHeight + 40
|
return this.entityHeight + 40
|
||||||
},
|
},
|
||||||
totalEntityCardWidth() {
|
totalEntityCardWidth() {
|
||||||
@@ -300,7 +299,6 @@ export default {
|
|||||||
this.bookshelfHeight = clientHeight
|
this.bookshelfHeight = clientHeight
|
||||||
this.bookshelfWidth = clientWidth
|
this.bookshelfWidth = clientWidth
|
||||||
this.entitiesPerShelf = this.showBookshelfListView ? 1 : Math.floor((this.bookshelfWidth - 16) / this.totalEntityCardWidth)
|
this.entitiesPerShelf = this.showBookshelfListView ? 1 : Math.floor((this.bookshelfWidth - 16) / this.totalEntityCardWidth)
|
||||||
|
|
||||||
this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 2
|
this.shelvesPerPage = Math.ceil(this.bookshelfHeight / this.shelfHeight) + 2
|
||||||
this.bookshelfMarginLeft = (this.bookshelfWidth - this.entitiesPerShelf * this.totalEntityCardWidth) / 2
|
this.bookshelfMarginLeft = (this.bookshelfWidth - this.entitiesPerShelf * this.totalEntityCardWidth) / 2
|
||||||
|
|
||||||
@@ -320,6 +318,15 @@ export default {
|
|||||||
await this.loadPage(0)
|
await this.loadPage(0)
|
||||||
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
var lastBookIndex = Math.min(this.totalEntities, this.shelvesPerPage * this.entitiesPerShelf)
|
||||||
this.mountEntites(0, lastBookIndex)
|
this.mountEntites(0, lastBookIndex)
|
||||||
|
|
||||||
|
// Set last scroll position for this bookshelf page
|
||||||
|
if (this.$store.state.lastBookshelfScrollData[this.page] && window['bookshelf-wrapper']) {
|
||||||
|
const { path, scrollTop } = this.$store.state.lastBookshelfScrollData[this.page]
|
||||||
|
if (path === this.routeFullPath) {
|
||||||
|
// Exact path match with query so use scroll position
|
||||||
|
window['bookshelf-wrapper'].scrollTop = scrollTop
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
scroll(e) {
|
scroll(e) {
|
||||||
if (!e || !e.target) return
|
if (!e || !e.target) return
|
||||||
@@ -362,6 +369,8 @@ export default {
|
|||||||
if (newSearchParams !== this.currentSFQueryString || newSearchParams !== currentQueryString) {
|
if (newSearchParams !== this.currentSFQueryString || newSearchParams !== currentQueryString) {
|
||||||
let newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + '?' + newSearchParams
|
let newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + '?' + newSearchParams
|
||||||
window.history.replaceState({ path: newurl }, '', newurl)
|
window.history.replaceState({ path: newurl }, '', newurl)
|
||||||
|
|
||||||
|
this.routeFullPath = window.location.pathname + (window.location.search || '') // Update for saving scroll position
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,7 +382,7 @@ export default {
|
|||||||
this.resetEntities()
|
this.resetEntities()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
libraryChanged(libid) {
|
libraryChanged() {
|
||||||
if (this.hasFilter) {
|
if (this.hasFilter) {
|
||||||
this.clearFilter()
|
this.clearFilter()
|
||||||
} else {
|
} else {
|
||||||
@@ -456,12 +465,22 @@ export default {
|
|||||||
this.$socket.$off('items_added', this.libraryItemsAdded)
|
this.$socket.$off('items_added', this.libraryItemsAdded)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updated() {
|
||||||
|
this.routeFullPath = window.location.pathname + (window.location.search || '')
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.routeFullPath = window.location.pathname + (window.location.search || '')
|
||||||
|
|
||||||
this.init()
|
this.init()
|
||||||
this.initListeners()
|
this.initListeners()
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.removeListeners()
|
this.removeListeners()
|
||||||
|
|
||||||
|
// Set bookshelf scroll position for specific bookshelf page and query
|
||||||
|
if (window['bookshelf-wrapper']) {
|
||||||
|
this.$store.commit('setLastBookshelfScrollData', { scrollTop: window['bookshelf-wrapper'].scrollTop || 0, path: this.routeFullPath, name: this.page })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -353,7 +353,34 @@ export default {
|
|||||||
this.isSelectionMode = val
|
this.isSelectionMode = val
|
||||||
if (!val) this.selected = false
|
if (!val) this.selected = false
|
||||||
},
|
},
|
||||||
setEntity(libraryItem) {
|
setEntity(_libraryItem) {
|
||||||
|
var libraryItem = _libraryItem
|
||||||
|
|
||||||
|
// this code block is only necessary when showing a selected series with sequence #
|
||||||
|
// it will update the selected series so we get realtime updates for series sequence changes
|
||||||
|
if (this.series) {
|
||||||
|
// i know.. but the libraryItem passed to this func cannot be modified so we need to create a copy
|
||||||
|
libraryItem = {
|
||||||
|
..._libraryItem,
|
||||||
|
media: {
|
||||||
|
..._libraryItem.media,
|
||||||
|
metadata: {
|
||||||
|
..._libraryItem.media.metadata
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var mediaMetadata = libraryItem.media.metadata
|
||||||
|
if (mediaMetadata.series) {
|
||||||
|
var newSeries = mediaMetadata.series.find((se) => se.id === this.series.id)
|
||||||
|
if (newSeries) {
|
||||||
|
// update selected series
|
||||||
|
libraryItem.media.metadata.series = newSeries
|
||||||
|
this.libraryItem = libraryItem
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.libraryItem = libraryItem
|
this.libraryItem = libraryItem
|
||||||
},
|
},
|
||||||
setLocalLibraryItem(localLibraryItem) {
|
setLocalLibraryItem(localLibraryItem) {
|
||||||
|
|||||||
@@ -54,16 +54,16 @@ export default {
|
|||||||
updatedAt() {
|
updatedAt() {
|
||||||
return this._author.updatedAt
|
return this._author.updatedAt
|
||||||
},
|
},
|
||||||
serverAddres() {
|
serverAddress() {
|
||||||
return this.$store.getters['user/getServerAddress']
|
return this.$store.getters['user/getServerAddress']
|
||||||
},
|
},
|
||||||
imgSrc() {
|
imgSrc() {
|
||||||
if (!this.imagePath || !this.serverAddres) return null
|
if (!this.imagePath || !this.serverAddress) return null
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production' && this.serverAddress.startsWith('http://192.168')) {
|
||||||
// Testing
|
// Testing
|
||||||
return `http://localhost:3333/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
return `http://localhost:3333/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
||||||
}
|
}
|
||||||
return `${this.serverAddres}/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
return `${this.serverAddress}/api/authors/${this.authorId}/image?token=${this.userToken}&ts=${this.updatedAt}`
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<div class="absolute cover-bg" ref="coverBg" />
|
<div class="absolute cover-bg" ref="coverBg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<img v-if="fullCoverUrl" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? 1 : 0 }" :class="showCoverBg && !hasCover ? 'object-contain' : 'object-fill'" />
|
<img v-if="fullCoverUrl" ref="cover" :src="fullCoverUrl" loading="lazy" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0 z-10 duration-300 transition-opacity" :style="{ opacity: imageReady ? 1 : 0 }" :class="showCoverBg && hasCover ? 'object-contain' : 'object-fill'" />
|
||||||
|
|
||||||
<div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
<div v-show="loading && libraryItem" class="absolute top-0 left-0 h-full w-full flex items-center justify-center">
|
||||||
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
<p class="font-book text-center" :style="{ fontSize: 0.75 * sizeMultiplier + 'rem' }">{{ title }}</p>
|
||||||
@@ -121,7 +121,7 @@ export default {
|
|||||||
return this.media.coverPath || this.placeholderUrl
|
return this.media.coverPath || this.placeholderUrl
|
||||||
},
|
},
|
||||||
hasCover() {
|
hasCover() {
|
||||||
return !!this.media.coverPath || this.localCover || this.downloadCover
|
return (!!this.media.coverPath && !this.isLocal) || this.localCover || this.downloadCover
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
var baseSize = this.squareAspectRatio ? 128 : 96
|
var baseSize = this.squareAspectRatio ? 128 : 96
|
||||||
|
|||||||
@@ -83,11 +83,7 @@ export default {
|
|||||||
this.$emit('input', val)
|
this.$emit('input', val)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isConnected() {
|
|
||||||
return this.$store.state.socketConnected
|
|
||||||
},
|
|
||||||
canCreateBookmark() {
|
canCreateBookmark() {
|
||||||
if (!this.isConnected) return false
|
|
||||||
return !this.bookmarks.find((bm) => bm.time === this.currentTime)
|
return !this.bookmarks.find((bm) => bm.time === this.currentTime)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
<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">
|
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
|
||||||
<template v-for="item in items">
|
<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">
|
<div class="relative flex items-center px-3">
|
||||||
<p class="font-normal block truncate text-base text-white text-opacity-80">{{ item.text }}</p>
|
<p class="font-normal block truncate text-base text-white text-opacity-80">{{ item.text }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -30,7 +30,8 @@ export default {
|
|||||||
items: {
|
items: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => []
|
default: () => []
|
||||||
}
|
},
|
||||||
|
selected: String // optional
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ export default {
|
|||||||
return this.filterData.narrators || []
|
return this.filterData.narrators || []
|
||||||
},
|
},
|
||||||
progress() {
|
progress() {
|
||||||
return ['Finished', 'In Progress', 'Not Started']
|
return ['Finished', 'In Progress', 'Not Started', 'Not Finished']
|
||||||
},
|
},
|
||||||
sublistItems() {
|
sublistItems() {
|
||||||
return (this[this.sublist] || []).map((item) => {
|
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>
|
||||||
@@ -51,7 +51,7 @@ export default {
|
|||||||
async clickedOption(lib) {
|
async clickedOption(lib) {
|
||||||
this.show = false
|
this.show = false
|
||||||
await this.$store.dispatch('libraries/fetch', lib.id)
|
await this.$store.dispatch('libraries/fetch', lib.id)
|
||||||
this.$eventBus.$emit('library-changed', lib.id)
|
this.$eventBus.$emit('library-changed')
|
||||||
this.$localStore.setLastLibraryId(lib.id)
|
this.$localStore.setLastLibraryId(lib.id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -47,6 +47,10 @@ export default {
|
|||||||
text: 'Size',
|
text: 'Size',
|
||||||
value: 'size'
|
value: 'size'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
text: 'Duration',
|
||||||
|
value: 'media.duration'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
text: 'File Birthtime',
|
text: 'File Birthtime',
|
||||||
value: 'birthtimeMs'
|
value: 'birthtimeMs'
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
<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" />
|
||||||
|
|
||||||
<div v-if="!isIos">
|
<div v-if="!isIos && userCanDownload">
|
||||||
<span v-if="isLocal" class="material-icons-outlined px-2 text-success text-lg">audio_file</span>
|
<span v-if="isLocal" class="material-icons-outlined px-2 text-success text-lg">audio_file</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-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>
|
<span v-else class="material-icons px-2 text-success text-xl">download_done</span>
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Dialog } from '@capacitor/dialog'
|
import { Dialog } from '@capacitor/dialog'
|
||||||
import { AbsDownloader } from '@/plugins/capacitor'
|
import { AbsFileSystem, AbsDownloader } from '@/plugins/capacitor'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
@@ -69,6 +69,9 @@ export default {
|
|||||||
mediaType() {
|
mediaType() {
|
||||||
return 'podcast'
|
return 'podcast'
|
||||||
},
|
},
|
||||||
|
userCanDownload() {
|
||||||
|
return this.$store.getters['user/getUserCanDownload']
|
||||||
|
},
|
||||||
audioFile() {
|
audioFile() {
|
||||||
return this.episode.audioFile
|
return this.episode.audioFile
|
||||||
},
|
},
|
||||||
@@ -132,8 +135,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
selectFolder() {
|
async selectFolder() {
|
||||||
this.$toast.error('Folder selector not implemented for podcasts yet')
|
var folderObj = await AbsFileSystem.selectFolder({ mediaType: this.mediaType })
|
||||||
|
if (folderObj.error) {
|
||||||
|
return this.$toast.error(`Error: ${folderObj.error || 'Unknown Error'}`)
|
||||||
|
}
|
||||||
|
return folderObj
|
||||||
},
|
},
|
||||||
downloadClick() {
|
downloadClick() {
|
||||||
if (this.downloadItem) return
|
if (this.downloadItem) return
|
||||||
|
|||||||
@@ -1,10 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full">
|
<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" />
|
<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>
|
</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>
|
</div>
|
||||||
</template>
|
</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
|
isLocal: Boolean // If is local then episodes and libraryItemId are local, otherwise local is passed in localLibraryItemId and localEpisodes
|
||||||
},
|
},
|
||||||
data() {
|
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: {
|
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
|
// Map of local episodes where server episode id is key
|
||||||
localEpisodeMap() {
|
localEpisodeMap() {
|
||||||
var epmap = {}
|
var epmap = {}
|
||||||
@@ -36,9 +131,36 @@ export default {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
return epmap
|
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() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -23,6 +23,10 @@
|
|||||||
3AF1970E2806E3CA0096F747 /* AbsAudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */; };
|
3AF1970E2806E3CA0096F747 /* AbsAudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */; };
|
||||||
3AF197102806E3DC0096F747 /* AbsAudioPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */; };
|
3AF197102806E3DC0096F747 /* AbsAudioPlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */; };
|
||||||
3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */; };
|
3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */; };
|
||||||
|
4D66B952282EE822008272D4 /* AbsDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B951282EE822008272D4 /* AbsDownloader.m */; };
|
||||||
|
4D66B954282EE87C008272D4 /* AbsDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B953282EE87C008272D4 /* AbsDownloader.swift */; };
|
||||||
|
4D66B956282EE951008272D4 /* AbsFileSystem.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B955282EE951008272D4 /* AbsFileSystem.m */; };
|
||||||
|
4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D66B957282EEA14008272D4 /* AbsFileSystem.swift */; };
|
||||||
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
|
||||||
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
|
504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
|
||||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
|
504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
|
||||||
@@ -50,6 +54,10 @@
|
|||||||
3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsAudioPlayer.swift; sourceTree = "<group>"; };
|
3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsAudioPlayer.swift; sourceTree = "<group>"; };
|
||||||
3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsAudioPlayer.m; sourceTree = "<group>"; };
|
3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsAudioPlayer.m; sourceTree = "<group>"; };
|
||||||
3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfo.swift; sourceTree = "<group>"; };
|
3AFCB5E727EA240D00ECCC05 /* NowPlayingInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NowPlayingInfo.swift; sourceTree = "<group>"; };
|
||||||
|
4D66B951282EE822008272D4 /* AbsDownloader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsDownloader.m; sourceTree = "<group>"; };
|
||||||
|
4D66B953282EE87C008272D4 /* AbsDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsDownloader.swift; sourceTree = "<group>"; };
|
||||||
|
4D66B955282EE951008272D4 /* AbsFileSystem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AbsFileSystem.m; sourceTree = "<group>"; };
|
||||||
|
4D66B957282EEA14008272D4 /* AbsFileSystem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbsFileSystem.swift; sourceTree = "<group>"; };
|
||||||
4D8D412C26E187E400BA5F0D /* App-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "App-Bridging-Header.h"; sourceTree = "<group>"; };
|
4D8D412C26E187E400BA5F0D /* App-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "App-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
|
||||||
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -111,6 +119,10 @@
|
|||||||
3AD4FCE628043E72006DB301 /* AbsDatabase.m */,
|
3AD4FCE628043E72006DB301 /* AbsDatabase.m */,
|
||||||
3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */,
|
3AF1970D2806E3CA0096F747 /* AbsAudioPlayer.swift */,
|
||||||
3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */,
|
3AF1970F2806E3DC0096F747 /* AbsAudioPlayer.m */,
|
||||||
|
4D66B951282EE822008272D4 /* AbsDownloader.m */,
|
||||||
|
4D66B953282EE87C008272D4 /* AbsDownloader.swift */,
|
||||||
|
4D66B955282EE951008272D4 /* AbsFileSystem.m */,
|
||||||
|
4D66B957282EEA14008272D4 /* AbsFileSystem.swift */,
|
||||||
);
|
);
|
||||||
path = plugins;
|
path = plugins;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -303,16 +315,20 @@
|
|||||||
3ABF580928059BAE005DFBE5 /* PlaybackSession.swift in Sources */,
|
3ABF580928059BAE005DFBE5 /* PlaybackSession.swift in Sources */,
|
||||||
3ABF618F2804325C0070250E /* PlayerHandler.swift in Sources */,
|
3ABF618F2804325C0070250E /* PlayerHandler.swift in Sources */,
|
||||||
3AD4FCED28044E6C006DB301 /* Store.swift in Sources */,
|
3AD4FCED28044E6C006DB301 /* Store.swift in Sources */,
|
||||||
|
4D66B958282EEA14008272D4 /* AbsFileSystem.swift in Sources */,
|
||||||
3AF1970E2806E3CA0096F747 /* AbsAudioPlayer.swift in Sources */,
|
3AF1970E2806E3CA0096F747 /* AbsAudioPlayer.swift in Sources */,
|
||||||
3AD4FCE928043FD7006DB301 /* ServerConnectionConfig.swift in Sources */,
|
3AD4FCE928043FD7006DB301 /* ServerConnectionConfig.swift in Sources */,
|
||||||
3A200C1527D64D7E00CBF02E /* AudioPlayer.swift in Sources */,
|
3A200C1527D64D7E00CBF02E /* AudioPlayer.swift in Sources */,
|
||||||
|
4D66B956282EE951008272D4 /* AbsFileSystem.m in Sources */,
|
||||||
3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */,
|
3AFCB5E827EA240D00ECCC05 /* NowPlayingInfo.swift in Sources */,
|
||||||
3AB34053280829BF0039308B /* Extensions.swift in Sources */,
|
3AB34053280829BF0039308B /* Extensions.swift in Sources */,
|
||||||
3AD4FCEB280443DD006DB301 /* Database.swift in Sources */,
|
3AD4FCEB280443DD006DB301 /* Database.swift in Sources */,
|
||||||
3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */,
|
3AD4FCE528043E50006DB301 /* AbsDatabase.swift in Sources */,
|
||||||
|
4D66B952282EE822008272D4 /* AbsDownloader.m in Sources */,
|
||||||
3AF197102806E3DC0096F747 /* AbsAudioPlayer.m in Sources */,
|
3AF197102806E3DC0096F747 /* AbsAudioPlayer.m in Sources */,
|
||||||
3AF1970C2806E2590096F747 /* ApiClient.swift in Sources */,
|
3AF1970C2806E2590096F747 /* ApiClient.swift in Sources */,
|
||||||
C4D0677528106D0C00B8F875 /* DataClasses.swift in Sources */,
|
C4D0677528106D0C00B8F875 /* DataClasses.swift in Sources */,
|
||||||
|
4D66B954282EE87C008272D4 /* AbsDownloader.swift in Sources */,
|
||||||
3AB34055280832720039308B /* PlayerEvents.swift in Sources */,
|
3AB34055280832720039308B /* PlayerEvents.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -459,12 +475,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 = 7;
|
CURRENT_PROJECT_VERSION = 9;
|
||||||
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.44;
|
MARKETING_VERSION = 0.9.47;
|
||||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev;
|
PRODUCT_BUNDLE_IDENTIFIER = com.audiobookshelf.app.dev;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
@@ -483,12 +499,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 = 7;
|
CURRENT_PROJECT_VERSION = 9;
|
||||||
DEVELOPMENT_TEAM = "";
|
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.44;
|
MARKETING_VERSION = 0.9.47;
|
||||||
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 = "";
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
//
|
||||||
|
// AbsDownloader.m
|
||||||
|
// App
|
||||||
|
//
|
||||||
|
// Created by advplyr on 5/13/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <Capacitor/Capacitor.h>
|
||||||
|
|
||||||
|
CAP_PLUGIN(AbsDownloader, "AbsDownloader",
|
||||||
|
CAP_PLUGIN_METHOD(downloadLibraryItem, CAPPluginReturnPromise);
|
||||||
|
)
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
//
|
||||||
|
// AbsDownloader.swift
|
||||||
|
// App
|
||||||
|
//
|
||||||
|
// Created by advplyr on 5/13/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Capacitor
|
||||||
|
|
||||||
|
@objc(AbsDownloader)
|
||||||
|
public class AbsDownloader: CAPPlugin {
|
||||||
|
@objc func downloadLibraryItem(_ call: CAPPluginCall) {
|
||||||
|
let libraryItemId = call.getString("libraryItemId")
|
||||||
|
let episodeId = call.getString("episodeId")
|
||||||
|
|
||||||
|
NSLog("Download library item \(libraryItemId ?? "N/A") episode \(episodeId ?? "")")
|
||||||
|
|
||||||
|
ApiClient.getLibraryItemWithProgress(libraryItemId: libraryItemId!, episodeId: episodeId) { libraryItem in
|
||||||
|
if (libraryItem == nil) {
|
||||||
|
NSLog("Library item not found")
|
||||||
|
call.resolve()
|
||||||
|
} else {
|
||||||
|
NSLog("Got library item \(libraryItem!)")
|
||||||
|
|
||||||
|
// TODO: break out in seperate functions
|
||||||
|
libraryItem!.media.tracks?.forEach { track in
|
||||||
|
NSLog("TRACK \(track.contentUrl!)")
|
||||||
|
// filename needs to be encoded otherwise would just use contentUrl
|
||||||
|
let filename = track.metadata?.filename ?? ""
|
||||||
|
let filenameEncoded = filename.addingPercentEncoding(withAllowedCharacters: NSCharacterSet.urlQueryAllowed)
|
||||||
|
let urlstr = "\(Store.serverConfig!.address)/s/item/\(libraryItemId!)/\(filenameEncoded ?? "")?token=\(Store.serverConfig!.token)"
|
||||||
|
let url = URL(string: urlstr)!
|
||||||
|
|
||||||
|
|
||||||
|
let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||||
|
let itemDirectory = documentsDirectory.appendingPathComponent("\(libraryItemId!)")
|
||||||
|
NSLog("ITEM DIR \(itemDirectory)")
|
||||||
|
|
||||||
|
// Create library item directory
|
||||||
|
do {
|
||||||
|
try FileManager.default.createDirectory(at: itemDirectory, withIntermediateDirectories: false)
|
||||||
|
} catch {
|
||||||
|
NSLog("Failed to CREATE LI DIRECTORY \(error)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output filename
|
||||||
|
let trackFilename = itemDirectory.appendingPathComponent("\(filename)")
|
||||||
|
|
||||||
|
let downloadTask = URLSession.shared.downloadTask(with: url) { urlOrNil, responseOrNil, errorOrNil in
|
||||||
|
|
||||||
|
guard let fileURL = urlOrNil else { return }
|
||||||
|
|
||||||
|
do {
|
||||||
|
NSLog("Download TMP file URL \(fileURL)")
|
||||||
|
let imageData = try Data(contentsOf:fileURL)
|
||||||
|
try imageData.write(to: trackFilename)
|
||||||
|
NSLog("Download written to \(trackFilename)")
|
||||||
|
} catch {
|
||||||
|
NSLog("FILE ERROR: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
downloadTask.resume()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
//
|
||||||
|
// AbsFileSystem.m
|
||||||
|
// App
|
||||||
|
//
|
||||||
|
// Created by advplyr on 5/13/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <Capacitor/Capacitor.h>
|
||||||
|
|
||||||
|
CAP_PLUGIN(AbsFileSystem, "AbsFileSystem",
|
||||||
|
CAP_PLUGIN_METHOD(selectFolder, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(checkFolderPermission, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(scanFolder, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(removeFolder, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(removeLocalLibraryItem, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(scanLocalLibraryItem, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(deleteItem, CAPPluginReturnPromise);
|
||||||
|
CAP_PLUGIN_METHOD(deleteTrackFromItem, CAPPluginReturnPromise);
|
||||||
|
)
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
//
|
||||||
|
// AbsFileSystem.swift
|
||||||
|
// App
|
||||||
|
//
|
||||||
|
// Created by advplyr on 5/13/22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Capacitor
|
||||||
|
|
||||||
|
@objc(AbsFileSystem)
|
||||||
|
public class AbsFileSystem: CAPPlugin {
|
||||||
|
@objc func selectFolder(_ call: CAPPluginCall) {
|
||||||
|
let mediaType = call.getString("mediaType")
|
||||||
|
|
||||||
|
// TODO: Implement
|
||||||
|
NSLog("Select Folder for media type \(mediaType ?? "UNSET")")
|
||||||
|
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func checkFolderPermission(_ call: CAPPluginCall) {
|
||||||
|
let folderUrl = call.getString("folderUrl")
|
||||||
|
|
||||||
|
// TODO: Is this even necessary on iOS?
|
||||||
|
NSLog("checkFolderPermission for folder \(folderUrl ?? "UNSET")")
|
||||||
|
|
||||||
|
call.resolve([
|
||||||
|
"value": true
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func scanFolder(_ call: CAPPluginCall) {
|
||||||
|
let folderId = call.getString("folderId")
|
||||||
|
let forceAudioProbe = call.getBool("forceAudioProbe", false)
|
||||||
|
|
||||||
|
// TODO: Implement
|
||||||
|
NSLog("scanFolder \(folderId ?? "UNSET") | Force Probe = \(forceAudioProbe)")
|
||||||
|
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func removeFolder(_ call: CAPPluginCall) {
|
||||||
|
let folderId = call.getString("folderId")
|
||||||
|
|
||||||
|
// TODO: Implement
|
||||||
|
NSLog("removeFolder \(folderId ?? "UNSET")")
|
||||||
|
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func removeLocalLibraryItem(_ call: CAPPluginCall) {
|
||||||
|
let localLibraryItemId = call.getString("localLibraryItemId")
|
||||||
|
|
||||||
|
// TODO: Implement
|
||||||
|
NSLog("removeLocalLibraryItem \(localLibraryItemId ?? "UNSET")")
|
||||||
|
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func scanLocalLibraryItem(_ call: CAPPluginCall) {
|
||||||
|
let localLibraryItemId = call.getString("localLibraryItemId")
|
||||||
|
let forceAudioProbe = call.getBool("forceAudioProbe", false)
|
||||||
|
|
||||||
|
// TODO: Implement
|
||||||
|
NSLog("scanLocalLibraryItem \(localLibraryItemId ?? "UNSET") | Force Probe = \(forceAudioProbe)")
|
||||||
|
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func deleteItem(_ call: CAPPluginCall) {
|
||||||
|
let localLibraryItemId = call.getString("localLibraryItemId")
|
||||||
|
let contentUrl = call.getString("contentUrl")
|
||||||
|
|
||||||
|
// TODO: Implement
|
||||||
|
NSLog("deleteItem \(localLibraryItemId ?? "UNSET") url \(contentUrl ?? "UNSET")")
|
||||||
|
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func deleteTrackFromItem(_ call: CAPPluginCall) {
|
||||||
|
let localLibraryItemId = call.getString("localLibraryItemId")
|
||||||
|
let trackLocalFileId = call.getString("trackLocalFileId")
|
||||||
|
let contentUrl = call.getString("contentUrl")
|
||||||
|
|
||||||
|
// TODO: Implement
|
||||||
|
NSLog("deleteTrackFromItem \(localLibraryItemId ?? "UNSET") track file \(trackLocalFileId ?? "UNSET") url \(contentUrl ?? "UNSET")")
|
||||||
|
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,13 +9,12 @@ install! 'cocoapods', :disable_input_output_paths => true
|
|||||||
def capacitor_pods
|
def capacitor_pods
|
||||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
pod 'CapacitorApp', :path => '..\..\node_modules\@capacitor\app'
|
||||||
pod 'CapacitorDialog', :path => '../../node_modules/@capacitor/dialog'
|
pod 'CapacitorDialog', :path => '..\..\node_modules\@capacitor\dialog'
|
||||||
pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics'
|
pod 'CapacitorHaptics', :path => '..\..\node_modules\@capacitor\haptics'
|
||||||
pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
|
pod 'CapacitorNetwork', :path => '..\..\node_modules\@capacitor\network'
|
||||||
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
pod 'CapacitorStatusBar', :path => '..\..\node_modules\@capacitor\status-bar'
|
||||||
pod 'CapacitorStorage', :path => '../../node_modules/@capacitor/storage'
|
pod 'CapacitorStorage', :path => '..\..\node_modules\@capacitor\storage'
|
||||||
pod 'RobingenzCapacitorAppUpdate', :path => '../../node_modules/@robingenz/capacitor-app-update'
|
|
||||||
end
|
end
|
||||||
|
|
||||||
target 'App' do
|
target 'App' do
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ struct LibraryItem: Codable {
|
|||||||
var folderId: String
|
var folderId: String
|
||||||
var path: String
|
var path: String
|
||||||
var relPath: String
|
var relPath: String
|
||||||
|
var isFile: Bool
|
||||||
var mtimeMs: Int64
|
var mtimeMs: Int64
|
||||||
var ctimeMs: Int64
|
var ctimeMs: Int64
|
||||||
var birthtimeMs: Int64
|
var birthtimeMs: Int64
|
||||||
@@ -27,6 +28,7 @@ struct LibraryItem: Codable {
|
|||||||
var mediaType: String
|
var mediaType: String
|
||||||
var media: MediaType
|
var media: MediaType
|
||||||
var libraryFiles: [LibraryFile]
|
var libraryFiles: [LibraryFile]
|
||||||
|
var userMediaProgress:MediaProgress?
|
||||||
}
|
}
|
||||||
struct MediaType: Codable {
|
struct MediaType: Codable {
|
||||||
var libraryItemId: String?
|
var libraryItemId: String?
|
||||||
@@ -125,3 +127,15 @@ struct LibraryFile: Codable {
|
|||||||
var ino: String
|
var ino: String
|
||||||
var metadata: FileMetadata
|
var metadata: FileMetadata
|
||||||
}
|
}
|
||||||
|
struct MediaProgress:Codable {
|
||||||
|
var id:String
|
||||||
|
var libraryItemId:String
|
||||||
|
var episodeId:String?
|
||||||
|
var duration:Double
|
||||||
|
var progress:Double
|
||||||
|
var currentTime:Double
|
||||||
|
var isFinished:Bool
|
||||||
|
var lastUpdate:Int64
|
||||||
|
var startedAt:Int64
|
||||||
|
var finishedAt:Int64?
|
||||||
|
}
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class AudioPlayer: NSObject {
|
|||||||
self.playWhenReady = playWhenReady
|
self.playWhenReady = playWhenReady
|
||||||
self.initialPlaybackRate = playbackRate
|
self.initialPlaybackRate = playbackRate
|
||||||
self.audioPlayer = AVQueuePlayer()
|
self.audioPlayer = AVQueuePlayer()
|
||||||
|
self.audioPlayer.automaticallyWaitsToMinimizeStalling = false
|
||||||
self.playbackSession = playbackSession
|
self.playbackSession = playbackSession
|
||||||
self.status = -1
|
self.status = -1
|
||||||
self.rate = 0.0
|
self.rate = 0.0
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class ApiClient {
|
|||||||
}).resume()
|
}).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) {
|
if (Store.serverConfig == nil) {
|
||||||
NSLog("Server config not set")
|
NSLog("Server config not set")
|
||||||
return
|
return
|
||||||
@@ -27,7 +27,7 @@ class ApiClient {
|
|||||||
"Authorization": "Bearer \(Store.serverConfig!.token)"
|
"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 {
|
switch response.result {
|
||||||
case .success(let obj):
|
case .success(let obj):
|
||||||
callback?(obj)
|
callback?(obj)
|
||||||
@@ -60,6 +60,27 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
public static func getResource<T: Decodable>(endpoint: String, decodable: T.Type = T.self, callback: ((_ param: T?) -> Void)?) {
|
||||||
|
if (Store.serverConfig == nil) {
|
||||||
|
NSLog("Server config not set")
|
||||||
|
callback?(nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let headers: HTTPHeaders = [
|
||||||
|
"Authorization": "Bearer \(Store.serverConfig!.token)"
|
||||||
|
]
|
||||||
|
|
||||||
|
AF.request("\(Store.serverConfig!.address)/\(endpoint)", method: .get, encoding: JSONEncoding.default, headers: headers).responseDecodable(of: decodable) { response in
|
||||||
|
switch response.result {
|
||||||
|
case .success(let obj):
|
||||||
|
callback?(obj)
|
||||||
|
case .failure(let error):
|
||||||
|
NSLog("api request to \(endpoint) failed")
|
||||||
|
print(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static func startPlaybackSession(libraryItemId: String, episodeId: String?, forceTranscode:Bool, 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"
|
||||||
@@ -67,10 +88,23 @@ class ApiClient {
|
|||||||
endpoint += "/\(episodeId!)"
|
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: [
|
ApiClient.postResource(endpoint: endpoint, parameters: [
|
||||||
"forceDirectPlay": !forceTranscode ? "1" : "",
|
"forceDirectPlay": !forceTranscode ? "1" : "",
|
||||||
"forceTranscode": forceTranscode ? "1" : "",
|
"forceTranscode": forceTranscode ? "1" : "",
|
||||||
"mediaPlayer": "AVPlayer",
|
"mediaPlayer": "AVPlayer",
|
||||||
|
"deviceInfo": [
|
||||||
|
"manufacturer": "Apple",
|
||||||
|
"model": modelCode,
|
||||||
|
"clientVersion": Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
||||||
|
]
|
||||||
], decodable: PlaybackSession.self) { obj in
|
], decodable: PlaybackSession.self) { obj in
|
||||||
var session = obj
|
var session = obj
|
||||||
|
|
||||||
@@ -83,4 +117,14 @@ class ApiClient {
|
|||||||
public static func reportPlaybackProgress(report: PlaybackReport, sessionId: String) {
|
public static func reportPlaybackProgress(report: PlaybackReport, sessionId: String) {
|
||||||
try? postResource(endpoint: "api/session/\(sessionId)/sync", parameters: report.asDictionary().mapValues({ value in "\(value)" }), callback: nil)
|
try? postResource(endpoint: "api/session/\(sessionId)/sync", parameters: report.asDictionary().mapValues({ value in "\(value)" }), callback: nil)
|
||||||
}
|
}
|
||||||
|
public static func getLibraryItemWithProgress(libraryItemId:String, episodeId:String?, callback: @escaping (_ param: LibraryItem?) -> Void) {
|
||||||
|
var endpoint = "api/items/\(libraryItemId)?expanded=1&include=progress"
|
||||||
|
if episodeId != nil {
|
||||||
|
endpoint += "&episodeId=\(episodeId!)"
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiClient.getResource(endpoint: endpoint, decodable: LibraryItem.self) { obj in
|
||||||
|
callback(obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { AppUpdate } from '@robingenz/capacitor-app-update'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -79,36 +77,6 @@ export default {
|
|||||||
this.$refs.streamContainer.streamOpen(stream)
|
this.$refs.streamContainer.streamOpen(stream)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async clickUpdateToast() {
|
|
||||||
var immediateUpdateAllowed = this.$store.state.appUpdateInfo.immediateUpdateAllowed
|
|
||||||
if (immediateUpdateAllowed) {
|
|
||||||
await AppUpdate.performImmediateUpdate()
|
|
||||||
} else {
|
|
||||||
await AppUpdate.openAppStore()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async checkForUpdate() {
|
|
||||||
if (this.$platform == 'web') return
|
|
||||||
console.log('Checking for app update')
|
|
||||||
const result = await AppUpdate.getAppUpdateInfo()
|
|
||||||
if (!result) {
|
|
||||||
console.error('Invalid version check')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.log('App Update Info', JSON.stringify(result))
|
|
||||||
this.$store.commit('setAppUpdateInfo', result)
|
|
||||||
if (result.updateAvailability === 2) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.$toast.info(`Update is available! Click to update.`, {
|
|
||||||
draggable: false,
|
|
||||||
hideProgressBar: false,
|
|
||||||
timeout: 20000,
|
|
||||||
closeButton: true,
|
|
||||||
onClick: this.clickUpdateToast
|
|
||||||
})
|
|
||||||
}, 5000)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async loadSavedSettings() {
|
async loadSavedSettings() {
|
||||||
var userSavedServerSettings = await this.$localStore.getServerSettings()
|
var userSavedServerSettings = await this.$localStore.getServerSettings()
|
||||||
if (userSavedServerSettings) {
|
if (userSavedServerSettings) {
|
||||||
@@ -202,8 +170,8 @@ export default {
|
|||||||
this.inittingLibraries = true
|
this.inittingLibraries = true
|
||||||
await this.$store.dispatch('libraries/load')
|
await this.$store.dispatch('libraries/load')
|
||||||
console.log(`[default] initLibraries loaded ${this.currentLibraryId}`)
|
console.log(`[default] initLibraries loaded ${this.currentLibraryId}`)
|
||||||
|
await this.$store.dispatch('libraries/fetch', this.currentLibraryId)
|
||||||
this.$eventBus.$emit('library-changed')
|
this.$eventBus.$emit('library-changed')
|
||||||
this.$store.dispatch('libraries/fetch', this.currentLibraryId)
|
|
||||||
this.inittingLibraries = false
|
this.inittingLibraries = false
|
||||||
},
|
},
|
||||||
async syncLocalMediaProgress() {
|
async syncLocalMediaProgress() {
|
||||||
@@ -229,16 +197,66 @@ export default {
|
|||||||
console.log('[default] syncLocalMediaProgress No local media progress to sync')
|
console.log('[default] syncLocalMediaProgress No local media progress to sync')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
userUpdated(user) {
|
async userUpdated(user) {
|
||||||
if (this.user && this.user.id == user.id) {
|
if (this.user && this.user.id == user.id) {
|
||||||
this.$store.commit('user/setUser', user)
|
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() {
|
async mounted() {
|
||||||
this.$socket.on('connection-update', this.socketConnectionUpdate)
|
this.$socket.on('connection-update', this.socketConnectionUpdate)
|
||||||
this.$socket.on('initialized', this.socketInit)
|
this.$socket.on('initialized', this.socketInit)
|
||||||
this.$socket.on('user_updated', this.userUpdated)
|
this.$socket.on('user_updated', this.userUpdated)
|
||||||
|
this.$socket.on('user_media_progress_updated', this.userMediaProgressUpdated)
|
||||||
|
|
||||||
if (this.$store.state.isFirstLoad) {
|
if (this.$store.state.isFirstLoad) {
|
||||||
this.$store.commit('setIsFirstLoad', false)
|
this.$store.commit('setIsFirstLoad', false)
|
||||||
@@ -255,7 +273,6 @@ export default {
|
|||||||
console.log(`[default] finished connection attempt or already connected ${!!this.user}`)
|
console.log(`[default] finished connection attempt or already connected ${!!this.user}`)
|
||||||
await this.syncLocalMediaProgress()
|
await this.syncLocalMediaProgress()
|
||||||
this.$store.dispatch('globals/loadLocalMediaProgress')
|
this.$store.dispatch('globals/loadLocalMediaProgress')
|
||||||
this.checkForUpdate()
|
|
||||||
this.loadSavedSettings()
|
this.loadSavedSettings()
|
||||||
this.hasMounted = true
|
this.hasMounted = true
|
||||||
}
|
}
|
||||||
@@ -264,6 +281,7 @@ export default {
|
|||||||
this.$socket.off('connection-update', this.socketConnectionUpdate)
|
this.$socket.off('connection-update', this.socketConnectionUpdate)
|
||||||
this.$socket.off('initialized', this.socketInit)
|
this.$socket.off('initialized', this.socketInit)
|
||||||
this.$socket.off('user_updated', this.userUpdated)
|
this.$socket.off('user_updated', this.userUpdated)
|
||||||
|
this.$socket.off('user_media_progress_updated', this.userMediaProgressUpdated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-app",
|
"name": "audiobookshelf-app",
|
||||||
"version": "0.9.44-beta",
|
"version": "0.9.49-beta",
|
||||||
"author": "advplyr",
|
"author": "advplyr",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "nuxt --hostname localhost --port 1337",
|
"dev": "nuxt --hostname 0.0.0.0 --port 1337",
|
||||||
"build": "nuxt build",
|
"build": "nuxt build",
|
||||||
"start": "nuxt start",
|
"start": "nuxt start",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
@@ -23,7 +23,6 @@
|
|||||||
"@capacitor/status-bar": "^1.0.8",
|
"@capacitor/status-bar": "^1.0.8",
|
||||||
"@capacitor/storage": "^1.2.5",
|
"@capacitor/storage": "^1.2.5",
|
||||||
"@nuxtjs/axios": "^5.13.6",
|
"@nuxtjs/axios": "^5.13.6",
|
||||||
"@robingenz/capacitor-app-update": "^1.3.1",
|
|
||||||
"core-js": "^3.15.1",
|
"core-js": "^3.15.1",
|
||||||
"date-fns": "^2.25.0",
|
"date-fns": "^2.25.0",
|
||||||
"epubjs": "^0.3.88",
|
"epubjs": "^0.3.88",
|
||||||
|
|||||||
@@ -19,19 +19,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="font-mono pt-1 pb-4">{{ $config.version }}</p>
|
<p class="font-mono pt-1 pb-4">{{ $config.version }}</p>
|
||||||
|
|
||||||
<ui-btn v-if="isUpdateAvailable" class="w-full my-4" color="success" @click="clickUpdate">Update is available</ui-btn>
|
|
||||||
|
|
||||||
<ui-btn v-if="!isUpdateAvailable || immediateUpdateAllowed" class="w-full my-4" color="primary" @click="openAppStore">Open app store</ui-btn>
|
|
||||||
|
|
||||||
<p class="text-xs text-gray-400">UA: {{ updateAvailability }} | Avail: {{ availableVersion }} | Curr: {{ currentVersion }} | ImmedAllowed: {{ immediateUpdateAllowed }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { AppUpdate } from '@robingenz/capacitor-app-update'
|
|
||||||
import { AbsAudioPlayer } from '@/plugins/capacitor'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
asyncData({ redirect, store }) {
|
asyncData({ redirect, store }) {
|
||||||
if (!store.state.socketConnected) {
|
if (!store.state.socketConnected) {
|
||||||
@@ -58,43 +49,21 @@ export default {
|
|||||||
},
|
},
|
||||||
serverAddress() {
|
serverAddress() {
|
||||||
return this.serverConnectionConfig.address
|
return this.serverConnectionConfig.address
|
||||||
},
|
|
||||||
appUpdateInfo() {
|
|
||||||
return this.$store.state.appUpdateInfo
|
|
||||||
},
|
|
||||||
availableVersion() {
|
|
||||||
return this.appUpdateInfo ? this.appUpdateInfo.availableVersion : null
|
|
||||||
},
|
|
||||||
currentVersion() {
|
|
||||||
return this.appUpdateInfo ? this.appUpdateInfo.currentVersion : null
|
|
||||||
},
|
|
||||||
immediateUpdateAllowed() {
|
|
||||||
return this.appUpdateInfo ? !!this.appUpdateInfo.immediateUpdateAllowed : false
|
|
||||||
},
|
|
||||||
updateAvailability() {
|
|
||||||
return this.appUpdateInfo ? this.appUpdateInfo.updateAvailability : null
|
|
||||||
},
|
|
||||||
isUpdateAvailable() {
|
|
||||||
return this.updateAvailability === 2
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async logout() {
|
async logout() {
|
||||||
await this.$axios.$post('/logout').catch((error) => {
|
if (this.user) {
|
||||||
console.error(error)
|
await this.$axios.$post('/logout').catch((error) => {
|
||||||
})
|
console.error(error)
|
||||||
this.$server.logout()
|
})
|
||||||
this.$router.push('/connect')
|
|
||||||
},
|
|
||||||
openAppStore() {
|
|
||||||
AppUpdate.openAppStore()
|
|
||||||
},
|
|
||||||
async clickUpdate() {
|
|
||||||
if (this.immediateUpdateAllowed) {
|
|
||||||
AppUpdate.performImmediateUpdate()
|
|
||||||
} else {
|
|
||||||
AppUpdate.openAppStore()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$socket.logout()
|
||||||
|
await this.$db.logout()
|
||||||
|
this.$localStore.removeLastLibraryId()
|
||||||
|
this.$store.commit('user/logout')
|
||||||
|
this.$router.push('/connect')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
|
|||||||
@@ -4,10 +4,6 @@
|
|||||||
<home-bookshelf-toolbar v-show="!isHome" />
|
<home-bookshelf-toolbar v-show="!isHome" />
|
||||||
<div id="bookshelf-wrapper" class="main-content overflow-y-auto overflow-x-hidden relative" :class="isHome ? 'home-page' : ''">
|
<div id="bookshelf-wrapper" class="main-content overflow-y-auto overflow-x-hidden relative" :class="isHome ? 'home-page' : ''">
|
||||||
<nuxt-child />
|
<nuxt-child />
|
||||||
|
|
||||||
<!-- <div v-if="isLoading" class="absolute top-0 left-0 w-full h-full flex items-center justify-center">
|
|
||||||
<ui-loading-indicator />
|
|
||||||
</div>-->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ export default {
|
|||||||
}
|
}
|
||||||
this.loading = false
|
this.loading = false
|
||||||
},
|
},
|
||||||
async libraryChanged(libid) {
|
async libraryChanged() {
|
||||||
if (this.currentLibraryId) {
|
if (this.currentLibraryId) {
|
||||||
await this.fetchCategories()
|
await this.fetchCategories()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,100 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-full px-3 py-4 overflow-y-auto">
|
<div class="w-full h-full px-3 py-4 overflow-y-auto">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div class="w-32">
|
<div class="w-16">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<covers-book-cover :library-item="libraryItem" :width="128" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<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.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 shadow-sm z-10" :class="userIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 64 * 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="title-container flex-grow pl-2">
|
||||||
<h1 class="text-lg">{{ title }}</h1>
|
<div class="flex relative pr-6">
|
||||||
<h3 v-if="seriesName" class="text-gray-300 text-sm leading-6">{{ seriesName }}</h3>
|
<h1 class="text-base">{{ title }}</h1>
|
||||||
<p class="text-sm text-gray-400">by {{ author }}</p>
|
|
||||||
<p v-if="numTracks" class="text-gray-300 text-sm my-1">
|
<button class="absolute top-0 right-0 h-full px-1 outline-none" @click="moreButtonPress">
|
||||||
{{ $elapsedPretty(duration) }}
|
<span class="material-icons text-xl">more_vert</span>
|
||||||
<span v-if="!isLocal" class="px-4">{{ $bytesPretty(size) }}</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>
|
||||||
<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 v-if="narrators && narrators.length" class="text-sm text-gray-400 py-0.5">
|
||||||
<p class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
|
Narrated By
|
||||||
<p v-if="progressPercent < 1" class="text-gray-400 text-xs">{{ $elapsedPretty(userTimeRemaining) }} remaining</p>
|
<template v-for="(narrator, index) in narrators"
|
||||||
<p v-else class="text-gray-400 text-xs">Finished {{ $formatDate(userProgressFinishedAt) }}</p>
|
><nuxt-link :key="narrator" :to="`/bookshelf/library?filter=narrators.${$encode(narrator)}`">{{ narrator }}</nuxt-link
|
||||||
<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 :key="`${narrator}-comma`" v-if="index < narrators.length - 1">, </span></template
|
||||||
<span class="material-icons text-sm">close</span>
|
>
|
||||||
</div>
|
</p>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="isLocal" class="flex mt-4">
|
<!-- Show an indicator for local library items whether they are linked to a server item and if that server item is connected -->
|
||||||
<ui-btn color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mr-2" :padding-x="4" @click="playClick">
|
<p v-if="isLocal && serverLibraryItemId" style="font-size: 10px" class="text-success py-1 uppercase tracking-widest">connected</p>
|
||||||
<span v-show="!isPlaying" class="material-icons">play_arrow</span>
|
<p v-else-if="isLocal && libraryItem.serverAddress" style="font-size: 10px" class="text-gray-400 py-1">{{ libraryItem.serverAddress }}</p>
|
||||||
<span class="px-1 text-sm">{{ isPlaying ? 'Playing' : 'Play' }}</span>
|
|
||||||
</ui-btn>
|
<div v-if="numTracks" class="flex text-gray-100 text-xs my-2 -mx-0.5">
|
||||||
<ui-btn v-if="showRead" color="info" class="flex items-center justify-center mr-2" :class="showPlay ? '' : 'flex-grow'" :padding-x="2" @click="readBook">
|
<div class="bg-primary bg-opacity-80 px-3 py-0.5 rounded-full mx-0.5">
|
||||||
<span class="material-icons">auto_stories</span>
|
<p>{{ $elapsedPretty(duration) }}</p>
|
||||||
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
|
</div>
|
||||||
</ui-btn>
|
<!-- TODO: Local books dont save the size -->
|
||||||
<ui-read-icon-btn v-if="!isPodcast" :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="flex items-center justify-center" @click="toggleFinished" />
|
<div v-if="size" class="bg-primary bg-opacity-80 px-3 py-0.5 rounded-full mx-0.5">
|
||||||
</div>
|
<p>{{ $bytesPretty(size) }}</p>
|
||||||
<div v-else-if="(user && (showPlay || showRead)) || hasLocal" class="flex mt-4">
|
</div>
|
||||||
<ui-btn v-if="showPlay" color="success" :disabled="isPlaying" class="flex items-center justify-center flex-grow mr-2" :padding-x="4" @click="playClick">
|
<div class="bg-primary bg-opacity-80 px-3 py-0.5 rounded-full mx-0.5">
|
||||||
<span v-show="!isPlaying" class="material-icons">play_arrow</span>
|
<p>{{ numTracks }} Track{{ numTracks > 1 ? 's' : '' }}</p>
|
||||||
<span class="px-1 text-sm">{{ isPlaying ? (isStreaming ? 'Streaming' : 'Playing') : hasLocal ? 'Play' : 'Stream' }}</span>
|
</div>
|
||||||
</ui-btn>
|
<div v-if="numChapters" class="bg-primary bg-opacity-80 px-3 py-0.5 rounded-full mx-0.5">
|
||||||
<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">
|
<p>{{ numChapters }} Chapter{{ numChapters > 1 ? 's' : '' }}</p>
|
||||||
<span class="material-icons">auto_stories</span>
|
</div>
|
||||||
<span v-if="!showPlay" class="px-2 text-base">Read {{ ebookFormat }}</span>
|
</div>
|
||||||
</ui-btn>
|
|
||||||
<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">
|
<div>
|
||||||
<span class="material-icons" :class="downloadItem ? 'animate-pulse' : ''">{{ downloadItem ? 'downloading' : 'download' }}</span>
|
<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' : ''">
|
||||||
</ui-btn>
|
<p class="leading-6">Your Progress: {{ Math.round(progressPercent * 100) }}%</p>
|
||||||
<ui-read-icon-btn v-if="!isPodcast" :disabled="isProcessingReadUpdate" :is-read="userIsFinished" class="flex items-center justify-center" @click="toggleFinished" />
|
<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>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isLocal" 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 ? '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>
|
||||||
|
|
||||||
<div v-if="downloadItem" class="py-3">
|
<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" />
|
<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-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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -82,7 +126,7 @@ export default {
|
|||||||
console.log(libraryItemId)
|
console.log(libraryItemId)
|
||||||
if (libraryItemId.startsWith('local')) {
|
if (libraryItemId.startsWith('local')) {
|
||||||
libraryItem = await app.$db.getLocalLibraryItem(libraryItemId)
|
libraryItem = await app.$db.getLocalLibraryItem(libraryItemId)
|
||||||
console.log('Got lli', libraryItem)
|
console.log('Got lli', libraryItemId)
|
||||||
} else if (store.state.user.serverConnectionConfig) {
|
} else if (store.state.user.serverConnectionConfig) {
|
||||||
libraryItem = await app.$axios.$get(`/api/items/${libraryItemId}?expanded=1`).catch((error) => {
|
libraryItem = await app.$axios.$get(`/api/items/${libraryItemId}?expanded=1`).catch((error) => {
|
||||||
console.error('Failed', error)
|
console.error('Failed', error)
|
||||||
@@ -110,13 +154,18 @@ export default {
|
|||||||
return {
|
return {
|
||||||
resettingProgress: false,
|
resettingProgress: false,
|
||||||
isProcessingReadUpdate: false,
|
isProcessingReadUpdate: false,
|
||||||
showSelectLocalFolder: false
|
showSelectLocalFolder: false,
|
||||||
|
showMoreMenu: false,
|
||||||
|
showDetailsModal: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isIos() {
|
isIos() {
|
||||||
return this.$platform === 'ios'
|
return this.$platform === 'ios'
|
||||||
},
|
},
|
||||||
|
userCanDownload() {
|
||||||
|
return this.$store.getters['user/getUserCanDownload']
|
||||||
|
},
|
||||||
isLocal() {
|
isLocal() {
|
||||||
return this.libraryItem.isLocal
|
return this.libraryItem.isLocal
|
||||||
},
|
},
|
||||||
@@ -166,9 +215,17 @@ export default {
|
|||||||
title() {
|
title() {
|
||||||
return this.mediaMetadata.title
|
return this.mediaMetadata.title
|
||||||
},
|
},
|
||||||
author() {
|
podcastAuthor() {
|
||||||
if (this.isPodcast) return this.mediaMetadata.author
|
if (!this.isPodcast) return null
|
||||||
return this.mediaMetadata.authorName
|
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() {
|
description() {
|
||||||
return this.mediaMetadata.description || ''
|
return this.mediaMetadata.description || ''
|
||||||
@@ -176,9 +233,16 @@ export default {
|
|||||||
series() {
|
series() {
|
||||||
return this.mediaMetadata.series || []
|
return this.mediaMetadata.series || []
|
||||||
},
|
},
|
||||||
seriesName() {
|
seriesList() {
|
||||||
// For books only on toJSONExpanded
|
if (this.isPodcast) return null
|
||||||
return this.mediaMetadata.seriesName || ''
|
return this.series.map((se) => {
|
||||||
|
var text = se.name
|
||||||
|
if (se.sequence) text += ` #${se.sequence}`
|
||||||
|
return {
|
||||||
|
...se,
|
||||||
|
text
|
||||||
|
}
|
||||||
|
})
|
||||||
},
|
},
|
||||||
duration() {
|
duration() {
|
||||||
return this.media.duration
|
return this.media.duration
|
||||||
@@ -223,6 +287,10 @@ export default {
|
|||||||
if (!this.media.tracks) return 0
|
if (!this.media.tracks) return 0
|
||||||
return this.media.tracks.length || 0
|
return this.media.tracks.length || 0
|
||||||
},
|
},
|
||||||
|
numChapters() {
|
||||||
|
if (!this.media.chapters) return 0
|
||||||
|
return this.media.chapters.length || 0
|
||||||
|
},
|
||||||
isMissing() {
|
isMissing() {
|
||||||
return this.libraryItem.isMissing
|
return this.libraryItem.isMissing
|
||||||
},
|
},
|
||||||
@@ -235,6 +303,10 @@ export default {
|
|||||||
showRead() {
|
showRead() {
|
||||||
return this.ebookFile && this.ebookFormat !== 'pdf'
|
return this.ebookFile && this.ebookFormat !== 'pdf'
|
||||||
},
|
},
|
||||||
|
showDownload() {
|
||||||
|
if (this.isIos) return false
|
||||||
|
return this.user && this.userCanDownload && this.showPlay && !this.hasLocal
|
||||||
|
},
|
||||||
ebookFile() {
|
ebookFile() {
|
||||||
return this.media.ebookFile
|
return this.media.ebookFile
|
||||||
},
|
},
|
||||||
@@ -242,9 +314,6 @@ export default {
|
|||||||
if (!this.ebookFile) return null
|
if (!this.ebookFile) return null
|
||||||
return this.ebookFile.ebookFormat
|
return this.ebookFile.ebookFormat
|
||||||
},
|
},
|
||||||
hasStoragePermission() {
|
|
||||||
return this.$store.state.hasStoragePermission
|
|
||||||
},
|
|
||||||
downloadItem() {
|
downloadItem() {
|
||||||
return this.$store.getters['globals/getDownloadItem'](this.libraryItemId)
|
return this.$store.getters['globals/getDownloadItem'](this.libraryItemId)
|
||||||
},
|
},
|
||||||
@@ -253,9 +322,41 @@ export default {
|
|||||||
},
|
},
|
||||||
isCasting() {
|
isCasting() {
|
||||||
return this.$store.state.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: {
|
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() {
|
readBook() {
|
||||||
this.$store.commit('openReader', this.libraryItem)
|
this.$store.commit('openReader', this.libraryItem)
|
||||||
},
|
},
|
||||||
@@ -279,7 +380,7 @@ export default {
|
|||||||
if (this.isLocal) {
|
if (this.isLocal) {
|
||||||
// TODO: If connected to server also sync with server
|
// TODO: If connected to server also sync with server
|
||||||
await this.$db.removeLocalMediaProgress(this.libraryItemId)
|
await this.$db.removeLocalMediaProgress(this.libraryItemId)
|
||||||
this.$store.commit('globals/removeLocalMediaProgress', this.libraryItemId)
|
this.$store.commit('globals/removeLocalMediaProgressForItem', this.libraryItemId)
|
||||||
} else {
|
} else {
|
||||||
var progressId = this.userItemProgress.id
|
var progressId = this.userItemProgress.id
|
||||||
await this.$axios
|
await this.$axios
|
||||||
@@ -319,13 +420,17 @@ export default {
|
|||||||
if (this.downloadItem) {
|
if (this.downloadItem) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.download()
|
|
||||||
},
|
|
||||||
async download(selectedLocalFolder = null) {
|
|
||||||
if (!this.numTracks) {
|
if (!this.numTracks) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (this.isIos) {
|
||||||
|
// no local folders on iOS
|
||||||
|
this.startDownload()
|
||||||
|
} else {
|
||||||
|
this.download()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async download(selectedLocalFolder = null) {
|
||||||
// Get the local folder to download to
|
// Get the local folder to download to
|
||||||
var localFolder = selectedLocalFolder
|
var localFolder = selectedLocalFolder
|
||||||
if (!localFolder) {
|
if (!localFolder) {
|
||||||
@@ -363,9 +468,15 @@ export default {
|
|||||||
this.startDownload(localFolder)
|
this.startDownload(localFolder)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async startDownload(localFolder) {
|
async startDownload(localFolder = null) {
|
||||||
console.log('Starting download to local folder', localFolder.name)
|
const payload = {
|
||||||
var downloadRes = await AbsDownloader.downloadLibraryItem({ libraryItemId: this.libraryItemId, localFolderId: localFolder.id })
|
libraryItemId: this.libraryItemId
|
||||||
|
}
|
||||||
|
if (localFolder) {
|
||||||
|
console.log('Starting download to local folder', localFolder.name)
|
||||||
|
payload.localFolderId = localFolder.id
|
||||||
|
}
|
||||||
|
var downloadRes = await AbsDownloader.downloadLibraryItem(payload)
|
||||||
if (downloadRes && downloadRes.error) {
|
if (downloadRes && downloadRes.error) {
|
||||||
var errorMsg = downloadRes.error || 'Unknown error'
|
var errorMsg = downloadRes.error || 'Unknown error'
|
||||||
console.error('Download error', errorMsg)
|
console.error('Download error', errorMsg)
|
||||||
@@ -432,4 +543,11 @@ export default {
|
|||||||
this.$socket.off('item_updated', this.itemUpdated)
|
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) {
|
if (this.selectedAudioTrack || this.selectedEpisode) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
text: 'Hard Delete',
|
text: 'Remove & Delete Files',
|
||||||
value: 'track-delete'
|
value: 'track-delete'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -208,7 +208,7 @@ export default {
|
|||||||
value: 'remove'
|
value: 'remove'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: 'Hard Delete',
|
text: 'Remove & Delete Files',
|
||||||
value: 'delete'
|
value: 'delete'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ class AbsAudioPlayerWeb extends WebPlugin {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.player.currentTime = this.trackStartTime
|
this.player.currentTime = this.trackStartTime
|
||||||
|
|
||||||
this.sendPlaybackMetadata(PlayerState.READY)
|
this.sendPlaybackMetadata(PlayerState.READY)
|
||||||
if (this.playWhenReady) {
|
if (this.playWhenReady) {
|
||||||
this.player.play()
|
this.player.play()
|
||||||
@@ -195,10 +196,9 @@ class AbsAudioPlayerWeb extends WebPlugin {
|
|||||||
evtTimeupdate() { }
|
evtTimeupdate() { }
|
||||||
|
|
||||||
sendPlaybackMetadata(playerState) {
|
sendPlaybackMetadata(playerState) {
|
||||||
var currentTime = this.player ? this.player.currentTime || 0 : 0
|
|
||||||
this.notifyListeners('onMetadata', {
|
this.notifyListeners('onMetadata', {
|
||||||
duration: this.totalDuration,
|
duration: this.totalDuration,
|
||||||
currentTime,
|
currentTime: this.overallCurrentTime,
|
||||||
playerState
|
playerState
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -195,6 +195,10 @@ class AbsDatabaseWeb extends WebPlugin {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async syncServerMediaProgressWithLocalMediaProgress(payload) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
async updateLocalTrackOrder({ localLibraryItemId, tracks }) {
|
async updateLocalTrackOrder({ localLibraryItemId, tracks }) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { Capacitor } from '@capacitor/core';
|
|
||||||
import { AbsDatabase } from './capacitor/AbsDatabase'
|
import { AbsDatabase } from './capacitor/AbsDatabase'
|
||||||
|
|
||||||
const isWeb = Capacitor.getPlatform() == 'web'
|
|
||||||
|
|
||||||
class DbService {
|
class DbService {
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
@@ -73,6 +70,10 @@ class DbService {
|
|||||||
return AbsDatabase.syncLocalMediaProgressWithServer()
|
return AbsDatabase.syncLocalMediaProgressWithServer()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
syncServerMediaProgressWithLocalMediaProgress(payload) {
|
||||||
|
return AbsDatabase.syncServerMediaProgressWithLocalMediaProgress(payload)
|
||||||
|
}
|
||||||
|
|
||||||
updateLocalTrackOrder(payload) {
|
updateLocalTrackOrder(payload) {
|
||||||
return AbsDatabase.updateLocalTrackOrder(payload)
|
return AbsDatabase.updateLocalTrackOrder(payload)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,7 +83,16 @@ class LocalStorage {
|
|||||||
await Storage.set({ key: 'lastLibraryId', value: libraryId })
|
await Storage.set({ key: 'lastLibraryId', value: libraryId })
|
||||||
console.log('[LocalStorage] Set Last Library Id', libraryId)
|
console.log('[LocalStorage] Set Last Library Id', libraryId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[LocalStorage] Failed to set current library', error)
|
console.error('[LocalStorage] Failed to set last library id', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeLastLibraryId() {
|
||||||
|
try {
|
||||||
|
await Storage.remove({ key: 'lastLibraryId' })
|
||||||
|
console.log('[LocalStorage] Remove Last Library Id')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[LocalStorage] Failed to remove last library id', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ class ServerSocket extends EventEmitter {
|
|||||||
console.log('[SOCKET] User Item Progress Updated', JSON.stringify(data))
|
console.log('[SOCKET] User Item Progress Updated', JSON.stringify(data))
|
||||||
var progress = data.data
|
var progress = data.data
|
||||||
this.$store.commit('user/updateUserMediaProgress', progress)
|
this.$store.commit('user/updateUserMediaProgress', progress)
|
||||||
|
this.emit('user_media_progress_updated', progress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,13 +22,16 @@ export const getters = {
|
|||||||
if (media.coverPath.startsWith('http:') || media.coverPath.startsWith('https:')) return media.coverPath
|
if (media.coverPath.startsWith('http:') || media.coverPath.startsWith('https:')) return media.coverPath
|
||||||
|
|
||||||
var userToken = rootGetters['user/getToken']
|
var userToken = rootGetters['user/getToken']
|
||||||
|
var serverAddress = rootGetters['user/getServerAddress']
|
||||||
|
if (!userToken || !serverAddress) return placeholder
|
||||||
|
|
||||||
var lastUpdate = libraryItem.updatedAt || Date.now()
|
var lastUpdate = libraryItem.updatedAt || Date.now()
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') { // Testing
|
if (process.env.NODE_ENV !== 'production') { // Testing
|
||||||
// return `http://localhost:3333/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}`
|
// return `http://localhost:3333/api/items/${libraryItem.id}/cover?token=${userToken}&ts=${lastUpdate}`
|
||||||
}
|
}
|
||||||
|
|
||||||
var url = new URL(`/api/items/${libraryItem.id}/cover`, rootGetters['user/getServerAddress'])
|
var url = new URL(`/api/items/${libraryItem.id}/cover`, serverAddress)
|
||||||
return `${url}?token=${userToken}&ts=${lastUpdate}`
|
return `${url}?token=${userToken}&ts=${lastUpdate}`
|
||||||
},
|
},
|
||||||
getLocalMediaProgressById: (state) => (localLibraryItemId, episodeId = null) => {
|
getLocalMediaProgressById: (state) => (localLibraryItemId, episodeId = null) => {
|
||||||
@@ -36,6 +39,12 @@ export const getters = {
|
|||||||
if (episodeId != null && lmp.localEpisodeId != episodeId) return false
|
if (episodeId != null && lmp.localEpisodeId != episodeId) return false
|
||||||
return lmp.localLibraryItemId == localLibraryItemId
|
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 +93,9 @@ export const mutations = {
|
|||||||
removeLocalMediaProgress(state, id) {
|
removeLocalMediaProgress(state, id) {
|
||||||
state.localMediaProgress = state.localMediaProgress.filter(lmp => lmp.id != 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) {
|
setLastSearch(state, val) {
|
||||||
state.lastSearch = val
|
state.lastSearch = val
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const state = () => ({
|
|||||||
playerIsPlaying: false,
|
playerIsPlaying: false,
|
||||||
isCasting: false,
|
isCasting: false,
|
||||||
isCastAvailable: false,
|
isCastAvailable: false,
|
||||||
appUpdateInfo: null,
|
|
||||||
socketConnected: false,
|
socketConnected: false,
|
||||||
networkConnected: false,
|
networkConnected: false,
|
||||||
networkConnectionType: null,
|
networkConnectionType: null,
|
||||||
@@ -17,7 +16,8 @@ export const state = () => ({
|
|||||||
showReader: false,
|
showReader: false,
|
||||||
showSideDrawer: false,
|
showSideDrawer: false,
|
||||||
isNetworkListenerInit: false,
|
isNetworkListenerInit: false,
|
||||||
serverSettings: null
|
serverSettings: null,
|
||||||
|
lastBookshelfScrollData: {}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
@@ -55,6 +55,9 @@ export const actions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const mutations = {
|
export const mutations = {
|
||||||
|
setLastBookshelfScrollData(state, { scrollTop, path, name }) {
|
||||||
|
state.lastBookshelfScrollData[name] = { scrollTop, path }
|
||||||
|
},
|
||||||
setPlayerItem(state, playbackSession) {
|
setPlayerItem(state, playbackSession) {
|
||||||
state.playerIsLocal = playbackSession ? playbackSession.playMethod == this.$constants.PlayMethod.LOCAL : false
|
state.playerIsLocal = playbackSession ? playbackSession.playMethod == this.$constants.PlayMethod.LOCAL : false
|
||||||
|
|
||||||
@@ -84,9 +87,6 @@ export const mutations = {
|
|||||||
setIsFirstLoad(state, val) {
|
setIsFirstLoad(state, val) {
|
||||||
state.isFirstLoad = val
|
state.isFirstLoad = val
|
||||||
},
|
},
|
||||||
setAppUpdateInfo(state, info) {
|
|
||||||
state.appUpdateInfo = info
|
|
||||||
},
|
|
||||||
setSocketConnected(state, val) {
|
setSocketConnected(state, val) {
|
||||||
state.socketConnected = val
|
state.socketConnected = val
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export const mutations = {
|
|||||||
},
|
},
|
||||||
reset(state) {
|
reset(state) {
|
||||||
state.lastLoad = 0
|
state.lastLoad = 0
|
||||||
|
state.currentLibraryId = null
|
||||||
state.libraries = []
|
state.libraries = []
|
||||||
},
|
},
|
||||||
setCurrentLibrary(state, val) {
|
setCurrentLibrary(state, val) {
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ export const getters = {
|
|||||||
},
|
},
|
||||||
getUserSetting: (state) => (key) => {
|
getUserSetting: (state) => (key) => {
|
||||||
return state.settings ? state.settings[key] || null : null
|
return state.settings ? state.settings[key] || null : null
|
||||||
|
},
|
||||||
|
getUserCanDownload: (state) => {
|
||||||
|
return state.user && state.user.permissions ? !!state.user.permissions.download : false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||