Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84632c8a32 | ||
|
|
df717fc87f | ||
|
|
2b9f5c866b | ||
|
|
2de81130a1 | ||
|
|
2942d703ee | ||
|
|
3ef6c345e4 | ||
|
|
0a4052dec2 | ||
|
|
ef4bb6c7a8 | ||
|
|
d9c38d1592 | ||
|
|
a7227acbc3 | ||
|
|
bd5229941b | ||
|
|
0583ce617e | ||
|
|
7462eb3bc4 | ||
|
|
18e15ff056 | ||
|
|
4f6468c23a | ||
|
|
31531197a2 | ||
|
|
479de5f067 | ||
|
|
03240415b8 | ||
|
|
f8a6b6a936 | ||
|
|
28e50dd461 | ||
|
|
f06afbd08c | ||
|
|
860c7aac80 | ||
|
|
8bab4ae383 | ||
|
|
69171e5732 | ||
|
|
31abaa019f | ||
|
|
ad57a90e4d | ||
|
|
925e8fd17e | ||
|
|
7b857f04d4 | ||
|
|
453744a637 | ||
|
|
062d4dc069 | ||
|
|
21bd98b367 | ||
|
|
ccd206e95d | ||
|
|
bca5421dbc | ||
|
|
20085c5eed | ||
|
|
a8c66ff808 | ||
|
|
b2d3edca81 | ||
|
|
4b7fa7a543 | ||
|
|
a018dcf085 | ||
|
|
091c1bcb9f | ||
|
|
d89c97ba1a | ||
|
|
56fe350104 | ||
|
|
eb552170e3 | ||
|
|
29d022adcc | ||
|
|
d9afdaf56f | ||
|
|
699976dcd8 | ||
|
|
acf85f4f09 | ||
|
|
8f6dd72df2 | ||
|
|
297eca6a86 | ||
|
|
b1805875b9 | ||
|
|
16b06b5eb8 | ||
|
|
dbbb5faa78 | ||
|
|
5482b30324 | ||
|
|
5bae5411c8 | ||
|
|
f75a8dfb86 | ||
|
|
716d07c74e | ||
|
|
7de0304a3d | ||
|
|
df54094221 | ||
|
|
3207841384 | ||
|
|
ae571d6b79 | ||
|
|
f6ee0bf8df | ||
|
|
d59f3ae0b6 | ||
|
|
89041c4141 | ||
|
|
aac77949b4 | ||
|
|
eb40f6e6e9 | ||
|
|
34b3910ff8 | ||
|
|
83fdf4fd65 | ||
|
|
ea6bb0a847 | ||
|
|
d85158bd70 | ||
|
|
22c5fee48e | ||
|
|
0890a44c0b | ||
|
|
4fd4cc5604 | ||
|
|
c73b71345a | ||
|
|
11f0af1ebd | ||
|
|
a532f988b0 | ||
|
|
bec2796771 | ||
|
|
feec7f7399 | ||
|
|
067c2f84dd | ||
|
|
69996a4346 | ||
|
|
80cda129d2 | ||
|
|
dd0ff04155 | ||
|
|
c69558aa7f | ||
|
|
8fefea0d94 | ||
|
|
0ddee6f44e | ||
|
|
feec1ab55a | ||
|
|
49500aecd6 | ||
|
|
894bf100d2 | ||
|
|
7ba810adb8 | ||
|
|
d10f33362e | ||
|
|
82e8fbba2b | ||
|
|
64325fe2a6 | ||
|
|
99217fee48 | ||
|
|
f12e64990c | ||
|
|
ef7bb9a9d8 | ||
|
|
5ac925e151 | ||
|
|
acc5bde33a | ||
|
|
b521f37ec1 | ||
|
|
379fa21571 | ||
|
|
1aa6a441f3 | ||
|
|
705bbaccc1 | ||
|
|
8365e06762 | ||
|
|
139a0b3412 | ||
|
|
ea6b21169e | ||
|
|
a262efe9da | ||
|
|
ed5fb09007 | ||
|
|
d3c6429fd6 | ||
|
|
371fcacc87 | ||
|
|
75666e523f | ||
|
|
592ea7c533 | ||
|
|
b860dfc791 | ||
|
|
96dde8cf31 | ||
|
|
b4a37fed28 | ||
|
|
ad3fee8907 | ||
|
|
94db55598a | ||
|
|
9687f47b6b | ||
|
|
af216d9b38 | ||
|
|
c7879c2bc0 | ||
|
|
7db503a2cf | ||
|
|
d72295a0e6 | ||
|
|
638da4400f | ||
|
|
af9a7aafae | ||
|
|
ab9f7fed64 | ||
|
|
b62ce27487 | ||
|
|
6d9e902fe7 | ||
|
|
a0faf3f7d4 | ||
|
|
32dfb3b40a | ||
|
|
6bbcd300e4 | ||
|
|
c54d9b3c5f | ||
|
|
9ab03fbcd4 | ||
|
|
a852114e9c | ||
|
|
3c9b8c1c6f | ||
|
|
4735b51dc0 | ||
|
|
23927a1d59 | ||
|
|
40b592f7c1 | ||
|
|
4da31f5850 | ||
|
|
f79e96cbfe | ||
|
|
2dbce1e3bb | ||
|
|
cdfbbcbf8a | ||
|
|
890852bef5 | ||
|
|
3b285db21a | ||
|
|
d37be88977 | ||
|
|
e6d27a1487 | ||
|
|
5d23c17d30 | ||
|
|
9c9c1c3b94 | ||
|
|
4e94fd6ad0 | ||
|
|
81a8203cca | ||
|
|
b6b206c288 | ||
|
|
ed2310a2e7 | ||
|
|
bae5d97087 | ||
|
|
3e12ff139b | ||
|
|
7ccd8db085 | ||
|
|
fdc829d06c | ||
|
|
e7f61e34e8 | ||
|
|
141877f111 | ||
|
|
b71a976e61 | ||
|
|
68c95ed6d5 | ||
|
|
7a68cc99af | ||
|
|
7294bc7033 | ||
|
|
f8836be147 | ||
|
|
713999eb83 | ||
|
|
cc4c9787c0 | ||
|
|
ecc5cf3e1b | ||
|
|
ba7d2be03e | ||
|
|
368f349c78 | ||
|
|
639d641c07 | ||
|
|
d9313a1654 | ||
|
|
9dd532285c | ||
|
|
92f10fdb5c | ||
|
|
9d0f43a664 | ||
|
|
453365fcd6 | ||
|
|
6231c5fcc4 |
@@ -22,6 +22,8 @@ kotlin {
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
||||||
|
|
||||||
|
namespace 'com.audiobookshelf.app'
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
}
|
}
|
||||||
@@ -33,8 +35,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 88
|
versionCode 92
|
||||||
versionName "0.9.57-beta"
|
versionName "0.9.61-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.
|
||||||
@@ -43,6 +45,12 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
applicationIdSuffix ".debug"
|
||||||
|
versionNameSuffix "-debug"
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
release {
|
release {
|
||||||
minifyEnabled false
|
minifyEnabled false
|
||||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
@@ -72,6 +80,7 @@ configurations.all {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||||
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||||
implementation project(':capacitor-android')
|
implementation project(':capacitor-android')
|
||||||
@@ -115,7 +124,7 @@ dependencies {
|
|||||||
implementation 'io.github.pilgr:paperdb:2.7.2'
|
implementation 'io.github.pilgr:paperdb:2.7.2'
|
||||||
|
|
||||||
// Simple Storage
|
// Simple Storage
|
||||||
implementation "com.anggrayudi:storage:0.14.0"
|
implementation "com.anggrayudi:storage:1.5.4"
|
||||||
|
|
||||||
// OK HTTP
|
// OK HTTP
|
||||||
implementation 'com.squareup.okhttp3:okhttp:4.9.2'
|
implementation 'com.squareup.okhttp3:okhttp:4.9.2'
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_1_8
|
sourceCompatibility JavaVersion.VERSION_11
|
||||||
targetCompatibility JavaVersion.VERSION_1_8
|
targetCompatibility JavaVersion.VERSION_11
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,8 +13,8 @@ dependencies {
|
|||||||
implementation project(':capacitor-dialog')
|
implementation project(':capacitor-dialog')
|
||||||
implementation project(':capacitor-haptics')
|
implementation project(':capacitor-haptics')
|
||||||
implementation project(':capacitor-network')
|
implementation project(':capacitor-network')
|
||||||
|
implementation project(':capacitor-preferences')
|
||||||
implementation project(':capacitor-status-bar')
|
implementation project(':capacitor-status-bar')
|
||||||
implementation project(':capacitor-storage')
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background android:drawable="@mipmap/ic_launcher_background"/>
|
||||||
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
|
</adaptive-icon>
|
||||||
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 879 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 685 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 8.0 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -2,7 +2,6 @@
|
|||||||
<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"
|
||||||
package="com.audiobookshelf.app"
|
|
||||||
android:installLocation="preferExternal" >
|
android:installLocation="preferExternal" >
|
||||||
|
|
||||||
<!-- Permissions -->
|
<!-- Permissions -->
|
||||||
@@ -75,7 +74,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
|
<receiver android:name="androidx.media.session.MediaButtonReceiver" android:exported="false">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|||||||
@@ -16,11 +16,11 @@
|
|||||||
"classpath": "com.capacitorjs.plugins.network.NetworkPlugin"
|
"classpath": "com.capacitorjs.plugins.network.NetworkPlugin"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pkg": "@capacitor/status-bar",
|
"pkg": "@capacitor/preferences",
|
||||||
"classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin"
|
"classpath": "com.capacitorjs.plugins.preferences.PreferencesPlugin"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pkg": "@capacitor/storage",
|
"pkg": "@capacitor/status-bar",
|
||||||
"classpath": "com.capacitorjs.plugins.storage.StoragePlugin"
|
"classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import android.util.Log
|
|||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import com.anggrayudi.storage.SimpleStorage
|
import com.anggrayudi.storage.SimpleStorage
|
||||||
import com.anggrayudi.storage.SimpleStorageHelper
|
import com.anggrayudi.storage.SimpleStorageHelper
|
||||||
import com.audiobookshelf.app.data.AbsDatabase
|
import com.audiobookshelf.app.plugins.AbsDatabase
|
||||||
import com.audiobookshelf.app.data.DbManager
|
import com.audiobookshelf.app.managers.DbManager
|
||||||
import com.audiobookshelf.app.player.PlayerNotificationService
|
import com.audiobookshelf.app.player.PlayerNotificationService
|
||||||
import com.audiobookshelf.app.plugins.AbsAudioPlayer
|
import com.audiobookshelf.app.plugins.AbsAudioPlayer
|
||||||
import com.audiobookshelf.app.plugins.AbsDownloader
|
import com.audiobookshelf.app.plugins.AbsDownloader
|
||||||
@@ -51,11 +51,17 @@ class MainActivity : BridgeActivity() {
|
|||||||
// .detectLeakedClosableObjects()
|
// .detectLeakedClosableObjects()
|
||||||
// .penaltyLog()
|
// .penaltyLog()
|
||||||
// .build())
|
// .build())
|
||||||
|
DbManager.initialize(applicationContext)
|
||||||
|
|
||||||
|
registerPlugin(AbsAudioPlayer::class.java)
|
||||||
|
registerPlugin(AbsDownloader::class.java)
|
||||||
|
registerPlugin(AbsFileSystem::class.java)
|
||||||
|
registerPlugin(AbsDatabase::class.java)
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
Log.d(tag, "onCreate")
|
Log.d(tag, "onCreate")
|
||||||
|
|
||||||
DbManager.initialize(applicationContext)
|
|
||||||
|
|
||||||
val permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
|
val permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
if (permission != PackageManager.PERMISSION_GRANTED) {
|
if (permission != PackageManager.PERMISSION_GRANTED) {
|
||||||
@@ -63,11 +69,6 @@ class MainActivity : BridgeActivity() {
|
|||||||
PERMISSIONS_ALL,
|
PERMISSIONS_ALL,
|
||||||
REQUEST_PERMISSIONS)
|
REQUEST_PERMISSIONS)
|
||||||
}
|
}
|
||||||
|
|
||||||
registerPlugin(AbsAudioPlayer::class.java)
|
|
||||||
registerPlugin(AbsDownloader::class.java)
|
|
||||||
registerPlugin(AbsFileSystem::class.java)
|
|
||||||
registerPlugin(AbsDatabase::class.java)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
data class AudioTrack(
|
||||||
|
var index:Int,
|
||||||
|
var startOffset:Double,
|
||||||
|
var duration:Double,
|
||||||
|
var title:String,
|
||||||
|
var contentUrl:String,
|
||||||
|
var mimeType:String,
|
||||||
|
var metadata:FileMetadata?,
|
||||||
|
var isLocal:Boolean,
|
||||||
|
var localFileId:String?,
|
||||||
|
var audioProbeResult:AudioProbeResult?,
|
||||||
|
var serverIndex:Int? // Need to know if server track index is different
|
||||||
|
) {
|
||||||
|
|
||||||
|
@get:JsonIgnore
|
||||||
|
val startOffsetMs get() = (startOffset * 1000L).toLong()
|
||||||
|
@get:JsonIgnore
|
||||||
|
val durationMs get() = (duration * 1000L).toLong()
|
||||||
|
@get:JsonIgnore
|
||||||
|
val endOffsetMs get() = startOffsetMs + durationMs
|
||||||
|
@get:JsonIgnore
|
||||||
|
val relPath get() = metadata?.relPath ?: ""
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun getBookChapter():BookChapter {
|
||||||
|
return BookChapter(index + 1,startOffset, startOffset + duration, title)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
package com.audiobookshelf.app.data
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v4.media.MediaDescriptionCompat
|
import android.support.v4.media.MediaDescriptionCompat
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import androidx.media.utils.MediaConstants
|
import androidx.media.utils.MediaConstants
|
||||||
|
import com.audiobookshelf.app.media.MediaManager
|
||||||
import com.fasterxml.jackson.annotation.*
|
import com.fasterxml.jackson.annotation.*
|
||||||
|
|
||||||
// This auto-detects whether it is a Book or Podcast
|
// This auto-detects whether it is a Book or Podcast
|
||||||
@@ -50,7 +52,8 @@ 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_ep_" + at.localFileId,episodes?.size ?: 0 + 1,null,null,at.title,null,null,null,at,at.duration,0, null)
|
val localEpisodeId = "local_ep_" + at.localFileId
|
||||||
|
val newEpisode = PodcastEpisode(localEpisodeId,(episodes?.size ?: 0) + 1,null,null,at.title,null,null,null, null, null, at,at.duration,0, null, localEpisodeId)
|
||||||
episodes?.add(newEpisode)
|
episodes?.add(newEpisode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,7 +66,8 @@ class Podcast(
|
|||||||
}
|
}
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun addAudioTrack(audioTrack:AudioTrack) {
|
override fun addAudioTrack(audioTrack:AudioTrack) {
|
||||||
val newEpisode = PodcastEpisode("local_" + audioTrack.localFileId,(episodes?.size ?: 0) + 1,null,null,audioTrack.title,null,null,null,audioTrack,audioTrack.duration,0, null)
|
val localEpisodeId = "local_ep_" + audioTrack.localFileId
|
||||||
|
val newEpisode = PodcastEpisode(localEpisodeId,(episodes?.size ?: 0) + 1,null,null,audioTrack.title,null,null,null, null, null,audioTrack,audioTrack.duration,0, null, localEpisodeId)
|
||||||
episodes?.add(newEpisode)
|
episodes?.add(newEpisode)
|
||||||
|
|
||||||
var index = 1
|
var index = 1
|
||||||
@@ -82,9 +86,17 @@ class Podcast(
|
|||||||
index++
|
index++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Used for FolderScanner local podcast item to get copy of Podcast excluding episodes
|
||||||
|
@JsonIgnore
|
||||||
|
override fun getLocalCopy(): Podcast {
|
||||||
|
return Podcast(metadata as PodcastMetadata,coverPath,tags, mutableListOf(),autoDownloadEpisodes, 0)
|
||||||
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode):PodcastEpisode {
|
fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode):PodcastEpisode {
|
||||||
val newEpisode = PodcastEpisode("local_" + episode.id,(episodes?.size ?: 0) + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,audioTrack,audioTrack.duration,0, episode.id)
|
val localEpisodeId = "local_ep_" + episode.id
|
||||||
|
val newEpisode = PodcastEpisode(localEpisodeId,(episodes?.size ?: 0) + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,null,null,audioTrack,audioTrack.duration,0, episode.id, localEpisodeId)
|
||||||
episodes?.add(newEpisode)
|
episodes?.add(newEpisode)
|
||||||
|
|
||||||
var index = 1
|
var index = 1
|
||||||
@@ -95,10 +107,14 @@ class Podcast(
|
|||||||
return newEpisode
|
return newEpisode
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used for FolderScanner local podcast item to get copy of Podcast excluding episodes
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getLocalCopy(): Podcast {
|
fun getNextUnfinishedEpisode(libraryItemId:String, mediaManager: MediaManager):PodcastEpisode? {
|
||||||
return Podcast(metadata as PodcastMetadata,coverPath,tags, mutableListOf(),autoDownloadEpisodes, 0)
|
val sortedEpisodes = episodes?.sortedByDescending { it.publishedAt }
|
||||||
|
val podcastEpisode = sortedEpisodes?.find { episode ->
|
||||||
|
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItemId && it.episodeId == episode.id }
|
||||||
|
progress == null || !progress.isFinished
|
||||||
|
}
|
||||||
|
return podcastEpisode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,10 +138,13 @@ class Book(
|
|||||||
override fun setAudioTracks(audioTracks:MutableList<AudioTrack>) {
|
override fun setAudioTracks(audioTracks:MutableList<AudioTrack>) {
|
||||||
tracks = audioTracks
|
tracks = audioTracks
|
||||||
tracks?.sortBy { it.index }
|
tracks?.sortBy { it.index }
|
||||||
// TODO: Is it necessary to calculate this each time? check if can remove safely
|
|
||||||
var totalDuration = 0.0
|
var totalDuration = 0.0
|
||||||
|
var currentStartOffset = 0.0
|
||||||
tracks?.forEach {
|
tracks?.forEach {
|
||||||
totalDuration += it.duration
|
totalDuration += it.duration
|
||||||
|
it.startOffset = currentStartOffset
|
||||||
|
currentStartOffset += it.duration
|
||||||
}
|
}
|
||||||
duration = totalDuration
|
duration = totalDuration
|
||||||
}
|
}
|
||||||
@@ -158,7 +177,6 @@ class Book(
|
|||||||
}
|
}
|
||||||
duration = totalDuration
|
duration = totalDuration
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getLocalCopy(): Book {
|
override fun getLocalCopy(): Book {
|
||||||
return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(),null,null, 0)
|
return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(),null,null, 0)
|
||||||
@@ -228,21 +246,32 @@ data class PodcastEpisode(
|
|||||||
var title:String?,
|
var title:String?,
|
||||||
var subtitle:String?,
|
var subtitle:String?,
|
||||||
var description:String?,
|
var description:String?,
|
||||||
|
var pubDate:String?,
|
||||||
|
var publishedAt:Long?,
|
||||||
var audioFile:AudioFile?,
|
var audioFile:AudioFile?,
|
||||||
var audioTrack:AudioTrack?,
|
var audioTrack:AudioTrack?,
|
||||||
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
|
||||||
|
var localEpisodeId:String? // For Android Auto server episodes with local copy
|
||||||
) {
|
) {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getMediaDescription(libraryItem:LibraryItemWrapper, progress:MediaProgressWrapper?): MediaDescriptionCompat {
|
fun getMediaDescription(libraryItem:LibraryItemWrapper, progress:MediaProgressWrapper?, ctx: Context?): MediaDescriptionCompat {
|
||||||
val coverUri = if(libraryItem is LocalLibraryItem) {
|
val coverUri = if (libraryItem is LocalLibraryItem) {
|
||||||
libraryItem.getCoverUri()
|
libraryItem.getCoverUri()
|
||||||
} else {
|
} else {
|
||||||
(libraryItem as LibraryItem).getCoverUri()
|
(libraryItem as LibraryItem).getCoverUri()
|
||||||
}
|
}
|
||||||
|
|
||||||
val extras = Bundle()
|
val extras = Bundle()
|
||||||
|
|
||||||
|
if (localEpisodeId != null) {
|
||||||
|
extras.putLong(
|
||||||
|
MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
|
||||||
|
MediaDescriptionCompat.STATUS_DOWNLOADED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (progress != null) {
|
if (progress != null) {
|
||||||
if (progress.isFinished) {
|
if (progress.isFinished) {
|
||||||
extras.putInt(
|
extras.putInt(
|
||||||
@@ -264,22 +293,21 @@ data class PodcastEpisode(
|
|||||||
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
|
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// return MediaMetadataCompat.Builder().apply {
|
|
||||||
// putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
val libraryItemDescription = libraryItem.getMediaDescription(null, ctx)
|
||||||
// putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
|
val mediaId = localEpisodeId ?: id
|
||||||
// putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
|
val mediaDescriptionBuilder = MediaDescriptionCompat.Builder()
|
||||||
// putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, podcast.metadata.getAuthorDisplayName())
|
.setMediaId(mediaId)
|
||||||
// putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString())
|
|
||||||
//
|
|
||||||
// }.build()
|
|
||||||
val libraryItemDescription = libraryItem.getMediaDescription(null)
|
|
||||||
return MediaDescriptionCompat.Builder()
|
|
||||||
.setMediaId(id)
|
|
||||||
.setTitle(title)
|
.setTitle(title)
|
||||||
.setIconUri(coverUri)
|
.setIconUri(coverUri)
|
||||||
.setSubtitle(libraryItemDescription.title)
|
.setSubtitle(libraryItemDescription.title)
|
||||||
.setExtras(extras)
|
.setExtras(extras)
|
||||||
.build()
|
|
||||||
|
libraryItemDescription.iconBitmap?.let {
|
||||||
|
mediaDescriptionBuilder.setIconBitmap(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mediaDescriptionBuilder.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,36 +357,6 @@ data class Folder(
|
|||||||
var fullPath:String
|
var fullPath:String
|
||||||
)
|
)
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
data class AudioTrack(
|
|
||||||
var index:Int,
|
|
||||||
var startOffset:Double,
|
|
||||||
var duration:Double,
|
|
||||||
var title:String,
|
|
||||||
var contentUrl:String,
|
|
||||||
var mimeType:String,
|
|
||||||
var metadata:FileMetadata?,
|
|
||||||
var isLocal:Boolean,
|
|
||||||
var localFileId:String?,
|
|
||||||
var audioProbeResult:AudioProbeResult?,
|
|
||||||
var serverIndex:Int? // Need to know if server track index is different
|
|
||||||
) {
|
|
||||||
|
|
||||||
@get:JsonIgnore
|
|
||||||
val startOffsetMs get() = (startOffset * 1000L).toLong()
|
|
||||||
@get:JsonIgnore
|
|
||||||
val durationMs get() = (duration * 1000L).toLong()
|
|
||||||
@get:JsonIgnore
|
|
||||||
val endOffsetMs get() = startOffsetMs + durationMs
|
|
||||||
@get:JsonIgnore
|
|
||||||
val relPath get() = metadata?.relPath ?: ""
|
|
||||||
|
|
||||||
@JsonIgnore
|
|
||||||
fun getBookChapter():BookChapter {
|
|
||||||
return BookChapter(index + 1,startOffset, startOffset + duration, title)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class BookChapter(
|
data class BookChapter(
|
||||||
var id:Int,
|
var id:Int,
|
||||||
@@ -372,26 +370,14 @@ data class BookChapter(
|
|||||||
val endMs get() = (end * 1000L).toLong()
|
val endMs get() = (end * 1000L).toLong()
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
|
||||||
class MediaProgress(
|
|
||||||
var id:String,
|
|
||||||
var libraryItemId:String,
|
|
||||||
var episodeId:String?,
|
|
||||||
var duration:Double, // seconds
|
|
||||||
progress:Double, // 0 to 1
|
|
||||||
var currentTime:Double,
|
|
||||||
isFinished:Boolean,
|
|
||||||
var lastUpdate:Long,
|
|
||||||
var startedAt:Long,
|
|
||||||
var finishedAt:Long?
|
|
||||||
) : MediaProgressWrapper(isFinished, progress)
|
|
||||||
|
|
||||||
@JsonTypeInfo(use= JsonTypeInfo.Id.DEDUCTION, defaultImpl = MediaProgress::class)
|
@JsonTypeInfo(use= JsonTypeInfo.Id.DEDUCTION, defaultImpl = MediaProgress::class)
|
||||||
@JsonSubTypes(
|
@JsonSubTypes(
|
||||||
JsonSubTypes.Type(MediaProgress::class),
|
JsonSubTypes.Type(MediaProgress::class),
|
||||||
JsonSubTypes.Type(LocalMediaProgress::class)
|
JsonSubTypes.Type(LocalMediaProgress::class)
|
||||||
)
|
)
|
||||||
open class MediaProgressWrapper(var isFinished:Boolean, var progress:Double)
|
open class MediaProgressWrapper(var isFinished:Boolean, var currentTime:Double, var progress:Double) {
|
||||||
|
open val mediaItemId get() = ""
|
||||||
|
}
|
||||||
|
|
||||||
// Helper class
|
// Helper class
|
||||||
data class LibraryItemWithEpisode(
|
data class LibraryItemWithEpisode(
|
||||||
|
|||||||
@@ -1,11 +1,25 @@
|
|||||||
package com.audiobookshelf.app.data
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.support.v4.media.MediaDescriptionCompat
|
import android.support.v4.media.MediaDescriptionCompat
|
||||||
|
import android.util.Log
|
||||||
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.fasterxml.jackson.annotation.JsonSubTypes
|
import com.fasterxml.jackson.annotation.JsonSubTypes
|
||||||
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
import com.fasterxml.jackson.annotation.JsonTypeInfo
|
||||||
|
|
||||||
|
enum class LockOrientationSetting {
|
||||||
|
NONE, PORTRAIT, LANDSCAPE
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class HapticFeedbackSetting {
|
||||||
|
OFF, LIGHT, MEDIUM, HEAVY
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class ShakeSensitivitySetting {
|
||||||
|
VERY_LOW, LOW, MEDIUM, HIGH, VERY_HIGH
|
||||||
|
}
|
||||||
|
|
||||||
data class ServerConnectionConfig(
|
data class ServerConnectionConfig(
|
||||||
var id:String,
|
var id:String,
|
||||||
var index:Int,
|
var index:Int,
|
||||||
@@ -17,46 +31,6 @@ data class ServerConnectionConfig(
|
|||||||
var customHeaders:Map<String, String>?
|
var customHeaders:Map<String, String>?
|
||||||
)
|
)
|
||||||
|
|
||||||
data class DeviceSettings(
|
|
||||||
var disableAutoRewind:Boolean,
|
|
||||||
var enableAltView:Boolean,
|
|
||||||
var jumpBackwardsTime:Int,
|
|
||||||
var jumpForwardTime:Int,
|
|
||||||
var disableShakeToResetSleepTimer:Boolean
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
// Static method to get default device settings
|
|
||||||
fun default():DeviceSettings {
|
|
||||||
return DeviceSettings(
|
|
||||||
disableAutoRewind = false,
|
|
||||||
enableAltView = false,
|
|
||||||
jumpBackwardsTime = 10,
|
|
||||||
jumpForwardTime = 10,
|
|
||||||
disableShakeToResetSleepTimer = false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@get:JsonIgnore
|
|
||||||
val jumpBackwardsTimeMs get() = (jumpBackwardsTime ?: default().jumpBackwardsTime) * 1000L
|
|
||||||
@get:JsonIgnore
|
|
||||||
val jumpForwardTimeMs get() = (jumpForwardTime ?: default().jumpBackwardsTime) * 1000L
|
|
||||||
}
|
|
||||||
|
|
||||||
data class DeviceData(
|
|
||||||
var serverConnectionConfigs:MutableList<ServerConnectionConfig>,
|
|
||||||
var lastServerConnectionConfigId:String?,
|
|
||||||
var currentLocalPlaybackSession:PlaybackSession?, // Stored to open up where left off for local media
|
|
||||||
var deviceSettings:DeviceSettings?
|
|
||||||
) {
|
|
||||||
@JsonIgnore
|
|
||||||
fun getLastServerConnectionConfig():ServerConnectionConfig? {
|
|
||||||
return lastServerConnectionConfigId?.let { lsccid ->
|
|
||||||
return serverConnectionConfigs.find { it.id == lsccid }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class LocalFile(
|
data class LocalFile(
|
||||||
var id:String,
|
var id:String,
|
||||||
@@ -95,7 +69,7 @@ data class LocalFolder(
|
|||||||
)
|
)
|
||||||
open class LibraryItemWrapper(var id:String) {
|
open class LibraryItemWrapper(var id:String) {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
open fun getMediaDescription(progress:MediaProgressWrapper?): MediaDescriptionCompat { return MediaDescriptionCompat.Builder().build() }
|
open fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context?): MediaDescriptionCompat { return MediaDescriptionCompat.Builder().build() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
@@ -114,3 +88,80 @@ data class PlayItemRequestPayload(
|
|||||||
var forceTranscode:Boolean,
|
var forceTranscode:Boolean,
|
||||||
var deviceInfo:DeviceInfo
|
var deviceInfo:DeviceInfo
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
data class DeviceSettings(
|
||||||
|
var disableAutoRewind:Boolean,
|
||||||
|
var enableAltView:Boolean,
|
||||||
|
var jumpBackwardsTime:Int,
|
||||||
|
var jumpForwardTime:Int,
|
||||||
|
var disableShakeToResetSleepTimer:Boolean,
|
||||||
|
var shakeSensitivity: ShakeSensitivitySetting,
|
||||||
|
var lockOrientation: LockOrientationSetting,
|
||||||
|
var hapticFeedback: HapticFeedbackSetting,
|
||||||
|
var autoSleepTimer: Boolean,
|
||||||
|
var autoSleepTimerStartTime: String,
|
||||||
|
var autoSleepTimerEndTime: String,
|
||||||
|
var sleepTimerLength: Long // Time in milliseconds
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
// Static method to get default device settings
|
||||||
|
fun default():DeviceSettings {
|
||||||
|
return DeviceSettings(
|
||||||
|
disableAutoRewind = false,
|
||||||
|
enableAltView = true,
|
||||||
|
jumpBackwardsTime = 10,
|
||||||
|
jumpForwardTime = 10,
|
||||||
|
disableShakeToResetSleepTimer = false,
|
||||||
|
shakeSensitivity = ShakeSensitivitySetting.MEDIUM,
|
||||||
|
lockOrientation = LockOrientationSetting.NONE,
|
||||||
|
hapticFeedback = HapticFeedbackSetting.LIGHT,
|
||||||
|
autoSleepTimer = false,
|
||||||
|
autoSleepTimerStartTime = "22:00",
|
||||||
|
autoSleepTimerEndTime = "06:00",
|
||||||
|
sleepTimerLength = 900000L // 15 minutes
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:JsonIgnore
|
||||||
|
val jumpBackwardsTimeMs get() = jumpBackwardsTime * 1000L
|
||||||
|
@get:JsonIgnore
|
||||||
|
val jumpForwardTimeMs get() = jumpForwardTime * 1000L
|
||||||
|
@get:JsonIgnore
|
||||||
|
val autoSleepTimerStartHour get() = autoSleepTimerStartTime.split(":")[0].toInt()
|
||||||
|
@get:JsonIgnore
|
||||||
|
val autoSleepTimerStartMinute get() = autoSleepTimerStartTime.split(":")[1].toInt()
|
||||||
|
@get:JsonIgnore
|
||||||
|
val autoSleepTimerEndHour get() = autoSleepTimerEndTime.split(":")[0].toInt()
|
||||||
|
@get:JsonIgnore
|
||||||
|
val autoSleepTimerEndMinute get() = autoSleepTimerEndTime.split(":")[1].toInt()
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun getShakeThresholdGravity() : Float { // Used in ShakeDetector
|
||||||
|
return if (shakeSensitivity == ShakeSensitivitySetting.VERY_HIGH) 1.2f
|
||||||
|
else if (shakeSensitivity == ShakeSensitivitySetting.HIGH) 1.4f
|
||||||
|
else if (shakeSensitivity == ShakeSensitivitySetting.MEDIUM) 1.6f
|
||||||
|
else if (shakeSensitivity == ShakeSensitivitySetting.LOW) 2f
|
||||||
|
else if (shakeSensitivity == ShakeSensitivitySetting.VERY_LOW) 2.7f
|
||||||
|
else {
|
||||||
|
Log.e("DeviceSetting", "Invalid ShakeSensitivitySetting $shakeSensitivity")
|
||||||
|
1.6f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class DeviceData(
|
||||||
|
var serverConnectionConfigs:MutableList<ServerConnectionConfig>,
|
||||||
|
var lastServerConnectionConfigId:String?,
|
||||||
|
var currentLocalPlaybackSession: PlaybackSession?, // Stored to open up where left off for local media
|
||||||
|
var deviceSettings: DeviceSettings?
|
||||||
|
) {
|
||||||
|
@JsonIgnore
|
||||||
|
fun getLastServerConnectionConfig(): ServerConnectionConfig? {
|
||||||
|
return lastServerConnectionConfigId?.let { lsccid ->
|
||||||
|
return serverConnectionConfigs.find { it.id == lsccid }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v4.media.MediaDescriptionCompat
|
import android.support.v4.media.MediaDescriptionCompat
|
||||||
@@ -29,7 +30,8 @@ class LibraryItem(
|
|||||||
var mediaType:String,
|
var mediaType:String,
|
||||||
var media:MediaType,
|
var media:MediaType,
|
||||||
var libraryFiles:MutableList<LibraryFile>?,
|
var libraryFiles:MutableList<LibraryFile>?,
|
||||||
var userMediaProgress:MediaProgress? // Only included when requesting library item with progress (for downloads)
|
var userMediaProgress:MediaProgress?, // Only included when requesting library item with progress (for downloads)
|
||||||
|
var localLibraryItemId:String? // For Android Auto
|
||||||
) : LibraryItemWrapper(id) {
|
) : LibraryItemWrapper(id) {
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val title get() = media.metadata.title
|
val title get() = media.metadata.title
|
||||||
@@ -55,8 +57,16 @@ class LibraryItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getMediaDescription(progress:MediaProgressWrapper?): MediaDescriptionCompat {
|
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context?): MediaDescriptionCompat {
|
||||||
val extras = Bundle()
|
val extras = Bundle()
|
||||||
|
|
||||||
|
if (localLibraryItemId != null) {
|
||||||
|
extras.putLong(
|
||||||
|
MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
|
||||||
|
MediaDescriptionCompat.STATUS_DOWNLOADED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (progress != null) {
|
if (progress != null) {
|
||||||
if (progress.isFinished) {
|
if (progress.isFinished) {
|
||||||
extras.putInt(
|
extras.putInt(
|
||||||
@@ -79,8 +89,9 @@ class LibraryItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val mediaId = localLibraryItemId ?: id
|
||||||
return MediaDescriptionCompat.Builder()
|
return MediaDescriptionCompat.Builder()
|
||||||
.setMediaId(id)
|
.setMediaId(mediaId)
|
||||||
.setTitle(title)
|
.setTitle(title)
|
||||||
.setIconUri(getCoverUri())
|
.setIconUri(getCoverUri())
|
||||||
.setSubtitle(authorName)
|
.setSubtitle(authorName)
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
package com.audiobookshelf.app.data
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.ImageDecoder
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.MediaStore
|
||||||
import android.support.v4.media.MediaDescriptionCompat
|
import android.support.v4.media.MediaDescriptionCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.media.utils.MediaConstants
|
import androidx.media.utils.MediaConstants
|
||||||
@@ -9,6 +14,7 @@ import com.audiobookshelf.app.R
|
|||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
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.audiobookshelf.app.player.PLAYMETHOD_LOCAL
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
@@ -96,10 +102,26 @@ class LocalLibraryItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getMediaDescription(progress:MediaProgressWrapper?): MediaDescriptionCompat {
|
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx:Context?): MediaDescriptionCompat {
|
||||||
val coverUri = getCoverUri()
|
val coverUri = getCoverUri()
|
||||||
|
|
||||||
|
var bitmap:Bitmap? = null
|
||||||
|
if (coverContentUrl != null) {
|
||||||
|
ctx?.let {
|
||||||
|
bitmap = if (Build.VERSION.SDK_INT < 28) {
|
||||||
|
MediaStore.Images.Media.getBitmap(it.contentResolver, coverUri)
|
||||||
|
} else {
|
||||||
|
val source: ImageDecoder.Source = ImageDecoder.createSource(it.contentResolver, coverUri)
|
||||||
|
ImageDecoder.decodeBitmap(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val extras = Bundle()
|
val extras = Bundle()
|
||||||
|
extras.putLong(
|
||||||
|
MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
|
||||||
|
MediaDescriptionCompat.STATUS_DOWNLOADED
|
||||||
|
)
|
||||||
if (progress != null) {
|
if (progress != null) {
|
||||||
if (progress.isFinished) {
|
if (progress.isFinished) {
|
||||||
extras.putInt(
|
extras.putInt(
|
||||||
@@ -122,12 +144,17 @@ class LocalLibraryItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return MediaDescriptionCompat.Builder()
|
val mediaDescriptionBuilder = MediaDescriptionCompat.Builder()
|
||||||
.setMediaId(id)
|
.setMediaId(id)
|
||||||
.setTitle(title)
|
.setTitle(title)
|
||||||
.setIconUri(coverUri)
|
.setIconUri(coverUri)
|
||||||
.setSubtitle(authorName)
|
.setSubtitle(authorName)
|
||||||
.setExtras(extras)
|
.setExtras(extras)
|
||||||
.build()
|
|
||||||
|
bitmap?.let {
|
||||||
|
mediaDescriptionBuilder.setIconBitmap(bitmap)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mediaDescriptionBuilder.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.audiobookshelf.app.data
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -11,7 +12,7 @@ class LocalMediaProgress(
|
|||||||
var localEpisodeId:String?,
|
var localEpisodeId:String?,
|
||||||
var duration:Double,
|
var duration:Double,
|
||||||
progress:Double, // 0 to 1
|
progress:Double, // 0 to 1
|
||||||
var currentTime:Double,
|
currentTime:Double,
|
||||||
isFinished:Boolean,
|
isFinished:Boolean,
|
||||||
var lastUpdate:Long,
|
var lastUpdate:Long,
|
||||||
var startedAt:Long,
|
var startedAt:Long,
|
||||||
@@ -22,9 +23,16 @@ class LocalMediaProgress(
|
|||||||
var serverUserId:String?,
|
var serverUserId:String?,
|
||||||
var libraryItemId:String?,
|
var libraryItemId:String?,
|
||||||
var episodeId:String?
|
var episodeId:String?
|
||||||
) : MediaProgressWrapper(isFinished, progress) {
|
) : MediaProgressWrapper(isFinished, currentTime, progress) {
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val progressPercent get() = if (progress.isNaN()) 0 else (progress * 100).roundToInt()
|
val progressPercent get() = if (progress.isNaN()) 0 else (progress * 100).roundToInt()
|
||||||
|
@get:JsonIgnore
|
||||||
|
override val mediaItemId get() = if (libraryItemId != null) {
|
||||||
|
if (episodeId.isNullOrEmpty()) libraryItemId ?: "" else "$libraryItemId-$episodeId"
|
||||||
|
} else {
|
||||||
|
if (localEpisodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun updateIsFinished(finished:Boolean) {
|
fun updateIsFinished(finished:Boolean) {
|
||||||
@@ -41,8 +49,7 @@ class LocalMediaProgress(
|
|||||||
fun updateFromPlaybackSession(playbackSession:PlaybackSession) {
|
fun updateFromPlaybackSession(playbackSession:PlaybackSession) {
|
||||||
currentTime = playbackSession.currentTime
|
currentTime = playbackSession.currentTime
|
||||||
progress = playbackSession.progress
|
progress = playbackSession.progress
|
||||||
lastUpdate = System.currentTimeMillis()
|
lastUpdate = playbackSession.updatedAt
|
||||||
|
|
||||||
isFinished = playbackSession.progress >= 0.99
|
isFinished = playbackSession.progress >= 0.99
|
||||||
finishedAt = if (isFinished) lastUpdate else null
|
finishedAt = if (isFinished) lastUpdate else null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
class MediaItemEvent(
|
||||||
|
var name:String, // e.g. Play/Pause/Stop/Seek/Save
|
||||||
|
var type:String, // Playback/Info
|
||||||
|
var description:String?,
|
||||||
|
var currentTime:Number?, // Seconds
|
||||||
|
var serverSyncAttempted:Boolean?,
|
||||||
|
var serverSyncSuccess:Boolean?,
|
||||||
|
var serverSyncMessage:String?,
|
||||||
|
var timestamp: Long
|
||||||
|
)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
class MediaItemHistory(
|
||||||
|
var id: String, // media id
|
||||||
|
var mediaDisplayTitle: String,
|
||||||
|
var libraryItemId: String,
|
||||||
|
var episodeId: String?,
|
||||||
|
var isLocal:Boolean,
|
||||||
|
var serverConnectionConfigId:String?,
|
||||||
|
var serverAddress:String?,
|
||||||
|
var serverUserId:String?,
|
||||||
|
var createdAt: Long,
|
||||||
|
var events:MutableList<MediaItemEvent>,
|
||||||
|
)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
class MediaProgress(
|
||||||
|
var id:String,
|
||||||
|
var libraryItemId:String,
|
||||||
|
var episodeId:String?,
|
||||||
|
var duration:Double, // seconds
|
||||||
|
progress:Double, // 0 to 1
|
||||||
|
currentTime:Double,
|
||||||
|
isFinished:Boolean,
|
||||||
|
var lastUpdate:Long,
|
||||||
|
var startedAt:Long,
|
||||||
|
var finishedAt:Long?
|
||||||
|
) : MediaProgressWrapper(isFinished, currentTime, progress) {
|
||||||
|
|
||||||
|
@get:JsonIgnore
|
||||||
|
override val mediaItemId get() = if (episodeId.isNullOrEmpty()) libraryItemId else "$libraryItemId-$episodeId"
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
package com.audiobookshelf.app.data
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.ImageDecoder
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.MediaStore
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
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
|
||||||
@@ -12,12 +15,7 @@ import com.google.android.exoplayer2.MediaMetadata
|
|||||||
import com.google.android.gms.cast.MediaInfo
|
import com.google.android.gms.cast.MediaInfo
|
||||||
import com.google.android.gms.cast.MediaQueueItem
|
import com.google.android.gms.cast.MediaQueueItem
|
||||||
import com.google.android.gms.common.images.WebImage
|
import com.google.android.gms.common.images.WebImage
|
||||||
|
import com.audiobookshelf.app.player.*
|
||||||
// TODO: enum or something in kotlin?
|
|
||||||
val PLAYMETHOD_DIRECTPLAY = 0
|
|
||||||
val PLAYMETHOD_DIRECTSTREAM = 1
|
|
||||||
val PLAYMETHOD_TRANSCODE = 2
|
|
||||||
val PLAYMETHOD_LOCAL = 3
|
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
class PlaybackSession(
|
class PlaybackSession(
|
||||||
@@ -53,17 +51,21 @@ class PlaybackSession(
|
|||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val isLocal get() = playMethod == PLAYMETHOD_LOCAL
|
val isLocal get() = playMethod == PLAYMETHOD_LOCAL
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
|
val isPodcastEpisode get() = mediaType == "podcast"
|
||||||
|
@get:JsonIgnore
|
||||||
val currentTimeMs get() = (currentTime * 1000L).toLong()
|
val currentTimeMs get() = (currentTime * 1000L).toLong()
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val totalDurationMs get() = (getTotalDuration() * 1000L).toLong()
|
val totalDurationMs get() = (getTotalDuration() * 1000L).toLong()
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val localLibraryItemId get() = localLibraryItem?.id ?: ""
|
val localLibraryItemId get() = localLibraryItem?.id ?: ""
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val localMediaProgressId get() = if (episodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
|
val localMediaProgressId get() = if (localEpisodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val progress get() = currentTime / getTotalDuration()
|
val progress get() = currentTime / getTotalDuration()
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val isLocalLibraryItemOnly get() = localLibraryItemId != "" && libraryItemId == null
|
val isLocalLibraryItemOnly get() = localLibraryItemId != "" && libraryItemId == null
|
||||||
|
@get:JsonIgnore
|
||||||
|
val mediaItemId get() = if (isLocalLibraryItemOnly) localMediaProgressId else if (episodeId.isNullOrEmpty()) libraryItemId ?: "" else "$libraryItemId-$episodeId"
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getCurrentTrackIndex():Int {
|
fun getCurrentTrackIndex():Int {
|
||||||
@@ -76,12 +78,41 @@ class PlaybackSession(
|
|||||||
return audioTracks.size - 1
|
return audioTracks.size - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun getNextTrackIndex():Int {
|
||||||
|
for (i in 0 until audioTracks.size) {
|
||||||
|
val track = audioTracks[i]
|
||||||
|
if (currentTimeMs < track.startOffsetMs) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return audioTracks.size - 1
|
||||||
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getChapterForTime(time:Long):BookChapter? {
|
fun getChapterForTime(time:Long):BookChapter? {
|
||||||
if (chapters.isEmpty()) return null
|
if (chapters.isEmpty()) return null
|
||||||
return chapters.find { time >= it.startMs && it.endMs > time}
|
return chapters.find { time >= it.startMs && it.endMs > time}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun getCurrentTrackEndTime():Long {
|
||||||
|
val currentTrack = audioTracks[this.getCurrentTrackIndex()]
|
||||||
|
return currentTrack.startOffsetMs + currentTrack.durationMs
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun getNextChapterForTime(time:Long):BookChapter? {
|
||||||
|
if (chapters.isEmpty()) return null
|
||||||
|
return chapters.find { time < it.startMs } // First chapter where start time is > then time
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun getNextTrackEndTime():Long {
|
||||||
|
val currentTrack = audioTracks[this.getNextTrackIndex()]
|
||||||
|
return currentTrack.startOffsetMs + currentTrack.durationMs
|
||||||
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getCurrentTrackTimeMs():Long {
|
fun getCurrentTrackTimeMs():Long {
|
||||||
val currentTrack = audioTracks[this.getCurrentTrackIndex()]
|
val currentTrack = audioTracks[this.getCurrentTrackIndex()]
|
||||||
@@ -105,9 +136,9 @@ class PlaybackSession(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getCoverUri(): Uri {
|
fun getCoverUri(): Uri {
|
||||||
if (localLibraryItem?.coverContentUrl != null) return Uri.parse(localLibraryItem?.coverContentUrl) ?: Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
if (localLibraryItem?.coverContentUrl != null) return Uri.parse(localLibraryItem?.coverContentUrl) ?: Uri.parse("android.resource://com.audiobookshelf.app/" + com.audiobookshelf.app.R.drawable.icon)
|
||||||
|
|
||||||
if (coverPath == null) return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
if (coverPath == null) return Uri.parse("android.resource://com.audiobookshelf.app/" + com.audiobookshelf.app.R.drawable.icon)
|
||||||
return Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}")
|
return Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,13 +149,33 @@ class PlaybackSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getMediaMetadataCompat(): MediaMetadataCompat {
|
fun getMediaMetadataCompat(ctx: Context): MediaMetadataCompat {
|
||||||
val 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)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, displayAuthor)
|
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, displayAuthor)
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, displayAuthor)
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, displayAuthor)
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, displayAuthor)
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, displayAuthor)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getCoverUri().toString())
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, getCoverUri().toString())
|
||||||
|
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, getCoverUri().toString())
|
||||||
|
|
||||||
|
// Local covers get bitmap
|
||||||
|
if (localLibraryItem?.coverContentUrl != null) {
|
||||||
|
val bitmap = if (Build.VERSION.SDK_INT < 28) {
|
||||||
|
MediaStore.Images.Media.getBitmap(ctx.contentResolver, getCoverUri())
|
||||||
|
} else {
|
||||||
|
val source: ImageDecoder.Source = ImageDecoder.createSource(ctx.contentResolver, getCoverUri())
|
||||||
|
ImageDecoder.decodeBitmap(source)
|
||||||
|
}
|
||||||
|
metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap)
|
||||||
|
metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, bitmap)
|
||||||
|
}
|
||||||
|
|
||||||
return metadataBuilder.build()
|
return metadataBuilder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,6 +187,9 @@ class PlaybackSession(
|
|||||||
.setArtist(displayAuthor)
|
.setArtist(displayAuthor)
|
||||||
.setAlbumArtist(displayAuthor)
|
.setAlbumArtist(displayAuthor)
|
||||||
.setSubtitle(displayAuthor)
|
.setSubtitle(displayAuthor)
|
||||||
|
.setAlbumTitle(displayAuthor)
|
||||||
|
.setDescription(displayAuthor)
|
||||||
|
.setArtworkUri(getCoverUri())
|
||||||
|
|
||||||
val contentUri = this.getContentUri(audioTrack)
|
val contentUri = this.getContentUri(audioTrack)
|
||||||
metadataBuilder.setMediaUri(contentUri)
|
metadataBuilder.setMediaUri(contentUri)
|
||||||
@@ -169,7 +223,9 @@ class PlaybackSession(
|
|||||||
|
|
||||||
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_TITLE, displayTitle ?: "")
|
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_TITLE, displayTitle ?: "")
|
||||||
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_ARTIST, displayAuthor ?: "")
|
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_ARTIST, displayAuthor ?: "")
|
||||||
|
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_ALBUM_TITLE, displayAuthor ?: "")
|
||||||
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_CHAPTER_TITLE, audioTrack.title)
|
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_CHAPTER_TITLE, audioTrack.title)
|
||||||
|
|
||||||
castMetadata.putInt(com.google.android.gms.cast.MediaMetadata.KEY_TRACK_NUMBER, audioTrack.index)
|
castMetadata.putInt(com.google.android.gms.cast.MediaMetadata.KEY_TRACK_NUMBER, audioTrack.index)
|
||||||
return castMetadata
|
return castMetadata
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.audiobookshelf.app.device
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.data.*
|
import com.audiobookshelf.app.data.*
|
||||||
|
import com.audiobookshelf.app.managers.DbManager
|
||||||
import com.audiobookshelf.app.player.PlayerNotificationService
|
import com.audiobookshelf.app.player.PlayerNotificationService
|
||||||
|
|
||||||
interface WidgetEventEmitter {
|
interface WidgetEventEmitter {
|
||||||
@@ -11,8 +12,8 @@ interface WidgetEventEmitter {
|
|||||||
object DeviceManager {
|
object DeviceManager {
|
||||||
const val tag = "DeviceManager"
|
const val tag = "DeviceManager"
|
||||||
|
|
||||||
val dbManager:DbManager = DbManager()
|
val dbManager: DbManager = DbManager()
|
||||||
var deviceData:DeviceData = dbManager.getDeviceData()
|
var deviceData: DeviceData = dbManager.getDeviceData()
|
||||||
var serverConnectionConfig: ServerConnectionConfig? = null
|
var serverConnectionConfig: ServerConnectionConfig? = null
|
||||||
|
|
||||||
val serverConnectionConfigId get() = serverConnectionConfig?.id ?: ""
|
val serverConnectionConfigId get() = serverConnectionConfig?.id ?: ""
|
||||||
@@ -20,12 +21,21 @@ object DeviceManager {
|
|||||||
val serverUserId get() = serverConnectionConfig?.userId ?: ""
|
val serverUserId get() = serverConnectionConfig?.userId ?: ""
|
||||||
val token get() = serverConnectionConfig?.token ?: ""
|
val token get() = serverConnectionConfig?.token ?: ""
|
||||||
val isConnectedToServer get() = serverConnectionConfig != null
|
val isConnectedToServer get() = serverConnectionConfig != null
|
||||||
val hasLastServerConnectionConfig get() = deviceData.getLastServerConnectionConfig() != null
|
|
||||||
|
|
||||||
var widgetUpdater:WidgetEventEmitter? = null
|
var widgetUpdater:WidgetEventEmitter? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Log.d(tag, "Device Manager Singleton invoked")
|
Log.d(tag, "Device Manager Singleton invoked")
|
||||||
|
|
||||||
|
// Initialize new sleep timer settings and shake sensitivity added in v0.9.61
|
||||||
|
if (deviceData.deviceSettings?.autoSleepTimerStartTime == null || deviceData.deviceSettings?.autoSleepTimerEndTime == null) {
|
||||||
|
deviceData.deviceSettings?.autoSleepTimerStartTime = "22:00"
|
||||||
|
deviceData.deviceSettings?.autoSleepTimerStartTime = "06:00"
|
||||||
|
deviceData.deviceSettings?.sleepTimerLength = 900000L
|
||||||
|
}
|
||||||
|
if (deviceData.deviceSettings?.shakeSensitivity == null) {
|
||||||
|
deviceData.deviceSettings?.shakeSensitivity = ShakeSensitivitySetting.MEDIUM
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBase64Id(id:String):String {
|
fun getBase64Id(id:String):String {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import com.arthenica.ffmpegkit.FFmpegKitConfig
|
|||||||
import com.arthenica.ffmpegkit.FFprobeKit
|
import com.arthenica.ffmpegkit.FFprobeKit
|
||||||
import com.arthenica.ffmpegkit.Level
|
import com.arthenica.ffmpegkit.Level
|
||||||
import com.audiobookshelf.app.data.*
|
import com.audiobookshelf.app.data.*
|
||||||
import com.audiobookshelf.app.plugins.AbsDownloader
|
import com.audiobookshelf.app.models.DownloadItem
|
||||||
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.fasterxml.jackson.module.kotlin.readValue
|
||||||
@@ -18,7 +18,7 @@ import org.json.JSONException
|
|||||||
|
|
||||||
class FolderScanner(var ctx: Context) {
|
class FolderScanner(var ctx: Context) {
|
||||||
private val tag = "FolderScanner"
|
private val tag = "FolderScanner"
|
||||||
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||||
|
|
||||||
data class DownloadItemScanResult(val localLibraryItem:LocalLibraryItem, var localMediaProgress:LocalMediaProgress?)
|
data class DownloadItemScanResult(val localLibraryItem:LocalLibraryItem, var localMediaProgress:LocalMediaProgress?)
|
||||||
|
|
||||||
@@ -72,18 +72,18 @@ class FolderScanner(var ctx: Context) {
|
|||||||
Log.d(tag, "Iterating over Folder Found ${itemFolder.name} | ${itemFolder.getSimplePath(ctx)} | URI: ${itemFolder.uri}")
|
Log.d(tag, "Iterating over Folder Found ${itemFolder.name} | ${itemFolder.getSimplePath(ctx)} | URI: ${itemFolder.uri}")
|
||||||
val existingItem = existingLocalLibraryItems.find { emi -> emi.id == getLocalLibraryItemId(itemFolder.id) }
|
val existingItem = existingLocalLibraryItems.find { emi -> emi.id == getLocalLibraryItemId(itemFolder.id) }
|
||||||
|
|
||||||
val result = scanLibraryItemFolder(itemFolder, localFolder, existingItem, forceAudioProbe)
|
when (scanLibraryItemFolder(itemFolder, localFolder, existingItem, forceAudioProbe)) {
|
||||||
|
ItemScanResult.REMOVED -> mediaItemsRemoved++
|
||||||
if (result == ItemScanResult.REMOVED) mediaItemsRemoved++
|
ItemScanResult.UPDATED -> mediaItemsUpdated++
|
||||||
else if (result == ItemScanResult.UPDATED) mediaItemsUpdated++
|
ItemScanResult.ADDED -> mediaItemsAdded++
|
||||||
else if (result == ItemScanResult.ADDED) mediaItemsAdded++
|
else -> mediaItemsUpToDate++
|
||||||
else mediaItemsUpToDate++
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(tag, "Folder $${localFolder.name} scan Results: $mediaItemsAdded Added | $mediaItemsUpdated Updated | $mediaItemsRemoved Removed | $mediaItemsUpToDate Up-to-date")
|
Log.d(tag, "Folder $${localFolder.name} scan Results: $mediaItemsAdded Added | $mediaItemsUpdated Updated | $mediaItemsRemoved Removed | $mediaItemsUpToDate Up-to-date")
|
||||||
|
|
||||||
return if (mediaItemsAdded > 0 || mediaItemsUpdated > 0 || mediaItemsRemoved > 0) {
|
return if (mediaItemsAdded > 0 || mediaItemsUpdated > 0 || mediaItemsRemoved > 0) {
|
||||||
var folderLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id) // Get all local media items
|
val folderLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id) // Get all local media items
|
||||||
FolderScanResult(mediaItemsAdded, mediaItemsUpdated, mediaItemsRemoved, mediaItemsUpToDate, localFolder, folderLibraryItems)
|
FolderScanResult(mediaItemsAdded, mediaItemsUpdated, mediaItemsRemoved, mediaItemsUpToDate, localFolder, folderLibraryItems)
|
||||||
} else {
|
} else {
|
||||||
Log.d(tag, "No Media Items to save")
|
Log.d(tag, "No Media Items to save")
|
||||||
@@ -91,7 +91,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun scanLibraryItemFolder(itemFolder:DocumentFile, localFolder:LocalFolder, existingItem:LocalLibraryItem?, forceAudioProbe:Boolean):ItemScanResult {
|
private fun scanLibraryItemFolder(itemFolder:DocumentFile, localFolder:LocalFolder, existingItem:LocalLibraryItem?, forceAudioProbe:Boolean):ItemScanResult {
|
||||||
val itemFolderName = itemFolder.name ?: ""
|
val itemFolderName = itemFolder.name ?: ""
|
||||||
val itemId = getLocalLibraryItemId(itemFolder.id)
|
val itemId = getLocalLibraryItemId(itemFolder.id)
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
Log.d(tag, "File attributes Id:${localFileId}|ContentUrl:${localFile.contentUrl}|isDownloadsDocument:${file.isDownloadsDocument}")
|
Log.d(tag, "File attributes Id:${localFileId}|ContentUrl:${localFile.contentUrl}|isDownloadsDocument:${file.isDownloadsDocument}")
|
||||||
|
|
||||||
if (isAudio) {
|
if (isAudio) {
|
||||||
var audioTrackToAdd:AudioTrack? = null
|
val audioTrackToAdd:AudioTrack?
|
||||||
|
|
||||||
val existingAudioTrack = existingAudioTracks.find { eat -> eat.localFileId == localFileId }
|
val existingAudioTrack = existingAudioTracks.find { eat -> eat.localFileId == localFileId }
|
||||||
if (existingAudioTrack != null) { // Update existing audio track
|
if (existingAudioTrack != null) { // Update existing audio track
|
||||||
@@ -219,16 +219,18 @@ class FolderScanner(var ctx: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Scan item after download and create local library item
|
// Scan item after download and create local library item
|
||||||
fun scanDownloadItem(downloadItem: AbsDownloader.DownloadItem):DownloadItemScanResult? {
|
fun scanDownloadItem(downloadItem: DownloadItem, cb: (DownloadItemScanResult?) -> Unit) {
|
||||||
val folderDf = DocumentFileCompat.fromUri(ctx, Uri.parse(downloadItem.localFolder.contentUrl))
|
val folderDf = DocumentFileCompat.fromUri(ctx, Uri.parse(downloadItem.localFolder.contentUrl))
|
||||||
val foldersFound = folderDf?.search(false, DocumentFileType.FOLDER) ?: mutableListOf()
|
val foldersFound = folderDf?.search(true, DocumentFileType.FOLDER) ?: mutableListOf()
|
||||||
|
|
||||||
var itemFolderId = ""
|
var itemFolderId = ""
|
||||||
var itemFolderUrl = ""
|
var itemFolderUrl = ""
|
||||||
var itemFolderBasePath = ""
|
var itemFolderBasePath = ""
|
||||||
var itemFolderAbsolutePath = ""
|
var itemFolderAbsolutePath = ""
|
||||||
foldersFound.forEach {
|
foldersFound.forEach {
|
||||||
if (it.name == downloadItem.itemTitle) {
|
// e.g. absolute path is "storage/emulated/0/Audiobooks/Orson Scott Card/Enders Game"
|
||||||
|
// and itemSubfolder is "Orson Scott Card/Enders Game"
|
||||||
|
if (it.getAbsolutePath(ctx).endsWith(downloadItem.itemSubfolder)) {
|
||||||
itemFolderId = it.id
|
itemFolderId = it.id
|
||||||
itemFolderUrl = it.uri.toString()
|
itemFolderUrl = it.uri.toString()
|
||||||
itemFolderBasePath = it.getBasePath(ctx)
|
itemFolderBasePath = it.getBasePath(ctx)
|
||||||
@@ -238,13 +240,13 @@ class FolderScanner(var ctx: Context) {
|
|||||||
|
|
||||||
if (itemFolderUrl == "") {
|
if (itemFolderUrl == "") {
|
||||||
Log.d(tag, "scanDownloadItem failed to find media folder")
|
Log.d(tag, "scanDownloadItem failed to find media folder")
|
||||||
return null
|
return cb(null)
|
||||||
}
|
}
|
||||||
val df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(itemFolderUrl))
|
val df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(itemFolderUrl))
|
||||||
|
|
||||||
if (df == null) {
|
if (df == null) {
|
||||||
Log.e(tag, "Folder Doc File Invalid ${downloadItem.itemFolderPath}")
|
Log.e(tag, "Folder Doc File Invalid ${downloadItem.itemFolderPath}")
|
||||||
return null
|
return cb(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
val localLibraryItemId = getLocalLibraryItemId(itemFolderId)
|
val localLibraryItemId = getLocalLibraryItemId(itemFolderId)
|
||||||
@@ -256,7 +258,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
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
|
||||||
var localLibraryItem:LocalLibraryItem? = null
|
var localLibraryItem:LocalLibraryItem?
|
||||||
if (downloadItem.mediaType == "book") {
|
if (downloadItem.mediaType == "book") {
|
||||||
localLibraryItem = LocalLibraryItem(localLibraryItemId, downloadItem.localFolder.id, itemFolderBasePath, itemFolderAbsolutePath, itemFolderUrl, false, downloadItem.mediaType, downloadItem.media.getLocalCopy(), mutableListOf(), null, null, true, downloadItem.serverConnectionConfigId, downloadItem.serverAddress, downloadItem.serverUserId, downloadItem.libraryItemId)
|
localLibraryItem = LocalLibraryItem(localLibraryItemId, downloadItem.localFolder.id, itemFolderBasePath, itemFolderAbsolutePath, itemFolderUrl, false, downloadItem.mediaType, downloadItem.media.getLocalCopy(), mutableListOf(), null, null, true, downloadItem.serverConnectionConfigId, downloadItem.serverAddress, downloadItem.serverUserId, downloadItem.libraryItemId)
|
||||||
} else {
|
} else {
|
||||||
@@ -268,53 +270,50 @@ class FolderScanner(var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val audioTracks:MutableList<AudioTrack> = mutableListOf()
|
val audioTracks:MutableList<AudioTrack> = mutableListOf()
|
||||||
|
|
||||||
filesFound.forEach { docFile ->
|
filesFound.forEach { docFile ->
|
||||||
val itemPart = downloadItem.downloadItemParts.find { itemPart ->
|
val itemPart = downloadItem.downloadItemParts.find { itemPart ->
|
||||||
itemPart.filename == docFile.name
|
itemPart.filename == docFile.name
|
||||||
}
|
|
||||||
if (itemPart == null) {
|
|
||||||
if (downloadItem.mediaType == "book") { // for books every download item should be a file found
|
|
||||||
Log.e(tag, "scanDownloadItem: Item part not found for doc file ${docFile.name} | ${docFile.getAbsolutePath(ctx)} | ${docFile.uri}")
|
|
||||||
}
|
|
||||||
} else if (itemPart.audioTrack != null) { // Is audio track
|
|
||||||
val audioTrackFromServer = itemPart.audioTrack
|
|
||||||
Log.d(tag, "scanDownloadItem: Audio Track from Server index = ${audioTrackFromServer?.index}")
|
|
||||||
|
|
||||||
val localFileId = DeviceManager.getBase64Id(docFile.id)
|
|
||||||
val localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
|
|
||||||
localLibraryItem.localFiles.add(localFile)
|
|
||||||
|
|
||||||
// TODO: Make asynchronous
|
|
||||||
val audioProbeResult = probeAudioFile(localFile.absolutePath)
|
|
||||||
|
|
||||||
// Create new audio track
|
|
||||||
val track = AudioTrack(audioTrackFromServer.index, audioTrackFromServer.startOffset, audioProbeResult?.duration ?: 0.0, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, audioTrackFromServer.index)
|
|
||||||
audioTracks.add(track)
|
|
||||||
|
|
||||||
Log.d(tag, "scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}")
|
|
||||||
|
|
||||||
// Add podcast episodes to library
|
|
||||||
itemPart.episode?.let { podcastEpisode ->
|
|
||||||
val podcast = localLibraryItem.media as Podcast
|
|
||||||
val newEpisode = podcast.addEpisode(track, podcastEpisode)
|
|
||||||
localEpisodeId = newEpisode.id
|
|
||||||
Log.d(tag, "scanDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}")
|
|
||||||
}
|
|
||||||
} else { // Cover image
|
|
||||||
val localFileId = DeviceManager.getBase64Id(docFile.id)
|
|
||||||
val localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
|
|
||||||
|
|
||||||
localLibraryItem.coverAbsolutePath = localFile.absolutePath
|
|
||||||
localLibraryItem.coverContentUrl = localFile.contentUrl
|
|
||||||
localLibraryItem.localFiles.add(localFile)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (itemPart == null) {
|
||||||
|
if (downloadItem.mediaType == "book") { // for books every download item should be a file found
|
||||||
|
Log.e(tag, "scanDownloadItem: Item part not found for doc file ${docFile.name} | ${docFile.getAbsolutePath(ctx)} | ${docFile.uri}")
|
||||||
|
}
|
||||||
|
} else if (itemPart.audioTrack != null) { // Is audio track
|
||||||
|
val audioTrackFromServer = itemPart.audioTrack
|
||||||
|
Log.d(tag, "scanDownloadItem: Audio Track from Server index = ${audioTrackFromServer.index}")
|
||||||
|
|
||||||
|
val localFileId = DeviceManager.getBase64Id(docFile.id)
|
||||||
|
val localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
|
||||||
|
localLibraryItem.localFiles.add(localFile)
|
||||||
|
|
||||||
|
// Create new audio track
|
||||||
|
val track = AudioTrack(audioTrackFromServer.index, audioTrackFromServer.startOffset, audioTrackFromServer.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, null, audioTrackFromServer.index)
|
||||||
|
audioTracks.add(track)
|
||||||
|
|
||||||
|
Log.d(tag, "scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}")
|
||||||
|
|
||||||
|
// Add podcast episodes to library
|
||||||
|
itemPart.episode?.let { podcastEpisode ->
|
||||||
|
val podcast = localLibraryItem.media as Podcast
|
||||||
|
val newEpisode = podcast.addEpisode(track, podcastEpisode)
|
||||||
|
localEpisodeId = newEpisode.id
|
||||||
|
Log.d(tag, "scanDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}")
|
||||||
|
}
|
||||||
|
} else { // Cover image
|
||||||
|
val localFileId = DeviceManager.getBase64Id(docFile.id)
|
||||||
|
val localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
|
||||||
|
|
||||||
|
localLibraryItem.coverAbsolutePath = localFile.absolutePath
|
||||||
|
localLibraryItem.coverContentUrl = localFile.contentUrl
|
||||||
|
localLibraryItem.localFiles.add(localFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (audioTracks.isEmpty()) {
|
if (audioTracks.isEmpty()) {
|
||||||
Log.d(tag, "scanDownloadItem did not find any audio tracks in folder for ${downloadItem.itemFolderPath}")
|
Log.d(tag, "scanDownloadItem did not find any audio tracks in folder for ${downloadItem.itemFolderPath}")
|
||||||
return null
|
return cb(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
// For books sort audio tracks then set
|
// For books sort audio tracks then set
|
||||||
@@ -364,7 +363,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
|
|
||||||
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
|
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
|
||||||
|
|
||||||
return downloadItemScanResult
|
cb(downloadItemScanResult)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun scanLocalLibraryItem(localLibraryItem:LocalLibraryItem, forceAudioProbe:Boolean):LocalLibraryItemScanResult? {
|
fun scanLocalLibraryItem(localLibraryItem:LocalLibraryItem, forceAudioProbe:Boolean):LocalLibraryItemScanResult? {
|
||||||
@@ -401,7 +400,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
localLibraryItem.localFiles.removeIf { it.id == localFileId }
|
localLibraryItem.localFiles.removeIf { it.id == localFileId }
|
||||||
|
|
||||||
if (existingAudioTracks.find { it.localFileId == localFileId } != null) {
|
if (existingAudioTracks.find { it.localFileId == localFileId } != null) {
|
||||||
Log.d(tag, "scanLocalLibraryItem audio track file ${localFileId} was removed from ${localLibraryItem.absolutePath}")
|
Log.d(tag, "scanLocalLibraryItem audio track file $localFileId was removed from ${localLibraryItem.absolutePath}")
|
||||||
localLibraryItem.media.removeAudioTrack(localFileId)
|
localLibraryItem.media.removeAudioTrack(localFileId)
|
||||||
}
|
}
|
||||||
wasUpdated = true
|
wasUpdated = true
|
||||||
@@ -464,7 +463,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
return LocalLibraryItemScanResult(wasUpdated, localLibraryItem)
|
return LocalLibraryItemScanResult(wasUpdated, localLibraryItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun probeAudioFile(absolutePath:String):AudioProbeResult? {
|
private fun probeAudioFile(absolutePath:String):AudioProbeResult? {
|
||||||
val session = FFprobeKit.execute("-i \"${absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet")
|
val session = FFprobeKit.execute("-i \"${absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet")
|
||||||
|
|
||||||
var probeObject:JSObject? = null
|
var probeObject:JSObject? = null
|
||||||
@@ -475,13 +474,13 @@ class FolderScanner(var ctx: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Log.d(tag, "FFprobe output $probeObject")
|
Log.d(tag, "FFprobe output $probeObject")
|
||||||
if (probeObject == null || !probeObject.has("streams")) { // Check if output is empty
|
return if (probeObject == null || !probeObject.has("streams")) { // Check if output is empty
|
||||||
Log.d(tag, "probeAudioFile Probe audio file $absolutePath failed or invalid")
|
Log.d(tag, "probeAudioFile Probe audio file $absolutePath failed or invalid")
|
||||||
return null
|
null
|
||||||
} else {
|
} else {
|
||||||
val audioProbeResult = jacksonMapper.readValue<AudioProbeResult>(session.output)
|
val audioProbeResult = jacksonMapper.readValue<AudioProbeResult>(session.output)
|
||||||
Log.d(tag, "Probe Result DATA ${audioProbeResult.duration} | ${audioProbeResult.size} | ${audioProbeResult.title} | ${audioProbeResult.artist}")
|
Log.d(tag, "Probe Result DATA ${audioProbeResult.duration} | ${audioProbeResult.size} | ${audioProbeResult.title} | ${audioProbeResult.artist}")
|
||||||
return audioProbeResult
|
audioProbeResult
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
package com.audiobookshelf.app.data
|
package com.audiobookshelf.app.managers
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.plugins.AbsDownloader
|
import com.audiobookshelf.app.data.*
|
||||||
|
import com.audiobookshelf.app.models.DownloadItem
|
||||||
import io.paperdb.Paper
|
import io.paperdb.Paper
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ class DbManager {
|
|||||||
val tag = "DbManager"
|
val tag = "DbManager"
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
var isDbInitialized = false
|
private var isDbInitialized = false
|
||||||
|
|
||||||
fun initialize(ctx: Context) {
|
fun initialize(ctx: Context) {
|
||||||
if (isDbInitialized) return
|
if (isDbInitialized) return
|
||||||
@@ -23,14 +24,14 @@ class DbManager {
|
|||||||
fun getDeviceData(): DeviceData {
|
fun getDeviceData(): DeviceData {
|
||||||
return Paper.book("device").read("data") ?: DeviceData(mutableListOf(), null, null, DeviceSettings.default())
|
return Paper.book("device").read("data") ?: DeviceData(mutableListOf(), null, null, DeviceSettings.default())
|
||||||
}
|
}
|
||||||
fun saveDeviceData(deviceData:DeviceData) {
|
fun saveDeviceData(deviceData: DeviceData) {
|
||||||
Paper.book("device").write("data", deviceData)
|
Paper.book("device").write("data", deviceData)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLocalLibraryItems(mediaType:String? = null):MutableList<LocalLibraryItem> {
|
fun getLocalLibraryItems(mediaType:String? = null):MutableList<LocalLibraryItem> {
|
||||||
val localLibraryItems:MutableList<LocalLibraryItem> = mutableListOf()
|
val localLibraryItems:MutableList<LocalLibraryItem> = mutableListOf()
|
||||||
Paper.book("localLibraryItems").allKeys.forEach {
|
Paper.book("localLibraryItems").allKeys.forEach {
|
||||||
val localLibraryItem:LocalLibraryItem? = Paper.book("localLibraryItems").read(it)
|
val localLibraryItem: LocalLibraryItem? = Paper.book("localLibraryItems").read(it)
|
||||||
if (localLibraryItem != null && (mediaType.isNullOrEmpty() || mediaType == localLibraryItem.mediaType)) {
|
if (localLibraryItem != null && (mediaType.isNullOrEmpty() || mediaType == localLibraryItem.mediaType)) {
|
||||||
localLibraryItems.add(localLibraryItem)
|
localLibraryItems.add(localLibraryItem)
|
||||||
}
|
}
|
||||||
@@ -45,16 +46,16 @@ class DbManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLocalLibraryItemByLId(libraryItemId:String):LocalLibraryItem? {
|
fun getLocalLibraryItemByLId(libraryItemId:String): LocalLibraryItem? {
|
||||||
return getLocalLibraryItems().find { it.libraryItemId == libraryItemId }
|
return getLocalLibraryItems().find { it.libraryItemId == libraryItemId }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLocalLibraryItem(localLibraryItemId:String):LocalLibraryItem? {
|
fun getLocalLibraryItem(localLibraryItemId:String): LocalLibraryItem? {
|
||||||
return Paper.book("localLibraryItems").read(localLibraryItemId)
|
return Paper.book("localLibraryItems").read(localLibraryItemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLocalLibraryItemWithEpisode(podcastEpisodeId:String):LibraryItemWithEpisode? {
|
fun getLocalLibraryItemWithEpisode(podcastEpisodeId:String): LibraryItemWithEpisode? {
|
||||||
var podcastEpisode:PodcastEpisode? = null
|
var podcastEpisode: PodcastEpisode? = null
|
||||||
val localLibraryItem = getLocalLibraryItems("podcast").find { localLibraryItem ->
|
val localLibraryItem = getLocalLibraryItems("podcast").find { localLibraryItem ->
|
||||||
val podcast = localLibraryItem.media as Podcast
|
val podcast = localLibraryItem.media as Podcast
|
||||||
podcastEpisode = podcast.episodes?.find { it.id == podcastEpisodeId }
|
podcastEpisode = podcast.episodes?.find { it.id == podcastEpisodeId }
|
||||||
@@ -77,15 +78,15 @@ class DbManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveLocalLibraryItem(localLibraryItem:LocalLibraryItem) {
|
fun saveLocalLibraryItem(localLibraryItem: LocalLibraryItem) {
|
||||||
Paper.book("localLibraryItems").write(localLibraryItem.id, localLibraryItem)
|
Paper.book("localLibraryItems").write(localLibraryItem.id, localLibraryItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveLocalFolder(localFolder:LocalFolder) {
|
fun saveLocalFolder(localFolder: LocalFolder) {
|
||||||
Paper.book("localFolders").write(localFolder.id,localFolder)
|
Paper.book("localFolders").write(localFolder.id,localFolder)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLocalFolder(folderId:String):LocalFolder? {
|
fun getLocalFolder(folderId:String): LocalFolder? {
|
||||||
return Paper.book("localFolders").read(folderId)
|
return Paper.book("localFolders").read(folderId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +108,7 @@ class DbManager {
|
|||||||
Paper.book("localFolders").delete(folderId)
|
Paper.book("localFolders").delete(folderId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveDownloadItem(downloadItem: AbsDownloader.DownloadItem) {
|
fun saveDownloadItem(downloadItem: DownloadItem) {
|
||||||
Paper.book("downloadItems").write(downloadItem.id, downloadItem)
|
Paper.book("downloadItems").write(downloadItem.id, downloadItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,21 +116,21 @@ class DbManager {
|
|||||||
Paper.book("downloadItems").delete(downloadItemId)
|
Paper.book("downloadItems").delete(downloadItemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDownloadItems():List<AbsDownloader.DownloadItem> {
|
fun getDownloadItems():List<DownloadItem> {
|
||||||
val downloadItems:MutableList<AbsDownloader.DownloadItem> = mutableListOf()
|
val downloadItems:MutableList<DownloadItem> = mutableListOf()
|
||||||
Paper.book("downloadItems").allKeys.forEach { downloadItemId ->
|
Paper.book("downloadItems").allKeys.forEach { downloadItemId ->
|
||||||
Paper.book("downloadItems").read<AbsDownloader.DownloadItem>(downloadItemId)?.let {
|
Paper.book("downloadItems").read<DownloadItem>(downloadItemId)?.let {
|
||||||
downloadItems.add(it)
|
downloadItems.add(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return downloadItems
|
return downloadItems
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveLocalMediaProgress(mediaProgress:LocalMediaProgress) {
|
fun saveLocalMediaProgress(mediaProgress: LocalMediaProgress) {
|
||||||
Paper.book("localMediaProgress").write(mediaProgress.id,mediaProgress)
|
Paper.book("localMediaProgress").write(mediaProgress.id,mediaProgress)
|
||||||
}
|
}
|
||||||
// For books this will just be the localLibraryItemId for podcast episodes this will be "{localLibraryItemId}-{episodeId}"
|
// For books this will just be the localLibraryItemId for podcast episodes this will be "{localLibraryItemId}-{episodeId}"
|
||||||
fun getLocalMediaProgress(localMediaProgressId:String):LocalMediaProgress? {
|
fun getLocalMediaProgress(localMediaProgressId:String): LocalMediaProgress? {
|
||||||
return Paper.book("localMediaProgress").read(localMediaProgressId)
|
return Paper.book("localMediaProgress").read(localMediaProgressId)
|
||||||
}
|
}
|
||||||
fun getAllLocalMediaProgress():List<LocalMediaProgress> {
|
fun getAllLocalMediaProgress():List<LocalMediaProgress> {
|
||||||
@@ -154,7 +155,7 @@ class DbManager {
|
|||||||
val localLibraryItems = getLocalLibraryItems()
|
val localLibraryItems = getLocalLibraryItems()
|
||||||
|
|
||||||
localLibraryItems.forEach { lli ->
|
localLibraryItems.forEach { lli ->
|
||||||
var hasUpates = false
|
var hasUpdates = false
|
||||||
|
|
||||||
// Check local files
|
// Check local files
|
||||||
lli.localFiles = lli.localFiles.filter { localFile ->
|
lli.localFiles = lli.localFiles.filter { localFile ->
|
||||||
@@ -162,7 +163,7 @@ class DbManager {
|
|||||||
val file = File(localFile.absolutePath)
|
val file = File(localFile.absolutePath)
|
||||||
if (!file.exists()) {
|
if (!file.exists()) {
|
||||||
Log.d(tag, "cleanLocalLibraryItems: Local file ${localFile.absolutePath} was removed from library item ${lli.media.metadata.title}")
|
Log.d(tag, "cleanLocalLibraryItems: Local file ${localFile.absolutePath} was removed from library item ${lli.media.metadata.title}")
|
||||||
hasUpates = true
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
file.exists()
|
file.exists()
|
||||||
} as MutableList<LocalFile>
|
} as MutableList<LocalFile>
|
||||||
@@ -173,7 +174,7 @@ class DbManager {
|
|||||||
podcast.episodes = podcast.episodes?.filter { ep ->
|
podcast.episodes = podcast.episodes?.filter { ep ->
|
||||||
if (lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } == null) {
|
if (lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } == null) {
|
||||||
Log.d(tag, "cleanLocalLibraryItems: Podcast episode ${ep.title} was removed from library item ${lli.media.metadata.title}")
|
Log.d(tag, "cleanLocalLibraryItems: Podcast episode ${ep.title} was removed from library item ${lli.media.metadata.title}")
|
||||||
hasUpates = true
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
ep.audioTrack != null && lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } != null
|
ep.audioTrack != null && lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } != null
|
||||||
} as MutableList<PodcastEpisode>
|
} as MutableList<PodcastEpisode>
|
||||||
@@ -182,7 +183,7 @@ class DbManager {
|
|||||||
book.tracks = book.tracks?.filter { track ->
|
book.tracks = book.tracks?.filter { track ->
|
||||||
if (lli.localFiles.find { lf -> lf.id == track.localFileId } == null) {
|
if (lli.localFiles.find { lf -> lf.id == track.localFileId } == null) {
|
||||||
Log.d(tag, "cleanLocalLibraryItems: Audio track ${track.title} was removed from library item ${lli.media.metadata.title}")
|
Log.d(tag, "cleanLocalLibraryItems: Audio track ${track.title} was removed from library item ${lli.media.metadata.title}")
|
||||||
hasUpates = true
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
lli.localFiles.find { lf -> lf.id == track.localFileId } != null
|
lli.localFiles.find { lf -> lf.id == track.localFileId } != null
|
||||||
} as MutableList<AudioTrack>
|
} as MutableList<AudioTrack>
|
||||||
@@ -196,11 +197,11 @@ class DbManager {
|
|||||||
Log.d(tag, "cleanLocalLibraryItems: Cover $it was removed from library item ${lli.media.metadata.title}")
|
Log.d(tag, "cleanLocalLibraryItems: Cover $it was removed from library item ${lli.media.metadata.title}")
|
||||||
lli.coverAbsolutePath = null
|
lli.coverAbsolutePath = null
|
||||||
lli.coverContentUrl = null
|
lli.coverContentUrl = null
|
||||||
hasUpates = true
|
hasUpdates = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasUpates) {
|
if (hasUpdates) {
|
||||||
Log.d(tag, "cleanLocalLibraryItems: Saving local library item ${lli.id}")
|
Log.d(tag, "cleanLocalLibraryItems: Saving local library item ${lli.id}")
|
||||||
Paper.book("localLibraryItems").write(lli.id, lli)
|
Paper.book("localLibraryItems").write(lli.id, lli)
|
||||||
}
|
}
|
||||||
@@ -236,10 +237,18 @@ class DbManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveLocalPlaybackSession(playbackSession:PlaybackSession) {
|
fun saveLocalPlaybackSession(playbackSession: PlaybackSession) {
|
||||||
Paper.book("localPlaybackSession").write(playbackSession.id,playbackSession)
|
Paper.book("localPlaybackSession").write(playbackSession.id,playbackSession)
|
||||||
}
|
}
|
||||||
fun getLocalPlaybackSession(playbackSessionId:String):PlaybackSession? {
|
fun getLocalPlaybackSession(playbackSessionId:String): PlaybackSession? {
|
||||||
return Paper.book("localPlaybackSession").read(playbackSessionId)
|
return Paper.book("localPlaybackSession").read(playbackSessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun saveMediaItemHistory(mediaItemHistory: MediaItemHistory) {
|
||||||
|
Paper.book("mediaItemHistory").write(mediaItemHistory.id,mediaItemHistory)
|
||||||
|
}
|
||||||
|
fun getMediaItemHistory(id:String): MediaItemHistory? {
|
||||||
|
return Paper.book("mediaItemHistory").read(id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
package com.audiobookshelf.app.managers
|
||||||
|
|
||||||
|
import android.app.DownloadManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import com.anggrayudi.storage.callback.FileCallback
|
||||||
|
import com.anggrayudi.storage.file.DocumentFileCompat
|
||||||
|
import com.anggrayudi.storage.file.MimeType
|
||||||
|
import com.anggrayudi.storage.file.getAbsolutePath
|
||||||
|
import com.anggrayudi.storage.file.moveFileTo
|
||||||
|
import com.anggrayudi.storage.media.FileDescription
|
||||||
|
import com.audiobookshelf.app.MainActivity
|
||||||
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
|
import com.audiobookshelf.app.device.FolderScanner
|
||||||
|
import com.audiobookshelf.app.models.DownloadItem
|
||||||
|
import com.audiobookshelf.app.models.DownloadItemPart
|
||||||
|
import com.fasterxml.jackson.core.json.JsonReadFeature
|
||||||
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
|
import com.getcapacitor.JSObject
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class DownloadItemManager(var downloadManager:DownloadManager, private var folderScanner: FolderScanner, var mainActivity: MainActivity, private var clientEventEmitter:DownloadEventEmitter) {
|
||||||
|
val tag = "DownloadItemManager"
|
||||||
|
private val maxSimultaneousDownloads = 1
|
||||||
|
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||||
|
|
||||||
|
enum class DownloadCheckStatus {
|
||||||
|
InProgress,
|
||||||
|
Successful,
|
||||||
|
Failed
|
||||||
|
}
|
||||||
|
|
||||||
|
var downloadItemQueue: MutableList<DownloadItem> = mutableListOf() // All pending and downloading items
|
||||||
|
var currentDownloadItemParts: MutableList<DownloadItemPart> = mutableListOf() // Item parts currently being downloaded
|
||||||
|
|
||||||
|
interface DownloadEventEmitter {
|
||||||
|
fun onDownloadItem(downloadItem:DownloadItem)
|
||||||
|
fun onDownloadItemPartUpdate(downloadItemPart:DownloadItemPart)
|
||||||
|
fun onDownloadItemComplete(jsobj:JSObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var isDownloading:Boolean = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addDownloadItem(downloadItem:DownloadItem) {
|
||||||
|
DeviceManager.dbManager.saveDownloadItem(downloadItem)
|
||||||
|
Log.i(tag, "Add download item ${downloadItem.media.metadata.title}")
|
||||||
|
|
||||||
|
downloadItemQueue.add(downloadItem)
|
||||||
|
clientEventEmitter.onDownloadItem(downloadItem)
|
||||||
|
checkUpdateDownloadQueue()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkUpdateDownloadQueue() {
|
||||||
|
for (downloadItem in downloadItemQueue) {
|
||||||
|
val numPartsToGet = maxSimultaneousDownloads - currentDownloadItemParts.size
|
||||||
|
val nextDownloadItemParts = downloadItem.getNextDownloadItemParts(numPartsToGet)
|
||||||
|
Log.d(tag, "checkUpdateDownloadQueue: numPartsToGet=$numPartsToGet, nextDownloadItemParts=${nextDownloadItemParts.size}")
|
||||||
|
|
||||||
|
if (nextDownloadItemParts.size > 0) {
|
||||||
|
nextDownloadItemParts.forEach {
|
||||||
|
val dlRequest = it.getDownloadRequest()
|
||||||
|
val downloadId = downloadManager.enqueue(dlRequest)
|
||||||
|
it.downloadId = downloadId
|
||||||
|
Log.d(tag, "checkUpdateDownloadQueue: Starting download item part, downloadId=$downloadId")
|
||||||
|
currentDownloadItemParts.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentDownloadItemParts.size >= maxSimultaneousDownloads) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentDownloadItemParts.size > 0) startWatchingDownloads()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startWatchingDownloads() {
|
||||||
|
if (isDownloading) return // Already watching
|
||||||
|
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
Log.d(tag, "Starting watching downloads")
|
||||||
|
isDownloading = true
|
||||||
|
|
||||||
|
while (currentDownloadItemParts.size > 0) {
|
||||||
|
|
||||||
|
val itemParts = currentDownloadItemParts.filter { !it.isMoving }.map { it }
|
||||||
|
for (downloadItemPart in itemParts) {
|
||||||
|
val downloadCheckStatus = checkDownloadItemPart(downloadItemPart)
|
||||||
|
clientEventEmitter.onDownloadItemPartUpdate(downloadItemPart)
|
||||||
|
|
||||||
|
// Will move to final destination, remove current item parts, and check if download item is finished
|
||||||
|
handleDownloadItemPartCheck(downloadCheckStatus, downloadItemPart)
|
||||||
|
}
|
||||||
|
|
||||||
|
delay(500)
|
||||||
|
|
||||||
|
if (currentDownloadItemParts.size < maxSimultaneousDownloads) {
|
||||||
|
checkUpdateDownloadQueue()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(tag, "Finished watching downloads")
|
||||||
|
isDownloading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkDownloadItemPart(downloadItemPart:DownloadItemPart):DownloadCheckStatus {
|
||||||
|
val downloadId = downloadItemPart.downloadId ?: return DownloadCheckStatus.Failed
|
||||||
|
|
||||||
|
val query = DownloadManager.Query().setFilterById(downloadId)
|
||||||
|
downloadManager.query(query).use {
|
||||||
|
if (it.moveToFirst()) {
|
||||||
|
val bytesColumnIndex = it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
|
||||||
|
val statusColumnIndex = it.getColumnIndex(DownloadManager.COLUMN_STATUS)
|
||||||
|
val bytesDownloadedColumnIndex = it.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
|
||||||
|
|
||||||
|
val totalBytes = if (bytesColumnIndex >= 0) it.getInt(bytesColumnIndex) else 0
|
||||||
|
val downloadStatus = if (statusColumnIndex >= 0) it.getInt(statusColumnIndex) else 0
|
||||||
|
val bytesDownloadedSoFar = if (bytesDownloadedColumnIndex >= 0) it.getLong(bytesDownloadedColumnIndex) else 0
|
||||||
|
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} bytes $totalBytes | bytes dled $bytesDownloadedSoFar | downloadStatus $downloadStatus")
|
||||||
|
|
||||||
|
return when (downloadStatus) {
|
||||||
|
DownloadManager.STATUS_SUCCESSFUL -> {
|
||||||
|
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Successful")
|
||||||
|
downloadItemPart.completed = true
|
||||||
|
downloadItemPart.progress = 1
|
||||||
|
downloadItemPart.bytesDownloaded = bytesDownloadedSoFar
|
||||||
|
|
||||||
|
DownloadCheckStatus.Successful
|
||||||
|
}
|
||||||
|
DownloadManager.STATUS_FAILED -> {
|
||||||
|
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Failed")
|
||||||
|
downloadItemPart.completed = true
|
||||||
|
downloadItemPart.failed = true
|
||||||
|
|
||||||
|
DownloadCheckStatus.Failed
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val percentProgress = if (totalBytes > 0) ((bytesDownloadedSoFar * 100L) / totalBytes) else 0
|
||||||
|
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Progress = $percentProgress%")
|
||||||
|
downloadItemPart.progress = percentProgress
|
||||||
|
downloadItemPart.bytesDownloaded = bytesDownloadedSoFar
|
||||||
|
|
||||||
|
DownloadCheckStatus.InProgress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(tag, "Download ${downloadItemPart.filename} not found in dlmanager")
|
||||||
|
downloadItemPart.completed = true
|
||||||
|
downloadItemPart.failed = true
|
||||||
|
return DownloadCheckStatus.Failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun handleDownloadItemPartCheck(downloadCheckStatus:DownloadCheckStatus, downloadItemPart:DownloadItemPart) {
|
||||||
|
val downloadItem = downloadItemQueue.find { it.id == downloadItemPart.downloadItemId }
|
||||||
|
if (downloadItem == null) {
|
||||||
|
Log.e(tag, "Download item part finished but download item not found ${downloadItemPart.filename}")
|
||||||
|
currentDownloadItemParts.remove(downloadItemPart)
|
||||||
|
} else if (downloadCheckStatus == DownloadCheckStatus.Successful) {
|
||||||
|
|
||||||
|
val file = DocumentFileCompat.fromUri(mainActivity, downloadItemPart.destinationUri)
|
||||||
|
Log.d(tag, "DOWNLOAD: DESTINATION URI ${downloadItemPart.destinationUri}")
|
||||||
|
|
||||||
|
val fcb = object : FileCallback() {
|
||||||
|
override fun onPrepare() {
|
||||||
|
Log.d(tag, "DOWNLOAD: PREPARING MOVE FILE")
|
||||||
|
}
|
||||||
|
override fun onFailed(errorCode: ErrorCode) {
|
||||||
|
Log.e(tag, "DOWNLOAD: FAILED TO MOVE FILE $errorCode")
|
||||||
|
downloadItemPart.failed = true
|
||||||
|
downloadItemPart.isMoving = false
|
||||||
|
file?.delete()
|
||||||
|
checkDownloadItemFinished(downloadItem)
|
||||||
|
currentDownloadItemParts.remove(downloadItemPart)
|
||||||
|
}
|
||||||
|
override fun onCompleted(result:Any) {
|
||||||
|
Log.d(tag, "DOWNLOAD: FILE MOVE COMPLETED")
|
||||||
|
val resultDocFile = result as DocumentFile
|
||||||
|
Log.d(tag, "DOWNLOAD: COMPLETED FILE INFO (name=${resultDocFile.name}) ${resultDocFile.getAbsolutePath(mainActivity)}")
|
||||||
|
|
||||||
|
// Rename to fix appended .mp3 on m4b/m4a files
|
||||||
|
// REF: https://github.com/anggrayudi/SimpleStorage/issues/94
|
||||||
|
if (resultDocFile.name?.endsWith(".m4b.mp3") == true || resultDocFile.name?.endsWith(".m4a.mp3") == true) {
|
||||||
|
resultDocFile.renameTo(downloadItemPart.filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadItemPart.moved = true
|
||||||
|
downloadItemPart.isMoving = false
|
||||||
|
checkDownloadItemFinished(downloadItem)
|
||||||
|
currentDownloadItemParts.remove(downloadItemPart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val localFolderFile = DocumentFileCompat.fromUri(mainActivity, Uri.parse(downloadItemPart.localFolderUrl))
|
||||||
|
if (localFolderFile == null) {
|
||||||
|
// fAILED
|
||||||
|
downloadItemPart.failed = true
|
||||||
|
Log.e(tag, "Local Folder File from uri is null")
|
||||||
|
checkDownloadItemFinished(downloadItem)
|
||||||
|
currentDownloadItemParts.remove(downloadItemPart)
|
||||||
|
} else {
|
||||||
|
downloadItemPart.isMoving = true
|
||||||
|
val mimetype = if (downloadItemPart.audioTrack != null) MimeType.AUDIO else MimeType.IMAGE
|
||||||
|
val fileDescription = FileDescription(downloadItemPart.filename, downloadItemPart.finalDestinationSubfolder, mimetype)
|
||||||
|
file?.moveFileTo(mainActivity, localFolderFile, fileDescription, fcb)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (downloadCheckStatus != DownloadCheckStatus.InProgress) {
|
||||||
|
checkDownloadItemFinished(downloadItem)
|
||||||
|
currentDownloadItemParts.remove(downloadItemPart)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkDownloadItemFinished(downloadItem:DownloadItem) {
|
||||||
|
if (downloadItem.isDownloadFinished) {
|
||||||
|
Log.i(tag, "Download Item finished ${downloadItem.media.metadata.title}")
|
||||||
|
|
||||||
|
folderScanner.scanDownloadItem(downloadItem) { downloadItemScanResult ->
|
||||||
|
Log.d(tag, "Item download complete ${downloadItem.itemTitle} | local library item id: ${downloadItemScanResult?.localLibraryItem?.id}")
|
||||||
|
|
||||||
|
val jsobj = JSObject()
|
||||||
|
jsobj.put("libraryItemId", downloadItem.id)
|
||||||
|
jsobj.put("localFolderId", downloadItem.localFolder.id)
|
||||||
|
|
||||||
|
downloadItemScanResult?.localLibraryItem?.let { localLibraryItem ->
|
||||||
|
jsobj.put("localLibraryItem", JSObject(jacksonMapper.writeValueAsString(localLibraryItem)))
|
||||||
|
}
|
||||||
|
downloadItemScanResult?.localMediaProgress?.let { localMediaProgress ->
|
||||||
|
jsobj.put("localMediaProgress", JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
|
||||||
|
}
|
||||||
|
|
||||||
|
clientEventEmitter.onDownloadItemComplete(jsobj)
|
||||||
|
downloadItemQueue.remove(downloadItem)
|
||||||
|
DeviceManager.dbManager.removeDownloadItem(downloadItem.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
package com.audiobookshelf.app.player
|
package com.audiobookshelf.app.managers
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
|
import com.audiobookshelf.app.player.PlayerNotificationService
|
||||||
|
import com.audiobookshelf.app.player.SLEEP_TIMER_WAKE_UP_EXPIRATION
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
const val SLEEP_EXTENSION_TIME = 900000L // 15m
|
class SleepTimerManager constructor(private val playerNotificationService: PlayerNotificationService) {
|
||||||
|
|
||||||
class SleepTimerManager constructor(val playerNotificationService:PlayerNotificationService) {
|
|
||||||
private val tag = "SleepTimerManager"
|
private val tag = "SleepTimerManager"
|
||||||
|
|
||||||
private var sleepTimerTask:TimerTask? = null
|
private var sleepTimerTask:TimerTask? = null
|
||||||
@@ -18,7 +19,6 @@ class SleepTimerManager constructor(val playerNotificationService:PlayerNotifica
|
|||||||
private var sleepTimerEndTime:Long = 0L
|
private var sleepTimerEndTime:Long = 0L
|
||||||
private var sleepTimerLength:Long = 0L
|
private var sleepTimerLength:Long = 0L
|
||||||
private var sleepTimerElapsed:Long = 0L
|
private var sleepTimerElapsed:Long = 0L
|
||||||
private var sleepTimerExtensionTime:Long = 0L
|
|
||||||
private var sleepTimerFinishedAt:Long = 0L
|
private var sleepTimerFinishedAt:Long = 0L
|
||||||
|
|
||||||
private fun getCurrentTime():Long {
|
private fun getCurrentTime():Long {
|
||||||
@@ -57,7 +57,7 @@ class SleepTimerManager constructor(val playerNotificationService:PlayerNotifica
|
|||||||
fun setSleepTimer(time: Long, isChapterTime: Boolean) : Boolean {
|
fun setSleepTimer(time: Long, isChapterTime: Boolean) : Boolean {
|
||||||
Log.d(tag, "Setting Sleep Timer for $time is chapter time $isChapterTime")
|
Log.d(tag, "Setting Sleep Timer for $time is chapter time $isChapterTime")
|
||||||
sleepTimerTask?.cancel()
|
sleepTimerTask?.cancel()
|
||||||
sleepTimerRunning = false
|
sleepTimerRunning = true
|
||||||
sleepTimerFinishedAt = 0L
|
sleepTimerFinishedAt = 0L
|
||||||
sleepTimerElapsed = 0L
|
sleepTimerElapsed = 0L
|
||||||
|
|
||||||
@@ -72,7 +72,6 @@ class SleepTimerManager constructor(val playerNotificationService:PlayerNotifica
|
|||||||
}
|
}
|
||||||
sleepTimerEndTime = time
|
sleepTimerEndTime = time
|
||||||
sleepTimerLength = 0
|
sleepTimerLength = 0
|
||||||
sleepTimerExtensionTime = SLEEP_EXTENSION_TIME
|
|
||||||
|
|
||||||
if (sleepTimerEndTime > getDuration()) {
|
if (sleepTimerEndTime > getDuration()) {
|
||||||
sleepTimerEndTime = getDuration()
|
sleepTimerEndTime = getDuration()
|
||||||
@@ -80,7 +79,6 @@ class SleepTimerManager constructor(val playerNotificationService:PlayerNotifica
|
|||||||
} else {
|
} else {
|
||||||
sleepTimerLength = time
|
sleepTimerLength = time
|
||||||
sleepTimerEndTime = 0L
|
sleepTimerEndTime = 0L
|
||||||
sleepTimerExtensionTime = time
|
|
||||||
|
|
||||||
if (sleepTimerLength + getCurrentTime() > getDuration()) {
|
if (sleepTimerLength + getCurrentTime() > getDuration()) {
|
||||||
sleepTimerLength = getDuration() - getCurrentTime()
|
sleepTimerLength = getDuration() - getCurrentTime()
|
||||||
@@ -89,9 +87,8 @@ class SleepTimerManager constructor(val playerNotificationService:PlayerNotifica
|
|||||||
|
|
||||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
||||||
|
|
||||||
sleepTimerRunning = true
|
|
||||||
sleepTimerTask = Timer("SleepTimer", false).schedule(0L, 1000L) {
|
sleepTimerTask = Timer("SleepTimer", false).schedule(0L, 1000L) {
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post {
|
||||||
if (getIsPlaying()) {
|
if (getIsPlaying()) {
|
||||||
sleepTimerElapsed += 1000L
|
sleepTimerElapsed += 1000L
|
||||||
|
|
||||||
@@ -109,9 +106,9 @@ class SleepTimerManager constructor(val playerNotificationService:PlayerNotifica
|
|||||||
playerNotificationService.clientEventEmitter?.onSleepTimerEnded(getCurrentTime())
|
playerNotificationService.clientEventEmitter?.onSleepTimerEnded(getCurrentTime())
|
||||||
clearSleepTimer()
|
clearSleepTimer()
|
||||||
sleepTimerFinishedAt = System.currentTimeMillis()
|
sleepTimerFinishedAt = System.currentTimeMillis()
|
||||||
} else if (sleepTimeSecondsRemaining <= 30) {
|
} else if (sleepTimeSecondsRemaining <= 60) {
|
||||||
// Start fading out audio
|
// Start fading out audio
|
||||||
val volume = sleepTimeSecondsRemaining / 30F
|
val volume = sleepTimeSecondsRemaining / 60F
|
||||||
Log.d(tag, "SLEEP VOLUME FADE $volume | ${sleepTimeSecondsRemaining}s remaining")
|
Log.d(tag, "SLEEP VOLUME FADE $volume | ${sleepTimeSecondsRemaining}s remaining")
|
||||||
setVolume(volume)
|
setVolume(volume)
|
||||||
}
|
}
|
||||||
@@ -121,7 +118,7 @@ class SleepTimerManager constructor(val playerNotificationService:PlayerNotifica
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearSleepTimer() {
|
private fun clearSleepTimer() {
|
||||||
sleepTimerTask?.cancel()
|
sleepTimerTask?.cancel()
|
||||||
sleepTimerTask = null
|
sleepTimerTask = null
|
||||||
sleepTimerEndTime = 0
|
sleepTimerEndTime = 0
|
||||||
@@ -163,21 +160,40 @@ class SleepTimerManager constructor(val playerNotificationService:PlayerNotifica
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun extendSleepTime() {
|
// Get the chapter end time for use in End of Chapter timers
|
||||||
if (!sleepTimerRunning) return
|
// if less than 2s remain in chapter then use the next chapter
|
||||||
setVolume(1F)
|
private fun getChapterEndTime():Long? {
|
||||||
if (sleepTimerEndTime == 0L) {
|
val currentChapterEndTimeMs = playerNotificationService.getEndTimeOfChapterOrTrack()
|
||||||
sleepTimerLength += sleepTimerExtensionTime
|
if (currentChapterEndTimeMs == null) {
|
||||||
if (sleepTimerLength + getCurrentTime() > getDuration()) sleepTimerLength = getDuration() - getCurrentTime()
|
Log.e(tag, "Getting chapter sleep timer end of chapter/track but there is no current session")
|
||||||
} else {
|
return null
|
||||||
sleepTimerEndTime += sleepTimerExtensionTime
|
|
||||||
if (sleepTimerEndTime > getDuration()) sleepTimerEndTime = getDuration()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
val timeLeftInChapter = currentChapterEndTimeMs - getCurrentTime()
|
||||||
|
return if (timeLeftInChapter < 2000L) {
|
||||||
|
Log.i(tag, "Getting chapter sleep timer time and current chapter has less than 2s remaining")
|
||||||
|
val nextChapterEndTimeMs = playerNotificationService.getEndTimeOfNextChapterOrTrack()
|
||||||
|
if (nextChapterEndTimeMs == null || currentChapterEndTimeMs == nextChapterEndTimeMs) {
|
||||||
|
Log.e(tag, "Invalid next chapter time. No current session or equal to current chapter. $nextChapterEndTimeMs")
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
nextChapterEndTimeMs
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentChapterEndTimeMs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkShouldExtendSleepTimer() {
|
private fun resetChapterTimer() {
|
||||||
|
this.getChapterEndTime()?.let { chapterEndTime ->
|
||||||
|
Log.d(tag, "Resetting stopped sleep timer to end of chapter $chapterEndTime")
|
||||||
|
vibrate()
|
||||||
|
setSleepTimer(chapterEndTime, true)
|
||||||
|
play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkShouldResetSleepTimer() {
|
||||||
if (!sleepTimerRunning) {
|
if (!sleepTimerRunning) {
|
||||||
if (sleepTimerFinishedAt <= 0L) return
|
if (sleepTimerFinishedAt <= 0L) return
|
||||||
|
|
||||||
@@ -189,17 +205,35 @@ class SleepTimerManager constructor(val playerNotificationService:PlayerNotifica
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val newSleepTime = if (sleepTimerExtensionTime >= 0) sleepTimerExtensionTime else SLEEP_EXTENSION_TIME
|
// Set sleep timer
|
||||||
vibrate()
|
// When sleepTimerLength is 0 then use end of chapter/track time
|
||||||
setSleepTimer(newSleepTime, false)
|
if (sleepTimerLength == 0L) {
|
||||||
play()
|
Log.d(tag, "Resetting stopped chapter sleep timer")
|
||||||
|
resetChapterTimer()
|
||||||
|
} else {
|
||||||
|
Log.d(tag, "Resetting stopped sleep timer to length $sleepTimerLength")
|
||||||
|
vibrate()
|
||||||
|
setSleepTimer(sleepTimerLength, false)
|
||||||
|
play()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Only extend if within 30 seconds of finishing
|
|
||||||
val sleepTimeRemaining = getSleepTimerTimeRemainingSeconds()
|
// Does not apply to chapter sleep timers and timer must be running for at least 3 seconds
|
||||||
if (sleepTimeRemaining <= 30) {
|
if (sleepTimerLength > 0L && sleepTimerElapsed > 3000L) {
|
||||||
|
Log.d(tag, "Resetting running sleep timer to length $sleepTimerLength")
|
||||||
vibrate()
|
vibrate()
|
||||||
extendSleepTime()
|
setSleepTimer(sleepTimerLength, false)
|
||||||
|
} else if (sleepTimerLength == 0L) {
|
||||||
|
// When navigating to previous chapters make sure this is still the end of the current chapter
|
||||||
|
this.getChapterEndTime()?.let { chapterEndTime ->
|
||||||
|
if (chapterEndTime != sleepTimerEndTime) {
|
||||||
|
Log.d(tag, "Resetting chapter sleep timer to end of chapter $chapterEndTime from $sleepTimerEndTime")
|
||||||
|
vibrate()
|
||||||
|
setSleepTimer(chapterEndTime, true)
|
||||||
|
play()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +243,7 @@ class SleepTimerManager constructor(val playerNotificationService:PlayerNotifica
|
|||||||
Log.d(tag, "Shake to reset sleep timer is disabled")
|
Log.d(tag, "Shake to reset sleep timer is disabled")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
checkShouldExtendSleepTimer()
|
checkShouldResetSleepTimer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,4 +288,53 @@ class SleepTimerManager constructor(val playerNotificationService:PlayerNotifica
|
|||||||
setVolume(1F)
|
setVolume(1F)
|
||||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun checkAutoSleepTimer() {
|
||||||
|
if (sleepTimerRunning) { // Sleep timer already running
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DeviceManager.deviceData.deviceSettings?.let { deviceSettings ->
|
||||||
|
if (!deviceSettings.autoSleepTimer) return // Check auto sleep timer is enabled
|
||||||
|
|
||||||
|
val startCalendar = Calendar.getInstance()
|
||||||
|
startCalendar.set(Calendar.HOUR_OF_DAY, deviceSettings.autoSleepTimerStartHour)
|
||||||
|
startCalendar.set(Calendar.MINUTE, deviceSettings.autoSleepTimerStartMinute)
|
||||||
|
val endCalendar = Calendar.getInstance()
|
||||||
|
endCalendar.set(Calendar.HOUR_OF_DAY, deviceSettings.autoSleepTimerEndHour)
|
||||||
|
endCalendar.set(Calendar.MINUTE, deviceSettings.autoSleepTimerEndMinute)
|
||||||
|
|
||||||
|
// In cases where end time is earlier then start time then we add a day to end time
|
||||||
|
// e.g. start time 22:00 and end time 06:00. End time will be treated as 6am the next day.
|
||||||
|
// e.g. start time 08:00 and end time 22:00. Start and end time will be the same day.
|
||||||
|
if (endCalendar.before(startCalendar)) {
|
||||||
|
endCalendar.add(Calendar.DAY_OF_MONTH, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentCalendar = Calendar.getInstance()
|
||||||
|
val currentHour = SimpleDateFormat("HH:mm", Locale.getDefault()).format(currentCalendar.time)
|
||||||
|
if (currentCalendar.after(startCalendar) && currentCalendar.before(endCalendar)) {
|
||||||
|
Log.i(tag, "Current hour $currentHour is between ${deviceSettings.autoSleepTimerStartTime} and ${deviceSettings.autoSleepTimerEndTime} - starting sleep timer")
|
||||||
|
|
||||||
|
// Set sleep timer
|
||||||
|
// When sleepTimerLength is 0 then use end of chapter/track time
|
||||||
|
if (deviceSettings.sleepTimerLength == 0L) {
|
||||||
|
val chapterEndTimeMs = this.getChapterEndTime()
|
||||||
|
if (chapterEndTimeMs == null) {
|
||||||
|
Log.e(tag, "Setting auto sleep timer to end of chapter/track but there is no current session")
|
||||||
|
} else {
|
||||||
|
setSleepTimer(chapterEndTimeMs, true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setSleepTimer(deviceSettings.sleepTimerLength, false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(tag, "Current hour $currentHour is NOT between ${deviceSettings.autoSleepTimerStartTime} and ${deviceSettings.autoSleepTimerEndTime}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleMediaPlayEvent() {
|
||||||
|
checkShouldResetSleepTimer()
|
||||||
|
checkAutoSleepTimer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package com.audiobookshelf.app.media
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.audiobookshelf.app.data.*
|
||||||
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
|
import com.audiobookshelf.app.player.PlayerNotificationService
|
||||||
|
import com.audiobookshelf.app.player.SyncResult
|
||||||
|
|
||||||
|
object MediaEventManager {
|
||||||
|
const val tag = "MediaEventManager"
|
||||||
|
|
||||||
|
var clientEventEmitter: PlayerNotificationService.ClientEventEmitter? = null
|
||||||
|
|
||||||
|
fun playEvent(playbackSession: PlaybackSession) {
|
||||||
|
Log.i(tag, "Play Event for media \"${playbackSession.displayTitle}\"")
|
||||||
|
addPlaybackEvent("Play", playbackSession, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pauseEvent(playbackSession: PlaybackSession, syncResult: SyncResult?) {
|
||||||
|
Log.i(tag, "Pause Event for media \"${playbackSession.displayTitle}\"")
|
||||||
|
addPlaybackEvent("Pause", playbackSession, syncResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopEvent(playbackSession: PlaybackSession, syncResult: SyncResult?) {
|
||||||
|
Log.i(tag, "Stop Event for media \"${playbackSession.displayTitle}\"")
|
||||||
|
addPlaybackEvent("Stop", playbackSession, syncResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveEvent(playbackSession: PlaybackSession, syncResult: SyncResult?) {
|
||||||
|
Log.i(tag, "Save Event for media \"${playbackSession.displayTitle}\"")
|
||||||
|
addPlaybackEvent("Save", playbackSession, syncResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun finishedEvent(playbackSession: PlaybackSession, syncResult: SyncResult?) {
|
||||||
|
Log.i(tag, "Finished Event for media \"${playbackSession.displayTitle}\"")
|
||||||
|
addPlaybackEvent("Finished", playbackSession, syncResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun seekEvent(playbackSession: PlaybackSession, syncResult: SyncResult?) {
|
||||||
|
Log.i(tag, "Seek Event for media \"${playbackSession.displayTitle}\", currentTime=${playbackSession.currentTime}")
|
||||||
|
addPlaybackEvent("Seek", playbackSession, syncResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun syncEvent(mediaProgress: MediaProgressWrapper, description: String) {
|
||||||
|
Log.i(tag, "Sync Event for media item id \"${mediaProgress.mediaItemId}\", currentTime=${mediaProgress.currentTime}")
|
||||||
|
addSyncEvent("Sync", mediaProgress, description)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addSyncEvent(eventName:String, mediaProgress:MediaProgressWrapper, description: String) {
|
||||||
|
val mediaItemHistory = getMediaItemHistoryMediaItem(mediaProgress.mediaItemId)
|
||||||
|
if (mediaItemHistory == null) {
|
||||||
|
Log.w(tag, "addSyncEvent: Media Item History not created yet for media item id ${mediaProgress.mediaItemId}")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val mediaItemEvent = MediaItemEvent(
|
||||||
|
name = eventName,
|
||||||
|
type = "Sync",
|
||||||
|
description = description,
|
||||||
|
currentTime = mediaProgress.currentTime,
|
||||||
|
serverSyncAttempted = false,
|
||||||
|
serverSyncSuccess = null,
|
||||||
|
serverSyncMessage = null,
|
||||||
|
timestamp = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
mediaItemHistory.events.add(mediaItemEvent)
|
||||||
|
DeviceManager.dbManager.saveMediaItemHistory(mediaItemHistory)
|
||||||
|
|
||||||
|
clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addPlaybackEvent(eventName:String, playbackSession:PlaybackSession, syncResult:SyncResult?) {
|
||||||
|
val mediaItemHistory = getMediaItemHistoryMediaItem(playbackSession.mediaItemId) ?: createMediaItemHistoryForSession(playbackSession)
|
||||||
|
|
||||||
|
val mediaItemEvent = MediaItemEvent(
|
||||||
|
name = eventName,
|
||||||
|
type = "Playback",
|
||||||
|
description = "",
|
||||||
|
currentTime = playbackSession.currentTime,
|
||||||
|
serverSyncAttempted = syncResult?.serverSyncAttempted ?: false,
|
||||||
|
serverSyncSuccess = syncResult?.serverSyncSuccess,
|
||||||
|
serverSyncMessage = syncResult?.serverSyncMessage,
|
||||||
|
timestamp = System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
mediaItemHistory.events.add(mediaItemEvent)
|
||||||
|
DeviceManager.dbManager.saveMediaItemHistory(mediaItemHistory)
|
||||||
|
|
||||||
|
clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getMediaItemHistoryMediaItem(mediaItemId: String) : MediaItemHistory? {
|
||||||
|
return DeviceManager.dbManager.getMediaItemHistory(mediaItemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createMediaItemHistoryForSession(playbackSession: PlaybackSession):MediaItemHistory {
|
||||||
|
Log.i(tag, "Creating new media item history for media \"${playbackSession.displayTitle}\"")
|
||||||
|
val isLocalOnly = playbackSession.isLocalLibraryItemOnly
|
||||||
|
val libraryItemId = if (isLocalOnly) playbackSession.localLibraryItemId else playbackSession.libraryItemId ?: ""
|
||||||
|
val episodeId:String? = if (isLocalOnly && playbackSession.localEpisodeId != null) playbackSession.localEpisodeId else playbackSession.episodeId
|
||||||
|
return MediaItemHistory(
|
||||||
|
id = playbackSession.mediaItemId,
|
||||||
|
mediaDisplayTitle = playbackSession.displayTitle ?: "Unset",
|
||||||
|
libraryItemId,
|
||||||
|
episodeId,
|
||||||
|
isLocalOnly,
|
||||||
|
playbackSession.
|
||||||
|
serverConnectionConfigId,
|
||||||
|
playbackSession.serverAddress,
|
||||||
|
playbackSession.userId,
|
||||||
|
createdAt = System.currentTimeMillis(),
|
||||||
|
events = mutableListOf())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,24 +15,24 @@ import org.json.JSONException
|
|||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
||||||
val tag = "MediaManager"
|
val tag = "MediaManager"
|
||||||
|
|
||||||
var serverLibraryItems = mutableListOf<LibraryItem>() // Store all items here
|
private var serverLibraryItems = mutableListOf<LibraryItem>() // Store all items here
|
||||||
var selectedLibraryItems = mutableListOf<LibraryItem>()
|
private var selectedLibraryItems = mutableListOf<LibraryItem>()
|
||||||
var selectedLibraryId = ""
|
private var selectedLibraryId = ""
|
||||||
|
|
||||||
var selectedPodcast:Podcast? = null
|
private var selectedPodcast:Podcast? = null
|
||||||
var selectedLibraryItemId:String? = null
|
private var selectedLibraryItemId:String? = null
|
||||||
var podcastEpisodeLibraryItemMap = mutableMapOf<String, LibraryItemWithEpisode>()
|
private var podcastEpisodeLibraryItemMap = mutableMapOf<String, LibraryItemWithEpisode>()
|
||||||
var serverLibraryCategories = listOf<LibraryCategory>()
|
private var serverLibraryCategories = listOf<LibraryCategory>()
|
||||||
|
private var serverConfigIdUsed:String? = null
|
||||||
|
private var serverConfigLastPing:Long = 0L
|
||||||
|
var serverUserMediaProgress:MutableList<MediaProgress> = mutableListOf()
|
||||||
var serverItemsInProgress = listOf<ItemInProgress>()
|
var serverItemsInProgress = listOf<ItemInProgress>()
|
||||||
var serverLibraries = listOf<Library>()
|
var serverLibraries = listOf<Library>()
|
||||||
var serverConfigIdUsed:String? = null
|
|
||||||
var serverConfigLastPing:Long = 0L
|
|
||||||
var serverUserMediaProgress:MutableList<MediaProgress> = mutableListOf()
|
|
||||||
|
|
||||||
var userSettingsPlaybackRate:Float? = null
|
private var userSettingsPlaybackRate:Float? = null
|
||||||
|
|
||||||
fun getIsLibrary(id:String) : Boolean {
|
fun getIsLibrary(id:String) : Boolean {
|
||||||
return serverLibraries.find { it.id == id } != null
|
return serverLibraries.find { it.id == id } != null
|
||||||
@@ -76,7 +76,7 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadItemsInProgressForAllLibraries(cb: (List<ItemInProgress>) -> Unit) {
|
private fun loadItemsInProgressForAllLibraries(cb: (List<ItemInProgress>) -> Unit) {
|
||||||
if (serverItemsInProgress.isNotEmpty()) {
|
if (serverItemsInProgress.isNotEmpty()) {
|
||||||
cb(serverItemsInProgress)
|
cb(serverItemsInProgress)
|
||||||
} else {
|
} else {
|
||||||
@@ -109,7 +109,7 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadLibraryItem(libraryItemId:String, cb: (LibraryItemWrapper?) -> Unit) {
|
private fun loadLibraryItem(libraryItemId:String, cb: (LibraryItemWrapper?) -> Unit) {
|
||||||
if (libraryItemId.startsWith("local")) {
|
if (libraryItemId.startsWith("local")) {
|
||||||
cb(DeviceManager.dbManager.getLocalLibraryItem(libraryItemId))
|
cb(DeviceManager.dbManager.getLocalLibraryItem(libraryItemId))
|
||||||
} else {
|
} else {
|
||||||
@@ -121,7 +121,7 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadPodcastEpisodeMediaBrowserItems(libraryItemId:String, cb: (MutableList<MediaBrowserCompat.MediaItem>) -> Unit) {
|
fun loadPodcastEpisodeMediaBrowserItems(libraryItemId:String, ctx:Context, cb: (MutableList<MediaBrowserCompat.MediaItem>) -> Unit) {
|
||||||
loadLibraryItem(libraryItemId) { libraryItemWrapper ->
|
loadLibraryItem(libraryItemId) { libraryItemWrapper ->
|
||||||
Log.d(tag, "Loaded Podcast library item $libraryItemWrapper")
|
Log.d(tag, "Loaded Podcast library item $libraryItemWrapper")
|
||||||
|
|
||||||
@@ -138,7 +138,8 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
Log.d(tag, "Local Podcast Episode ${podcastEpisode.title} | ${podcastEpisode.id}")
|
Log.d(tag, "Local Podcast Episode ${podcastEpisode.title} | ${podcastEpisode.id}")
|
||||||
|
|
||||||
val progress = DeviceManager.dbManager.getLocalMediaProgress("${libraryItemWrapper.id}-${podcastEpisode.id}")
|
val progress = DeviceManager.dbManager.getLocalMediaProgress("${libraryItemWrapper.id}-${podcastEpisode.id}")
|
||||||
val description = podcastEpisode.getMediaDescription(libraryItemWrapper, progress)
|
val description = podcastEpisode.getMediaDescription(libraryItemWrapper, progress, ctx)
|
||||||
|
|
||||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
}
|
}
|
||||||
children?.let { cb(children as MutableList) } ?: cb(mutableListOf())
|
children?.let { cb(children as MutableList) } ?: cb(mutableListOf())
|
||||||
@@ -157,7 +158,15 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
val children = podcast.episodes?.map { podcastEpisode ->
|
val children = podcast.episodes?.map { podcastEpisode ->
|
||||||
|
|
||||||
val progress = serverUserMediaProgress.find { it.libraryItemId == libraryItemWrapper.id && it.episodeId == podcastEpisode.id }
|
val progress = serverUserMediaProgress.find { it.libraryItemId == libraryItemWrapper.id && it.episodeId == podcastEpisode.id }
|
||||||
val description = podcastEpisode.getMediaDescription(libraryItemWrapper, progress)
|
|
||||||
|
// to show download icon
|
||||||
|
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItemWrapper.id)
|
||||||
|
localLibraryItem?.let { lli ->
|
||||||
|
val localEpisode = (lli.media as Podcast).episodes?.find { it.serverEpisodeId == podcastEpisode.id }
|
||||||
|
podcastEpisode.localEpisodeId = localEpisode?.id
|
||||||
|
}
|
||||||
|
|
||||||
|
val description = podcastEpisode.getMediaDescription(libraryItemWrapper, progress, ctx)
|
||||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
}
|
}
|
||||||
children?.let { cb(children as MutableList) } ?: cb(mutableListOf())
|
children?.let { cb(children as MutableList) } ?: cb(mutableListOf())
|
||||||
@@ -167,7 +176,7 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun loadLibraries(cb: (List<Library>) -> Unit) {
|
private fun loadLibraries(cb: (List<Library>) -> Unit) {
|
||||||
if (serverLibraries.isNotEmpty()) {
|
if (serverLibraries.isNotEmpty()) {
|
||||||
cb(serverLibraries)
|
cb(serverLibraries)
|
||||||
} else {
|
} else {
|
||||||
@@ -178,9 +187,9 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun checkServerConnection(config:ServerConnectionConfig) : Boolean {
|
private suspend fun checkServerConnection(config:ServerConnectionConfig) : Boolean {
|
||||||
var successfulPing = false
|
var successfulPing = false
|
||||||
suspendCoroutine<Boolean> { cont ->
|
suspendCoroutine { cont ->
|
||||||
apiHandler.pingServer(config) {
|
apiHandler.pingServer(config) {
|
||||||
Log.d(tag, "checkServerConnection: Checked server conn for ${config.address} result = $it")
|
Log.d(tag, "checkServerConnection: Checked server conn for ${config.address} result = $it")
|
||||||
successfulPing = it
|
successfulPing = it
|
||||||
@@ -190,9 +199,9 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
return successfulPing
|
return successfulPing
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun authorize(config:ServerConnectionConfig) : MutableList<MediaProgress> {
|
private suspend fun authorize(config:ServerConnectionConfig) : MutableList<MediaProgress> {
|
||||||
var mediaProgress:MutableList<MediaProgress> = mutableListOf()
|
var mediaProgress:MutableList<MediaProgress> = mutableListOf()
|
||||||
suspendCoroutine<MutableList<MediaProgress>> { cont ->
|
suspendCoroutine { cont ->
|
||||||
apiHandler.authorize(config) {
|
apiHandler.authorize(config) {
|
||||||
Log.d(tag, "authorize: Authorized server config ${config.address} result = $it")
|
Log.d(tag, "authorize: Authorized server config ${config.address} result = $it")
|
||||||
if (!it.isNullOrEmpty()) {
|
if (!it.isNullOrEmpty()) {
|
||||||
@@ -204,7 +213,7 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
return mediaProgress
|
return mediaProgress
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkSetValidServerConnectionConfig(cb: (Boolean) -> Unit) = runBlocking {
|
private fun checkSetValidServerConnectionConfig(cb: (Boolean) -> Unit) = runBlocking {
|
||||||
Log.d(tag, "checkSetValidServerConnectionConfig | $serverConfigIdUsed")
|
Log.d(tag, "checkSetValidServerConnectionConfig | $serverConfigIdUsed")
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
@@ -266,6 +275,23 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun loadServerUserMediaProgress(cb: () -> Unit) {
|
||||||
|
Log.d(tag, "Loading server media progress")
|
||||||
|
if (DeviceManager.serverConnectionConfig == null) {
|
||||||
|
return cb()
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceManager.serverConnectionConfig?.let { config ->
|
||||||
|
apiHandler.authorize(config) {
|
||||||
|
Log.d(tag, "loadServerUserMediaProgress: Authorized server config ${config.address} result = $it")
|
||||||
|
if (!it.isNullOrEmpty()) {
|
||||||
|
serverUserMediaProgress = it
|
||||||
|
}
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun loadAndroidAutoItems(cb: () -> Unit) {
|
fun loadAndroidAutoItems(cb: () -> Unit) {
|
||||||
Log.d(tag, "Load android auto items")
|
Log.d(tag, "Load android auto items")
|
||||||
|
|
||||||
@@ -314,18 +340,18 @@ class MediaManager(var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getPodcastWithEpisodeByEpisodeId(id:String) : LibraryItemWithEpisode? {
|
fun getPodcastWithEpisodeByEpisodeId(id:String) : LibraryItemWithEpisode? {
|
||||||
if (id.startsWith("local")) {
|
return if (id.startsWith("local")) {
|
||||||
return DeviceManager.dbManager.getLocalLibraryItemWithEpisode(id)
|
DeviceManager.dbManager.getLocalLibraryItemWithEpisode(id)
|
||||||
} else {
|
} else {
|
||||||
return podcastEpisodeLibraryItemMap[id]
|
podcastEpisodeLibraryItemMap[id]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getById(id:String) : LibraryItemWrapper? {
|
fun getById(id:String) : LibraryItemWrapper? {
|
||||||
if (id.startsWith("local")) {
|
return if (id.startsWith("local")) {
|
||||||
return DeviceManager.dbManager.getLocalLibraryItem(id)
|
DeviceManager.dbManager.getLocalLibraryItem(id)
|
||||||
} else {
|
} else {
|
||||||
return serverLibraryItems.find { it.id == id }
|
serverLibraryItems.find { it.id == id }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ 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.MediaProgress
|
||||||
import com.audiobookshelf.app.data.PlaybackSession
|
import com.audiobookshelf.app.data.PlaybackSession
|
||||||
|
import com.audiobookshelf.app.data.Podcast
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
|
import com.audiobookshelf.app.media.MediaEventManager
|
||||||
import com.audiobookshelf.app.server.ApiHandler
|
import com.audiobookshelf.app.server.ApiHandler
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
@@ -17,6 +19,12 @@ data class MediaProgressSyncData(
|
|||||||
var currentTime:Double // seconds
|
var currentTime:Double // seconds
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class SyncResult(
|
||||||
|
var serverSyncAttempted:Boolean,
|
||||||
|
var serverSyncSuccess:Boolean?,
|
||||||
|
var serverSyncMessage:String?
|
||||||
|
)
|
||||||
|
|
||||||
class MediaProgressSyncer(val playerNotificationService:PlayerNotificationService, private val apiHandler: ApiHandler) {
|
class MediaProgressSyncer(val playerNotificationService:PlayerNotificationService, private val apiHandler: ApiHandler) {
|
||||||
private val tag = "MediaProgressSync"
|
private val tag = "MediaProgressSync"
|
||||||
private val METERED_CONNECTION_SYNC_INTERVAL = 60000
|
private val METERED_CONNECTION_SYNC_INTERVAL = 60000
|
||||||
@@ -30,15 +38,15 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
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
|
||||||
|
|
||||||
val currentDisplayTitle get() = currentPlaybackSession?.displayTitle ?: "Unset"
|
private val currentDisplayTitle get() = currentPlaybackSession?.displayTitle ?: "Unset"
|
||||||
val currentIsLocal get() = currentPlaybackSession?.isLocal == true
|
private val currentIsLocal get() = currentPlaybackSession?.isLocal == true
|
||||||
val currentSessionId get() = currentPlaybackSession?.id ?: ""
|
val currentSessionId get() = currentPlaybackSession?.id ?: ""
|
||||||
val currentPlaybackDuration get() = currentPlaybackSession?.duration ?: 0.0
|
private val currentPlaybackDuration get() = currentPlaybackSession?.duration ?: 0.0
|
||||||
|
|
||||||
fun start() {
|
fun start(playbackSession:PlaybackSession) {
|
||||||
if (listeningTimerRunning) {
|
if (listeningTimerRunning) {
|
||||||
Log.d(tag, "start: Timer already running for $currentDisplayTitle")
|
Log.d(tag, "start: Timer already running for $currentDisplayTitle")
|
||||||
if (playerNotificationService.getCurrentPlaybackSessionId() != currentSessionId) {
|
if (playbackSession.id != currentSessionId) {
|
||||||
Log.d(tag, "Playback session changed, reset timer")
|
Log.d(tag, "Playback session changed, reset timer")
|
||||||
currentLocalMediaProgress = null
|
currentLocalMediaProgress = null
|
||||||
listeningTimerTask?.cancel()
|
listeningTimerTask?.cancel()
|
||||||
@@ -48,25 +56,32 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
} else {
|
} else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else if (playerNotificationService.getCurrentPlaybackSessionId() != currentSessionId) {
|
} else if (playbackSession.id != currentSessionId) {
|
||||||
currentLocalMediaProgress = null
|
currentLocalMediaProgress = null
|
||||||
}
|
}
|
||||||
|
|
||||||
listeningTimerRunning = true
|
listeningTimerRunning = true
|
||||||
lastSyncTime = System.currentTimeMillis()
|
lastSyncTime = System.currentTimeMillis()
|
||||||
Log.d(tag, "start: init last sync time $lastSyncTime")
|
Log.d(tag, "start: init last sync time $lastSyncTime")
|
||||||
currentPlaybackSession = playerNotificationService.getCurrentPlaybackSessionCopy()
|
currentPlaybackSession = playbackSession.clone()
|
||||||
|
|
||||||
listeningTimerTask = Timer("ListeningTimer", false).schedule(0L, 5000L) {
|
listeningTimerTask = Timer("ListeningTimer", false).schedule(15000L, 15000L) {
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
if (playerNotificationService.currentPlayer.isPlaying) {
|
if (playerNotificationService.currentPlayer.isPlaying) {
|
||||||
// Only sync with server on unmetered connection every 5s OR sync with server if last sync time is >= 60s
|
// Set auto sleep timer if enabled and within start/end time
|
||||||
|
playerNotificationService.sleepTimerManager.checkAutoSleepTimer()
|
||||||
|
|
||||||
|
// Only sync with server on unmetered connection every 15s OR sync with server if last sync time is >= 60s
|
||||||
val shouldSyncServer = PlayerNotificationService.isUnmeteredNetwork || System.currentTimeMillis() - lastSyncTime >= METERED_CONNECTION_SYNC_INTERVAL
|
val shouldSyncServer = PlayerNotificationService.isUnmeteredNetwork || System.currentTimeMillis() - lastSyncTime >= METERED_CONNECTION_SYNC_INTERVAL
|
||||||
|
|
||||||
val currentTime = playerNotificationService.getCurrentTimeSeconds()
|
val currentTime = playerNotificationService.getCurrentTimeSeconds()
|
||||||
if (currentTime > 0) {
|
if (currentTime > 0) {
|
||||||
sync(shouldSyncServer, currentTime) {
|
sync(shouldSyncServer, currentTime) { syncResult ->
|
||||||
Log.d(tag, "Sync complete")
|
Log.d(tag, "Sync complete")
|
||||||
|
|
||||||
|
currentPlaybackSession?.let { playbackSession ->
|
||||||
|
MediaEventManager.saveEvent(playbackSession, syncResult)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,8 +89,19 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun play(playbackSession:PlaybackSession) {
|
||||||
|
Log.d(tag, "play ${playbackSession.displayTitle}")
|
||||||
|
MediaEventManager.playEvent(playbackSession)
|
||||||
|
|
||||||
|
start(playbackSession)
|
||||||
|
}
|
||||||
|
|
||||||
fun stop(shouldSync:Boolean? = true, cb: () -> Unit) {
|
fun stop(shouldSync:Boolean? = true, cb: () -> Unit) {
|
||||||
if (!listeningTimerRunning) return
|
if (!listeningTimerRunning) {
|
||||||
|
reset()
|
||||||
|
return cb()
|
||||||
|
}
|
||||||
|
|
||||||
listeningTimerTask?.cancel()
|
listeningTimerTask?.cancel()
|
||||||
listeningTimerTask = null
|
listeningTimerTask = null
|
||||||
listeningTimerRunning = false
|
listeningTimerRunning = false
|
||||||
@@ -83,11 +109,19 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
|
|
||||||
val currentTime = if (shouldSync == true) playerNotificationService.getCurrentTimeSeconds() else 0.0
|
val currentTime = if (shouldSync == true) playerNotificationService.getCurrentTimeSeconds() else 0.0
|
||||||
if (currentTime > 0) { // Current time should always be > 0 on stop
|
if (currentTime > 0) { // Current time should always be > 0 on stop
|
||||||
sync(true, currentTime) {
|
sync(true, currentTime) { syncResult ->
|
||||||
|
currentPlaybackSession?.let { playbackSession ->
|
||||||
|
MediaEventManager.stopEvent(playbackSession, syncResult)
|
||||||
|
}
|
||||||
|
|
||||||
reset()
|
reset()
|
||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
currentPlaybackSession?.let { playbackSession ->
|
||||||
|
MediaEventManager.stopEvent(playbackSession, null)
|
||||||
|
}
|
||||||
|
|
||||||
reset()
|
reset()
|
||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
@@ -95,6 +129,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
|
|
||||||
fun pause(cb: () -> Unit) {
|
fun pause(cb: () -> Unit) {
|
||||||
if (!listeningTimerRunning) return
|
if (!listeningTimerRunning) return
|
||||||
|
|
||||||
listeningTimerTask?.cancel()
|
listeningTimerTask?.cancel()
|
||||||
listeningTimerTask = null
|
listeningTimerTask = null
|
||||||
listeningTimerRunning = false
|
listeningTimerRunning = false
|
||||||
@@ -103,39 +138,82 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
|
|
||||||
val currentTime = playerNotificationService.getCurrentTimeSeconds()
|
val currentTime = playerNotificationService.getCurrentTimeSeconds()
|
||||||
if (currentTime > 0) { // Current time should always be > 0 on pause
|
if (currentTime > 0) { // Current time should always be > 0 on pause
|
||||||
sync(true, currentTime) {
|
sync(true, currentTime) { syncResult ->
|
||||||
lastSyncTime = 0L
|
lastSyncTime = 0L
|
||||||
Log.d(tag, "pause: Set last sync time 0 $lastSyncTime")
|
Log.d(tag, "pause: Set last sync time 0 $lastSyncTime")
|
||||||
failedSyncs = 0
|
failedSyncs = 0
|
||||||
|
|
||||||
|
currentPlaybackSession?.let { playbackSession ->
|
||||||
|
MediaEventManager.pauseEvent(playbackSession, syncResult)
|
||||||
|
}
|
||||||
|
|
||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
lastSyncTime = 0L
|
lastSyncTime = 0L
|
||||||
Log.d(tag, "pause: Set last sync time 0 $lastSyncTime (current time < 0)")
|
Log.d(tag, "pause: Set last sync time 0 $lastSyncTime (current time < 0)")
|
||||||
failedSyncs = 0
|
failedSyncs = 0
|
||||||
|
|
||||||
|
currentPlaybackSession?.let { playbackSession ->
|
||||||
|
MediaEventManager.pauseEvent(playbackSession, null)
|
||||||
|
}
|
||||||
|
|
||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun finished(cb: () -> Unit) {
|
||||||
|
if (!listeningTimerRunning) return
|
||||||
|
|
||||||
|
listeningTimerTask?.cancel()
|
||||||
|
listeningTimerTask = null
|
||||||
|
listeningTimerRunning = false
|
||||||
|
Log.d(tag, "finished: Stopping listening for $currentDisplayTitle")
|
||||||
|
|
||||||
|
sync(true, currentPlaybackSession?.duration ?: 0.0) { syncResult ->
|
||||||
|
reset()
|
||||||
|
|
||||||
|
currentPlaybackSession?.let { playbackSession ->
|
||||||
|
MediaEventManager.finishedEvent(playbackSession, syncResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun seek() {
|
||||||
|
currentPlaybackSession?.currentTime = playerNotificationService.getCurrentTimeSeconds()
|
||||||
|
Log.d(tag, "seek: $currentDisplayTitle, currentTime=${currentPlaybackSession?.currentTime}")
|
||||||
|
|
||||||
|
if (currentPlaybackSession == null) {
|
||||||
|
Log.e(tag, "seek: Playback session not set")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
MediaEventManager.seekEvent(currentPlaybackSession!!, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun syncFromServerProgress(mediaProgress: MediaProgress) {
|
fun syncFromServerProgress(mediaProgress: MediaProgress) {
|
||||||
currentPlaybackSession?.let {
|
currentPlaybackSession?.let {
|
||||||
it.updatedAt = mediaProgress.lastUpdate
|
it.updatedAt = mediaProgress.lastUpdate
|
||||||
it.currentTime = mediaProgress.currentTime
|
it.currentTime = mediaProgress.currentTime
|
||||||
|
|
||||||
|
MediaEventManager.syncEvent(mediaProgress, "Received from server get media progress request while playback session open")
|
||||||
|
|
||||||
DeviceManager.dbManager.saveLocalPlaybackSession(it)
|
DeviceManager.dbManager.saveLocalPlaybackSession(it)
|
||||||
saveLocalProgress(it)
|
saveLocalProgress(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sync(shouldSyncServer:Boolean, currentTime:Double, cb: () -> Unit) {
|
fun sync(shouldSyncServer:Boolean, currentTime:Double, cb: (SyncResult?) -> Unit) {
|
||||||
if (lastSyncTime <= 0) {
|
if (lastSyncTime <= 0) {
|
||||||
Log.e(tag, "Last sync time is not set $lastSyncTime")
|
Log.e(tag, "Last sync time is not set $lastSyncTime")
|
||||||
return
|
return cb(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
val diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
|
val diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
|
||||||
if (diffSinceLastSync < 1000L) {
|
if (diffSinceLastSync < 1000L) {
|
||||||
return cb()
|
return cb(null)
|
||||||
}
|
}
|
||||||
val listeningTimeToAdd = diffSinceLastSync / 1000L
|
val listeningTimeToAdd = diffSinceLastSync / 1000L
|
||||||
|
|
||||||
@@ -145,7 +223,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
|
|
||||||
if (currentPlaybackSession?.progress?.isNaN() == true) {
|
if (currentPlaybackSession?.progress?.isNaN() == true) {
|
||||||
Log.e(tag, "Current Playback Session invalid progress ${currentPlaybackSession?.progress} | Current Time: ${currentPlaybackSession?.currentTime} | Duration: ${currentPlaybackSession?.getTotalDuration()}")
|
Log.e(tag, "Current Playback Session invalid progress ${currentPlaybackSession?.progress} | Current Time: ${currentPlaybackSession?.currentTime} | Duration: ${currentPlaybackSession?.getTotalDuration()}")
|
||||||
return cb()
|
return cb(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentIsLocal) {
|
if (currentIsLocal) {
|
||||||
@@ -158,11 +236,12 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
// Local library item is linked to a server library item
|
// Local library item is linked to a server library item
|
||||||
// Send sync to server also if connected to this server and local item belongs to this server
|
// Send sync to server also if connected to this server and local item belongs to this server
|
||||||
if (shouldSyncServer && !it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
|
if (shouldSyncServer && !it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
|
||||||
apiHandler.sendLocalProgressSync(it) { syncSuccess ->
|
apiHandler.sendLocalProgressSync(it) { syncSuccess, errorMsg ->
|
||||||
Log.d(
|
Log.d(
|
||||||
tag,
|
tag,
|
||||||
"Local progress sync data sent to server $currentDisplayTitle for time $currentTime"
|
"Local progress sync data sent to server $currentDisplayTitle for time $currentTime"
|
||||||
)
|
)
|
||||||
|
|
||||||
if (syncSuccess) {
|
if (syncSuccess) {
|
||||||
failedSyncs = 0
|
failedSyncs = 0
|
||||||
playerNotificationService.alertSyncSuccess()
|
playerNotificationService.alertSyncSuccess()
|
||||||
@@ -175,15 +254,16 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
Log.e(tag, "Local Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
|
Log.e(tag, "Local Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
|
||||||
}
|
}
|
||||||
|
|
||||||
cb()
|
cb(SyncResult(true, syncSuccess, errorMsg))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
cb()
|
cb(SyncResult(false, null, null))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (shouldSyncServer) {
|
} else if (shouldSyncServer) {
|
||||||
apiHandler.sendProgressSync(currentSessionId, syncData) {
|
Log.d(tag, "sync: currentSessionId=$currentSessionId")
|
||||||
if (it) {
|
apiHandler.sendProgressSync(currentSessionId, syncData) { syncSuccess, errorMsg ->
|
||||||
|
if (syncSuccess) {
|
||||||
Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime")
|
Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime")
|
||||||
failedSyncs = 0
|
failedSyncs = 0
|
||||||
playerNotificationService.alertSyncSuccess()
|
playerNotificationService.alertSyncSuccess()
|
||||||
@@ -196,10 +276,10 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
}
|
}
|
||||||
Log.e(tag, "Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
|
Log.e(tag, "Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime")
|
||||||
}
|
}
|
||||||
cb()
|
cb(SyncResult(true, syncSuccess, errorMsg))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
cb()
|
cb(SyncResult(false, null, null))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,17 +290,18 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
currentLocalMediaProgress = playbackSession.getNewLocalMediaProgress()
|
currentLocalMediaProgress = playbackSession.getNewLocalMediaProgress()
|
||||||
} else {
|
} else {
|
||||||
currentLocalMediaProgress = mediaProgress
|
currentLocalMediaProgress = mediaProgress
|
||||||
|
currentLocalMediaProgress?.updateFromPlaybackSession(playbackSession)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
currentLocalMediaProgress?.updateFromPlaybackSession(playbackSession)
|
currentLocalMediaProgress?.updateFromPlaybackSession(playbackSession)
|
||||||
}
|
}
|
||||||
|
|
||||||
currentLocalMediaProgress?.let {
|
currentLocalMediaProgress?.let {
|
||||||
if (it.progress.isNaN()) {
|
if (it.progress.isNaN()) {
|
||||||
Log.e(tag, "Invalid progress on local media progress")
|
Log.e(tag, "Invalid progress on local media progress")
|
||||||
} else {
|
} else {
|
||||||
DeviceManager.dbManager.saveLocalMediaProgress(it)
|
DeviceManager.dbManager.saveLocalMediaProgress(it)
|
||||||
playerNotificationService.clientEventEmitter?.onLocalMediaProgressUpdate(it)
|
playerNotificationService.clientEventEmitter?.onLocalMediaProgressUpdate(it)
|
||||||
|
|
||||||
Log.d(tag, "Saved Local Progress Current Time: ID ${it.id} | ${it.currentTime} | Duration ${it.duration} | Progress ${it.progressPercent}%")
|
Log.d(tag, "Saved Local Progress Current Time: ID ${it.id} | ${it.currentTime} | Duration ${it.duration} | Progress ${it.progressPercent}%")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.audiobookshelf.app.models
|
||||||
|
|
||||||
|
import com.audiobookshelf.app.data.LocalFolder
|
||||||
|
import com.audiobookshelf.app.data.MediaProgress
|
||||||
|
import com.audiobookshelf.app.data.MediaType
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
|
|
||||||
|
data class DownloadItem(
|
||||||
|
val id: String,
|
||||||
|
val libraryItemId:String,
|
||||||
|
val episodeId:String?,
|
||||||
|
val userMediaProgress: MediaProgress?,
|
||||||
|
val serverConnectionConfigId:String,
|
||||||
|
val serverAddress:String,
|
||||||
|
val serverUserId:String,
|
||||||
|
val mediaType: String,
|
||||||
|
val itemFolderPath:String,
|
||||||
|
val localFolder: LocalFolder,
|
||||||
|
val itemTitle: String,
|
||||||
|
val itemSubfolder: String,
|
||||||
|
val media: MediaType,
|
||||||
|
val downloadItemParts: MutableList<DownloadItemPart>
|
||||||
|
) {
|
||||||
|
@get:JsonIgnore
|
||||||
|
val isDownloadFinished get() = !downloadItemParts.any { !it.completed || it.isMoving }
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun getNextDownloadItemParts(limit:Int): MutableList<DownloadItemPart> {
|
||||||
|
val itemParts = mutableListOf<DownloadItemPart>()
|
||||||
|
if (limit == 0) return itemParts
|
||||||
|
|
||||||
|
for (it in downloadItemParts) {
|
||||||
|
if (!it.completed && it.downloadId == null) {
|
||||||
|
itemParts.add(it)
|
||||||
|
if (itemParts.size > limit) break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return itemParts
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.audiobookshelf.app.models
|
||||||
|
|
||||||
|
import android.app.DownloadManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import com.audiobookshelf.app.data.AudioTrack
|
||||||
|
import com.audiobookshelf.app.data.LocalFolder
|
||||||
|
import com.audiobookshelf.app.data.PodcastEpisode
|
||||||
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
data class DownloadItemPart(
|
||||||
|
val id: String,
|
||||||
|
val downloadItemId: String,
|
||||||
|
val filename: String,
|
||||||
|
val fileSize: Long,
|
||||||
|
val finalDestinationPath:String,
|
||||||
|
val serverPath: String,
|
||||||
|
val localFolderName: String,
|
||||||
|
val localFolderUrl: String,
|
||||||
|
val localFolderId: String,
|
||||||
|
val audioTrack: AudioTrack?,
|
||||||
|
val episode: PodcastEpisode?,
|
||||||
|
var completed:Boolean,
|
||||||
|
var moved:Boolean,
|
||||||
|
var isMoving:Boolean,
|
||||||
|
var failed:Boolean,
|
||||||
|
@JsonIgnore val uri: Uri,
|
||||||
|
@JsonIgnore val destinationUri: Uri,
|
||||||
|
@JsonIgnore val finalDestinationUri: Uri,
|
||||||
|
val finalDestinationSubfolder: String,
|
||||||
|
var downloadId: Long?,
|
||||||
|
var progress: Long,
|
||||||
|
var bytesDownloaded: Long
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun make(downloadItemId:String, filename:String, fileSize: Long, destinationFile: File, finalDestinationFile: File, subfolder:String, serverPath:String, localFolder: LocalFolder, audioTrack: AudioTrack?, episode: PodcastEpisode?) :DownloadItemPart {
|
||||||
|
val destinationUri = Uri.fromFile(destinationFile)
|
||||||
|
val finalDestinationUri = Uri.fromFile(finalDestinationFile)
|
||||||
|
|
||||||
|
var downloadUrl = "${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}"
|
||||||
|
if (serverPath.endsWith("/cover")) downloadUrl += "&format=jpeg" // For cover images force to jpeg
|
||||||
|
val downloadUri = Uri.parse(downloadUrl)
|
||||||
|
Log.d("DownloadItemPart", "Audio File Destination Uri: $destinationUri | Final Destination Uri: $finalDestinationUri | Download URI $downloadUri")
|
||||||
|
return DownloadItemPart(
|
||||||
|
id = DeviceManager.getBase64Id(finalDestinationFile.absolutePath),
|
||||||
|
downloadItemId,
|
||||||
|
filename = filename,
|
||||||
|
fileSize = fileSize,
|
||||||
|
finalDestinationPath = finalDestinationFile.absolutePath,
|
||||||
|
serverPath = serverPath,
|
||||||
|
localFolderName = localFolder.name,
|
||||||
|
localFolderUrl = localFolder.contentUrl,
|
||||||
|
localFolderId = localFolder.id,
|
||||||
|
audioTrack = audioTrack,
|
||||||
|
episode = episode,
|
||||||
|
completed = false,
|
||||||
|
moved = false,
|
||||||
|
isMoving = false,
|
||||||
|
failed = false,
|
||||||
|
uri = downloadUri,
|
||||||
|
destinationUri = destinationUri,
|
||||||
|
finalDestinationUri = finalDestinationUri,
|
||||||
|
finalDestinationSubfolder = subfolder,
|
||||||
|
downloadId = null,
|
||||||
|
progress = 0,
|
||||||
|
bytesDownloaded = 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun getDownloadRequest(): DownloadManager.Request {
|
||||||
|
val dlRequest = DownloadManager.Request(uri)
|
||||||
|
dlRequest.setTitle(filename)
|
||||||
|
dlRequest.setDescription("Downloading to $localFolderName with filename $filename")
|
||||||
|
dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
|
||||||
|
dlRequest.setDestinationUri(destinationUri)
|
||||||
|
return dlRequest
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,24 +2,25 @@ package com.audiobookshelf.app.player
|
|||||||
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.ImageDecoder
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.provider.MediaStore
|
||||||
import android.support.v4.media.session.MediaControllerCompat
|
import android.support.v4.media.session.MediaControllerCompat
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
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.request.RequestOptions
|
|
||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
const val NOTIFICATION_LARGE_ICON_SIZE = 144 // px
|
const val NOTIFICATION_LARGE_ICON_SIZE = 144 // px
|
||||||
|
|
||||||
class AbMediaDescriptionAdapter constructor(private val controller: MediaControllerCompat, val playerNotificationService: PlayerNotificationService) : PlayerNotificationManager.MediaDescriptionAdapter {
|
class AbMediaDescriptionAdapter constructor(private val controller: MediaControllerCompat, private val playerNotificationService: PlayerNotificationService) : PlayerNotificationManager.MediaDescriptionAdapter {
|
||||||
private val tag = "MediaDescriptionAdapter"
|
private val tag = "MediaDescriptionAdapter"
|
||||||
|
|
||||||
var currentIconUri: Uri? = null
|
private var currentIconUri: Uri? = null
|
||||||
var currentBitmap: Bitmap? = null
|
private var currentBitmap: Bitmap? = null
|
||||||
|
|
||||||
private val serviceJob = SupervisorJob()
|
private val serviceJob = SupervisorJob()
|
||||||
private val serviceScope = CoroutineScope(Dispatchers.Main + serviceJob)
|
private val serviceScope = CoroutineScope(Dispatchers.Main + serviceJob)
|
||||||
@@ -42,13 +43,26 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
|
|||||||
// `getCurrentLargeIcon` don't cause the bitmap to be recreated.
|
// `getCurrentLargeIcon` don't cause the bitmap to be recreated.
|
||||||
currentIconUri = albumArtUri
|
currentIconUri = albumArtUri
|
||||||
Log.d(tag, "ART $currentIconUri")
|
Log.d(tag, "ART $currentIconUri")
|
||||||
serviceScope.launch {
|
|
||||||
currentBitmap = albumArtUri?.let {
|
if (currentIconUri.toString().startsWith("content://")) {
|
||||||
resolveUriAsBitmap(it)
|
currentBitmap = if (Build.VERSION.SDK_INT < 28) {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
MediaStore.Images.Media.getBitmap(playerNotificationService.contentResolver, currentIconUri)
|
||||||
|
} else {
|
||||||
|
val source: ImageDecoder.Source = ImageDecoder.createSource(playerNotificationService.contentResolver, currentIconUri!!)
|
||||||
|
ImageDecoder.decodeBitmap(source)
|
||||||
}
|
}
|
||||||
currentBitmap?.let { callback.onBitmap(it) }
|
currentBitmap
|
||||||
|
} else {
|
||||||
|
serviceScope.launch {
|
||||||
|
currentBitmap = albumArtUri?.let {
|
||||||
|
resolveUriAsBitmap(it)
|
||||||
|
}
|
||||||
|
currentBitmap?.let { callback.onBitmap(it) }
|
||||||
|
}
|
||||||
|
null
|
||||||
}
|
}
|
||||||
null
|
|
||||||
} else {
|
} else {
|
||||||
currentBitmap
|
currentBitmap
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,11 +17,10 @@ class BrowseTree(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* get uri to drawable or any other resource type if u wish
|
* get uri to drawable or any other resource type if u wish
|
||||||
* @param context - context
|
|
||||||
* @param drawableId - drawable res id
|
* @param drawableId - drawable res id
|
||||||
* @return - uri
|
* @return - uri
|
||||||
*/
|
*/
|
||||||
fun getUriToDrawable(@AnyRes drawableId: Int): Uri {
|
private fun getUriToDrawable(@AnyRes drawableId: Int): Uri {
|
||||||
return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE
|
return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE
|
||||||
+ "://" + context.resources.getResourcePackageName(drawableId)
|
+ "://" + context.resources.getResourcePackageName(drawableId)
|
||||||
+ '/' + context.resources.getResourceTypeName(drawableId)
|
+ '/' + context.resources.getResourceTypeName(drawableId)
|
||||||
@@ -49,7 +48,7 @@ class BrowseTree(
|
|||||||
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.icon_library_folder).toString())
|
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.icon_library_folder).toString())
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
if (!itemsInProgress.isEmpty()) {
|
if (itemsInProgress.isNotEmpty()) {
|
||||||
rootList += continueListeningMetadata
|
rootList += continueListeningMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ package com.audiobookshelf.app.player
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.*
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
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
|
||||||
@@ -18,7 +15,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
var tag = "MediaSessionCallback"
|
var tag = "MediaSessionCallback"
|
||||||
|
|
||||||
private var mediaButtonClickCount: Int = 0
|
private var mediaButtonClickCount: Int = 0
|
||||||
var mediaButtonClickTimeout: Long = 1000 //ms
|
private var mediaButtonClickTimeout: Long = 1000 //ms
|
||||||
|
|
||||||
override fun onPrepare() {
|
override fun onPrepare() {
|
||||||
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
|
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
|
||||||
@@ -28,7 +25,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
Log.e(tag, "Failed to play library item")
|
Log.e(tag, "Failed to play library item")
|
||||||
} else {
|
} else {
|
||||||
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
|
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post {
|
||||||
playerNotificationService.preparePlayer(it,true, playbackRate)
|
playerNotificationService.preparePlayer(it,true, playbackRate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,7 +51,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
Log.e(tag, "Failed to play library item")
|
Log.e(tag, "Failed to play library item")
|
||||||
} else {
|
} else {
|
||||||
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
|
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post {
|
||||||
playerNotificationService.preparePlayer(it, true, playbackRate)
|
playerNotificationService.preparePlayer(it, true, playbackRate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,7 +90,7 @@ 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
|
val libraryItemWrapper: LibraryItemWrapper?
|
||||||
var podcastEpisode: PodcastEpisode? = null
|
var podcastEpisode: PodcastEpisode? = null
|
||||||
|
|
||||||
if (mediaId.isNullOrEmpty()) {
|
if (mediaId.isNullOrEmpty()) {
|
||||||
@@ -116,7 +113,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
Log.e(tag, "Failed to play library item")
|
Log.e(tag, "Failed to play library item")
|
||||||
} else {
|
} else {
|
||||||
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
|
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post {
|
||||||
playerNotificationService.preparePlayer(it, true, playbackRate)
|
playerNotificationService.preparePlayer(it, true, playbackRate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,11 +125,17 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
return handleCallMediaButton(mediaButtonEvent)
|
return handleCallMediaButton(mediaButtonEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleCallMediaButton(intent: Intent): Boolean {
|
private fun handleCallMediaButton(intent: Intent): Boolean {
|
||||||
Log.w(tag, "handleCallMediaButton $intent | ${intent.action}")
|
Log.w(tag, "handleCallMediaButton $intent | ${intent.action}")
|
||||||
|
|
||||||
if(Intent.ACTION_MEDIA_BUTTON == intent.action) {
|
if(Intent.ACTION_MEDIA_BUTTON == intent.action) {
|
||||||
val keyEvent = intent.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT)
|
val keyEvent = if (Build.VERSION.SDK_INT >= 33) {
|
||||||
|
intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT)
|
||||||
|
}
|
||||||
|
|
||||||
Log.d(tag, "handleCallMediaButton keyEvent = $keyEvent | action ${keyEvent?.action}")
|
Log.d(tag, "handleCallMediaButton keyEvent = $keyEvent | action ${keyEvent?.action}")
|
||||||
|
|
||||||
if (keyEvent?.action == KeyEvent.ACTION_DOWN) {
|
if (keyEvent?.action == KeyEvent.ACTION_DOWN) {
|
||||||
@@ -166,7 +169,6 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
Log.d(tag, "handleCallMediaButton: Media Play")
|
Log.d(tag, "handleCallMediaButton: Media Play")
|
||||||
if (0 == mediaButtonClickCount) {
|
if (0 == mediaButtonClickCount) {
|
||||||
playerNotificationService.play()
|
playerNotificationService.play()
|
||||||
playerNotificationService.sleepTimerManager.checkShouldExtendSleepTimer()
|
|
||||||
}
|
}
|
||||||
handleMediaButtonClickCount()
|
handleMediaButtonClickCount()
|
||||||
}
|
}
|
||||||
@@ -181,6 +183,12 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
|
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
|
||||||
playerNotificationService.jumpBackward()
|
playerNotificationService.jumpBackward()
|
||||||
}
|
}
|
||||||
|
KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
|
||||||
|
playerNotificationService.jumpForward()
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_MEDIA_REWIND -> {
|
||||||
|
playerNotificationService.jumpBackward()
|
||||||
|
}
|
||||||
KeyEvent.KEYCODE_MEDIA_STOP -> {
|
KeyEvent.KEYCODE_MEDIA_STOP -> {
|
||||||
playerNotificationService.closePlayback()
|
playerNotificationService.closePlayback()
|
||||||
}
|
}
|
||||||
@@ -192,7 +200,6 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
} else {
|
} else {
|
||||||
if (0 == mediaButtonClickCount) {
|
if (0 == mediaButtonClickCount) {
|
||||||
playerNotificationService.play()
|
playerNotificationService.play()
|
||||||
playerNotificationService.sleepTimerManager.checkShouldExtendSleepTimer()
|
|
||||||
}
|
}
|
||||||
handleMediaButtonClickCount()
|
handleMediaButtonClickCount()
|
||||||
}
|
}
|
||||||
@@ -239,6 +246,8 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
when (action) {
|
when (action) {
|
||||||
CUSTOM_ACTION_JUMP_FORWARD -> onFastForward()
|
CUSTOM_ACTION_JUMP_FORWARD -> onFastForward()
|
||||||
CUSTOM_ACTION_JUMP_BACKWARD -> onRewind()
|
CUSTOM_ACTION_JUMP_BACKWARD -> onRewind()
|
||||||
|
CUSTOM_ACTION_SKIP_FORWARD -> onSkipToNext()
|
||||||
|
CUSTOM_ACTION_SKIP_BACKWARD -> onSkipToPrevious()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
|||||||
Log.e(tag, "Failed to play library item")
|
Log.e(tag, "Failed to play library item")
|
||||||
} else {
|
} else {
|
||||||
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
|
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post {
|
||||||
playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
|
playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,7 +46,7 @@ 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? = null
|
val libraryItemWrapper: LibraryItemWrapper?
|
||||||
var podcastEpisode: PodcastEpisode? = null
|
var podcastEpisode: PodcastEpisode? = null
|
||||||
|
|
||||||
if (mediaId.startsWith("ep_") || mediaId.startsWith("local_ep_")) { // Playing podcast episode
|
if (mediaId.startsWith("ep_") || mediaId.startsWith("local_ep_")) { // Playing podcast episode
|
||||||
@@ -63,7 +63,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
|||||||
Log.e(tag, "Failed to play library item")
|
Log.e(tag, "Failed to play library item")
|
||||||
} else {
|
} else {
|
||||||
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
|
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post {
|
||||||
playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
|
playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,7 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
|
|||||||
Log.e(tag, "Failed to play library item")
|
Log.e(tag, "Failed to play library item")
|
||||||
} else {
|
} else {
|
||||||
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
|
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post {
|
||||||
playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
|
playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,3 +2,19 @@ package com.audiobookshelf.app.player
|
|||||||
|
|
||||||
const val CUSTOM_ACTION_JUMP_FORWARD = "com.audiobookshelf.customAction.jump_forward";
|
const val CUSTOM_ACTION_JUMP_FORWARD = "com.audiobookshelf.customAction.jump_forward";
|
||||||
const val CUSTOM_ACTION_JUMP_BACKWARD = "com.audiobookshelf.customAction.jump_backward";
|
const val CUSTOM_ACTION_JUMP_BACKWARD = "com.audiobookshelf.customAction.jump_backward";
|
||||||
|
const val CUSTOM_ACTION_SKIP_FORWARD = "com.audiobookshelf.customAction.skip_forward";
|
||||||
|
const val CUSTOM_ACTION_SKIP_BACKWARD = "com.audiobookshelf.customAction.skip_backward";
|
||||||
|
|
||||||
|
const val PLAYMETHOD_DIRECTPLAY = 0
|
||||||
|
const val PLAYMETHOD_DIRECTSTREAM = 1
|
||||||
|
const val PLAYMETHOD_TRANSCODE = 2
|
||||||
|
const val PLAYMETHOD_LOCAL = 3
|
||||||
|
|
||||||
|
//
|
||||||
|
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
||||||
|
//
|
||||||
|
const val ANDROID_AUTO_PKG_NAME = "com.google.android.projection.gearhead"
|
||||||
|
const val ANDROID_AUTO_SIMULATOR_PKG_NAME = "com.google.android.autosimulator"
|
||||||
|
const val ANDROID_WEARABLE_PKG_NAME = "com.google.android.wearable.app"
|
||||||
|
const val ANDROID_GSEARCH_PKG_NAME = "com.google.android.googlequicksearchbox"
|
||||||
|
const val ANDROID_AUTOMOTIVE_PKG_NAME = "com.google.android.carassistant"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.audiobookshelf.app.player
|
package com.audiobookshelf.app.player
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.audiobookshelf.app.data.PlaybackSession
|
||||||
import com.audiobookshelf.app.data.PlayerState
|
import com.audiobookshelf.app.data.PlayerState
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import com.google.android.exoplayer2.PlaybackException
|
import com.google.android.exoplayer2.PlaybackException
|
||||||
@@ -13,16 +14,101 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
var lastPauseTime: Long = 0 //ms
|
var lastPauseTime: Long = 0 //ms
|
||||||
|
var lazyIsPlaying: Boolean = false
|
||||||
}
|
}
|
||||||
|
|
||||||
private var onSeekBack: Boolean = false
|
|
||||||
|
|
||||||
override fun onPlayerError(error: PlaybackException) {
|
override fun onPlayerError(error: PlaybackException) {
|
||||||
val 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPositionDiscontinuity(
|
||||||
|
oldPosition: Player.PositionInfo,
|
||||||
|
newPosition: Player.PositionInfo,
|
||||||
|
reason: Int
|
||||||
|
) {
|
||||||
|
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
|
||||||
|
// If playing set seeking flag
|
||||||
|
Log.d(tag, "onPositionDiscontinuity: oldPosition=${oldPosition.positionMs}/${oldPosition.mediaItemIndex}, newPosition=${newPosition.positionMs}/${newPosition.mediaItemIndex}, isPlaying=${playerNotificationService.currentPlayer.isPlaying} reason=SEEK")
|
||||||
|
playerNotificationService.mediaProgressSyncer.seek()
|
||||||
|
lastPauseTime = 0 // When seeking while paused reset the auto-rewind
|
||||||
|
} else {
|
||||||
|
Log.d(tag, "onPositionDiscontinuity: oldPosition=${oldPosition.positionMs}/${oldPosition.mediaItemIndex}, newPosition=${newPosition.positionMs}/${newPosition.mediaItemIndex}, isPlaying=${playerNotificationService.currentPlayer.isPlaying}, reason=$reason")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||||
|
Log.d(tag, "onIsPlayingChanged to $isPlaying | ${playerNotificationService.getMediaPlayer()} | playbackState=${playerNotificationService.currentPlayer.playbackState}")
|
||||||
|
|
||||||
|
val player = playerNotificationService.currentPlayer
|
||||||
|
|
||||||
|
// Goal of these 2 if statements and the lazyIsPlaying is to ignore this event when it is triggered by a seek
|
||||||
|
// When a seek occurs the player is paused and buffering, then plays again right afterwards.
|
||||||
|
if (!isPlaying && player.playbackState == Player.STATE_BUFFERING) {
|
||||||
|
Log.d(tag, "onIsPlayingChanged: Pause event when buffering is ignored")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (lazyIsPlaying == isPlaying) {
|
||||||
|
Log.d(tag, "onIsPlayingChanged: Lazy is playing $lazyIsPlaying is already set to this so ignoring")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lazyIsPlaying = isPlaying
|
||||||
|
|
||||||
|
if (isPlaying) {
|
||||||
|
Log.d(tag, "SeekBackTime: Player is playing")
|
||||||
|
if (lastPauseTime > 0 && DeviceManager.deviceData.deviceSettings?.disableAutoRewind != true) {
|
||||||
|
Log.d(tag, "SeekBackTime: playing started now set seek back time $lastPauseTime")
|
||||||
|
var seekBackTime = calcPauseSeekBackTime()
|
||||||
|
if (seekBackTime > 0) {
|
||||||
|
// Current chapter is used so that seek back does not go back to the previous chapter
|
||||||
|
val currentChapter = playerNotificationService.getCurrentBookChapter()
|
||||||
|
val minSeekBackTime = currentChapter?.startMs ?: 0
|
||||||
|
|
||||||
|
val currentTime = playerNotificationService.getCurrentTime()
|
||||||
|
val newTime = currentTime - seekBackTime
|
||||||
|
if (newTime < minSeekBackTime) {
|
||||||
|
seekBackTime = currentTime - minSeekBackTime
|
||||||
|
}
|
||||||
|
Log.d(tag, "SeekBackTime $seekBackTime")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(seekBackTime)
|
||||||
|
if (!shouldCarryOn) return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seekBackTime > 0L) {
|
||||||
|
playerNotificationService.seekBackward(seekBackTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.d(tag, "SeekBackTime: Player not playing set last pause time | playbackState=${player.playbackState}")
|
||||||
|
lastPauseTime = System.currentTimeMillis()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start/stop progress sync interval
|
||||||
|
if (isPlaying) {
|
||||||
|
// Handles auto-starting sleep timer and resetting sleep timer
|
||||||
|
playerNotificationService.sleepTimerManager.handleMediaPlayEvent()
|
||||||
|
|
||||||
|
player.volume = 1F // Volume on sleep timer might have decreased this
|
||||||
|
val playbackSession: PlaybackSession? = playerNotificationService.mediaProgressSyncer.currentPlaybackSession ?: playerNotificationService.currentPlaybackSession
|
||||||
|
playbackSession?.let { playerNotificationService.mediaProgressSyncer.play(it) }
|
||||||
|
} else {
|
||||||
|
playerNotificationService.mediaProgressSyncer.pause {
|
||||||
|
Log.d(tag, "Media Progress Syncer paused and synced")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playerNotificationService.clientEventEmitter?.onPlayingUpdate(isPlaying)
|
||||||
|
|
||||||
|
DeviceManager.widgetUpdater?.onPlayerChanged(playerNotificationService)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onEvents(player: Player, events: Player.Events) {
|
override fun onEvents(player: Player, events: Player.Events) {
|
||||||
Log.d(tag, "onEvents ${playerNotificationService.getMediaPlayer()} | ${events.size()}")
|
Log.d(tag, "onEvents ${playerNotificationService.getMediaPlayer()} | ${events.size()}")
|
||||||
|
|
||||||
@@ -41,7 +127,7 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||||||
Log.d(tag, "STATE_READY : " + playerNotificationService.currentPlayer.duration)
|
Log.d(tag, "STATE_READY : " + playerNotificationService.currentPlayer.duration)
|
||||||
|
|
||||||
if (lastPauseTime == 0L) {
|
if (lastPauseTime == 0L) {
|
||||||
lastPauseTime = -1;
|
lastPauseTime = -1
|
||||||
}
|
}
|
||||||
playerNotificationService.sendClientMetadata(PlayerState.READY)
|
playerNotificationService.sendClientMetadata(PlayerState.READY)
|
||||||
}
|
}
|
||||||
@@ -52,6 +138,8 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||||||
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_ENDED) {
|
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_ENDED) {
|
||||||
Log.d(tag, "STATE_ENDED")
|
Log.d(tag, "STATE_ENDED")
|
||||||
playerNotificationService.sendClientMetadata(PlayerState.ENDED)
|
playerNotificationService.sendClientMetadata(PlayerState.ENDED)
|
||||||
|
|
||||||
|
playerNotificationService.handlePlaybackEnded()
|
||||||
}
|
}
|
||||||
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_IDLE) {
|
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_IDLE) {
|
||||||
Log.d(tag, "STATE_IDLE")
|
Log.d(tag, "STATE_IDLE")
|
||||||
@@ -65,62 +153,6 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||||||
if (events.contains(Player.EVENT_PLAYLIST_METADATA_CHANGED)) {
|
if (events.contains(Player.EVENT_PLAYLIST_METADATA_CHANGED)) {
|
||||||
Log.d(tag, "EVENT_PLAYLIST_METADATA_CHANGED ${playerNotificationService.getMediaPlayer()}")
|
Log.d(tag, "EVENT_PLAYLIST_METADATA_CHANGED ${playerNotificationService.getMediaPlayer()}")
|
||||||
}
|
}
|
||||||
if (events.contains(Player.EVENT_IS_PLAYING_CHANGED)) {
|
|
||||||
Log.d(tag, "EVENT IS PLAYING CHANGED ${playerNotificationService.getMediaPlayer()}")
|
|
||||||
|
|
||||||
if (player.isPlaying) {
|
|
||||||
Log.d(tag, "SeekBackTime: Player is playing")
|
|
||||||
if (lastPauseTime > 0 && DeviceManager.deviceData.deviceSettings?.disableAutoRewind != true) {
|
|
||||||
var seekBackTime = 0L
|
|
||||||
if (onSeekBack) onSeekBack = false
|
|
||||||
else {
|
|
||||||
Log.d(tag, "SeekBackTime: playing started now set seek back time $lastPauseTime")
|
|
||||||
seekBackTime = calcPauseSeekBackTime()
|
|
||||||
if (seekBackTime > 0) {
|
|
||||||
// Current chapter is used so that seek back does not go back to the previous chapter
|
|
||||||
val currentChapter = playerNotificationService.getCurrentBookChapter()
|
|
||||||
val minSeekBackTime = currentChapter?.startMs ?: 0
|
|
||||||
|
|
||||||
val currentTime = playerNotificationService.getCurrentTime()
|
|
||||||
val newTime = currentTime - seekBackTime
|
|
||||||
if (newTime < minSeekBackTime) {
|
|
||||||
seekBackTime = currentTime - minSeekBackTime
|
|
||||||
}
|
|
||||||
Log.d(tag, "SeekBackTime $seekBackTime")
|
|
||||||
onSeekBack = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(seekBackTime)
|
|
||||||
if (!shouldCarryOn) return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seekBackTime > 0L) {
|
|
||||||
playerNotificationService.seekBackward(seekBackTime)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.d(tag, "SeekBackTime: Player not playing set last pause time")
|
|
||||||
lastPauseTime = System.currentTimeMillis()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start/stop progress sync interval
|
|
||||||
if (player.isPlaying) {
|
|
||||||
player.volume = 1F // Volume on sleep timer might have decreased this
|
|
||||||
playerNotificationService.mediaProgressSyncer.start()
|
|
||||||
} else {
|
|
||||||
playerNotificationService.mediaProgressSyncer.pause {
|
|
||||||
Log.d(tag, "Media Progress Syncer paused and synced")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
playerNotificationService.clientEventEmitter?.onPlayingUpdate(player.isPlaying)
|
|
||||||
|
|
||||||
DeviceManager.widgetUpdater?.onPlayerChanged(playerNotificationService)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calcPauseSeekBackTime() : Long {
|
private fun calcPauseSeekBackTime() : Long {
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ package com.audiobookshelf.app.player
|
|||||||
import android.app.*
|
import android.app.*
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
import android.graphics.ImageDecoder
|
||||||
import android.hardware.Sensor
|
import android.hardware.Sensor
|
||||||
import android.hardware.SensorManager
|
import android.hardware.SensorManager
|
||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
@@ -11,8 +13,10 @@ import android.net.Network
|
|||||||
import android.net.NetworkCapabilities
|
import android.net.NetworkCapabilities
|
||||||
import android.net.NetworkRequest
|
import android.net.NetworkRequest
|
||||||
import android.os.*
|
import android.os.*
|
||||||
|
import android.provider.MediaStore
|
||||||
import android.support.v4.media.MediaBrowserCompat
|
import android.support.v4.media.MediaBrowserCompat
|
||||||
import android.support.v4.media.MediaDescriptionCompat
|
import android.support.v4.media.MediaDescriptionCompat
|
||||||
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
import android.support.v4.media.session.MediaControllerCompat
|
import android.support.v4.media.session.MediaControllerCompat
|
||||||
import android.support.v4.media.session.MediaSessionCompat
|
import android.support.v4.media.session.MediaSessionCompat
|
||||||
import android.support.v4.media.session.PlaybackStateCompat
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
@@ -27,9 +31,10 @@ import com.audiobookshelf.app.R
|
|||||||
import com.audiobookshelf.app.data.*
|
import com.audiobookshelf.app.data.*
|
||||||
import com.audiobookshelf.app.data.DeviceInfo
|
import com.audiobookshelf.app.data.DeviceInfo
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
|
import com.audiobookshelf.app.managers.DbManager
|
||||||
|
import com.audiobookshelf.app.managers.SleepTimerManager
|
||||||
import com.audiobookshelf.app.media.MediaManager
|
import com.audiobookshelf.app.media.MediaManager
|
||||||
import com.audiobookshelf.app.server.ApiHandler
|
import com.audiobookshelf.app.server.ApiHandler
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
|
||||||
import com.google.android.exoplayer2.*
|
import com.google.android.exoplayer2.*
|
||||||
import com.google.android.exoplayer2.audio.AudioAttributes
|
import com.google.android.exoplayer2.audio.AudioAttributes
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||||
@@ -45,6 +50,8 @@ import kotlin.concurrent.schedule
|
|||||||
|
|
||||||
|
|
||||||
const val SLEEP_TIMER_WAKE_UP_EXPIRATION = 120000L // 2m
|
const val SLEEP_TIMER_WAKE_UP_EXPIRATION = 120000L // 2m
|
||||||
|
const val PLAYER_CAST = "cast-player"
|
||||||
|
const val PLAYER_EXO = "exo-player"
|
||||||
|
|
||||||
class PlayerNotificationService : MediaBrowserServiceCompat() {
|
class PlayerNotificationService : MediaBrowserServiceCompat() {
|
||||||
|
|
||||||
@@ -55,6 +62,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
var isSwitchingPlayer = false // Used when switching between cast player and exoplayer
|
var isSwitchingPlayer = false // Used when switching between cast player and exoplayer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val tag = "PlayerNotificationService"
|
||||||
|
|
||||||
interface ClientEventEmitter {
|
interface ClientEventEmitter {
|
||||||
fun onPlaybackSession(playbackSession:PlaybackSession)
|
fun onPlaybackSession(playbackSession:PlaybackSession)
|
||||||
fun onPlaybackClosed()
|
fun onPlaybackClosed()
|
||||||
@@ -68,9 +77,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
fun onProgressSyncFailing()
|
fun onProgressSyncFailing()
|
||||||
fun onProgressSyncSuccess()
|
fun onProgressSyncSuccess()
|
||||||
fun onNetworkMeteredChanged(isUnmetered:Boolean)
|
fun onNetworkMeteredChanged(isUnmetered:Boolean)
|
||||||
|
fun onMediaItemHistoryUpdated(mediaItemHistory:MediaItemHistory)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val tag = "PlayerService"
|
|
||||||
private val binder = LocalBinder()
|
private val binder = LocalBinder()
|
||||||
|
|
||||||
var clientEventEmitter:ClientEventEmitter? = null
|
var clientEventEmitter:ClientEventEmitter? = null
|
||||||
@@ -95,7 +103,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
private var channelId = "audiobookshelf_channel"
|
private var channelId = "audiobookshelf_channel"
|
||||||
private var channelName = "Audiobookshelf Channel"
|
private var channelName = "Audiobookshelf Channel"
|
||||||
|
|
||||||
private var currentPlaybackSession:PlaybackSession? = null
|
var currentPlaybackSession:PlaybackSession? = null
|
||||||
private var initialPlaybackRate:Float? = null
|
private var initialPlaybackRate:Float? = null
|
||||||
|
|
||||||
private var isAndroidAuto = false
|
private var isAndroidAuto = false
|
||||||
@@ -133,6 +141,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
return START_STICKY
|
return START_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Deprecated in Java")
|
||||||
override fun onStart(intent: Intent?, startId: Int) {
|
override fun onStart(intent: Intent?, startId: Int) {
|
||||||
Log.d(tag, "onStart $startId")
|
Log.d(tag, "onStart $startId")
|
||||||
}
|
}
|
||||||
@@ -221,7 +230,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
.apply {
|
.apply {
|
||||||
setSessionActivity(sessionActivityPendingIntent)
|
setSessionActivity(sessionActivityPendingIntent)
|
||||||
isActive = true
|
isActive = true
|
||||||
setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val mediaController = MediaControllerCompat(ctx, mediaSession.sessionToken)
|
val mediaController = MediaControllerCompat(ctx, mediaSession.sessionToken)
|
||||||
@@ -248,6 +256,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
playerNotificationManager.setPriority(NotificationCompat.PRIORITY_MAX)
|
playerNotificationManager.setPriority(NotificationCompat.PRIORITY_MAX)
|
||||||
playerNotificationManager.setUseFastForwardActionInCompactView(true)
|
playerNotificationManager.setUseFastForwardActionInCompactView(true)
|
||||||
playerNotificationManager.setUseRewindActionInCompactView(true)
|
playerNotificationManager.setUseRewindActionInCompactView(true)
|
||||||
|
playerNotificationManager.setSmallIcon(R.drawable.exo_icon_localaudio)
|
||||||
|
|
||||||
// Unknown action
|
// Unknown action
|
||||||
playerNotificationManager.setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE)
|
playerNotificationManager.setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE)
|
||||||
@@ -256,6 +265,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
mediaSessionConnector = MediaSessionConnector(mediaSession)
|
mediaSessionConnector = MediaSessionConnector(mediaSession)
|
||||||
val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) {
|
val queueNavigator: TimelineQueueNavigator = object : TimelineQueueNavigator(mediaSession) {
|
||||||
|
override fun getSupportedQueueNavigatorActions(player: Player): Long {
|
||||||
|
return PlaybackStateCompat.ACTION_PLAY_PAUSE or PlaybackStateCompat.ACTION_PLAY or PlaybackStateCompat.ACTION_PAUSE
|
||||||
|
}
|
||||||
|
|
||||||
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
|
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
|
||||||
if (currentPlaybackSession == null) {
|
if (currentPlaybackSession == null) {
|
||||||
Log.e(tag,"Playback session is not set - returning blank MediaDescriptionCompat")
|
Log.e(tag,"Playback session is not set - returning blank MediaDescriptionCompat")
|
||||||
@@ -264,6 +277,17 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
val coverUri = currentPlaybackSession!!.getCoverUri()
|
val coverUri = currentPlaybackSession!!.getCoverUri()
|
||||||
|
|
||||||
|
var bitmap:Bitmap? = null
|
||||||
|
// Local covers get bitmap
|
||||||
|
if (currentPlaybackSession!!.localLibraryItem?.coverContentUrl != null) {
|
||||||
|
bitmap = if (Build.VERSION.SDK_INT < 28) {
|
||||||
|
MediaStore.Images.Media.getBitmap(ctx.contentResolver, coverUri)
|
||||||
|
} else {
|
||||||
|
val source: ImageDecoder.Source = ImageDecoder.createSource(ctx.contentResolver, coverUri)
|
||||||
|
ImageDecoder.decodeBitmap(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fix for local images crashing on Android 11 for specific devices
|
// Fix for local images crashing on Android 11 for specific devices
|
||||||
// https://stackoverflow.com/questions/64186578/android-11-mediastyle-notification-crash/64232958#64232958
|
// https://stackoverflow.com/questions/64186578/android-11-mediastyle-notification-crash/64232958#64232958
|
||||||
try {
|
try {
|
||||||
@@ -276,11 +300,19 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
Log.e(tag, "Grant uri permission error $error")
|
Log.e(tag, "Grant uri permission error $error")
|
||||||
}
|
}
|
||||||
|
|
||||||
return MediaDescriptionCompat.Builder()
|
val extra = Bundle()
|
||||||
.setMediaId(currentPlaybackSession!!.id)
|
extra.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, currentPlaybackSession!!.displayAuthor)
|
||||||
|
|
||||||
|
val mediaDescriptionBuilder = MediaDescriptionCompat.Builder()
|
||||||
|
.setExtras(extra)
|
||||||
.setTitle(currentPlaybackSession!!.displayTitle)
|
.setTitle(currentPlaybackSession!!.displayTitle)
|
||||||
.setSubtitle(currentPlaybackSession!!.displayAuthor)
|
.setIconUri(coverUri)
|
||||||
.setIconUri(coverUri).build()
|
|
||||||
|
bitmap?.let {
|
||||||
|
mediaDescriptionBuilder.setIconBitmap(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
return mediaDescriptionBuilder.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,11 +328,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
mediaSessionConnector.setQueueNavigator(queueNavigator)
|
mediaSessionConnector.setQueueNavigator(queueNavigator)
|
||||||
mediaSessionConnector.setPlaybackPreparer(MediaSessionPlaybackPreparer(this))
|
mediaSessionConnector.setPlaybackPreparer(MediaSessionPlaybackPreparer(this))
|
||||||
|
|
||||||
mediaSessionConnector.setCustomActionProviders(
|
|
||||||
JumpForwardCustomActionProvider(),
|
|
||||||
JumpBackwardCustomActionProvider(),
|
|
||||||
)
|
|
||||||
|
|
||||||
mediaSession.setCallback(MediaSessionCallback(this))
|
mediaSession.setCallback(MediaSessionCallback(this))
|
||||||
|
|
||||||
initializeMPlayer()
|
initializeMPlayer()
|
||||||
@@ -342,20 +369,43 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: When an item isFinished the currentTime should be reset to 0
|
||||||
|
// will reset the time if currentTime is within 5s of duration (for android auto)
|
||||||
|
Log.d(tag, "Prepare Player Session Current Time=${playbackSession.currentTime}, Duration=${playbackSession.duration}")
|
||||||
|
if (playbackSession.duration - playbackSession.currentTime < 5) {
|
||||||
|
Log.d(tag, "Prepare Player Session is finished, so restart it")
|
||||||
|
playbackSession.currentTime = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
isClosed = false
|
isClosed = false
|
||||||
|
val customActionProviders = mutableListOf(
|
||||||
|
JumpBackwardCustomActionProvider(),
|
||||||
|
JumpForwardCustomActionProvider(),
|
||||||
|
)
|
||||||
|
val metadata = playbackSession.getMediaMetadataCompat(ctx)
|
||||||
|
mediaSession.setMetadata(metadata)
|
||||||
|
val mediaItems = playbackSession.getMediaItems()
|
||||||
val playbackRateToUse = playbackRate ?: initialPlaybackRate ?: 1f
|
val playbackRateToUse = playbackRate ?: initialPlaybackRate ?: 1f
|
||||||
initialPlaybackRate = playbackRate
|
initialPlaybackRate = playbackRate
|
||||||
|
|
||||||
|
if (playbackSession.mediaPlayer != PLAYER_CAST && mediaItems.size > 1) {
|
||||||
|
customActionProviders.addAll(listOf(
|
||||||
|
SkipBackwardCustomActionProvider(),
|
||||||
|
SkipForwardCustomActionProvider(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
mediaSessionConnector.setCustomActionProviders(*customActionProviders.toTypedArray())
|
||||||
|
|
||||||
playbackSession.mediaPlayer = getMediaPlayer()
|
playbackSession.mediaPlayer = getMediaPlayer()
|
||||||
|
|
||||||
if (playbackSession.mediaPlayer == "cast-player" && playbackSession.isLocal) {
|
if (playbackSession.mediaPlayer == PLAYER_CAST && playbackSession.isLocal) {
|
||||||
Log.w(tag, "Cannot cast local media item - switching player")
|
Log.w(tag, "Cannot cast local media item - switching player")
|
||||||
currentPlaybackSession = null
|
currentPlaybackSession = null
|
||||||
switchToPlayer(false)
|
switchToPlayer(false)
|
||||||
playbackSession.mediaPlayer = getMediaPlayer()
|
playbackSession.mediaPlayer = getMediaPlayer()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playbackSession.mediaPlayer == "cast-player") {
|
if (playbackSession.mediaPlayer == PLAYER_CAST) {
|
||||||
// If cast-player is the first player to be used
|
// If cast-player is the first player to be used
|
||||||
mediaSessionConnector.setPlayer(castPlayer)
|
mediaSessionConnector.setPlayer(castPlayer)
|
||||||
playerNotificationManager.setPlayer(castPlayer)
|
playerNotificationManager.setPlayer(castPlayer)
|
||||||
@@ -366,10 +416,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
clientEventEmitter?.onPlaybackSession(playbackSession)
|
clientEventEmitter?.onPlaybackSession(playbackSession)
|
||||||
|
|
||||||
val metadata = playbackSession.getMediaMetadataCompat()
|
|
||||||
mediaSession.setMetadata(metadata)
|
|
||||||
val mediaItems = playbackSession.getMediaItems()
|
|
||||||
|
|
||||||
if (mediaItems.isEmpty()) {
|
if (mediaItems.isEmpty()) {
|
||||||
Log.e(tag, "Invalid playback session no media items to play")
|
Log.e(tag, "Invalid playback session no media items to play")
|
||||||
currentPlaybackSession = null
|
currentPlaybackSession = null
|
||||||
@@ -452,6 +498,36 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun handlePlaybackEnded() {
|
||||||
|
Log.d(tag, "handlePlaybackEnded")
|
||||||
|
if (isAndroidAuto && currentPlaybackSession?.isPodcastEpisode == true) {
|
||||||
|
Log.d(tag, "Podcast playback ended on android auto")
|
||||||
|
val libraryItem = currentPlaybackSession?.libraryItem ?: return
|
||||||
|
|
||||||
|
// Need to sync with server to set as finished
|
||||||
|
mediaProgressSyncer.finished {
|
||||||
|
// Need to reload media progress
|
||||||
|
mediaManager.loadServerUserMediaProgress {
|
||||||
|
val podcast = libraryItem.media as Podcast
|
||||||
|
val nextEpisode = podcast.getNextUnfinishedEpisode(libraryItem.id, mediaManager)
|
||||||
|
Log.d(tag, "handlePlaybackEnded nextEpisode=$nextEpisode")
|
||||||
|
nextEpisode?.let { podcastEpisode ->
|
||||||
|
mediaManager.play(libraryItem, podcastEpisode, getPlayItemRequestPayload(false)) {
|
||||||
|
if (it == null) {
|
||||||
|
Log.e(tag, "Failed to play library item")
|
||||||
|
} else {
|
||||||
|
val playbackRate = mediaManager.getSavedPlaybackRate()
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
preparePlayer(it,true, playbackRate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun startNewPlaybackSession() {
|
fun startNewPlaybackSession() {
|
||||||
currentPlaybackSession?.let { playbackSession ->
|
currentPlaybackSession?.let { playbackSession ->
|
||||||
val forceTranscode = playbackSession.isHLS // If already HLS then force
|
val forceTranscode = playbackSession.isHLS // If already HLS then force
|
||||||
@@ -564,14 +640,22 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
return currentPlaybackSession?.clone()
|
return currentPlaybackSession?.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrentPlaybackSessionId() :String? {
|
|
||||||
return currentPlaybackSession?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getCurrentBookChapter():BookChapter? {
|
fun getCurrentBookChapter():BookChapter? {
|
||||||
return currentPlaybackSession?.getChapterForTime(this.getCurrentTime())
|
return currentPlaybackSession?.getChapterForTime(this.getCurrentTime())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getEndTimeOfChapterOrTrack():Long? {
|
||||||
|
return getCurrentBookChapter()?.endMs ?: currentPlaybackSession?.getCurrentTrackEndTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getNextBookChapter():BookChapter? {
|
||||||
|
return currentPlaybackSession?.getNextChapterForTime(this.getCurrentTime())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getEndTimeOfNextChapterOrTrack():Long? {
|
||||||
|
return getNextBookChapter()?.endMs ?: currentPlaybackSession?.getNextTrackEndTime()
|
||||||
|
}
|
||||||
|
|
||||||
// Called from PlayerListener play event
|
// Called from PlayerListener play event
|
||||||
// check with server if progress has updated since last play and sync progress update
|
// check with server if progress has updated since last play and sync progress update
|
||||||
fun checkCurrentSessionProgress(seekBackTime:Long):Boolean {
|
fun checkCurrentSessionProgress(seekBackTime:Long):Boolean {
|
||||||
@@ -608,7 +692,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
seekPlayer(playbackSession.currentTimeMs)
|
seekPlayer(playbackSession.currentTimeMs)
|
||||||
// Should already be playing
|
// Should already be playing
|
||||||
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
||||||
mediaProgressSyncer.start()
|
currentPlaybackSession?.let { mediaProgressSyncer.play(it) }
|
||||||
clientEventEmitter?.onPlayingUpdate(true)
|
clientEventEmitter?.onPlayingUpdate(true)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -616,9 +700,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
if (seekBackTime > 0L) {
|
if (seekBackTime > 0L) {
|
||||||
seekBackward(seekBackTime)
|
seekBackward(seekBackTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should already be playing
|
// Should already be playing
|
||||||
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
||||||
mediaProgressSyncer.start()
|
currentPlaybackSession?.let { mediaProgressSyncer.play(it) }
|
||||||
clientEventEmitter?.onPlayingUpdate(true)
|
clientEventEmitter?.onPlayingUpdate(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -627,7 +712,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
// Streaming from server so check if playback session still exists on server
|
// Streaming from server so check if playback session still exists on server
|
||||||
Log.d(
|
Log.d(
|
||||||
tag,
|
tag,
|
||||||
"checkCurrentSessionProgress: Checking if playback session for server stream is still available"
|
"checkCurrentSessionProgress: Checking if playback session ${playbackSession.id} for server stream is still available"
|
||||||
)
|
)
|
||||||
apiHandler.getPlaybackSession(playbackSession.id) {
|
apiHandler.getPlaybackSession(playbackSession.id) {
|
||||||
if (it == null) {
|
if (it == null) {
|
||||||
@@ -648,7 +733,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
currentPlayer.volume = 1F // Volume on sleep timer might have decreased this
|
||||||
mediaProgressSyncer.start()
|
mediaProgressSyncer.play(it)
|
||||||
clientEventEmitter?.onPlayingUpdate(true)
|
clientEventEmitter?.onPlayingUpdate(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -682,14 +767,23 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun seekPlayer(time: Long) {
|
fun seekPlayer(time: Long) {
|
||||||
Log.d(tag, "seekPlayer mediaCount = ${currentPlayer.mediaItemCount} | $time")
|
var timeToSeek = time
|
||||||
|
Log.d(tag, "seekPlayer mediaCount = ${currentPlayer.mediaItemCount} | $timeToSeek")
|
||||||
|
if (timeToSeek < 0) {
|
||||||
|
Log.w(tag, "seekPlayer invalid time $timeToSeek - setting to 0")
|
||||||
|
timeToSeek = 0L
|
||||||
|
} else if (timeToSeek > getDuration()) {
|
||||||
|
Log.w(tag, "seekPlayer invalid time $timeToSeek - setting to MAX - 2000")
|
||||||
|
timeToSeek = getDuration() - 2000L
|
||||||
|
}
|
||||||
|
|
||||||
if (currentPlayer.mediaItemCount > 1) {
|
if (currentPlayer.mediaItemCount > 1) {
|
||||||
currentPlaybackSession?.currentTime = time / 1000.0
|
currentPlaybackSession?.currentTime = timeToSeek / 1000.0
|
||||||
val newWindowIndex = currentPlaybackSession?.getCurrentTrackIndex() ?: 0
|
val newWindowIndex = currentPlaybackSession?.getCurrentTrackIndex() ?: 0
|
||||||
val newTimeOffset = currentPlaybackSession?.getCurrentTrackTimeMs() ?: 0
|
val newTimeOffset = currentPlaybackSession?.getCurrentTrackTimeMs() ?: 0
|
||||||
currentPlayer.seekTo(newWindowIndex, newTimeOffset)
|
currentPlayer.seekTo(newWindowIndex, newTimeOffset)
|
||||||
} else {
|
} else {
|
||||||
currentPlayer.seekTo(time)
|
currentPlayer.seekTo(timeToSeek)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -738,10 +832,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentPlaybackSession = null
|
currentPlaybackSession = null
|
||||||
|
mediaProgressSyncer.reset()
|
||||||
clientEventEmitter?.onPlaybackClosed()
|
clientEventEmitter?.onPlaybackClosed()
|
||||||
PlayerListener.lastPauseTime = 0
|
PlayerListener.lastPauseTime = 0
|
||||||
isClosed = true
|
isClosed = true
|
||||||
stopForeground(true)
|
stopForeground(Service.STOP_FOREGROUND_REMOVE)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -751,7 +846,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getMediaPlayer():String {
|
fun getMediaPlayer():String {
|
||||||
return if(currentPlayer == castPlayer) "cast-player" else "exo-player"
|
return if(currentPlayer == castPlayer) PLAYER_CAST else PLAYER_EXO
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getDeviceInfo(): DeviceInfo {
|
fun getDeviceInfo(): DeviceInfo {
|
||||||
@@ -765,8 +860,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
return DeviceInfo(Build.MANUFACTURER, Build.MODEL, Build.BRAND, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
|
return DeviceInfo(Build.MANUFACTURER, Build.MODEL, Build.BRAND, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
@get:JsonIgnore
|
private val deviceSettings get() = DeviceManager.deviceData.deviceSettings ?: DeviceSettings.default()
|
||||||
val deviceSettings get() = DeviceManager.deviceData.deviceSettings ?: DeviceSettings.default()
|
|
||||||
|
|
||||||
fun getPlayItemRequestPayload(forceTranscode:Boolean):PlayItemRequestPayload {
|
fun getPlayItemRequestPayload(forceTranscode:Boolean):PlayItemRequestPayload {
|
||||||
return PlayItemRequestPayload(getMediaPlayer(), !forceTranscode, forceTranscode, getDeviceInfo())
|
return PlayItemRequestPayload(getMediaPlayer(), !forceTranscode, forceTranscode, getDeviceInfo())
|
||||||
@@ -787,11 +881,6 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
//
|
//
|
||||||
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
||||||
//
|
//
|
||||||
private val ANDROID_AUTO_PKG_NAME = "com.google.android.projection.gearhead"
|
|
||||||
private val ANDROID_AUTO_SIMULATOR_PKG_NAME = "com.google.android.autosimulator"
|
|
||||||
private val ANDROID_WEARABLE_PKG_NAME = "com.google.android.wearable.app"
|
|
||||||
private val ANDROID_GSEARCH_PKG_NAME = "com.google.android.googlequicksearchbox"
|
|
||||||
private val ANDROID_AUTOMOTIVE_PKG_NAME = "com.google.android.carassistant"
|
|
||||||
private val VALID_MEDIA_BROWSERS = mutableListOf("com.audiobookshelf.app", ANDROID_AUTO_PKG_NAME, ANDROID_AUTO_SIMULATOR_PKG_NAME, ANDROID_WEARABLE_PKG_NAME, ANDROID_GSEARCH_PKG_NAME, ANDROID_AUTOMOTIVE_PKG_NAME)
|
private val VALID_MEDIA_BROWSERS = mutableListOf("com.audiobookshelf.app", ANDROID_AUTO_PKG_NAME, ANDROID_AUTO_SIMULATOR_PKG_NAME, ANDROID_WEARABLE_PKG_NAME, ANDROID_GSEARCH_PKG_NAME, ANDROID_AUTOMOTIVE_PKG_NAME)
|
||||||
|
|
||||||
private val AUTO_MEDIA_ROOT = "/"
|
private val AUTO_MEDIA_ROOT = "/"
|
||||||
@@ -844,7 +933,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
if (parentMediaId.startsWith("li_") || parentMediaId.startsWith("local_")) { // Show podcast episodes
|
if (parentMediaId.startsWith("li_") || parentMediaId.startsWith("local_")) { // Show podcast episodes
|
||||||
Log.d(tag, "Loading podcast episodes")
|
Log.d(tag, "Loading podcast episodes")
|
||||||
mediaManager.loadPodcastEpisodeMediaBrowserItems(parentMediaId) {
|
mediaManager.loadPodcastEpisodeMediaBrowserItems(parentMediaId, ctx) {
|
||||||
result.sendResult(it)
|
result.sendResult(it)
|
||||||
}
|
}
|
||||||
} else if (::browseTree.isInitialized && browseTree[parentMediaId] == null && mediaManager.getIsLibrary(parentMediaId)) { // Load library items for library
|
} else if (::browseTree.isInitialized && browseTree[parentMediaId] == null && mediaManager.getIsLibrary(parentMediaId)) { // Load library items for library
|
||||||
@@ -852,11 +941,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
mediaManager.loadLibraryItemsWithAudio(parentMediaId) { libraryItems ->
|
mediaManager.loadLibraryItemsWithAudio(parentMediaId) { libraryItems ->
|
||||||
val children = libraryItems.map { libraryItem ->
|
val children = libraryItems.map { libraryItem ->
|
||||||
if (libraryItem.mediaType == "podcast") { // Podcasts are browseable
|
if (libraryItem.mediaType == "podcast") { // Podcasts are browseable
|
||||||
val mediaDescription = libraryItem.getMediaDescription(null)
|
val mediaDescription = libraryItem.getMediaDescription(null, ctx)
|
||||||
MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||||
} else {
|
} else {
|
||||||
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
|
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
|
||||||
val description = libraryItem.getMediaDescription(progress)
|
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
|
||||||
|
libraryItem.localLibraryItemId = localLibraryItem?.id
|
||||||
|
val description = libraryItem.getMediaDescription(progress, ctx)
|
||||||
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -870,13 +961,13 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
localBooks.forEach { localLibraryItem ->
|
localBooks.forEach { localLibraryItem ->
|
||||||
val progress = DeviceManager.dbManager.getLocalMediaProgress(localLibraryItem.id)
|
val progress = DeviceManager.dbManager.getLocalMediaProgress(localLibraryItem.id)
|
||||||
val description = localLibraryItem.getMediaDescription(progress)
|
val description = localLibraryItem.getMediaDescription(progress, ctx)
|
||||||
|
|
||||||
localBrowseItems += MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
localBrowseItems += MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
localPodcasts.forEach { localLibraryItem ->
|
localPodcasts.forEach { localLibraryItem ->
|
||||||
val mediaDescription = localLibraryItem.getMediaDescription(null)
|
val mediaDescription = localLibraryItem.getMediaDescription(null, ctx)
|
||||||
localBrowseItems += MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
localBrowseItems += MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -892,15 +983,26 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
progress = DeviceManager.dbManager.getLocalMediaProgress("${itemInProgress.libraryItemWrapper.id}-${itemInProgress.episode.id}")
|
progress = DeviceManager.dbManager.getLocalMediaProgress("${itemInProgress.libraryItemWrapper.id}-${itemInProgress.episode.id}")
|
||||||
} else {
|
} else {
|
||||||
progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == itemInProgress.libraryItemWrapper.id && it.episodeId == itemInProgress.episode.id }
|
progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == itemInProgress.libraryItemWrapper.id && it.episodeId == itemInProgress.episode.id }
|
||||||
|
|
||||||
|
// to show download icon
|
||||||
|
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(itemInProgress.libraryItemWrapper.id)
|
||||||
|
localLibraryItem?.let { lli ->
|
||||||
|
val localEpisode = (lli.media as Podcast).episodes?.find { it.serverEpisodeId == itemInProgress.episode.id }
|
||||||
|
itemInProgress.episode.localEpisodeId = localEpisode?.id
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
mediaDescription = itemInProgress.episode.getMediaDescription(itemInProgress.libraryItemWrapper,progress)
|
mediaDescription = itemInProgress.episode.getMediaDescription(itemInProgress.libraryItemWrapper, progress, ctx)
|
||||||
} else {
|
} else {
|
||||||
if (itemInProgress.isLocal) {
|
if (itemInProgress.isLocal) {
|
||||||
progress = DeviceManager.dbManager.getLocalMediaProgress(itemInProgress.libraryItemWrapper.id)
|
progress = DeviceManager.dbManager.getLocalMediaProgress(itemInProgress.libraryItemWrapper.id)
|
||||||
} else {
|
} else {
|
||||||
progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == itemInProgress.libraryItemWrapper.id }
|
progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == itemInProgress.libraryItemWrapper.id }
|
||||||
|
|
||||||
|
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(itemInProgress.libraryItemWrapper.id)
|
||||||
|
(itemInProgress.libraryItemWrapper as LibraryItem).localLibraryItemId = localLibraryItem?.id // To show downloaded icon
|
||||||
}
|
}
|
||||||
mediaDescription = itemInProgress.libraryItemWrapper.getMediaDescription(progress)
|
mediaDescription = itemInProgress.libraryItemWrapper.getMediaDescription(progress, ctx)
|
||||||
}
|
}
|
||||||
localBrowseItems += MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
localBrowseItems += MediaBrowserCompat.MediaItem(mediaDescription, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
|
||||||
}
|
}
|
||||||
@@ -949,7 +1051,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
// Shake sensor used for sleep timer
|
// Shake sensor used for sleep timer
|
||||||
fun registerSensor() {
|
fun registerSensor() {
|
||||||
if (isShakeSensorRegistered) {
|
if (isShakeSensorRegistered) {
|
||||||
Log.w(tag, "Shake sensor already registered")
|
Log.i(tag, "Shake sensor already registered")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
shakeSensorUnregisterTask?.cancel()
|
shakeSensorUnregisterTask?.cancel()
|
||||||
@@ -969,7 +1071,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
// Unregister shake sensor after wake up expiration
|
// Unregister shake sensor after wake up expiration
|
||||||
shakeSensorUnregisterTask?.cancel()
|
shakeSensorUnregisterTask?.cancel()
|
||||||
shakeSensorUnregisterTask = Timer("ShakeUnregisterTimer", false).schedule(SLEEP_TIMER_WAKE_UP_EXPIRATION) {
|
shakeSensorUnregisterTask = Timer("ShakeUnregisterTimer", false).schedule(SLEEP_TIMER_WAKE_UP_EXPIRATION) {
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post {
|
||||||
Log.d(tag, "wake time expired: Unregistering shake sensor")
|
Log.d(tag, "wake time expired: Unregistering shake sensor")
|
||||||
mSensorManager!!.unregisterListener(mShakeDetector)
|
mSensorManager!!.unregisterListener(mShakeDetector)
|
||||||
isShakeSensorRegistered = false
|
isShakeSensorRegistered = false
|
||||||
@@ -1024,5 +1126,39 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
).build()
|
).build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inner class SkipForwardCustomActionProvider : CustomActionProvider {
|
||||||
|
override fun onCustomAction(player: Player, action: String, extras: Bundle?) {
|
||||||
|
/*
|
||||||
|
This does not appear to ever get called. Instead, MediaSessionCallback.onCustomAction() is
|
||||||
|
responsible to reacting to a custom action.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? {
|
||||||
|
return PlaybackStateCompat.CustomAction.Builder(
|
||||||
|
CUSTOM_ACTION_SKIP_FORWARD,
|
||||||
|
getContext().getString(R.string.action_skip_forward),
|
||||||
|
R.drawable.skip_next_24
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class SkipBackwardCustomActionProvider : CustomActionProvider {
|
||||||
|
override fun onCustomAction(player: Player, action: String, extras: Bundle?) {
|
||||||
|
/*
|
||||||
|
This does not appear to ever get called. Instead, MediaSessionCallback.onCustomAction() is
|
||||||
|
responsible to reacting to a custom action.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCustomAction(player: Player): PlaybackStateCompat.CustomAction? {
|
||||||
|
return PlaybackStateCompat.CustomAction.Builder(
|
||||||
|
CUSTOM_ACTION_SKIP_BACKWARD,
|
||||||
|
getContext().getString(R.string.action_skip_backward),
|
||||||
|
R.drawable.skip_previous_24
|
||||||
|
).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.hardware.Sensor
|
|||||||
import android.hardware.SensorEvent
|
import android.hardware.SensorEvent
|
||||||
import android.hardware.SensorEventListener
|
import android.hardware.SensorEventListener
|
||||||
import android.hardware.SensorManager
|
import android.hardware.SensorManager
|
||||||
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
class ShakeDetector : SensorEventListener {
|
class ShakeDetector : SensorEventListener {
|
||||||
@@ -34,7 +35,8 @@ class ShakeDetector : SensorEventListener {
|
|||||||
val gZ = z / SensorManager.GRAVITY_EARTH
|
val gZ = z / SensorManager.GRAVITY_EARTH
|
||||||
// gForce will be close to 1 when there is no movement.
|
// gForce will be close to 1 when there is no movement.
|
||||||
val gForce: Float = sqrt(gX * gX + gY * gY + gZ * gZ)
|
val gForce: Float = sqrt(gX * gX + gY * gY + gZ * gZ)
|
||||||
if (gForce > SHAKE_THRESHOLD_GRAVITY) {
|
val shakeThreshold = DeviceManager.deviceData.deviceSettings?.getShakeThresholdGravity() ?: 1.6f
|
||||||
|
if (gForce > shakeThreshold) {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
// ignore shake events too close to each other (500ms)
|
// ignore shake events too close to each other (500ms)
|
||||||
if (mShakeTimestamp + SHAKE_SLOP_TIME_MS > now) {
|
if (mShakeTimestamp + SHAKE_SLOP_TIME_MS > now) {
|
||||||
@@ -59,7 +61,7 @@ class ShakeDetector : SensorEventListener {
|
|||||||
* from the Google Play Store and run it to see how
|
* from the Google Play Store and run it to see how
|
||||||
* many G's it takes to register a shake
|
* many G's it takes to register a shake
|
||||||
*/
|
*/
|
||||||
private const val SHAKE_THRESHOLD_GRAVITY = 1.5f // orig 2.7f
|
// var SHAKE_THRESHOLD_GRAVITY = 1.5f // orig 2.7f
|
||||||
private const val SHAKE_SLOP_TIME_MS = 500
|
private const val SHAKE_SLOP_TIME_MS = 500
|
||||||
private const val SHAKE_COUNT_RESET_TIME_MS = 3000
|
private const val SHAKE_COUNT_RESET_TIME_MS = 3000
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import android.util.Log
|
|||||||
import com.audiobookshelf.app.MainActivity
|
import com.audiobookshelf.app.MainActivity
|
||||||
import com.audiobookshelf.app.data.*
|
import com.audiobookshelf.app.data.*
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
|
import com.audiobookshelf.app.media.MediaEventManager
|
||||||
import com.audiobookshelf.app.player.CastManager
|
import com.audiobookshelf.app.player.CastManager
|
||||||
|
import com.audiobookshelf.app.player.PlayerListener
|
||||||
import com.audiobookshelf.app.player.PlayerNotificationService
|
import com.audiobookshelf.app.player.PlayerNotificationService
|
||||||
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
|
||||||
@@ -21,7 +23,7 @@ import org.json.JSONObject
|
|||||||
@CapacitorPlugin(name = "AbsAudioPlayer")
|
@CapacitorPlugin(name = "AbsAudioPlayer")
|
||||||
class AbsAudioPlayer : Plugin() {
|
class AbsAudioPlayer : Plugin() {
|
||||||
private val tag = "AbsAudioPlayer"
|
private val tag = "AbsAudioPlayer"
|
||||||
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||||
|
|
||||||
private lateinit var mainActivity: MainActivity
|
private lateinit var mainActivity: MainActivity
|
||||||
private lateinit var apiHandler:ApiHandler
|
private lateinit var apiHandler:ApiHandler
|
||||||
@@ -88,7 +90,13 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
override fun onNetworkMeteredChanged(isUnmetered:Boolean) {
|
override fun onNetworkMeteredChanged(isUnmetered:Boolean) {
|
||||||
emit("onNetworkMeteredChanged", isUnmetered)
|
emit("onNetworkMeteredChanged", isUnmetered)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onMediaItemHistoryUpdated(mediaItemHistory:MediaItemHistory) {
|
||||||
|
notifyListeners("onMediaItemHistoryUpdated", JSObject(jacksonMapper.writeValueAsString(mediaItemHistory)))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
MediaEventManager.clientEventEmitter = playerNotificationService.clientEventEmitter
|
||||||
}
|
}
|
||||||
mainActivity.pluginCallback = foregroundServiceReady
|
mainActivity.pluginCallback = foregroundServiceReady
|
||||||
}
|
}
|
||||||
@@ -160,6 +168,8 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
val episodeId = call.getString("episodeId", "").toString()
|
val episodeId = call.getString("episodeId", "").toString()
|
||||||
val playWhenReady = call.getBoolean("playWhenReady") == true
|
val playWhenReady = call.getBoolean("playWhenReady") == true
|
||||||
val playbackRate = call.getFloat("playbackRate",1f) ?: 1f
|
val playbackRate = call.getFloat("playbackRate",1f) ?: 1f
|
||||||
|
val startTimeOverride = call.getDouble("startTime")
|
||||||
|
Log.d(tag, "prepareLibraryItem lid=$libraryItemId, startTimeOverride=$startTimeOverride")
|
||||||
|
|
||||||
if (libraryItemId.isEmpty()) {
|
if (libraryItemId.isEmpty()) {
|
||||||
Log.e(tag, "Invalid call to play library item no library item id")
|
Log.e(tag, "Invalid call to play library item no library item id")
|
||||||
@@ -181,10 +191,16 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
Log.d(tag, "prepareLibraryItem: Preparing Local Media item ${jacksonMapper.writeValueAsString(it)}")
|
Log.d(tag, "prepareLibraryItem: Preparing Local Media item ${jacksonMapper.writeValueAsString(it)}")
|
||||||
val playbackSession = it.getPlaybackSession(episode)
|
val playbackSession = it.getPlaybackSession(episode)
|
||||||
|
if (startTimeOverride != null) {
|
||||||
|
Log.d(tag, "prepareLibraryItem: Using start time override $startTimeOverride")
|
||||||
|
playbackSession.currentTime = startTimeOverride
|
||||||
|
}
|
||||||
|
|
||||||
if (playerNotificationService.mediaProgressSyncer.listeningTimerRunning) { // If progress syncing then first stop before preparing next
|
if (playerNotificationService.mediaProgressSyncer.listeningTimerRunning) { // If progress syncing then first stop before preparing next
|
||||||
playerNotificationService.mediaProgressSyncer.stop {
|
playerNotificationService.mediaProgressSyncer.stop {
|
||||||
Log.d(tag, "Media progress syncer was already syncing - stopped")
|
Log.d(tag, "Media progress syncer was already syncing - stopped")
|
||||||
|
PlayerListener.lazyIsPlaying = false
|
||||||
|
|
||||||
Handler(Looper.getMainLooper()).post { // TODO: This was needed again which is probably a design a flaw
|
Handler(Looper.getMainLooper()).post { // TODO: This was needed again which is probably a design a flaw
|
||||||
playerNotificationService.preparePlayer(
|
playerNotificationService.preparePlayer(
|
||||||
playbackSession,
|
playbackSession,
|
||||||
@@ -194,6 +210,7 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
playerNotificationService.mediaProgressSyncer.reset()
|
||||||
playerNotificationService.preparePlayer(playbackSession, playWhenReady, playbackRate)
|
playerNotificationService.preparePlayer(playbackSession, playWhenReady, playbackRate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,28 +218,25 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
}
|
}
|
||||||
} else { // Play library item from server
|
} else { // Play library item from server
|
||||||
val playItemRequestPayload = playerNotificationService.getPlayItemRequestPayload(false)
|
val playItemRequestPayload = playerNotificationService.getPlayItemRequestPayload(false)
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) {
|
playerNotificationService.mediaProgressSyncer.stop {
|
||||||
if (it == null) {
|
apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) {
|
||||||
call.resolve(JSObject("{\"error\":\"Server play request failed\"}"))
|
if (it == null) {
|
||||||
} else {
|
call.resolve(JSObject("{\"error\":\"Server play request failed\"}"))
|
||||||
|
|
||||||
Handler(Looper.getMainLooper()).post {
|
|
||||||
Log.d(tag, "Preparing Player playback session ${jacksonMapper.writeValueAsString(it)}")
|
|
||||||
|
|
||||||
if (playerNotificationService.mediaProgressSyncer.listeningTimerRunning) { // If progress syncing then first stop before preparing next
|
|
||||||
playerNotificationService.mediaProgressSyncer.stop {
|
|
||||||
Log.d(tag, "Media progress syncer was already syncing - stopped")
|
|
||||||
Handler(Looper.getMainLooper()).post { // TODO: This was needed again which is probably a design a flaw
|
|
||||||
playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
|
if (startTimeOverride != null) {
|
||||||
|
Log.d(tag, "prepareLibraryItem: Using start time override $startTimeOverride")
|
||||||
|
it.currentTime = startTimeOverride
|
||||||
|
}
|
||||||
|
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
Log.d(tag, "Preparing Player playback session ${jacksonMapper.writeValueAsString(it)}")
|
||||||
|
PlayerListener.lazyIsPlaying = false
|
||||||
|
playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
|
||||||
|
}
|
||||||
|
call.resolve(JSObject(jacksonMapper.writeValueAsString(it)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
call.resolve(JSObject(jacksonMapper.writeValueAsString(it)))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -267,6 +281,7 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun seek(call: PluginCall) {
|
fun seek(call: PluginCall) {
|
||||||
val time:Int = call.getInt("value", 0) ?: 0 // Value in seconds
|
val time:Int = call.getInt("value", 0) ?: 0 // Value in seconds
|
||||||
|
Log.d(tag, "seek action to $time")
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
playerNotificationService.seekPlayer(time * 1000L) // convert to ms
|
playerNotificationService.seekPlayer(time * 1000L) // convert to ms
|
||||||
call.resolve()
|
call.resolve()
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package com.audiobookshelf.app.data
|
package com.audiobookshelf.app.plugins
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.MainActivity
|
import com.audiobookshelf.app.MainActivity
|
||||||
|
import com.audiobookshelf.app.data.*
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
|
import com.audiobookshelf.app.media.MediaEventManager
|
||||||
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
|
||||||
@@ -12,12 +14,11 @@ import com.getcapacitor.annotation.CapacitorPlugin
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.json.JSONObject
|
|
||||||
|
|
||||||
@CapacitorPlugin(name = "AbsDatabase")
|
@CapacitorPlugin(name = "AbsDatabase")
|
||||||
class AbsDatabase : Plugin() {
|
class AbsDatabase : Plugin() {
|
||||||
val tag = "AbsDatabase"
|
val tag = "AbsDatabase"
|
||||||
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||||
|
|
||||||
lateinit var mainActivity: MainActivity
|
lateinit var mainActivity: MainActivity
|
||||||
lateinit var apiHandler: ApiHandler
|
lateinit var apiHandler: ApiHandler
|
||||||
@@ -189,6 +190,24 @@ class AbsDatabase : Plugin() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun getLocalMediaProgressForServerItem(call:PluginCall) {
|
||||||
|
val libraryItemId = call.getString("libraryItemId", "").toString()
|
||||||
|
var episodeId:String? = call.getString("episodeId", "").toString()
|
||||||
|
if (episodeId == "") episodeId = null
|
||||||
|
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
val allLocalMediaProgress = DeviceManager.dbManager.getAllLocalMediaProgress()
|
||||||
|
val localMediaProgress = allLocalMediaProgress.find { libraryItemId == it.libraryItemId && (episodeId == null || it.episodeId == episodeId) }
|
||||||
|
|
||||||
|
if (localMediaProgress == null) {
|
||||||
|
call.resolve()
|
||||||
|
} else {
|
||||||
|
call.resolve(JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun removeLocalMediaProgress(call:PluginCall) {
|
fun removeLocalMediaProgress(call:PluginCall) {
|
||||||
val localMediaProgressId = call.getString("localMediaProgressId", "").toString()
|
val localMediaProgressId = call.getString("localMediaProgressId", "").toString()
|
||||||
@@ -257,6 +276,8 @@ class AbsDatabase : Plugin() {
|
|||||||
Log.w(tag, "syncServerMediaProgressWithLocalMediaProgress Local media progress not found $localMediaProgressId")
|
Log.w(tag, "syncServerMediaProgressWithLocalMediaProgress Local media progress not found $localMediaProgressId")
|
||||||
call.resolve()
|
call.resolve()
|
||||||
} else {
|
} else {
|
||||||
|
MediaEventManager.syncEvent(mediaProgress, "Received from webhook event")
|
||||||
|
|
||||||
localMediaProgress.updateFromServerMediaProgress(mediaProgress)
|
localMediaProgress.updateFromServerMediaProgress(mediaProgress)
|
||||||
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
|
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
|
||||||
call.resolve(JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
|
call.resolve(JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
|
||||||
@@ -287,8 +308,8 @@ class AbsDatabase : Plugin() {
|
|||||||
return call.resolve(JSObject("{\"error\":\"Invalid library item not a podcast\"}"))
|
return call.resolve(JSObject("{\"error\":\"Invalid library item not a podcast\"}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
var duration = 0.0
|
val duration: Double
|
||||||
var podcastEpisode:PodcastEpisode? = null
|
var podcastEpisode: PodcastEpisode? = null
|
||||||
if (!localEpisodeId.isNullOrEmpty()) {
|
if (!localEpisodeId.isNullOrEmpty()) {
|
||||||
val podcast = localLibraryItem.media as Podcast
|
val podcast = localLibraryItem.media as Podcast
|
||||||
podcastEpisode = podcast.episodes?.find { episode ->
|
podcastEpisode = podcast.episodes?.find { episode ->
|
||||||
@@ -407,4 +428,19 @@ class AbsDatabase : Plugin() {
|
|||||||
call.resolve(JSObject(jacksonMapper.writeValueAsString(DeviceManager.deviceData)))
|
call.resolve(JSObject(jacksonMapper.writeValueAsString(DeviceManager.deviceData)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun getMediaItemHistory(call:PluginCall) { // Returns device data
|
||||||
|
Log.d(tag, "getMediaItemHistory ${call.data}")
|
||||||
|
val mediaId = call.getString("mediaId") ?: ""
|
||||||
|
|
||||||
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
|
val mediaItemHistory = DeviceManager.dbManager.getMediaItemHistory(mediaId)
|
||||||
|
if (mediaItemHistory == null) {
|
||||||
|
call.resolve()
|
||||||
|
} else {
|
||||||
|
call.resolve(JSObject(jacksonMapper.writeValueAsString(mediaItemHistory)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,20 +2,17 @@ package com.audiobookshelf.app.plugins
|
|||||||
|
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import com.anggrayudi.storage.callback.FileCallback
|
|
||||||
import com.anggrayudi.storage.file.*
|
|
||||||
import com.anggrayudi.storage.media.FileDescription
|
|
||||||
import com.audiobookshelf.app.MainActivity
|
import com.audiobookshelf.app.MainActivity
|
||||||
import com.audiobookshelf.app.data.*
|
import com.audiobookshelf.app.data.*
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import com.audiobookshelf.app.device.FolderScanner
|
import com.audiobookshelf.app.device.FolderScanner
|
||||||
|
import com.audiobookshelf.app.models.DownloadItem
|
||||||
|
import com.audiobookshelf.app.models.DownloadItemPart
|
||||||
import com.audiobookshelf.app.server.ApiHandler
|
import com.audiobookshelf.app.server.ApiHandler
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
import com.audiobookshelf.app.managers.DownloadItemManager
|
||||||
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.getcapacitor.JSObject
|
import com.getcapacitor.JSObject
|
||||||
@@ -23,108 +20,37 @@ import com.getcapacitor.Plugin
|
|||||||
import com.getcapacitor.PluginCall
|
import com.getcapacitor.PluginCall
|
||||||
import com.getcapacitor.PluginMethod
|
import com.getcapacitor.PluginMethod
|
||||||
import com.getcapacitor.annotation.CapacitorPlugin
|
import com.getcapacitor.annotation.CapacitorPlugin
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.GlobalScope
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@CapacitorPlugin(name = "AbsDownloader")
|
@CapacitorPlugin(name = "AbsDownloader")
|
||||||
class AbsDownloader : Plugin() {
|
class AbsDownloader : Plugin() {
|
||||||
private val tag = "AbsDownloader"
|
private val tag = "AbsDownloader"
|
||||||
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||||
|
|
||||||
lateinit var mainActivity: MainActivity
|
lateinit var mainActivity: MainActivity
|
||||||
lateinit var downloadManager: DownloadManager
|
lateinit var downloadManager: DownloadManager
|
||||||
lateinit var apiHandler: ApiHandler
|
lateinit var apiHandler: ApiHandler
|
||||||
lateinit var folderScanner: FolderScanner
|
lateinit var folderScanner: FolderScanner
|
||||||
|
lateinit var downloadItemManager: DownloadItemManager
|
||||||
|
|
||||||
data class DownloadItemPart(
|
private val clientEventEmitter = (object : DownloadItemManager.DownloadEventEmitter {
|
||||||
val id: String,
|
override fun onDownloadItem(downloadItem:DownloadItem) {
|
||||||
val filename: String,
|
notifyListeners("onDownloadItem", JSObject(jacksonMapper.writeValueAsString(downloadItem)))
|
||||||
val finalDestinationPath:String,
|
|
||||||
val itemTitle: String,
|
|
||||||
val serverPath: String,
|
|
||||||
val localFolderName: String,
|
|
||||||
val localFolderUrl: String,
|
|
||||||
val localFolderId: String,
|
|
||||||
val audioTrack: AudioTrack?,
|
|
||||||
val episode:PodcastEpisode?,
|
|
||||||
var completed:Boolean,
|
|
||||||
var moved:Boolean,
|
|
||||||
var failed:Boolean,
|
|
||||||
@JsonIgnore val uri: Uri,
|
|
||||||
@JsonIgnore val destinationUri: Uri,
|
|
||||||
@JsonIgnore val finalDestinationUri: Uri,
|
|
||||||
var downloadId: Long?,
|
|
||||||
var progress: Long
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
fun make(filename:String, destinationFile:File, finalDestinationFile:File, itemTitle:String, serverPath:String, localFolder:LocalFolder, audioTrack:AudioTrack?, episode:PodcastEpisode?) :DownloadItemPart {
|
|
||||||
val destinationUri = Uri.fromFile(destinationFile)
|
|
||||||
val finalDestinationUri = Uri.fromFile(finalDestinationFile)
|
|
||||||
|
|
||||||
var downloadUrl = "${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}"
|
|
||||||
if (serverPath.endsWith("/cover")) downloadUrl += "&format=jpeg" // For cover images force to jpeg
|
|
||||||
val downloadUri = Uri.parse(downloadUrl)
|
|
||||||
Log.d("DownloadItemPart", "Audio File Destination Uri: $destinationUri | Final Destination Uri: $finalDestinationUri | Download URI $downloadUri")
|
|
||||||
return DownloadItemPart(
|
|
||||||
id = DeviceManager.getBase64Id(finalDestinationFile.absolutePath),
|
|
||||||
filename = filename, finalDestinationFile.absolutePath,
|
|
||||||
itemTitle = itemTitle,
|
|
||||||
serverPath = serverPath,
|
|
||||||
localFolderName = localFolder.name,
|
|
||||||
localFolderUrl = localFolder.contentUrl,
|
|
||||||
localFolderId = localFolder.id,
|
|
||||||
audioTrack = audioTrack,
|
|
||||||
episode = episode,
|
|
||||||
completed = false,
|
|
||||||
moved = false,
|
|
||||||
failed = false,
|
|
||||||
uri = downloadUri,
|
|
||||||
destinationUri = destinationUri,
|
|
||||||
finalDestinationUri = finalDestinationUri,
|
|
||||||
downloadId = null,
|
|
||||||
progress = 0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
override fun onDownloadItemPartUpdate(downloadItemPart:DownloadItemPart) {
|
||||||
@JsonIgnore
|
notifyListeners("onDownloadItemPartUpdate", JSObject(jacksonMapper.writeValueAsString(downloadItemPart)))
|
||||||
fun getDownloadRequest(): DownloadManager.Request {
|
|
||||||
val dlRequest = DownloadManager.Request(uri)
|
|
||||||
dlRequest.setTitle(filename)
|
|
||||||
dlRequest.setDescription("Downloading to $localFolderName for book $itemTitle")
|
|
||||||
dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
|
||||||
dlRequest.setDestinationUri(destinationUri)
|
|
||||||
return dlRequest
|
|
||||||
}
|
}
|
||||||
}
|
override fun onDownloadItemComplete(jsobj:JSObject) {
|
||||||
|
notifyListeners("onItemDownloadComplete", jsobj)
|
||||||
data class DownloadItem(
|
}
|
||||||
val id: String,
|
})
|
||||||
val libraryItemId:String,
|
|
||||||
val episodeId:String?,
|
|
||||||
val userMediaProgress:MediaProgress?,
|
|
||||||
val serverConnectionConfigId:String,
|
|
||||||
val serverAddress:String,
|
|
||||||
val serverUserId:String,
|
|
||||||
val mediaType: String,
|
|
||||||
val itemFolderPath:String,
|
|
||||||
val localFolder: LocalFolder,
|
|
||||||
val itemTitle: String,
|
|
||||||
val media:MediaType,
|
|
||||||
val downloadItemParts: MutableList<DownloadItemPart>
|
|
||||||
)
|
|
||||||
|
|
||||||
var downloadQueue: MutableList<DownloadItem> = mutableListOf()
|
|
||||||
|
|
||||||
override fun load() {
|
override fun load() {
|
||||||
mainActivity = (activity as MainActivity)
|
mainActivity = (activity as MainActivity)
|
||||||
downloadManager = activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
downloadManager = activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
folderScanner = FolderScanner(mainActivity)
|
folderScanner = FolderScanner(mainActivity)
|
||||||
apiHandler = ApiHandler(mainActivity)
|
apiHandler = ApiHandler(mainActivity)
|
||||||
|
downloadItemManager = DownloadItemManager(downloadManager, folderScanner, mainActivity, clientEventEmitter)
|
||||||
Log.d(tag, "Build SDK ${Build.VERSION.SDK_INT}")
|
Log.d(tag, "Build SDK ${Build.VERSION.SDK_INT}")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +63,7 @@ class AbsDownloader : Plugin() {
|
|||||||
Log.d(tag, "Download library item $libraryItemId to folder $localFolderId / episode: $episodeId")
|
Log.d(tag, "Download library item $libraryItemId to folder $localFolderId / episode: $episodeId")
|
||||||
|
|
||||||
val downloadId = if (episodeId.isEmpty()) libraryItemId else "$libraryItemId-$episodeId"
|
val downloadId = if (episodeId.isEmpty()) libraryItemId else "$libraryItemId-$episodeId"
|
||||||
if (downloadQueue.find { it.id == downloadId } != null) {
|
if (downloadItemManager.downloadItemQueue.find { it.id == downloadId } != null) {
|
||||||
Log.d(tag, "Download already started for this media entity $downloadId")
|
Log.d(tag, "Download already started for this media entity $downloadId")
|
||||||
return call.resolve(JSObject("{\"error\":\"Download already started for this media entity\"}"))
|
return call.resolve(JSObject("{\"error\":\"Download already started for this media entity\"}"))
|
||||||
}
|
}
|
||||||
@@ -205,74 +131,84 @@ class AbsDownloader : Plugin() {
|
|||||||
private fun startLibraryItemDownload(libraryItem: LibraryItem, localFolder: LocalFolder, episode:PodcastEpisode?) {
|
private fun startLibraryItemDownload(libraryItem: LibraryItem, localFolder: LocalFolder, episode:PodcastEpisode?) {
|
||||||
val tempFolderPath = mainActivity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
|
val tempFolderPath = mainActivity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
|
||||||
|
Log.d(tag, "downloadCacheDirectory=$tempFolderPath")
|
||||||
|
|
||||||
if (libraryItem.mediaType == "book") {
|
if (libraryItem.mediaType == "book") {
|
||||||
val bookTitle = cleanStringForFileSystem(libraryItem.media.metadata.title)
|
val bookTitle = cleanStringForFileSystem(libraryItem.media.metadata.title)
|
||||||
|
val bookAuthor = cleanStringForFileSystem(libraryItem.media.metadata.getAuthorDisplayName())
|
||||||
|
|
||||||
val tracks = libraryItem.media.getAudioTracks()
|
val tracks = libraryItem.media.getAudioTracks()
|
||||||
Log.d(tag, "Starting library item download with ${tracks.size} tracks")
|
Log.d(tag, "Starting library item download with ${tracks.size} tracks")
|
||||||
val itemFolderPath = localFolder.absolutePath + "/" + bookTitle
|
val itemSubfolder = "$bookAuthor/$bookTitle"
|
||||||
val downloadItem = DownloadItem(libraryItem.id, libraryItem.id, null, libraryItem.userMediaProgress,DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, libraryItem.media, mutableListOf())
|
val itemFolderPath = "${localFolder.absolutePath}/$itemSubfolder"
|
||||||
|
val downloadItem = DownloadItem(libraryItem.id, libraryItem.id, null, libraryItem.userMediaProgress,DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, itemSubfolder, libraryItem.media, mutableListOf())
|
||||||
|
|
||||||
// Create download item part for each audio track
|
// Create download item part for each audio track
|
||||||
tracks.forEach { audioTrack ->
|
tracks.forEach { audioTrack ->
|
||||||
|
val fileSize = audioTrack.metadata?.size ?: 0
|
||||||
val serverPath = "/s/item/${libraryItem.id}/${cleanRelPath(audioTrack.relPath)}"
|
val serverPath = "/s/item/${libraryItem.id}/${cleanRelPath(audioTrack.relPath)}"
|
||||||
val destinationFilename = getFilenameFromRelPath(audioTrack.relPath)
|
val destinationFilename = getFilenameFromRelPath(audioTrack.relPath)
|
||||||
Log.d(tag, "Audio File Server Path $serverPath | AF RelPath ${audioTrack.relPath} | LocalFolder Path ${localFolder.absolutePath} | DestName ${destinationFilename}")
|
Log.d(tag, "Audio File Server Path $serverPath | AF RelPath ${audioTrack.relPath} | LocalFolder Path ${localFolder.absolutePath} | DestName $destinationFilename")
|
||||||
|
|
||||||
val finalDestinationFile = File("$itemFolderPath/$destinationFilename")
|
val finalDestinationFile = File("$itemFolderPath/$destinationFilename")
|
||||||
val destinationFile = File("$tempFolderPath/$destinationFilename")
|
val destinationFile = File("$tempFolderPath/$destinationFilename")
|
||||||
|
|
||||||
|
if (destinationFile.exists()) {
|
||||||
|
Log.d(tag, "TEMP Audio file already exists, removing it from ${destinationFile.absolutePath}")
|
||||||
|
destinationFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
if (finalDestinationFile.exists()) {
|
if (finalDestinationFile.exists()) {
|
||||||
Log.d(tag, "Audio file already exists, removing it from ${finalDestinationFile.absolutePath}")
|
Log.d(tag, "Audio file already exists, removing it from ${finalDestinationFile.absolutePath}")
|
||||||
finalDestinationFile.delete()
|
finalDestinationFile.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
val downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,bookTitle,serverPath,localFolder,audioTrack,null)
|
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, fileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,audioTrack,null)
|
||||||
downloadItem.downloadItemParts.add(downloadItemPart)
|
downloadItem.downloadItemParts.add(downloadItemPart)
|
||||||
|
|
||||||
val dlRequest = downloadItemPart.getDownloadRequest()
|
|
||||||
val downloadId = downloadManager.enqueue(dlRequest)
|
|
||||||
downloadItemPart.downloadId = downloadId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (downloadItem.downloadItemParts.isNotEmpty()) {
|
if (downloadItem.downloadItemParts.isNotEmpty()) {
|
||||||
// Add cover download item
|
// Add cover download item
|
||||||
if (libraryItem.media.coverPath != null && libraryItem.media.coverPath?.isNotEmpty() == true) {
|
if (libraryItem.media.coverPath != null && libraryItem.media.coverPath?.isNotEmpty() == true) {
|
||||||
|
val coverLibraryFile = libraryItem.libraryFiles?.find { it.metadata.path == libraryItem.media.coverPath }
|
||||||
|
val coverFileSize = coverLibraryFile?.metadata?.size ?: 0
|
||||||
|
|
||||||
val serverPath = "/api/items/${libraryItem.id}/cover"
|
val serverPath = "/api/items/${libraryItem.id}/cover"
|
||||||
val destinationFilename = "cover.jpg"
|
val destinationFilename = "cover-${libraryItem.id}.jpg"
|
||||||
val destinationFile = File("$tempFolderPath/$destinationFilename")
|
val destinationFile = File("$tempFolderPath/$destinationFilename")
|
||||||
val finalDestinationFile = File("$itemFolderPath/$destinationFilename")
|
val finalDestinationFile = File("$itemFolderPath/$destinationFilename")
|
||||||
|
|
||||||
|
if (destinationFile.exists()) {
|
||||||
|
Log.d(tag, "TEMP Audio file already exists, removing it from ${destinationFile.absolutePath}")
|
||||||
|
destinationFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
if (finalDestinationFile.exists()) {
|
if (finalDestinationFile.exists()) {
|
||||||
Log.d(tag, "Cover already exists, removing it from ${finalDestinationFile.absolutePath}")
|
Log.d(tag, "Cover already exists, removing it from ${finalDestinationFile.absolutePath}")
|
||||||
finalDestinationFile.delete()
|
finalDestinationFile.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
val downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,bookTitle,serverPath,localFolder,null,null)
|
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, coverFileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,null,null)
|
||||||
downloadItem.downloadItemParts.add(downloadItemPart)
|
downloadItem.downloadItemParts.add(downloadItemPart)
|
||||||
|
|
||||||
val dlRequest = downloadItemPart.getDownloadRequest()
|
|
||||||
val downloadId = downloadManager.enqueue(dlRequest)
|
|
||||||
downloadItemPart.downloadId = downloadId
|
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadQueue.add(downloadItem)
|
downloadItemManager.addDownloadItem(downloadItem)
|
||||||
startWatchingDownloads(downloadItem)
|
|
||||||
DeviceManager.dbManager.saveDownloadItem(downloadItem)
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Podcast episode download
|
// Podcast episode download
|
||||||
val podcastTitle = cleanStringForFileSystem(libraryItem.media.metadata.title)
|
val podcastTitle = cleanStringForFileSystem(libraryItem.media.metadata.title)
|
||||||
|
|
||||||
val audioTrack = episode?.audioTrack
|
val audioTrack = episode?.audioTrack
|
||||||
|
val fileSize = audioTrack?.metadata?.size ?: 0
|
||||||
|
|
||||||
Log.d(tag, "Starting podcast episode download")
|
Log.d(tag, "Starting podcast episode download")
|
||||||
val itemFolderPath = localFolder.absolutePath + "/" + podcastTitle
|
val itemFolderPath = localFolder.absolutePath + "/" + podcastTitle
|
||||||
val downloadItemId = "${libraryItem.id}-${episode?.id}"
|
val downloadItemId = "${libraryItem.id}-${episode?.id}"
|
||||||
val downloadItem = DownloadItem(downloadItemId, libraryItem.id, episode?.id, libraryItem.userMediaProgress, DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, podcastTitle, libraryItem.media, mutableListOf())
|
val downloadItem = DownloadItem(downloadItemId, libraryItem.id, episode?.id, libraryItem.userMediaProgress, DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, podcastTitle, podcastTitle, libraryItem.media, mutableListOf())
|
||||||
|
|
||||||
var serverPath = "/s/item/${libraryItem.id}/${cleanRelPath(audioTrack?.relPath ?: "")}"
|
var serverPath = "/s/item/${libraryItem.id}/${cleanRelPath(audioTrack?.relPath ?: "")}"
|
||||||
var destinationFilename = getFilenameFromRelPath(audioTrack?.relPath ?: "")
|
var destinationFilename = getFilenameFromRelPath(audioTrack?.relPath ?: "")
|
||||||
Log.d(tag, "Audio File Server Path $serverPath | AF RelPath ${audioTrack?.relPath} | LocalFolder Path ${localFolder.absolutePath} | DestName ${destinationFilename}")
|
Log.d(tag, "Audio File Server Path $serverPath | AF RelPath ${audioTrack?.relPath} | LocalFolder Path ${localFolder.absolutePath} | DestName $destinationFilename")
|
||||||
|
|
||||||
var destinationFile = File("$tempFolderPath/$destinationFilename")
|
var destinationFile = File("$tempFolderPath/$destinationFilename")
|
||||||
var finalDestinationFile = File("$itemFolderPath/$destinationFilename")
|
var finalDestinationFile = File("$itemFolderPath/$destinationFilename")
|
||||||
@@ -281,14 +217,13 @@ class AbsDownloader : Plugin() {
|
|||||||
finalDestinationFile.delete()
|
finalDestinationFile.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
var downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,episode)
|
var downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,fileSize, destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,episode)
|
||||||
downloadItem.downloadItemParts.add(downloadItemPart)
|
downloadItem.downloadItemParts.add(downloadItemPart)
|
||||||
|
|
||||||
var dlRequest = downloadItemPart.getDownloadRequest()
|
|
||||||
var downloadId = downloadManager.enqueue(dlRequest)
|
|
||||||
downloadItemPart.downloadId = downloadId
|
|
||||||
|
|
||||||
if (libraryItem.media.coverPath != null && libraryItem.media.coverPath?.isNotEmpty() == true) {
|
if (libraryItem.media.coverPath != null && libraryItem.media.coverPath?.isNotEmpty() == true) {
|
||||||
|
val coverLibraryFile = libraryItem.libraryFiles?.find { it.metadata.path == libraryItem.media.coverPath }
|
||||||
|
val coverFileSize = coverLibraryFile?.metadata?.size ?: 0
|
||||||
|
|
||||||
serverPath = "/api/items/${libraryItem.id}/cover"
|
serverPath = "/api/items/${libraryItem.id}/cover"
|
||||||
destinationFilename = "cover.jpg"
|
destinationFilename = "cover.jpg"
|
||||||
|
|
||||||
@@ -298,134 +233,12 @@ 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,null,null)
|
downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,coverFileSize,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,null)
|
||||||
downloadItem.downloadItemParts.add(downloadItemPart)
|
downloadItem.downloadItemParts.add(downloadItemPart)
|
||||||
|
|
||||||
dlRequest = downloadItemPart.getDownloadRequest()
|
|
||||||
downloadId = downloadManager.enqueue(dlRequest)
|
|
||||||
downloadItemPart.downloadId = downloadId
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadQueue.add(downloadItem)
|
downloadItemManager.addDownloadItem(downloadItem)
|
||||||
startWatchingDownloads(downloadItem)
|
|
||||||
DeviceManager.dbManager.saveDownloadItem(downloadItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startWatchingDownloads(downloadItem: DownloadItem) {
|
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
|
||||||
while (downloadItem.downloadItemParts.find { !it.moved && !it.failed } != null) { // While some item is not completed
|
|
||||||
val numPartsBefore = downloadItem.downloadItemParts.size
|
|
||||||
checkDownloads(downloadItem)
|
|
||||||
|
|
||||||
// Keep database updated as item parts finish downloading
|
|
||||||
if (downloadItem.downloadItemParts.size > 0 && downloadItem.downloadItemParts.size != numPartsBefore) {
|
|
||||||
Log.d(tag, "Save download item on num parts changed from $numPartsBefore to ${downloadItem.downloadItemParts.size}")
|
|
||||||
DeviceManager.dbManager.saveDownloadItem(downloadItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyListeners("onItemDownloadUpdate", JSObject(jacksonMapper.writeValueAsString(downloadItem)))
|
|
||||||
delay(500)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove download notifications
|
|
||||||
downloadItem.downloadItemParts.forEach { downloadItemPart ->
|
|
||||||
downloadItemPart.downloadId?.let {
|
|
||||||
downloadManager.remove(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val downloadItemScanResult = folderScanner.scanDownloadItem(downloadItem)
|
|
||||||
DeviceManager.dbManager.removeDownloadItem(downloadItem.id)
|
|
||||||
downloadQueue.remove(downloadItem)
|
|
||||||
|
|
||||||
Log.d(tag, "Item download complete ${downloadItem.itemTitle} | local library item id: ${downloadItemScanResult?.localLibraryItem?.id} | Items remaining in Queue ${downloadQueue.size}")
|
|
||||||
|
|
||||||
val jsobj = JSObject()
|
|
||||||
jsobj.put("libraryItemId", downloadItem.id)
|
|
||||||
jsobj.put("localFolderId", downloadItem.localFolder.id)
|
|
||||||
|
|
||||||
downloadItemScanResult?.localLibraryItem?.let { localLibraryItem ->
|
|
||||||
jsobj.put("localLibraryItem", JSObject(jacksonMapper.writeValueAsString(localLibraryItem)))
|
|
||||||
}
|
|
||||||
downloadItemScanResult?.localMediaProgress?.let { localMediaProgress ->
|
|
||||||
jsobj.put("localMediaProgress", JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
|
|
||||||
}
|
|
||||||
notifyListeners("onItemDownloadComplete", jsobj)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkDownloads(downloadItem: DownloadItem) {
|
|
||||||
val itemParts = downloadItem.downloadItemParts.map { it }
|
|
||||||
for (downloadItemPart in itemParts) {
|
|
||||||
if (downloadItemPart.downloadId != null) {
|
|
||||||
val dlid = downloadItemPart.downloadId!!
|
|
||||||
val query = DownloadManager.Query().setFilterById(dlid)
|
|
||||||
downloadManager.query(query).use {
|
|
||||||
if (it.moveToFirst()) {
|
|
||||||
val bytesColumnIndex = it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
|
|
||||||
val statusColumnIndex = it.getColumnIndex(DownloadManager.COLUMN_STATUS)
|
|
||||||
val bytesDownloadedColumnIndex = it.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
|
|
||||||
|
|
||||||
val totalBytes = if (bytesColumnIndex >= 0) it.getInt(bytesColumnIndex) else 0
|
|
||||||
val downloadStatus = if (statusColumnIndex >= 0) it.getInt(statusColumnIndex) else 0
|
|
||||||
val bytesDownloadedSoFar = if (bytesDownloadedColumnIndex >= 0) it.getInt(bytesDownloadedColumnIndex) else 0
|
|
||||||
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} bytes $totalBytes | bytes dled $bytesDownloadedSoFar | downloadStatus $downloadStatus")
|
|
||||||
|
|
||||||
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL) {
|
|
||||||
// Once file download is complete move the file to the final destination
|
|
||||||
if (!downloadItemPart.completed) {
|
|
||||||
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Done")
|
|
||||||
downloadItemPart.completed = true
|
|
||||||
|
|
||||||
val file = DocumentFileCompat.fromUri(mainActivity, downloadItemPart.destinationUri)
|
|
||||||
Log.d(tag, "DOWNLOAD: Attempt move for file at destination ${downloadItemPart.destinationUri} | ${file?.getBasePath(mainActivity)}")
|
|
||||||
|
|
||||||
val fcb = object : FileCallback() {
|
|
||||||
override fun onPrepare() {
|
|
||||||
Log.d(tag, "DOWNLOAD: PREPARING MOVE FILE")
|
|
||||||
}
|
|
||||||
override fun onFailed(errorCode:ErrorCode) {
|
|
||||||
Log.e(tag, "DOWNLOAD: FAILED TO MOVE FILE $errorCode")
|
|
||||||
downloadItemPart.failed = true
|
|
||||||
file?.delete()
|
|
||||||
}
|
|
||||||
override fun onCompleted(result:Any) {
|
|
||||||
Log.d(tag, "DOWNLOAD: FILE MOVE COMPLETED")
|
|
||||||
val resultDocFile = result as DocumentFile
|
|
||||||
Log.d(tag, "DOWNLOAD: COMPLETED FILE INFO ${resultDocFile.getAbsolutePath(mainActivity)}")
|
|
||||||
downloadItemPart.moved = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(tag, "DOWNLOAD: Move file to final destination path: ${downloadItemPart.finalDestinationPath}")
|
|
||||||
val localFolderFile = DocumentFileCompat.fromUri(mainActivity,Uri.parse(downloadItemPart.localFolderUrl))
|
|
||||||
if (localFolderFile == null) {
|
|
||||||
Log.e(tag, "Local Folder File from uri is null")
|
|
||||||
} else {
|
|
||||||
val mimetype = if (downloadItemPart.audioTrack != null) MimeType.AUDIO else MimeType.IMAGE
|
|
||||||
val fileDescription = FileDescription(downloadItemPart.filename, downloadItemPart.itemTitle, mimetype)
|
|
||||||
file?.moveFileTo(mainActivity,localFolderFile,fileDescription,fcb)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Why is kotlin requiring an else here..
|
|
||||||
}
|
|
||||||
} else if (downloadStatus == DownloadManager.STATUS_FAILED) {
|
|
||||||
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Failed")
|
|
||||||
downloadItem.downloadItemParts.remove(downloadItemPart)
|
|
||||||
} else {
|
|
||||||
//update progress
|
|
||||||
val percentProgress = if (totalBytes > 0) ((bytesDownloadedSoFar * 100L) / totalBytes) else 0
|
|
||||||
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Progress = $percentProgress%")
|
|
||||||
downloadItemPart.progress = percentProgress
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.d(tag, "Download ${downloadItemPart.filename} not found in dlmanager")
|
|
||||||
downloadItem.downloadItemParts.remove(downloadItemPart)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import kotlinx.coroutines.launch
|
|||||||
class AbsFileSystem : Plugin() {
|
class AbsFileSystem : Plugin() {
|
||||||
private val TAG = "AbsFileSystem"
|
private val TAG = "AbsFileSystem"
|
||||||
private val tag = "AbsFileSystem"
|
private val tag = "AbsFileSystem"
|
||||||
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||||
|
|
||||||
lateinit var mainActivity: MainActivity
|
lateinit var mainActivity: MainActivity
|
||||||
|
|
||||||
@@ -85,21 +85,28 @@ class AbsFileSystem : Plugin() {
|
|||||||
call.resolve(JSObject(jacksonMapper.writeValueAsString(localFolder)))
|
call.resolve(JSObject(jacksonMapper.writeValueAsString(localFolder)))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStorageAccessDenied(requestCode: Int, folder: DocumentFile?, storageType: StorageType) {
|
override fun onStorageAccessDenied(
|
||||||
|
requestCode: Int,
|
||||||
|
folder: DocumentFile?,
|
||||||
|
storageType: StorageType,
|
||||||
|
storageId: String
|
||||||
|
) {
|
||||||
|
Log.e(tag, "Storage Access Denied ${folder?.getAbsolutePath(mainActivity)}")
|
||||||
|
|
||||||
val jsobj = JSObject()
|
val jsobj = JSObject()
|
||||||
if (requestCode == REQUEST_CODE_SELECT_FOLDER) {
|
if (requestCode == REQUEST_CODE_SELECT_FOLDER) {
|
||||||
|
|
||||||
val builder: AlertDialog.Builder = AlertDialog.Builder(mainActivity)
|
val builder: AlertDialog.Builder = AlertDialog.Builder(mainActivity)
|
||||||
builder.setMessage(
|
builder.setMessage(
|
||||||
"You have no write access to this storage, thus selecting this folder is useless." +
|
"You have no write access to this storage, thus selecting this folder is useless." +
|
||||||
"\nWould you like to grant access to this folder?")
|
"\nWould you like to grant access to this folder?")
|
||||||
builder.setNegativeButton("Dont Allow") { _, _ ->
|
builder.setNegativeButton("Dont Allow") { _, _ ->
|
||||||
run {
|
run {
|
||||||
jsobj.put("error", "User Canceled, Access Denied")
|
jsobj.put("error", "User Canceled, Access Denied")
|
||||||
call.resolve(jsobj)
|
call.resolve(jsobj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
builder.setPositiveButton("Allow.") { _, _ -> mainActivity.storageHelper.requestStorageAccess(REQUEST_CODE_SDCARD_ACCESS, storageType) }
|
builder.setPositiveButton("Allow.") { _, _ -> mainActivity.storageHelper.requestStorageAccess(REQUEST_CODE_SDCARD_ACCESS, initialPath = FileFullPath(mainActivity, storageId, "")) }
|
||||||
builder.show()
|
builder.show()
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "STORAGE ACCESS DENIED $requestCode")
|
Log.d(TAG, "STORAGE ACCESS DENIED $requestCode")
|
||||||
@@ -131,7 +138,7 @@ class AbsFileSystem : Plugin() {
|
|||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun checkStoragePermission(call: PluginCall) {
|
fun checkStoragePermission(call: PluginCall) {
|
||||||
var res: Boolean
|
val res: Boolean
|
||||||
if (Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.P) {
|
if (Build.VERSION.SDK_INT <= android.os.Build.VERSION_CODES.P) {
|
||||||
res = SimpleStorage.hasStoragePermission(context)
|
res = SimpleStorage.hasStoragePermission(context)
|
||||||
Log.d(TAG, "checkStoragePermission: Check Storage Access $res")
|
Log.d(TAG, "checkStoragePermission: Check Storage Access $res")
|
||||||
@@ -140,33 +147,33 @@ class AbsFileSystem : Plugin() {
|
|||||||
res = true
|
res = true
|
||||||
}
|
}
|
||||||
|
|
||||||
var jsobj = JSObject()
|
val jsobj = JSObject()
|
||||||
jsobj.put("value", res)
|
jsobj.put("value", res)
|
||||||
call.resolve(jsobj)
|
call.resolve(jsobj)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun checkFolderPermissions(call: PluginCall) {
|
fun checkFolderPermissions(call: PluginCall) {
|
||||||
var folderUrl = call.data.getString("folderUrl", "").toString()
|
val folderUrl = call.data.getString("folderUrl", "").toString()
|
||||||
Log.d(TAG, "Check Folder Permissions for $folderUrl")
|
Log.d(TAG, "Check Folder Permissions for $folderUrl")
|
||||||
|
|
||||||
var hasAccess = SimpleStorage.hasStorageAccess(context,folderUrl,true)
|
val hasAccess = SimpleStorage.hasStorageAccess(context,folderUrl,true)
|
||||||
|
|
||||||
var jsobj = JSObject()
|
val jsobj = JSObject()
|
||||||
jsobj.put("value", hasAccess)
|
jsobj.put("value", hasAccess)
|
||||||
call.resolve(jsobj)
|
call.resolve(jsobj)
|
||||||
}
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun scanFolder(call: PluginCall) {
|
fun scanFolder(call: PluginCall) {
|
||||||
var folderId = call.data.getString("folderId", "").toString()
|
val folderId = call.data.getString("folderId", "").toString()
|
||||||
var forceAudioProbe = call.data.getBoolean("forceAudioProbe")
|
val forceAudioProbe = call.data.getBoolean("forceAudioProbe")
|
||||||
Log.d(TAG, "Scan Folder $folderId | Force Audio Probe $forceAudioProbe")
|
Log.d(TAG, "Scan Folder $folderId | Force Audio Probe $forceAudioProbe")
|
||||||
|
|
||||||
var folder: LocalFolder? = DeviceManager.dbManager.getLocalFolder(folderId)
|
val folder: LocalFolder? = DeviceManager.dbManager.getLocalFolder(folderId)
|
||||||
folder?.let {
|
folder?.let {
|
||||||
var folderScanner = FolderScanner(context)
|
val folderScanner = FolderScanner(context)
|
||||||
var folderScanResult = folderScanner.scanForMediaItems(it, forceAudioProbe)
|
val folderScanResult = folderScanner.scanForMediaItems(it, forceAudioProbe)
|
||||||
if (folderScanResult == null) {
|
if (folderScanResult == null) {
|
||||||
Log.d(TAG, "NO Scan DATA")
|
Log.d(TAG, "NO Scan DATA")
|
||||||
return call.resolve(JSObject())
|
return call.resolve(JSObject())
|
||||||
@@ -179,28 +186,28 @@ class AbsFileSystem : Plugin() {
|
|||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun removeFolder(call: PluginCall) {
|
fun removeFolder(call: PluginCall) {
|
||||||
var folderId = call.data.getString("folderId", "").toString()
|
val folderId = call.data.getString("folderId", "").toString()
|
||||||
DeviceManager.dbManager.removeLocalFolder(folderId)
|
DeviceManager.dbManager.removeLocalFolder(folderId)
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun removeLocalLibraryItem(call: PluginCall) {
|
fun removeLocalLibraryItem(call: PluginCall) {
|
||||||
var localLibraryItemId = call.data.getString("localLibraryItemId", "").toString()
|
val localLibraryItemId = call.data.getString("localLibraryItemId", "").toString()
|
||||||
DeviceManager.dbManager.removeLocalLibraryItem(localLibraryItemId)
|
DeviceManager.dbManager.removeLocalLibraryItem(localLibraryItemId)
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun scanLocalLibraryItem(call: PluginCall) {
|
fun scanLocalLibraryItem(call: PluginCall) {
|
||||||
var localLibraryItemId = call.data.getString("localLibraryItemId", "").toString()
|
val localLibraryItemId = call.data.getString("localLibraryItemId", "").toString()
|
||||||
var forceAudioProbe = call.data.getBoolean("forceAudioProbe")
|
val forceAudioProbe = call.data.getBoolean("forceAudioProbe")
|
||||||
Log.d(TAG, "Scan Local library item $localLibraryItemId | Force Audio Probe $forceAudioProbe")
|
Log.d(TAG, "Scan Local library item $localLibraryItemId | Force Audio Probe $forceAudioProbe")
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
var localLibraryItem: LocalLibraryItem? = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
|
val localLibraryItem: LocalLibraryItem? = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
|
||||||
localLibraryItem?.let {
|
localLibraryItem?.let {
|
||||||
var folderScanner = FolderScanner(context)
|
val folderScanner = FolderScanner(context)
|
||||||
var scanResult = folderScanner.scanLocalLibraryItem(it, forceAudioProbe)
|
val scanResult = folderScanner.scanLocalLibraryItem(it, forceAudioProbe)
|
||||||
if (scanResult == null) {
|
if (scanResult == null) {
|
||||||
Log.d(TAG, "NO Scan DATA")
|
Log.d(TAG, "NO Scan DATA")
|
||||||
call.resolve(JSObject())
|
call.resolve(JSObject())
|
||||||
@@ -214,13 +221,13 @@ class AbsFileSystem : Plugin() {
|
|||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun deleteItem(call: PluginCall) {
|
fun deleteItem(call: PluginCall) {
|
||||||
var localLibraryItemId = call.data.getString("id", "").toString()
|
val localLibraryItemId = call.data.getString("id", "").toString()
|
||||||
var absolutePath = call.data.getString("absolutePath", "").toString()
|
val absolutePath = call.data.getString("absolutePath", "").toString()
|
||||||
var contentUrl = call.data.getString("contentUrl", "").toString()
|
val contentUrl = call.data.getString("contentUrl", "").toString()
|
||||||
Log.d(tag, "deleteItem $absolutePath | $contentUrl")
|
Log.d(tag, "deleteItem $absolutePath | $contentUrl")
|
||||||
|
|
||||||
var docfile = DocumentFileCompat.fromUri(mainActivity, Uri.parse(contentUrl))
|
val docfile = DocumentFileCompat.fromUri(mainActivity, Uri.parse(contentUrl))
|
||||||
var success = docfile?.delete() == true
|
val success = docfile?.delete() == true
|
||||||
if (success) {
|
if (success) {
|
||||||
DeviceManager.dbManager.removeLocalLibraryItem(localLibraryItemId)
|
DeviceManager.dbManager.removeLocalLibraryItem(localLibraryItemId)
|
||||||
}
|
}
|
||||||
@@ -229,19 +236,19 @@ class AbsFileSystem : Plugin() {
|
|||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun deleteTrackFromItem(call: PluginCall) {
|
fun deleteTrackFromItem(call: PluginCall) {
|
||||||
var localLibraryItemId = call.data.getString("id", "").toString()
|
val localLibraryItemId = call.data.getString("id", "").toString()
|
||||||
var trackLocalFileId = call.data.getString("trackLocalFileId", "").toString()
|
val trackLocalFileId = call.data.getString("trackLocalFileId", "").toString()
|
||||||
var contentUrl = call.data.getString("trackContentUrl", "").toString()
|
val contentUrl = call.data.getString("trackContentUrl", "").toString()
|
||||||
Log.d(tag, "deleteTrackFromItem $contentUrl")
|
Log.d(tag, "deleteTrackFromItem $contentUrl")
|
||||||
|
|
||||||
var localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
|
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
|
||||||
if (localLibraryItem == null) {
|
if (localLibraryItem == null) {
|
||||||
Log.e(tag, "deleteTrackFromItem: LLI does not exist $localLibraryItemId")
|
Log.e(tag, "deleteTrackFromItem: LLI does not exist $localLibraryItemId")
|
||||||
return call.resolve(JSObject("{\"success\":false}"))
|
return call.resolve(JSObject("{\"success\":false}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
var docfile = DocumentFileCompat.fromUri(mainActivity, Uri.parse(contentUrl))
|
val docfile = DocumentFileCompat.fromUri(mainActivity, Uri.parse(contentUrl))
|
||||||
var success = docfile?.delete() == true
|
val success = docfile?.delete() == true
|
||||||
if (success) {
|
if (success) {
|
||||||
localLibraryItem.media.removeAudioTrack(trackLocalFileId)
|
localLibraryItem.media.removeAudioTrack(trackLocalFileId)
|
||||||
localLibraryItem.removeLocalFile(trackLocalFileId)
|
localLibraryItem.removeLocalFile(trackLocalFileId)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.net.NetworkCapabilities
|
|||||||
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.media.MediaEventManager
|
||||||
import com.audiobookshelf.app.player.MediaProgressSyncData
|
import com.audiobookshelf.app.player.MediaProgressSyncData
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
import com.fasterxml.jackson.core.json.JsonReadFeature
|
import com.fasterxml.jackson.core.json.JsonReadFeature
|
||||||
@@ -31,8 +32,8 @@ class ApiHandler(var ctx:Context) {
|
|||||||
|
|
||||||
data class LocalMediaProgressSyncPayload(val localMediaProgress:List<LocalMediaProgress>)
|
data class LocalMediaProgressSyncPayload(val localMediaProgress:List<LocalMediaProgress>)
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class MediaProgressSyncResponsePayload(val numServerProgressUpdates:Int, val localProgressUpdates:List<LocalMediaProgress>)
|
data class MediaProgressSyncResponsePayload(val numServerProgressUpdates:Int, val localProgressUpdates:List<LocalMediaProgress>, val serverProgressUpdates:List<MediaProgress>)
|
||||||
data class LocalMediaProgressSyncResultsPayload(var numLocalMediaProgressForServer:Int, var numServerProgressUpdates:Int, var numLocalProgressUpdates:Int)
|
data class LocalMediaProgressSyncResultsPayload(var numLocalMediaProgressForServer:Int, var numServerProgressUpdates:Int, var numLocalProgressUpdates:Int, var serverProgressUpdates:List<MediaProgress>)
|
||||||
|
|
||||||
fun getRequest(endpoint:String, httpClient:OkHttpClient?, config:ServerConnectionConfig?, cb: (JSObject) -> Unit) {
|
fun getRequest(endpoint:String, httpClient:OkHttpClient?, config:ServerConnectionConfig?, cb: (JSObject) -> Unit) {
|
||||||
val address = config?.address ?: DeviceManager.serverAddress
|
val address = config?.address ?: DeviceManager.serverAddress
|
||||||
@@ -138,12 +139,16 @@ class ApiHandler(var ctx:Context) {
|
|||||||
val mapper = jacksonMapper
|
val mapper = jacksonMapper
|
||||||
getRequest("/api/libraries", null,null) {
|
getRequest("/api/libraries", null,null) {
|
||||||
val libraries = mutableListOf<Library>()
|
val libraries = mutableListOf<Library>()
|
||||||
if (it.has("value")) {
|
|
||||||
val array = it.getJSONArray("value")
|
var array = JSONArray()
|
||||||
for (i in 0 until array.length()) {
|
if (it.has("libraries")) { // TODO: Server 2.2.9 changed to this
|
||||||
val library = mapper.readValue<Library>(array.get(i).toString())
|
array = it.getJSONArray("libraries")
|
||||||
libraries.add(library)
|
} else if (it.has("value")) {
|
||||||
}
|
array = it.getJSONArray("value")
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i in 0 until array.length()) {
|
||||||
|
libraries.add(mapper.readValue(array.get(i).toString()))
|
||||||
}
|
}
|
||||||
cb(libraries)
|
cb(libraries)
|
||||||
}
|
}
|
||||||
@@ -222,26 +227,26 @@ class ApiHandler(var ctx:Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendProgressSync(sessionId:String, syncData: MediaProgressSyncData, cb: (Boolean) -> Unit) {
|
fun sendProgressSync(sessionId:String, syncData: MediaProgressSyncData, cb: (Boolean, String?) -> Unit) {
|
||||||
val payload = JSObject(jacksonMapper.writeValueAsString(syncData))
|
val payload = JSObject(jacksonMapper.writeValueAsString(syncData))
|
||||||
|
|
||||||
postRequest("/api/session/$sessionId/sync", payload, null) {
|
postRequest("/api/session/$sessionId/sync", payload, null) {
|
||||||
if (!it.getString("error").isNullOrEmpty()) {
|
if (!it.getString("error").isNullOrEmpty()) {
|
||||||
cb(false)
|
cb(false, it.getString("error"))
|
||||||
} else {
|
} else {
|
||||||
cb(true)
|
cb(true, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: (Boolean) -> Unit) {
|
fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: (Boolean, String?) -> Unit) {
|
||||||
val payload = JSObject(jacksonMapper.writeValueAsString(playbackSession))
|
val payload = JSObject(jacksonMapper.writeValueAsString(playbackSession))
|
||||||
|
|
||||||
postRequest("/api/session/local", payload, null) {
|
postRequest("/api/session/local", payload, null) {
|
||||||
if (!it.getString("error").isNullOrEmpty()) {
|
if (!it.getString("error").isNullOrEmpty()) {
|
||||||
cb(false)
|
cb(false, it.getString("error"))
|
||||||
} else {
|
} else {
|
||||||
cb(true)
|
cb(true, null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,7 +254,7 @@ class ApiHandler(var ctx:Context) {
|
|||||||
fun syncMediaProgress(cb: (LocalMediaProgressSyncResultsPayload) -> Unit) {
|
fun syncMediaProgress(cb: (LocalMediaProgressSyncResultsPayload) -> Unit) {
|
||||||
if (!isOnline()) {
|
if (!isOnline()) {
|
||||||
Log.d(tag, "Error not online")
|
Log.d(tag, "Error not online")
|
||||||
cb(LocalMediaProgressSyncResultsPayload(0,0,0))
|
cb(LocalMediaProgressSyncResultsPayload(0,0,0, mutableListOf()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,7 +263,7 @@ class ApiHandler(var ctx:Context) {
|
|||||||
it.serverConnectionConfigId == DeviceManager.serverConnectionConfig?.id
|
it.serverConnectionConfigId == DeviceManager.serverConnectionConfig?.id
|
||||||
}
|
}
|
||||||
|
|
||||||
val localSyncResultsPayload = LocalMediaProgressSyncResultsPayload(localMediaProgress.size,0, 0)
|
val localSyncResultsPayload = LocalMediaProgressSyncResultsPayload(localMediaProgress.size,0, 0, mutableListOf())
|
||||||
|
|
||||||
if (localMediaProgress.isNotEmpty()) {
|
if (localMediaProgress.isNotEmpty()) {
|
||||||
Log.d(tag, "Sending sync local progress request with ${localMediaProgress.size} progress items")
|
Log.d(tag, "Sending sync local progress request with ${localMediaProgress.size} progress items")
|
||||||
@@ -274,14 +279,21 @@ class ApiHandler(var ctx:Context) {
|
|||||||
val progressSyncResponsePayload = jacksonMapper.readValue<MediaProgressSyncResponsePayload>(it.toString())
|
val progressSyncResponsePayload = jacksonMapper.readValue<MediaProgressSyncResponsePayload>(it.toString())
|
||||||
|
|
||||||
localSyncResultsPayload.numLocalProgressUpdates = progressSyncResponsePayload.localProgressUpdates.size
|
localSyncResultsPayload.numLocalProgressUpdates = progressSyncResponsePayload.localProgressUpdates.size
|
||||||
|
localSyncResultsPayload.serverProgressUpdates = progressSyncResponsePayload.serverProgressUpdates
|
||||||
localSyncResultsPayload.numServerProgressUpdates = progressSyncResponsePayload.numServerProgressUpdates
|
localSyncResultsPayload.numServerProgressUpdates = progressSyncResponsePayload.numServerProgressUpdates
|
||||||
Log.d(tag, "Media Progress Sync | Local Updates: $localSyncResultsPayload")
|
Log.d(tag, "Media Progress Sync | Local Updates: $localSyncResultsPayload")
|
||||||
if (progressSyncResponsePayload.localProgressUpdates.isNotEmpty()) {
|
if (progressSyncResponsePayload.localProgressUpdates.isNotEmpty()) {
|
||||||
// Update all local media progress
|
// Update all local media progress
|
||||||
progressSyncResponsePayload.localProgressUpdates.forEach { localMediaProgress ->
|
progressSyncResponsePayload.localProgressUpdates.forEach { localMediaProgress ->
|
||||||
|
MediaEventManager.syncEvent(localMediaProgress, "Local progress updated. Received from server sync local API request")
|
||||||
|
|
||||||
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
|
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
progressSyncResponsePayload.serverProgressUpdates.forEach { localMediaProgress ->
|
||||||
|
MediaEventManager.syncEvent(localMediaProgress, "Server progress updated. Received from server sync local API request")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cb(localSyncResultsPayload)
|
cb(localSyncResultsPayload)
|
||||||
@@ -317,6 +329,7 @@ class ApiHandler(var ctx:Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getPlaybackSession(playbackSessionId:String, cb: (PlaybackSession?) -> Unit) {
|
fun getPlaybackSession(playbackSessionId:String, cb: (PlaybackSession?) -> Unit) {
|
||||||
|
Log.d(tag, "getPlaybackSession for $playbackSessionId for server ${DeviceManager.serverAddress}")
|
||||||
val endpoint = "/api/session/$playbackSessionId"
|
val endpoint = "/api/session/$playbackSessionId"
|
||||||
getRequest(endpoint, null, null) {
|
getRequest(endpoint, null, null) {
|
||||||
val err = it.getString("error")
|
val err = it.getString("error")
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<group android:scaleX="1.127451"
|
||||||
|
android:scaleY="1.127451"
|
||||||
|
android:translateX="-1.5294118"
|
||||||
|
android:translateY="-1.5294118">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<group android:scaleX="1.127451"
|
||||||
|
android:scaleY="1.127451"
|
||||||
|
android:translateX="-1.5294118"
|
||||||
|
android:translateY="-1.5294118">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M6,6h2v12L6,18zM9.5,12l8.5,6L18,6z"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M6,6h2v12L6,18zM9.5,12l8.5,6L18,6z"/>
|
||||||
|
</vector>
|
||||||
@@ -11,6 +11,8 @@
|
|||||||
<item name="windowActionBar">false</item>
|
<item name="windowActionBar">false</item>
|
||||||
<item name="windowNoTitle">true</item>
|
<item name="windowNoTitle">true</item>
|
||||||
<item name="android:background">@null</item>
|
<item name="android:background">@null</item>
|
||||||
|
<item name="android:statusBarColor">@color/background_dark</item>
|
||||||
|
<item name="android:navigationBarColor">@color/background_dark</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,4 +3,5 @@
|
|||||||
<color name="light_blue_200">#FF81D4FA</color>
|
<color name="light_blue_200">#FF81D4FA</color>
|
||||||
<color name="light_blue_600">#FF039BE5</color>
|
<color name="light_blue_600">#FF039BE5</color>
|
||||||
<color name="light_blue_900">#FF01579B</color>
|
<color name="light_blue_900">#FF01579B</color>
|
||||||
|
<color name="background_dark">#262626</color>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -8,4 +8,6 @@
|
|||||||
<string name="app_widget_description">Simple widget for audiobookshelf playback</string>
|
<string name="app_widget_description">Simple widget for audiobookshelf playback</string>
|
||||||
<string name="action_jump_forward">Jump Forward</string>
|
<string name="action_jump_forward">Jump Forward</string>
|
||||||
<string name="action_jump_backward">Jump Backward</string>
|
<string name="action_jump_backward">Jump Backward</string>
|
||||||
|
<string name="action_skip_forward">Skip Forward</string>
|
||||||
|
<string name="action_skip_backward">Skip Backward</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -9,14 +9,16 @@
|
|||||||
<item name="colorAccent">@color/colorAccent</item>
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.NoActionBar">
|
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||||
<item name="windowActionBar">false</item>
|
<item name="windowActionBar">false</item>
|
||||||
<item name="windowNoTitle">true</item>
|
<item name="windowNoTitle">true</item>
|
||||||
<item name="android:background">@null</item>
|
<item name="android:background">@null</item>
|
||||||
|
<item name="android:statusBarColor">@color/background_dark</item>
|
||||||
|
<item name="android:navigationBarColor">@color/background_dark</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar">
|
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||||
<!-- <item name="android:background">@drawable/screen</item>-->
|
<!-- <item name="android:background">@drawable/screen</item>-->
|
||||||
</style>
|
</style>
|
||||||
<style name="Widget.Android.AppWidget.Container" parent="android:Widget">
|
<style name="Widget.Android.AppWidget.Container" parent="android:Widget">
|
||||||
|
|||||||
@@ -2,5 +2,9 @@
|
|||||||
<widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
|
<widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
|
||||||
<access origin="*" />
|
<access origin="*" />
|
||||||
|
|
||||||
|
<feature name="CDVOrientation">
|
||||||
|
<param name="android-package" value="cordova.plugins.screenorientation.CDVOrientation"/>
|
||||||
|
</feature>
|
||||||
|
|
||||||
|
|
||||||
</widget>
|
</widget>
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '1.5.30'
|
ext.kotlin_version = '1.7.20'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.google.gms:google-services:4.3.10'
|
classpath 'com.google.gms:google-services:4.3.13'
|
||||||
classpath 'com.android.tools.build:gradle:7.2.2'
|
classpath 'com.android.tools.build:gradle:7.4.0'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
@@ -29,3 +29,4 @@ allprojects {
|
|||||||
task clean(type: Delete) {
|
task clean(type: Delete) {
|
||||||
delete rootProject.buildDir
|
delete rootProject.buildDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ project(':capacitor-haptics').projectDir = new File('../node_modules/@capacitor/
|
|||||||
include ':capacitor-network'
|
include ':capacitor-network'
|
||||||
project(':capacitor-network').projectDir = new File('../node_modules/@capacitor/network/android')
|
project(':capacitor-network').projectDir = new File('../node_modules/@capacitor/network/android')
|
||||||
|
|
||||||
|
include ':capacitor-preferences'
|
||||||
|
project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/android')
|
||||||
|
|
||||||
include ':capacitor-status-bar'
|
include ':capacitor-status-bar'
|
||||||
project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')
|
project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')
|
||||||
|
|
||||||
include ':capacitor-storage'
|
|
||||||
project(':capacitor-storage').projectDir = new File('../node_modules/@capacitor/storage/android')
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#Sun Apr 17 13:28:55 CDT 2022
|
#Sun Apr 17 13:28:55 CDT 2022
|
||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
ext {
|
ext {
|
||||||
minSdkVersion = 24
|
minSdkVersion = 24
|
||||||
compileSdkVersion = 31
|
compileSdkVersion = 33
|
||||||
targetSdkVersion = 30
|
targetSdkVersion = 32
|
||||||
androidxActivityVersion = '1.2.0'
|
androidxActivityVersion = '1.4.0'
|
||||||
androidxAppCompatVersion = '1.4.1'
|
androidxAppCompatVersion = '1.4.2'
|
||||||
androidxCoordinatorLayoutVersion = '1.2.0'
|
androidxCoordinatorLayoutVersion = '1.2.0'
|
||||||
androidxCoreVersion = '1.6.0'
|
androidxCoreVersion = '1.8.0'
|
||||||
androidPlayCore = '1.9.0'
|
androidPlayCore = '1.9.0'
|
||||||
androidxFragmentVersion = '1.3.0'
|
androidxFragmentVersion = '1.4.1'
|
||||||
junitVersion = '4.13.1'
|
junitVersion = '4.13.2'
|
||||||
androidxJunitVersion = '1.1.2'
|
androidxJunitVersion = '1.1.3'
|
||||||
androidxEspressoCoreVersion = '3.3.0'
|
androidxEspressoCoreVersion = '3.4.0'
|
||||||
cordovaAndroidVersion = '10.1.1'
|
cordovaAndroidVersion = '10.1.1'
|
||||||
androidx_car_version = '1.0.0-alpha7'
|
androidx_car_version = '1.0.0-alpha7'
|
||||||
androidx_core_ktx_version = '1.7.0'
|
androidx_core_ktx_version = '1.7.0'
|
||||||
@@ -27,11 +27,13 @@ ext {
|
|||||||
gradle_version = '3.1.4'
|
gradle_version = '3.1.4'
|
||||||
gson_version = '2.8.5'
|
gson_version = '2.8.5'
|
||||||
junit_version = '4.13'
|
junit_version = '4.13'
|
||||||
kotlin_version = '1.5.30'
|
kotlin_version = '1.7.20'
|
||||||
kotlin_coroutines_version = '1.1.0'
|
kotlin_coroutines_version = '1.1.0'
|
||||||
multidex_version = '1.0.3'
|
multidex_version = '1.0.3'
|
||||||
play_services_auth_version = '18.1.0'
|
play_services_auth_version = '18.1.0'
|
||||||
recycler_view_version = '1.1.0'
|
recycler_view_version = '1.1.0'
|
||||||
robolectric_version = '4.2'
|
robolectric_version = '4.2'
|
||||||
test_runner_version = '1.1.0'
|
test_runner_version = '1.1.0'
|
||||||
|
coreSplashScreenVersion = '1.0.0-rc01'
|
||||||
|
androidxWebkitVersion = '1.4.0'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'absicons';
|
||||||
|
src: url('~static/fonts/absicons/absicons.eot?2jfq33');
|
||||||
|
src: url('~static/fonts/absicons/absicons.eot?2jfq33#iefix') format('embedded-opentype'),
|
||||||
|
url('~static/fonts/absicons/absicons.ttf?2jfq33') format('truetype'),
|
||||||
|
url('~static/fonts/absicons/absicons.woff?2jfq33') format('woff'),
|
||||||
|
url('~static/fonts/absicons/absicons.svg?2jfq33#absicons') format('svg');
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.abs-icons {
|
||||||
|
/* use !important to prevent issues with browser extensions that change fonts */
|
||||||
|
font-family: 'absicons' !important;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
font-variant: normal;
|
||||||
|
text-transform: none;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
/* Better Font Rendering =========== */
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-list:before {
|
||||||
|
content: "\e907";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-home:before {
|
||||||
|
content: "\e909";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-authors:before {
|
||||||
|
content: "\e90a";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-books-1:before {
|
||||||
|
content: "\e905";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-microphone-1:before {
|
||||||
|
content: "\e902";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-radio:before {
|
||||||
|
content: "\e903";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-podcast:before {
|
||||||
|
content: "\e904";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-audiobookshelf:before {
|
||||||
|
content: "\e900";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-database:before {
|
||||||
|
content: "\e906";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-microphone-2:before {
|
||||||
|
content: "\e901";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-columns:before {
|
||||||
|
content: "\e90b";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-headphones:before {
|
||||||
|
content: "\e910";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-music:before {
|
||||||
|
content: "\e911";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-video:before {
|
||||||
|
content: "\e914";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-microphone-3:before {
|
||||||
|
content: "\e91e";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-book-1:before {
|
||||||
|
content: "\e91f";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-books-2:before {
|
||||||
|
content: "\e920";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-file-picture:before {
|
||||||
|
content: "\e927";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-database-1:before {
|
||||||
|
content: "\e964";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-rocket:before {
|
||||||
|
content: "\e9a5";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-power:before {
|
||||||
|
content: "\e9b5";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-star:before {
|
||||||
|
content: "\e9d9";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-heart:before {
|
||||||
|
content: "\e9da";
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-rss:before {
|
||||||
|
content: "\ea9b";
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
@import "./fonts.css";
|
@import "./fonts.css";
|
||||||
@import './defaultStyles.css';
|
@import './defaultStyles.css';
|
||||||
|
@import './absicons.css';
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #262626;
|
background-color: #262626;
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
font-family: 'Material Icons';
|
font-family: 'Material Icons';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: url(/fonts/MaterialIcons.woff2) format('woff2');
|
src: url(/fonts/MaterialIcons-Regular.ttf) format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Material Icons Outlined';
|
font-family: 'Material Icons Outlined';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: url(/fonts/MaterialIconsOutlined.woff2) format('woff2');
|
src: url(/fonts/MaterialIconsOutlined-Regular.otf) format('opentype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.material-icons {
|
.material-icons {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-16 bg-primary relative">
|
<div class="w-full h-16 bg-primary relative z-20">
|
||||||
<div id="appbar" class="absolute top-0 left-0 w-full h-full z-10 flex items-center px-2">
|
<div id="appbar" class="absolute top-0 left-0 w-full h-full flex items-center px-2">
|
||||||
<nuxt-link v-show="!showBack" to="/" class="mr-3">
|
<nuxt-link v-show="!showBack" to="/" class="mr-3">
|
||||||
<img src="/Logo.png" class="h-10 w-10" />
|
<img src="/Logo.png" class="h-10 w-10" />
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
</a>
|
</a>
|
||||||
<div v-if="user && currentLibrary">
|
<div v-if="user && currentLibrary">
|
||||||
<div class="pl-1.5 pr-2.5 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
|
<div class="pl-1.5 pr-2.5 py-2 bg-bg bg-opacity-30 rounded-md flex items-center" @click="clickShowLibraryModal">
|
||||||
<widgets-library-icon :icon="currentLibraryIcon" :size="4" />
|
<ui-library-icon :icon="currentLibraryIcon" :size="4" font-size="base" />
|
||||||
<p class="text-sm font-book leading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
|
<p class="text-sm font-book leading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -21,12 +21,12 @@
|
|||||||
<widgets-download-progress-indicator />
|
<widgets-download-progress-indicator />
|
||||||
|
|
||||||
<!-- Must be connected to a server to cast, only supports media items on server -->
|
<!-- Must be connected to a server to cast, only supports media items on server -->
|
||||||
<div v-show="isCastAvailable && user" class="mx-2 cursor-pointer mt-1.5">
|
<div v-show="isCastAvailable && user" class="mx-2 cursor-pointer flex items-center pt-0.5" @click="castClick">
|
||||||
<span class="material-icons text-2xl" :class="isCasting ? 'text-success' : ''" @click="castClick">cast</span>
|
<span class="material-icons" :class="isCasting ? 'text-success' : ''">cast</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nuxt-link v-if="user" class="h-7 mx-1.5" to="/search">
|
<nuxt-link v-if="user" class="h-7 mx-1.5" style="padding-top: 3px" to="/search">
|
||||||
<span class="material-icons" style="font-size: 1.75rem">search</span>
|
<span class="material-icons">search</span>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
|
|
||||||
<div class="h-7 mx-1.5">
|
<div class="h-7 mx-1.5">
|
||||||
|
|||||||
@@ -1,19 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<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="playbackSession" id="streamContainer" class="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 absolute top-0 left-0 pointer-events-auto" :style="{ backgroundColor: coverRgb }">
|
||||||
<div class="top-2 left-4 absolute cursor-pointer">
|
<div class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: linear-gradient(169deg, rgba(0, 0, 0, 0) 0%, rgba(38, 38, 38, 1) 80%)" />
|
||||||
<span class="material-icons text-5xl" @click="collapseFullscreen">expand_more</span>
|
|
||||||
|
<div class="top-4 left-4 absolute cursor-pointer">
|
||||||
|
<span class="material-icons text-5xl" :class="{ 'text-black text-opacity-75': coverBgIsLight }" @click="collapseFullscreen">expand_more</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="showCastBtn" class="top-4 right-16 absolute cursor-pointer">
|
<div v-show="showCastBtn" class="top-6 right-16 absolute cursor-pointer">
|
||||||
<span class="material-icons text-3xl" :class="isCasting ? 'text-success' : ''" @click="castClick">cast</span>
|
<span class="material-icons text-3xl" :class="isCasting ? (coverBgIsLight ? 'text-successDark' : 'text-success') : coverBgIsLight ? 'text-black' : ''" @click="castClick">cast</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="top-4 right-4 absolute cursor-pointer">
|
<div class="top-6 right-4 absolute cursor-pointer">
|
||||||
<span class="material-icons text-3xl" @click="showMoreMenuDialog = true">more_vert</span>
|
<span class="material-icons text-3xl" :class="{ 'text-black text-opacity-75': coverBgIsLight }" @click="showMoreMenuDialog = true">more_vert</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="top-2 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75" style="font-size: 10px" :class="{ 'text-success': isLocalPlayMethod, 'text-accent': !isLocalPlayMethod }">{{ isDirectPlayMethod ? 'Direct' : isLocalPlayMethod ? 'Local' : 'Transcode' }}</p>
|
<p class="top-4 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75" style="font-size: 10px" :class="{ 'text-success': isLocalPlayMethod, 'text-accent': !isLocalPlayMethod }">{{ isDirectPlayMethod ? 'Direct' : isLocalPlayMethod ? 'Local' : 'Transcode' }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="useChapterTrack && showFullscreen" class="absolute total-track w-full px-3 z-30">
|
<div v-if="useChapterTrack && useTotalTrack && showFullscreen" class="absolute total-track w-full px-3 z-30">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ currentTimePretty }}</p>
|
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ currentTimePretty }}</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
@@ -29,8 +31,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cover-wrapper absolute z-30 pointer-events-auto" @click="clickContainer">
|
<div class="cover-wrapper absolute z-30 pointer-events-auto" @click="clickContainer">
|
||||||
<div class="cover-container bookCoverWrapper bg-black bg-opacity-75 w-full h-full">
|
<div class="w-full h-full flex justify-center">
|
||||||
<covers-book-cover v-if="libraryItem || localLibraryItemCoverSrc" :library-item="libraryItem" :download-cover="localLibraryItemCoverSrc" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" />
|
<covers-book-cover v-if="libraryItem || localLibraryItemCoverSrc" ref="cover" :library-item="libraryItem" :download-cover="localLibraryItemCoverSrc" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" @imageLoaded="coverImageLoaded" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="syncStatus === $constants.SyncStatus.FAILED" class="absolute top-0 left-0 w-full h-full flex items-center justify-center z-30">
|
<div v-if="syncStatus === $constants.SyncStatus.FAILED" class="absolute top-0 left-0 w-full h-full flex items-center justify-center z-30">
|
||||||
@@ -39,14 +41,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden" @click="clickTitleAndAuthor">
|
<div class="title-author-texts absolute z-30 left-0 right-0 overflow-hidden" @click="clickTitleAndAuthor">
|
||||||
<p class="title-text font-book truncate">{{ title }}</p>
|
<p class="title-text truncate">{{ title }}</p>
|
||||||
<p class="author-text text-white text-opacity-75 truncate">by {{ authorName }}</p>
|
<p class="author-text text-white text-opacity-75 truncate">{{ authorName }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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 id="playerContent" class="playerContainer w-full z-20 absolute bottom-0 left-0 right-0 p-2 pointer-events-auto transition-all" :style="{ backgroundColor: showFullscreen ? '' : coverRgb }" @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 bottom-4 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 && isServerItem && networkConnected" class="material-icons text-3xl text-white text-opacity-75 cursor-pointer" @click="$emit('showBookmarks')">{{ bookmarks.length ? 'bookmark' : 'bookmark_border' }}</span>
|
||||||
<!-- hidden for podcasts but still using this as a placeholder -->
|
<!-- hidden for podcasts but still using this as a placeholder -->
|
||||||
<span v-else class="material-icons text-3xl text-white text-opacity-0">bookmark</span>
|
<span v-else class="material-icons text-3xl text-white text-opacity-0">bookmark</span>
|
||||||
|
|
||||||
@@ -61,13 +63,16 @@
|
|||||||
<span class="material-icons text-3xl text-white cursor-pointer" :class="chapters.length ? 'text-opacity-75' : 'text-opacity-10'" @click="showChapterModal = true">format_list_bulleted</span>
|
<span class="material-icons text-3xl text-white cursor-pointer" :class="chapters.length ? 'text-opacity-75' : 'text-opacity-10'" @click="showChapterModal = true">format_list_bulleted</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="w-full h-full absolute top-0 left-0 pointer-events-none" style="background: linear-gradient(145deg, rgba(38, 38, 38, 0.5) 0%, rgba(38, 38, 38, 0.9) 20%, rgb(38, 38, 38) 60%)" />
|
||||||
|
|
||||||
<div id="playerControls" class="absolute right-0 bottom-0 py-2">
|
<div id="playerControls" class="absolute right-0 bottom-0 py-2">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-center">
|
||||||
<span v-show="showFullscreen && !lockUi" class="material-icons next-icon text-white text-opacity-75 cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
|
<span v-show="showFullscreen && !lockUi" class="material-icons next-icon text-white text-opacity-75 cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpChapterStart">first_page</span>
|
||||||
<span v-show="!lockUi" class="material-icons jump-icon text-white cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpBackwards">{{ jumpBackwardsIcon }}</span>
|
<span v-show="!lockUi" class="material-icons jump-icon text-white cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpBackwards">{{ jumpBackwardsIcon }}</span>
|
||||||
<div class="play-btn cursor-pointer shadow-sm flex items-center justify-center rounded-full text-primary mx-4" :class="{ 'animate-spin': seekLoading, 'bg-accent': !isLocalPlayMethod, 'bg-success': isLocalPlayMethod }" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
|
<div class="play-btn cursor-pointer shadow-sm flex items-center justify-center rounded-full text-primary mx-4 relative overflow-hidden" :style="{ backgroundColor: coverRgb }" :class="{ 'animate-spin': seekLoading }" @mousedown.prevent @mouseup.prevent @click.stop="playPauseClick">
|
||||||
<span v-if="!isLoading" class="material-icons">{{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}</span>
|
<div v-if="!coverBgIsLight" class="absolute top-0 left-0 w-full h-full bg-white bg-opacity-20 pointer-events-none" />
|
||||||
|
|
||||||
|
<span v-if="!isLoading" class="material-icons" :class="{ 'text-white': coverRgb && !coverBgIsLight }">{{ seekLoading ? 'autorenew' : !isPlaying ? 'play_arrow' : 'pause' }}</span>
|
||||||
<widgets-spinner-icon v-else class="h-8 w-8" />
|
<widgets-spinner-icon v-else class="h-8 w-8" />
|
||||||
</div>
|
</div>
|
||||||
<span v-show="!lockUi" class="material-icons jump-icon text-white cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpForward">{{ jumpForwardIcon }}</span>
|
<span v-show="!lockUi" class="material-icons jump-icon text-white cursor-pointer" :class="isLoading ? 'text-opacity-10' : 'text-opacity-75'" @click.stop="jumpForward">{{ jumpForwardIcon }}</span>
|
||||||
@@ -75,49 +80,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="playerTrack" class="absolute bottom-0 left-0 w-full px-3">
|
<div id="playerTrack" class="absolute left-0 w-full px-3">
|
||||||
<div ref="track" class="h-1.5 w-full bg-gray-500 bg-opacity-50 relative" :class="{ 'animate-pulse': isLoading }" @touchstart="touchstartTrack" @click="clickTrack">
|
<div class="flex pb-0.5">
|
||||||
<div ref="readyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none" />
|
|
||||||
<div ref="bufferedTrack" class="h-full bg-gray-500 absolute top-0 left-0 pointer-events-none" />
|
|
||||||
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
|
|
||||||
<div ref="draggingTrack" class="h-full bg-warning bg-opacity-25 absolute top-0 left-0 pointer-events-none" />
|
|
||||||
<div ref="trackCursor" class="h-3.5 w-3.5 rounded-full bg-gray-200 absolute -top-1 pointer-events-none" :class="{ 'opacity-0': lockUi }" />
|
|
||||||
</div>
|
|
||||||
<div id="timestamp-row" class="flex pt-0.5">
|
|
||||||
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem" ref="currentTimestamp">0:00</p>
|
<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>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ timeRemainingPretty }}</p>
|
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ timeRemainingPretty }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div ref="track" class="h-1.5 w-full bg-gray-500 bg-opacity-50 relative" :class="{ 'animate-pulse': isLoading }" @touchstart="touchstartTrack" @click="clickTrack">
|
||||||
|
<div ref="readyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none" />
|
||||||
|
<div ref="bufferedTrack" class="h-full bg-gray-500 absolute top-0 left-0 pointer-events-none" />
|
||||||
|
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
|
||||||
|
<div ref="draggingTrack" class="h-full bg-warning bg-opacity-25 absolute top-0 left-0 pointer-events-none" />
|
||||||
|
<div ref="trackCursor" class="h-3.5 w-3.5 rounded-full bg-gray-200 absolute -top-1 pointer-events-none" :class="{ 'opacity-0': lockUi || !showFullscreen }" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" @select="selectChapter" />
|
<modals-chapters-modal v-model="showChapterModal" :current-chapter="currentChapter" :chapters="chapters" @select="selectChapter" />
|
||||||
<modals-dialog v-model="showMoreMenuDialog" :items="menuItems" @action="clickMenuAction">
|
<modals-dialog v-model="showMoreMenuDialog" :items="menuItems" @action="clickMenuAction" />
|
||||||
<template v-slot:chapter_track="{ item }">
|
|
||||||
<li class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="clickMenuAction(item.value)">
|
|
||||||
<div class="flex items-center px-3">
|
|
||||||
<span v-if="item.icon" class="material-icons-outlined text-xl mr-2 text-white text-opacity-80">{{ item.icon }}</span>
|
|
||||||
<span class="font-normal block truncate text-base text-white text-opacity-80">{{ item.text }}</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
<template v-slot:lock="{ item }">
|
|
||||||
<li class="text-gray-50 select-none relative py-4 cursor-pointer hover:bg-black-400" role="option" @click="clickMenuAction(item.value)">
|
|
||||||
<div class="flex items-center px-3">
|
|
||||||
<span v-if="item.icon" class="material-icons-outlined text-xl mr-2 text-opacity-80" :class="{ 'text-red-500': lockUi, 'text-white': !lockUi }">{{ item.icon }}</span>
|
|
||||||
<span class="font-normal block truncate text-base text-white text-opacity-80">{{ item.text }}</span>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
</modals-dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Capacitor } from '@capacitor/core'
|
import { Capacitor } from '@capacitor/core'
|
||||||
import { AbsAudioPlayer } from '@/plugins/capacitor'
|
import { AbsAudioPlayer } from '@/plugins/capacitor'
|
||||||
|
import { FastAverageColor } from 'fast-average-color'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
@@ -126,11 +115,13 @@ export default {
|
|||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
sleepTimerRunning: Boolean,
|
sleepTimerRunning: Boolean,
|
||||||
sleepTimeRemaining: Number
|
sleepTimeRemaining: Number,
|
||||||
|
isServerItem: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
windowHeight: 0,
|
windowHeight: 0,
|
||||||
|
windowWidth: 0,
|
||||||
playbackSession: null,
|
playbackSession: null,
|
||||||
showChapterModal: false,
|
showChapterModal: false,
|
||||||
showFullscreen: false,
|
showFullscreen: false,
|
||||||
@@ -156,39 +147,62 @@ export default {
|
|||||||
touchStartTime: 0,
|
touchStartTime: 0,
|
||||||
touchEndY: 0,
|
touchEndY: 0,
|
||||||
useChapterTrack: false,
|
useChapterTrack: false,
|
||||||
|
useTotalTrack: true,
|
||||||
lockUi: false,
|
lockUi: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
touchTrackStart: false,
|
touchTrackStart: false,
|
||||||
dragPercent: 0,
|
dragPercent: 0,
|
||||||
syncStatus: 0,
|
syncStatus: 0,
|
||||||
showMoreMenuDialog: false
|
showMoreMenuDialog: false,
|
||||||
|
coverRgb: 'rgb(55, 56, 56)',
|
||||||
|
coverBgIsLight: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
showFullscreen(val) {
|
showFullscreen(val) {
|
||||||
this.updateScreenSize()
|
this.updateScreenSize()
|
||||||
this.$store.commit('setPlayerFullscreen', !!val)
|
this.$store.commit('setPlayerFullscreen', !!val)
|
||||||
|
},
|
||||||
|
bookCoverAspectRatio() {
|
||||||
|
this.updateScreenSize()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
menuItems() {
|
menuItems() {
|
||||||
var items = [
|
const items = []
|
||||||
{
|
// TODO: Implement on iOS
|
||||||
text: 'Chapter Track',
|
if (this.$platform !== 'ios' && !this.isPodcast && this.mediaId) {
|
||||||
value: 'chapter_track',
|
items.push({
|
||||||
icon: this.useChapterTrack ? 'check_box' : 'check_box_outline_blank'
|
text: 'History',
|
||||||
},
|
value: 'history',
|
||||||
{
|
icon: 'history'
|
||||||
text: this.lockUi ? 'Unlock Player' : 'Lock Player',
|
})
|
||||||
value: 'lock',
|
}
|
||||||
icon: this.lockUi ? 'lock' : 'lock_open'
|
|
||||||
},
|
items.push(
|
||||||
{
|
...[
|
||||||
text: 'Close Player',
|
{
|
||||||
value: 'close',
|
text: 'Total Track',
|
||||||
icon: 'close'
|
value: 'total_track',
|
||||||
}
|
icon: this.useTotalTrack ? 'check_box' : 'check_box_outline_blank'
|
||||||
]
|
},
|
||||||
|
{
|
||||||
|
text: 'Chapter Track',
|
||||||
|
value: 'chapter_track',
|
||||||
|
icon: this.useChapterTrack ? 'check_box' : 'check_box_outline_blank'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: this.lockUi ? 'Unlock Player' : 'Lock Player',
|
||||||
|
value: 'lock',
|
||||||
|
icon: this.lockUi ? 'lock' : 'lock_open'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Close Player',
|
||||||
|
value: 'close',
|
||||||
|
icon: 'close'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
return items
|
return items
|
||||||
},
|
},
|
||||||
@@ -205,18 +219,33 @@ export default {
|
|||||||
return this.$store.getters['getJumpBackwardsTime']
|
return this.$store.getters['getJumpBackwardsTime']
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
bookCoverWidth() {
|
bookCoverWidth() {
|
||||||
if (this.showFullscreen) return this.fullscreenBookCoverWidth
|
if (this.showFullscreen) return this.fullscreenBookCoverWidth
|
||||||
return 60
|
return 46 / this.bookCoverAspectRatio
|
||||||
},
|
},
|
||||||
fullscreenBookCoverWidth() {
|
fullscreenBookCoverWidth() {
|
||||||
var heightScale = (this.windowHeight - 200) / 651
|
if (this.windowWidth < this.windowHeight) {
|
||||||
if (this.bookCoverAspectRatio === 1) {
|
// Portrait
|
||||||
return 260 * heightScale
|
let sideSpace = 20
|
||||||
|
if (this.bookCoverAspectRatio === 1.6) sideSpace += (this.windowWidth - sideSpace) * 0.375
|
||||||
|
|
||||||
|
const availableHeight = this.windowHeight - 400
|
||||||
|
let width = this.windowWidth - sideSpace
|
||||||
|
const totalHeight = width * this.bookCoverAspectRatio
|
||||||
|
if (totalHeight > availableHeight) {
|
||||||
|
width = availableHeight / this.bookCoverAspectRatio
|
||||||
|
}
|
||||||
|
return width
|
||||||
|
} else {
|
||||||
|
// Landscape
|
||||||
|
const heightScale = (this.windowHeight - 200) / 651
|
||||||
|
if (this.bookCoverAspectRatio === 1) {
|
||||||
|
return 260 * heightScale
|
||||||
|
}
|
||||||
|
return 190 * heightScale
|
||||||
}
|
}
|
||||||
return 200 * heightScale
|
|
||||||
},
|
},
|
||||||
showCastBtn() {
|
showCastBtn() {
|
||||||
return this.$store.state.isCastAvailable
|
return this.$store.state.isCastAvailable
|
||||||
@@ -324,9 +353,36 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
return secondsRemaining + 's'
|
return secondsRemaining + 's'
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
networkConnected() {
|
||||||
|
return this.$store.state.networkConnected
|
||||||
|
},
|
||||||
|
mediaId() {
|
||||||
|
if (this.isPodcast || !this.playbackSession) return null
|
||||||
|
if (this.playbackSession.libraryItemId) {
|
||||||
|
return this.playbackSession.episodeId ? `${this.playbackSession.libraryItemId}-${this.playbackSession.episodeId}` : this.playbackSession.libraryItemId
|
||||||
|
}
|
||||||
|
const localLibraryItem = this.playbackSession.localLibraryItem
|
||||||
|
if (!localLibraryItem) return null
|
||||||
|
|
||||||
|
return this.playbackSession.localEpisodeId ? `${localLibraryItem.id}-${this.playbackSession.localEpisodeId}` : localLibraryItem.id
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
async coverImageLoaded(fullCoverUrl) {
|
||||||
|
if (!fullCoverUrl) return
|
||||||
|
|
||||||
|
const fac = new FastAverageColor()
|
||||||
|
fac
|
||||||
|
.getColorAsync(fullCoverUrl)
|
||||||
|
.then((color) => {
|
||||||
|
this.coverRgb = color.rgba
|
||||||
|
this.coverBgIsLight = color.isLight
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
})
|
||||||
|
},
|
||||||
clickTitleAndAuthor() {
|
clickTitleAndAuthor() {
|
||||||
if (!this.showFullscreen) return
|
if (!this.showFullscreen) return
|
||||||
const llid = this.libraryItem ? this.libraryItem.id : this.localLibraryItem ? this.localLibraryItem.id : null
|
const llid = this.libraryItem ? this.libraryItem.id : this.localLibraryItem ? this.localLibraryItem.id : null
|
||||||
@@ -335,15 +391,18 @@ export default {
|
|||||||
this.showFullscreen = false
|
this.showFullscreen = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
touchstartTrack(e) {
|
async touchstartTrack(e) {
|
||||||
|
await this.$hapticsImpact()
|
||||||
if (!e || !e.touches || !this.$refs.track || !this.showFullscreen || this.lockUi) return
|
if (!e || !e.touches || !this.$refs.track || !this.showFullscreen || this.lockUi) return
|
||||||
this.touchTrackStart = true
|
this.touchTrackStart = true
|
||||||
},
|
},
|
||||||
selectChapter(chapter) {
|
async selectChapter(chapter) {
|
||||||
|
await this.$hapticsImpact()
|
||||||
this.seek(chapter.start)
|
this.seek(chapter.start)
|
||||||
this.showChapterModal = false
|
this.showChapterModal = false
|
||||||
},
|
},
|
||||||
castClick() {
|
async castClick() {
|
||||||
|
await this.$hapticsImpact()
|
||||||
if (this.isLocalPlayMethod) {
|
if (this.isLocalPlayMethod) {
|
||||||
this.$eventBus.$emit('cast-local-item')
|
this.$eventBus.$emit('cast-local-item')
|
||||||
return
|
return
|
||||||
@@ -362,12 +421,14 @@ export default {
|
|||||||
this.showFullscreen = false
|
this.showFullscreen = false
|
||||||
this.forceCloseDropdownMenu()
|
this.forceCloseDropdownMenu()
|
||||||
},
|
},
|
||||||
jumpNextChapter() {
|
async jumpNextChapter() {
|
||||||
|
await this.$hapticsImpact()
|
||||||
if (this.isLoading) return
|
if (this.isLoading) return
|
||||||
if (!this.nextChapter) return
|
if (!this.nextChapter) return
|
||||||
this.seek(this.nextChapter.start)
|
this.seek(this.nextChapter.start)
|
||||||
},
|
},
|
||||||
jumpChapterStart() {
|
async jumpChapterStart() {
|
||||||
|
await this.$hapticsImpact()
|
||||||
if (this.isLoading) return
|
if (this.isLoading) return
|
||||||
if (!this.currentChapter) {
|
if (!this.currentChapter) {
|
||||||
return this.restart()
|
return this.restart()
|
||||||
@@ -375,9 +436,9 @@ export default {
|
|||||||
|
|
||||||
// If 4 seconds or less into current chapter, then go to previous
|
// If 4 seconds or less into current chapter, then go to previous
|
||||||
if (this.currentTime - this.currentChapter.start <= 4) {
|
if (this.currentTime - this.currentChapter.start <= 4) {
|
||||||
var currChapterIndex = this.chapters.findIndex((ch) => Number(ch.start) <= this.currentTime && Number(ch.end) >= this.currentTime)
|
const currChapterIndex = this.chapters.findIndex((ch) => Number(ch.start) <= this.currentTime && Number(ch.end) >= this.currentTime)
|
||||||
if (currChapterIndex > 0) {
|
if (currChapterIndex > 0) {
|
||||||
var prevChapter = this.chapters[currChapterIndex - 1]
|
const prevChapter = this.chapters[currChapterIndex - 1]
|
||||||
this.seek(prevChapter.start)
|
this.seek(prevChapter.start)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -387,7 +448,8 @@ export default {
|
|||||||
showSleepTimerModal() {
|
showSleepTimerModal() {
|
||||||
this.$emit('showSleepTimer')
|
this.$emit('showSleepTimer')
|
||||||
},
|
},
|
||||||
setPlaybackSpeed(speed) {
|
async setPlaybackSpeed(speed) {
|
||||||
|
await this.$hapticsImpact()
|
||||||
console.log(`[AudioPlayer] Set Playback Rate: ${speed}`)
|
console.log(`[AudioPlayer] Set Playback Rate: ${speed}`)
|
||||||
this.currentPlaybackRate = speed
|
this.currentPlaybackRate = speed
|
||||||
AbsAudioPlayer.setPlaybackSpeed({ value: speed })
|
AbsAudioPlayer.setPlaybackSpeed({ value: speed })
|
||||||
@@ -395,11 +457,13 @@ export default {
|
|||||||
restart() {
|
restart() {
|
||||||
this.seek(0)
|
this.seek(0)
|
||||||
},
|
},
|
||||||
jumpBackwards() {
|
async jumpBackwards() {
|
||||||
|
await this.$hapticsImpact()
|
||||||
if (this.isLoading) return
|
if (this.isLoading) return
|
||||||
AbsAudioPlayer.seekBackward({ value: this.jumpBackwardsTime })
|
AbsAudioPlayer.seekBackward({ value: this.jumpBackwardsTime })
|
||||||
},
|
},
|
||||||
jumpForward() {
|
async jumpForward() {
|
||||||
|
await this.$hapticsImpact()
|
||||||
if (this.isLoading) return
|
if (this.isLoading) return
|
||||||
AbsAudioPlayer.seekForward({ value: this.jumpForwardTime })
|
AbsAudioPlayer.seekForward({ value: this.jumpForwardTime })
|
||||||
},
|
},
|
||||||
@@ -408,19 +472,19 @@ export default {
|
|||||||
this.updateReadyTrack()
|
this.updateReadyTrack()
|
||||||
},
|
},
|
||||||
setChunksReady(chunks, numSegments) {
|
setChunksReady(chunks, numSegments) {
|
||||||
var largestSeg = 0
|
let largestSeg = 0
|
||||||
for (let i = 0; i < chunks.length; i++) {
|
for (let i = 0; i < chunks.length; i++) {
|
||||||
var chunk = chunks[i]
|
const chunk = chunks[i]
|
||||||
if (typeof chunk === 'string') {
|
if (typeof chunk === 'string') {
|
||||||
var chunkRange = chunk.split('-').map((c) => Number(c))
|
const chunkRange = chunk.split('-').map((c) => Number(c))
|
||||||
if (chunkRange.length < 2) continue
|
if (chunkRange.length < 2) continue
|
||||||
if (chunkRange[1] > largestSeg) largestSeg = chunkRange[1]
|
if (chunkRange[1] > largestSeg) largestSeg = chunkRange[1]
|
||||||
} else if (chunk > largestSeg) {
|
} else if (chunk > largestSeg) {
|
||||||
largestSeg = chunk
|
largestSeg = chunk
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var percentageReady = largestSeg / numSegments
|
const percentageReady = largestSeg / numSegments
|
||||||
var widthReady = Math.round(this.trackWidth * percentageReady)
|
const widthReady = Math.round(this.trackWidth * percentageReady)
|
||||||
if (this.readyTrackWidth === widthReady) {
|
if (this.readyTrackWidth === widthReady) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -472,17 +536,17 @@ export default {
|
|||||||
},
|
},
|
||||||
updateTrack() {
|
updateTrack() {
|
||||||
// Update progress track UI
|
// Update progress track UI
|
||||||
var percentDone = this.currentTime / this.totalDuration
|
let percentDone = this.currentTime / this.totalDuration
|
||||||
var totalPercentDone = percentDone
|
const totalPercentDone = percentDone
|
||||||
var bufferedPercent = this.bufferedTime / this.totalDuration
|
let bufferedPercent = this.bufferedTime / this.totalDuration
|
||||||
var totalBufferedPercent = bufferedPercent
|
const totalBufferedPercent = bufferedPercent
|
||||||
|
|
||||||
if (this.useChapterTrack && this.currentChapter) {
|
if (this.useChapterTrack && this.currentChapter) {
|
||||||
var currChapTime = this.currentTime - this.currentChapter.start
|
const currChapTime = this.currentTime - this.currentChapter.start
|
||||||
percentDone = currChapTime / this.currentChapterDuration
|
percentDone = currChapTime / this.currentChapterDuration
|
||||||
bufferedPercent = Math.max(0, Math.min(1, (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration))
|
bufferedPercent = Math.max(0, Math.min(1, (this.bufferedTime - this.currentChapter.start) / this.currentChapterDuration))
|
||||||
}
|
}
|
||||||
var ptWidth = Math.round(percentDone * this.trackWidth)
|
const ptWidth = Math.round(percentDone * this.trackWidth)
|
||||||
this.$refs.playedTrack.style.width = ptWidth + 'px'
|
this.$refs.playedTrack.style.width = ptWidth + 'px'
|
||||||
this.$refs.bufferedTrack.style.width = Math.round(bufferedPercent * this.trackWidth) + 'px'
|
this.$refs.bufferedTrack.style.width = Math.round(bufferedPercent * this.trackWidth) + 'px'
|
||||||
|
|
||||||
@@ -539,6 +603,7 @@ export default {
|
|||||||
this.seek(time)
|
this.seek(time)
|
||||||
},
|
},
|
||||||
async playPauseClick() {
|
async playPauseClick() {
|
||||||
|
await this.$hapticsImpact()
|
||||||
if (this.isLoading) return
|
if (this.isLoading) return
|
||||||
|
|
||||||
this.isPlaying = !!((await AbsAudioPlayer.playPause()) || {}).playing
|
this.isPlaying = !!((await AbsAudioPlayer.playPause()) || {}).playing
|
||||||
@@ -641,19 +706,32 @@ export default {
|
|||||||
ts.innerText = currTimeStr
|
ts.innerText = currTimeStr
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clickMenuAction(action) {
|
async clickMenuAction(action) {
|
||||||
|
await this.$hapticsImpact()
|
||||||
this.showMoreMenuDialog = false
|
this.showMoreMenuDialog = false
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
if (action === 'lock') {
|
if (action === 'history') {
|
||||||
|
this.$router.push(`/media/${this.mediaId}/history?title=${this.title}`)
|
||||||
|
this.showFullscreen = false
|
||||||
|
} else if (action === 'lock') {
|
||||||
this.lockUi = !this.lockUi
|
this.lockUi = !this.lockUi
|
||||||
this.$localStore.setPlayerLock(this.lockUi)
|
this.$localStore.setPlayerLock(this.lockUi)
|
||||||
} else if (action === 'chapter_track') {
|
} else if (action === 'chapter_track') {
|
||||||
this.useChapterTrack = !this.useChapterTrack
|
this.useChapterTrack = !this.useChapterTrack
|
||||||
|
this.useTotalTrack = !this.useChapterTrack || this.useTotalTrack
|
||||||
|
|
||||||
this.updateTimestamp()
|
this.updateTimestamp()
|
||||||
this.updateTrack()
|
this.updateTrack()
|
||||||
this.updateReadyTrack()
|
this.updateReadyTrack()
|
||||||
this.$localStore.setUseChapterTrack(this.useChapterTrack)
|
this.$localStore.setUseChapterTrack(this.useChapterTrack)
|
||||||
|
} else if (action === 'total_track') {
|
||||||
|
this.useTotalTrack = !this.useTotalTrack
|
||||||
|
this.useChapterTrack = !this.useTotalTrack || this.useChapterTrack
|
||||||
|
|
||||||
|
this.updateTimestamp()
|
||||||
|
this.updateTrack()
|
||||||
|
this.updateReadyTrack()
|
||||||
|
this.$localStore.setUseTotalTrack(this.useTotalTrack)
|
||||||
} else if (action === 'close') {
|
} else if (action === 'close') {
|
||||||
this.closePlayback()
|
this.closePlayback()
|
||||||
}
|
}
|
||||||
@@ -750,16 +828,27 @@ export default {
|
|||||||
this.onProgressSyncSuccess = AbsAudioPlayer.addListener('onProgressSyncSuccess', this.showProgressSyncSuccess)
|
this.onProgressSyncSuccess = AbsAudioPlayer.addListener('onProgressSyncSuccess', this.showProgressSyncSuccess)
|
||||||
},
|
},
|
||||||
screenOrientationChange() {
|
screenOrientationChange() {
|
||||||
setTimeout(this.updateScreenSize, 50)
|
setTimeout(() => {
|
||||||
|
this.updateScreenSize()
|
||||||
|
if (this.$refs.track) {
|
||||||
|
this.trackWidth = this.$refs.track.clientWidth
|
||||||
|
this.updateTrack()
|
||||||
|
this.updateReadyTrack()
|
||||||
|
}
|
||||||
|
}, 50)
|
||||||
},
|
},
|
||||||
updateScreenSize() {
|
updateScreenSize() {
|
||||||
this.windowHeight = window.innerHeight
|
this.windowHeight = window.innerHeight
|
||||||
var coverHeight = this.fullscreenBookCoverWidth * this.bookCoverAspectRatio
|
this.windowWidth = window.innerWidth
|
||||||
|
const coverHeight = this.fullscreenBookCoverWidth * this.bookCoverAspectRatio
|
||||||
|
const coverImageWidthCollapsed = 46 / this.bookCoverAspectRatio
|
||||||
document.documentElement.style.setProperty('--cover-image-width', this.fullscreenBookCoverWidth + 'px')
|
document.documentElement.style.setProperty('--cover-image-width', this.fullscreenBookCoverWidth + 'px')
|
||||||
document.documentElement.style.setProperty('--cover-image-height', coverHeight + 'px')
|
document.documentElement.style.setProperty('--cover-image-height', coverHeight + 'px')
|
||||||
|
document.documentElement.style.setProperty('--cover-image-width-collapsed', coverImageWidthCollapsed + 'px')
|
||||||
|
document.documentElement.style.setProperty('--cover-image-height-collapsed', 46 + 'px')
|
||||||
|
document.documentElement.style.setProperty('--title-author-left-offset-collapsed', 24 + coverImageWidthCollapsed + 'px')
|
||||||
},
|
},
|
||||||
minimizePlayerEvt() {
|
minimizePlayerEvt() {
|
||||||
console.log('Minimize Player Evt')
|
|
||||||
this.showFullscreen = false
|
this.showFullscreen = false
|
||||||
},
|
},
|
||||||
showProgressSyncIsFailing() {
|
showProgressSyncIsFailing() {
|
||||||
@@ -821,12 +910,11 @@ export default {
|
|||||||
:root {
|
:root {
|
||||||
--cover-image-width: 0px;
|
--cover-image-width: 0px;
|
||||||
--cover-image-height: 0px;
|
--cover-image-height: 0px;
|
||||||
--cover-image-width-collapsed: 60px;
|
--cover-image-width-collapsed: 46px;
|
||||||
--cover-image-height-collapsed: 60px;
|
--cover-image-height-collapsed: 46px;
|
||||||
}
|
--title-author-left-offset-collapsed: 70px;
|
||||||
.bookCoverWrapper {
|
|
||||||
box-shadow: 3px -2px 5px #00000066;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.playerContainer {
|
.playerContainer {
|
||||||
height: 100px;
|
height: 100px;
|
||||||
}
|
}
|
||||||
@@ -836,23 +924,22 @@ export default {
|
|||||||
#playerContent {
|
#playerContent {
|
||||||
box-shadow: 0px -8px 8px #11111155;
|
box-shadow: 0px -8px 8px #11111155;
|
||||||
}
|
}
|
||||||
|
.fullscreen #playerContent {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
#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);
|
||||||
transition-property: margin;
|
transition-property: margin;
|
||||||
margin-bottom: 4px;
|
bottom: 20px;
|
||||||
}
|
}
|
||||||
.fullscreen #playerTrack {
|
.fullscreen #playerTrack {
|
||||||
margin-bottom: 18px;
|
top: 20px;
|
||||||
}
|
bottom: unset;
|
||||||
|
|
||||||
.ios-player #timestamp-row {
|
|
||||||
padding-left: 16px;
|
|
||||||
padding-right: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.cover-wrapper {
|
.cover-wrapper {
|
||||||
bottom: 44px;
|
bottom: 48px;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
height: var(--cover-image-height-collapsed);
|
height: var(--cover-image-height-collapsed);
|
||||||
width: var(--cover-image-width-collapsed);
|
width: var(--cover-image-width-collapsed);
|
||||||
@@ -873,8 +960,8 @@ export default {
|
|||||||
transform-origin: left bottom;
|
transform-origin: left bottom;
|
||||||
|
|
||||||
width: 40%;
|
width: 40%;
|
||||||
bottom: 50px;
|
bottom: 56px;
|
||||||
left: 80px;
|
left: var(--title-author-left-offset-collapsed);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
.title-author-texts .title-text {
|
.title-author-texts .title-text {
|
||||||
@@ -899,10 +986,10 @@ export default {
|
|||||||
pointer-events: auto;
|
pointer-events: auto;
|
||||||
}
|
}
|
||||||
.fullscreen .title-author-texts .title-text {
|
.fullscreen .title-author-texts .title-text {
|
||||||
font-size: clamp(0.8rem, calc(var(--cover-image-height) / 260 * 20), 1.2rem);
|
font-size: clamp(0.8rem, calc(var(--cover-image-height) / 260 * 20), 1.5rem);
|
||||||
}
|
}
|
||||||
.fullscreen .title-author-texts .author-text {
|
.fullscreen .title-author-texts .author-text {
|
||||||
font-size: clamp(0.6rem, calc(var(--cover-image-height) / 260 * 16), 1rem);
|
font-size: clamp(0.6rem, calc(var(--cover-image-height) / 260 * 16), 1.1rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
#playerControls {
|
#playerControls {
|
||||||
@@ -912,7 +999,7 @@ export default {
|
|||||||
width: 140px;
|
width: 140px;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
padding-right: 12px;
|
padding-right: 12px;
|
||||||
bottom: 45px;
|
bottom: 50px;
|
||||||
}
|
}
|
||||||
#playerControls .jump-icon {
|
#playerControls .jump-icon {
|
||||||
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);
|
||||||
@@ -949,7 +1036,7 @@ export default {
|
|||||||
|
|
||||||
.fullscreen #playerControls {
|
.fullscreen #playerControls {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
bottom: 100px;
|
bottom: 94px;
|
||||||
}
|
}
|
||||||
.fullscreen #playerControls .jump-icon {
|
.fullscreen #playerControls .jump-icon {
|
||||||
margin: 0px 18px;
|
margin: 0px 18px;
|
||||||
@@ -970,4 +1057,4 @@ export default {
|
|||||||
.fullscreen #playerControls .play-btn .material-icons {
|
.fullscreen #playerControls .play-btn .material-icons {
|
||||||
font-size: 2.1rem;
|
font-size: 2.1rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<app-audio-player ref="audioPlayer" :bookmarks="bookmarks" :sleep-timer-running="isSleepTimerRunning" :sleep-time-remaining="sleepTimeRemaining" @selectPlaybackSpeed="showPlaybackSpeedModal = true" @updateTime="(t) => (currentTime = t)" @showSleepTimer="showSleepTimer" @showBookmarks="showBookmarks" />
|
<app-audio-player ref="audioPlayer" :bookmarks="bookmarks" :sleep-timer-running="isSleepTimerRunning" :sleep-time-remaining="sleepTimeRemaining" :is-server-item="!!serverLibraryItemId" @selectPlaybackSpeed="showPlaybackSpeedModal = true" @updateTime="(t) => (currentTime = t)" @showSleepTimer="showSleepTimer" @showBookmarks="showBookmarks" />
|
||||||
|
|
||||||
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-rate.sync="playbackSpeed" @update:playbackRate="updatePlaybackSpeed" @change="changePlaybackSpeed" />
|
<modals-playback-speed-modal v-model="showPlaybackSpeedModal" :playback-rate.sync="playbackSpeed" @update:playbackRate="updatePlaybackSpeed" @change="changePlaybackSpeed" />
|
||||||
<modals-sleep-timer-modal v-model="showSleepTimerModal" :current-time="sleepTimeRemaining" :sleep-timer-running="isSleepTimerRunning" :current-end-of-chapter-time="currentEndOfChapterTime" @change="selectSleepTimeout" @cancel="cancelSleepTimer" @increase="increaseSleepTimer" @decrease="decreaseSleepTimer" />
|
<modals-sleep-timer-modal v-model="showSleepTimerModal" :current-time="sleepTimeRemaining" :sleep-timer-running="isSleepTimerRunning" :current-end-of-chapter-time="currentEndOfChapterTime" @change="selectSleepTimeout" @cancel="cancelSleepTimer" @increase="increaseSleepTimer" @decrease="decreaseSleepTimer" />
|
||||||
@@ -38,21 +38,10 @@ export default {
|
|||||||
serverEpisodeId: null
|
serverEpisodeId: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
socketConnected(newVal) {
|
|
||||||
if (newVal) {
|
|
||||||
console.log('Socket Connected set listeners')
|
|
||||||
this.setListeners()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
bookmarks() {
|
bookmarks() {
|
||||||
if (!this.serverLibraryItemId) return []
|
if (!this.serverLibraryItemId) return []
|
||||||
return this.$store.getters['user/getUserBookmarksForItem'](this.serverLibraryItemId)
|
return this.$store.getters['user/getUserBookmarksForItem'](this.serverLibraryItemId)
|
||||||
},
|
|
||||||
socketConnected() {
|
|
||||||
return this.$store.state.socketConnected
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -62,7 +51,7 @@ export default {
|
|||||||
selectBookmark(bookmark) {
|
selectBookmark(bookmark) {
|
||||||
this.showBookmarksModal = false
|
this.showBookmarksModal = false
|
||||||
if (!bookmark || isNaN(bookmark.time)) return
|
if (!bookmark || isNaN(bookmark.time)) return
|
||||||
var bookmarkTime = Number(bookmark.time)
|
const bookmarkTime = Number(bookmark.time)
|
||||||
if (this.$refs.audioPlayer) {
|
if (this.$refs.audioPlayer) {
|
||||||
this.$refs.audioPlayer.seek(bookmarkTime)
|
this.$refs.audioPlayer.seek(bookmarkTime)
|
||||||
}
|
}
|
||||||
@@ -71,8 +60,6 @@ export default {
|
|||||||
this.isSleepTimerRunning = false
|
this.isSleepTimerRunning = false
|
||||||
if (currentPosition) {
|
if (currentPosition) {
|
||||||
console.log('Sleep Timer Ended Current Position: ' + currentPosition)
|
console.log('Sleep Timer Ended Current Position: ' + currentPosition)
|
||||||
var currentTime = Math.floor(currentPosition / 1000)
|
|
||||||
// TODO: Was syncing to the server here before
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSleepTimerSet({ value: sleepTimeRemaining }) {
|
onSleepTimerSet({ value: sleepTimeRemaining }) {
|
||||||
@@ -155,19 +142,10 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Settings have been loaded (at least once, so it's safe to kickoff onReady)
|
// Settings have been loaded (at least once, so it's safe to kickoff onReady)
|
||||||
this.settingsLoaded = true
|
if (!this.settingsLoaded) {
|
||||||
this.notifyOnReady()
|
this.settingsLoaded = true
|
||||||
},
|
this.notifyOnReady()
|
||||||
setListeners() {
|
}
|
||||||
// if (!this.$server.socket) {
|
|
||||||
// console.error('Invalid server socket not set')
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// this.$server.socket.on('stream_open', this.streamOpen)
|
|
||||||
// this.$server.socket.on('stream_closed', this.streamClosed)
|
|
||||||
// this.$server.socket.on('stream_progress', this.streamProgress)
|
|
||||||
// this.$server.socket.on('stream_ready', this.streamReady)
|
|
||||||
// this.$server.socket.on('stream_reset', this.streamReset)
|
|
||||||
},
|
},
|
||||||
closeStreamOnly() {
|
closeStreamOnly() {
|
||||||
// If user logs out or disconnects from server and not playing local
|
// If user logs out or disconnects from server and not playing local
|
||||||
@@ -204,13 +182,14 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
async playLibraryItem(payload) {
|
async playLibraryItem(payload) {
|
||||||
var libraryItemId = payload.libraryItemId
|
const libraryItemId = payload.libraryItemId
|
||||||
var episodeId = payload.episodeId
|
const episodeId = payload.episodeId
|
||||||
|
const startTime = payload.startTime
|
||||||
|
|
||||||
// When playing local library item and can also play this item from the server
|
// When playing local library item and can also play this item from the server
|
||||||
// then store the server library item id so it can be used if a cast is made
|
// then store the server library item id so it can be used if a cast is made
|
||||||
var serverLibraryItemId = payload.serverLibraryItemId || null
|
const serverLibraryItemId = payload.serverLibraryItemId || null
|
||||||
var serverEpisodeId = payload.serverEpisodeId || null
|
const serverEpisodeId = payload.serverEpisodeId || null
|
||||||
|
|
||||||
if (libraryItemId.startsWith('local') && this.$store.state.isCasting) {
|
if (libraryItemId.startsWith('local') && this.$store.state.isCasting) {
|
||||||
const { value } = await Dialog.confirm({
|
const { value } = await Dialog.confirm({
|
||||||
@@ -222,16 +201,28 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if already playing this item then jump to start time
|
||||||
|
if (this.$store.getters['getIsMediaStreaming'](libraryItemId, episodeId)) {
|
||||||
|
console.log('Already streaming item', startTime)
|
||||||
|
if (startTime !== undefined && startTime !== null) {
|
||||||
|
// seek to start time
|
||||||
|
AbsAudioPlayer.seek({ value: Math.floor(startTime) })
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
this.serverLibraryItemId = null
|
this.serverLibraryItemId = null
|
||||||
this.serverEpisodeId = null
|
this.serverEpisodeId = null
|
||||||
|
|
||||||
var playbackRate = 1
|
let playbackRate = 1
|
||||||
if (this.$refs.audioPlayer) {
|
if (this.$refs.audioPlayer) {
|
||||||
playbackRate = this.$refs.audioPlayer.currentPlaybackRate || 1
|
playbackRate = this.$refs.audioPlayer.currentPlaybackRate || 1
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Called playLibraryItem', libraryItemId)
|
console.log('Called playLibraryItem', libraryItemId)
|
||||||
AbsAudioPlayer.prepareLibraryItem({ libraryItemId, episodeId, playWhenReady: true, playbackRate })
|
const preparePayload = { libraryItemId, episodeId, playWhenReady: true, playbackRate }
|
||||||
|
if (startTime !== undefined && startTime !== null) preparePayload.startTime = startTime
|
||||||
|
AbsAudioPlayer.prepareLibraryItem(preparePayload)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
const errorMsg = data.error || 'Failed to play'
|
const errorMsg = data.error || 'Failed to play'
|
||||||
@@ -276,7 +267,7 @@ export default {
|
|||||||
notifyOnReady() {
|
notifyOnReady() {
|
||||||
// If settings aren't loaded yet, native player will receive incorrect settings
|
// If settings aren't loaded yet, native player will receive incorrect settings
|
||||||
console.log('Notify on ready... settingsLoaded:', this.settingsLoaded, 'isReady:', this.isReady)
|
console.log('Notify on ready... settingsLoaded:', this.settingsLoaded, 'isReady:', this.isReady)
|
||||||
if ( this.settingsLoaded && this.isReady ) {
|
if (this.settingsLoaded && this.isReady) {
|
||||||
AbsAudioPlayer.onReady()
|
AbsAudioPlayer.onReady()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -290,13 +281,12 @@ export default {
|
|||||||
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}`)
|
||||||
|
|
||||||
this.setListeners()
|
|
||||||
this.$eventBus.$on('abs-ui-ready', this.onReady)
|
this.$eventBus.$on('abs-ui-ready', this.onReady)
|
||||||
this.$eventBus.$on('play-item', this.playLibraryItem)
|
this.$eventBus.$on('play-item', this.playLibraryItem)
|
||||||
this.$eventBus.$on('pause-item', this.pauseItem)
|
this.$eventBus.$on('pause-item', this.pauseItem)
|
||||||
this.$eventBus.$on('close-stream', this.closeStreamOnly)
|
this.$eventBus.$on('close-stream', this.closeStreamOnly)
|
||||||
this.$eventBus.$on('cast-local-item', this.castLocalItem)
|
this.$eventBus.$on('cast-local-item', this.castLocalItem)
|
||||||
this.$store.commit('user/addSettingsListener', { id: 'streamContainer', meth: this.settingsUpdated })
|
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
if (this.onLocalMediaProgressUpdateListener) this.onLocalMediaProgressUpdateListener.remove()
|
if (this.onLocalMediaProgressUpdateListener) this.onLocalMediaProgressUpdateListener.remove()
|
||||||
@@ -304,19 +294,12 @@ export default {
|
|||||||
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.$server.socket) {
|
|
||||||
// this.$server.socket.off('stream_open', this.streamOpen)
|
|
||||||
// this.$server.socket.off('stream_closed', this.streamClosed)
|
|
||||||
// this.$server.socket.off('stream_progress', this.streamProgress)
|
|
||||||
// this.$server.socket.off('stream_ready', this.streamReady)
|
|
||||||
// this.$server.socket.off('stream_reset', this.streamReset)
|
|
||||||
// }
|
|
||||||
this.$eventBus.$off('abs-ui-ready', this.onReady)
|
this.$eventBus.$off('abs-ui-ready', this.onReady)
|
||||||
this.$eventBus.$off('play-item', this.playLibraryItem)
|
this.$eventBus.$off('play-item', this.playLibraryItem)
|
||||||
this.$eventBus.$off('pause-item', this.pauseItem)
|
this.$eventBus.$off('pause-item', this.pauseItem)
|
||||||
this.$eventBus.$off('close-stream', this.closeStreamOnly)
|
this.$eventBus.$off('close-stream', this.closeStreamOnly)
|
||||||
this.$eventBus.$off('cast-local-item', this.castLocalItem)
|
this.$eventBus.$off('cast-local-item', this.castLocalItem)
|
||||||
this.$store.commit('user/removeSettingsListener', 'streamContainer')
|
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -115,10 +115,10 @@ export default {
|
|||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
items.push({
|
items.push({
|
||||||
icon: 'download',
|
icon: 'download',
|
||||||
iconOutlined: false,
|
iconOutlined: false,
|
||||||
text: 'Downloads',
|
text: 'Downloads',
|
||||||
to: '/downloads'
|
to: '/downloads'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
items.push({
|
items.push({
|
||||||
@@ -137,6 +137,7 @@ export default {
|
|||||||
this.show = false
|
this.show = false
|
||||||
},
|
},
|
||||||
async logout() {
|
async logout() {
|
||||||
|
await this.$hapticsImpact()
|
||||||
if (this.user) {
|
if (this.user) {
|
||||||
await this.$axios.$post('/logout').catch((error) => {
|
await this.$axios.$post('/logout').catch((error) => {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
|
|||||||
@@ -89,22 +89,22 @@ export default {
|
|||||||
return this.bookCoverAspectRatio === 1
|
return this.bookCoverAspectRatio === 1
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
bookWidth() {
|
bookWidth() {
|
||||||
var coverSize = 100
|
var coverSize = 100
|
||||||
if (window.innerWidth <= 375) coverSize = 90
|
if (window.innerWidth <= 375) coverSize = 90
|
||||||
|
|
||||||
if (this.isCoverSquareAspectRatio) return coverSize * 1.6
|
if (this.isCoverSquareAspectRatio || this.entityName === 'playlists') return coverSize * 1.6
|
||||||
return coverSize
|
return coverSize
|
||||||
},
|
},
|
||||||
bookHeight() {
|
bookHeight() {
|
||||||
if (this.isCoverSquareAspectRatio) return this.bookWidth
|
if (this.isCoverSquareAspectRatio || this.entityName === 'playlists') return this.bookWidth
|
||||||
return this.bookWidth * 1.6
|
return this.bookWidth * 1.6
|
||||||
},
|
},
|
||||||
entityWidth() {
|
entityWidth() {
|
||||||
if (this.showBookshelfListView) return this.bookshelfWidth - 16
|
if (this.showBookshelfListView) return this.bookshelfWidth - 16
|
||||||
if (this.isBookEntity) return this.bookWidth
|
if (this.isBookEntity || this.entityName === 'playlists') return this.bookWidth
|
||||||
return this.bookWidth * 2
|
return this.bookWidth * 2
|
||||||
},
|
},
|
||||||
entityHeight() {
|
entityHeight() {
|
||||||
@@ -134,7 +134,7 @@ export default {
|
|||||||
return this.$store.getters['getAltViewEnabled']
|
return this.$store.getters['getAltViewEnabled']
|
||||||
},
|
},
|
||||||
sizeMultiplier() {
|
sizeMultiplier() {
|
||||||
var baseSize = this.isCoverSquareAspectRatio ? 192 : 120
|
const baseSize = this.isCoverSquareAspectRatio ? 192 : 120
|
||||||
return this.entityWidth / baseSize
|
return this.entityWidth / baseSize
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -145,7 +145,7 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
async fetchEntities(page) {
|
async fetchEntities(page) {
|
||||||
var startIndex = page * this.booksPerFetch
|
const startIndex = page * this.booksPerFetch
|
||||||
|
|
||||||
this.isFetchingEntities = true
|
this.isFetchingEntities = true
|
||||||
|
|
||||||
@@ -153,11 +153,11 @@ export default {
|
|||||||
this.currentSFQueryString = this.buildSearchParams()
|
this.currentSFQueryString = this.buildSearchParams()
|
||||||
}
|
}
|
||||||
|
|
||||||
var entityPath = this.entityName === 'books' || this.entityName === 'series-books' ? `items` : this.entityName
|
const entityPath = this.entityName === 'books' || this.entityName === 'series-books' ? `items` : this.entityName
|
||||||
var sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
||||||
var fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1`
|
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1`
|
||||||
|
|
||||||
var payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => {
|
const payload = await this.$axios.$get(`/api/libraries/${this.currentLibraryId}/${entityPath}${fullQueryString}`).catch((error) => {
|
||||||
console.error('failed to fetch books', error)
|
console.error('failed to fetch books', error)
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
@@ -179,13 +179,13 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < payload.results.length; i++) {
|
for (let i = 0; i < payload.results.length; i++) {
|
||||||
var index = i + startIndex
|
const index = i + startIndex
|
||||||
this.entities[index] = payload.results[i]
|
this.entities[index] = payload.results[i]
|
||||||
if (this.entityComponentRefs[index]) {
|
if (this.entityComponentRefs[index]) {
|
||||||
this.entityComponentRefs[index].setEntity(this.entities[index])
|
this.entityComponentRefs[index].setEntity(this.entities[index])
|
||||||
|
|
||||||
if (this.isBookEntity) {
|
if (this.isBookEntity) {
|
||||||
var localLibraryItem = this.localLibraryItems.find((lli) => lli.libraryItemId == this.entities[index].id)
|
const localLibraryItem = this.localLibraryItems.find((lli) => lli.libraryItemId == this.entities[index].id)
|
||||||
if (localLibraryItem) {
|
if (localLibraryItem) {
|
||||||
this.entityComponentRefs[index].setLocalLibraryItem(localLibraryItem)
|
this.entityComponentRefs[index].setLocalLibraryItem(localLibraryItem)
|
||||||
}
|
}
|
||||||
@@ -309,7 +309,7 @@ export default {
|
|||||||
var { clientHeight, clientWidth } = bookshelf
|
var { clientHeight, clientWidth } = bookshelf
|
||||||
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 = Math.max(1, 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
|
||||||
|
|
||||||
@@ -356,8 +356,6 @@ export default {
|
|||||||
let searchParams = new URLSearchParams()
|
let searchParams = new URLSearchParams()
|
||||||
if (this.page === 'series-books') {
|
if (this.page === 'series-books') {
|
||||||
searchParams.set('filter', `series.${this.$encode(this.seriesId)}`)
|
searchParams.set('filter', `series.${this.$encode(this.seriesId)}`)
|
||||||
searchParams.set('sort', 'book.volumeNumber')
|
|
||||||
searchParams.set('desc', 0)
|
|
||||||
} else {
|
} else {
|
||||||
if (this.filterBy && this.filterBy !== 'all') {
|
if (this.filterBy && this.filterBy !== 'all') {
|
||||||
searchParams.set('filter', this.filterBy)
|
searchParams.set('filter', this.filterBy)
|
||||||
@@ -373,15 +371,16 @@ export default {
|
|||||||
return searchParams.toString()
|
return searchParams.toString()
|
||||||
},
|
},
|
||||||
checkUpdateSearchParams() {
|
checkUpdateSearchParams() {
|
||||||
var newSearchParams = this.buildSearchParams()
|
const newSearchParams = this.buildSearchParams()
|
||||||
var currentQueryString = window.location.search
|
let currentQueryString = window.location.search
|
||||||
if (currentQueryString && currentQueryString.startsWith('?')) currentQueryString = currentQueryString.slice(1)
|
if (currentQueryString && currentQueryString.startsWith('?')) currentQueryString = currentQueryString.slice(1)
|
||||||
|
|
||||||
if (newSearchParams === '') {
|
if (newSearchParams === '' && !currentQueryString) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if (newSearchParams !== this.currentSFQueryString || newSearchParams !== currentQueryString) {
|
if (newSearchParams !== this.currentSFQueryString || newSearchParams !== currentQueryString) {
|
||||||
let newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + '?' + newSearchParams
|
const queryString = newSearchParams ? `?${newSearchParams}` : ''
|
||||||
|
let newurl = window.location.protocol + '//' + window.location.host + window.location.pathname + queryString
|
||||||
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
|
this.routeFullPath = window.location.pathname + (window.location.search || '') // Update for saving scroll position
|
||||||
@@ -391,12 +390,17 @@ export default {
|
|||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
settingsUpdated(settings) {
|
settingsUpdated(settings) {
|
||||||
var wasUpdated = this.checkUpdateSearchParams()
|
const wasUpdated = this.checkUpdateSearchParams()
|
||||||
if (wasUpdated) {
|
if (wasUpdated) {
|
||||||
this.resetEntities()
|
this.resetEntities()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
libraryChanged() {
|
libraryChanged() {
|
||||||
|
if (this.currentLibraryMediaType !== 'book' && (this.page === 'series' || this.page === 'collections' || this.page === 'series-books')) {
|
||||||
|
this.$router.replace('/bookshelf')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (this.hasFilter) {
|
if (this.hasFilter) {
|
||||||
this.clearFilter()
|
this.clearFilter()
|
||||||
} else {
|
} else {
|
||||||
@@ -449,13 +453,13 @@ export default {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
initListeners() {
|
initListeners() {
|
||||||
var bookshelf = document.getElementById('bookshelf-wrapper')
|
const bookshelf = document.getElementById('bookshelf-wrapper')
|
||||||
if (bookshelf) {
|
if (bookshelf) {
|
||||||
bookshelf.addEventListener('scroll', this.scroll)
|
bookshelf.addEventListener('scroll', this.scroll)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$eventBus.$on('library-changed', this.libraryChanged)
|
this.$eventBus.$on('library-changed', this.libraryChanged)
|
||||||
this.$store.commit('user/addSettingsListener', { id: 'lazy-bookshelf', meth: this.settingsUpdated })
|
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
||||||
|
|
||||||
this.$socket.$on('item_updated', this.libraryItemUpdated)
|
this.$socket.$on('item_updated', this.libraryItemUpdated)
|
||||||
this.$socket.$on('item_added', this.libraryItemAdded)
|
this.$socket.$on('item_added', this.libraryItemAdded)
|
||||||
@@ -464,13 +468,13 @@ export default {
|
|||||||
this.$socket.$on('items_added', this.libraryItemsAdded)
|
this.$socket.$on('items_added', this.libraryItemsAdded)
|
||||||
},
|
},
|
||||||
removeListeners() {
|
removeListeners() {
|
||||||
var bookshelf = document.getElementById('bookshelf-wrapper')
|
const bookshelf = document.getElementById('bookshelf-wrapper')
|
||||||
if (bookshelf) {
|
if (bookshelf) {
|
||||||
bookshelf.removeEventListener('scroll', this.scroll)
|
bookshelf.removeEventListener('scroll', this.scroll)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$eventBus.$off('library-changed', this.libraryChanged)
|
this.$eventBus.$off('library-changed', this.libraryChanged)
|
||||||
this.$store.commit('user/removeSettingsListener', 'lazy-bookshelf')
|
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
||||||
|
|
||||||
this.$socket.$off('item_updated', this.libraryItemUpdated)
|
this.$socket.$off('item_updated', this.libraryItemUpdated)
|
||||||
this.$socket.$off('item_added', this.libraryItemAdded)
|
this.$socket.$off('item_added', this.libraryItemAdded)
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export default {
|
|||||||
return this.bookCoverAspectRatio === 1
|
return this.bookCoverAspectRatio === 1
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
altViewEnabled() {
|
altViewEnabled() {
|
||||||
return this.$store.getters['getAltViewEnabled']
|
return this.$store.getters['getAltViewEnabled']
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
coverWidth() {
|
coverWidth() {
|
||||||
if (this.bookCoverAspectRatio === 1) return 50 * 1.2
|
if (this.bookCoverAspectRatio === 1) return 50 * 1.2
|
||||||
|
|||||||
@@ -319,9 +319,6 @@ export default {
|
|||||||
userIsRoot() {
|
userIsRoot() {
|
||||||
return this.store.getters['user/getIsRoot']
|
return this.store.getters['user/getIsRoot']
|
||||||
},
|
},
|
||||||
_socket() {
|
|
||||||
return this.$root.socket || this.$nuxt.$root.socket
|
|
||||||
},
|
|
||||||
titleFontSize() {
|
titleFontSize() {
|
||||||
return 0.75 * this.sizeMultiplier
|
return 0.75 * this.sizeMultiplier
|
||||||
},
|
},
|
||||||
@@ -404,12 +401,13 @@ export default {
|
|||||||
// Server books may have a local library item
|
// Server books may have a local library item
|
||||||
this.localLibraryItem = localLibraryItem
|
this.localLibraryItem = localLibraryItem
|
||||||
},
|
},
|
||||||
clickCard(e) {
|
async clickCard(e) {
|
||||||
if (this.isSelectionMode) {
|
if (this.isSelectionMode) {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.selectBtnClick()
|
this.selectBtnClick()
|
||||||
} else if (this.recentEpisode) {
|
} else if (this.recentEpisode) {
|
||||||
|
await this.$hapticsImpact()
|
||||||
var eventBus = this.$eventBus || this.$nuxt.$eventBus
|
var eventBus = this.$eventBus || this.$nuxt.$eventBus
|
||||||
if (this.streamIsPlaying) {
|
if (this.streamIsPlaying) {
|
||||||
eventBus.$emit('pause-item')
|
eventBus.$emit('pause-item')
|
||||||
@@ -528,9 +526,9 @@ export default {
|
|||||||
this.imageReady = true
|
this.imageReady = true
|
||||||
|
|
||||||
if (this.$refs.cover && this.bookCoverSrc !== this.placeholderUrl) {
|
if (this.$refs.cover && this.bookCoverSrc !== this.placeholderUrl) {
|
||||||
var { naturalWidth, naturalHeight } = this.$refs.cover
|
const { naturalWidth, naturalHeight } = this.$refs.cover
|
||||||
var aspectRatio = naturalHeight / naturalWidth
|
const aspectRatio = naturalHeight / naturalWidth
|
||||||
var arDiff = Math.abs(aspectRatio - this.bookCoverAspectRatio)
|
const arDiff = Math.abs(aspectRatio - this.bookCoverAspectRatio)
|
||||||
|
|
||||||
// If image aspect ratio is <= 1.45 or >= 1.75 then use cover bg, otherwise stretch to fit
|
// If image aspect ratio is <= 1.45 or >= 1.75 then use cover bg, otherwise stretch to fit
|
||||||
if (arDiff > 0.15) {
|
if (arDiff > 0.15) {
|
||||||
|
|||||||
@@ -63,9 +63,6 @@ export default {
|
|||||||
var router = this.$router || this.$nuxt.$router
|
var router = this.$router || this.$nuxt.$router
|
||||||
router.push(`/collection/${this.collection.id}`)
|
router.push(`/collection/${this.collection.id}`)
|
||||||
},
|
},
|
||||||
clickEdit() {
|
|
||||||
this.$emit('edit', this.collection)
|
|
||||||
},
|
|
||||||
destroy() {
|
destroy() {
|
||||||
// destroy the vue listeners, etc
|
// destroy the vue listeners, etc
|
||||||
this.$destroy()
|
this.$destroy()
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="card" :id="`book-card-${index}`" :style="{ minWidth: width + 'px', maxWidth: width + 'px', height: height + 'px' }" class="rounded-sm z-10 cursor-pointer py-1" @click="clickCard">
|
<div ref="card" :id="`book-card-${index}`" :style="{ minWidth: width + 'px', maxWidth: width + 'px', height: height + 'px' }" class="rounded-sm z-10 cursor-pointer py-1" @click="clickCard">
|
||||||
<div class="h-full flex relative">
|
<div class="h-full flex relative">
|
||||||
<div class="w-20 h-20 relative" style="min-width: 80px; max-width: 80px">
|
<div class="list-card-cover relative">
|
||||||
<!-- When cover image does not fill -->
|
<!-- When cover image does not fill -->
|
||||||
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
||||||
<div class="absolute cover-bg" ref="coverBg" />
|
<div class="absolute cover-bg" ref="coverBg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full h-full absolute top-0 left-0 bg-primary">
|
<div class="w-full h-full absolute top-0 left-0">
|
||||||
<img v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
|
<img v-show="libraryItem" ref="cover" :src="bookCoverSrc" class="w-full h-full transition-opacity duration-300" :class="showCoverBg ? 'object-contain' : 'object-fill'" @load="imageLoaded" :style="{ opacity: imageReady ? 1 : 0 }" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- No progress shown for collapsed series or podcasts in library -->
|
<!-- No progress shown for collapsed series or podcasts in library -->
|
||||||
<div v-if="!isPodcast && !collapsedSeries" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: 80 * userProgressPercent + 'px' }"></div>
|
<div v-if="!isPodcast && !collapsedSeries" class="absolute bottom-0 left-0 h-1 shadow-sm max-w-full z-10 rounded-b" :class="itemIsFinished ? 'bg-success' : 'bg-yellow-400'" :style="{ width: coverWidth * userProgressPercent + 'px' }"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-grow px-2">
|
<div class="flex-grow px-2">
|
||||||
<p class="whitespace-normal" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">
|
<p class="whitespace-normal" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">
|
||||||
@@ -76,6 +76,9 @@ export default {
|
|||||||
this.libraryItem = newVal
|
this.libraryItem = newVal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
bookCoverAspectRatio() {
|
||||||
|
this.setCSSProperties()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -278,9 +281,6 @@ export default {
|
|||||||
userIsRoot() {
|
userIsRoot() {
|
||||||
return this.store.getters['user/getIsRoot']
|
return this.store.getters['user/getIsRoot']
|
||||||
},
|
},
|
||||||
_socket() {
|
|
||||||
return this.$root.socket || this.$nuxt.$root.socket
|
|
||||||
},
|
|
||||||
titleFontSize() {
|
titleFontSize() {
|
||||||
return 0.75 * this.sizeMultiplier
|
return 0.75 * this.sizeMultiplier
|
||||||
},
|
},
|
||||||
@@ -316,6 +316,9 @@ export default {
|
|||||||
if (!this.isAlternativeBookshelfView) return 0
|
if (!this.isAlternativeBookshelfView) return 0
|
||||||
else if (!this.displaySortLine) return 3 * this.sizeMultiplier
|
else if (!this.displaySortLine) return 3 * this.sizeMultiplier
|
||||||
return 4.25 * this.sizeMultiplier
|
return 4.25 * this.sizeMultiplier
|
||||||
|
},
|
||||||
|
coverWidth() {
|
||||||
|
return 80 / this.bookCoverAspectRatio
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -436,9 +439,9 @@ export default {
|
|||||||
this.imageReady = true
|
this.imageReady = true
|
||||||
|
|
||||||
if (this.$refs.cover && this.bookCoverSrc !== this.placeholderUrl) {
|
if (this.$refs.cover && this.bookCoverSrc !== this.placeholderUrl) {
|
||||||
var { naturalWidth, naturalHeight } = this.$refs.cover
|
const { naturalWidth, naturalHeight } = this.$refs.cover
|
||||||
var aspectRatio = naturalHeight / naturalWidth
|
const aspectRatio = naturalHeight / naturalWidth
|
||||||
var arDiff = Math.abs(aspectRatio - this.bookCoverAspectRatio)
|
const arDiff = Math.abs(aspectRatio - this.bookCoverAspectRatio)
|
||||||
|
|
||||||
// If image aspect ratio is <= 1.45 or >= 1.75 then use cover bg, otherwise stretch to fit
|
// If image aspect ratio is <= 1.45 or >= 1.75 then use cover bg, otherwise stretch to fit
|
||||||
if (arDiff > 0.15) {
|
if (arDiff > 0.15) {
|
||||||
@@ -448,9 +451,13 @@ export default {
|
|||||||
this.showCoverBg = false
|
this.showCoverBg = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
setCSSProperties() {
|
||||||
|
document.documentElement.style.setProperty('--list-card-cover-width', this.coverWidth + 'px')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
this.setCSSProperties()
|
||||||
if (this.bookMount) {
|
if (this.bookMount) {
|
||||||
this.setEntity(this.bookMount)
|
this.setEntity(this.bookMount)
|
||||||
|
|
||||||
@@ -461,3 +468,17 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--list-card-cover-width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-card-cover {
|
||||||
|
height: 80px;
|
||||||
|
max-height: 80px;
|
||||||
|
width: var(--list-card-cover-width);
|
||||||
|
min-width: var(--list-card-cover-width);
|
||||||
|
max-width: var(--list-card-cover-width);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="card" :id="`playlist-card-${index}`" :style="{ width: width + 'px', height: height + 'px' }" class="absolute top-0 left-0 rounded-sm z-30 cursor-pointer" @click="clickCard">
|
||||||
|
<div class="absolute top-0 left-0 w-full box-shadow-book shadow-height" />
|
||||||
|
<div class="w-full h-full bg-primary relative rounded overflow-hidden">
|
||||||
|
<covers-playlist-cover ref="cover" :items="items" :width="width" :height="height" />
|
||||||
|
</div>
|
||||||
|
<div class="categoryPlacard absolute z-30 left-0 right-0 mx-auto -bottom-6 h-6 rounded-md font-book text-center" :style="{ width: Math.min(160, width) + 'px' }">
|
||||||
|
<div class="w-full h-full flex items-center justify-center rounded-sm border" :class="isAltViewEnabled ? 'altBookshelfLabel' : 'shinyBlack'" :style="{ padding: `0rem ${0.5 * sizeMultiplier}rem` }">
|
||||||
|
<p class="truncate" :style="{ fontSize: labelFontSize + 'rem' }">{{ title }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
index: Number,
|
||||||
|
width: Number,
|
||||||
|
height: Number,
|
||||||
|
bookCoverAspectRatio: Number,
|
||||||
|
playlistMount: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
},
|
||||||
|
isAltViewEnabled: Boolean
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
playlist: null,
|
||||||
|
isSelectionMode: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
labelFontSize() {
|
||||||
|
if (this.width < 160) return 0.75
|
||||||
|
return 0.875
|
||||||
|
},
|
||||||
|
sizeMultiplier() {
|
||||||
|
if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2)
|
||||||
|
return this.width / 240
|
||||||
|
},
|
||||||
|
title() {
|
||||||
|
return this.playlist ? this.playlist.name : ''
|
||||||
|
},
|
||||||
|
items() {
|
||||||
|
return this.playlist ? this.playlist.items || [] : []
|
||||||
|
},
|
||||||
|
store() {
|
||||||
|
return this.$store || this.$nuxt.$store
|
||||||
|
},
|
||||||
|
currentLibraryId() {
|
||||||
|
return this.store.state.libraries.currentLibraryId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setEntity(playlist) {
|
||||||
|
this.playlist = playlist
|
||||||
|
},
|
||||||
|
setSelectionMode(val) {
|
||||||
|
this.isSelectionMode = val
|
||||||
|
},
|
||||||
|
clickCard() {
|
||||||
|
if (!this.playlist) return
|
||||||
|
var router = this.$router || this.$nuxt.$router
|
||||||
|
router.push(`/playlist/${this.playlist.id}`)
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
// destroy the vue listeners, etc
|
||||||
|
this.$destroy()
|
||||||
|
|
||||||
|
// remove the element from the DOM
|
||||||
|
if (this.$el && this.$el.parentNode) {
|
||||||
|
this.$el.parentNode.removeChild(this.$el)
|
||||||
|
} else if (this.$el && this.$el.remove) {
|
||||||
|
this.$el.remove()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.playlistMount) {
|
||||||
|
this.setEntity(this.playlistMount)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -61,10 +61,6 @@ export default {
|
|||||||
},
|
},
|
||||||
seriesId() {
|
seriesId() {
|
||||||
return this.series ? this.series.id : null
|
return this.series ? this.series.id : null
|
||||||
},
|
|
||||||
hasValidCovers() {
|
|
||||||
var validCovers = this.books.map((bookItem) => bookItem.book.cover)
|
|
||||||
return !!validCovers.length
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
bookCoverAspectRatio() {
|
bookCoverAspectRatio() {
|
||||||
return this.$store.getters['getBookCoverAspectRatio']
|
return this.$store.getters['libraries/getBookCoverAspectRatio']
|
||||||
},
|
},
|
||||||
name() {
|
name() {
|
||||||
return this.series.name
|
return this.series.name
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async connectToServer(config) {
|
async connectToServer(config) {
|
||||||
|
await this.$hapticsImpact()
|
||||||
console.log('[ServerConnectForm] connectToServer', config.address)
|
console.log('[ServerConnectForm] connectToServer', config.address)
|
||||||
this.processing = true
|
this.processing = true
|
||||||
this.serverConfig = {
|
this.serverConfig = {
|
||||||
@@ -159,6 +160,7 @@ export default {
|
|||||||
},
|
},
|
||||||
async removeServerConfigClick() {
|
async removeServerConfigClick() {
|
||||||
if (!this.serverConfig.id) return
|
if (!this.serverConfig.id) return
|
||||||
|
await this.$hapticsImpact()
|
||||||
|
|
||||||
const { value } = await Dialog.confirm({
|
const { value } = await Dialog.confirm({
|
||||||
title: 'Confirm',
|
title: 'Confirm',
|
||||||
@@ -189,7 +191,8 @@ export default {
|
|||||||
this.showForm = true
|
this.showForm = true
|
||||||
this.showAuth = true
|
this.showAuth = true
|
||||||
},
|
},
|
||||||
newServerConfigClick() {
|
async newServerConfigClick() {
|
||||||
|
await this.$hapticsImpact()
|
||||||
this.serverConfig = {
|
this.serverConfig = {
|
||||||
address: '',
|
address: '',
|
||||||
userId: '',
|
userId: '',
|
||||||
@@ -348,4 +351,4 @@ export default {
|
|||||||
this.init()
|
this.init()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ export default {
|
|||||||
default: 120
|
default: 120
|
||||||
},
|
},
|
||||||
bookCoverAspectRatio: Number,
|
bookCoverAspectRatio: Number,
|
||||||
downloadCover: String
|
downloadCover: String,
|
||||||
|
raw: Boolean
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@@ -115,7 +116,7 @@ export default {
|
|||||||
if (this.downloadCover) return this.downloadCover
|
if (this.downloadCover) return this.downloadCover
|
||||||
if (!this.libraryItem) return null
|
if (!this.libraryItem) return null
|
||||||
var store = this.$store || this.$nuxt.$store
|
var store = this.$store || this.$nuxt.$store
|
||||||
return store.getters['globals/getLibraryItemCoverSrc'](this.libraryItem, this.placeholderUrl)
|
return store.getters['globals/getLibraryItemCoverSrc'](this.libraryItem, this.placeholderUrl, this.raw)
|
||||||
},
|
},
|
||||||
cover() {
|
cover() {
|
||||||
return this.media.coverPath || this.placeholderUrl
|
return this.media.coverPath || this.placeholderUrl
|
||||||
@@ -168,6 +169,8 @@ export default {
|
|||||||
this.showCoverBg = false
|
this.showCoverBg = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.$emit('imageLoaded', this.fullCoverUrl)
|
||||||
},
|
},
|
||||||
imageError(err) {
|
imageError(err) {
|
||||||
this.loading = false
|
this.loading = false
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative rounded-sm overflow-hidden" :style="{ width: width + 'px', height: height + 'px' }">
|
||||||
|
<div v-if="items.length" class="flex flex-wrap justify-center h-full relative bg-primary bg-opacity-95 rounded-sm">
|
||||||
|
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||||
|
<covers-book-cover v-for="(li, index) in libraryItemCovers" :key="index" :library-item="li" :width="itemCoverWidth" :book-cover-aspect-ratio="1" />
|
||||||
|
</div>
|
||||||
|
<div v-else class="relative w-full h-full flex items-center justify-center p-2 bg-primary rounded-sm">
|
||||||
|
<div class="absolute top-0 left-0 w-full h-full bg-gray-400 bg-opacity-5" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
width: Number,
|
||||||
|
height: Number
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
sizeMultiplier() {
|
||||||
|
return this.width / (120 * 1.6 * 2)
|
||||||
|
},
|
||||||
|
itemCoverWidth() {
|
||||||
|
if (this.libraryItemCovers.length === 1) return this.width
|
||||||
|
return this.width / 2
|
||||||
|
},
|
||||||
|
libraryItemCovers() {
|
||||||
|
if (!this.items.length) return []
|
||||||
|
if (this.items.length === 1) return [this.items[0].libraryItem]
|
||||||
|
|
||||||
|
const covers = []
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
let index = i % this.items.length
|
||||||
|
if (this.items.length === 2 && i >= 2) index = (i + 1) % 2 // for playlists with 2 items show covers in checker pattern
|
||||||
|
|
||||||
|
covers.push(this.items[index].libraryItem)
|
||||||
|
}
|
||||||
|
return covers
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
<template>
|
||||||
|
<div class="relative rounded-sm" :style="{ height: width * bookCoverAspectRatio + 'px', width: width + 'px', maxWidth: width + 'px', minWidth: width + 'px' }" @mouseover="isHovering = true" @mouseleave="isHovering = false">
|
||||||
|
<div class="w-full h-full relative overflow-hidden">
|
||||||
|
<div v-show="showCoverBg" class="absolute top-0 left-0 w-full h-full overflow-hidden rounded-sm bg-primary">
|
||||||
|
<div class="absolute cover-bg" ref="coverBg" />
|
||||||
|
</div>
|
||||||
|
<img ref="cover" :src="cover" @error="imageError" @load="imageLoaded" class="w-full h-full absolute top-0 left-0" :class="showCoverBg ? 'object-contain' : 'object-fill'" />
|
||||||
|
|
||||||
|
<a v-if="!imageFailed && showOpenNewTab && isHovering" :href="cover" @click.stop target="_blank" class="absolute bg-primary flex items-center justify-center shadow-sm rounded-full hover:scale-110 transform duration-100" :style="{ top: sizeMultiplier * 0.5 + 'rem', right: sizeMultiplier * 0.5 + 'rem', width: 2.5 * sizeMultiplier + 'rem', height: 2.5 * sizeMultiplier + 'rem' }">
|
||||||
|
<span class="material-icons" :style="{ fontSize: sizeMultiplier * 1.75 + 'rem' }">open_in_new</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="imageFailed" class="absolute top-0 left-0 right-0 bottom-0 w-full h-full bg-red-100" :style="{ padding: placeholderCoverPadding + 'rem' }">
|
||||||
|
<div class="w-full h-full border-2 border-error flex flex-col items-center justify-center">
|
||||||
|
<img src="/Logo.png" class="mb-2" :style="{ height: 64 * sizeMultiplier + 'px' }" />
|
||||||
|
<p class="text-center font-book text-error" :style="{ fontSize: sizeMultiplier + 'rem' }">Invalid Cover</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="!imageFailed && showResolution" class="absolute -bottom-5 left-0 right-0 mx-auto text-xs text-gray-300 text-center">{{ resolution }}</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
src: String,
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: 120
|
||||||
|
},
|
||||||
|
showOpenNewTab: Boolean,
|
||||||
|
bookCoverAspectRatio: Number,
|
||||||
|
showResolution: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
imageFailed: false,
|
||||||
|
showCoverBg: false,
|
||||||
|
isHovering: false,
|
||||||
|
naturalHeight: 0,
|
||||||
|
naturalWidth: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
cover() {
|
||||||
|
this.imageFailed = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
cover() {
|
||||||
|
return this.src
|
||||||
|
},
|
||||||
|
sizeMultiplier() {
|
||||||
|
return this.width / 120
|
||||||
|
},
|
||||||
|
placeholderCoverPadding() {
|
||||||
|
return 0.8 * this.sizeMultiplier
|
||||||
|
},
|
||||||
|
resolution() {
|
||||||
|
return `${this.naturalWidth}x${this.naturalHeight}px`
|
||||||
|
},
|
||||||
|
placeholderUrl() {
|
||||||
|
const config = this.$config || this.$nuxt.$config
|
||||||
|
return `${config.routerBasePath}/book_placeholder.jpg`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setCoverBg() {
|
||||||
|
if (this.$refs.coverBg) {
|
||||||
|
this.$refs.coverBg.style.backgroundImage = `url("${this.src}")`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
imageLoaded() {
|
||||||
|
if (this.$refs.cover && this.src !== this.placeholderUrl) {
|
||||||
|
var { naturalWidth, naturalHeight } = this.$refs.cover
|
||||||
|
this.naturalHeight = naturalHeight
|
||||||
|
this.naturalWidth = naturalWidth
|
||||||
|
|
||||||
|
var aspectRatio = naturalHeight / naturalWidth
|
||||||
|
var arDiff = Math.abs(aspectRatio - this.bookCoverAspectRatio)
|
||||||
|
|
||||||
|
// If image aspect ratio is <= 1.45 or >= 1.75 then use cover bg, otherwise stretch to fit
|
||||||
|
if (arDiff > 0.15) {
|
||||||
|
this.showCoverBg = true
|
||||||
|
this.$nextTick(this.setCoverBg)
|
||||||
|
} else {
|
||||||
|
this.showCoverBg = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
imageError(err) {
|
||||||
|
console.error('ImgError', err)
|
||||||
|
this.imageFailed = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full px-2">
|
||||||
|
<img v-if="podcast.imageUrl" :src="podcast.imageUrl" class="h-36 w-36 object-contain mx-auto mb-2" />
|
||||||
|
|
||||||
|
<ui-text-input-with-label v-model="podcast.title" :label="'Title'" class="mb-2 text-sm" @input="titleUpdated" />
|
||||||
|
|
||||||
|
<ui-text-input-with-label v-model="podcast.author" :label="'Author'" class="mb-2 text-sm" />
|
||||||
|
|
||||||
|
<ui-text-input-with-label v-model="podcast.feedUrl" :label="'Feed URL'" readonly class="mb-2 text-sm" />
|
||||||
|
|
||||||
|
<ui-multi-select v-model="podcast.genres" :items="podcast.genres" :label="'Genres'" class="mb-2 text-sm" />
|
||||||
|
|
||||||
|
<ui-textarea-with-label v-model="podcast.description" :label="'Description'" :rows="3" class="mb-2 text-sm" />
|
||||||
|
|
||||||
|
<ui-dropdown v-model="selectedFolderId" :items="folderItems" :disabled="processing" :label="'Folder'" class="mb-2 text-sm" @input="folderUpdated" />
|
||||||
|
|
||||||
|
<ui-text-input-with-label v-model="fullPath" :label="'Podcast Path'" input-class="h-10" readonly class="mb-2 text-sm" />
|
||||||
|
|
||||||
|
<div class="flex items-center py-4 px-2">
|
||||||
|
<ui-checkbox v-model="podcast.autoDownloadEpisodes" :label="'Auto Download Episodes'" checkbox-bg="primary" border-color="gray-600" label-class="pl-2 text-sm font-semibold" />
|
||||||
|
<div class="flex-grow" />
|
||||||
|
<ui-btn color="success" @click="submit">Submit</ui-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Path from 'path'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
processing: Boolean,
|
||||||
|
podcastData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
},
|
||||||
|
podcastFeedData: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
selectedFolderId: null,
|
||||||
|
fullPath: null,
|
||||||
|
podcast: {
|
||||||
|
title: '',
|
||||||
|
author: '',
|
||||||
|
description: '',
|
||||||
|
releaseDate: '',
|
||||||
|
genres: [],
|
||||||
|
feedUrl: '',
|
||||||
|
feedImageUrl: '',
|
||||||
|
itunesPageUrl: '',
|
||||||
|
itunesId: '',
|
||||||
|
itunesArtistId: '',
|
||||||
|
autoDownloadEpisodes: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
_processing: {
|
||||||
|
get() {
|
||||||
|
return this.processing
|
||||||
|
},
|
||||||
|
set(val) {
|
||||||
|
this.$emit('update:processing', val)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title() {
|
||||||
|
return this._podcastData.title
|
||||||
|
},
|
||||||
|
currentLibrary() {
|
||||||
|
return this.$store.getters['libraries/getCurrentLibrary']
|
||||||
|
},
|
||||||
|
folders() {
|
||||||
|
if (!this.currentLibrary) return []
|
||||||
|
return this.currentLibrary.folders || []
|
||||||
|
},
|
||||||
|
folderItems() {
|
||||||
|
return this.folders.map((fold) => {
|
||||||
|
return {
|
||||||
|
value: fold.id,
|
||||||
|
text: fold.fullPath
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
_podcastData() {
|
||||||
|
return this.podcastData || {}
|
||||||
|
},
|
||||||
|
feedMetadata() {
|
||||||
|
if (!this.podcastFeedData) return {}
|
||||||
|
return this.podcastFeedData.metadata || {}
|
||||||
|
},
|
||||||
|
episodes() {
|
||||||
|
if (!this.podcastFeedData) return []
|
||||||
|
return this.podcastFeedData.episodes || []
|
||||||
|
},
|
||||||
|
selectedFolder() {
|
||||||
|
return this.folders.find((f) => f.id === this.selectedFolderId)
|
||||||
|
},
|
||||||
|
selectedFolderPath() {
|
||||||
|
if (!this.selectedFolder) return ''
|
||||||
|
return this.selectedFolder.fullPath
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
titleUpdated() {
|
||||||
|
this.folderUpdated()
|
||||||
|
},
|
||||||
|
folderUpdated() {
|
||||||
|
if (!this.selectedFolderPath || !this.podcast.title) {
|
||||||
|
this.fullPath = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.fullPath = Path.join(this.selectedFolderPath, this.$sanitizeFilename(this.podcast.title))
|
||||||
|
},
|
||||||
|
submit() {
|
||||||
|
const podcastPayload = {
|
||||||
|
path: this.fullPath,
|
||||||
|
folderId: this.selectedFolderId,
|
||||||
|
libraryId: this.currentLibrary.id,
|
||||||
|
media: {
|
||||||
|
metadata: {
|
||||||
|
title: this.podcast.title,
|
||||||
|
author: this.podcast.author,
|
||||||
|
description: this.podcast.description,
|
||||||
|
releaseDate: this.podcast.releaseDate,
|
||||||
|
genres: [...this.podcast.genres],
|
||||||
|
feedUrl: this.podcast.feedUrl,
|
||||||
|
imageUrl: this.podcast.imageUrl,
|
||||||
|
itunesPageUrl: this.podcast.itunesPageUrl,
|
||||||
|
itunesId: this.podcast.itunesId,
|
||||||
|
itunesArtistId: this.podcast.itunesArtistId,
|
||||||
|
language: this.podcast.language
|
||||||
|
},
|
||||||
|
autoDownloadEpisodes: this.podcast.autoDownloadEpisodes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
console.log('Podcast payload', podcastPayload)
|
||||||
|
|
||||||
|
this._processing = true
|
||||||
|
this.$axios
|
||||||
|
.$post('/api/podcasts', podcastPayload)
|
||||||
|
.then((libraryItem) => {
|
||||||
|
this._processing = false
|
||||||
|
this.$toast.success('Podcast added')
|
||||||
|
this.$router.push(`/item/${libraryItem.id}`)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
var errorMsg = error.response && error.response.data ? error.response.data : 'Failed to add podcast'
|
||||||
|
console.error('Failed to create podcast', error)
|
||||||
|
this._processing = false
|
||||||
|
this.$toast.error(errorMsg)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
init() {
|
||||||
|
// Prefer using itunes podcast data but not always passed in if manually entering rss feed
|
||||||
|
this.podcast.title = this._podcastData.title || this.feedMetadata.title || ''
|
||||||
|
this.podcast.author = this._podcastData.artistName || this.feedMetadata.author || ''
|
||||||
|
this.podcast.description = this._podcastData.description || this.feedMetadata.descriptionPlain || ''
|
||||||
|
this.podcast.releaseDate = this._podcastData.releaseDate || ''
|
||||||
|
this.podcast.genres = this._podcastData.genres || this.feedMetadata.categories || []
|
||||||
|
this.podcast.feedUrl = this._podcastData.feedUrl || this.feedMetadata.feedUrl || ''
|
||||||
|
this.podcast.imageUrl = this._podcastData.cover || this.feedMetadata.image || ''
|
||||||
|
this.podcast.itunesPageUrl = this._podcastData.pageUrl || ''
|
||||||
|
this.podcast.itunesId = this._podcastData.id || ''
|
||||||
|
this.podcast.itunesArtistId = this._podcastData.artistId || ''
|
||||||
|
this.podcast.language = this._podcastData.language || ''
|
||||||
|
this.podcast.autoDownloadEpisodes = false
|
||||||
|
|
||||||
|
if (this.folderItems[0]) {
|
||||||
|
this.selectedFolderId = this.folderItems[0].value
|
||||||
|
this.folderUpdated()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="w-full h-9 bg-bg relative">
|
<div class="w-full h-9 bg-bg relative">
|
||||||
<div id="bookshelf-navbar" class="absolute z-10 top-0 left-0 w-full h-full flex bg-secondary text-gray-200">
|
<div id="bookshelf-navbar" class="absolute z-10 top-0 left-0 w-full h-full flex bg-secondary text-gray-200">
|
||||||
<nuxt-link v-for="item in items" :key="item.to" :to="item.to" class="h-full flex items-center justify-center" :style="{ width: isPodcast ? '50%' : '25%' }" :class="routeName === item.routeName ? 'bg-primary' : 'text-gray-400'">
|
<nuxt-link v-for="item in items" :key="item.to" :to="item.to" class="h-full flex-grow flex items-center justify-center" :class="routeName === item.routeName ? 'bg-primary' : 'text-gray-400'">
|
||||||
<p>{{ item.text }}</p>
|
<p v-if="routeName === item.routeName" class="text-sm font-semibold">{{ item.text }}</p>
|
||||||
|
<span v-else-if="item.iconPack === 'abs-icons'" class="abs-icons" :class="`icon-${item.icon} ${item.iconClass || ''}`"></span>
|
||||||
|
<span v-else :class="`${item.iconPack} ${item.iconClass || ''}`">{{ item.icon }}</span>
|
||||||
</nuxt-link>
|
</nuxt-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -14,43 +16,114 @@ export default {
|
|||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
currentLibrary() {
|
||||||
|
return this.$store.getters['libraries/getCurrentLibrary']
|
||||||
|
},
|
||||||
|
currentLibraryIcon() {
|
||||||
|
return this.currentLibrary ? this.currentLibrary.icon : 'database'
|
||||||
|
},
|
||||||
|
userHasPlaylists() {
|
||||||
|
return this.$store.state.libraries.numUserPlaylists
|
||||||
|
},
|
||||||
|
userIsAdminOrUp() {
|
||||||
|
return this.$store.getters['user/getIsAdminOrUp']
|
||||||
|
},
|
||||||
items() {
|
items() {
|
||||||
|
let items = []
|
||||||
if (this.isPodcast) {
|
if (this.isPodcast) {
|
||||||
return [
|
items = [
|
||||||
{
|
{
|
||||||
to: '/bookshelf',
|
to: '/bookshelf',
|
||||||
routeName: 'bookshelf',
|
routeName: 'bookshelf',
|
||||||
|
iconPack: 'abs-icons',
|
||||||
|
icon: 'home',
|
||||||
|
iconClass: 'text-xl',
|
||||||
|
text: 'Home'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: '/bookshelf/latest',
|
||||||
|
routeName: 'bookshelf-latest',
|
||||||
|
iconPack: 'abs-icons',
|
||||||
|
icon: 'list',
|
||||||
|
iconClass: 'text-xl',
|
||||||
|
text: 'Latest'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: '/bookshelf/library',
|
||||||
|
routeName: 'bookshelf-library',
|
||||||
|
iconPack: 'abs-icons',
|
||||||
|
icon: this.currentLibraryIcon,
|
||||||
|
iconClass: 'text-lg',
|
||||||
|
text: 'Library'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
if (this.userIsAdminOrUp) {
|
||||||
|
items.push({
|
||||||
|
to: '/bookshelf/search',
|
||||||
|
routeName: 'bookshelf-search',
|
||||||
|
iconPack: 'material-icons',
|
||||||
|
icon: 'podcasts',
|
||||||
|
iconClass: 'text-xl',
|
||||||
|
text: 'Search'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
items = [
|
||||||
|
{
|
||||||
|
to: '/bookshelf',
|
||||||
|
routeName: 'bookshelf',
|
||||||
|
iconPack: 'abs-icons',
|
||||||
|
icon: 'home',
|
||||||
|
iconClass: 'text-xl',
|
||||||
text: 'Home'
|
text: 'Home'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
to: '/bookshelf/library',
|
to: '/bookshelf/library',
|
||||||
routeName: 'bookshelf-library',
|
routeName: 'bookshelf-library',
|
||||||
|
iconPack: 'abs-icons',
|
||||||
|
icon: this.currentLibraryIcon,
|
||||||
|
iconClass: 'text-lg',
|
||||||
text: 'Library'
|
text: 'Library'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: '/bookshelf/series',
|
||||||
|
routeName: 'bookshelf-series',
|
||||||
|
iconPack: 'abs-icons',
|
||||||
|
icon: 'columns',
|
||||||
|
iconClass: 'text-lg pt-px',
|
||||||
|
text: 'Series'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
to: '/bookshelf/collections',
|
||||||
|
routeName: 'bookshelf-collections',
|
||||||
|
iconPack: 'material-icons-outlined',
|
||||||
|
icon: 'collections_bookmark',
|
||||||
|
iconClass: 'text-xl',
|
||||||
|
text: 'Collections'
|
||||||
}
|
}
|
||||||
|
// {
|
||||||
|
// to: '/bookshelf/authors',
|
||||||
|
// routeName: 'bookshelf-authors',
|
||||||
|
// iconPack: 'abs-icons',
|
||||||
|
// icon: 'authors',
|
||||||
|
// iconClass: 'text-2xl pb-px',
|
||||||
|
// text: 'Authors'
|
||||||
|
// }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
return [
|
|
||||||
{
|
if (this.userHasPlaylists) {
|
||||||
to: '/bookshelf',
|
items.push({
|
||||||
routeName: 'bookshelf',
|
to: '/bookshelf/playlists',
|
||||||
text: 'Home'
|
routeName: 'bookshelf-playlists',
|
||||||
},
|
iconPack: 'material-icons',
|
||||||
{
|
icon: 'queue_music',
|
||||||
to: '/bookshelf/library',
|
text: 'Playlists'
|
||||||
routeName: 'bookshelf-library',
|
})
|
||||||
text: 'Library'
|
}
|
||||||
},
|
|
||||||
{
|
return items
|
||||||
to: '/bookshelf/series',
|
|
||||||
routeName: 'bookshelf-series',
|
|
||||||
text: 'Series'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
to: '/bookshelf/collections',
|
|
||||||
routeName: 'bookshelf-collections',
|
|
||||||
text: 'Collections'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
routeName() {
|
routeName() {
|
||||||
return this.$route.name
|
return this.$route.name
|
||||||
@@ -62,7 +135,9 @@ export default {
|
|||||||
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
return this.$store.getters['libraries/getCurrentLibraryMediaType']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {},
|
methods: {
|
||||||
|
isSelected(item) {}
|
||||||
|
},
|
||||||
mounted() {}
|
mounted() {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<p v-show="!selectedSeriesName" class="font-book pt-1">{{ totalEntities }} {{ entityTitle }}</p>
|
<p v-show="!selectedSeriesName" class="font-book pt-1">{{ totalEntities }} {{ entityTitle }}</p>
|
||||||
<p v-show="selectedSeriesName" class="ml-2 font-book pt-1">{{ selectedSeriesName }} ({{ totalEntities }})</p>
|
<p v-show="selectedSeriesName" class="ml-2 font-book pt-1">{{ selectedSeriesName }} ({{ totalEntities }})</p>
|
||||||
<div class="flex-grow" />
|
<div class="flex-grow" />
|
||||||
<span v-if="page == 'library' || seriesBookPage" class="material-icons px-2" @click="bookshelfListView = !bookshelfListView">{{ !bookshelfListView ? 'view_list' : 'grid_view' }}</span>
|
<span v-if="page == 'library' || seriesBookPage" class="material-icons px-2" @click="changeView">{{ !bookshelfListView ? 'view_list' : 'grid_view' }}</span>
|
||||||
<template v-if="page === 'library'">
|
<template v-if="page === 'library'">
|
||||||
<div class="relative flex items-center px-2">
|
<div class="relative flex items-center px-2">
|
||||||
<span class="material-icons" @click="showFilterModal = true">filter_alt</span>
|
<span class="material-icons" @click="showFilterModal = true">filter_alt</span>
|
||||||
@@ -64,6 +64,8 @@ export default {
|
|||||||
return 'Series'
|
return 'Series'
|
||||||
} else if (this.page === 'collections') {
|
} else if (this.page === 'collections') {
|
||||||
return 'Collections'
|
return 'Collections'
|
||||||
|
} else if (this.page === 'playlists') {
|
||||||
|
return 'Playlists'
|
||||||
}
|
}
|
||||||
return ''
|
return ''
|
||||||
},
|
},
|
||||||
@@ -85,8 +87,7 @@ export default {
|
|||||||
this.saveSettings()
|
this.saveSettings()
|
||||||
},
|
},
|
||||||
saveSettings() {
|
saveSettings() {
|
||||||
this.$store.commit('user/setSettings', this.settings) // Immediate update
|
this.$store.dispatch('user/updateUserSettings', this.settings)
|
||||||
this.$store.dispatch('user/updateUserSettings', this.settings) // TODO: No need to update settings on server...
|
|
||||||
},
|
},
|
||||||
async init() {
|
async init() {
|
||||||
this.bookshelfListView = await this.$localStore.getBookshelfListView()
|
this.bookshelfListView = await this.$localStore.getBookshelfListView()
|
||||||
@@ -100,16 +101,20 @@ export default {
|
|||||||
},
|
},
|
||||||
setTotalEntities(total) {
|
setTotalEntities(total) {
|
||||||
this.totalEntities = total
|
this.totalEntities = total
|
||||||
|
},
|
||||||
|
async changeView() {
|
||||||
|
this.bookshelfListView = !this.bookshelfListView
|
||||||
|
await this.$hapticsImpact()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.init()
|
this.init()
|
||||||
this.$eventBus.$on('bookshelf-total-entities', this.setTotalEntities)
|
this.$eventBus.$on('bookshelf-total-entities', this.setTotalEntities)
|
||||||
this.$store.commit('user/addSettingsListener', { id: 'bookshelftoolbar', meth: this.settingsUpdated })
|
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
this.$eventBus.$off('bookshelf-total-entities', this.setTotalEntities)
|
this.$eventBus.$off('bookshelf-total-entities', this.setTotalEntities)
|
||||||
this.$store.commit('user/removeSettingsListener', 'bookshelftoolbar')
|
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -118,4 +123,4 @@ export default {
|
|||||||
#bookshelf-toolbar {
|
#bookshelf-toolbar {
|
||||||
box-shadow: 0px 5px 5px #11111155;
|
box-shadow: 0px 5px 5px #11111155;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
<template>
|
|
||||||
<svg fill="currentColor" class="h-full w-full p-px" viewBox="0 0 1978.03 2349.44">
|
|
||||||
<path
|
|
||||||
d="M2519.5,1438.39c-12.13-10.1-31-25-56.57-42.62V1197.31c0-505.94-410.15-916.09-916.1-916.09h0c-505.94,0-916.09,410.15-916.09,916.09v198.46c-25.57,17.66-44.44,32.52-56.57,42.62a45.45,45.45,0,0,0-16.35,34.95v237.74a45.45,45.45,0,0,0,16.35,35c28.28,23.54,93.18,72.92,194.22,123.55v23.11c0,62.32,40.21,112.85,89.8,112.85h0c49.59,0,89.8-50.53,89.8-112.85V1322.51c0-62.33-40.21-112.86-89.8-112.86h0c-47.51,0-86.4,46.38-89.58,105.07l-.22.11V1197.31c0-429.92,348.52-778.43,778.44-778.43h0c429.92,0,778.44,348.51,778.44,778.43v117.52l-.22-.11c-3.18-58.69-42.06-105.07-89.58-105.07h0c-49.59,0-89.79,50.53-89.79,112.86v570.18c0,62.32,40.2,112.85,89.79,112.85h0c49.6,0,89.8-50.53,89.8-112.85v-23.11c101.05-50.63,165.95-100,194.23-123.55a45.48,45.48,0,0,0,16.35-35V1473.34A45.48,45.48,0,0,0,2519.5,1438.39Z"
|
|
||||||
transform="translate(-557.82 -281.22)"
|
|
||||||
/>
|
|
||||||
<path d="M1227.4,2429.63a108.47,108.47,0,0,0,108.47-108.47V1106.56A108.47,108.47,0,0,0,1227.4,998.08H1115.33a108.48,108.48,0,0,0-108.48,108.48v1214.6a108.47,108.47,0,0,0,108.48,108.47ZM1047.75,1289.38H1295v25.83H1047.75Z" transform="translate(-557.82 -281.22)" />
|
|
||||||
<path d="M1602.87,2429.63a108.47,108.47,0,0,0,108.47-108.47V1106.56a108.47,108.47,0,0,0-108.47-108.48H1490.8a108.48,108.48,0,0,0-108.48,108.48v1214.6a108.47,108.47,0,0,0,108.48,108.47ZM1423.22,1289.38h247.22v25.83H1423.22Z" transform="translate(-557.82 -281.22)" />
|
|
||||||
<path d="M1978.34,2429.63a108.47,108.47,0,0,0,108.47-108.47V1106.56a108.47,108.47,0,0,0-108.47-108.48H1866.27a108.48,108.48,0,0,0-108.48,108.48v1214.6a108.47,108.47,0,0,0,108.48,108.47ZM1798.69,1289.38h247.22v25.83H1798.69Z" transform="translate(-557.82 -281.22)" />
|
|
||||||
<rect x="180.05" y="2185.95" width="1617.93" height="163.49" rx="81.74" />
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
computed: {},
|
|
||||||
methods: {},
|
|
||||||
mounted() {}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<template>
|
|
||||||
<svg fill="currentColor" viewBox="0 0 24 24">
|
|
||||||
<path fill="currentColor" d="M9 3V18H12V3H9M12 5L16 18L19 17L15 4L12 5M5 5V18H8V5H5M3 19V21H21V19H3Z" />
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
computed: {},
|
|
||||||
methods: {},
|
|
||||||
mounted() {}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<template>
|
|
||||||
<svg viewBox="0 0 24 24">
|
|
||||||
<path fill="currentColor" d="M6,19L9,15.14L11.14,17.72L14.14,13.86L18,19H6M6,4H11V12L8.5,10.5L6,12M18,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V4A2,2 0 0,0 18,2Z" />
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
computed: {},
|
|
||||||
methods: {},
|
|
||||||
mounted() {}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
<template>
|
|
||||||
<svg class="p-px" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" />
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
data() {
|
|
||||||
return {}
|
|
||||||
},
|
|
||||||
computed: {},
|
|
||||||
methods: {},
|
|
||||||
mounted() {}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||