Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdbbd56a58 | ||
|
|
d6a1eba657 | ||
|
|
da899bfddd | ||
|
|
b7b746fb2a | ||
|
|
c8053355df | ||
|
|
1ddb6eca00 | ||
|
|
f42c624cba | ||
|
|
f8fbeef9b7 | ||
|
|
46b82a81ce | ||
|
|
48342e5bd3 | ||
|
|
147f89f870 | ||
|
|
8b39ba9c92 | ||
|
|
13d32dc1f7 | ||
|
|
0d1312e497 | ||
|
|
ff4f8324e7 | ||
|
|
b4bf10d409 | ||
|
|
ae37861f7d | ||
|
|
1da03cdd65 | ||
|
|
a7443d0992 | ||
|
|
5a953464ab | ||
|
|
953bde5a42 | ||
|
|
481d0d0285 | ||
|
|
7b3a005ad6 | ||
|
|
4f9d341f69 | ||
|
|
9e7d1d1617 | ||
|
|
4f03c2a35d | ||
|
|
db3c5396e9 | ||
|
|
455af239a1 | ||
|
|
d8bc26f5f8 | ||
|
|
543ac209e4 | ||
|
|
c8b5cefeb5 | ||
|
|
f1411011e6 | ||
|
|
f3964dda17 | ||
|
|
d207e88e18 | ||
|
|
2643e98fc0 | ||
|
|
410648e316 | ||
|
|
373221703d | ||
|
|
fbcb8620f9 | ||
|
|
a8ac4970cc | ||
|
|
195e33ae32 | ||
|
|
9acf695802 | ||
|
|
e6aaccfc74 | ||
|
|
87c74fe78b | ||
|
|
7dc319c1bd | ||
|
|
c093548b71 | ||
|
|
b067a2d3d5 | ||
|
|
a9ae7bfdb8 | ||
|
|
9dcf22614c | ||
|
|
c7564d439e | ||
|
|
157dc01673 | ||
|
|
cce6e1d0ab | ||
|
|
b935cb5cf1 | ||
|
|
9ab37ac525 | ||
|
|
7031c895b5 | ||
|
|
7c739bf3bf | ||
|
|
facf56930e | ||
|
|
2c3dff3544 | ||
|
|
b1bf68b8cd | ||
|
|
78aade7694 | ||
|
|
83a4474375 | ||
|
|
1b47c38efa | ||
|
|
922d9f22ce | ||
|
|
d7feb52dd8 | ||
|
|
fafaad8b5f | ||
|
|
928f33005a | ||
|
|
0448d20583 | ||
|
|
f9d408b33f | ||
|
|
9386ae95a3 | ||
|
|
c530e0f7e3 | ||
|
|
e39a5e1ba3 | ||
|
|
cb9e317e74 | ||
|
|
8061ac0ced | ||
|
|
daff8355d7 | ||
|
|
231289e9ac | ||
|
|
87df022af0 | ||
|
|
07fdac9746 | ||
|
|
0d482aaa42 | ||
|
|
d48d1fdde3 | ||
|
|
65b69cc020 | ||
|
|
a3794a6a6a | ||
|
|
695ab84940 | ||
|
|
5399ec67d7 | ||
|
|
d9a6ec65df | ||
|
|
e842de78dd | ||
|
|
260363bf4f | ||
|
|
6d7dffd4de | ||
|
|
ef1d8a8eb4 | ||
|
|
cdd7a1a636 | ||
|
|
6584cd1522 | ||
|
|
d01b602306 | ||
|
|
c90f2c136a | ||
|
|
15ccc7192e | ||
|
|
e4345a4438 | ||
|
|
3f4d564bad | ||
|
|
3311f66da8 | ||
|
|
e711ada0b3 | ||
|
|
840979d635 | ||
|
|
53c9109534 | ||
|
|
87ca6018d3 | ||
|
|
c590087578 | ||
|
|
3963ffa0e2 | ||
|
|
eaa0c9914a | ||
|
|
d9b0b8c33d | ||
|
|
fb4e7e6b55 | ||
|
|
78a063a669 | ||
|
|
cb5d32bbb9 | ||
|
|
f594b734c0 | ||
|
|
bf62b82037 | ||
|
|
39fbb0902e | ||
|
|
7818500f0f | ||
|
|
1ea984b915 | ||
|
|
f28405081a | ||
|
|
317f00e1b8 | ||
|
|
f8ce0b8355 | ||
|
|
9fd6ff9fd2 | ||
|
|
3931020c83 | ||
|
|
8bdb6b0b66 | ||
|
|
33bdee66a3 | ||
|
|
a81f50878e | ||
|
|
322f7d75dd | ||
|
|
2f34da4e99 | ||
|
|
5a9e30b08c | ||
|
|
04e233816d | ||
|
|
3ad1042b13 | ||
|
|
4fb9201823 | ||
|
|
02089e1489 | ||
|
|
da442f4a17 | ||
|
|
a0f8e6fb93 | ||
|
|
20cf5a6132 | ||
|
|
f5ca30620f | ||
|
|
72ec9d81b1 | ||
|
|
29568412e6 | ||
|
|
f94140ec6b | ||
|
|
51062e7403 | ||
|
|
0942bd2548 | ||
|
|
5939fee66f | ||
|
|
68560fd5f5 | ||
|
|
a38b826898 | ||
|
|
0158ccc4c8 | ||
|
|
a5ba6c64c9 | ||
|
|
d67454db9a | ||
|
|
fb35d8b1a0 | ||
|
|
960f75bfba | ||
|
|
83027a5787 | ||
|
|
bc6ef96465 | ||
|
|
ff77dbf3f8 | ||
|
|
3f78fb7a14 | ||
|
|
152904b7c7 | ||
|
|
9214fecc86 | ||
|
|
049397f7ab | ||
|
|
0f42a411a9 | ||
|
|
4575c61118 | ||
|
|
7575a4f883 | ||
|
|
8d4c322bce | ||
|
|
28989f536a | ||
|
|
3318f3501c | ||
|
|
c9ad0c7897 | ||
|
|
018a988124 | ||
|
|
750c370692 | ||
|
|
08ee08c698 | ||
|
|
d899fd4d89 | ||
|
|
aab44d8cee | ||
|
|
2c2fac6e34 | ||
|
|
47ccc54749 | ||
|
|
05d7d0d0d8 | ||
|
|
210fb7b88f | ||
|
|
4eeb7f24d8 | ||
|
|
9496ee1d97 | ||
|
|
f4b05a3236 | ||
|
|
b73763a9ec | ||
|
|
e3aad8c44f | ||
|
|
1e7c298aa2 | ||
|
|
b9652c7cd8 | ||
|
|
04f00d2994 | ||
|
|
3da6728f4c | ||
|
|
5704ca2ae8 | ||
|
|
6cd04e7098 | ||
|
|
290c3560d2 | ||
|
|
40bda3d48b | ||
|
|
63a838dc9d | ||
|
|
127bf7d4e0 | ||
|
|
9af4947232 | ||
|
|
3233a2a078 | ||
|
|
28ede56c7c | ||
|
|
5f6c1f57b0 | ||
|
|
ec704bf501 | ||
|
|
5e0c740f02 | ||
|
|
a185c3abe9 | ||
|
|
e8c6fc635f | ||
|
|
f362db3855 | ||
|
|
9ae584665e | ||
|
|
1aa852c3dc | ||
|
|
ef46ad7a93 | ||
|
|
cbd74d08e1 | ||
|
|
851d0357a0 | ||
|
|
fb5c23d190 | ||
|
|
1743d0e13f | ||
|
|
25af0c4170 | ||
|
|
4bcb77250d | ||
|
|
6114e03cb7 | ||
|
|
9866a787bd | ||
|
|
20e64523b2 | ||
|
|
50a53ec0d1 | ||
|
|
40b2ff5a97 | ||
|
|
48cc000b95 | ||
|
|
75aaf4d784 | ||
|
|
23220ecae6 | ||
|
|
94b4dda94f | ||
|
|
86c5d1a3e9 | ||
|
|
d7be01935f | ||
|
|
b2b647e2f7 | ||
|
|
405cd21c32 | ||
|
|
17a894f5ef | ||
|
|
23c90f4118 | ||
|
|
f215efdcd0 | ||
|
|
2f243787ce | ||
|
|
39909a398e | ||
|
|
5e98a4ff2f | ||
|
|
8710775872 | ||
|
|
99d60eaf9a | ||
|
|
55b7f05685 | ||
|
|
944aa5de66 | ||
|
|
a12eb7efdf | ||
|
|
62a5a2d069 | ||
|
|
ebd65b4fa7 | ||
|
|
152cce2e9e | ||
|
|
ea847a62ff | ||
|
|
2a5ec618f0 | ||
|
|
3fc89377b4 | ||
|
|
93c16e6a62 |
@@ -0,0 +1,81 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang=en>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv=content-type content="text/html; charset=utf-8" />
|
||||||
|
<meta name=viewport content="width=device-width, initial-scale=1">
|
||||||
|
<title>Audiobookshelf :: Testers</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: rgb(55, 56, 56);
|
||||||
|
font-family: Sans;
|
||||||
|
margin: 40px auto;
|
||||||
|
max-width: 500px;
|
||||||
|
color: #ddd;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
width: 100px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-weight: 100;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
#apk {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto auto;
|
||||||
|
justify-content: center;
|
||||||
|
column-gap: 15px;
|
||||||
|
text-align: left;
|
||||||
|
font-family: monospace;
|
||||||
|
background-color: #222;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<a href="https://audiobookshelf.org">
|
||||||
|
<img src=logo.png />
|
||||||
|
</a>
|
||||||
|
<h1>Audiobookshelf :: Testers</h1>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Here you can download the latest version of Audiobookshelf for Android for <b>testing</b>.
|
||||||
|
The app is automatically built for every new patch added to the project.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div id=apk>
|
||||||
|
<div>Built on</div>
|
||||||
|
<div>__DATE__</div>
|
||||||
|
|
||||||
|
<div>Git commit</div>
|
||||||
|
<a href="https://github.com/advplyr/audiobookshelf-app/commit/__COMMIT__">__COMMIT__</a>
|
||||||
|
|
||||||
|
<div>Download</div>
|
||||||
|
<a href="__APK__">__APK__</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Using this requires some technical knowledge to install the APK manually.
|
||||||
|
Regular users should use the releases published on the <a href="https://play.google.com/store/apps/details?id=com.audiobookshelf.app">Play Store</a>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<p>Report bugs, request features, provide feedback, and contribute on <a href="https://github.com/advplyr/audiobookshelf-app/issues">GitHub</a></p>
|
||||||
|
<p>
|
||||||
|
<a href="https://github.com/advplyr/audiobookshelf-app/issues">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" width=32 height=32 viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
name: Build APK
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths-ignore:
|
||||||
|
- 'ios/**'
|
||||||
|
- 'readme.md'
|
||||||
|
pull_request:
|
||||||
|
paths-ignore:
|
||||||
|
- 'ios/**'
|
||||||
|
- 'readme.md'
|
||||||
|
jobs:
|
||||||
|
main:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout sources
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: use Node.js 16.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
|
||||||
|
- name: install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: build Nuxt project
|
||||||
|
run: npm run generate
|
||||||
|
|
||||||
|
- name: copy to Android project
|
||||||
|
run: npx cap sync
|
||||||
|
|
||||||
|
- name: build Android app
|
||||||
|
run: ./android/gradlew assembleDebug -p android --no-daemon
|
||||||
|
|
||||||
|
- name: rename apk
|
||||||
|
working-directory: android/app/build/outputs/apk/debug/
|
||||||
|
run: |
|
||||||
|
build="$(date +%Y%m%d-%H%M%S)-$(git rev-parse --short HEAD)"
|
||||||
|
name="audiobookshelf-${build}.apk"
|
||||||
|
mv -v app-debug.apk "${name}"
|
||||||
|
|
||||||
|
- name: upload app
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: audiobookshelf-apk
|
||||||
|
path: android/app/build/outputs/apk/debug/*.apk
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
name: Publish Test App
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths-ignore:
|
||||||
|
- 'ios/**'
|
||||||
|
- 'readme.md'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: checkout sources
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: use Node.js 16.x
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
|
||||||
|
- name: install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: build Nuxt project
|
||||||
|
run: npm run generate
|
||||||
|
|
||||||
|
- name: copy to Android project
|
||||||
|
run: npx cap sync
|
||||||
|
|
||||||
|
- name: build Android app
|
||||||
|
run: ./android/gradlew assembleDebug -p android --no-daemon
|
||||||
|
|
||||||
|
- name: rename apk
|
||||||
|
working-directory: android/app/build/outputs/apk/debug/
|
||||||
|
run: |
|
||||||
|
build="$(date +%Y%m%d-%H%M%S)-$(git rev-parse --short HEAD)"
|
||||||
|
name="audiobookshelf-${build}.apk"
|
||||||
|
mv -v app-debug.apk "${name}"
|
||||||
|
|
||||||
|
- name: prepare test page ressources
|
||||||
|
run: |
|
||||||
|
mkdir ghpages
|
||||||
|
cp android/app/build/outputs/apk/debug/*apk ghpages/
|
||||||
|
cp static/Logo.png ghpages/logo.png
|
||||||
|
cp .github/testing-page-template.html ghpages/index.html
|
||||||
|
|
||||||
|
- name: build test page
|
||||||
|
working-directory: ghpages
|
||||||
|
run: |
|
||||||
|
sed -i "s/__DATE__/$(date)/g" index.html
|
||||||
|
sed -i "s/__COMMIT__/$(git rev-parse --short HEAD)/g" index.html
|
||||||
|
sed -i "s/__APK__/$(ls *apk)/g" index.html
|
||||||
|
|
||||||
|
- name: upload test page artifact
|
||||||
|
uses: actions/upload-pages-artifact@v1
|
||||||
|
with:
|
||||||
|
path: ./ghpages
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: build
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v1
|
||||||
@@ -21,8 +21,6 @@ kotlin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|
||||||
|
|
||||||
namespace 'com.audiobookshelf.app'
|
namespace 'com.audiobookshelf.app'
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
viewBinding true
|
viewBinding true
|
||||||
@@ -35,8 +33,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 92
|
versionCode 96
|
||||||
versionName "0.9.61-beta"
|
versionName "0.9.65-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.
|
||||||
@@ -74,8 +72,6 @@ configurations.all {
|
|||||||
force("androidx.media:media:$androidx_media_version")
|
force("androidx.media:media:$androidx_media_version")
|
||||||
force("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version")
|
force("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version")
|
||||||
force("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version")
|
force("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version")
|
||||||
|
|
||||||
// failOnVersionConflict()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,9 +84,6 @@ dependencies {
|
|||||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
implementation 'androidx.constraintlayout:constraintlayout:2.1.3'
|
||||||
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||||
|
|
||||||
// testImplementation "junit:junit:$junitVersion"
|
|
||||||
// androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
|
||||||
// androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
|
||||||
implementation project(':capacitor-cordova-android-plugins')
|
implementation project(':capacitor-cordova-android-plugins')
|
||||||
|
|
||||||
implementation "androidx.core:core-ktx:$androidx_core_ktx_version"
|
implementation "androidx.core:core-ktx:$androidx_core_ktx_version"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ android {
|
|||||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation project(':capacitor-app')
|
implementation project(':capacitor-app')
|
||||||
|
implementation project(':capacitor-clipboard')
|
||||||
implementation project(':capacitor-dialog')
|
implementation project(':capacitor-dialog')
|
||||||
implementation project(':capacitor-haptics')
|
implementation project(':capacitor-haptics')
|
||||||
implementation project(':capacitor-network')
|
implementation project(':capacitor-network')
|
||||||
|
|||||||
@@ -21,18 +21,21 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
android:usesCleartextTraffic="true" >
|
android:usesCleartextTraffic="true"
|
||||||
<!-- <receiver-->
|
android:largeHeap="true" >
|
||||||
<!-- android:name=".NewAppWidget"-->
|
|
||||||
<!-- android:exported="true" >-->
|
|
||||||
<!-- <intent-filter>-->
|
|
||||||
<!-- <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />-->
|
|
||||||
<!-- </intent-filter>-->
|
|
||||||
|
|
||||||
<!-- <meta-data-->
|
<!-- Android app widget -->
|
||||||
<!-- android:name="android.appwidget.provider"-->
|
<receiver
|
||||||
<!-- android:resource="@xml/new_app_widget_info" />-->
|
android:name="MediaPlayerWidget"
|
||||||
<!-- </receiver>-->
|
android:exported="false">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.appwidget.provider"
|
||||||
|
android:resource="@xml/media_player_widget_info" />
|
||||||
|
</receiver>
|
||||||
|
|
||||||
<!-- Used by Android Auto -->
|
<!-- Used by Android Auto -->
|
||||||
<meta-data
|
<meta-data
|
||||||
@@ -74,7 +77,7 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<receiver android:name="androidx.media.session.MediaButtonReceiver" android:exported="false">
|
<receiver android:name="androidx.media.session.MediaButtonReceiver" android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
@@ -83,11 +86,22 @@
|
|||||||
<service
|
<service
|
||||||
android:name=".player.PlayerNotificationService"
|
android:name=".player.PlayerNotificationService"
|
||||||
android:enabled="true"
|
android:enabled="true"
|
||||||
android:exported="true" >
|
android:exported="true"
|
||||||
|
android:foregroundServiceType="mediaPlayback">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.media.browse.MediaBrowserService" />
|
<action android:name="android.media.browse.MediaBrowserService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:grantUriPermissions="true"
|
||||||
|
android:exported="false">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
"pkg": "@capacitor/app",
|
"pkg": "@capacitor/app",
|
||||||
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
|
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"pkg": "@capacitor/clipboard",
|
||||||
|
"classpath": "com.capacitorjs.plugins.clipboard.ClipboardPlugin"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"pkg": "@capacitor/dialog",
|
"pkg": "@capacitor/dialog",
|
||||||
"classpath": "com.capacitorjs.plugins.dialog.DialogPlugin"
|
"classpath": "com.capacitorjs.plugins.dialog.DialogPlugin"
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ 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.plugins.AbsDatabase
|
|
||||||
import com.audiobookshelf.app.managers.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.AbsDatabase
|
||||||
import com.audiobookshelf.app.plugins.AbsDownloader
|
import com.audiobookshelf.app.plugins.AbsDownloader
|
||||||
import com.audiobookshelf.app.plugins.AbsFileSystem
|
import com.audiobookshelf.app.plugins.AbsFileSystem
|
||||||
import com.getcapacitor.BridgeActivity
|
import com.getcapacitor.BridgeActivity
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package com.audiobookshelf.app
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.appwidget.AppWidgetProvider
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.net.Uri
|
||||||
|
import android.support.v4.media.session.PlaybackStateCompat
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.RemoteViews
|
||||||
|
import androidx.media.session.MediaButtonReceiver
|
||||||
|
import com.audiobookshelf.app.data.PlaybackSession
|
||||||
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
|
import com.audiobookshelf.app.managers.DbManager
|
||||||
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.request.RequestOptions
|
||||||
|
import com.bumptech.glide.request.target.AppWidgetTarget
|
||||||
|
import com.bumptech.glide.request.transition.Transition
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of App Widget functionality.
|
||||||
|
*/
|
||||||
|
class MediaPlayerWidget : AppWidgetProvider() {
|
||||||
|
val tag = "MediaPlayerWidget"
|
||||||
|
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||||
|
Log.d(tag, "onUpdate $appWidgetIds")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEnabled(context: Context) {
|
||||||
|
Log.i(tag, "onEnabled check context ${context.packageName}")
|
||||||
|
|
||||||
|
DbManager.initialize(context)
|
||||||
|
|
||||||
|
DeviceManager.deviceData.lastPlaybackSession?.let {
|
||||||
|
val appWidgetManager = AppWidgetManager.getInstance(context)
|
||||||
|
val componentName = ComponentName(context, MediaPlayerWidget::class.java)
|
||||||
|
val ids = appWidgetManager.getAppWidgetIds(componentName)
|
||||||
|
Log.d(tag, "Setting initial widget state with last playback session ${it.displayTitle}")
|
||||||
|
for (widgetId in ids) {
|
||||||
|
updateAppWidget(context, appWidgetManager, widgetId, it, false, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter relevant functionality for when the first widget is created
|
||||||
|
DeviceManager.initializeWidgetUpdater(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, playbackSession: PlaybackSession?, isPlaying:Boolean, isAppClosed:Boolean) {
|
||||||
|
val tag = "MediaPlayerWidget"
|
||||||
|
val views = RemoteViews(context.packageName, R.layout.media_player_widget)
|
||||||
|
Log.i(tag, "updateAppWidget ${playbackSession?.displayTitle ?: "No Title"} isPlaying=$isPlaying isAppClosed=$isAppClosed")
|
||||||
|
val wholeWidgetClickI = Intent(context, MainActivity::class.java)
|
||||||
|
wholeWidgetClickI.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
val wholeWidgetClickPI = PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
System.currentTimeMillis().toInt(),
|
||||||
|
wholeWidgetClickI,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
val playPausePI = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PLAY_PAUSE)
|
||||||
|
views.setOnClickPendingIntent(R.id.widgetPlayPauseButton, playPausePI)
|
||||||
|
|
||||||
|
val fastForwardPI = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_FAST_FORWARD)
|
||||||
|
views.setOnClickPendingIntent(R.id.widgetFastForwardButton, fastForwardPI)
|
||||||
|
|
||||||
|
val rewindPI = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_REWIND)
|
||||||
|
views.setOnClickPendingIntent(R.id.widgetRewindButton, rewindPI)
|
||||||
|
|
||||||
|
// Show/Hide button container
|
||||||
|
views.setViewVisibility(R.id.widgetButtonContainer, if (isAppClosed) View.GONE else View.VISIBLE)
|
||||||
|
|
||||||
|
views.setOnClickPendingIntent(R.id.widgetBackground, wholeWidgetClickPI)
|
||||||
|
|
||||||
|
val imageUri = playbackSession?.getCoverUri(context) ?: Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)
|
||||||
|
val awt: AppWidgetTarget = object : AppWidgetTarget(context.applicationContext, R.id.widgetAlbumArt, views, appWidgetId) {
|
||||||
|
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
||||||
|
super.onResourceReady(resource, transition)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val artist = playbackSession?.displayAuthor ?: "Unknown"
|
||||||
|
views.setTextViewText(R.id.widgetArtistText, artist)
|
||||||
|
|
||||||
|
val title = playbackSession?.displayTitle ?: "Unknown"
|
||||||
|
views.setTextViewText(R.id.widgetMediaTitle, title)
|
||||||
|
|
||||||
|
val options = RequestOptions().override(300, 300).placeholder(R.drawable.icon).error(R.drawable.icon)
|
||||||
|
Glide.with(context.applicationContext).asBitmap().load(imageUri).apply(options).into(awt)
|
||||||
|
|
||||||
|
Log.i(tag, "Update App Widget | Is Playing=$isPlaying | isAppClosed=$isAppClosed")
|
||||||
|
|
||||||
|
val playPauseResource = if (isPlaying) R.drawable.ic_media_pause_dark else R.drawable.ic_media_play_dark
|
||||||
|
views.setImageViewResource(R.id.widgetPlayPauseButton, playPauseResource)
|
||||||
|
|
||||||
|
// Instruct the widget manager to update the widget
|
||||||
|
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||||
|
}
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
package com.audiobookshelf.app
|
|
||||||
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.appwidget.AppWidgetManager
|
|
||||||
import android.appwidget.AppWidgetProvider
|
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.net.Uri
|
|
||||||
import android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_PAUSE
|
|
||||||
import android.util.Log
|
|
||||||
import android.widget.RemoteViews
|
|
||||||
import androidx.media.session.MediaButtonReceiver.buildMediaButtonPendingIntent
|
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
|
||||||
import com.audiobookshelf.app.device.WidgetEventEmitter
|
|
||||||
import com.audiobookshelf.app.player.PlayerNotificationService
|
|
||||||
import com.bumptech.glide.Glide
|
|
||||||
import com.bumptech.glide.request.RequestOptions
|
|
||||||
import com.bumptech.glide.request.target.AppWidgetTarget
|
|
||||||
import com.bumptech.glide.request.transition.Transition
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Implementation of App Widget functionality.
|
|
||||||
*/
|
|
||||||
class NewAppWidget : AppWidgetProvider() {
|
|
||||||
val tag = "NewAppWidget"
|
|
||||||
|
|
||||||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
|
||||||
// There may be multiple widgets active, so update all of them
|
|
||||||
for (appWidgetId in appWidgetIds) {
|
|
||||||
updateAppWidget(context, appWidgetManager, appWidgetId, null,false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onEnabled(context: Context) {
|
|
||||||
Log.w(tag, "onEnabled check context ${context.packageName}")
|
|
||||||
|
|
||||||
// Enter relevant functionality for when the first widget is created
|
|
||||||
DeviceManager.widgetUpdater = (object : WidgetEventEmitter {
|
|
||||||
override fun onPlayerChanged(pns:PlayerNotificationService) {
|
|
||||||
val isPlaying = pns.currentPlayer.isPlaying
|
|
||||||
Log.i(tag, "onPlayerChanged | Is Playing? $isPlaying")
|
|
||||||
|
|
||||||
val appWidgetManager = AppWidgetManager.getInstance(context)
|
|
||||||
val componentName = ComponentName(context, NewAppWidget::class.java)
|
|
||||||
val ids = appWidgetManager.getAppWidgetIds(componentName)
|
|
||||||
|
|
||||||
val playbackSession = pns.getCurrentPlaybackSessionCopy()
|
|
||||||
val cover = playbackSession?.getCoverUri()
|
|
||||||
|
|
||||||
for (widgetId in ids) {
|
|
||||||
updateAppWidget(context, appWidgetManager, widgetId, cover, isPlaying)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDisabled(context: Context) {
|
|
||||||
// Enter relevant functionality for when the last widget is disabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, coverUri:Uri?, isPlaying:Boolean) {
|
|
||||||
|
|
||||||
val views = RemoteViews(context.packageName, R.layout.new_app_widget)
|
|
||||||
|
|
||||||
val playPausePI = buildMediaButtonPendingIntent(context, ACTION_PLAY_PAUSE)
|
|
||||||
views.setOnClickPendingIntent(R.id.playPauseIcon, playPausePI)
|
|
||||||
|
|
||||||
val wholeWidgetClickI = Intent(context, MainActivity::class.java)
|
|
||||||
wholeWidgetClickI.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
val wholeWidgetClickPI = PendingIntent.getActivity(
|
|
||||||
context,
|
|
||||||
System.currentTimeMillis().toInt(),
|
|
||||||
wholeWidgetClickI,
|
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
||||||
)
|
|
||||||
views.setOnClickPendingIntent(R.id.appWidget, wholeWidgetClickPI)
|
|
||||||
|
|
||||||
val imageUri = coverUri ?: Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
|
||||||
val awt: AppWidgetTarget = object : AppWidgetTarget(context.applicationContext, R.id.imageView, views, appWidgetId) {
|
|
||||||
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
|
||||||
super.onResourceReady(resource, transition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val options = RequestOptions().override(300, 300).placeholder(R.drawable.icon).error(R.drawable.icon)
|
|
||||||
Glide.with(context.applicationContext).asBitmap().load(imageUri).apply(options).into(awt)
|
|
||||||
|
|
||||||
val playPauseResource = if (isPlaying) R.drawable.ic_media_pause_dark else R.drawable.ic_media_play_dark
|
|
||||||
views.setImageViewResource(R.id.playPauseIcon, playPauseResource)
|
|
||||||
|
|
||||||
// Instruct the widget manager to update the widget
|
|
||||||
appWidgetManager.updateAppWidget(appWidgetId, views)
|
|
||||||
}
|
|
||||||
@@ -53,7 +53,7 @@ class Podcast(
|
|||||||
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 localEpisodeId = "local_ep_" + at.localFileId
|
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)
|
val newEpisode = PodcastEpisode(localEpisodeId,(episodes?.size ?: 0) + 1,null,null,at.title,null,null,null, null, null, at,null,at.duration,0, null, localEpisodeId)
|
||||||
episodes?.add(newEpisode)
|
episodes?.add(newEpisode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,7 +67,7 @@ class Podcast(
|
|||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun addAudioTrack(audioTrack:AudioTrack) {
|
override fun addAudioTrack(audioTrack:AudioTrack) {
|
||||||
val localEpisodeId = "local_ep_" + audioTrack.localFileId
|
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)
|
val newEpisode = PodcastEpisode(localEpisodeId,(episodes?.size ?: 0) + 1,null,null,audioTrack.title,null,null,null, null, null,audioTrack,null,audioTrack.duration,0, null, localEpisodeId)
|
||||||
episodes?.add(newEpisode)
|
episodes?.add(newEpisode)
|
||||||
|
|
||||||
var index = 1
|
var index = 1
|
||||||
@@ -96,7 +96,7 @@ class Podcast(
|
|||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode):PodcastEpisode {
|
fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode):PodcastEpisode {
|
||||||
val localEpisodeId = "local_ep_" + 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)
|
val newEpisode = PodcastEpisode(localEpisodeId,(episodes?.size ?: 0) + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,null,null,audioTrack,episode.chapters,audioTrack.duration,0, episode.id, localEpisodeId)
|
||||||
episodes?.add(newEpisode)
|
episodes?.add(newEpisode)
|
||||||
|
|
||||||
var index = 1
|
var index = 1
|
||||||
@@ -126,6 +126,7 @@ class Book(
|
|||||||
var audioFiles:List<AudioFile>?,
|
var audioFiles:List<AudioFile>?,
|
||||||
var chapters:List<BookChapter>?,
|
var chapters:List<BookChapter>?,
|
||||||
var tracks:MutableList<AudioTrack>?,
|
var tracks:MutableList<AudioTrack>?,
|
||||||
|
var ebookFile: EBookFile?,
|
||||||
var size:Long?,
|
var size:Long?,
|
||||||
var duration:Double?,
|
var duration:Double?,
|
||||||
var numTracks:Int?
|
var numTracks:Int?
|
||||||
@@ -179,7 +180,7 @@ class Book(
|
|||||||
}
|
}
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getLocalCopy(): Book {
|
override fun getLocalCopy(): Book {
|
||||||
return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(),null,null, 0)
|
return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(), ebookFile, null,null, 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,15 +251,16 @@ data class PodcastEpisode(
|
|||||||
var publishedAt:Long?,
|
var publishedAt:Long?,
|
||||||
var audioFile:AudioFile?,
|
var audioFile:AudioFile?,
|
||||||
var audioTrack:AudioTrack?,
|
var audioTrack:AudioTrack?,
|
||||||
|
var chapters:List<BookChapter>?,
|
||||||
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
|
var localEpisodeId:String? // For Android Auto server episodes with local copy
|
||||||
) {
|
) {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getMediaDescription(libraryItem:LibraryItemWrapper, progress:MediaProgressWrapper?, ctx: Context?): 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(ctx)
|
||||||
} else {
|
} else {
|
||||||
(libraryItem as LibraryItem).getCoverUri()
|
(libraryItem as LibraryItem).getCoverUri()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,20 @@ data class LocalFile(
|
|||||||
if (mimeType == "video/mp4") return true
|
if (mimeType == "video/mp4") return true
|
||||||
return mimeType?.startsWith("audio") == true
|
return mimeType?.startsWith("audio") == true
|
||||||
}
|
}
|
||||||
|
@JsonIgnore
|
||||||
|
fun isEBookFile():Boolean {
|
||||||
|
return getEBookFormat() != null
|
||||||
|
}
|
||||||
|
@JsonIgnore
|
||||||
|
fun getEBookFormat():String? {
|
||||||
|
if (mimeType == "application/epub+zip") return "epub"
|
||||||
|
if (mimeType == "application/pdf") return "pdf"
|
||||||
|
if (mimeType == "application/x-mobipocket-ebook") return "mobi"
|
||||||
|
if (mimeType == "application/vnd.comicbook+zip") return "cbz"
|
||||||
|
if (mimeType == "application/vnd.comicbook-rar") return "cbr"
|
||||||
|
if (mimeType == "application/vnd.amazon.mobi8-ebook") return "azw3"
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
@@ -69,14 +83,14 @@ data class LocalFolder(
|
|||||||
)
|
)
|
||||||
open class LibraryItemWrapper(var id:String) {
|
open class LibraryItemWrapper(var id:String) {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
open fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context?): MediaDescriptionCompat { return MediaDescriptionCompat.Builder().build() }
|
open fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat { return MediaDescriptionCompat.Builder().build() }
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class DeviceInfo(
|
data class DeviceInfo(
|
||||||
|
var deviceId:String,
|
||||||
var manufacturer:String,
|
var manufacturer:String,
|
||||||
var model:String,
|
var model:String,
|
||||||
var brand:String,
|
|
||||||
var sdkVersion:Int,
|
var sdkVersion:Int,
|
||||||
var clientVersion: String
|
var clientVersion: String
|
||||||
)
|
)
|
||||||
@@ -95,6 +109,7 @@ data class DeviceSettings(
|
|||||||
var enableAltView:Boolean,
|
var enableAltView:Boolean,
|
||||||
var jumpBackwardsTime:Int,
|
var jumpBackwardsTime:Int,
|
||||||
var jumpForwardTime:Int,
|
var jumpForwardTime:Int,
|
||||||
|
var enableMp3IndexSeeking:Boolean,
|
||||||
var disableShakeToResetSleepTimer:Boolean,
|
var disableShakeToResetSleepTimer:Boolean,
|
||||||
var shakeSensitivity: ShakeSensitivitySetting,
|
var shakeSensitivity: ShakeSensitivitySetting,
|
||||||
var lockOrientation: LockOrientationSetting,
|
var lockOrientation: LockOrientationSetting,
|
||||||
@@ -102,7 +117,11 @@ data class DeviceSettings(
|
|||||||
var autoSleepTimer: Boolean,
|
var autoSleepTimer: Boolean,
|
||||||
var autoSleepTimerStartTime: String,
|
var autoSleepTimerStartTime: String,
|
||||||
var autoSleepTimerEndTime: String,
|
var autoSleepTimerEndTime: String,
|
||||||
var sleepTimerLength: Long // Time in milliseconds
|
var autoSleepTimerAutoRewind: Boolean,
|
||||||
|
var autoSleepTimerAutoRewindTime: Long, //Time in milliseconds
|
||||||
|
var sleepTimerLength: Long, // Time in milliseconds
|
||||||
|
var disableSleepTimerFadeOut: Boolean,
|
||||||
|
var disableSleepTimerResetFeedback: Boolean
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
// Static method to get default device settings
|
// Static method to get default device settings
|
||||||
@@ -112,6 +131,7 @@ data class DeviceSettings(
|
|||||||
enableAltView = true,
|
enableAltView = true,
|
||||||
jumpBackwardsTime = 10,
|
jumpBackwardsTime = 10,
|
||||||
jumpForwardTime = 10,
|
jumpForwardTime = 10,
|
||||||
|
enableMp3IndexSeeking = false,
|
||||||
disableShakeToResetSleepTimer = false,
|
disableShakeToResetSleepTimer = false,
|
||||||
shakeSensitivity = ShakeSensitivitySetting.MEDIUM,
|
shakeSensitivity = ShakeSensitivitySetting.MEDIUM,
|
||||||
lockOrientation = LockOrientationSetting.NONE,
|
lockOrientation = LockOrientationSetting.NONE,
|
||||||
@@ -119,7 +139,11 @@ data class DeviceSettings(
|
|||||||
autoSleepTimer = false,
|
autoSleepTimer = false,
|
||||||
autoSleepTimerStartTime = "22:00",
|
autoSleepTimerStartTime = "22:00",
|
||||||
autoSleepTimerEndTime = "06:00",
|
autoSleepTimerEndTime = "06:00",
|
||||||
sleepTimerLength = 900000L // 15 minutes
|
sleepTimerLength = 900000L, // 15 minutes
|
||||||
|
autoSleepTimerAutoRewind = false,
|
||||||
|
autoSleepTimerAutoRewindTime = 300000L, // 5 minutes
|
||||||
|
disableSleepTimerFadeOut = false,
|
||||||
|
disableSleepTimerResetFeedback = false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,6 +161,7 @@ data class DeviceSettings(
|
|||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val autoSleepTimerEndMinute get() = autoSleepTimerEndTime.split(":")[1].toInt()
|
val autoSleepTimerEndMinute get() = autoSleepTimerEndTime.split(":")[1].toInt()
|
||||||
|
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getShakeThresholdGravity() : Float { // Used in ShakeDetector
|
fun getShakeThresholdGravity() : Float { // Used in ShakeDetector
|
||||||
return if (shakeSensitivity == ShakeSensitivitySetting.VERY_HIGH) 1.2f
|
return if (shakeSensitivity == ShakeSensitivitySetting.VERY_HIGH) 1.2f
|
||||||
@@ -154,8 +179,8 @@ data class DeviceSettings(
|
|||||||
data class DeviceData(
|
data class DeviceData(
|
||||||
var serverConnectionConfigs:MutableList<ServerConnectionConfig>,
|
var serverConnectionConfigs:MutableList<ServerConnectionConfig>,
|
||||||
var lastServerConnectionConfigId:String?,
|
var lastServerConnectionConfigId:String?,
|
||||||
var currentLocalPlaybackSession: PlaybackSession?, // Stored to open up where left off for local media
|
var deviceSettings: DeviceSettings?,
|
||||||
var deviceSettings: DeviceSettings?
|
var lastPlaybackSession: PlaybackSession?
|
||||||
) {
|
) {
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getLastServerConnectionConfig(): ServerConnectionConfig? {
|
fun getLastServerConnectionConfig(): ServerConnectionConfig? {
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.audiobookshelf.app.data
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
data class EBookFile(
|
||||||
|
var ino:String,
|
||||||
|
var metadata:FileMetadata?,
|
||||||
|
var ebookFormat:String,
|
||||||
|
var isLocal:Boolean,
|
||||||
|
var localFileId:String?,
|
||||||
|
var contentUrl:String?
|
||||||
|
)
|
||||||
@@ -5,6 +5,7 @@ import android.net.Uri
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.support.v4.media.MediaDescriptionCompat
|
import android.support.v4.media.MediaDescriptionCompat
|
||||||
import androidx.media.utils.MediaConstants
|
import androidx.media.utils.MediaConstants
|
||||||
|
import com.audiobookshelf.app.BuildConfig
|
||||||
import com.audiobookshelf.app.R
|
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
|
||||||
@@ -41,7 +42,7 @@ class LibraryItem(
|
|||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getCoverUri(): Uri {
|
fun getCoverUri(): Uri {
|
||||||
if (media.coverPath == null) {
|
if (media.coverPath == null) {
|
||||||
return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
return Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}")
|
return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}")
|
||||||
@@ -57,7 +58,7 @@ class LibraryItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context?): MediaDescriptionCompat {
|
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
|
||||||
val extras = Bundle()
|
val extras = Bundle()
|
||||||
|
|
||||||
if (localLibraryItemId != null) {
|
if (localLibraryItemId != null) {
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ import android.os.Bundle
|
|||||||
import android.provider.MediaStore
|
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.core.content.FileProvider
|
||||||
|
import androidx.core.net.toFile
|
||||||
import androidx.media.utils.MediaConstants
|
import androidx.media.utils.MediaConstants
|
||||||
|
import com.audiobookshelf.app.BuildConfig
|
||||||
import com.audiobookshelf.app.R
|
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
|
||||||
@@ -45,8 +48,11 @@ class LocalLibraryItem(
|
|||||||
val isPodcast get() = mediaType == "podcast"
|
val isPodcast get() = mediaType == "podcast"
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getCoverUri(): Uri {
|
fun getCoverUri(ctx:Context): Uri {
|
||||||
return if (coverContentUrl != null) Uri.parse(coverContentUrl) else Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
|
if (coverContentUrl?.startsWith("file:") == true) {
|
||||||
|
return FileProvider.getUriForFile(ctx, "${BuildConfig.APPLICATION_ID}.fileprovider", Uri.parse(coverContentUrl).toFile())
|
||||||
|
}
|
||||||
|
return if (coverContentUrl != null) Uri.parse(coverContentUrl) else Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
@@ -73,7 +79,7 @@ class LocalLibraryItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getPlaybackSession(episode:PodcastEpisode?):PlaybackSession {
|
fun getPlaybackSession(episode:PodcastEpisode?, deviceInfo:DeviceInfo):PlaybackSession {
|
||||||
val localEpisodeId = episode?.id
|
val localEpisodeId = episode?.id
|
||||||
val sessionId = "play_local_${UUID.randomUUID()}"
|
val sessionId = "play_local_${UUID.randomUUID()}"
|
||||||
|
|
||||||
@@ -82,18 +88,22 @@ class LocalLibraryItem(
|
|||||||
val mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(mediaProgressId)
|
val mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(mediaProgressId)
|
||||||
val currentTime = mediaProgress?.currentTime ?: 0.0
|
val currentTime = mediaProgress?.currentTime ?: 0.0
|
||||||
|
|
||||||
// TODO: Clean up add mediaType methods for displayTitle and displayAuthor
|
|
||||||
val mediaMetadata = media.metadata
|
val mediaMetadata = media.metadata
|
||||||
val chapters = if (mediaType == "book") (media as Book).chapters else mutableListOf()
|
var chapters = if (mediaType == "book") (media as Book).chapters else mutableListOf()
|
||||||
var audioTracks = media.getAudioTracks() as MutableList<AudioTrack>
|
var audioTracks = media.getAudioTracks() as MutableList<AudioTrack>
|
||||||
val authorName = mediaMetadata.getAuthorDisplayName()
|
val authorName = mediaMetadata.getAuthorDisplayName()
|
||||||
|
val displayTitle = episode?.title ?: mediaMetadata.title
|
||||||
|
var duration = getDuration()
|
||||||
if (episode != null) { // Get podcast episode audio track
|
if (episode != null) { // Get podcast episode audio track
|
||||||
episode.audioTrack?.let { at -> mutableListOf(at) }?.let { tracks -> audioTracks = tracks }
|
episode.audioTrack?.let { at -> mutableListOf(at) }?.let { tracks -> audioTracks = tracks }
|
||||||
|
chapters = episode.chapters
|
||||||
|
duration = episode.audioTrack?.duration ?: 0.0
|
||||||
Log.d("LocalLibraryItem", "getPlaybackSession: Got podcast episode audio track ${audioTracks.size}")
|
Log.d("LocalLibraryItem", "getPlaybackSession: Got podcast episode audio track ${audioTracks.size}")
|
||||||
}
|
}
|
||||||
|
|
||||||
val dateNow = System.currentTimeMillis()
|
val dateNow = System.currentTimeMillis()
|
||||||
return PlaybackSession(sessionId,serverUserId,libraryItemId,episode?.serverEpisodeId, mediaType, mediaMetadata, chapters ?: mutableListOf(), mediaMetadata.title, authorName,null,getDuration(),PLAYMETHOD_LOCAL,dateNow,0L,0L, audioTracks,currentTime,null,this,localEpisodeId,serverConnectionConfigId, serverAddress, "exo-player")
|
return PlaybackSession(sessionId,serverUserId,libraryItemId,episode?.serverEpisodeId, mediaType, mediaMetadata, deviceInfo,chapters ?: mutableListOf(), displayTitle, authorName,null,duration,PLAYMETHOD_LOCAL,dateNow,0L,0L, audioTracks,currentTime,null,this,localEpisodeId,serverConnectionConfigId, serverAddress, "exo-player")
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
@@ -102,18 +112,16 @@ class LocalLibraryItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx:Context?): MediaDescriptionCompat {
|
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx:Context): MediaDescriptionCompat {
|
||||||
val coverUri = getCoverUri()
|
val coverUri = getCoverUri(ctx)
|
||||||
|
|
||||||
var bitmap:Bitmap? = null
|
var bitmap:Bitmap? = null
|
||||||
if (coverContentUrl != null) {
|
if (coverContentUrl != null) {
|
||||||
ctx?.let {
|
bitmap = if (Build.VERSION.SDK_INT < 28) {
|
||||||
bitmap = if (Build.VERSION.SDK_INT < 28) {
|
MediaStore.Images.Media.getBitmap(ctx.contentResolver, coverUri)
|
||||||
MediaStore.Images.Media.getBitmap(it.contentResolver, coverUri)
|
} else {
|
||||||
} else {
|
val source: ImageDecoder.Source = ImageDecoder.createSource(ctx.contentResolver, coverUri)
|
||||||
val source: ImageDecoder.Source = ImageDecoder.createSource(it.contentResolver, coverUri)
|
ImageDecoder.decodeBitmap(source)
|
||||||
ImageDecoder.decodeBitmap(source)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ data class LocalMediaItem(
|
|||||||
var basePath:String,
|
var basePath:String,
|
||||||
var absolutePath:String,
|
var absolutePath:String,
|
||||||
var audioTracks:MutableList<AudioTrack>,
|
var audioTracks:MutableList<AudioTrack>,
|
||||||
|
var ebookFile:EBookFile?,
|
||||||
var localFiles:MutableList<LocalFile>,
|
var localFiles:MutableList<LocalFile>,
|
||||||
var coverContentUrl:String?,
|
var coverContentUrl:String?,
|
||||||
var coverAbsolutePath:String?
|
var coverAbsolutePath:String?
|
||||||
@@ -61,7 +62,7 @@ data class LocalMediaItem(
|
|||||||
val mediaMetadata = getMediaMetadata()
|
val mediaMetadata = getMediaMetadata()
|
||||||
if (mediaType == "book") {
|
if (mediaType == "book") {
|
||||||
val chapters = getAudiobookChapters()
|
val chapters = getAudiobookChapters()
|
||||||
val book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration(),audioTracks.size)
|
val book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,ebookFile,getTotalSize(),getDuration(),audioTracks.size)
|
||||||
return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true,null,null,null,null)
|
return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true,null,null,null,null)
|
||||||
} else {
|
} else {
|
||||||
val podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false, 0)
|
val podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false, 0)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
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
|
||||||
@@ -14,6 +13,8 @@ class LocalMediaProgress(
|
|||||||
progress:Double, // 0 to 1
|
progress:Double, // 0 to 1
|
||||||
currentTime:Double,
|
currentTime:Double,
|
||||||
isFinished:Boolean,
|
isFinished:Boolean,
|
||||||
|
var ebookLocation:String?, // cfi tag
|
||||||
|
var ebookProgress:Double?, // 0 to 1
|
||||||
var lastUpdate:Long,
|
var lastUpdate:Long,
|
||||||
var startedAt:Long,
|
var startedAt:Long,
|
||||||
var finishedAt:Long?,
|
var finishedAt:Long?,
|
||||||
@@ -33,6 +34,11 @@ class LocalMediaProgress(
|
|||||||
if (localEpisodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
|
if (localEpisodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun isMatch(mediaProgress:MediaProgress):Boolean {
|
||||||
|
if (episodeId != null) return libraryItemId == mediaProgress.libraryItemId && episodeId == mediaProgress.episodeId
|
||||||
|
return libraryItemId == mediaProgress.libraryItemId
|
||||||
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun updateIsFinished(finished:Boolean) {
|
fun updateIsFinished(finished:Boolean) {
|
||||||
@@ -54,11 +60,20 @@ class LocalMediaProgress(
|
|||||||
finishedAt = if (isFinished) lastUpdate else null
|
finishedAt = if (isFinished) lastUpdate else null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
fun updateEbookProgress(ebookLocation:String, ebookProgress:Double) {
|
||||||
|
lastUpdate = System.currentTimeMillis()
|
||||||
|
this.ebookProgress = ebookProgress
|
||||||
|
this.ebookLocation = ebookLocation
|
||||||
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun updateFromServerMediaProgress(serverMediaProgress:MediaProgress) {
|
fun updateFromServerMediaProgress(serverMediaProgress:MediaProgress) {
|
||||||
isFinished = serverMediaProgress.isFinished
|
isFinished = serverMediaProgress.isFinished
|
||||||
progress = serverMediaProgress.progress
|
progress = serverMediaProgress.progress
|
||||||
currentTime = serverMediaProgress.currentTime
|
currentTime = serverMediaProgress.currentTime
|
||||||
|
ebookProgress = serverMediaProgress.ebookProgress
|
||||||
|
ebookLocation = serverMediaProgress.ebookLocation
|
||||||
duration = serverMediaProgress.duration
|
duration = serverMediaProgress.duration
|
||||||
lastUpdate = serverMediaProgress.lastUpdate
|
lastUpdate = serverMediaProgress.lastUpdate
|
||||||
finishedAt = serverMediaProgress.finishedAt
|
finishedAt = serverMediaProgress.finishedAt
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ class MediaProgress(
|
|||||||
progress:Double, // 0 to 1
|
progress:Double, // 0 to 1
|
||||||
currentTime:Double,
|
currentTime:Double,
|
||||||
isFinished:Boolean,
|
isFinished:Boolean,
|
||||||
|
var ebookLocation:String?, // cfi tag
|
||||||
|
var ebookProgress:Double?, // 0 to 1
|
||||||
var lastUpdate:Long,
|
var lastUpdate:Long,
|
||||||
var startedAt:Long,
|
var startedAt:Long,
|
||||||
var finishedAt:Long?
|
var finishedAt:Long?
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ import android.net.Uri
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.support.v4.media.MediaMetadataCompat
|
import android.support.v4.media.MediaMetadataCompat
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.core.net.toFile
|
||||||
|
import com.audiobookshelf.app.BuildConfig
|
||||||
|
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.media.MediaProgressSyncData
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore
|
import com.fasterxml.jackson.annotation.JsonIgnore
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
import com.google.android.exoplayer2.MediaItem
|
import com.google.android.exoplayer2.MediaItem
|
||||||
@@ -25,6 +29,7 @@ class PlaybackSession(
|
|||||||
var episodeId:String?,
|
var episodeId:String?,
|
||||||
var mediaType:String,
|
var mediaType:String,
|
||||||
var mediaMetadata:MediaTypeMetadata,
|
var mediaMetadata:MediaTypeMetadata,
|
||||||
|
var deviceInfo:DeviceInfo,
|
||||||
var chapters:List<BookChapter>,
|
var chapters:List<BookChapter>,
|
||||||
var displayTitle: String?,
|
var displayTitle: String?,
|
||||||
var displayAuthor: String?,
|
var displayAuthor: String?,
|
||||||
@@ -135,10 +140,17 @@ class PlaybackSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getCoverUri(): Uri {
|
fun getCoverUri(ctx:Context): Uri {
|
||||||
if (localLibraryItem?.coverContentUrl != null) return Uri.parse(localLibraryItem?.coverContentUrl) ?: Uri.parse("android.resource://com.audiobookshelf.app/" + com.audiobookshelf.app.R.drawable.icon)
|
if (localLibraryItem?.coverContentUrl != null) {
|
||||||
|
var coverUri = Uri.parse(localLibraryItem?.coverContentUrl.toString())
|
||||||
|
if (coverUri.toString().startsWith("file:")) {
|
||||||
|
coverUri = FileProvider.getUriForFile(ctx, "${BuildConfig.APPLICATION_ID}.fileprovider", coverUri.toFile())
|
||||||
|
}
|
||||||
|
|
||||||
if (coverPath == null) return Uri.parse("android.resource://com.audiobookshelf.app/" + com.audiobookshelf.app.R.drawable.icon)
|
return coverUri ?: Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coverPath == null) return Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + 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}")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,6 +162,8 @@ class PlaybackSession(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getMediaMetadataCompat(ctx: Context): MediaMetadataCompat {
|
fun getMediaMetadataCompat(ctx: Context): MediaMetadataCompat {
|
||||||
|
val coverUri = getCoverUri(ctx)
|
||||||
|
|
||||||
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)
|
||||||
@@ -160,16 +174,16 @@ class PlaybackSession(
|
|||||||
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, displayAuthor)
|
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, displayAuthor)
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, 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_ALBUM_ART_URI, coverUri.toString())
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, getCoverUri().toString())
|
.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, coverUri.toString())
|
||||||
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, getCoverUri().toString())
|
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString())
|
||||||
|
|
||||||
// Local covers get bitmap
|
// Local covers get bitmap
|
||||||
if (localLibraryItem?.coverContentUrl != null) {
|
if (localLibraryItem?.coverContentUrl != null) {
|
||||||
val bitmap = if (Build.VERSION.SDK_INT < 28) {
|
val bitmap = if (Build.VERSION.SDK_INT < 28) {
|
||||||
MediaStore.Images.Media.getBitmap(ctx.contentResolver, getCoverUri())
|
MediaStore.Images.Media.getBitmap(ctx.contentResolver, coverUri)
|
||||||
} else {
|
} else {
|
||||||
val source: ImageDecoder.Source = ImageDecoder.createSource(ctx.contentResolver, getCoverUri())
|
val source: ImageDecoder.Source = ImageDecoder.createSource(ctx.contentResolver, coverUri)
|
||||||
ImageDecoder.decodeBitmap(source)
|
ImageDecoder.decodeBitmap(source)
|
||||||
}
|
}
|
||||||
metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap)
|
metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap)
|
||||||
@@ -180,7 +194,9 @@ class PlaybackSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getExoMediaMetadata(audioTrack:AudioTrack): MediaMetadata {
|
fun getExoMediaMetadata(ctx:Context): MediaMetadata {
|
||||||
|
val coverUri = getCoverUri(ctx)
|
||||||
|
|
||||||
val metadataBuilder = MediaMetadata.Builder()
|
val metadataBuilder = MediaMetadata.Builder()
|
||||||
.setTitle(displayTitle)
|
.setTitle(displayTitle)
|
||||||
.setDisplayTitle(displayTitle)
|
.setDisplayTitle(displayTitle)
|
||||||
@@ -189,20 +205,18 @@ class PlaybackSession(
|
|||||||
.setSubtitle(displayAuthor)
|
.setSubtitle(displayAuthor)
|
||||||
.setAlbumTitle(displayAuthor)
|
.setAlbumTitle(displayAuthor)
|
||||||
.setDescription(displayAuthor)
|
.setDescription(displayAuthor)
|
||||||
.setArtworkUri(getCoverUri())
|
.setArtworkUri(coverUri)
|
||||||
|
.setMediaType(MediaMetadata.MEDIA_TYPE_AUDIO_BOOK)
|
||||||
val contentUri = this.getContentUri(audioTrack)
|
|
||||||
metadataBuilder.setMediaUri(contentUri)
|
|
||||||
|
|
||||||
return metadataBuilder.build()
|
return metadataBuilder.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getMediaItems():List<MediaItem> {
|
fun getMediaItems(ctx:Context):List<MediaItem> {
|
||||||
val mediaItems:MutableList<MediaItem> = mutableListOf()
|
val mediaItems:MutableList<MediaItem> = mutableListOf()
|
||||||
|
|
||||||
for (audioTrack in audioTracks) {
|
for (audioTrack in audioTracks) {
|
||||||
val mediaMetadata = this.getExoMediaMetadata(audioTrack)
|
val mediaMetadata = this.getExoMediaMetadata(ctx)
|
||||||
val mediaUri = this.getContentUri(audioTrack)
|
val mediaUri = this.getContentUri(audioTrack)
|
||||||
val mimeType = audioTrack.mimeType
|
val mimeType = audioTrack.mimeType
|
||||||
|
|
||||||
@@ -250,11 +264,11 @@ class PlaybackSession(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun clone():PlaybackSession {
|
fun clone():PlaybackSession {
|
||||||
return PlaybackSession(id,userId,libraryItemId,episodeId,mediaType,mediaMetadata,chapters,displayTitle,displayAuthor,coverPath,duration,playMethod,startedAt,updatedAt,timeListening,audioTracks,currentTime,libraryItem,localLibraryItem,localEpisodeId,serverConnectionConfigId,serverAddress, mediaPlayer)
|
return PlaybackSession(id,userId,libraryItemId,episodeId,mediaType,mediaMetadata,deviceInfo,chapters,displayTitle,displayAuthor,coverPath,duration,playMethod,startedAt,updatedAt,timeListening,audioTracks,currentTime,libraryItem,localLibraryItem,localEpisodeId,serverConnectionConfigId,serverAddress, mediaPlayer)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun syncData(syncData:MediaProgressSyncData) {
|
fun syncData(syncData: MediaProgressSyncData) {
|
||||||
timeListening += syncData.timeListened
|
timeListening += syncData.timeListened
|
||||||
updatedAt = System.currentTimeMillis()
|
updatedAt = System.currentTimeMillis()
|
||||||
currentTime = syncData.currentTime
|
currentTime = syncData.currentTime
|
||||||
@@ -262,6 +276,6 @@ class PlaybackSession(
|
|||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getNewLocalMediaProgress():LocalMediaProgress {
|
fun getNewLocalMediaProgress():LocalMediaProgress {
|
||||||
return LocalMediaProgress(localMediaProgressId,localLibraryItemId,localEpisodeId,getTotalDuration(),progress,currentTime,false,updatedAt,startedAt,null,serverConnectionConfigId,serverAddress,userId,libraryItemId,episodeId)
|
return LocalMediaProgress(localMediaProgressId,localLibraryItemId,localEpisodeId,getTotalDuration(),progress,currentTime,false,null,null,updatedAt,startedAt,null,serverConnectionConfigId,serverAddress,userId,libraryItemId,episodeId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
package com.audiobookshelf.app.device
|
package com.audiobookshelf.app.device
|
||||||
|
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.content.ComponentName
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import com.audiobookshelf.app.MediaPlayerWidget
|
||||||
import com.audiobookshelf.app.data.*
|
import com.audiobookshelf.app.data.*
|
||||||
import com.audiobookshelf.app.managers.DbManager
|
import com.audiobookshelf.app.managers.DbManager
|
||||||
import com.audiobookshelf.app.player.PlayerNotificationService
|
import com.audiobookshelf.app.player.PlayerNotificationService
|
||||||
|
import com.audiobookshelf.app.updateAppWidget
|
||||||
|
|
||||||
interface WidgetEventEmitter {
|
interface WidgetEventEmitter {
|
||||||
fun onPlayerChanged(pns:PlayerNotificationService)
|
fun onPlayerChanged(pns:PlayerNotificationService)
|
||||||
|
fun onPlayerClosed()
|
||||||
}
|
}
|
||||||
|
|
||||||
object DeviceManager {
|
object DeviceManager {
|
||||||
@@ -36,6 +44,10 @@ object DeviceManager {
|
|||||||
if (deviceData.deviceSettings?.shakeSensitivity == null) {
|
if (deviceData.deviceSettings?.shakeSensitivity == null) {
|
||||||
deviceData.deviceSettings?.shakeSensitivity = ShakeSensitivitySetting.MEDIUM
|
deviceData.deviceSettings?.shakeSensitivity = ShakeSensitivitySetting.MEDIUM
|
||||||
}
|
}
|
||||||
|
// Initialize auto sleep timer auto rewind added in v0.9.64
|
||||||
|
if (deviceData.deviceSettings?.autoSleepTimerAutoRewindTime == null) {
|
||||||
|
deviceData.deviceSettings?.autoSleepTimerAutoRewindTime = 300000L // 5 minutes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBase64Id(id:String):String {
|
fun getBase64Id(id:String):String {
|
||||||
@@ -46,4 +58,54 @@ object DeviceManager {
|
|||||||
if (id == null) return null
|
if (id == null) return null
|
||||||
return deviceData.serverConnectionConfigs.find { it.id == id }
|
return deviceData.serverConnectionConfigs.find { it.id == id }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun checkConnectivity(ctx:Context): Boolean {
|
||||||
|
val connectivityManager = ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||||
|
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
||||||
|
if (capabilities != null) {
|
||||||
|
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
|
||||||
|
Log.i("Internet", "NetworkCapabilities.TRANSPORT_CELLULAR")
|
||||||
|
return true
|
||||||
|
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
|
||||||
|
Log.i("Internet", "NetworkCapabilities.TRANSPORT_WIFI")
|
||||||
|
return true
|
||||||
|
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
|
||||||
|
Log.i("Internet", "NetworkCapabilities.TRANSPORT_ETHERNET")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLastPlaybackSession(playbackSession:PlaybackSession) {
|
||||||
|
deviceData.lastPlaybackSession = playbackSession
|
||||||
|
dbManager.saveDeviceData(deviceData)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initializeWidgetUpdater(context:Context) {
|
||||||
|
Log.d(tag, "Initializing widget updater")
|
||||||
|
widgetUpdater = (object : WidgetEventEmitter {
|
||||||
|
override fun onPlayerChanged(pns: PlayerNotificationService) {
|
||||||
|
|
||||||
|
val isPlaying = pns.currentPlayer.isPlaying
|
||||||
|
|
||||||
|
val appWidgetManager = AppWidgetManager.getInstance(context)
|
||||||
|
val componentName = ComponentName(context, MediaPlayerWidget::class.java)
|
||||||
|
val ids = appWidgetManager.getAppWidgetIds(componentName)
|
||||||
|
val playbackSession = pns.getCurrentPlaybackSessionCopy()
|
||||||
|
|
||||||
|
for (widgetId in ids) {
|
||||||
|
updateAppWidget(context, appWidgetManager, widgetId, playbackSession, isPlaying, PlayerNotificationService.isClosed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
override fun onPlayerClosed() {
|
||||||
|
val appWidgetManager = AppWidgetManager.getInstance(context)
|
||||||
|
val componentName = ComponentName(context, MediaPlayerWidget::class.java)
|
||||||
|
val ids = appWidgetManager.getAppWidgetIds(componentName)
|
||||||
|
for (widgetId in ids) {
|
||||||
|
updateAppWidget(context, appWidgetManager, widgetId, deviceData.lastPlaybackSession, false, PlayerNotificationService.isClosed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
|||||||
import com.fasterxml.jackson.module.kotlin.readValue
|
import com.fasterxml.jackson.module.kotlin.readValue
|
||||||
import com.getcapacitor.JSObject
|
import com.getcapacitor.JSObject
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class FolderScanner(var ctx: Context) {
|
class FolderScanner(var ctx: Context) {
|
||||||
private val tag = "FolderScanner"
|
private val tag = "FolderScanner"
|
||||||
@@ -51,7 +52,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
var mediaItemsUpToDate = 0
|
var mediaItemsUpToDate = 0
|
||||||
|
|
||||||
// Search for files in media item folder
|
// Search for files in media item folder
|
||||||
val foldersFound = df.search(false, DocumentFileType.FOLDER)
|
val foldersFound = df.search(true, DocumentFileType.FOLDER)
|
||||||
|
|
||||||
// Match folders found with local library items already saved in db
|
// Match folders found with local library items already saved in db
|
||||||
var existingLocalLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id)
|
var existingLocalLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id)
|
||||||
@@ -72,11 +73,16 @@ 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) }
|
||||||
|
|
||||||
when (scanLibraryItemFolder(itemFolder, localFolder, existingItem, forceAudioProbe)) {
|
val filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/*"))
|
||||||
ItemScanResult.REMOVED -> mediaItemsRemoved++
|
|
||||||
ItemScanResult.UPDATED -> mediaItemsUpdated++
|
// Do not scan folders that have no media items and not an existing item already
|
||||||
ItemScanResult.ADDED -> mediaItemsAdded++
|
if (existingItem != null || filesInFolder.isNotEmpty()) {
|
||||||
else -> mediaItemsUpToDate++
|
when (scanLibraryItemFolder(itemFolder, filesInFolder, localFolder, existingItem, forceAudioProbe)) {
|
||||||
|
ItemScanResult.REMOVED -> mediaItemsRemoved++
|
||||||
|
ItemScanResult.UPDATED -> mediaItemsUpdated++
|
||||||
|
ItemScanResult.ADDED -> mediaItemsAdded++
|
||||||
|
else -> mediaItemsUpToDate++
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +97,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scanLibraryItemFolder(itemFolder:DocumentFile, localFolder:LocalFolder, existingItem:LocalLibraryItem?, forceAudioProbe:Boolean):ItemScanResult {
|
private fun scanLibraryItemFolder(itemFolder:DocumentFile, filesInFolder:List<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)
|
||||||
|
|
||||||
@@ -105,8 +111,8 @@ class FolderScanner(var ctx: Context) {
|
|||||||
var startOffset = 0.0
|
var startOffset = 0.0
|
||||||
var coverContentUrl:String? = null
|
var coverContentUrl:String? = null
|
||||||
var coverAbsolutePath:String? = null
|
var coverAbsolutePath:String? = null
|
||||||
|
var hasEBookFile = false
|
||||||
val filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream"))
|
var newEBookFile:EBookFile? = null
|
||||||
|
|
||||||
val existingLocalFilesRemoved = existingLocalFiles.filter { elf ->
|
val existingLocalFilesRemoved = existingLocalFiles.filter { elf ->
|
||||||
filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder
|
filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder
|
||||||
@@ -119,7 +125,6 @@ class FolderScanner(var ctx: Context) {
|
|||||||
filesInFolder.forEach { file ->
|
filesInFolder.forEach { file ->
|
||||||
val mimeType = file.mimeType ?: ""
|
val mimeType = file.mimeType ?: ""
|
||||||
val filename = file.name ?: ""
|
val filename = file.name ?: ""
|
||||||
val isAudio = mimeType.startsWith("audio") || mimeType == "video/mp4"
|
|
||||||
Log.d(tag, "Found $mimeType file $filename in folder $itemFolderName")
|
Log.d(tag, "Found $mimeType file $filename in folder $itemFolderName")
|
||||||
|
|
||||||
val localFileId = DeviceManager.getBase64Id(file.id)
|
val localFileId = DeviceManager.getBase64Id(file.id)
|
||||||
@@ -129,7 +134,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 (localFile.isAudioFile()) {
|
||||||
val audioTrackToAdd:AudioTrack?
|
val audioTrackToAdd:AudioTrack?
|
||||||
|
|
||||||
val existingAudioTrack = existingAudioTracks.find { eat -> eat.localFileId == localFileId }
|
val existingAudioTrack = existingAudioTracks.find { eat -> eat.localFileId == localFileId }
|
||||||
@@ -171,6 +176,15 @@ class FolderScanner(var ctx: Context) {
|
|||||||
startOffset += audioTrackToAdd.duration
|
startOffset += audioTrackToAdd.duration
|
||||||
index++
|
index++
|
||||||
audioTracks.add(audioTrackToAdd)
|
audioTracks.add(audioTrackToAdd)
|
||||||
|
} else if (localFile.isEBookFile()) {
|
||||||
|
val existingLocalFile = existingLocalFiles.find { elf -> elf.id == localFileId }
|
||||||
|
|
||||||
|
if (localFolder.mediaType == "book") {
|
||||||
|
hasEBookFile = true
|
||||||
|
if (existingLocalFile == null) {
|
||||||
|
newEBookFile = EBookFile(localFileId, null, localFile.getEBookFormat() ?: "", true, localFileId, localFile.contentUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
val existingLocalFile = existingLocalFiles.find { elf -> elf.id == localFileId }
|
val existingLocalFile = existingLocalFiles.find { elf -> elf.id == localFileId }
|
||||||
|
|
||||||
@@ -195,7 +209,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingItem != null && audioTracks.isEmpty()) {
|
if (existingItem != null && audioTracks.isEmpty() && !hasEBookFile) {
|
||||||
Log.d(tag, "Local library item ${existingItem.media.metadata.title} no longer has audio tracks - removing item")
|
Log.d(tag, "Local library item ${existingItem.media.metadata.title} no longer has audio tracks - removing item")
|
||||||
DeviceManager.dbManager.removeLocalLibraryItem(existingItem.id)
|
DeviceManager.dbManager.removeLocalLibraryItem(existingItem.id)
|
||||||
return ItemScanResult.REMOVED
|
return ItemScanResult.REMOVED
|
||||||
@@ -207,9 +221,9 @@ class FolderScanner(var ctx: Context) {
|
|||||||
existingItem.updateFromScan(audioTracks,localFiles)
|
existingItem.updateFromScan(audioTracks,localFiles)
|
||||||
DeviceManager.dbManager.saveLocalLibraryItem(existingItem)
|
DeviceManager.dbManager.saveLocalLibraryItem(existingItem)
|
||||||
return ItemScanResult.UPDATED
|
return ItemScanResult.UPDATED
|
||||||
} else if (audioTracks.isNotEmpty()) {
|
} else if (audioTracks.isNotEmpty() || newEBookFile != null) {
|
||||||
Log.d(tag, "Found local media item named $itemFolderName with ${audioTracks.size} tracks and ${localFiles.size} local files")
|
Log.d(tag, "Found local media item named $itemFolderName with ${audioTracks.size} tracks and ${localFiles.size} local files")
|
||||||
val localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, itemFolder.uri.toString(), itemFolder.getSimplePath(ctx), itemFolder.getBasePath(ctx), itemFolder.getAbsolutePath(ctx),audioTracks,localFiles,coverContentUrl,coverAbsolutePath)
|
val localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, itemFolder.uri.toString(), itemFolder.getSimplePath(ctx), itemFolder.getBasePath(ctx), itemFolder.getAbsolutePath(ctx),audioTracks,newEBookFile,localFiles,coverContentUrl,coverAbsolutePath)
|
||||||
val localLibraryItem = localMediaItem.getLocalLibraryItem()
|
val localLibraryItem = localMediaItem.getLocalLibraryItem()
|
||||||
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
|
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
|
||||||
return ItemScanResult.ADDED
|
return ItemScanResult.ADDED
|
||||||
@@ -218,101 +232,126 @@ class FolderScanner(var ctx: Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan item after download and create local library item
|
private fun scanInternalDownloadItem(downloadItem:DownloadItem, cb: (DownloadItemScanResult?) -> Unit) {
|
||||||
fun scanDownloadItem(downloadItem: DownloadItem, cb: (DownloadItemScanResult?) -> Unit) {
|
val localLibraryItemId = "local_${downloadItem.libraryItemId}"
|
||||||
val folderDf = DocumentFileCompat.fromUri(ctx, Uri.parse(downloadItem.localFolder.contentUrl))
|
|
||||||
val foldersFound = folderDf?.search(true, DocumentFileType.FOLDER) ?: mutableListOf()
|
|
||||||
|
|
||||||
var itemFolderId = ""
|
|
||||||
var itemFolderUrl = ""
|
|
||||||
var itemFolderBasePath = ""
|
|
||||||
var itemFolderAbsolutePath = ""
|
|
||||||
foldersFound.forEach {
|
|
||||||
// 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
|
|
||||||
itemFolderUrl = it.uri.toString()
|
|
||||||
itemFolderBasePath = it.getBasePath(ctx)
|
|
||||||
itemFolderAbsolutePath = it.getAbsolutePath(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (itemFolderUrl == "") {
|
|
||||||
Log.d(tag, "scanDownloadItem failed to find media folder")
|
|
||||||
return cb(null)
|
|
||||||
}
|
|
||||||
val df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(itemFolderUrl))
|
|
||||||
|
|
||||||
if (df == null) {
|
|
||||||
Log.e(tag, "Folder Doc File Invalid ${downloadItem.itemFolderPath}")
|
|
||||||
return cb(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
val localLibraryItemId = getLocalLibraryItemId(itemFolderId)
|
|
||||||
Log.d(tag, "scanDownloadItem starting for ${downloadItem.itemFolderPath} | ${df.uri} | Item Folder Id:$itemFolderId | LLI Id:$localLibraryItemId")
|
|
||||||
|
|
||||||
// Search for files in media item folder
|
|
||||||
// m4b files showing as mimeType application/octet-stream on Android 10 and earlier see #154
|
|
||||||
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream"))
|
|
||||||
Log.d(tag, "scanDownloadItem ${filesFound.size} files found in ${downloadItem.itemFolderPath}")
|
|
||||||
|
|
||||||
var localEpisodeId:String? = null
|
var localEpisodeId:String? = null
|
||||||
var localLibraryItem:LocalLibraryItem?
|
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, downloadItem.itemFolderPath, downloadItem.itemFolderPath, "", false, downloadItem.mediaType, downloadItem.media.getLocalCopy(), mutableListOf(), null, null, true, downloadItem.serverConnectionConfigId, downloadItem.serverAddress, downloadItem.serverUserId, downloadItem.libraryItemId)
|
||||||
} else {
|
} else {
|
||||||
// Lookup or create podcast local library item
|
// Lookup or create podcast local library item
|
||||||
localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
|
localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
|
||||||
if (localLibraryItem == null) {
|
if (localLibraryItem == null) {
|
||||||
Log.d(tag, "[FolderScanner] Podcast local library item not created yet for ${downloadItem.media.metadata.title}")
|
Log.d(tag, "[FolderScanner] Podcast local library item not created yet for ${downloadItem.media.metadata.title}")
|
||||||
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, downloadItem.itemFolderPath, downloadItem.itemFolderPath, "", false, downloadItem.mediaType, downloadItem.media.getLocalCopy(), mutableListOf(), null, null, true,downloadItem.serverConnectionConfigId,downloadItem.serverAddress,downloadItem.serverUserId,downloadItem.libraryItemId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val audioTracks:MutableList<AudioTrack> = mutableListOf()
|
val audioTracks:MutableList<AudioTrack> = mutableListOf()
|
||||||
|
var foundEBookFile = false
|
||||||
|
|
||||||
filesFound.forEach { docFile ->
|
downloadItem.downloadItemParts.forEach { downloadItemPart ->
|
||||||
val itemPart = downloadItem.downloadItemParts.find { itemPart ->
|
Log.d(tag, "Scan internal storage item with finalDestinationUri=${downloadItemPart.finalDestinationUri}")
|
||||||
itemPart.filename == docFile.name
|
|
||||||
}
|
val file = File(downloadItemPart.finalDestinationPath)
|
||||||
if (itemPart == null) {
|
Log.d(tag, "Scan internal storage item created file ${file.name}")
|
||||||
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}")
|
if (file == null) {
|
||||||
|
Log.e(tag, "scanInternalDownloadItem: Null docFile for path ${downloadItemPart.finalDestinationPath}")
|
||||||
|
} else {
|
||||||
|
if (downloadItemPart.audioTrack != null) {
|
||||||
|
val audioTrackFromServer = downloadItemPart.audioTrack
|
||||||
|
Log.d(
|
||||||
|
tag,
|
||||||
|
"scanInternalDownloadItem: Audio Track from Server index = ${audioTrackFromServer.index}"
|
||||||
|
)
|
||||||
|
|
||||||
|
val localFileId = DeviceManager.getBase64Id(file.name)
|
||||||
|
Log.d(tag, "Scan internal file localFileId=$localFileId")
|
||||||
|
val localFile = LocalFile(
|
||||||
|
localFileId,
|
||||||
|
file.name,
|
||||||
|
downloadItemPart.finalDestinationUri.toString(),
|
||||||
|
file.getBasePath(ctx),
|
||||||
|
file.absolutePath,
|
||||||
|
file.getSimplePath(ctx),
|
||||||
|
file.mimeType,
|
||||||
|
file.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,
|
||||||
|
"scanInternalDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add podcast episodes to library
|
||||||
|
downloadItemPart.episode?.let { podcastEpisode ->
|
||||||
|
val podcast = localLibraryItem.media as Podcast
|
||||||
|
val newEpisode = podcast.addEpisode(track, podcastEpisode)
|
||||||
|
localEpisodeId = newEpisode.id
|
||||||
|
Log.d(
|
||||||
|
tag,
|
||||||
|
"scanInternalDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (downloadItemPart.ebookFile != null) {
|
||||||
|
foundEBookFile = true
|
||||||
|
Log.d(tag, "scanInternalDownloadItem: Ebook file found with mimetype=${file.mimeType}")
|
||||||
|
val localFileId = DeviceManager.getBase64Id(file.name)
|
||||||
|
val localFile = LocalFile(
|
||||||
|
localFileId,
|
||||||
|
file.name,
|
||||||
|
Uri.fromFile(file).toString(),
|
||||||
|
file.getBasePath(ctx),
|
||||||
|
file.absolutePath,
|
||||||
|
file.getSimplePath(ctx),
|
||||||
|
file.mimeType,
|
||||||
|
file.length()
|
||||||
|
)
|
||||||
|
localLibraryItem.localFiles.add(localFile)
|
||||||
|
|
||||||
|
val ebookFile = EBookFile(
|
||||||
|
downloadItemPart.ebookFile.ino,
|
||||||
|
downloadItemPart.ebookFile.metadata,
|
||||||
|
downloadItemPart.ebookFile.ebookFormat,
|
||||||
|
true,
|
||||||
|
localFileId,
|
||||||
|
localFile.contentUrl
|
||||||
|
)
|
||||||
|
(localLibraryItem.media as Book).ebookFile = ebookFile
|
||||||
|
Log.d(tag, "scanInternalDownloadItem: Ebook file added to lli ${localFile.contentUrl}")
|
||||||
|
} else {
|
||||||
|
val localFileId = DeviceManager.getBase64Id(file.name)
|
||||||
|
val localFile = LocalFile(localFileId,file.name,Uri.fromFile(file).toString(),file.getBasePath(ctx),file.absolutePath,file.getSimplePath(ctx),file.mimeType,file.length())
|
||||||
|
|
||||||
|
localLibraryItem.coverAbsolutePath = localFile.absolutePath
|
||||||
|
localLibraryItem.coverContentUrl = localFile.contentUrl
|
||||||
|
localLibraryItem.localFiles.add(localFile)
|
||||||
}
|
}
|
||||||
} 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() && !foundEBookFile) {
|
||||||
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 or ebook file in folder for ${downloadItem.itemFolderPath}")
|
||||||
return cb(null)
|
return cb(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,6 +386,175 @@ class FolderScanner(var ctx: Context) {
|
|||||||
progress = mediaProgress.progress,
|
progress = mediaProgress.progress,
|
||||||
currentTime = mediaProgress.currentTime,
|
currentTime = mediaProgress.currentTime,
|
||||||
isFinished = false,
|
isFinished = false,
|
||||||
|
ebookLocation = mediaProgress.ebookLocation,
|
||||||
|
ebookProgress = mediaProgress.ebookProgress,
|
||||||
|
lastUpdate = mediaProgress.lastUpdate,
|
||||||
|
startedAt = mediaProgress.startedAt,
|
||||||
|
finishedAt = mediaProgress.finishedAt,
|
||||||
|
serverConnectionConfigId = downloadItem.serverConnectionConfigId,
|
||||||
|
serverAddress = downloadItem.serverAddress,
|
||||||
|
serverUserId = downloadItem.serverUserId,
|
||||||
|
libraryItemId = downloadItem.libraryItemId,
|
||||||
|
episodeId = downloadItem.episodeId)
|
||||||
|
Log.d(tag, "scanLibraryItemFolder: Saving local media progress ${newLocalMediaProgress.id} at progress ${newLocalMediaProgress.progress}")
|
||||||
|
DeviceManager.dbManager.saveLocalMediaProgress(newLocalMediaProgress)
|
||||||
|
|
||||||
|
downloadItemScanResult.localMediaProgress = newLocalMediaProgress
|
||||||
|
}
|
||||||
|
|
||||||
|
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
|
||||||
|
|
||||||
|
cb(downloadItemScanResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan item after download and create local library item
|
||||||
|
fun scanDownloadItem(downloadItem: DownloadItem, cb: (DownloadItemScanResult?) -> Unit) {
|
||||||
|
// If downloading to internal storage handle separately
|
||||||
|
if (downloadItem.isInternalStorage) {
|
||||||
|
scanInternalDownloadItem(downloadItem, cb)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val folderDf = DocumentFileCompat.fromUri(ctx, Uri.parse(downloadItem.localFolder.contentUrl))
|
||||||
|
val foldersFound = folderDf?.search(true, DocumentFileType.FOLDER) ?: mutableListOf()
|
||||||
|
|
||||||
|
var itemFolderId = ""
|
||||||
|
var itemFolderUrl = ""
|
||||||
|
var itemFolderBasePath = ""
|
||||||
|
var itemFolderAbsolutePath = ""
|
||||||
|
foldersFound.forEach {
|
||||||
|
// 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
|
||||||
|
itemFolderUrl = it.uri.toString()
|
||||||
|
itemFolderBasePath = it.getBasePath(ctx)
|
||||||
|
itemFolderAbsolutePath = it.getAbsolutePath(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemFolderUrl == "") {
|
||||||
|
Log.d(tag, "scanDownloadItem failed to find media folder")
|
||||||
|
return cb(null)
|
||||||
|
}
|
||||||
|
val df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(itemFolderUrl))
|
||||||
|
|
||||||
|
if (df == null) {
|
||||||
|
Log.e(tag, "Folder Doc File Invalid ${downloadItem.itemFolderPath}")
|
||||||
|
return cb(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
val localLibraryItemId = getLocalLibraryItemId(itemFolderId)
|
||||||
|
Log.d(tag, "scanDownloadItem starting for ${downloadItem.itemFolderPath} | ${df.uri} | Item Folder Id:$itemFolderId | LLI Id:$localLibraryItemId")
|
||||||
|
|
||||||
|
// Search for files in media item folder
|
||||||
|
// m4b files showing as mimeType application/octet-stream on Android 10 and earlier see #154
|
||||||
|
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/*"))
|
||||||
|
Log.d(tag, "scanDownloadItem ${filesFound.size} files found in ${downloadItem.itemFolderPath}")
|
||||||
|
|
||||||
|
var localEpisodeId:String? = null
|
||||||
|
var localLibraryItem:LocalLibraryItem?
|
||||||
|
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)
|
||||||
|
} else {
|
||||||
|
// Lookup or create podcast local library item
|
||||||
|
localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
|
||||||
|
if (localLibraryItem == null) {
|
||||||
|
Log.d(tag, "[FolderScanner] Podcast local library item not created yet for ${downloadItem.media.metadata.title}")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val audioTracks:MutableList<AudioTrack> = mutableListOf()
|
||||||
|
var foundEBookFile = false
|
||||||
|
|
||||||
|
filesFound.forEach { docFile ->
|
||||||
|
val itemPart = downloadItem.downloadItemParts.find { itemPart ->
|
||||||
|
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)
|
||||||
|
|
||||||
|
// 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 if (itemPart.ebookFile != null) { // Ebook
|
||||||
|
foundEBookFile = true
|
||||||
|
Log.d(tag, "scanDownloadItem: Ebook file found with mimetype=${docFile.mimeType}")
|
||||||
|
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)
|
||||||
|
|
||||||
|
val ebookFile = EBookFile(itemPart.ebookFile.ino, itemPart.ebookFile.metadata, itemPart.ebookFile.ebookFormat, true, localFileId, localFile.contentUrl)
|
||||||
|
(localLibraryItem.media as Book).ebookFile = ebookFile
|
||||||
|
Log.d(tag, "scanDownloadItem: Ebook file added to lli ${localFile.contentUrl}")
|
||||||
|
} 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() && !foundEBookFile) {
|
||||||
|
Log.d(tag, "scanDownloadItem did not find any audio tracks or ebook file in folder for ${downloadItem.itemFolderPath}")
|
||||||
|
return cb(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For books sort audio tracks then set
|
||||||
|
if (downloadItem.mediaType == "book") {
|
||||||
|
audioTracks.sortBy { it.index }
|
||||||
|
|
||||||
|
var indexCheck = 1
|
||||||
|
var startOffset = 0.0
|
||||||
|
audioTracks.forEach { audioTrack ->
|
||||||
|
if (audioTrack.index != indexCheck || audioTrack.startOffset != startOffset) {
|
||||||
|
audioTrack.index = indexCheck
|
||||||
|
audioTrack.startOffset = startOffset
|
||||||
|
}
|
||||||
|
indexCheck++
|
||||||
|
startOffset += audioTrack.duration
|
||||||
|
}
|
||||||
|
|
||||||
|
localLibraryItem.media.setAudioTracks(audioTracks)
|
||||||
|
}
|
||||||
|
|
||||||
|
val downloadItemScanResult = DownloadItemScanResult(localLibraryItem,null)
|
||||||
|
|
||||||
|
// If library item had media progress then make local media progress and save
|
||||||
|
downloadItem.userMediaProgress?.let { mediaProgress ->
|
||||||
|
val localMediaProgressId = if (downloadItem.episodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
|
||||||
|
val newLocalMediaProgress = LocalMediaProgress(
|
||||||
|
id = localMediaProgressId,
|
||||||
|
localLibraryItemId = localLibraryItemId,
|
||||||
|
localEpisodeId = localEpisodeId,
|
||||||
|
duration = mediaProgress.duration,
|
||||||
|
progress = mediaProgress.progress,
|
||||||
|
currentTime = mediaProgress.currentTime,
|
||||||
|
isFinished = false,
|
||||||
|
ebookLocation = mediaProgress.ebookLocation,
|
||||||
|
ebookProgress = mediaProgress.ebookProgress,
|
||||||
lastUpdate = mediaProgress.lastUpdate,
|
lastUpdate = mediaProgress.lastUpdate,
|
||||||
startedAt = mediaProgress.startedAt,
|
startedAt = mediaProgress.startedAt,
|
||||||
finishedAt = mediaProgress.finishedAt,
|
finishedAt = mediaProgress.finishedAt,
|
||||||
@@ -378,7 +586,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
var wasUpdated = false
|
var wasUpdated = false
|
||||||
|
|
||||||
// Search for files in media item folder
|
// Search for files in media item folder
|
||||||
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/octet-stream"))
|
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/*"))
|
||||||
Log.d(tag, "scanLocalLibraryItem ${filesFound.size} files found in ${localLibraryItem.absolutePath}")
|
Log.d(tag, "scanLocalLibraryItem ${filesFound.size} files found in ${localLibraryItem.absolutePath}")
|
||||||
|
|
||||||
filesFound.forEach {
|
filesFound.forEach {
|
||||||
@@ -424,7 +632,7 @@ class FolderScanner(var ctx: Context) {
|
|||||||
val audioProbeResult = probeAudioFile(localFile.absolutePath)
|
val audioProbeResult = probeAudioFile(localFile.absolutePath)
|
||||||
|
|
||||||
val existingTrack = existingAudioTracks.find { audioTrack ->
|
val existingTrack = existingAudioTracks.find { audioTrack ->
|
||||||
audioTrack.localFileId == localFile.id
|
audioTrack.localFileId == localFileId
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingTrack == null) {
|
if (existingTrack == null) {
|
||||||
@@ -443,6 +651,16 @@ class FolderScanner(var ctx: Context) {
|
|||||||
|
|
||||||
wasUpdated = true
|
wasUpdated = true
|
||||||
}
|
}
|
||||||
|
} else if (localFile.isEBookFile()) {
|
||||||
|
if (localLibraryItem.mediaType == "book") {
|
||||||
|
val existingEbookFile = (localLibraryItem.media as Book).ebookFile
|
||||||
|
if (existingEbookFile == null || existingEbookFile.localFileId != localFileId) {
|
||||||
|
val ebookFile = EBookFile(localFileId, null, localFile.getEBookFormat() ?: "", true, localFileId, localFile.contentUrl)
|
||||||
|
(localLibraryItem.media as Book).ebookFile = ebookFile
|
||||||
|
Log.d(tag, "scanLocalLibraryItem: Ebook file added to lli ${localFile.contentUrl}")
|
||||||
|
wasUpdated = true
|
||||||
|
}
|
||||||
|
}
|
||||||
} else { // Check if cover is empty
|
} else { // Check if cover is empty
|
||||||
if (localLibraryItem.coverContentUrl == null) {
|
if (localLibraryItem.coverContentUrl == null) {
|
||||||
Log.d(tag, "scanLocalLibraryItem setting cover for ${localLibraryItem.media.metadata.title}")
|
Log.d(tag, "scanLocalLibraryItem setting cover for ${localLibraryItem.media.metadata.title}")
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ 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, DeviceSettings.default(), null)
|
||||||
}
|
}
|
||||||
fun saveDeviceData(deviceData: DeviceData) {
|
fun saveDeviceData(deviceData: DeviceData) {
|
||||||
Paper.book("device").write("data", deviceData)
|
Paper.book("device").write("data", deviceData)
|
||||||
@@ -237,18 +237,26 @@ class DbManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveLocalPlaybackSession(playbackSession: PlaybackSession) {
|
|
||||||
Paper.book("localPlaybackSession").write(playbackSession.id,playbackSession)
|
|
||||||
}
|
|
||||||
fun getLocalPlaybackSession(playbackSessionId:String): PlaybackSession? {
|
|
||||||
return Paper.book("localPlaybackSession").read(playbackSessionId)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun saveMediaItemHistory(mediaItemHistory: MediaItemHistory) {
|
fun saveMediaItemHistory(mediaItemHistory: MediaItemHistory) {
|
||||||
Paper.book("mediaItemHistory").write(mediaItemHistory.id,mediaItemHistory)
|
Paper.book("mediaItemHistory").write(mediaItemHistory.id,mediaItemHistory)
|
||||||
}
|
}
|
||||||
fun getMediaItemHistory(id:String): MediaItemHistory? {
|
fun getMediaItemHistory(id:String): MediaItemHistory? {
|
||||||
return Paper.book("mediaItemHistory").read(id)
|
return Paper.book("mediaItemHistory").read(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun savePlaybackSession(playbackSession: PlaybackSession) {
|
||||||
|
Paper.book("playbackSession").write(playbackSession.id,playbackSession)
|
||||||
|
}
|
||||||
|
fun removePlaybackSession(playbackSessionId:String) {
|
||||||
|
Paper.book("playbackSession").delete(playbackSessionId)
|
||||||
|
}
|
||||||
|
fun getPlaybackSessions():List<PlaybackSession> {
|
||||||
|
val sessions:MutableList<PlaybackSession> = mutableListOf()
|
||||||
|
Paper.book("playbackSession").allKeys.forEach { playbackSessionId ->
|
||||||
|
Paper.book("playbackSession").read<PlaybackSession>(playbackSessionId)?.let {
|
||||||
|
sessions.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sessions
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,10 +22,13 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class DownloadItemManager(var downloadManager:DownloadManager, private var folderScanner: FolderScanner, var mainActivity: MainActivity, private var clientEventEmitter:DownloadEventEmitter) {
|
class DownloadItemManager(var downloadManager:DownloadManager, private var folderScanner: FolderScanner, var mainActivity: MainActivity, private var clientEventEmitter:DownloadEventEmitter) {
|
||||||
val tag = "DownloadItemManager"
|
val tag = "DownloadItemManager"
|
||||||
private val maxSimultaneousDownloads = 1
|
private val maxSimultaneousDownloads = 3
|
||||||
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||||
|
|
||||||
enum class DownloadCheckStatus {
|
enum class DownloadCheckStatus {
|
||||||
@@ -43,6 +46,11 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
|
|||||||
fun onDownloadItemComplete(jsobj:JSObject)
|
fun onDownloadItemComplete(jsobj:JSObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface InternalProgressCallback {
|
||||||
|
fun onProgress(totalBytesWritten:Long, progress: Long)
|
||||||
|
fun onComplete(failed: Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
var isDownloading:Boolean = false
|
var isDownloading:Boolean = false
|
||||||
}
|
}
|
||||||
@@ -64,11 +72,32 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
|
|||||||
|
|
||||||
if (nextDownloadItemParts.size > 0) {
|
if (nextDownloadItemParts.size > 0) {
|
||||||
nextDownloadItemParts.forEach {
|
nextDownloadItemParts.forEach {
|
||||||
val dlRequest = it.getDownloadRequest()
|
if (it.isInternalStorage) {
|
||||||
val downloadId = downloadManager.enqueue(dlRequest)
|
val file = File(it.finalDestinationPath)
|
||||||
it.downloadId = downloadId
|
file.parentFile?.mkdirs()
|
||||||
Log.d(tag, "checkUpdateDownloadQueue: Starting download item part, downloadId=$downloadId")
|
|
||||||
currentDownloadItemParts.add(it)
|
val fileOutputStream = FileOutputStream(it.finalDestinationPath)
|
||||||
|
val internalProgressCallback = (object : InternalProgressCallback {
|
||||||
|
override fun onProgress(totalBytesWritten:Long, progress: Long) {
|
||||||
|
it.bytesDownloaded = totalBytesWritten
|
||||||
|
it.progress = progress
|
||||||
|
}
|
||||||
|
override fun onComplete(failed:Boolean) {
|
||||||
|
it.failed = failed
|
||||||
|
it.completed = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Log.d(tag, "Start internal download to destination path ${it.finalDestinationPath} from ${it.serverUrl}")
|
||||||
|
InternalDownloadManager(fileOutputStream, internalProgressCallback).download(it.serverUrl)
|
||||||
|
currentDownloadItemParts.add(it)
|
||||||
|
} else {
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,14 +117,25 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
|
|||||||
isDownloading = true
|
isDownloading = true
|
||||||
|
|
||||||
while (currentDownloadItemParts.size > 0) {
|
while (currentDownloadItemParts.size > 0) {
|
||||||
|
|
||||||
val itemParts = currentDownloadItemParts.filter { !it.isMoving }.map { it }
|
val itemParts = currentDownloadItemParts.filter { !it.isMoving }.map { it }
|
||||||
for (downloadItemPart in itemParts) {
|
for (downloadItemPart in itemParts) {
|
||||||
val downloadCheckStatus = checkDownloadItemPart(downloadItemPart)
|
if (downloadItemPart.isInternalStorage) {
|
||||||
clientEventEmitter.onDownloadItemPartUpdate(downloadItemPart)
|
clientEventEmitter.onDownloadItemPartUpdate(downloadItemPart)
|
||||||
|
|
||||||
// Will move to final destination, remove current item parts, and check if download item is finished
|
if (downloadItemPart.completed) {
|
||||||
handleDownloadItemPartCheck(downloadCheckStatus, downloadItemPart)
|
val downloadItem = downloadItemQueue.find { it.id == downloadItemPart.downloadItemId }
|
||||||
|
downloadItem?.let {
|
||||||
|
checkDownloadItemFinished(it)
|
||||||
|
}
|
||||||
|
currentDownloadItemParts.remove(downloadItemPart)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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)
|
delay(500)
|
||||||
@@ -165,7 +205,6 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
|
|||||||
Log.e(tag, "Download item part finished but download item not found ${downloadItemPart.filename}")
|
Log.e(tag, "Download item part finished but download item not found ${downloadItemPart.filename}")
|
||||||
currentDownloadItemParts.remove(downloadItemPart)
|
currentDownloadItemParts.remove(downloadItemPart)
|
||||||
} else if (downloadCheckStatus == DownloadCheckStatus.Successful) {
|
} else if (downloadCheckStatus == DownloadCheckStatus.Successful) {
|
||||||
|
|
||||||
val file = DocumentFileCompat.fromUri(mainActivity, downloadItemPart.destinationUri)
|
val file = DocumentFileCompat.fromUri(mainActivity, downloadItemPart.destinationUri)
|
||||||
Log.d(tag, "DOWNLOAD: DESTINATION URI ${downloadItemPart.destinationUri}")
|
Log.d(tag, "DOWNLOAD: DESTINATION URI ${downloadItemPart.destinationUri}")
|
||||||
|
|
||||||
@@ -188,7 +227,8 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
|
|||||||
|
|
||||||
// Rename to fix appended .mp3 on m4b/m4a files
|
// Rename to fix appended .mp3 on m4b/m4a files
|
||||||
// REF: https://github.com/anggrayudi/SimpleStorage/issues/94
|
// REF: https://github.com/anggrayudi/SimpleStorage/issues/94
|
||||||
if (resultDocFile.name?.endsWith(".m4b.mp3") == true || resultDocFile.name?.endsWith(".m4a.mp3") == true) {
|
val docNameLowerCase = resultDocFile.name?.lowercase(Locale.getDefault()) ?: ""
|
||||||
|
if (docNameLowerCase.endsWith(".m4b.mp3")|| docNameLowerCase.endsWith(".m4a.mp3")) {
|
||||||
resultDocFile.renameTo(downloadItemPart.filename)
|
resultDocFile.renameTo(downloadItemPart.filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,23 +263,27 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
|
|||||||
if (downloadItem.isDownloadFinished) {
|
if (downloadItem.isDownloadFinished) {
|
||||||
Log.i(tag, "Download Item finished ${downloadItem.media.metadata.title}")
|
Log.i(tag, "Download Item finished ${downloadItem.media.metadata.title}")
|
||||||
|
|
||||||
folderScanner.scanDownloadItem(downloadItem) { downloadItemScanResult ->
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
Log.d(tag, "Item download complete ${downloadItem.itemTitle} | local library item id: ${downloadItemScanResult?.localLibraryItem?.id}")
|
folderScanner.scanDownloadItem(downloadItem) { downloadItemScanResult ->
|
||||||
|
Log.d(tag, "Item download complete ${downloadItem.itemTitle} | local library item id: ${downloadItemScanResult?.localLibraryItem?.id}")
|
||||||
|
|
||||||
val jsobj = JSObject()
|
val jsobj = JSObject()
|
||||||
jsobj.put("libraryItemId", downloadItem.id)
|
jsobj.put("libraryItemId", downloadItem.id)
|
||||||
jsobj.put("localFolderId", downloadItem.localFolder.id)
|
jsobj.put("localFolderId", downloadItem.localFolder.id)
|
||||||
|
|
||||||
downloadItemScanResult?.localLibraryItem?.let { localLibraryItem ->
|
downloadItemScanResult?.localLibraryItem?.let { localLibraryItem ->
|
||||||
jsobj.put("localLibraryItem", JSObject(jacksonMapper.writeValueAsString(localLibraryItem)))
|
jsobj.put("localLibraryItem", JSObject(jacksonMapper.writeValueAsString(localLibraryItem)))
|
||||||
|
}
|
||||||
|
downloadItemScanResult?.localMediaProgress?.let { localMediaProgress ->
|
||||||
|
jsobj.put("localMediaProgress", JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
|
||||||
|
}
|
||||||
|
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
clientEventEmitter.onDownloadItemComplete(jsobj)
|
||||||
|
downloadItemQueue.remove(downloadItem)
|
||||||
|
DeviceManager.dbManager.removeDownloadItem(downloadItem.id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
downloadItemScanResult?.localMediaProgress?.let { localMediaProgress ->
|
|
||||||
jsobj.put("localMediaProgress", JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
|
|
||||||
}
|
|
||||||
|
|
||||||
clientEventEmitter.onDownloadItemComplete(jsobj)
|
|
||||||
downloadItemQueue.remove(downloadItem)
|
|
||||||
DeviceManager.dbManager.removeDownloadItem(downloadItem.id)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package com.audiobookshelf.app.managers
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.google.common.net.HttpHeaders.CONTENT_LENGTH
|
||||||
|
import okhttp3.*
|
||||||
|
import java.io.*
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class InternalDownloadManager(outputStream:FileOutputStream, private val progressCallback: DownloadItemManager.InternalProgressCallback) : AutoCloseable {
|
||||||
|
private val tag = "InternalDownloadManager"
|
||||||
|
|
||||||
|
private val client: OkHttpClient = OkHttpClient()
|
||||||
|
private val writer = BinaryFileWriter(outputStream, progressCallback)
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun download(url:String) {
|
||||||
|
val request: Request = Request.Builder().url(url).build()
|
||||||
|
client.newCall(request).enqueue(object : Callback {
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
Log.e(tag, "download URL $url FAILED")
|
||||||
|
progressCallback.onComplete(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
val responseBody: ResponseBody = response.body
|
||||||
|
?: throw IllegalStateException("Response doesn't contain a file")
|
||||||
|
|
||||||
|
val length: Long = (response.header(CONTENT_LENGTH, "1") ?: "0").toLong()
|
||||||
|
writer.write(responseBody.byteStream(), length)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(Exception::class)
|
||||||
|
override fun close() {
|
||||||
|
writer.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BinaryFileWriter(outputStream: OutputStream, progressCallback: DownloadItemManager.InternalProgressCallback) :
|
||||||
|
AutoCloseable {
|
||||||
|
private val outputStream: OutputStream
|
||||||
|
private val progressCallback: DownloadItemManager.InternalProgressCallback
|
||||||
|
|
||||||
|
init {
|
||||||
|
this.outputStream = outputStream
|
||||||
|
this.progressCallback = progressCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
fun write(inputStream: InputStream?, length: Long): Long {
|
||||||
|
BufferedInputStream(inputStream).use { input ->
|
||||||
|
val dataBuffer = ByteArray(CHUNK_SIZE)
|
||||||
|
var readBytes: Int
|
||||||
|
var totalBytes: Long = 0
|
||||||
|
while (input.read(dataBuffer).also { readBytes = it } != -1) {
|
||||||
|
totalBytes += readBytes.toLong()
|
||||||
|
outputStream.write(dataBuffer, 0, readBytes)
|
||||||
|
progressCallback.onProgress(totalBytes, (totalBytes * 100L) / length)
|
||||||
|
}
|
||||||
|
progressCallback.onComplete(false)
|
||||||
|
return totalBytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun close() {
|
||||||
|
outputStream.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val CHUNK_SIZE = 1024
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.audiobookshelf.app.managers
|
package com.audiobookshelf.app.managers
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.media.metrics.PlaybackSession
|
||||||
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
|
||||||
@@ -20,6 +21,9 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
private var sleepTimerLength:Long = 0L
|
private var sleepTimerLength:Long = 0L
|
||||||
private var sleepTimerElapsed:Long = 0L
|
private var sleepTimerElapsed:Long = 0L
|
||||||
private var sleepTimerFinishedAt:Long = 0L
|
private var sleepTimerFinishedAt:Long = 0L
|
||||||
|
private var isAutoSleepTimer:Boolean = false // When timer was auto-set
|
||||||
|
private var isFirstAutoSleepTimer: Boolean = true
|
||||||
|
private var sleepTimerSessionId:String = ""
|
||||||
|
|
||||||
private fun getCurrentTime():Long {
|
private fun getCurrentTime():Long {
|
||||||
return playerNotificationService.getCurrentTime()
|
return playerNotificationService.getCurrentTime()
|
||||||
@@ -54,12 +58,13 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
return (((sleepTimerEndTime - getCurrentTime()) / 1000).toDouble()).roundToInt()
|
return (((sleepTimerEndTime - getCurrentTime()) / 1000).toDouble()).roundToInt()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSleepTimer(time: Long, isChapterTime: Boolean) : Boolean {
|
private 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 = true
|
sleepTimerRunning = true
|
||||||
sleepTimerFinishedAt = 0L
|
sleepTimerFinishedAt = 0L
|
||||||
sleepTimerElapsed = 0L
|
sleepTimerElapsed = 0L
|
||||||
|
setVolume(1f)
|
||||||
|
|
||||||
// Register shake sensor
|
// Register shake sensor
|
||||||
playerNotificationService.registerSensor()
|
playerNotificationService.registerSensor()
|
||||||
@@ -79,13 +84,9 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
} else {
|
} else {
|
||||||
sleepTimerLength = time
|
sleepTimerLength = time
|
||||||
sleepTimerEndTime = 0L
|
sleepTimerEndTime = 0L
|
||||||
|
|
||||||
if (sleepTimerLength + getCurrentTime() > getDuration()) {
|
|
||||||
sleepTimerLength = getDuration() - getCurrentTime()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds(), isAutoSleepTimer)
|
||||||
|
|
||||||
sleepTimerTask = Timer("SleepTimer", false).schedule(0L, 1000L) {
|
sleepTimerTask = Timer("SleepTimer", false).schedule(0L, 1000L) {
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
@@ -96,7 +97,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
Log.d(tag, "Timer Elapsed $sleepTimerElapsed | Sleep TIMER time remaining $sleepTimeSecondsRemaining s")
|
Log.d(tag, "Timer Elapsed $sleepTimerElapsed | Sleep TIMER time remaining $sleepTimeSecondsRemaining s")
|
||||||
|
|
||||||
if (sleepTimeSecondsRemaining > 0) {
|
if (sleepTimeSecondsRemaining > 0) {
|
||||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(sleepTimeSecondsRemaining)
|
playerNotificationService.clientEventEmitter?.onSleepTimerSet(sleepTimeSecondsRemaining, isAutoSleepTimer)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sleepTimeSecondsRemaining <= 0) {
|
if (sleepTimeSecondsRemaining <= 0) {
|
||||||
@@ -106,11 +107,14 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
playerNotificationService.clientEventEmitter?.onSleepTimerEnded(getCurrentTime())
|
playerNotificationService.clientEventEmitter?.onSleepTimerEnded(getCurrentTime())
|
||||||
clearSleepTimer()
|
clearSleepTimer()
|
||||||
sleepTimerFinishedAt = System.currentTimeMillis()
|
sleepTimerFinishedAt = System.currentTimeMillis()
|
||||||
} else if (sleepTimeSecondsRemaining <= 60) {
|
} else if (sleepTimeSecondsRemaining <= 60 && DeviceManager.deviceData.deviceSettings?.disableSleepTimerFadeOut != true) {
|
||||||
// Start fading out audio
|
// Start fading out audio down to 10% volume
|
||||||
val volume = sleepTimeSecondsRemaining / 60F
|
val percentToReduce = 1 - (sleepTimeSecondsRemaining / 60F)
|
||||||
|
val volume = 1f - (percentToReduce * 0.9f)
|
||||||
Log.d(tag, "SLEEP VOLUME FADE $volume | ${sleepTimeSecondsRemaining}s remaining")
|
Log.d(tag, "SLEEP VOLUME FADE $volume | ${sleepTimeSecondsRemaining}s remaining")
|
||||||
setVolume(volume)
|
setVolume(volume)
|
||||||
|
} else {
|
||||||
|
setVolume(1f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,12 +122,20 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setManualSleepTimer(playbackSessionId:String, time: Long, isChapterTime:Boolean):Boolean {
|
||||||
|
sleepTimerSessionId = playbackSessionId
|
||||||
|
isAutoSleepTimer = false
|
||||||
|
return setSleepTimer(time, isChapterTime)
|
||||||
|
}
|
||||||
|
|
||||||
private fun clearSleepTimer() {
|
private fun clearSleepTimer() {
|
||||||
sleepTimerTask?.cancel()
|
sleepTimerTask?.cancel()
|
||||||
sleepTimerTask = null
|
sleepTimerTask = null
|
||||||
sleepTimerEndTime = 0
|
sleepTimerEndTime = 0
|
||||||
sleepTimerRunning = false
|
sleepTimerRunning = false
|
||||||
playerNotificationService.unregisterSensor()
|
playerNotificationService.unregisterSensor()
|
||||||
|
|
||||||
|
setVolume(1f)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSleepTimerTime():Long {
|
fun getSleepTimerTime():Long {
|
||||||
@@ -132,12 +144,21 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
|
|
||||||
fun cancelSleepTimer() {
|
fun cancelSleepTimer() {
|
||||||
Log.d(tag, "Canceling Sleep Timer")
|
Log.d(tag, "Canceling Sleep Timer")
|
||||||
|
|
||||||
|
if (isAutoSleepTimer) {
|
||||||
|
Log.i(tag, "Disabling auto sleep timer")
|
||||||
|
DeviceManager.deviceData.deviceSettings?.autoSleepTimer = false
|
||||||
|
DeviceManager.dbManager.saveDeviceData(DeviceManager.deviceData)
|
||||||
|
}
|
||||||
|
|
||||||
clearSleepTimer()
|
clearSleepTimer()
|
||||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(0)
|
playerNotificationService.clientEventEmitter?.onSleepTimerSet(0, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vibrate when extending sleep timer by shaking
|
// Vibrate when resetting sleep timer
|
||||||
private fun vibrate() {
|
private fun vibrateFeedback() {
|
||||||
|
if (DeviceManager.deviceData.deviceSettings?.disableSleepTimerResetFeedback == true) return
|
||||||
|
|
||||||
val context = playerNotificationService.getContext()
|
val context = playerNotificationService.getContext()
|
||||||
val vibrator:Vibrator
|
val vibrator:Vibrator
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
@@ -187,7 +208,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
private fun resetChapterTimer() {
|
private fun resetChapterTimer() {
|
||||||
this.getChapterEndTime()?.let { chapterEndTime ->
|
this.getChapterEndTime()?.let { chapterEndTime ->
|
||||||
Log.d(tag, "Resetting stopped sleep timer to end of chapter $chapterEndTime")
|
Log.d(tag, "Resetting stopped sleep timer to end of chapter $chapterEndTime")
|
||||||
vibrate()
|
vibrateFeedback()
|
||||||
setSleepTimer(chapterEndTime, true)
|
setSleepTimer(chapterEndTime, true)
|
||||||
play()
|
play()
|
||||||
}
|
}
|
||||||
@@ -205,6 +226,17 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Automatically Rewind in the book if settings is enabled
|
||||||
|
if (isAutoSleepTimer) {
|
||||||
|
DeviceManager.deviceData.deviceSettings?.let { deviceSettings ->
|
||||||
|
if (deviceSettings.autoSleepTimerAutoRewind && !isFirstAutoSleepTimer) {
|
||||||
|
Log.i(tag, "Auto sleep timer auto rewind seeking back ${deviceSettings.autoSleepTimerAutoRewindTime}ms")
|
||||||
|
playerNotificationService.seekBackward(deviceSettings.autoSleepTimerAutoRewindTime)
|
||||||
|
}
|
||||||
|
isFirstAutoSleepTimer = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Set sleep timer
|
// Set sleep timer
|
||||||
// When sleepTimerLength is 0 then use end of chapter/track time
|
// When sleepTimerLength is 0 then use end of chapter/track time
|
||||||
if (sleepTimerLength == 0L) {
|
if (sleepTimerLength == 0L) {
|
||||||
@@ -212,7 +244,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
resetChapterTimer()
|
resetChapterTimer()
|
||||||
} else {
|
} else {
|
||||||
Log.d(tag, "Resetting stopped sleep timer to length $sleepTimerLength")
|
Log.d(tag, "Resetting stopped sleep timer to length $sleepTimerLength")
|
||||||
vibrate()
|
vibrateFeedback()
|
||||||
setSleepTimer(sleepTimerLength, false)
|
setSleepTimer(sleepTimerLength, false)
|
||||||
play()
|
play()
|
||||||
}
|
}
|
||||||
@@ -222,14 +254,14 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
// Does not apply to chapter sleep timers and timer must be running for at least 3 seconds
|
// Does not apply to chapter sleep timers and timer must be running for at least 3 seconds
|
||||||
if (sleepTimerLength > 0L && sleepTimerElapsed > 3000L) {
|
if (sleepTimerLength > 0L && sleepTimerElapsed > 3000L) {
|
||||||
Log.d(tag, "Resetting running sleep timer to length $sleepTimerLength")
|
Log.d(tag, "Resetting running sleep timer to length $sleepTimerLength")
|
||||||
vibrate()
|
vibrateFeedback()
|
||||||
setSleepTimer(sleepTimerLength, false)
|
setSleepTimer(sleepTimerLength, false)
|
||||||
} else if (sleepTimerLength == 0L) {
|
} else if (sleepTimerLength == 0L) {
|
||||||
// When navigating to previous chapters make sure this is still the end of the current chapter
|
// When navigating to previous chapters make sure this is still the end of the current chapter
|
||||||
this.getChapterEndTime()?.let { chapterEndTime ->
|
this.getChapterEndTime()?.let { chapterEndTime ->
|
||||||
if (chapterEndTime != sleepTimerEndTime) {
|
if (chapterEndTime != sleepTimerEndTime) {
|
||||||
Log.d(tag, "Resetting chapter sleep timer to end of chapter $chapterEndTime from $sleepTimerEndTime")
|
Log.d(tag, "Resetting chapter sleep timer to end of chapter $chapterEndTime from $sleepTimerEndTime")
|
||||||
vibrate()
|
vibrateFeedback()
|
||||||
setSleepTimer(chapterEndTime, true)
|
setSleepTimer(chapterEndTime, true)
|
||||||
play()
|
play()
|
||||||
}
|
}
|
||||||
@@ -264,7 +296,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
}
|
}
|
||||||
|
|
||||||
setVolume(1F)
|
setVolume(1F)
|
||||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds(), isAutoSleepTimer)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun decreaseSleepTime(time: Long) {
|
fun decreaseSleepTime(time: Long) {
|
||||||
@@ -286,7 +318,7 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
}
|
}
|
||||||
|
|
||||||
setVolume(1F)
|
setVolume(1F)
|
||||||
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
|
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds(), isAutoSleepTimer)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkAutoSleepTimer() {
|
fun checkAutoSleepTimer() {
|
||||||
@@ -303,18 +335,31 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
endCalendar.set(Calendar.HOUR_OF_DAY, deviceSettings.autoSleepTimerEndHour)
|
endCalendar.set(Calendar.HOUR_OF_DAY, deviceSettings.autoSleepTimerEndHour)
|
||||||
endCalendar.set(Calendar.MINUTE, deviceSettings.autoSleepTimerEndMinute)
|
endCalendar.set(Calendar.MINUTE, deviceSettings.autoSleepTimerEndMinute)
|
||||||
|
|
||||||
// In cases where end time is earlier then start time then we add a day to end time
|
val currentCalendar = Calendar.getInstance()
|
||||||
// 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.
|
// In cases where end time is before start time then we shift the time window forward or backward based on the current time.
|
||||||
|
// e.g. start time 22:00 and end time 06:00.
|
||||||
|
// If current time is less than start time (e.g. 00:30) then start time will be the previous day.
|
||||||
|
// If current time is greater than start time (e.g. 23:00) then end time will be the next day.
|
||||||
if (endCalendar.before(startCalendar)) {
|
if (endCalendar.before(startCalendar)) {
|
||||||
endCalendar.add(Calendar.DAY_OF_MONTH, 1)
|
if (currentCalendar.before(startCalendar)) { // Shift start back a day
|
||||||
|
startCalendar.add(Calendar.DAY_OF_MONTH, -1)
|
||||||
|
} else { // Shift end forward a day
|
||||||
|
endCalendar.add(Calendar.DAY_OF_MONTH, 1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val currentCalendar = Calendar.getInstance()
|
|
||||||
val currentHour = SimpleDateFormat("HH:mm", Locale.getDefault()).format(currentCalendar.time)
|
val currentHour = SimpleDateFormat("HH:mm", Locale.getDefault()).format(currentCalendar.time)
|
||||||
if (currentCalendar.after(startCalendar) && currentCalendar.before(endCalendar)) {
|
if (currentCalendar.after(startCalendar) && currentCalendar.before(endCalendar)) {
|
||||||
Log.i(tag, "Current hour $currentHour is between ${deviceSettings.autoSleepTimerStartTime} and ${deviceSettings.autoSleepTimerEndTime} - starting sleep timer")
|
Log.i(tag, "Current hour $currentHour is between ${deviceSettings.autoSleepTimerStartTime} and ${deviceSettings.autoSleepTimerEndTime} - starting sleep timer")
|
||||||
|
|
||||||
|
// Automatically Rewind in the book if settings is enabled
|
||||||
|
if (deviceSettings.autoSleepTimerAutoRewind && !isFirstAutoSleepTimer) {
|
||||||
|
Log.i(tag, "Auto sleep timer auto rewind seeking back ${deviceSettings.autoSleepTimerAutoRewindTime}ms")
|
||||||
|
playerNotificationService.seekBackward(deviceSettings.autoSleepTimerAutoRewindTime)
|
||||||
|
}
|
||||||
|
isFirstAutoSleepTimer = false
|
||||||
|
|
||||||
// Set sleep timer
|
// Set sleep timer
|
||||||
// When sleepTimerLength is 0 then use end of chapter/track time
|
// When sleepTimerLength is 0 then use end of chapter/track time
|
||||||
if (deviceSettings.sleepTimerLength == 0L) {
|
if (deviceSettings.sleepTimerLength == 0L) {
|
||||||
@@ -322,19 +367,31 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
|
|||||||
if (chapterEndTimeMs == null) {
|
if (chapterEndTimeMs == null) {
|
||||||
Log.e(tag, "Setting auto sleep timer to end of chapter/track but there is no current session")
|
Log.e(tag, "Setting auto sleep timer to end of chapter/track but there is no current session")
|
||||||
} else {
|
} else {
|
||||||
|
isAutoSleepTimer = true
|
||||||
setSleepTimer(chapterEndTimeMs, true)
|
setSleepTimer(chapterEndTimeMs, true)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
isAutoSleepTimer = true
|
||||||
setSleepTimer(deviceSettings.sleepTimerLength, false)
|
setSleepTimer(deviceSettings.sleepTimerLength, false)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
isFirstAutoSleepTimer = true
|
||||||
Log.d(tag, "Current hour $currentHour is NOT between ${deviceSettings.autoSleepTimerStartTime} and ${deviceSettings.autoSleepTimerEndTime}")
|
Log.d(tag, "Current hour $currentHour is NOT between ${deviceSettings.autoSleepTimerStartTime} and ${deviceSettings.autoSleepTimerEndTime}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun handleMediaPlayEvent() {
|
fun handleMediaPlayEvent(playbackSessionId:String) {
|
||||||
checkShouldResetSleepTimer()
|
// Check if the playback session has changed
|
||||||
|
// If it hasn't changed OR the sleep timer is running then check reset the timer
|
||||||
|
// e.g. You set a manual sleep timer for 10 mins, then decide to change books, the sleep timer will stay on and reset to 10 mins
|
||||||
|
if (sleepTimerSessionId == playbackSessionId || sleepTimerRunning) {
|
||||||
|
checkShouldResetSleepTimer()
|
||||||
|
} else {
|
||||||
|
isFirstAutoSleepTimer = true
|
||||||
|
}
|
||||||
|
sleepTimerSessionId = playbackSessionId
|
||||||
|
|
||||||
checkAutoSleepTimer()
|
checkAutoSleepTimer()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.util.Log
|
|||||||
import com.audiobookshelf.app.data.*
|
import com.audiobookshelf.app.data.*
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import com.audiobookshelf.app.player.PlayerNotificationService
|
import com.audiobookshelf.app.player.PlayerNotificationService
|
||||||
import com.audiobookshelf.app.player.SyncResult
|
|
||||||
|
|
||||||
object MediaEventManager {
|
object MediaEventManager {
|
||||||
const val tag = "MediaEventManager"
|
const val tag = "MediaEventManager"
|
||||||
@@ -69,7 +68,7 @@ object MediaEventManager {
|
|||||||
clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory)
|
clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addPlaybackEvent(eventName:String, playbackSession:PlaybackSession, syncResult:SyncResult?) {
|
private fun addPlaybackEvent(eventName:String, playbackSession:PlaybackSession, syncResult: SyncResult?) {
|
||||||
val mediaItemHistory = getMediaItemHistoryMediaItem(playbackSession.mediaItemId) ?: createMediaItemHistoryForSession(playbackSession)
|
val mediaItemHistory = getMediaItemHistoryMediaItem(playbackSession.mediaItemId) ?: createMediaItemHistoryForSession(playbackSession)
|
||||||
|
|
||||||
val mediaItemEvent = MediaItemEvent(
|
val mediaItemEvent = MediaItemEvent(
|
||||||
@@ -103,8 +102,7 @@ object MediaEventManager {
|
|||||||
libraryItemId,
|
libraryItemId,
|
||||||
episodeId,
|
episodeId,
|
||||||
isLocalOnly,
|
isLocalOnly,
|
||||||
playbackSession.
|
playbackSession.serverConnectionConfigId,
|
||||||
serverConnectionConfigId,
|
|
||||||
playbackSession.serverAddress,
|
playbackSession.serverAddress,
|
||||||
playbackSession.userId,
|
playbackSession.userId,
|
||||||
createdAt = System.currentTimeMillis(),
|
createdAt = System.currentTimeMillis(),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import java.util.*
|
|||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
|
import org.json.JSONObject
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
var serverItemsInProgress = listOf<ItemInProgress>()
|
var serverItemsInProgress = listOf<ItemInProgress>()
|
||||||
var serverLibraries = listOf<Library>()
|
var serverLibraries = listOf<Library>()
|
||||||
|
|
||||||
private var userSettingsPlaybackRate:Float? = null
|
var userSettingsPlaybackRate:Float? = null
|
||||||
|
|
||||||
fun getIsLibrary(id:String) : Boolean {
|
fun getIsLibrary(id:String) : Boolean {
|
||||||
return serverLibraries.find { it.id == id } != null
|
return serverLibraries.find { it.id == id } != null
|
||||||
@@ -61,12 +62,41 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
return 1f
|
return 1f
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setSavedPlaybackRate(newRate: Float) {
|
||||||
|
val sharedPrefs = ctx.getSharedPreferences("CapacitorStorage", Activity.MODE_PRIVATE)
|
||||||
|
val sharedPrefEditor = sharedPrefs.edit()
|
||||||
|
if (sharedPrefs != null) {
|
||||||
|
val userSettingsPref = sharedPrefs.getString("userSettings", null)
|
||||||
|
if (userSettingsPref != null) {
|
||||||
|
try {
|
||||||
|
val userSettings = JSObject(userSettingsPref)
|
||||||
|
// toString().toDouble() to prevent float conversion issues (ex 1.2f becomes 1.2000000476837158d)
|
||||||
|
userSettings.put("playbackRate", newRate.toString().toDouble())
|
||||||
|
sharedPrefEditor.putString("userSettings", userSettings.toString())
|
||||||
|
sharedPrefEditor.apply()
|
||||||
|
userSettingsPlaybackRate = newRate
|
||||||
|
Log.d(tag, "Saved userSettings JSON from Android Auto with playbackRate=$newRate")
|
||||||
|
} catch(je:JSONException) {
|
||||||
|
Log.e(tag, "Failed to save userSettings JSON ${je.localizedMessage}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Not sure if this is the best place for this, but if a user has not changed any user settings in the app
|
||||||
|
// the object will not exist yet, could be moved to a centralized place or created on first app load
|
||||||
|
val userSettings = JSONObject()
|
||||||
|
userSettings.put("playbackRate", newRate.toString().toDouble())
|
||||||
|
sharedPrefEditor.putString("userSettings", userSettings.toString())
|
||||||
|
userSettingsPlaybackRate = newRate
|
||||||
|
Log.d(tag, "Created and saved userSettings JSON from Android Auto with playbackRate=$newRate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun checkResetServerItems() {
|
fun checkResetServerItems() {
|
||||||
// When opening android auto need to check if still connected to server
|
// When opening android auto need to check if still connected to server
|
||||||
// and reset any server data already set
|
// and reset any server data already set
|
||||||
val serverConnConfig = if (DeviceManager.isConnectedToServer) DeviceManager.serverConnectionConfig else DeviceManager.deviceData.getLastServerConnectionConfig()
|
val serverConnConfig = if (DeviceManager.isConnectedToServer) DeviceManager.serverConnectionConfig else DeviceManager.deviceData.getLastServerConnectionConfig()
|
||||||
|
|
||||||
if (!DeviceManager.isConnectedToServer || !apiHandler.isOnline() || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) {
|
if (!DeviceManager.isConnectedToServer || !DeviceManager.checkConnectivity(ctx) || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) {
|
||||||
podcastEpisodeLibraryItemMap = mutableMapOf()
|
podcastEpisodeLibraryItemMap = mutableMapOf()
|
||||||
serverLibraryCategories = listOf()
|
serverLibraryCategories = listOf()
|
||||||
serverLibraries = listOf()
|
serverLibraries = listOf()
|
||||||
@@ -81,7 +111,10 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
cb(serverItemsInProgress)
|
cb(serverItemsInProgress)
|
||||||
} else {
|
} else {
|
||||||
apiHandler.getAllItemsInProgress { itemsInProgress ->
|
apiHandler.getAllItemsInProgress { itemsInProgress ->
|
||||||
serverItemsInProgress = itemsInProgress
|
serverItemsInProgress = itemsInProgress.filter {
|
||||||
|
val libraryItem = it.libraryItemWrapper as LibraryItem
|
||||||
|
libraryItem.checkHasTracks()
|
||||||
|
}
|
||||||
cb(serverItemsInProgress)
|
cb(serverItemsInProgress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -217,7 +250,7 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
Log.d(tag, "checkSetValidServerConnectionConfig | $serverConfigIdUsed")
|
Log.d(tag, "checkSetValidServerConnectionConfig | $serverConfigIdUsed")
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
if (!apiHandler.isOnline()) {
|
if (!DeviceManager.checkConnectivity(ctx)) {
|
||||||
serverUserMediaProgress = mutableListOf()
|
serverUserMediaProgress = mutableListOf()
|
||||||
cb(false)
|
cb(false)
|
||||||
} else {
|
} else {
|
||||||
@@ -364,7 +397,7 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
|
|||||||
|
|
||||||
fun play(libraryItemWrapper:LibraryItemWrapper, episode:PodcastEpisode?, playItemRequestPayload:PlayItemRequestPayload, cb: (PlaybackSession?) -> Unit) {
|
fun play(libraryItemWrapper:LibraryItemWrapper, episode:PodcastEpisode?, playItemRequestPayload:PlayItemRequestPayload, cb: (PlaybackSession?) -> Unit) {
|
||||||
if (libraryItemWrapper is LocalLibraryItem) {
|
if (libraryItemWrapper is LocalLibraryItem) {
|
||||||
cb(libraryItemWrapper.getPlaybackSession(episode))
|
cb(libraryItemWrapper.getPlaybackSession(episode, playItemRequestPayload.deviceInfo))
|
||||||
} else {
|
} else {
|
||||||
val libraryItem = libraryItemWrapper as LibraryItem
|
val libraryItem = libraryItemWrapper as LibraryItem
|
||||||
apiHandler.playLibraryItem(libraryItem.id,episode?.id ?: "", playItemRequestPayload) {
|
apiHandler.playLibraryItem(libraryItem.id,episode?.id ?: "", playItemRequestPayload) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.audiobookshelf.app.player
|
package com.audiobookshelf.app.media
|
||||||
|
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
@@ -6,9 +6,8 @@ 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.player.PlayerNotificationService
|
||||||
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
|
||||||
@@ -25,7 +24,7 @@ data class SyncResult(
|
|||||||
var serverSyncMessage:String?
|
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
|
||||||
|
|
||||||
@@ -39,7 +38,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
var currentLocalMediaProgress: LocalMediaProgress? = null
|
var currentLocalMediaProgress: LocalMediaProgress? = null
|
||||||
|
|
||||||
private val currentDisplayTitle get() = currentPlaybackSession?.displayTitle ?: "Unset"
|
private val currentDisplayTitle get() = currentPlaybackSession?.displayTitle ?: "Unset"
|
||||||
private val currentIsLocal get() = currentPlaybackSession?.isLocal == true
|
val currentIsLocal get() = currentPlaybackSession?.isLocal == true
|
||||||
val currentSessionId get() = currentPlaybackSession?.id ?: ""
|
val currentSessionId get() = currentPlaybackSession?.id ?: ""
|
||||||
private val currentPlaybackDuration get() = currentPlaybackSession?.duration ?: 0.0
|
private val currentPlaybackDuration get() = currentPlaybackSession?.duration ?: 0.0
|
||||||
|
|
||||||
@@ -62,8 +61,8 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
|
|
||||||
listeningTimerRunning = true
|
listeningTimerRunning = true
|
||||||
lastSyncTime = System.currentTimeMillis()
|
lastSyncTime = System.currentTimeMillis()
|
||||||
Log.d(tag, "start: init last sync time $lastSyncTime")
|
|
||||||
currentPlaybackSession = playbackSession.clone()
|
currentPlaybackSession = playbackSession.clone()
|
||||||
|
Log.d(tag, "start: init last sync time $lastSyncTime with playback session id=${currentPlaybackSession?.id}")
|
||||||
|
|
||||||
listeningTimerTask = Timer("ListeningTimer", false).schedule(15000L, 15000L) {
|
listeningTimerTask = Timer("ListeningTimer", false).schedule(15000L, 15000L) {
|
||||||
Handler(Looper.getMainLooper()).post() {
|
Handler(Looper.getMainLooper()).post() {
|
||||||
@@ -199,8 +198,6 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
it.currentTime = mediaProgress.currentTime
|
it.currentTime = mediaProgress.currentTime
|
||||||
|
|
||||||
MediaEventManager.syncEvent(mediaProgress, "Received from server get media progress request while playback session open")
|
MediaEventManager.syncEvent(mediaProgress, "Received from server get media progress request while playback session open")
|
||||||
|
|
||||||
DeviceManager.dbManager.saveLocalPlaybackSession(it)
|
|
||||||
saveLocalProgress(it)
|
saveLocalProgress(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,7 +215,6 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
val listeningTimeToAdd = diffSinceLastSync / 1000L
|
val listeningTimeToAdd = diffSinceLastSync / 1000L
|
||||||
|
|
||||||
val syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime)
|
val syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime)
|
||||||
|
|
||||||
currentPlaybackSession?.syncData(syncData)
|
currentPlaybackSession?.syncData(syncData)
|
||||||
|
|
||||||
if (currentPlaybackSession?.progress?.isNaN() == true) {
|
if (currentPlaybackSession?.progress?.isNaN() == true) {
|
||||||
@@ -226,32 +222,37 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
return cb(null)
|
return cb(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val hasNetworkConnection = DeviceManager.checkConnectivity(playerNotificationService)
|
||||||
|
|
||||||
|
// Save playback session to db (server linked sessions only)
|
||||||
|
// Sessions are removed once successfully synced with the server
|
||||||
|
currentPlaybackSession?.let {
|
||||||
|
if (!it.isLocalLibraryItemOnly) {
|
||||||
|
DeviceManager.dbManager.savePlaybackSession(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (currentIsLocal) {
|
if (currentIsLocal) {
|
||||||
// Save local progress sync
|
// Save local progress sync
|
||||||
currentPlaybackSession?.let {
|
currentPlaybackSession?.let {
|
||||||
DeviceManager.dbManager.saveLocalPlaybackSession(it)
|
|
||||||
saveLocalProgress(it)
|
saveLocalProgress(it)
|
||||||
lastSyncTime = System.currentTimeMillis()
|
lastSyncTime = System.currentTimeMillis()
|
||||||
|
|
||||||
// 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 (hasNetworkConnection && shouldSyncServer && !it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
|
||||||
apiHandler.sendLocalProgressSync(it) { syncSuccess, errorMsg ->
|
apiHandler.sendLocalProgressSync(it) { syncSuccess, errorMsg ->
|
||||||
Log.d(
|
|
||||||
tag,
|
|
||||||
"Local progress sync data sent to server $currentDisplayTitle for time $currentTime"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (syncSuccess) {
|
if (syncSuccess) {
|
||||||
failedSyncs = 0
|
failedSyncs = 0
|
||||||
playerNotificationService.alertSyncSuccess()
|
playerNotificationService.alertSyncSuccess()
|
||||||
|
DeviceManager.dbManager.removePlaybackSession(it.id) // Remove session from db
|
||||||
} else {
|
} else {
|
||||||
failedSyncs++
|
failedSyncs++
|
||||||
if (failedSyncs == 2) {
|
if (failedSyncs == 2) {
|
||||||
playerNotificationService.alertSyncFailing() // Show alert in client
|
playerNotificationService.alertSyncFailing() // Show alert in client
|
||||||
failedSyncs = 0
|
failedSyncs = 0
|
||||||
}
|
}
|
||||||
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 with session id=${it.id}")
|
||||||
}
|
}
|
||||||
|
|
||||||
cb(SyncResult(true, syncSuccess, errorMsg))
|
cb(SyncResult(true, syncSuccess, errorMsg))
|
||||||
@@ -260,7 +261,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
cb(SyncResult(false, null, null))
|
cb(SyncResult(false, null, null))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (shouldSyncServer) {
|
} else if (hasNetworkConnection && shouldSyncServer) {
|
||||||
Log.d(tag, "sync: currentSessionId=$currentSessionId")
|
Log.d(tag, "sync: currentSessionId=$currentSessionId")
|
||||||
apiHandler.sendProgressSync(currentSessionId, syncData) { syncSuccess, errorMsg ->
|
apiHandler.sendProgressSync(currentSessionId, syncData) { syncSuccess, errorMsg ->
|
||||||
if (syncSuccess) {
|
if (syncSuccess) {
|
||||||
@@ -268,13 +269,14 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
failedSyncs = 0
|
failedSyncs = 0
|
||||||
playerNotificationService.alertSyncSuccess()
|
playerNotificationService.alertSyncSuccess()
|
||||||
lastSyncTime = System.currentTimeMillis()
|
lastSyncTime = System.currentTimeMillis()
|
||||||
|
DeviceManager.dbManager.removePlaybackSession(currentSessionId) // Remove session from db
|
||||||
} else {
|
} else {
|
||||||
failedSyncs++
|
failedSyncs++
|
||||||
if (failedSyncs == 2) {
|
if (failedSyncs == 2) {
|
||||||
playerNotificationService.alertSyncFailing() // Show alert in client
|
playerNotificationService.alertSyncFailing() // Show alert in client
|
||||||
failedSyncs = 0
|
failedSyncs = 0
|
||||||
}
|
}
|
||||||
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 with session id=${currentSessionId}")
|
||||||
}
|
}
|
||||||
cb(SyncResult(true, syncSuccess, errorMsg))
|
cb(SyncResult(true, syncSuccess, errorMsg))
|
||||||
}
|
}
|
||||||
@@ -307,6 +309,7 @@ class MediaProgressSyncer(val playerNotificationService:PlayerNotificationServic
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun reset() {
|
fun reset() {
|
||||||
currentPlaybackSession = null
|
currentPlaybackSession = null
|
||||||
currentLocalMediaProgress = null
|
currentLocalMediaProgress = null
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ data class DownloadItem(
|
|||||||
val media: MediaType,
|
val media: MediaType,
|
||||||
val downloadItemParts: MutableList<DownloadItemPart>
|
val downloadItemParts: MutableList<DownloadItemPart>
|
||||||
) {
|
) {
|
||||||
|
@get:JsonIgnore
|
||||||
|
val isInternalStorage get() = localFolder.id.startsWith("internal-")
|
||||||
|
|
||||||
@get:JsonIgnore
|
@get:JsonIgnore
|
||||||
val isDownloadFinished get() = !downloadItemParts.any { !it.completed || it.isMoving }
|
val isDownloadFinished get() = !downloadItemParts.any { !it.completed || it.isMoving }
|
||||||
|
|
||||||
@@ -32,7 +35,7 @@ data class DownloadItem(
|
|||||||
for (it in downloadItemParts) {
|
for (it in downloadItemParts) {
|
||||||
if (!it.completed && it.downloadId == null) {
|
if (!it.completed && it.downloadId == null) {
|
||||||
itemParts.add(it)
|
itemParts.add(it)
|
||||||
if (itemParts.size > limit) break
|
if (itemParts.size >= limit) break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.app.DownloadManager
|
|||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.data.AudioTrack
|
import com.audiobookshelf.app.data.AudioTrack
|
||||||
|
import com.audiobookshelf.app.data.EBookFile
|
||||||
import com.audiobookshelf.app.data.LocalFolder
|
import com.audiobookshelf.app.data.LocalFolder
|
||||||
import com.audiobookshelf.app.data.PodcastEpisode
|
import com.audiobookshelf.app.data.PodcastEpisode
|
||||||
import com.audiobookshelf.app.device.DeviceManager
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
@@ -20,6 +21,7 @@ data class DownloadItemPart(
|
|||||||
val localFolderName: String,
|
val localFolderName: String,
|
||||||
val localFolderUrl: String,
|
val localFolderUrl: String,
|
||||||
val localFolderId: String,
|
val localFolderId: String,
|
||||||
|
val ebookFile: EBookFile?,
|
||||||
val audioTrack: AudioTrack?,
|
val audioTrack: AudioTrack?,
|
||||||
val episode: PodcastEpisode?,
|
val episode: PodcastEpisode?,
|
||||||
var completed:Boolean,
|
var completed:Boolean,
|
||||||
@@ -35,12 +37,12 @@ data class DownloadItemPart(
|
|||||||
var bytesDownloaded: Long
|
var bytesDownloaded: Long
|
||||||
) {
|
) {
|
||||||
companion object {
|
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 {
|
fun make(downloadItemId:String, filename:String, fileSize: Long, destinationFile: File, finalDestinationFile: File, subfolder:String, serverPath:String, localFolder: LocalFolder, ebookFile: EBookFile?, audioTrack: AudioTrack?, episode: PodcastEpisode?) :DownloadItemPart {
|
||||||
val destinationUri = Uri.fromFile(destinationFile)
|
val destinationUri = Uri.fromFile(destinationFile)
|
||||||
val finalDestinationUri = Uri.fromFile(finalDestinationFile)
|
val finalDestinationUri = Uri.fromFile(finalDestinationFile)
|
||||||
|
|
||||||
var downloadUrl = "${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}"
|
var downloadUrl = "${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}"
|
||||||
if (serverPath.endsWith("/cover")) downloadUrl += "&format=jpeg" // For cover images force to jpeg
|
if (serverPath.endsWith("/cover")) downloadUrl += "&format=jpeg&raw=1" // For cover images force to jpeg
|
||||||
val downloadUri = Uri.parse(downloadUrl)
|
val downloadUri = Uri.parse(downloadUrl)
|
||||||
Log.d("DownloadItemPart", "Audio File Destination Uri: $destinationUri | Final Destination Uri: $finalDestinationUri | Download URI $downloadUri")
|
Log.d("DownloadItemPart", "Audio File Destination Uri: $destinationUri | Final Destination Uri: $finalDestinationUri | Download URI $downloadUri")
|
||||||
return DownloadItemPart(
|
return DownloadItemPart(
|
||||||
@@ -53,6 +55,7 @@ data class DownloadItemPart(
|
|||||||
localFolderName = localFolder.name,
|
localFolderName = localFolder.name,
|
||||||
localFolderUrl = localFolder.contentUrl,
|
localFolderUrl = localFolder.contentUrl,
|
||||||
localFolderId = localFolder.id,
|
localFolderId = localFolder.id,
|
||||||
|
ebookFile = ebookFile,
|
||||||
audioTrack = audioTrack,
|
audioTrack = audioTrack,
|
||||||
episode = episode,
|
episode = episode,
|
||||||
completed = false,
|
completed = false,
|
||||||
@@ -70,6 +73,12 @@ data class DownloadItemPart(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@get:JsonIgnore
|
||||||
|
val isInternalStorage get() = localFolderId.startsWith("internal-")
|
||||||
|
|
||||||
|
@get:JsonIgnore
|
||||||
|
val serverUrl get() = "${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}"
|
||||||
|
|
||||||
@JsonIgnore
|
@JsonIgnore
|
||||||
fun getDownloadRequest(): DownloadManager.Request {
|
fun getDownloadRequest(): DownloadManager.Request {
|
||||||
val dlRequest = DownloadManager.Request(uri)
|
val dlRequest = DownloadManager.Request(uri)
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.audiobookshelf.app.models
|
||||||
|
|
||||||
|
import com.audiobookshelf.app.data.MediaProgress
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
|
||||||
|
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
data class User(
|
||||||
|
val id:String,
|
||||||
|
val username: String,
|
||||||
|
val mediaProgress:List<MediaProgress>
|
||||||
|
)
|
||||||
@@ -8,6 +8,7 @@ import android.os.Build
|
|||||||
import android.provider.MediaStore
|
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.BuildConfig
|
||||||
import com.audiobookshelf.app.R
|
import com.audiobookshelf.app.R
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
@@ -83,7 +84,7 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
|
|||||||
|
|
||||||
Glide.with(playerNotificationService)
|
Glide.with(playerNotificationService)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.load(Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon))
|
.load(Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon))
|
||||||
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)
|
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)
|
||||||
.get()
|
.get()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ class CastManager constructor(val mainActivity:Activity) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun startRouteScan(connListener:ChromecastListener) {
|
fun startRouteScan(connListener:ChromecastListener) {
|
||||||
var callback = object : ScanCallback() {
|
val callback = object : ScanCallback() {
|
||||||
override fun onRouteUpdate(routes: List<MediaRouter.RouteInfo>?) {
|
override fun onRouteUpdate(routes: List<MediaRouter.RouteInfo>?) {
|
||||||
Log.d(tag, "CAST On ROUTE UPDATED ${routes?.size} | ${getContext().castState}")
|
Log.d(tag, "CAST On ROUTE UPDATED ${routes?.size} | ${getContext().castState}")
|
||||||
// if the routes have changed, we may have an available device
|
// if the routes have changed, we may have an available device
|
||||||
@@ -136,7 +136,7 @@ class CastManager constructor(val mainActivity:Activity) {
|
|||||||
// Let the client know a receiver is available
|
// Let the client know a receiver is available
|
||||||
connListener.onReceiverAvailableUpdate(true)
|
connListener.onReceiverAvailableUpdate(true)
|
||||||
// Since we have a receiver we may also have an active session
|
// Since we have a receiver we may also have an active session
|
||||||
var session = getSessionManager()?.currentCastSession
|
val session = getSessionManager()?.currentCastSession
|
||||||
// If we do have a session
|
// If we do have a session
|
||||||
if (session != null) {
|
if (session != null) {
|
||||||
// Let the client know
|
// Let the client know
|
||||||
@@ -246,15 +246,15 @@ class CastManager constructor(val mainActivity:Activity) {
|
|||||||
onRouteUpdate(outRoutes)
|
onRouteUpdate(outRoutes)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRouteAdded(router: MediaRouter?, route: MediaRouter.RouteInfo?) {
|
override fun onRouteAdded(router: MediaRouter, route: MediaRouter.RouteInfo) {
|
||||||
onFilteredRouteUpdate()
|
onFilteredRouteUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRouteChanged(router: MediaRouter?, route: MediaRouter.RouteInfo?) {
|
override fun onRouteChanged(router: MediaRouter, route: MediaRouter.RouteInfo) {
|
||||||
onFilteredRouteUpdate()
|
onFilteredRouteUpdate()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRouteRemoved(router: MediaRouter?, route: MediaRouter.RouteInfo?) {
|
override fun onRouteRemoved(router: MediaRouter, route: MediaRouter.RouteInfo) {
|
||||||
onFilteredRouteUpdate()
|
onFilteredRouteUpdate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import com.google.android.exoplayer2.*
|
|||||||
import com.google.android.exoplayer2.C.TrackType
|
import com.google.android.exoplayer2.C.TrackType
|
||||||
import com.google.android.exoplayer2.MediaMetadata
|
import com.google.android.exoplayer2.MediaMetadata
|
||||||
import com.google.android.exoplayer2.Player.*
|
import com.google.android.exoplayer2.Player.*
|
||||||
import com.google.android.exoplayer2.TracksInfo.TrackGroupInfo
|
import com.google.android.exoplayer2.Tracks.Group
|
||||||
import com.google.android.exoplayer2.audio.AudioAttributes
|
import com.google.android.exoplayer2.audio.AudioAttributes
|
||||||
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener
|
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener
|
||||||
import com.google.android.exoplayer2.source.TrackGroup
|
import com.google.android.exoplayer2.source.TrackGroup
|
||||||
import com.google.android.exoplayer2.source.TrackGroupArray
|
import com.google.android.exoplayer2.source.TrackGroupArray
|
||||||
import com.google.android.exoplayer2.text.Cue
|
import com.google.android.exoplayer2.text.CueGroup
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelection
|
import com.google.android.exoplayer2.trackselection.TrackSelection
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
|
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
|
||||||
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters
|
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters
|
||||||
@@ -49,7 +49,7 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
|
|||||||
private var myPlayWhenReady:Boolean = false
|
private var myPlayWhenReady:Boolean = false
|
||||||
private var playbackParameters:PlaybackParameters = PlaybackParameters.DEFAULT
|
private var playbackParameters:PlaybackParameters = PlaybackParameters.DEFAULT
|
||||||
private var myAvailableCommands: Commands
|
private var myAvailableCommands: Commands
|
||||||
private var myCurrentTracksInfo: TracksInfo
|
private var myCurrentTracksInfo: Tracks
|
||||||
private var myCurrentTrackGroups: TrackGroupArray
|
private var myCurrentTrackGroups: TrackGroupArray
|
||||||
private var myCurrentTrackSelections: TrackSelectionArray
|
private var myCurrentTrackSelections: TrackSelectionArray
|
||||||
|
|
||||||
@@ -76,7 +76,7 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
|
|||||||
COMMAND_GET_MEDIA_ITEMS_METADATA,
|
COMMAND_GET_MEDIA_ITEMS_METADATA,
|
||||||
COMMAND_SET_MEDIA_ITEMS_METADATA,
|
COMMAND_SET_MEDIA_ITEMS_METADATA,
|
||||||
COMMAND_CHANGE_MEDIA_ITEMS,
|
COMMAND_CHANGE_MEDIA_ITEMS,
|
||||||
COMMAND_GET_TRACK_INFOS)
|
COMMAND_GET_TRACKS)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
var currentPlaybackState = Player.STATE_IDLE
|
var currentPlaybackState = Player.STATE_IDLE
|
||||||
@@ -95,7 +95,7 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
|
|||||||
Log.d(tag, "Init CastPlayer")
|
Log.d(tag, "Init CastPlayer")
|
||||||
myCurrentTrackGroups = TrackGroupArray.EMPTY
|
myCurrentTrackGroups = TrackGroupArray.EMPTY
|
||||||
myCurrentTrackSelections = EMPTY_TRACK_SELECTION_ARRAY
|
myCurrentTrackSelections = EMPTY_TRACK_SELECTION_ARRAY
|
||||||
myCurrentTracksInfo = TracksInfo.EMPTY
|
myCurrentTracksInfo = Tracks.EMPTY
|
||||||
statusListener = StatusListener()
|
statusListener = StatusListener()
|
||||||
timelineTracker = CastTimelineTracker()
|
timelineTracker = CastTimelineTracker()
|
||||||
myCurrentTimeline = CastTimeline.EMPTY_CAST_TIMELINE
|
myCurrentTimeline = CastTimeline.EMPTY_CAST_TIMELINE
|
||||||
@@ -385,7 +385,7 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
|
|||||||
}
|
}
|
||||||
if (updateTracksAndSelectionsAndNotifyIfChanged()) {
|
if (updateTracksAndSelectionsAndNotifyIfChanged()) {
|
||||||
listeners.queueEvent(
|
listeners.queueEvent(
|
||||||
EVENT_TRACKS_CHANGED) { listener: Listener -> listener.onTracksInfoChanged(currentTracksInfo) }
|
EVENT_TRACKS_CHANGED) { listener: Listener -> listener.onTracksChanged(currentTracks) }
|
||||||
}
|
}
|
||||||
updateAvailableCommandsAndNotifyIfChanged()
|
updateAvailableCommandsAndNotifyIfChanged()
|
||||||
listeners.flushEvents()
|
listeners.flushEvents()
|
||||||
@@ -405,7 +405,7 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
|
|||||||
val hasChanged = !myCurrentTrackGroups.isEmpty
|
val hasChanged = !myCurrentTrackGroups.isEmpty
|
||||||
myCurrentTrackGroups = TrackGroupArray.EMPTY
|
myCurrentTrackGroups = TrackGroupArray.EMPTY
|
||||||
myCurrentTrackSelections = EMPTY_TRACK_SELECTION_ARRAY
|
myCurrentTrackSelections = EMPTY_TRACK_SELECTION_ARRAY
|
||||||
myCurrentTracksInfo = TracksInfo.EMPTY
|
myCurrentTracksInfo = Tracks.EMPTY
|
||||||
return hasChanged
|
return hasChanged
|
||||||
}
|
}
|
||||||
var activeTrackIds = mediaStatus.activeTrackIds
|
var activeTrackIds = mediaStatus.activeTrackIds
|
||||||
@@ -414,7 +414,7 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
|
|||||||
}
|
}
|
||||||
val trackGroups = arrayOfNulls<TrackGroup>(castMediaTracks.size)
|
val trackGroups = arrayOfNulls<TrackGroup>(castMediaTracks.size)
|
||||||
val trackSelections = arrayOfNulls<TrackSelection>(RENDERER_COUNT)
|
val trackSelections = arrayOfNulls<TrackSelection>(RENDERER_COUNT)
|
||||||
val trackGroupInfos = arrayOfNulls<TrackGroupInfo>(castMediaTracks.size)
|
val trackGroupInfos = arrayOfNulls<Group>(castMediaTracks.size)
|
||||||
for (i in castMediaTracks.indices) {
|
for (i in castMediaTracks.indices) {
|
||||||
val mediaTrack = castMediaTracks[i]
|
val mediaTrack = castMediaTracks[i]
|
||||||
trackGroups[i] = TrackGroup( /* id= */i.toString(), mediaTrackToFormat(mediaTrack))
|
trackGroups[i] = TrackGroup( /* id= */i.toString(), mediaTrackToFormat(mediaTrack))
|
||||||
@@ -428,18 +428,16 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
|
|||||||
}
|
}
|
||||||
val trackSupport = intArrayOf(if (supported) C.FORMAT_HANDLED else C.FORMAT_UNSUPPORTED_TYPE)
|
val trackSupport = intArrayOf(if (supported) C.FORMAT_HANDLED else C.FORMAT_UNSUPPORTED_TYPE)
|
||||||
val trackSelected = booleanArrayOf(selected)
|
val trackSelected = booleanArrayOf(selected)
|
||||||
trackGroupInfos[i] = TrackGroupInfo(trackGroups[i]!!, trackSupport, trackType, trackSelected)
|
trackGroupInfos[i] = Group(trackGroups[i]!!, false, trackSupport, trackSelected)
|
||||||
}
|
}
|
||||||
|
|
||||||
val tg = trackGroups.filterNotNull().toTypedArray()
|
val tg = trackGroups.filterNotNull().toTypedArray()
|
||||||
var ts = trackSelections.filterNotNull().toTypedArray()
|
val ts = trackSelections.filterNotNull().toTypedArray()
|
||||||
var tgi = trackGroupInfos.filterNotNull().toMutableList()
|
val tgi = trackGroupInfos.filterNotNull().toMutableList()
|
||||||
val newTrackGroups = TrackGroupArray(*tg)
|
val newTrackGroups = TrackGroupArray(*tg)
|
||||||
val newTrackSelections = TrackSelectionArray(*ts)
|
val newTrackSelections = TrackSelectionArray(*ts)
|
||||||
val newTracksInfo = TracksInfo(tgi)
|
val newTracksInfo = Tracks(tgi)
|
||||||
if (newTrackGroups != currentTrackGroups
|
if (newTracksInfo != currentTracks) {
|
||||||
|| newTrackSelections != currentTrackSelections
|
|
||||||
|| newTracksInfo != currentTracksInfo) {
|
|
||||||
myCurrentTrackSelections = newTrackSelections
|
myCurrentTrackSelections = newTrackSelections
|
||||||
myCurrentTrackGroups = newTrackGroups
|
myCurrentTrackGroups = newTrackGroups
|
||||||
myCurrentTracksInfo = newTracksInfo
|
myCurrentTracksInfo = newTracksInfo
|
||||||
@@ -593,7 +591,7 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
|
|||||||
listeners.flushEvents()
|
listeners.flushEvents()
|
||||||
val pendingResult: PendingResult<RemoteMediaClient.MediaChannelResult> = if (playWhenReady) remoteMediaClient!!.play() else remoteMediaClient!!.pause()
|
val pendingResult: PendingResult<RemoteMediaClient.MediaChannelResult> = if (playWhenReady) remoteMediaClient!!.play() else remoteMediaClient!!.pause()
|
||||||
|
|
||||||
var resultCb = ResultCallback<RemoteMediaClient.MediaChannelResult?> {
|
val resultCb = ResultCallback<RemoteMediaClient.MediaChannelResult?> {
|
||||||
updatePlayerStateAndNotifyIfChanged()
|
updatePlayerStateAndNotifyIfChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -622,10 +620,10 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun seekTo(mediaItemIndex: Int, positionMs: Long) {
|
override fun seekTo(mediaItemIndex: Int, positionMs: Long, seekCommand: Int, isRepeatingCurrentItem: Boolean) {
|
||||||
Log.d(tag, "seekTo $mediaItemIndex position $positionMs")
|
Log.d(tag, "seekTo $mediaItemIndex position $positionMs")
|
||||||
|
|
||||||
var resultCb = ResultCallback<RemoteMediaClient.MediaChannelResult?> {
|
val resultCb = ResultCallback<RemoteMediaClient.MediaChannelResult?> {
|
||||||
val statusCode: Int = it.status.statusCode
|
val statusCode: Int = it.status.statusCode
|
||||||
if (statusCode != CastStatusCodes.SUCCESS && statusCode != CastStatusCodes.REPLACED) {
|
if (statusCode != CastStatusCodes.SUCCESS && statusCode != CastStatusCodes.REPLACED) {
|
||||||
Log.e(tag, "Seek failed. Error code $statusCode")
|
Log.e(tag, "Seek failed. Error code $statusCode")
|
||||||
@@ -645,14 +643,14 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
|
|||||||
|
|
||||||
// We assume the default position is 0. There is no support for seeking to the default position
|
// We assume the default position is 0. There is no support for seeking to the default position
|
||||||
// in RemoteMediaClient.
|
// in RemoteMediaClient.
|
||||||
var positionMsFinal = if (positionMs != C.TIME_UNSET) positionMs else 0
|
val positionMsFinal = if (positionMs != C.TIME_UNSET) positionMs else 0
|
||||||
if (mediaStatus != null) {
|
if (mediaStatus != null) {
|
||||||
if (currentMediaItemIndex != mediaItemIndex) {
|
if (currentMediaItemIndex != mediaItemIndex) {
|
||||||
Log.d(tag, "seekTo: Changing media item index from $currentMediaItemIndex to $mediaItemIndex")
|
Log.d(tag, "seekTo: Changing media item index from $currentMediaItemIndex to $mediaItemIndex")
|
||||||
remoteMediaClient?.queueJumpToItem(myCurrentTimeline.getPeriod(mediaItemIndex, period).uid as Int, positionMsFinal, JSONObject())?.setResultCallback(resultCb)
|
remoteMediaClient?.queueJumpToItem(myCurrentTimeline.getPeriod(mediaItemIndex, period).uid as Int, positionMsFinal, JSONObject())?.setResultCallback(resultCb)
|
||||||
} else {
|
} else {
|
||||||
Log.d(tag, "seekTo: Same media index seek to position $positionMsFinal")
|
Log.d(tag, "seekTo: Same media index seek to position $positionMsFinal")
|
||||||
var mediaSeekOptions = MediaSeekOptions.Builder().setPosition(positionMsFinal).build()
|
val mediaSeekOptions = MediaSeekOptions.Builder().setPosition(positionMsFinal).build()
|
||||||
remoteMediaClient?.seek(mediaSeekOptions)?.setResultCallback(resultCb)
|
remoteMediaClient?.seek(mediaSeekOptions)?.setResultCallback(resultCb)
|
||||||
}
|
}
|
||||||
val oldPosition = getCurrentPositionInfo()
|
val oldPosition = getCurrentPositionInfo()
|
||||||
@@ -709,7 +707,7 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
|
|||||||
|
|
||||||
val pendingResult: PendingResult<RemoteMediaClient.MediaChannelResult>? = remoteMediaClient?.setPlaybackRate(actualPlaybackParameters.speed.toDouble(), /* customData= */null)
|
val pendingResult: PendingResult<RemoteMediaClient.MediaChannelResult>? = remoteMediaClient?.setPlaybackRate(actualPlaybackParameters.speed.toDouble(), /* customData= */null)
|
||||||
|
|
||||||
var resultCb = ResultCallback<RemoteMediaClient.MediaChannelResult?> {
|
val resultCb = ResultCallback<RemoteMediaClient.MediaChannelResult?> {
|
||||||
updatePlaybackRateAndNotifyIfChanged()
|
updatePlaybackRateAndNotifyIfChanged()
|
||||||
listeners.flushEvents()
|
listeners.flushEvents()
|
||||||
}
|
}
|
||||||
@@ -736,15 +734,7 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
|
|||||||
sessionManager.endCurrentSession(false)
|
sessionManager.endCurrentSession(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCurrentTrackGroups(): TrackGroupArray {
|
override fun getCurrentTracks(): Tracks {
|
||||||
return this.myCurrentTrackGroups
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCurrentTrackSelections(): TrackSelectionArray {
|
|
||||||
return this.myCurrentTrackSelections
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCurrentTracksInfo(): TracksInfo {
|
|
||||||
return this.myCurrentTracksInfo
|
return this.myCurrentTracksInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -858,8 +848,12 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
|
|||||||
return VideoSize.UNKNOWN
|
return VideoSize.UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCurrentCues(): MutableList<Cue> {
|
override fun getSurfaceSize(): Size {
|
||||||
return mutableListOf()
|
return Size.UNKNOWN
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getCurrentCues(): CueGroup {
|
||||||
|
return CueGroup.EMPTY_TIME_ZERO
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getDeviceInfo(): DeviceInfo {
|
override fun getDeviceInfo(): DeviceInfo {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.util.Log
|
|||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import com.audiobookshelf.app.data.LibraryItemWrapper
|
import com.audiobookshelf.app.data.LibraryItemWrapper
|
||||||
import com.audiobookshelf.app.data.PodcastEpisode
|
import com.audiobookshelf.app.data.PodcastEpisode
|
||||||
|
import com.audiobookshelf.app.device.DeviceManager
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import kotlin.concurrent.schedule
|
import kotlin.concurrent.schedule
|
||||||
|
|
||||||
@@ -88,6 +89,25 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
playerNotificationService.seekPlayer(pos)
|
playerNotificationService.seekPlayer(pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun onChangeSpeed() {
|
||||||
|
// cycle to next speed, only contains preset android app options, as each increment needs it's own icon
|
||||||
|
// Rounding values in the event a non preset value (.5, 1, 1.2, 1.5, 2, 3) is selected in the phone app
|
||||||
|
val mediaManager = playerNotificationService.mediaManager
|
||||||
|
val newSpeed = when (mediaManager.getSavedPlaybackRate()) {
|
||||||
|
in 0.5f..0.7f -> 1.0f
|
||||||
|
in 0.8f..1.0f -> 1.2f
|
||||||
|
in 1.1f..1.2f -> 1.5f
|
||||||
|
in 1.3f..1.5f -> 2.0f
|
||||||
|
in 1.6f..2.0f -> 3.0f
|
||||||
|
in 2.1f..3.0f -> 0.5f
|
||||||
|
// anything set above 3 (can happen in the android app) will be reset to 1
|
||||||
|
else -> 1.0f
|
||||||
|
}
|
||||||
|
mediaManager.setSavedPlaybackRate(newSpeed)
|
||||||
|
playerNotificationService.setPlaybackSpeed(newSpeed)
|
||||||
|
playerNotificationService.clientEventEmitter?.onPlaybackSpeedChanged(newSpeed)
|
||||||
|
}
|
||||||
|
|
||||||
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")
|
||||||
val libraryItemWrapper: LibraryItemWrapper?
|
val libraryItemWrapper: LibraryItemWrapper?
|
||||||
@@ -138,17 +158,46 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
|
|
||||||
Log.d(tag, "handleCallMediaButton keyEvent = $keyEvent | action ${keyEvent?.action}")
|
Log.d(tag, "handleCallMediaButton keyEvent = $keyEvent | action ${keyEvent?.action}")
|
||||||
|
|
||||||
|
// Widget button intent is only sending the action down event
|
||||||
if (keyEvent?.action == KeyEvent.ACTION_DOWN) {
|
if (keyEvent?.action == KeyEvent.ACTION_DOWN) {
|
||||||
Log.d(tag, "handleCallMediaButton: key action_down for ${keyEvent.keyCode}")
|
Log.d(tag, "handleCallMediaButton: key action_down for ${keyEvent.keyCode}")
|
||||||
when (keyEvent.keyCode) {
|
when (keyEvent.keyCode) {
|
||||||
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
|
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
|
||||||
Log.d(tag, "handleCallMediaButton: Media Play/Pause")
|
Log.d(tag, "handleCallMediaButton: Media Play/Pause")
|
||||||
|
|
||||||
|
// TODO: Play/pause event sent from widget when app is closed. Currently the service gets destroyed before anything can happen
|
||||||
|
// if (playerNotificationService.currentPlaybackSession == null && DeviceManager.deviceData.lastPlaybackSession != null) {
|
||||||
|
// Log.i(tag, "No playback session but had one in the db")
|
||||||
|
//
|
||||||
|
// val connectionConfig = DeviceManager.deviceData.serverConnectionConfigs.find { it.id == DeviceManager.deviceData.lastPlaybackSession?.serverConnectionConfigId }
|
||||||
|
// connectionConfig?.let {
|
||||||
|
// Log.i(tag, "Setting playback session from db $it")
|
||||||
|
// DeviceManager.serverConnectionConfig = it
|
||||||
|
//
|
||||||
|
// playerNotificationService.currentPlaybackSession = DeviceManager.deviceData.lastPlaybackSession
|
||||||
|
// playerNotificationService.startNewPlaybackSession()
|
||||||
|
// return true
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
if (playerNotificationService.mPlayer.isPlaying) {
|
if (playerNotificationService.mPlayer.isPlaying) {
|
||||||
playerNotificationService.pause()
|
if (0 == mediaButtonClickCount) playerNotificationService.pause()
|
||||||
|
handleMediaButtonClickCount()
|
||||||
} else {
|
} else {
|
||||||
playerNotificationService.play()
|
if (0 == mediaButtonClickCount) {
|
||||||
|
playerNotificationService.play()
|
||||||
|
}
|
||||||
|
handleMediaButtonClickCount()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
|
||||||
|
Log.d(tag, "handleCallMediaButton: Media Fast Forward")
|
||||||
|
playerNotificationService.jumpForward()
|
||||||
|
}
|
||||||
|
KeyEvent.KEYCODE_MEDIA_REWIND -> {
|
||||||
|
Log.d(tag, "handleCallMediaButton: Media Rewind")
|
||||||
|
playerNotificationService.jumpBackward()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,27 +232,9 @@ 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()
|
||||||
}
|
}
|
||||||
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
|
|
||||||
Log.d(tag, "handleCallMediaButton: Media Play/Pause")
|
|
||||||
if (playerNotificationService.mPlayer.isPlaying) {
|
|
||||||
if (0 == mediaButtonClickCount) playerNotificationService.pause()
|
|
||||||
handleMediaButtonClickCount()
|
|
||||||
} else {
|
|
||||||
if (0 == mediaButtonClickCount) {
|
|
||||||
playerNotificationService.play()
|
|
||||||
}
|
|
||||||
handleMediaButtonClickCount()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
else -> {
|
||||||
Log.d(tag, "KeyCode:${keyEvent.keyCode}")
|
Log.d(tag, "KeyCode:${keyEvent.keyCode}")
|
||||||
return false
|
return false
|
||||||
@@ -248,6 +279,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
|
|||||||
CUSTOM_ACTION_JUMP_BACKWARD -> onRewind()
|
CUSTOM_ACTION_JUMP_BACKWARD -> onRewind()
|
||||||
CUSTOM_ACTION_SKIP_FORWARD -> onSkipToNext()
|
CUSTOM_ACTION_SKIP_FORWARD -> onSkipToNext()
|
||||||
CUSTOM_ACTION_SKIP_BACKWARD -> onSkipToPrevious()
|
CUSTOM_ACTION_SKIP_BACKWARD -> onSkipToPrevious()
|
||||||
|
CUSTOM_ACTION_CHANGE_SPEED -> onChangeSpeed()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const val CUSTOM_ACTION_JUMP_FORWARD = "com.audiobookshelf.customAction.jump_for
|
|||||||
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_FORWARD = "com.audiobookshelf.customAction.skip_forward";
|
||||||
const val CUSTOM_ACTION_SKIP_BACKWARD = "com.audiobookshelf.customAction.skip_backward";
|
const val CUSTOM_ACTION_SKIP_BACKWARD = "com.audiobookshelf.customAction.skip_backward";
|
||||||
|
const val CUSTOM_ACTION_CHANGE_SPEED = "com.audiobookshelf.customAction.change_speed";
|
||||||
|
|
||||||
const val PLAYMETHOD_DIRECTPLAY = 0
|
const val PLAYMETHOD_DIRECTPLAY = 0
|
||||||
const val PLAYMETHOD_DIRECTSTREAM = 1
|
const val PLAYMETHOD_DIRECTSTREAM = 1
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import com.audiobookshelf.app.device.DeviceManager
|
|||||||
import com.google.android.exoplayer2.PlaybackException
|
import com.google.android.exoplayer2.PlaybackException
|
||||||
import com.google.android.exoplayer2.Player
|
import com.google.android.exoplayer2.Player
|
||||||
|
|
||||||
const val PAUSE_LEN_BEFORE_RECHECK = 30000 // 30 seconds
|
//const val PAUSE_LEN_BEFORE_RECHECK = 30000 // 30 seconds
|
||||||
|
|
||||||
class PlayerListener(var playerNotificationService:PlayerNotificationService) : Player.Listener {
|
class PlayerListener(var playerNotificationService:PlayerNotificationService) : Player.Listener {
|
||||||
var tag = "PlayerListener"
|
var tag = "PlayerListener"
|
||||||
@@ -56,6 +56,9 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||||||
|
|
||||||
lazyIsPlaying = isPlaying
|
lazyIsPlaying = isPlaying
|
||||||
|
|
||||||
|
// Update widget
|
||||||
|
DeviceManager.widgetUpdater?.onPlayerChanged(playerNotificationService)
|
||||||
|
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
Log.d(tag, "SeekBackTime: Player is playing")
|
Log.d(tag, "SeekBackTime: Player is playing")
|
||||||
if (lastPauseTime > 0 && DeviceManager.deviceData.deviceSettings?.disableAutoRewind != true) {
|
if (lastPauseTime > 0 && DeviceManager.deviceData.deviceSettings?.disableAutoRewind != true) {
|
||||||
@@ -74,12 +77,13 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||||||
Log.d(tag, "SeekBackTime $seekBackTime")
|
Log.d(tag, "SeekBackTime $seekBackTime")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: this needs to be reworked so that the audio doesn't start playing before it checks for updated progress
|
||||||
// Check if playback session still exists or sync media progress if updated
|
// Check if playback session still exists or sync media progress if updated
|
||||||
val pauseLength: Long = System.currentTimeMillis() - lastPauseTime
|
// val pauseLength: Long = System.currentTimeMillis() - lastPauseTime
|
||||||
if (pauseLength > PAUSE_LEN_BEFORE_RECHECK) {
|
// if (pauseLength > PAUSE_LEN_BEFORE_RECHECK) {
|
||||||
val shouldCarryOn = playerNotificationService.checkCurrentSessionProgress(seekBackTime)
|
// val shouldCarryOn = playerNotificationService.checkCurrentSessionProgress(seekBackTime)
|
||||||
if (!shouldCarryOn) return
|
// if (!shouldCarryOn) return
|
||||||
}
|
// }
|
||||||
|
|
||||||
if (seekBackTime > 0L) {
|
if (seekBackTime > 0L) {
|
||||||
playerNotificationService.seekBackward(seekBackTime)
|
playerNotificationService.seekBackward(seekBackTime)
|
||||||
@@ -92,12 +96,15 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||||||
|
|
||||||
// Start/stop progress sync interval
|
// Start/stop progress sync interval
|
||||||
if (isPlaying) {
|
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
|
val playbackSession: PlaybackSession? = playerNotificationService.mediaProgressSyncer.currentPlaybackSession ?: playerNotificationService.currentPlaybackSession
|
||||||
playbackSession?.let { playerNotificationService.mediaProgressSyncer.play(it) }
|
playbackSession?.let {
|
||||||
|
// Handles auto-starting sleep timer and resetting sleep timer
|
||||||
|
playerNotificationService.sleepTimerManager.handleMediaPlayEvent(it.id)
|
||||||
|
|
||||||
|
player.volume = 1F // Volume on sleep timer might have decreased this
|
||||||
|
|
||||||
|
playerNotificationService.mediaProgressSyncer.play(it)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
playerNotificationService.mediaProgressSyncer.pause {
|
playerNotificationService.mediaProgressSyncer.pause {
|
||||||
Log.d(tag, "Media Progress Syncer paused and synced")
|
Log.d(tag, "Media Progress Syncer paused and synced")
|
||||||
@@ -105,8 +112,6 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
playerNotificationService.clientEventEmitter?.onPlayingUpdate(isPlaying)
|
playerNotificationService.clientEventEmitter?.onPlayingUpdate(isPlaying)
|
||||||
|
|
||||||
DeviceManager.widgetUpdater?.onPlayerChanged(playerNotificationService)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onEvents(player: Player, events: Player.Events) {
|
override fun onEvents(player: Player, events: Player.Events) {
|
||||||
|
|||||||
@@ -1,21 +1,37 @@
|
|||||||
package com.audiobookshelf.app.player
|
package com.audiobookshelf.app.player
|
||||||
|
|
||||||
import android.app.Notification
|
import android.app.Notification
|
||||||
|
import android.content.pm.ServiceInfo
|
||||||
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
||||||
|
|
||||||
class PlayerNotificationListener(var playerNotificationService:PlayerNotificationService) : PlayerNotificationManager.NotificationListener {
|
class PlayerNotificationListener(var playerNotificationService:PlayerNotificationService) : PlayerNotificationManager.NotificationListener {
|
||||||
var tag = "PlayerNotificationListener"
|
var tag = "PlayerNotificationListener"
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var isForegroundService = false
|
||||||
|
}
|
||||||
|
|
||||||
override fun onNotificationPosted(
|
override fun onNotificationPosted(
|
||||||
notificationId: Int,
|
notificationId: Int,
|
||||||
notification: Notification,
|
notification: Notification,
|
||||||
onGoing: Boolean) {
|
onGoing: Boolean) {
|
||||||
|
|
||||||
// Start foreground service
|
if (onGoing && !isForegroundService) {
|
||||||
Log.d(tag, "Notification Posted $notificationId - Start Foreground | $notification")
|
// Start foreground service
|
||||||
PlayerNotificationService.isClosed = false
|
Log.d(tag, "Notification Posted $notificationId - Start Foreground | $notification")
|
||||||
playerNotificationService.startForeground(notificationId, notification)
|
PlayerNotificationService.isClosed = false
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
playerNotificationService.startForeground(notificationId, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)
|
||||||
|
} else {
|
||||||
|
playerNotificationService.startForeground(notificationId, notification)
|
||||||
|
}
|
||||||
|
isForegroundService = true
|
||||||
|
} else {
|
||||||
|
Log.d(tag, "Notification posted $notificationId, not starting foreground - onGoing=$onGoing | isForegroundService=$isForegroundService")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNotificationCancelled(
|
override fun onNotificationCancelled(
|
||||||
@@ -28,16 +44,13 @@ class PlayerNotificationListener(var playerNotificationService:PlayerNotificatio
|
|||||||
} else {
|
} else {
|
||||||
Log.d(tag, "onNotificationCancelled not dismissed by user")
|
Log.d(tag, "onNotificationCancelled not dismissed by user")
|
||||||
|
|
||||||
// When stop button is pressed on the notification I guess it isn't considered "dismissedByUser" so we need to close playback ourselves
|
if (PlayerNotificationService.isSwitchingPlayer) {
|
||||||
if (!PlayerNotificationService.isClosed && !PlayerNotificationService.isSwitchingPlayer) {
|
|
||||||
Log.d(tag, "PNS is not closed - closing it now")
|
|
||||||
playerNotificationService.closePlayback()
|
|
||||||
} else if (PlayerNotificationService.isSwitchingPlayer) {
|
|
||||||
// When switching from cast player to exo player and vice versa the notification is cancelled and posted again
|
// When switching from cast player to exo player and vice versa the notification is cancelled and posted again
|
||||||
// so we don't want to cancel the playback during this switch
|
// so we don't want to cancel the playback during this switch
|
||||||
Log.d(tag, "PNS is switching player")
|
Log.d(tag, "PNS is switching player")
|
||||||
PlayerNotificationService.isSwitchingPlayer = false
|
PlayerNotificationService.isSwitchingPlayer = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
isForegroundService = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.audiobookshelf.app.player
|
package com.audiobookshelf.app.player
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.app.*
|
import android.app.*
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
@@ -8,12 +9,10 @@ import android.graphics.Color
|
|||||||
import android.graphics.ImageDecoder
|
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.*
|
||||||
import android.net.Network
|
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import android.net.NetworkRequest
|
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
|
import android.provider.Settings
|
||||||
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.MediaMetadataCompat
|
||||||
@@ -34,12 +33,15 @@ import com.audiobookshelf.app.device.DeviceManager
|
|||||||
import com.audiobookshelf.app.managers.DbManager
|
import com.audiobookshelf.app.managers.DbManager
|
||||||
import com.audiobookshelf.app.managers.SleepTimerManager
|
import com.audiobookshelf.app.managers.SleepTimerManager
|
||||||
import com.audiobookshelf.app.media.MediaManager
|
import com.audiobookshelf.app.media.MediaManager
|
||||||
|
import com.audiobookshelf.app.media.MediaProgressSyncer
|
||||||
import com.audiobookshelf.app.server.ApiHandler
|
import com.audiobookshelf.app.server.ApiHandler
|
||||||
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
|
||||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector.CustomActionProvider
|
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector.CustomActionProvider
|
||||||
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
|
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
|
||||||
|
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory
|
||||||
|
import com.google.android.exoplayer2.extractor.mp3.Mp3Extractor
|
||||||
import com.google.android.exoplayer2.source.MediaSource
|
import com.google.android.exoplayer2.source.MediaSource
|
||||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
||||||
@@ -59,6 +61,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
var isStarted = false
|
var isStarted = false
|
||||||
var isClosed = false
|
var isClosed = false
|
||||||
var isUnmeteredNetwork = false
|
var isUnmeteredNetwork = false
|
||||||
|
var hasNetworkConnectivity = false // Not 100% reliable has internet
|
||||||
var isSwitchingPlayer = false // Used when switching between cast player and exoplayer
|
var isSwitchingPlayer = false // Used when switching between cast player and exoplayer
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,7 +73,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
fun onPlayingUpdate(isPlaying: Boolean)
|
fun onPlayingUpdate(isPlaying: Boolean)
|
||||||
fun onMetadata(metadata: PlaybackMetadata)
|
fun onMetadata(metadata: PlaybackMetadata)
|
||||||
fun onSleepTimerEnded(currentPosition: Long)
|
fun onSleepTimerEnded(currentPosition: Long)
|
||||||
fun onSleepTimerSet(sleepTimeRemaining: Int)
|
fun onSleepTimerSet(sleepTimeRemaining: Int, isAutoSleepTimer:Boolean)
|
||||||
fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress)
|
fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress)
|
||||||
fun onPlaybackFailed(errorMessage:String)
|
fun onPlaybackFailed(errorMessage:String)
|
||||||
fun onMediaPlayerChanged(mediaPlayer:String)
|
fun onMediaPlayerChanged(mediaPlayer:String)
|
||||||
@@ -78,6 +81,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
fun onProgressSyncSuccess()
|
fun onProgressSyncSuccess()
|
||||||
fun onNetworkMeteredChanged(isUnmetered:Boolean)
|
fun onNetworkMeteredChanged(isUnmetered:Boolean)
|
||||||
fun onMediaItemHistoryUpdated(mediaItemHistory:MediaItemHistory)
|
fun onMediaItemHistoryUpdated(mediaItemHistory:MediaItemHistory)
|
||||||
|
fun onPlaybackSpeedChanged(playbackSpeed:Float)
|
||||||
}
|
}
|
||||||
private val binder = LocalBinder()
|
private val binder = LocalBinder()
|
||||||
|
|
||||||
@@ -97,7 +101,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
var castPlayer:CastPlayer? = null
|
var castPlayer:CastPlayer? = null
|
||||||
|
|
||||||
lateinit var sleepTimerManager:SleepTimerManager
|
lateinit var sleepTimerManager:SleepTimerManager
|
||||||
lateinit var mediaProgressSyncer:MediaProgressSyncer
|
lateinit var mediaProgressSyncer: MediaProgressSyncer
|
||||||
|
|
||||||
private var notificationId = 10
|
private var notificationId = 10
|
||||||
private var channelId = "audiobookshelf_channel"
|
private var channelId = "audiobookshelf_channel"
|
||||||
@@ -167,13 +171,16 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Log.d(tag, "onDestroy")
|
Log.d(tag, "onDestroy")
|
||||||
|
isStarted = false
|
||||||
|
isClosed = true
|
||||||
|
DeviceManager.widgetUpdater?.onPlayerChanged(this)
|
||||||
|
|
||||||
playerNotificationManager.setPlayer(null)
|
playerNotificationManager.setPlayer(null)
|
||||||
mPlayer.release()
|
mPlayer.release()
|
||||||
castPlayer?.release()
|
castPlayer?.release()
|
||||||
mediaSession.release()
|
mediaSession.release()
|
||||||
mediaProgressSyncer.reset()
|
mediaProgressSyncer.reset()
|
||||||
Log.d(tag, "onDestroy")
|
|
||||||
isStarted = false
|
|
||||||
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
@@ -182,6 +189,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||||
super.onTaskRemoved(rootIntent)
|
super.onTaskRemoved(rootIntent)
|
||||||
Log.d(tag, "onTaskRemoved")
|
Log.d(tag, "onTaskRemoved")
|
||||||
|
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,11 +198,17 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
super.onCreate()
|
super.onCreate()
|
||||||
ctx = this
|
ctx = this
|
||||||
|
|
||||||
|
// Initialize Paper
|
||||||
|
DbManager.initialize(ctx)
|
||||||
|
|
||||||
|
// Initialize widget
|
||||||
|
DeviceManager.initializeWidgetUpdater(ctx)
|
||||||
|
|
||||||
// To listen for network change from metered to unmetered
|
// To listen for network change from metered to unmetered
|
||||||
val networkRequest = NetworkRequest.Builder()
|
val networkRequest = NetworkRequest.Builder()
|
||||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
||||||
.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||||
.build()
|
.build()
|
||||||
val connectivityManager = getSystemService(ConnectivityManager::class.java) as ConnectivityManager
|
val connectivityManager = getSystemService(ConnectivityManager::class.java) as ConnectivityManager
|
||||||
connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
|
connectivityManager.registerNetworkCallback(networkRequest, networkCallback)
|
||||||
@@ -251,12 +265,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
playerNotificationManager.setUseNextAction(false)
|
playerNotificationManager.setUseNextAction(false)
|
||||||
playerNotificationManager.setUsePreviousAction(false)
|
playerNotificationManager.setUsePreviousAction(false)
|
||||||
playerNotificationManager.setUseChronometer(false)
|
playerNotificationManager.setUseChronometer(false)
|
||||||
playerNotificationManager.setUseStopAction(true)
|
playerNotificationManager.setUseStopAction(false)
|
||||||
playerNotificationManager.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
playerNotificationManager.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
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)
|
playerNotificationManager.setSmallIcon(R.drawable.icon_monochrome)
|
||||||
|
|
||||||
// Unknown action
|
// Unknown action
|
||||||
playerNotificationManager.setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE)
|
playerNotificationManager.setBadgeIconType(NotificationCompat.BADGE_ICON_LARGE)
|
||||||
@@ -275,10 +289,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
return MediaDescriptionCompat.Builder().build()
|
return MediaDescriptionCompat.Builder().build()
|
||||||
}
|
}
|
||||||
|
|
||||||
val coverUri = currentPlaybackSession!!.getCoverUri()
|
val coverUri = currentPlaybackSession!!.getCoverUri(ctx)
|
||||||
|
|
||||||
var bitmap:Bitmap? = null
|
var bitmap: Bitmap? = null
|
||||||
// Local covers get bitmap
|
// Local covers get bitmap
|
||||||
if (currentPlaybackSession!!.localLibraryItem?.coverContentUrl != null) {
|
if (currentPlaybackSession!!.localLibraryItem?.coverContentUrl != null) {
|
||||||
bitmap = if (Build.VERSION.SDK_INT < 28) {
|
bitmap = if (Build.VERSION.SDK_INT < 28) {
|
||||||
MediaStore.Images.Media.getBitmap(ctx.contentResolver, coverUri)
|
MediaStore.Images.Media.getBitmap(ctx.contentResolver, coverUri)
|
||||||
@@ -349,7 +363,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
.build()
|
.build()
|
||||||
mPlayer.setHandleAudioBecomingNoisy(true)
|
mPlayer.setHandleAudioBecomingNoisy(true)
|
||||||
mPlayer.addListener(PlayerListener(this))
|
mPlayer.addListener(PlayerListener(this))
|
||||||
val audioAttributes:AudioAttributes = AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).setContentType(C.CONTENT_TYPE_SPEECH).build()
|
val audioAttributes:AudioAttributes = AudioAttributes.Builder().setUsage(C.USAGE_MEDIA).setContentType(C.AUDIO_CONTENT_TYPE_SPEECH).build()
|
||||||
mPlayer.setAudioAttributes(audioAttributes, true)
|
mPlayer.setAudioAttributes(audioAttributes, true)
|
||||||
|
|
||||||
//attach player to playerNotificationManager
|
//attach player to playerNotificationManager
|
||||||
@@ -378,23 +392,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isClosed = false
|
isClosed = false
|
||||||
val customActionProviders = mutableListOf(
|
|
||||||
JumpBackwardCustomActionProvider(),
|
|
||||||
JumpForwardCustomActionProvider(),
|
|
||||||
)
|
|
||||||
val metadata = playbackSession.getMediaMetadataCompat(ctx)
|
val metadata = playbackSession.getMediaMetadataCompat(ctx)
|
||||||
mediaSession.setMetadata(metadata)
|
mediaSession.setMetadata(metadata)
|
||||||
val mediaItems = playbackSession.getMediaItems()
|
val mediaItems = playbackSession.getMediaItems(ctx)
|
||||||
val playbackRateToUse = playbackRate ?: initialPlaybackRate ?: 1f
|
val playbackRateToUse = playbackRate ?: initialPlaybackRate ?: 1f
|
||||||
initialPlaybackRate = playbackRate
|
initialPlaybackRate = playbackRate
|
||||||
|
|
||||||
if (playbackSession.mediaPlayer != PLAYER_CAST && mediaItems.size > 1) {
|
// Set actions on Android Auto like jump forward/backward
|
||||||
customActionProviders.addAll(listOf(
|
setMediaSessionConnectorCustomActions(playbackSession)
|
||||||
SkipBackwardCustomActionProvider(),
|
|
||||||
SkipForwardCustomActionProvider(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
mediaSessionConnector.setCustomActionProviders(*customActionProviders.toTypedArray())
|
|
||||||
|
|
||||||
playbackSession.mediaPlayer = getMediaPlayer()
|
playbackSession.mediaPlayer = getMediaPlayer()
|
||||||
|
|
||||||
@@ -412,10 +418,15 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentPlaybackSession = playbackSession
|
currentPlaybackSession = playbackSession
|
||||||
Log.d(tag, "Set CurrentPlaybackSession MediaPlayer ${currentPlaybackSession?.mediaPlayer}")
|
DeviceManager.setLastPlaybackSession(playbackSession) // Save playback session to use when app is closed
|
||||||
|
|
||||||
|
Log.d(tag, "Set CurrentPlaybackSession MediaPlayer ${currentPlaybackSession?.mediaPlayer}")
|
||||||
|
// Notify client
|
||||||
clientEventEmitter?.onPlaybackSession(playbackSession)
|
clientEventEmitter?.onPlaybackSession(playbackSession)
|
||||||
|
|
||||||
|
// Update widget
|
||||||
|
DeviceManager.widgetUpdater?.onPlayerChanged(this)
|
||||||
|
|
||||||
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
|
||||||
@@ -428,12 +439,26 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
if (playbackSession.isLocal) {
|
if (playbackSession.isLocal) {
|
||||||
Log.d(tag, "Playing Local Item")
|
Log.d(tag, "Playing Local Item")
|
||||||
val dataSourceFactory = DefaultDataSource.Factory(ctx)
|
val dataSourceFactory = DefaultDataSource.Factory(ctx)
|
||||||
mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItems[0])
|
|
||||||
|
val extractorsFactory = DefaultExtractorsFactory()
|
||||||
|
if (DeviceManager.deviceData.deviceSettings?.enableMp3IndexSeeking == true) {
|
||||||
|
// @see https://exoplayer.dev/troubleshooting.html#why-is-seeking-inaccurate-in-some-mp3-files
|
||||||
|
extractorsFactory.setMp3ExtractorFlags(Mp3Extractor.FLAG_ENABLE_INDEX_SEEKING)
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory).createMediaSource(mediaItems[0])
|
||||||
} else if (!playbackSession.isHLS) {
|
} else if (!playbackSession.isHLS) {
|
||||||
Log.d(tag, "Direct Playing Item")
|
Log.d(tag, "Direct Playing Item")
|
||||||
val dataSourceFactory = DefaultHttpDataSource.Factory()
|
val dataSourceFactory = DefaultHttpDataSource.Factory()
|
||||||
|
|
||||||
|
val extractorsFactory = DefaultExtractorsFactory()
|
||||||
|
if (DeviceManager.deviceData.deviceSettings?.enableMp3IndexSeeking == true) {
|
||||||
|
// @see https://exoplayer.dev/troubleshooting.html#why-is-seeking-inaccurate-in-some-mp3-files
|
||||||
|
extractorsFactory.setMp3ExtractorFlags(Mp3Extractor.FLAG_ENABLE_INDEX_SEEKING)
|
||||||
|
}
|
||||||
|
|
||||||
dataSourceFactory.setUserAgent(channelId)
|
dataSourceFactory.setUserAgent(channelId)
|
||||||
mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(mediaItems[0])
|
mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory).createMediaSource(mediaItems[0])
|
||||||
} else {
|
} else {
|
||||||
Log.d(tag, "Playing HLS Item")
|
Log.d(tag, "Playing HLS Item")
|
||||||
val dataSourceFactory = DefaultHttpDataSource.Factory()
|
val dataSourceFactory = DefaultHttpDataSource.Factory()
|
||||||
@@ -472,6 +497,22 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setMediaSessionConnectorCustomActions(playbackSession:PlaybackSession) {
|
||||||
|
val mediaItems = playbackSession.getMediaItems(ctx)
|
||||||
|
val customActionProviders = mutableListOf(
|
||||||
|
JumpBackwardCustomActionProvider(),
|
||||||
|
JumpForwardCustomActionProvider(),
|
||||||
|
ChangePlaybackSpeedCustomActionProvider() // Will be pushed to far left
|
||||||
|
)
|
||||||
|
if (playbackSession.mediaPlayer != PLAYER_CAST && mediaItems.size > 1) {
|
||||||
|
customActionProviders.addAll(listOf(
|
||||||
|
SkipBackwardCustomActionProvider(),
|
||||||
|
SkipForwardCustomActionProvider(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
mediaSessionConnector.setCustomActionProviders(*customActionProviders.toTypedArray())
|
||||||
|
}
|
||||||
|
|
||||||
fun handlePlayerPlaybackError(errorMessage:String) {
|
fun handlePlayerPlaybackError(errorMessage:String) {
|
||||||
// On error and was attempting to direct play - fallback to transcode
|
// On error and was attempting to direct play - fallback to transcode
|
||||||
currentPlaybackSession?.let { playbackSession ->
|
currentPlaybackSession?.let { playbackSession ->
|
||||||
@@ -481,16 +522,19 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
val libraryItemId = playbackSession.libraryItemId ?: "" // Must be true since direct play
|
val libraryItemId = playbackSession.libraryItemId ?: "" // Must be true since direct play
|
||||||
val episodeId = playbackSession.episodeId
|
val episodeId = playbackSession.episodeId
|
||||||
apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) {
|
mediaProgressSyncer.stop(false) {
|
||||||
if (it == null) { // Play request failed
|
apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) {
|
||||||
clientEventEmitter?.onPlaybackFailed(errorMessage)
|
if (it == null) { // Play request failed
|
||||||
closePlayback(true)
|
clientEventEmitter?.onPlaybackFailed(errorMessage)
|
||||||
} else {
|
closePlayback(true)
|
||||||
Handler(Looper.getMainLooper()).post {
|
} else {
|
||||||
preparePlayer(it, true, null)
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
preparePlayer(it, true, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
clientEventEmitter?.onPlaybackFailed(errorMessage)
|
clientEventEmitter?.onPlaybackFailed(errorMessage)
|
||||||
closePlayback(true)
|
closePlayback(true)
|
||||||
@@ -530,17 +574,22 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
fun startNewPlaybackSession() {
|
fun startNewPlaybackSession() {
|
||||||
currentPlaybackSession?.let { playbackSession ->
|
currentPlaybackSession?.let { playbackSession ->
|
||||||
|
Log.i(tag, "Starting new playback session for ${playbackSession.displayTitle}")
|
||||||
|
|
||||||
val forceTranscode = playbackSession.isHLS // If already HLS then force
|
val forceTranscode = playbackSession.isHLS // If already HLS then force
|
||||||
val playItemRequestPayload = getPlayItemRequestPayload(forceTranscode)
|
val playItemRequestPayload = getPlayItemRequestPayload(forceTranscode)
|
||||||
|
|
||||||
val libraryItemId = playbackSession.libraryItemId ?: "" // Must be true since direct play
|
val libraryItemId = playbackSession.libraryItemId ?: "" // Must be true since direct play
|
||||||
val episodeId = playbackSession.episodeId
|
val episodeId = playbackSession.episodeId
|
||||||
apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) {
|
mediaProgressSyncer.stop(false) {
|
||||||
if (it == null) {
|
apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) {
|
||||||
Log.e(tag, "Failed to start new playback session")
|
if (it == null) {
|
||||||
} else {
|
Log.e(tag, "Failed to start new playback session")
|
||||||
Handler(Looper.getMainLooper()).post {
|
} else {
|
||||||
preparePlayer(it, true, null)
|
Log.d(tag, "New playback session response from server with session id ${it.id} for \"${it.displayTitle}\"")
|
||||||
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
preparePlayer(it, true, null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -662,7 +711,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
if (currentPlaybackSession == null) return true
|
if (currentPlaybackSession == null) return true
|
||||||
|
|
||||||
mediaProgressSyncer.currentPlaybackSession?.let { playbackSession ->
|
mediaProgressSyncer.currentPlaybackSession?.let { playbackSession ->
|
||||||
if (!apiHandler.isOnline() || playbackSession.isLocalLibraryItemOnly) {
|
if (!DeviceManager.checkConnectivity(ctx) || playbackSession.isLocalLibraryItemOnly) {
|
||||||
return true // carry on
|
return true // carry on
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -703,7 +752,9 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
|
|
||||||
// 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
|
||||||
currentPlaybackSession?.let { mediaProgressSyncer.play(it) }
|
mediaProgressSyncer.currentPlaybackSession?.let { playbackSession ->
|
||||||
|
mediaProgressSyncer.play(playbackSession)
|
||||||
|
}
|
||||||
clientEventEmitter?.onPlayingUpdate(true)
|
clientEventEmitter?.onPlayingUpdate(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -733,7 +784,10 @@ 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.play(it)
|
mediaProgressSyncer.currentPlaybackSession?.let { playbackSession ->
|
||||||
|
mediaProgressSyncer.play(playbackSession)
|
||||||
|
}
|
||||||
|
|
||||||
clientEventEmitter?.onPlayingUpdate(true)
|
clientEventEmitter?.onPlayingUpdate(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -781,6 +835,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
currentPlaybackSession?.currentTime = timeToSeek / 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
|
||||||
|
Log.d(tag, "seekPlayer seekTo $newWindowIndex | $newTimeOffset")
|
||||||
currentPlayer.seekTo(newWindowIndex, newTimeOffset)
|
currentPlayer.seekTo(newWindowIndex, newTimeOffset)
|
||||||
} else {
|
} else {
|
||||||
currentPlayer.seekTo(timeToSeek)
|
currentPlayer.seekTo(timeToSeek)
|
||||||
@@ -812,15 +867,37 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setPlaybackSpeed(speed: Float) {
|
fun setPlaybackSpeed(speed: Float) {
|
||||||
|
mediaManager.userSettingsPlaybackRate = speed
|
||||||
currentPlayer.setPlaybackSpeed(speed)
|
currentPlayer.setPlaybackSpeed(speed)
|
||||||
|
|
||||||
|
// Refresh Android Auto actions
|
||||||
|
mediaProgressSyncer.currentPlaybackSession?.let {
|
||||||
|
setMediaSessionConnectorCustomActions(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun closePlayback(calledOnError:Boolean? = false) {
|
fun closePlayback(calledOnError:Boolean? = false) {
|
||||||
Log.d(tag, "closePlayback")
|
Log.d(tag, "closePlayback")
|
||||||
|
val isLocal = mediaProgressSyncer.currentIsLocal
|
||||||
|
val currentSessionId = mediaProgressSyncer.currentSessionId
|
||||||
if (mediaProgressSyncer.listeningTimerRunning) {
|
if (mediaProgressSyncer.listeningTimerRunning) {
|
||||||
Log.i(tag, "About to close playback so stopping media progress syncer first")
|
Log.i(tag, "About to close playback so stopping media progress syncer first")
|
||||||
mediaProgressSyncer.stop(calledOnError == false) { // If closing on error then do not sync progress (causes exception)
|
mediaProgressSyncer.stop(calledOnError == false) { // If closing on error then do not sync progress (causes exception)
|
||||||
Log.d(tag, "Media Progress syncer stopped")
|
Log.d(tag, "Media Progress syncer stopped")
|
||||||
|
|
||||||
|
// If not local session then close on server
|
||||||
|
if (!isLocal && currentSessionId != "") {
|
||||||
|
apiHandler.closePlaybackSession(currentSessionId) {
|
||||||
|
Log.d(tag, "Closed playback session $currentSessionId")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If not local session then close on server
|
||||||
|
if (!isLocal && currentSessionId != "") {
|
||||||
|
apiHandler.closePlaybackSession(currentSessionId) {
|
||||||
|
Log.d(tag, "Closed playback session $currentSessionId")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -834,8 +911,10 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
currentPlaybackSession = null
|
currentPlaybackSession = null
|
||||||
mediaProgressSyncer.reset()
|
mediaProgressSyncer.reset()
|
||||||
clientEventEmitter?.onPlaybackClosed()
|
clientEventEmitter?.onPlaybackClosed()
|
||||||
|
|
||||||
PlayerListener.lastPauseTime = 0
|
PlayerListener.lastPauseTime = 0
|
||||||
isClosed = true
|
isClosed = true
|
||||||
|
DeviceManager.widgetUpdater?.onPlayerClosed()
|
||||||
stopForeground(Service.STOP_FOREGROUND_REMOVE)
|
stopForeground(Service.STOP_FOREGROUND_REMOVE)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
}
|
}
|
||||||
@@ -849,6 +928,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
return if(currentPlayer == castPlayer) PLAYER_CAST else PLAYER_EXO
|
return if(currentPlayer == castPlayer) PLAYER_CAST else PLAYER_EXO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("HardwareIds")
|
||||||
fun getDeviceInfo(): DeviceInfo {
|
fun getDeviceInfo(): DeviceInfo {
|
||||||
/* EXAMPLE
|
/* EXAMPLE
|
||||||
manufacturer: Google
|
manufacturer: Google
|
||||||
@@ -857,7 +937,8 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
sdkVersion: 32
|
sdkVersion: 32
|
||||||
appVersion: 0.9.46-beta
|
appVersion: 0.9.46-beta
|
||||||
*/
|
*/
|
||||||
return DeviceInfo(Build.MANUFACTURER, Build.MODEL, Build.BRAND, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
|
val deviceId = Settings.Secure.getString(ctx.contentResolver, Settings.Secure.ANDROID_ID)
|
||||||
|
return DeviceInfo(deviceId, Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val deviceSettings get() = DeviceManager.deviceData.deviceSettings ?: DeviceSettings.default()
|
private val deviceSettings get() = DeviceManager.deviceData.deviceSettings ?: DeviceSettings.default()
|
||||||
@@ -881,7 +962,7 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
//
|
//
|
||||||
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
// MEDIA BROWSER STUFF (ANDROID AUTO)
|
||||||
//
|
//
|
||||||
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", "com.audiobookshelf.app.debug", "com.android.systemui", 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 = "/"
|
||||||
private val LIBRARIES_ROOT = "__LIBRARIES__"
|
private val LIBRARIES_ROOT = "__LIBRARIES__"
|
||||||
@@ -960,10 +1041,12 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
val localBrowseItems:MutableList<MediaBrowserCompat.MediaItem> = mutableListOf()
|
||||||
|
|
||||||
localBooks.forEach { localLibraryItem ->
|
localBooks.forEach { localLibraryItem ->
|
||||||
val progress = DeviceManager.dbManager.getLocalMediaProgress(localLibraryItem.id)
|
if (localLibraryItem.media.getAudioTracks().isNotEmpty()) {
|
||||||
val description = localLibraryItem.getMediaDescription(progress, ctx)
|
val progress = DeviceManager.dbManager.getLocalMediaProgress(localLibraryItem.id)
|
||||||
|
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 ->
|
||||||
@@ -1086,10 +1169,11 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
networkCapabilities: NetworkCapabilities
|
networkCapabilities: NetworkCapabilities
|
||||||
) {
|
) {
|
||||||
super.onCapabilitiesChanged(network, networkCapabilities)
|
super.onCapabilitiesChanged(network, networkCapabilities)
|
||||||
val unmetered = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
|
||||||
Log.i(tag, "Network capabilities changed is unmetered = $unmetered")
|
isUnmeteredNetwork = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
||||||
isUnmeteredNetwork = unmetered
|
hasNetworkConnectivity = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) && networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
clientEventEmitter?.onNetworkMeteredChanged(unmetered)
|
Log.i(tag, "Network capabilities changed. hasNetworkConnectivity=$hasNetworkConnectivity | isUnmeteredNetwork=$isUnmeteredNetwork")
|
||||||
|
clientEventEmitter?.onNetworkMeteredChanged(isUnmeteredNetwork)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1160,5 +1244,39 @@ class PlayerNotificationService : MediaBrowserServiceCompat() {
|
|||||||
).build()
|
).build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inner class ChangePlaybackSpeedCustomActionProvider : 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? {
|
||||||
|
val playbackRate = mediaManager.getSavedPlaybackRate()
|
||||||
|
|
||||||
|
// Rounding values in the event a non preset value (.5, 1, 1.2, 1.5, 2, 3) is selected in the phone app
|
||||||
|
val drawable: Int = when (playbackRate) {
|
||||||
|
in 0.5f..0.7f -> R.drawable.ic_play_speed_0_5x
|
||||||
|
in 0.8f..1.0f -> R.drawable.ic_play_speed_1_0x
|
||||||
|
in 1.1f..1.3f -> R.drawable.ic_play_speed_1_2x
|
||||||
|
in 1.4f..1.7f -> R.drawable.ic_play_speed_1_5x
|
||||||
|
in 1.8f..2.4f -> R.drawable.ic_play_speed_2_0x
|
||||||
|
in 2.5f..3.0f -> R.drawable.ic_play_speed_3_0x
|
||||||
|
// anything set above 3 will be show the 3x to save from creating 100 icons
|
||||||
|
else -> R.drawable.ic_play_speed_3_0x
|
||||||
|
}
|
||||||
|
val customActionExtras = Bundle()
|
||||||
|
customActionExtras.putFloat("speed", playbackRate)
|
||||||
|
return PlaybackStateCompat.CustomAction.Builder(
|
||||||
|
CUSTOM_ACTION_CHANGE_SPEED,
|
||||||
|
getContext().getString(R.string.action_change_speed),
|
||||||
|
drawable
|
||||||
|
)
|
||||||
|
.setExtras(customActionExtras)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,11 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
mainActivity = (activity as MainActivity)
|
mainActivity = (activity as MainActivity)
|
||||||
apiHandler = ApiHandler(mainActivity)
|
apiHandler = ApiHandler(mainActivity)
|
||||||
|
|
||||||
initCastManager()
|
try {
|
||||||
|
initCastManager()
|
||||||
|
} catch(e:Exception) {
|
||||||
|
Log.e(tag, "initCastManager exception ${e.printStackTrace()}")
|
||||||
|
}
|
||||||
|
|
||||||
val foregroundServiceReady : () -> Unit = {
|
val foregroundServiceReady : () -> Unit = {
|
||||||
playerNotificationService = mainActivity.foregroundService
|
playerNotificationService = mainActivity.foregroundService
|
||||||
@@ -63,8 +67,11 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
emit("onSleepTimerEnded", currentPosition)
|
emit("onSleepTimerEnded", currentPosition)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSleepTimerSet(sleepTimeRemaining: Int) {
|
override fun onSleepTimerSet(sleepTimeRemaining: Int, isAutoSleepTimer:Boolean) {
|
||||||
emit("onSleepTimerSet", sleepTimeRemaining)
|
val ret = JSObject()
|
||||||
|
ret.put("value", sleepTimeRemaining)
|
||||||
|
ret.put("isAuto", isAutoSleepTimer)
|
||||||
|
notifyListeners("onSleepTimerSet", ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress) {
|
override fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress) {
|
||||||
@@ -94,6 +101,10 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
override fun onMediaItemHistoryUpdated(mediaItemHistory:MediaItemHistory) {
|
override fun onMediaItemHistoryUpdated(mediaItemHistory:MediaItemHistory) {
|
||||||
notifyListeners("onMediaItemHistoryUpdated", JSObject(jacksonMapper.writeValueAsString(mediaItemHistory)))
|
notifyListeners("onMediaItemHistoryUpdated", JSObject(jacksonMapper.writeValueAsString(mediaItemHistory)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onPlaybackSpeedChanged(playbackSpeed:Float) {
|
||||||
|
emit("onPlaybackSpeedChanged", playbackSpeed)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
MediaEventManager.clientEventEmitter = playerNotificationService.clientEventEmitter
|
MediaEventManager.clientEventEmitter = playerNotificationService.clientEventEmitter
|
||||||
@@ -169,7 +180,7 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
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")
|
val startTimeOverride = call.getDouble("startTime")
|
||||||
Log.d(tag, "prepareLibraryItem lid=$libraryItemId, startTimeOverride=$startTimeOverride")
|
Log.d(tag, "prepareLibraryItem lid=$libraryItemId, startTimeOverride=$startTimeOverride, playbackRate=$playbackRate")
|
||||||
|
|
||||||
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")
|
||||||
@@ -190,7 +201,7 @@ 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, playerNotificationService.getDeviceInfo())
|
||||||
if (startTimeOverride != null) {
|
if (startTimeOverride != null) {
|
||||||
Log.d(tag, "prepareLibraryItem: Using start time override $startTimeOverride")
|
Log.d(tag, "prepareLibraryItem: Using start time override $startTimeOverride")
|
||||||
playbackSession.currentTime = startTimeOverride
|
playbackSession.currentTime = startTimeOverride
|
||||||
@@ -330,7 +341,8 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
val isChapterTime:Boolean = call.getBoolean("isChapterTime", false) == true
|
val isChapterTime:Boolean = call.getBoolean("isChapterTime", false) == true
|
||||||
|
|
||||||
Handler(Looper.getMainLooper()).post {
|
Handler(Looper.getMainLooper()).post {
|
||||||
val success:Boolean = playerNotificationService.sleepTimerManager.setSleepTimer(time, isChapterTime)
|
val playbackSession: PlaybackSession? = playerNotificationService.mediaProgressSyncer.currentPlaybackSession ?: playerNotificationService.currentPlaybackSession
|
||||||
|
val success:Boolean = playerNotificationService.sleepTimerManager.setManualSleepTimer(playbackSession?.id ?: "", time, isChapterTime)
|
||||||
val ret = JSObject()
|
val ret = JSObject()
|
||||||
ret.put("success", success)
|
ret.put("success", success)
|
||||||
call.resolve(ret)
|
call.resolve(ret)
|
||||||
@@ -371,7 +383,9 @@ class AbsAudioPlayer : Plugin() {
|
|||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun cancelSleepTimer(call: PluginCall) {
|
fun cancelSleepTimer(call: PluginCall) {
|
||||||
playerNotificationService.sleepTimerManager.cancelSleepTimer()
|
Handler(Looper.getMainLooper()).post {
|
||||||
|
playerNotificationService.sleepTimerManager.cancelSleepTimer()
|
||||||
|
}
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -216,13 +216,27 @@ class AbsDatabase : Plugin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun syncLocalMediaProgressWithServer(call:PluginCall) {
|
fun syncLocalSessionsWithServer(call:PluginCall) {
|
||||||
if (DeviceManager.serverConnectionConfig == null) {
|
if (DeviceManager.serverConnectionConfig == null) {
|
||||||
Log.e(tag, "syncLocalMediaProgressWithServer not connected to server")
|
Log.e(tag, "syncLocalSessionsWithServer not connected to server")
|
||||||
return call.resolve()
|
return call.resolve()
|
||||||
}
|
}
|
||||||
apiHandler.syncMediaProgress {
|
|
||||||
call.resolve(JSObject(jacksonMapper.writeValueAsString(it)))
|
apiHandler.syncLocalMediaProgressForUser {
|
||||||
|
Log.d(tag, "Finished syncing local media progress for user")
|
||||||
|
val savedSessions = DeviceManager.dbManager.getPlaybackSessions().filter { it.serverConnectionConfigId == DeviceManager.serverConnectionConfigId }
|
||||||
|
|
||||||
|
if (savedSessions.isNotEmpty()) {
|
||||||
|
apiHandler.sendSyncLocalSessions(savedSessions) { success, errorMsg ->
|
||||||
|
if (!success) {
|
||||||
|
call.resolve(JSObject("{\"error\":\"$errorMsg\"}"))
|
||||||
|
} else {
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +267,8 @@ class AbsDatabase : Plugin() {
|
|||||||
progress = mediaProgress.progress,
|
progress = mediaProgress.progress,
|
||||||
currentTime = mediaProgress.currentTime,
|
currentTime = mediaProgress.currentTime,
|
||||||
isFinished = mediaProgress.isFinished,
|
isFinished = mediaProgress.isFinished,
|
||||||
|
ebookLocation = mediaProgress.ebookLocation,
|
||||||
|
ebookProgress = mediaProgress.ebookProgress,
|
||||||
lastUpdate = mediaProgress.lastUpdate,
|
lastUpdate = mediaProgress.lastUpdate,
|
||||||
startedAt = mediaProgress.startedAt,
|
startedAt = mediaProgress.startedAt,
|
||||||
finishedAt = mediaProgress.finishedAt,
|
finishedAt = mediaProgress.finishedAt,
|
||||||
@@ -333,6 +349,8 @@ class AbsDatabase : Plugin() {
|
|||||||
progress = if (isFinished) 1.0 else 0.0,
|
progress = if (isFinished) 1.0 else 0.0,
|
||||||
currentTime = 0.0,
|
currentTime = 0.0,
|
||||||
isFinished = isFinished,
|
isFinished = isFinished,
|
||||||
|
ebookLocation = null,
|
||||||
|
ebookProgress = null,
|
||||||
lastUpdate = currentTime,
|
lastUpdate = currentTime,
|
||||||
startedAt = if (isFinished) currentTime else 0L,
|
startedAt = if (isFinished) currentTime else 0L,
|
||||||
finishedAt = if (isFinished) currentTime else null,
|
finishedAt = if (isFinished) currentTime else null,
|
||||||
@@ -377,6 +395,55 @@ class AbsDatabase : Plugin() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun updateLocalEbookProgress(call:PluginCall) {
|
||||||
|
val localLibraryItemId = call.getString("localLibraryItemId", "").toString()
|
||||||
|
val ebookLocation = call.getString("ebookLocation", "").toString()
|
||||||
|
val ebookProgress = call.getDouble("ebookProgress") ?: 0.0
|
||||||
|
|
||||||
|
val localMediaProgressId = localLibraryItemId
|
||||||
|
var localMediaProgress = DeviceManager.dbManager.getLocalMediaProgress(localMediaProgressId)
|
||||||
|
|
||||||
|
if (localMediaProgress == null) {
|
||||||
|
Log.d(tag, "updateLocalEbookProgress Local Media Progress not found $localMediaProgressId - Creating new")
|
||||||
|
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
|
||||||
|
?: return call.resolve(JSObject("{\"error\":\"Library Item not found\"}"))
|
||||||
|
|
||||||
|
val book = localLibraryItem.media as Book
|
||||||
|
|
||||||
|
localMediaProgress = LocalMediaProgress(
|
||||||
|
id = localMediaProgressId,
|
||||||
|
localLibraryItemId = localLibraryItemId,
|
||||||
|
localEpisodeId = null,
|
||||||
|
duration = book.duration ?: 0.0,
|
||||||
|
progress = 0.0,
|
||||||
|
currentTime = 0.0,
|
||||||
|
isFinished = false,
|
||||||
|
ebookLocation = ebookLocation,
|
||||||
|
ebookProgress = ebookProgress,
|
||||||
|
lastUpdate = System.currentTimeMillis(),
|
||||||
|
startedAt = 0L,
|
||||||
|
finishedAt = null,
|
||||||
|
serverConnectionConfigId = localLibraryItem.serverConnectionConfigId,
|
||||||
|
serverAddress = localLibraryItem.serverAddress,
|
||||||
|
serverUserId = localLibraryItem.serverUserId,
|
||||||
|
libraryItemId = localLibraryItem.libraryItemId,
|
||||||
|
episodeId = null)
|
||||||
|
} else {
|
||||||
|
localMediaProgress.updateEbookProgress(ebookLocation, ebookProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save local media progress locally
|
||||||
|
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
|
||||||
|
|
||||||
|
val lmpstring = jacksonMapper.writeValueAsString(localMediaProgress)
|
||||||
|
Log.d(tag, "updateLocalEbookProgress: Local Media Progress String $lmpstring")
|
||||||
|
|
||||||
|
val jsobj = JSObject()
|
||||||
|
jsobj.put("localMediaProgress", JSObject(lmpstring))
|
||||||
|
call.resolve(jsobj)
|
||||||
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun updateLocalTrackOrder(call:PluginCall) {
|
fun updateLocalTrackOrder(call:PluginCall) {
|
||||||
val localLibraryItemId = call.getString("localLibraryItemId", "") ?: ""
|
val localLibraryItemId = call.getString("localLibraryItemId", "") ?: ""
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package com.audiobookshelf.app.plugins
|
|||||||
|
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.audiobookshelf.app.MainActivity
|
import com.audiobookshelf.app.MainActivity
|
||||||
@@ -51,7 +50,6 @@ class AbsDownloader : Plugin() {
|
|||||||
folderScanner = FolderScanner(mainActivity)
|
folderScanner = FolderScanner(mainActivity)
|
||||||
apiHandler = ApiHandler(mainActivity)
|
apiHandler = ApiHandler(mainActivity)
|
||||||
downloadItemManager = DownloadItemManager(downloadManager, folderScanner, mainActivity, clientEventEmitter)
|
downloadItemManager = DownloadItemManager(downloadManager, folderScanner, mainActivity, clientEventEmitter)
|
||||||
Log.d(tag, "Build SDK ${Build.VERSION.SDK_INT}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
@@ -59,7 +57,7 @@ class AbsDownloader : Plugin() {
|
|||||||
val libraryItemId = call.data.getString("libraryItemId").toString()
|
val libraryItemId = call.data.getString("libraryItemId").toString()
|
||||||
var episodeId = call.data.getString("episodeId").toString()
|
var episodeId = call.data.getString("episodeId").toString()
|
||||||
if (episodeId == "null") episodeId = ""
|
if (episodeId == "null") episodeId = ""
|
||||||
val localFolderId = call.data.getString("localFolderId").toString()
|
var localFolderId = call.data.getString("localFolderId", "").toString()
|
||||||
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"
|
||||||
@@ -74,9 +72,18 @@ class AbsDownloader : Plugin() {
|
|||||||
} else {
|
} else {
|
||||||
Log.d(tag, "Got library item from server ${libraryItem.id}")
|
Log.d(tag, "Got library item from server ${libraryItem.id}")
|
||||||
|
|
||||||
val localFolder = DeviceManager.dbManager.getLocalFolder(localFolderId)
|
if (localFolderId == "") {
|
||||||
if (localFolder != null) {
|
localFolderId = "internal-${libraryItem.mediaType}"
|
||||||
|
}
|
||||||
|
var localFolder = DeviceManager.dbManager.getLocalFolder(localFolderId)
|
||||||
|
|
||||||
|
if (localFolder == null && localFolderId.startsWith("internal-")) {
|
||||||
|
Log.d(tag, "Creating new App Storage internal LocalFolder $localFolderId")
|
||||||
|
localFolder = LocalFolder(localFolderId, "Internal App Storage", "", "", "", "", "internal", libraryItem.mediaType)
|
||||||
|
DeviceManager.dbManager.saveLocalFolder(localFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localFolder != null) {
|
||||||
if (episodeId.isNotEmpty() && libraryItem.mediaType != "podcast") {
|
if (episodeId.isNotEmpty() && libraryItem.mediaType != "podcast") {
|
||||||
Log.e(tag, "Library item is not a podcast but episode was requested")
|
Log.e(tag, "Library item is not a podcast but episode was requested")
|
||||||
call.resolve(JSObject("{\"error\":\"Invalid library item not a podcast\"}"))
|
call.resolve(JSObject("{\"error\":\"Invalid library item not a podcast\"}"))
|
||||||
@@ -102,12 +109,6 @@ class AbsDownloader : Plugin() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean folder path so it can be used in URL
|
|
||||||
private fun cleanRelPath(relPath: String): String {
|
|
||||||
val cleanedRelPath = relPath.replace("\\", "/").replace("%", "%25").replace("#", "%23")
|
|
||||||
return if (cleanedRelPath.startsWith("/")) cleanedRelPath.substring(1) else cleanedRelPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// Item filenames could be the same if they are in sub-folders, this will make them unique
|
// Item filenames could be the same if they are in sub-folders, this will make them unique
|
||||||
private fun getFilenameFromRelPath(relPath: String): String {
|
private fun getFilenameFromRelPath(relPath: String): String {
|
||||||
var cleanedRelPath = relPath.replace("\\", "_").replace("/", "_")
|
var cleanedRelPath = relPath.replace("\\", "_").replace("/", "_")
|
||||||
@@ -129,7 +130,9 @@ 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 isInternal = localFolder.id.startsWith("internal-")
|
||||||
|
|
||||||
|
val tempFolderPath = if (isInternal) "${mainActivity.filesDir}/downloads/${libraryItem.id}" else mainActivity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
|
||||||
Log.d(tag, "downloadCacheDirectory=$tempFolderPath")
|
Log.d(tag, "downloadCacheDirectory=$tempFolderPath")
|
||||||
|
|
||||||
@@ -140,13 +143,40 @@ class AbsDownloader : Plugin() {
|
|||||||
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 itemSubfolder = "$bookAuthor/$bookTitle"
|
val itemSubfolder = "$bookAuthor/$bookTitle"
|
||||||
val itemFolderPath = "${localFolder.absolutePath}/$itemSubfolder"
|
val itemFolderPath = if (isInternal) "$tempFolderPath" else "${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())
|
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())
|
||||||
|
|
||||||
|
val book = libraryItem.media as Book
|
||||||
|
book.ebookFile?.let { ebookFile ->
|
||||||
|
val fileSize = ebookFile.metadata?.size ?: 0
|
||||||
|
val serverPath = "/api/items/${libraryItem.id}/file/${ebookFile.ino}/download"
|
||||||
|
val destinationFilename = getFilenameFromRelPath(ebookFile.metadata?.relPath ?: "")
|
||||||
|
val finalDestinationFile = File("$itemFolderPath/$destinationFilename")
|
||||||
|
val destinationFile = File("$tempFolderPath/$destinationFilename")
|
||||||
|
|
||||||
|
if (destinationFile.exists()) {
|
||||||
|
Log.d(tag, "TEMP ebook file already exists, removing it from ${destinationFile.absolutePath}")
|
||||||
|
destinationFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalDestinationFile.exists()) {
|
||||||
|
Log.d(tag, "ebook file already exists, removing it from ${finalDestinationFile.absolutePath}")
|
||||||
|
finalDestinationFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, fileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,ebookFile,null,null)
|
||||||
|
downloadItem.downloadItemParts.add(downloadItemPart)
|
||||||
|
}
|
||||||
|
|
||||||
// Create download item part for each audio track
|
// Create download item part for each audio track
|
||||||
|
val audioFiles = (libraryItem.media as Book).audioFiles ?: mutableListOf()
|
||||||
tracks.forEach { audioTrack ->
|
tracks.forEach { audioTrack ->
|
||||||
val fileSize = audioTrack.metadata?.size ?: 0
|
val fileSize = audioTrack.metadata?.size ?: 0
|
||||||
val serverPath = "/s/item/${libraryItem.id}/${cleanRelPath(audioTrack.relPath)}"
|
|
||||||
|
// TODO: Currently file ino is only stored on AudioFile. This should be updated server side to be in FileMetadata or on the AudioTrack
|
||||||
|
val audioFileIno = audioFiles.find { it.metadata.path == audioTrack.metadata?.path }?.ino
|
||||||
|
|
||||||
|
val serverPath = "/api/items/${libraryItem.id}/file/${audioFileIno}/download"
|
||||||
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")
|
||||||
|
|
||||||
@@ -163,7 +193,7 @@ class AbsDownloader : Plugin() {
|
|||||||
finalDestinationFile.delete()
|
finalDestinationFile.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, fileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,audioTrack,null)
|
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, fileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,null,audioTrack,null)
|
||||||
downloadItem.downloadItemParts.add(downloadItemPart)
|
downloadItem.downloadItemParts.add(downloadItemPart)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -188,7 +218,7 @@ class AbsDownloader : Plugin() {
|
|||||||
finalDestinationFile.delete()
|
finalDestinationFile.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, coverFileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,null,null)
|
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, coverFileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,null,null,null)
|
||||||
downloadItem.downloadItemParts.add(downloadItemPart)
|
downloadItem.downloadItemParts.add(downloadItemPart)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,14 +229,15 @@ class AbsDownloader : Plugin() {
|
|||||||
val podcastTitle = cleanStringForFileSystem(libraryItem.media.metadata.title)
|
val podcastTitle = cleanStringForFileSystem(libraryItem.media.metadata.title)
|
||||||
|
|
||||||
val audioTrack = episode?.audioTrack
|
val audioTrack = episode?.audioTrack
|
||||||
|
val audioFileIno = episode?.audioFile?.ino
|
||||||
val fileSize = audioTrack?.metadata?.size ?: 0
|
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 = if (isInternal) "$tempFolderPath" else "${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, 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 = "/api/items/${libraryItem.id}/file/${audioFileIno}/download"
|
||||||
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")
|
||||||
|
|
||||||
@@ -217,7 +248,7 @@ class AbsDownloader : Plugin() {
|
|||||||
finalDestinationFile.delete()
|
finalDestinationFile.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
var downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,fileSize, destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,episode)
|
var downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,fileSize, destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,audioTrack,episode)
|
||||||
downloadItem.downloadItemParts.add(downloadItemPart)
|
downloadItem.downloadItemParts.add(downloadItemPart)
|
||||||
|
|
||||||
if (libraryItem.media.coverPath != null && libraryItem.media.coverPath?.isNotEmpty() == true) {
|
if (libraryItem.media.coverPath != null && libraryItem.media.coverPath?.isNotEmpty() == true) {
|
||||||
@@ -233,7 +264,7 @@ class AbsDownloader : Plugin() {
|
|||||||
if (finalDestinationFile.exists()) {
|
if (finalDestinationFile.exists()) {
|
||||||
Log.d(tag, "Podcast cover already exists - not downloading cover again")
|
Log.d(tag, "Podcast cover already exists - not downloading cover again")
|
||||||
} else {
|
} else {
|
||||||
downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,coverFileSize,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,null)
|
downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,coverFileSize,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,null,null)
|
||||||
downloadItem.downloadItemParts.add(downloadItemPart)
|
downloadItem.downloadItemParts.add(downloadItemPart)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.audiobookshelf.app.plugins
|
package com.audiobookshelf.app.plugins
|
||||||
|
|
||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.database.Cursor
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -23,6 +22,7 @@ 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 java.io.File
|
||||||
|
|
||||||
@CapacitorPlugin(name = "AbsFileSystem")
|
@CapacitorPlugin(name = "AbsFileSystem")
|
||||||
class AbsFileSystem : Plugin() {
|
class AbsFileSystem : Plugin() {
|
||||||
@@ -164,6 +164,13 @@ class AbsFileSystem : Plugin() {
|
|||||||
call.resolve(jsobj)
|
call.resolve(jsobj)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun getSDKVersion(call: PluginCall) {
|
||||||
|
val jsObject = JSObject()
|
||||||
|
jsObject.put("version", Build.VERSION.SDK_INT)
|
||||||
|
call.resolve(jsObject)
|
||||||
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun scanFolder(call: PluginCall) {
|
fun scanFolder(call: PluginCall) {
|
||||||
val folderId = call.data.getString("folderId", "").toString()
|
val folderId = call.data.getString("folderId", "").toString()
|
||||||
@@ -226,8 +233,46 @@ class AbsFileSystem : Plugin() {
|
|||||||
val 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")
|
||||||
|
|
||||||
val docfile = DocumentFileCompat.fromUri(mainActivity, Uri.parse(contentUrl))
|
// Check if should delete subfolder
|
||||||
val success = docfile?.delete() == true
|
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
|
||||||
|
|
||||||
|
val success: Boolean
|
||||||
|
|
||||||
|
// If internal library item use File to delete
|
||||||
|
if (localLibraryItem?.folderId?.startsWith("internal-") == true) {
|
||||||
|
Log.d(tag, "Deleting internal library item at absolutePath $absolutePath")
|
||||||
|
val file = File(absolutePath)
|
||||||
|
success = file.deleteRecursively()
|
||||||
|
} else {
|
||||||
|
var subfolderPathToDelete = ""
|
||||||
|
localLibraryItem?.folderId?.let { folderId ->
|
||||||
|
val folder = DeviceManager.dbManager.getLocalFolder(folderId)
|
||||||
|
folder?.absolutePath?.let { folderPath ->
|
||||||
|
val splitAbsolutePath = absolutePath.split("/")
|
||||||
|
val fullSubDir = splitAbsolutePath.subList(0, splitAbsolutePath.size - 1).joinToString("/")
|
||||||
|
if (fullSubDir != folderPath) {
|
||||||
|
val subdirHasAnItem = DeviceManager.dbManager.getLocalLibraryItems().any { _localLibraryItem ->
|
||||||
|
if (_localLibraryItem.id == localLibraryItemId) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
_localLibraryItem.absolutePath.startsWith(fullSubDir)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subfolderPathToDelete = if (subdirHasAnItem) "" else fullSubDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val docfile = DocumentFileCompat.fromUri(mainActivity, Uri.parse(contentUrl))
|
||||||
|
success = docfile?.delete() == true
|
||||||
|
|
||||||
|
if (subfolderPathToDelete != "") {
|
||||||
|
Log.d(tag, "Deleting empty subfolder at $subfolderPathToDelete")
|
||||||
|
val docfilesub = DocumentFileCompat.fromFullPath(mainActivity, subfolderPathToDelete)
|
||||||
|
docfilesub?.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
DeviceManager.dbManager.removeLocalLibraryItem(localLibraryItemId)
|
DeviceManager.dbManager.removeLocalLibraryItem(localLibraryItemId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
package com.audiobookshelf.app.server
|
package com.audiobookshelf.app.server
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.ConnectivityManager
|
|
||||||
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.media.MediaEventManager
|
||||||
import com.audiobookshelf.app.player.MediaProgressSyncData
|
import com.audiobookshelf.app.media.MediaProgressSyncData
|
||||||
|
import com.audiobookshelf.app.media.SyncResult
|
||||||
|
import com.audiobookshelf.app.models.User
|
||||||
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
|
||||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||||
@@ -17,6 +17,7 @@ import com.getcapacitor.JSObject
|
|||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import okhttp3.internal.EMPTY_REQUEST
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
@@ -28,14 +29,14 @@ class ApiHandler(var ctx:Context) {
|
|||||||
|
|
||||||
private var defaultClient = OkHttpClient()
|
private var defaultClient = OkHttpClient()
|
||||||
private var pingClient = OkHttpClient.Builder().callTimeout(3, TimeUnit.SECONDS).build()
|
private var pingClient = OkHttpClient.Builder().callTimeout(3, TimeUnit.SECONDS).build()
|
||||||
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
|
||||||
|
|
||||||
data class LocalMediaProgressSyncPayload(val localMediaProgress:List<LocalMediaProgress>)
|
data class LocalSessionsSyncRequestPayload(val sessions:List<PlaybackSession>)
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
data class MediaProgressSyncResponsePayload(val numServerProgressUpdates:Int, val localProgressUpdates:List<LocalMediaProgress>, val serverProgressUpdates:List<MediaProgress>)
|
data class LocalSessionSyncResult(val id:String, val success:Boolean, val progressSynced:Boolean?, val error:String?)
|
||||||
data class LocalMediaProgressSyncResultsPayload(var numLocalMediaProgressForServer:Int, var numServerProgressUpdates:Int, var numLocalProgressUpdates:Int, var serverProgressUpdates:List<MediaProgress>)
|
data class LocalSessionsSyncResponsePayload(val results:List<LocalSessionSyncResult>)
|
||||||
|
|
||||||
fun getRequest(endpoint:String, httpClient:OkHttpClient?, config:ServerConnectionConfig?, cb: (JSObject) -> Unit) {
|
private fun getRequest(endpoint:String, httpClient:OkHttpClient?, config:ServerConnectionConfig?, cb: (JSObject) -> Unit) {
|
||||||
val address = config?.address ?: DeviceManager.serverAddress
|
val address = config?.address ?: DeviceManager.serverAddress
|
||||||
val token = config?.token ?: DeviceManager.token
|
val token = config?.token ?: DeviceManager.token
|
||||||
|
|
||||||
@@ -45,18 +46,20 @@ class ApiHandler(var ctx:Context) {
|
|||||||
makeRequest(request, httpClient, cb)
|
makeRequest(request, httpClient, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun postRequest(endpoint:String, payload: JSObject, config:ServerConnectionConfig?, cb: (JSObject) -> Unit) {
|
private fun postRequest(endpoint:String, payload: JSObject?, config:ServerConnectionConfig?, cb: (JSObject) -> Unit) {
|
||||||
val address = config?.address ?: DeviceManager.serverAddress
|
val address = config?.address ?: DeviceManager.serverAddress
|
||||||
val token = config?.token ?: DeviceManager.token
|
val token = config?.token ?: DeviceManager.token
|
||||||
val mediaType = "application/json; charset=utf-8".toMediaType()
|
val mediaType = "application/json; charset=utf-8".toMediaType()
|
||||||
val requestBody = payload.toString().toRequestBody(mediaType)
|
val requestBody = payload?.toString()?.toRequestBody(mediaType) ?: EMPTY_REQUEST
|
||||||
|
val requestUrl = "${address}$endpoint"
|
||||||
|
Log.d(tag, "postRequest to $requestUrl")
|
||||||
val request = Request.Builder().post(requestBody)
|
val request = Request.Builder().post(requestBody)
|
||||||
.url("${address}$endpoint").addHeader("Authorization", "Bearer ${token}")
|
.url(requestUrl).addHeader("Authorization", "Bearer ${token}")
|
||||||
.build()
|
.build()
|
||||||
makeRequest(request, null, cb)
|
makeRequest(request, null, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun patchRequest(endpoint:String, payload: JSObject, cb: (JSObject) -> Unit) {
|
private fun patchRequest(endpoint:String, payload: JSObject, cb: (JSObject) -> Unit) {
|
||||||
val mediaType = "application/json; charset=utf-8".toMediaType()
|
val mediaType = "application/json; charset=utf-8".toMediaType()
|
||||||
val requestBody = payload.toString().toRequestBody(mediaType)
|
val requestBody = payload.toString().toRequestBody(mediaType)
|
||||||
val request = Request.Builder().patch(requestBody)
|
val request = Request.Builder().patch(requestBody)
|
||||||
@@ -65,31 +68,7 @@ class ApiHandler(var ctx:Context) {
|
|||||||
makeRequest(request, null, cb)
|
makeRequest(request, null, cb)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isOnline(): Boolean {
|
private fun makeRequest(request:Request, httpClient:OkHttpClient?, cb: (JSObject) -> Unit) {
|
||||||
val connectivityManager = ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
|
||||||
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
|
||||||
if (capabilities != null) {
|
|
||||||
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
|
|
||||||
Log.i("Internet", "NetworkCapabilities.TRANSPORT_CELLULAR")
|
|
||||||
return true
|
|
||||||
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
|
|
||||||
Log.i("Internet", "NetworkCapabilities.TRANSPORT_WIFI")
|
|
||||||
return true
|
|
||||||
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
|
|
||||||
Log.i("Internet", "NetworkCapabilities.TRANSPORT_ETHERNET")
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isUsingCellularData(): Boolean {
|
|
||||||
val connectivityManager = ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
|
||||||
val capabilities = connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
|
|
||||||
return capabilities?.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) == true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun makeRequest(request:Request, httpClient:OkHttpClient?, cb: (JSObject) -> Unit) {
|
|
||||||
val client = httpClient ?: defaultClient
|
val client = httpClient ?: defaultClient
|
||||||
client.newCall(request).enqueue(object : Callback {
|
client.newCall(request).enqueue(object : Callback {
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
@@ -135,6 +114,18 @@ class ApiHandler(var ctx:Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getCurrentUser(cb: (User?) -> Unit) {
|
||||||
|
getRequest("/api/me", null, null) {
|
||||||
|
if (it.has("error")) {
|
||||||
|
Log.e(tag, it.getString("error") ?: "getCurrentUser Failed")
|
||||||
|
cb(null)
|
||||||
|
} else {
|
||||||
|
val user = jacksonMapper.readValue<User>(it.toString())
|
||||||
|
cb(user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun getLibraries(cb: (List<Library>) -> Unit) {
|
fun getLibraries(cb: (List<Library>) -> Unit) {
|
||||||
val mapper = jacksonMapper
|
val mapper = jacksonMapper
|
||||||
getRequest("/api/libraries", null,null) {
|
getRequest("/api/libraries", null,null) {
|
||||||
@@ -251,59 +242,6 @@ class ApiHandler(var ctx:Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun syncMediaProgress(cb: (LocalMediaProgressSyncResultsPayload) -> Unit) {
|
|
||||||
if (!isOnline()) {
|
|
||||||
Log.d(tag, "Error not online")
|
|
||||||
cb(LocalMediaProgressSyncResultsPayload(0,0,0, mutableListOf()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all local media progress connected to items on the current connected server
|
|
||||||
val localMediaProgress = DeviceManager.dbManager.getAllLocalMediaProgress().filter {
|
|
||||||
it.serverConnectionConfigId == DeviceManager.serverConnectionConfig?.id
|
|
||||||
}
|
|
||||||
|
|
||||||
val localSyncResultsPayload = LocalMediaProgressSyncResultsPayload(localMediaProgress.size,0, 0, mutableListOf())
|
|
||||||
|
|
||||||
if (localMediaProgress.isNotEmpty()) {
|
|
||||||
Log.d(tag, "Sending sync local progress request with ${localMediaProgress.size} progress items")
|
|
||||||
val payload = JSObject(jacksonMapper.writeValueAsString(LocalMediaProgressSyncPayload(localMediaProgress)))
|
|
||||||
postRequest("/api/me/sync-local-progress", payload, null) {
|
|
||||||
Log.d(tag, "Media Progress Sync payload $payload - response ${it}")
|
|
||||||
|
|
||||||
if (it.toString() == "{}") {
|
|
||||||
Log.e(tag, "Progress sync received empty object")
|
|
||||||
} else if (it.has("error")) {
|
|
||||||
Log.e(tag, it.getString("error") ?: "Progress sync error")
|
|
||||||
} else {
|
|
||||||
val progressSyncResponsePayload = jacksonMapper.readValue<MediaProgressSyncResponsePayload>(it.toString())
|
|
||||||
|
|
||||||
localSyncResultsPayload.numLocalProgressUpdates = progressSyncResponsePayload.localProgressUpdates.size
|
|
||||||
localSyncResultsPayload.serverProgressUpdates = progressSyncResponsePayload.serverProgressUpdates
|
|
||||||
localSyncResultsPayload.numServerProgressUpdates = progressSyncResponsePayload.numServerProgressUpdates
|
|
||||||
Log.d(tag, "Media Progress Sync | Local Updates: $localSyncResultsPayload")
|
|
||||||
if (progressSyncResponsePayload.localProgressUpdates.isNotEmpty()) {
|
|
||||||
// Update all local media progress
|
|
||||||
progressSyncResponsePayload.localProgressUpdates.forEach { localMediaProgress ->
|
|
||||||
MediaEventManager.syncEvent(localMediaProgress, "Local progress updated. Received from server sync local API request")
|
|
||||||
|
|
||||||
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
progressSyncResponsePayload.serverProgressUpdates.forEach { localMediaProgress ->
|
|
||||||
MediaEventManager.syncEvent(localMediaProgress, "Server progress updated. Received from server sync local API request")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cb(localSyncResultsPayload)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.d(tag, "No local media progress to sync")
|
|
||||||
cb(localSyncResultsPayload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateMediaProgress(libraryItemId:String,episodeId:String?,updatePayload:JSObject, cb: () -> Unit) {
|
fun updateMediaProgress(libraryItemId:String,episodeId:String?,updatePayload:JSObject, cb: () -> Unit) {
|
||||||
Log.d(tag, "updateMediaProgress $libraryItemId $episodeId $updatePayload")
|
Log.d(tag, "updateMediaProgress $libraryItemId $episodeId $updatePayload")
|
||||||
val endpoint = if(episodeId.isNullOrEmpty()) "/api/me/progress/$libraryItemId" else "/api/me/progress/$libraryItemId/$episodeId"
|
val endpoint = if(episodeId.isNullOrEmpty()) "/api/me/progress/$libraryItemId" else "/api/me/progress/$libraryItemId/$episodeId"
|
||||||
@@ -355,6 +293,13 @@ class ApiHandler(var ctx:Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun closePlaybackSession(playbackSessionId:String, cb: (Boolean) -> Unit) {
|
||||||
|
Log.d(tag, "closePlaybackSession: playbackSessionId=$playbackSessionId")
|
||||||
|
postRequest("/api/session/$playbackSessionId/close", null, null) {
|
||||||
|
cb(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun authorize(config:ServerConnectionConfig, cb: (MutableList<MediaProgress>?) -> Unit) {
|
fun authorize(config:ServerConnectionConfig, cb: (MutableList<MediaProgress>?) -> Unit) {
|
||||||
Log.d(tag, "authorize: Authorizing ${config.address}")
|
Log.d(tag, "authorize: Authorizing ${config.address}")
|
||||||
postRequest("/api/authorize", JSObject(), config) {
|
postRequest("/api/authorize", JSObject(), config) {
|
||||||
@@ -375,4 +320,65 @@ class ApiHandler(var ctx:Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun sendSyncLocalSessions(playbackSessions:List<PlaybackSession>, cb: (Boolean, String?) -> Unit) {
|
||||||
|
val payload = JSObject(jacksonMapper.writeValueAsString(LocalSessionsSyncRequestPayload(playbackSessions)))
|
||||||
|
|
||||||
|
postRequest("/api/session/local-all", payload, null) {
|
||||||
|
if (!it.getString("error").isNullOrEmpty()) {
|
||||||
|
cb(false, it.getString("error"))
|
||||||
|
} else {
|
||||||
|
val response = jacksonMapper.readValue<LocalSessionsSyncResponsePayload>(it.toString())
|
||||||
|
response.results.forEach { localSessionSyncResult ->
|
||||||
|
playbackSessions.find { ps -> ps.id == localSessionSyncResult.id }?.let { session ->
|
||||||
|
if (localSessionSyncResult.progressSynced == true) {
|
||||||
|
val syncResult = SyncResult(true, true, "Progress synced on server")
|
||||||
|
MediaEventManager.saveEvent(session, syncResult)
|
||||||
|
Log.i(tag, "Successfully synced session ${session.displayTitle} with server")
|
||||||
|
} else if (!localSessionSyncResult.success) {
|
||||||
|
Log.e(tag, "Failed to sync session ${session.displayTitle} with server. Error: ${localSessionSyncResult.error}")
|
||||||
|
}
|
||||||
|
DeviceManager.dbManager.removePlaybackSession(session.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cb(true, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun syncLocalMediaProgressForUser(cb: () -> Unit) {
|
||||||
|
// Get all local media progress for this server
|
||||||
|
val allLocalMediaProgress = DeviceManager.dbManager.getAllLocalMediaProgress().filter { it.serverConnectionConfigId == DeviceManager.serverConnectionConfigId }
|
||||||
|
if (allLocalMediaProgress.isEmpty()) {
|
||||||
|
Log.d(tag, "No local media progress to sync")
|
||||||
|
return cb()
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentUser { _user ->
|
||||||
|
_user?.let { user->
|
||||||
|
// Compare server user progress with local progress
|
||||||
|
user.mediaProgress.forEach { mediaProgress ->
|
||||||
|
// Get matching local media progress
|
||||||
|
allLocalMediaProgress.find { it.isMatch(mediaProgress) }?.let { localMediaProgress ->
|
||||||
|
if (mediaProgress.lastUpdate > localMediaProgress.lastUpdate) {
|
||||||
|
Log.d(tag, "Server progress for media item id=\"${mediaProgress.mediaItemId}\" is more recent then local. Updating local current time ${localMediaProgress.currentTime} to ${mediaProgress.currentTime}")
|
||||||
|
localMediaProgress.updateFromServerMediaProgress(mediaProgress)
|
||||||
|
MediaEventManager.syncEvent(mediaProgress, "Sync on server connection")
|
||||||
|
} else if (localMediaProgress.lastUpdate > mediaProgress.lastUpdate && localMediaProgress.ebookLocation != null && localMediaProgress.ebookLocation != mediaProgress.ebookLocation) {
|
||||||
|
// Patch ebook progress to server
|
||||||
|
val endpoint = "/api/me/progress/${localMediaProgress.libraryItemId}"
|
||||||
|
val updatePayload = JSObject()
|
||||||
|
updatePayload.put("ebookLocation", localMediaProgress.ebookLocation)
|
||||||
|
updatePayload.put("ebookProgress", localMediaProgress.ebookProgress)
|
||||||
|
updatePayload.put("lastUpdate", localMediaProgress.lastUpdate)
|
||||||
|
patchRequest(endpoint,updatePayload) {
|
||||||
|
Log.d(tag, "syncLocalMediaProgressForUser patched ebook progress")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cb()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="272"
|
||||||
|
android:viewportHeight="181.125"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<group android:scaleY="0.6659007"
|
||||||
|
android:translateY="30.256865">
|
||||||
|
<group android:translateY="144.77344">
|
||||||
|
<path android:pathData="M7,-50.96875Q7,-69.375,10.6875,-80.84375Q14.390625,-92.328125,21.671875,-98.15625Q28.953125,-104,40,-104Q48.140625,-104,54.53125,-100.625Q60.9375,-97.265625,64.6875,-90.921875Q68.4375,-84.59375,70.71875,-75.5Q73,-66.40625,73,-50.96875Q73,-32.703125,69.34375,-21.21875Q65.6875,-9.75,58.390625,-3.875Q51.109375,2,40,2Q25.359375,2,17.015625,-8.265625Q7,-21.71875,7,-50.96875ZM20,-51Q20,-25.828125,25.765625,-17.90625Q31.53125,-10,40,-10Q48.46875,-10,54.234375,-17.953125Q60,-25.90625,60,-51Q60,-75.734375,54.359375,-83.859375Q48.71875,-92,39.859375,-92Q31.40625,-92,26.09375,-84.828125Q20,-75.671875,20,-51Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.6"/>
|
||||||
|
<path android:pathData="M93,0L93,-13L106,-13L106,0L93,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.6"/>
|
||||||
|
<path android:pathData="M127,-27L140,-28Q141.42188,-19.03125,146.32812,-14.515625Q151.25,-10,159.04688,-10Q168.59375,-10,174.29688,-16.53125Q180,-23.0625,180,-33.859375Q180,-44.140625,174.70312,-50.0625Q169.42188,-56,159.5,-56Q153.29688,-56,148.95312,-53.5Q144.625,-51,142,-47L129,-49L140,-102L188,-102L188,-89L150.54688,-89L145.28125,-61.0625Q153.875,-68,163.3125,-68Q175.8125,-68,184.40625,-58.65625Q193,-49.328125,193,-34.65625Q193,-20.671875,185.125,-10.5Q175.54688,2,158.98438,2Q145.40625,2,136.8125,-5.9375Q128.21875,-13.875,127,-27Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.6"/>
|
||||||
|
<path android:pathData="M201,0L228.85938,-38L203,-75L218.57812,-75L230.82812,-57.484375Q234.23438,-52.609375,236.84375,-48.890625Q239.65625,-52.71875,242.28125,-56.296875L256,-75L271.875,-75L244.59375,-37.796875L271,0L255.42188,0L239.48438,-22.796875L236.60938,-26.90625L216.875,0L201,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.6"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="272"
|
||||||
|
android:viewportHeight="181.125"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<group android:scaleX="1.0434783"
|
||||||
|
android:scaleY="0.69485295"
|
||||||
|
android:translateX="-11.826087"
|
||||||
|
android:translateY="27.63488">
|
||||||
|
<group android:translateY="144.77344">
|
||||||
|
<path android:pathData="M54,-0L41,-0L41,-81.03125Q36.484375,-76.65625,29.15625,-72.265625Q21.828125,-67.890625,16,-65.703125L16,-78Q26.546875,-83.015625,34.4375,-90.140625Q42.34375,-97.28125,45.625,-104L54,-104L54,-0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M93,0L93,-13L106,-13L106,0L93,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M127,-50.96875Q127,-69.375,130.6875,-80.84375Q134.39062,-92.328125,141.67188,-98.15625Q148.95312,-104,160,-104Q168.14062,-104,174.53125,-100.625Q180.9375,-97.265625,184.6875,-90.921875Q188.4375,-84.59375,190.71875,-75.5Q193,-66.40625,193,-50.96875Q193,-32.703125,189.34375,-21.21875Q185.6875,-9.75,178.39062,-3.875Q171.10938,2,160,2Q145.35938,2,137.01562,-8.265625Q127,-21.71875,127,-50.96875ZM140,-51Q140,-25.828125,145.76562,-17.90625Q151.53125,-10,160,-10Q168.46875,-10,174.23438,-17.953125Q180,-25.90625,180,-51Q180,-75.734375,174.35938,-83.859375Q168.71875,-92,159.85938,-92Q151.40625,-92,146.09375,-84.828125Q140,-75.671875,140,-51Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M201,0L228.85938,-38L203,-75L218.57812,-75L230.82812,-57.484375Q234.23438,-52.609375,236.84375,-48.890625Q239.65625,-52.71875,242.28125,-56.296875L256,-75L271.875,-75L244.59375,-37.796875L271,0L255.42188,0L239.48438,-22.796875L236.60938,-26.90625L216.875,0L201,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="272"
|
||||||
|
android:viewportHeight="181.125"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<group android:scaleX="1.0434783"
|
||||||
|
android:scaleY="0.69485295"
|
||||||
|
android:translateX="-11.826087"
|
||||||
|
android:translateY="27.63488">
|
||||||
|
<group android:translateY="144.77344">
|
||||||
|
<path android:pathData="M54,-0L41,-0L41,-81.03125Q36.484375,-76.65625,29.15625,-72.265625Q21.828125,-67.890625,16,-65.703125L16,-78Q26.546875,-83.015625,34.4375,-90.140625Q42.34375,-97.28125,45.625,-104L54,-104L54,-0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M93,0L93,-13L106,-13L106,0L93,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M192,-13L192,0L124,0Q123.859375,-4.75,125.421875,-9.125Q127.921875,-16.359375,133.4375,-23.25Q138.96875,-30.140625,149.64062,-38.6875Q166.21875,-52.625,172.10938,-60.28125Q178,-67.9375,178,-75.015625Q178,-82.171875,172.70312,-87.078125Q167.40625,-92,158.875,-92Q149.875,-92,144.46875,-86.953125Q139.0625,-81.921875,139,-73L126,-75Q127.328125,-89.15625,135.95312,-96.578125Q144.59375,-104,159.15625,-104Q173.85938,-104,182.42188,-95.71875Q191,-87.4375,191,-75.203125Q191,-68.96875,188.59375,-62.953125Q186.20312,-56.9375,180.64062,-50.28125Q175.09375,-43.640625,161.73438,-32.375Q150.35938,-23.03125,146.96875,-19.6875Q143.59375,-16.359375,141.39062,-13L192,-13Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M201,0L228.85938,-38L203,-75L218.57812,-75L230.82812,-57.484375Q234.23438,-52.609375,236.84375,-48.890625Q239.65625,-52.71875,242.28125,-56.296875L256,-75L271.875,-75L244.59375,-37.796875L271,0L255.42188,0L239.48438,-22.796875L236.60938,-26.90625L216.875,0L201,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="272"
|
||||||
|
android:viewportHeight="181.125"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<group android:scaleX="1.0434783"
|
||||||
|
android:scaleY="0.69485295"
|
||||||
|
android:translateX="-11.826087"
|
||||||
|
android:translateY="27.63488">
|
||||||
|
<group android:translateY="144.77344">
|
||||||
|
<path android:pathData="M54,-0L41,-0L41,-81.03125Q36.484375,-76.65625,29.15625,-72.265625Q21.828125,-67.890625,16,-65.703125L16,-78Q26.546875,-83.015625,34.4375,-90.140625Q42.34375,-97.28125,45.625,-104L54,-104L54,-0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M93,0L93,-13L106,-13L106,0L93,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M127,-27L140,-28Q141.42188,-19.03125,146.32812,-14.515625Q151.25,-10,159.04688,-10Q168.59375,-10,174.29688,-16.53125Q180,-23.0625,180,-33.859375Q180,-44.140625,174.70312,-50.0625Q169.42188,-56,159.5,-56Q153.29688,-56,148.95312,-53.5Q144.625,-51,142,-47L129,-49L140,-102L188,-102L188,-89L150.54688,-89L145.28125,-61.0625Q153.875,-68,163.3125,-68Q175.8125,-68,184.40625,-58.65625Q193,-49.328125,193,-34.65625Q193,-20.671875,185.125,-10.5Q175.54688,2,158.98438,2Q145.40625,2,136.8125,-5.9375Q128.21875,-13.875,127,-27Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M201,0L228.85938,-38L203,-75L218.57812,-75L230.82812,-57.484375Q234.23438,-52.609375,236.84375,-48.890625Q239.65625,-52.71875,242.28125,-56.296875L256,-75L271.875,-75L244.59375,-37.796875L271,0L255.42188,0L239.48438,-22.796875L236.60938,-26.90625L216.875,0L201,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="272"
|
||||||
|
android:viewportHeight="181.125"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<group android:scaleY="0.6659007"
|
||||||
|
android:translateY="30.256865">
|
||||||
|
<group android:translateY="144.77344">
|
||||||
|
<path android:pathData="M72,-13L72,-0L4,-0Q3.859375,-4.75,5.421875,-9.125Q7.921875,-16.359375,13.4375,-23.25Q18.96875,-30.140625,29.640625,-38.6875Q46.21875,-52.625,52.109375,-60.28125Q58,-67.9375,58,-75.015625Q58,-82.171875,52.703125,-87.078125Q47.40625,-92,38.875,-92Q29.875,-92,24.46875,-86.953125Q19.0625,-81.921875,19,-73L6,-75Q7.328125,-89.15625,15.953125,-96.578125Q24.59375,-104,39.15625,-104Q53.859375,-104,62.421875,-95.71875Q71,-87.4375,71,-75.203125Q71,-68.96875,68.59375,-62.953125Q66.203125,-56.9375,60.640625,-50.28125Q55.09375,-43.640625,41.734375,-32.375Q30.359375,-23.03125,26.96875,-19.6875Q23.59375,-16.359375,21.390625,-13L72,-13Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M93,0L93,-13L106,-13L106,0L93,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M127,-50.96875Q127,-69.375,130.6875,-80.84375Q134.39062,-92.328125,141.67188,-98.15625Q148.95312,-104,160,-104Q168.14062,-104,174.53125,-100.625Q180.9375,-97.265625,184.6875,-90.921875Q188.4375,-84.59375,190.71875,-75.5Q193,-66.40625,193,-50.96875Q193,-32.703125,189.34375,-21.21875Q185.6875,-9.75,178.39062,-3.875Q171.10938,2,160,2Q145.35938,2,137.01562,-8.265625Q127,-21.71875,127,-50.96875ZM140,-51Q140,-25.828125,145.76562,-17.90625Q151.53125,-10,160,-10Q168.46875,-10,174.23438,-17.953125Q180,-25.90625,180,-51Q180,-75.734375,174.35938,-83.859375Q168.71875,-92,159.85938,-92Q151.40625,-92,146.09375,-84.828125Q140,-75.671875,140,-51Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M201,0L228.85938,-38L203,-75L218.57812,-75L230.82812,-57.484375Q234.23438,-52.609375,236.84375,-48.890625Q239.65625,-52.71875,242.28125,-56.296875L256,-75L271.875,-75L244.59375,-37.796875L271,0L255.42188,0L239.48438,-22.796875L236.60938,-26.90625L216.875,0L201,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="272"
|
||||||
|
android:viewportHeight="181.125"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<group android:scaleY="0.6659007"
|
||||||
|
android:translateY="30.256865">
|
||||||
|
<group android:translateY="144.77344">
|
||||||
|
<path android:pathData="M7,-27L20,-29Q22.109375,-18.90625,26.90625,-14.453125Q31.71875,-10,39.484375,-10Q48.609375,-10,54.296875,-16Q60,-22.015625,60,-30.90625Q60,-39.375,54.5,-44.890625Q49,-50.40625,40.515625,-50.40625Q37.046875,-50.40625,31.890625,-49L33.25,-61Q34.46875,-60.859375,35.203125,-60.859375Q42.8125,-60.859375,48.90625,-64.796875Q55,-68.75,55,-76.953125Q55,-83.46875,50.484375,-87.734375Q45.984375,-92,38.84375,-92Q31.765625,-92,27.046875,-87.75Q22.34375,-83.5,21,-75L8,-77Q10.28125,-89.859375,18.34375,-96.921875Q26.40625,-104,38.40625,-104Q46.6875,-104,53.65625,-100.390625Q60.625,-96.796875,64.3125,-90.578125Q68,-84.359375,68,-77.359375Q68,-70.71875,64.40625,-65.265625Q60.828125,-59.8125,53.796875,-56.59375Q62.9375,-54.453125,67.96875,-47.6875Q73,-40.9375,73,-30.796875Q73,-17.078125,63.375,-7.53125Q53.75,2,39.03125,2Q25.765625,2,17,-6.078125Q8.234375,-14.15625,7,-27Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M93,0L93,-13L106,-13L106,0L93,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M127,-50.96875Q127,-69.375,130.6875,-80.84375Q134.39062,-92.328125,141.67188,-98.15625Q148.95312,-104,160,-104Q168.14062,-104,174.53125,-100.625Q180.9375,-97.265625,184.6875,-90.921875Q188.4375,-84.59375,190.71875,-75.5Q193,-66.40625,193,-50.96875Q193,-32.703125,189.34375,-21.21875Q185.6875,-9.75,178.39062,-3.875Q171.10938,2,160,2Q145.35938,2,137.01562,-8.265625Q127,-21.71875,127,-50.96875ZM140,-51Q140,-25.828125,145.76562,-17.90625Q151.53125,-10,160,-10Q168.46875,-10,174.23438,-17.953125Q180,-25.90625,180,-51Q180,-75.734375,174.35938,-83.859375Q168.71875,-92,159.85938,-92Q151.40625,-92,146.09375,-84.828125Q140,-75.671875,140,-51Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
<path android:pathData="M201,0L228.85938,-38L203,-75L218.57812,-75L230.82812,-57.484375Q234.23438,-52.609375,236.84375,-48.890625Q239.65625,-52.71875,242.28125,-56.296875L256,-75L271.875,-75L244.59375,-37.796875L271,0L255.42188,0L239.48438,-22.796875L236.60938,-26.90625L216.875,0L201,0Z"
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:fillAlpha="0.8"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
|
After Width: | Height: | Size: 500 B |
|
After Width: | Height: | Size: 447 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 465 B |
|
After Width: | Height: | Size: 512 B |
|
After Width: | Height: | Size: 496 B |
|
After Width: | Height: | Size: 323 B |
|
After Width: | Height: | Size: 311 B |
|
After Width: | Height: | Size: 303 B |
|
After Width: | Height: | Size: 315 B |
|
After Width: | Height: | Size: 338 B |
|
After Width: | Height: | Size: 346 B |
|
Before Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 664 B |
|
After Width: | Height: | Size: 608 B |
|
After Width: | Height: | Size: 583 B |
|
After Width: | Height: | Size: 599 B |
|
After Width: | Height: | Size: 684 B |
|
After Width: | Height: | Size: 712 B |
|
After Width: | Height: | Size: 1013 B |
|
After Width: | Height: | Size: 895 B |
|
After Width: | Height: | Size: 866 B |
|
After Width: | Height: | Size: 892 B |
|
After Width: | Height: | Size: 1011 B |
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="862dp"
|
||||||
|
android:height="1024dp"
|
||||||
|
android:viewportWidth="862"
|
||||||
|
android:viewportHeight="1024">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M855,504.4c-5.3,-4.4 -13.5,-10.9 -24.7,-18.6v-86.5c0,-220.5 -178.8,-399.3 -399.3,-399.3v0c-220.5,0 -399.3,178.8 -399.3,399.3v86.5c-11.1,7.7 -19.4,14.2 -24.7,18.6 -4.4,3.7 -7.1,9.1 -7.1,15.2 0,0 0,0 0,0v-0,103.6c0,0 0,0 0,0 0,6.1 2.8,11.6 7.1,15.2l0,0c12.3,10.3 40.6,31.8 84.7,53.8v10.1c0,27.2 17.5,49.2 39.1,49.2v0c21.6,0 39.1,-22 39.1,-49.2v-248.5c0,-27.2 -17.5,-49.2 -39.1,-49.2v0c-20.7,0 -37.7,20.2 -39,45.8l-0.1,0v-51.2c0,-187.4 151.9,-339.3 339.3,-339.3v0c187.4,0 339.3,151.9 339.3,339.3v51.2l-0.1,-0c-1.4,-25.6 -18.3,-45.8 -39,-45.8v0c-21.6,0 -39.1,22 -39.1,49.2v248.5c0,27.2 17.5,49.2 39.1,49.2v0c21.6,0 39.1,-22 39.1,-49.2v-10.1c44,-22.1 72.3,-43.6 84.7,-53.8 4.4,-3.7 7.1,-9.1 7.1,-15.2 0,-0 0,-0 0,-0v0,-103.6c0,-0 0,-0 0,-0 0,-6.1 -2.8,-11.6 -7.1,-15.2l-0,-0zM291.8,936.4c26.1,0 47.3,-21.2 47.3,-47.3v0,-529.4c0,-0 0,-0 0,-0 0,-26.1 -21.2,-47.3 -47.3,-47.3h-48.8c-26.1,0 -47.3,21.2 -47.3,47.3v0,529.4c0,26.1 21.2,47.3 47.3,47.3 0,0 0,0 0,0v0zM213.5,439.4h107.8v11.3h-107.8zM455.5,936.4c26.1,0 47.3,-21.2 47.3,-47.3v0,-529.4c0,-0 0,-0 0,-0 0,-26.1 -21.2,-47.3 -47.3,-47.3h-48.8c-26.1,0 -47.3,21.2 -47.3,47.3v0,529.4c0,26.1 21.2,47.3 47.3,47.3 0,0 0,0 0,0v0zM377.2,439.4h107.8v11.3h-107.8zM619.1,936.4c26.1,0 47.3,-21.2 47.3,-47.3v0,-529.4c0,-0 0,-0 0,-0 0,-26.1 -21.2,-47.3 -47.3,-47.3h-48.8c-26.1,0 -47.3,21.2 -47.3,47.3v0,529.4c0,26.1 21.2,47.3 47.3,47.3 0,0 0,0 0,0v0zM540.8,439.4h107.8v11.3h-107.8zM114.1,952.7h633.9c19.7,0 35.6,15.9 35.6,35.6v0c0,19.7 -15.9,35.6 -35.6,35.6h-633.9c-19.7,0 -35.6,-15.9 -35.6,-35.6v-0c0,-19.7 15.9,-35.6 35.6,-35.6z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:id="@+id/widgetBackground"
|
||||||
|
android:layout_width="fill_parent"
|
||||||
|
android:layout_height="fill_parent"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:theme="@style/AppTheme.AppWidgetContainer"
|
||||||
|
style="@style/Widget.Android.AppWidget.Container">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="3"
|
||||||
|
android:background="?android:attr/colorAccent"
|
||||||
|
android:theme="@style/MediaPlayerWidgetTheme"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/widgetAlbumArt"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="2"
|
||||||
|
android:scaleType="centerCrop"
|
||||||
|
android:src="@drawable/icon" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="3"
|
||||||
|
android:layout_marginVertical="8dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/widgetArtistText"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="2"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:gravity="bottom"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="Artist"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/widgetMediaTitle"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="2"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:gravity="top"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="Title"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/widgetButtonContainer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="3"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:background="?android:attr/colorAccent"
|
||||||
|
android:theme="@style/MediaPlayerWidgetTheme">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/widgetRewindButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:src="@drawable/exo_icon_rewind"
|
||||||
|
style="@style/Widget.Android.AppWidget.InnerView"/>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/widgetPlayPauseButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_margin="6dp"
|
||||||
|
android:src="@drawable/ic_media_play_dark" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/widgetFastForwardButton"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:src="@drawable/exo_icon_fastforward" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="36dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/tinyCornerIcon"
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:layout_margin="6dp"
|
||||||
|
android:adjustViewBounds ="true"
|
||||||
|
android:src="@drawable/icon_monochrome" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
style="@style/Widget.Android.AppWidget.Container"
|
|
||||||
android:id="@+id/appWidget"
|
|
||||||
android:theme="@style/AppTheme.AppWidgetContainer"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/imageView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_centerInParent="true"
|
|
||||||
android:adjustViewBounds="true"
|
|
||||||
android:alpha="0.75"
|
|
||||||
android:contentDescription="Cover image"
|
|
||||||
android:visibility="visible" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/playPauseIcon"
|
|
||||||
android:layout_width="71dp"
|
|
||||||
android:layout_height="55dp"
|
|
||||||
android:layout_centerInParent="true"
|
|
||||||
app:srcCompat="@drawable/cast_ic_expanded_controller_play" />
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!--
|
||||||
|
Having themes.xml for night-v31 because of the priority order of the resource qualifiers.
|
||||||
|
-->
|
||||||
|
<style name="AppTheme.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault.DayNight">
|
||||||
|
<item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item>
|
||||||
|
<item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<resources>
|
||||||
|
|
||||||
|
<style name="Widget.Android.AppWidget.Container" parent="android:Widget">
|
||||||
|
<item name="android:id">@android:id/background</item>
|
||||||
|
<item name="android:padding">?attr/appWidgetPadding</item>
|
||||||
|
<item name="android:background">@drawable/app_widget_background</item>
|
||||||
|
<item name="android:clipToOutline">true</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Widget.Android.AppWidget.InnerView" parent="android:Widget">
|
||||||
|
<item name="android:padding">?attr/appWidgetPadding</item>
|
||||||
|
<!-- <item name="android:background">@drawable/app_widget_inner_view_background</item>-->
|
||||||
|
<!-- <item name="android:textColor">?android:attr/textColorPrimary</item>-->
|
||||||
|
<item name="android:clipToOutline">true</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!--
|
||||||
|
Having themes.xml for v31 variant because @android:dimen/system_app_widget_background_radius
|
||||||
|
and @android:dimen/system_app_widget_internal_padding requires API level 31
|
||||||
|
-->
|
||||||
|
<style name="AppTheme.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault.DayNight">
|
||||||
|
<item name="appWidgetRadius">@android:dimen/system_app_widget_background_radius</item>
|
||||||
|
<item name="appWidgetInnerRadius">@android:dimen/system_app_widget_inner_radius</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<declare-styleable name="AppWidgetAttrs">
|
<declare-styleable name="AppWidgetAttrs">
|
||||||
<attr name="appWidgetPadding" format="dimension" />
|
<attr name="appWidgetPadding" format="dimension" />
|
||||||
<attr name="appWidgetInnerRadius" format="dimension" />
|
<attr name="appWidgetInnerRadius" format="dimension" />
|
||||||
<attr name="appWidgetRadius" format="dimension" />
|
<attr name="appWidgetRadius" format="dimension" />
|
||||||
</declare-styleable>
|
<attr name="backgroundRadius" format="dimension" />
|
||||||
</resources>
|
<attr name="appWidgetViewPadding" format="dimension" />
|
||||||
|
</declare-styleable>
|
||||||
|
</resources>
|
||||||
|
|||||||
@@ -10,4 +10,5 @@
|
|||||||
<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_forward">Skip Forward</string>
|
||||||
<string name="action_skip_backward">Skip Backward</string>
|
<string name="action_skip_backward">Skip Backward</string>
|
||||||
|
<string name="action_change_speed">Change Playback Speed</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,33 +1,42 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||||
<!-- Customize your theme here. -->
|
<!-- Customize your theme here. -->
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
<item name="colorAccent">@color/colorAccent</item>
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.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:statusBarColor">@color/background_dark</item>
|
||||||
<item name="android:navigationBarColor">@color/background_dark</item>
|
<item name="android:navigationBarColor">@color/background_dark</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|
||||||
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
<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">
|
|
||||||
<item name="android:id">@android:id/background</item>
|
|
||||||
<item name="android:background">?android:attr/colorBackground</item>
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style name="Widget.Android.AppWidget.InnerView" parent="android:Widget">
|
<style name="MediaPlayerWidgetTheme" parent="Theme.AppCompat.DayNight">
|
||||||
<item name="android:background">?android:attr/colorBackground</item>
|
<!-- These colors match the vue pages -->
|
||||||
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
<item name="android:colorBackground">#232323</item>
|
||||||
</style>
|
<item name="android:colorAccent">#373838</item>
|
||||||
|
<item name="android:textColor">#ffffff</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Widget.Android.AppWidget.Container" parent="android:Widget">
|
||||||
|
<item name="android:id">@android:id/background</item>
|
||||||
|
<item name="android:background">?android:attr/colorBackground</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Widget.Android.AppWidget.InnerView" parent="android:Widget">
|
||||||
|
<!-- <item name="android:background">?android:attr/colorBackground</item>-->
|
||||||
|
<!-- <item name="android:textColor">?android:attr/textColorPrimary</item>-->
|
||||||
|
<item name="android:padding">0dp</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<style name="AppTheme.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault">
|
|
||||||
<!-- Radius of the outer bound of widgets to make the rounded corners -->
|
<style name="AppTheme.AppWidgetContainerParent" parent="@android:style/Theme.DeviceDefault">
|
||||||
<item name="appWidgetRadius">16dp</item>
|
<!-- Radius of the outer bound of widgets to make the rounded corners -->
|
||||||
<!--
|
<item name="appWidgetRadius">0dp</item>
|
||||||
|
<!--
|
||||||
Radius of the inner view's bound of widgets to make the rounded corners.
|
Radius of the inner view's bound of widgets to make the rounded corners.
|
||||||
It needs to be 8dp or less than the value of appWidgetRadius
|
It needs to be 8dp or less than the value of appWidgetRadius
|
||||||
-->
|
-->
|
||||||
<item name="appWidgetInnerRadius">8dp</item>
|
<item name="appWidgetInnerRadius">0dp</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="AppTheme.AppWidgetContainer"
|
<style name="AppTheme.AppWidgetContainer" parent="AppTheme.AppWidgetContainerParent">
|
||||||
parent="AppTheme.AppWidgetContainerParent">
|
<!-- Apply padding to avoid the content of the widget colliding with the rounded corners -->
|
||||||
<!-- Apply padding to avoid the content of the widget colliding with the rounded corners -->
|
<item name="appWidgetPadding">0dp</item>
|
||||||
<item name="appWidgetPadding">16dp</item>
|
</style>
|
||||||
</style>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
<paths>
|
||||||
<external-path name="my_images" path="." />
|
<files-path name="downloads" path="downloads/" />
|
||||||
<cache-path name="my_cache_images" path="." />
|
</paths>
|
||||||
</paths>
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:description="@string/app_widget_description"
|
||||||
|
android:initialKeyguardLayout="@layout/media_player_widget"
|
||||||
|
android:initialLayout="@layout/media_player_widget"
|
||||||
|
android:minWidth="275dp"
|
||||||
|
android:minHeight="50dp"
|
||||||
|
android:previewImage="@drawable/media_player_widget_preview"
|
||||||
|
android:previewLayout="@layout/media_player_widget"
|
||||||
|
android:resizeMode="horizontal"
|
||||||
|
android:targetCellWidth="4"
|
||||||
|
android:targetCellHeight="1"
|
||||||
|
android:updatePeriodMillis="86400000"
|
||||||
|
android:widgetCategory="home_screen" />
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:minWidth="110dp"
|
|
||||||
android:minHeight="40dp"
|
|
||||||
android:updatePeriodMillis="86400000"
|
|
||||||
android:previewImage="@drawable/example_appwidget_preview"
|
|
||||||
android:initialLayout="@layout/new_app_widget"
|
|
||||||
android:description="@string/app_widget_description"
|
|
||||||
android:resizeMode="horizontal|vertical"
|
|
||||||
android:widgetCategory="home_screen"
|
|
||||||
android:initialKeyguardLayout="@layout/new_app_widget"
|
|
||||||
/>
|
|
||||||
@@ -5,6 +5,9 @@ project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/
|
|||||||
include ':capacitor-app'
|
include ':capacitor-app'
|
||||||
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
||||||
|
|
||||||
|
include ':capacitor-clipboard'
|
||||||
|
project(':capacitor-clipboard').projectDir = new File('../node_modules/@capacitor/clipboard/android')
|
||||||
|
|
||||||
include ':capacitor-dialog'
|
include ':capacitor-dialog'
|
||||||
project(':capacitor-dialog').projectDir = new File('../node_modules/@capacitor/dialog/android')
|
project(':capacitor-dialog').projectDir = new File('../node_modules/@capacitor/dialog/android')
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
ext {
|
ext {
|
||||||
minSdkVersion = 24
|
minSdkVersion = 24
|
||||||
compileSdkVersion = 33
|
compileSdkVersion = 33
|
||||||
targetSdkVersion = 32
|
targetSdkVersion = 33
|
||||||
androidxActivityVersion = '1.4.0'
|
androidxActivityVersion = '1.4.0'
|
||||||
androidxAppCompatVersion = '1.4.2'
|
androidxAppCompatVersion = '1.4.2'
|
||||||
androidxCoordinatorLayoutVersion = '1.2.0'
|
androidxCoordinatorLayoutVersion = '1.2.0'
|
||||||
@@ -14,13 +14,13 @@ ext {
|
|||||||
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'
|
||||||
androidx_media_version = '1.5.0'
|
androidx_media_version = '1.6.0'
|
||||||
androidx_preference_version = '1.1.1'
|
androidx_preference_version = '1.1.1'
|
||||||
androidx_test_runner_version = '1.3.0'
|
androidx_test_runner_version = '1.3.0'
|
||||||
arch_lifecycle_version = '2.2.0'
|
arch_lifecycle_version = '2.2.0'
|
||||||
constraint_layout_version = '2.0.1'
|
constraint_layout_version = '2.0.1'
|
||||||
espresso_version = '3.3.0'
|
espresso_version = '3.3.0'
|
||||||
exoplayer_version = '2.17.0'
|
exoplayer_version = '2.18.7'
|
||||||
fragment_version = '1.2.5'
|
fragment_version = '1.2.5'
|
||||||
glide_version = '4.11.0'
|
glide_version = '4.11.0'
|
||||||
gms_strict_version_matcher_version = '1.0.3'
|
gms_strict_version_matcher_version = '1.0.3'
|
||||||
|
|||||||
@@ -2,6 +2,19 @@
|
|||||||
@import './defaultStyles.css';
|
@import './defaultStyles.css';
|
||||||
@import './absicons.css';
|
@import './absicons.css';
|
||||||
|
|
||||||
|
* {
|
||||||
|
-webkit-touch-callout: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input,
|
||||||
|
textarea {
|
||||||
|
-webkit-touch-callout: auto;
|
||||||
|
-webkit-user-select: auto;
|
||||||
|
user-select: auto;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background-color: #262626;
|
background-color: #262626;
|
||||||
}
|
}
|
||||||
@@ -20,9 +33,9 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#content.playerOpen {
|
#content.playerOpen {
|
||||||
height: calc(100% - 164px);
|
height: calc(100% - 184px);
|
||||||
min-height: calc(100% - 164px);
|
min-height: calc(100% - 184px);
|
||||||
max-height: calc(100% - 164px);
|
max-height: calc(100% - 184px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#bookshelf {
|
#bookshelf {
|
||||||
@@ -102,4 +115,55 @@ Bookshelf Label
|
|||||||
background-position: center;
|
background-position: center;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
filter: blur(20px);
|
filter: blur(20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.episode-subtitle {
|
||||||
|
word-break: break-word;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
line-height: 16px;
|
||||||
|
/* fallback */
|
||||||
|
max-height: 32px;
|
||||||
|
/* fallback */
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
/* number of lines to show */
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-clamp-2 {
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-clamp-4 {
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracksTable {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #474747;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracksTable tr:nth-child(even) {
|
||||||
|
background-color: #2e2e2e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracksTable tr {
|
||||||
|
background-color: #373838;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracksTable td {
|
||||||
|
padding: 8px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tracksTable th {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
@@ -48,25 +48,6 @@
|
|||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Gentium Book Basic';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url(/fonts/GentiumBookBasic.woff2) format('woff2');
|
|
||||||
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* latin */
|
|
||||||
@font-face {
|
|
||||||
font-family: 'Gentium Book Basic';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
font-display: swap;
|
|
||||||
src: url(/fonts/GentiumBookBasic.woff2) format('woff2');
|
|
||||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/* cyrillic-ext */
|
/* cyrillic-ext */
|
||||||
@font-face {
|
@font-face {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<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">
|
||||||
<ui-library-icon :icon="currentLibraryIcon" :size="4" font-size="base" />
|
<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-smleading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ export default {
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
castClick() {
|
castClick() {
|
||||||
if (this.$store.state.playerIsLocal) {
|
if (this.$store.getters['getIsCurrentSessionLocal']) {
|
||||||
this.$eventBus.$emit('cast-local-item')
|
this.$eventBus.$emit('cast-local-item')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,27 +12,27 @@
|
|||||||
<div class="top-6 right-4 absolute cursor-pointer">
|
<div class="top-6 right-4 absolute cursor-pointer">
|
||||||
<span class="material-icons text-3xl" :class="{ 'text-black text-opacity-75': coverBgIsLight }" @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-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>
|
<p class="top-4 absolute left-0 right-0 mx-auto text-center uppercase tracking-widest text-opacity-75" :class="{ 'text-black text-opacity-75': coverBgIsLight }" style="font-size: 10px">{{ isDirectPlayMethod ? 'Direct' : isLocalPlayMethod ? 'Local' : 'Transcode' }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="useChapterTrack && useTotalTrack && showFullscreen" class="absolute total-track w-full px-3 z-30">
|
<div v-if="useChapterTrack && useTotalTrack && showFullscreen" class="absolute total-track w-full z-30 px-6">
|
||||||
<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" />
|
||||||
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ totalTimeRemainingPretty }}</p>
|
<p class="font-mono text-white text-opacity-90" style="font-size: 0.8rem">{{ totalTimeRemainingPretty }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<div class="h-1 w-full bg-gray-500 bg-opacity-50 relative">
|
<div class="h-1 w-full bg-gray-500 bg-opacity-50 relative rounded-full">
|
||||||
<div ref="totalReadyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none" />
|
<div ref="totalReadyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none rounded-full" />
|
||||||
<div ref="totalBufferedTrack" class="h-full bg-gray-500 absolute top-0 left-0 pointer-events-none" />
|
<div ref="totalBufferedTrack" class="h-full bg-gray-500 absolute top-0 left-0 pointer-events-none rounded-full" />
|
||||||
<div ref="totalPlayedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
|
<div ref="totalPlayedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none rounded-full" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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="w-full h-full flex justify-center">
|
<div class="w-full h-full flex justify-center">
|
||||||
<covers-book-cover v-if="libraryItem || localLibraryItemCoverSrc" ref="cover" :library-item="libraryItem" :download-cover="localLibraryItemCoverSrc" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" @imageLoaded="coverImageLoaded" />
|
<covers-book-cover v-if="libraryItem || localLibraryItemCoverSrc" ref="cover" :library-item="libraryItem" :download-cover="localLibraryItemCoverSrc" :width="bookCoverWidth" :book-cover-aspect-ratio="bookCoverAspectRatio" raw @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">
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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 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 bottom-4 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 pb-4 pt-2 mx-auto px-6" style="max-width: 414px">
|
||||||
<div class="flex items-center justify-between pointer-events-auto">
|
<div class="flex items-center justify-between pointer-events-auto">
|
||||||
<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>
|
<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 -->
|
||||||
@@ -60,13 +60,13 @@
|
|||||||
<p class="text-xl font-mono text-success">{{ sleepTimeRemainingPretty }}</p>
|
<p class="text-xl font-mono text-success">{{ sleepTimeRemainingPretty }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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="clickChaptersBtn">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 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 mx-auto" style="max-width: 414px">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center max-w-full" :class="lockUi ? 'justify-center' : 'justify-between'">
|
||||||
<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 relative overflow-hidden" :style="{ backgroundColor: coverRgb }" :class="{ 'animate-spin': seekLoading }" @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">
|
||||||
@@ -80,25 +80,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="playerTrack" class="absolute left-0 w-full px-3">
|
<div id="playerTrack" class="absolute left-0 w-full px-6">
|
||||||
<div class="flex pb-0.5">
|
<div class="flex pointer-events-none">
|
||||||
<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>
|
|
||||||
<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="track" class="h-1.5 w-full bg-gray-500 bg-opacity-50 relative rounded-full" :class="{ 'animate-pulse': isLoading }" @click.stop>
|
||||||
<div ref="readyTrack" class="h-full bg-gray-600 absolute top-0 left-0 pointer-events-none" />
|
<div ref="readyTrack" class="h-full bg-gray-600 absolute top-0 left-0 rounded-full pointer-events-none" />
|
||||||
<div ref="bufferedTrack" class="h-full bg-gray-500 absolute top-0 left-0 pointer-events-none" />
|
<div ref="bufferedTrack" class="h-full bg-gray-500 absolute top-0 left-0 rounded-full pointer-events-none" />
|
||||||
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 pointer-events-none" />
|
<div ref="playedTrack" class="h-full bg-gray-200 absolute top-0 left-0 rounded-full 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-7 w-7 rounded-full absolute pointer-events-auto flex items-center justify-center" :style="{ top: '-11px' }" :class="{ 'opacity-0': lockUi || !showFullscreen }" @touchstart="touchstartCursor">
|
||||||
<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 class="bg-gray-200 rounded-full w-3.5 h-3.5 pointer-events-none" />
|
||||||
|
</div>
|
||||||
</div>
|
</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" :playback-rate="currentPlaybackRate" @select="selectChapter" />
|
||||||
<modals-dialog v-model="showMoreMenuDialog" :items="menuItems" @action="clickMenuAction" />
|
<modals-dialog v-model="showMoreMenuDialog" :items="menuItems" @action="clickMenuAction" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -143,6 +142,7 @@ export default {
|
|||||||
onMetadataListener: null,
|
onMetadataListener: null,
|
||||||
onProgressSyncFailing: null,
|
onProgressSyncFailing: null,
|
||||||
onProgressSyncSuccess: null,
|
onProgressSyncSuccess: null,
|
||||||
|
onPlaybackSpeedChangedListener: null,
|
||||||
touchStartY: 0,
|
touchStartY: 0,
|
||||||
touchStartTime: 0,
|
touchStartTime: 0,
|
||||||
touchEndY: 0,
|
touchEndY: 0,
|
||||||
@@ -150,8 +150,10 @@ export default {
|
|||||||
useTotalTrack: true,
|
useTotalTrack: true,
|
||||||
lockUi: false,
|
lockUi: false,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
touchTrackStart: false,
|
isDraggingCursor: false,
|
||||||
dragPercent: 0,
|
draggingTouchStartX: 0,
|
||||||
|
draggingTouchStartTime: 0,
|
||||||
|
draggingCurrentTime: 0,
|
||||||
syncStatus: 0,
|
syncStatus: 0,
|
||||||
showMoreMenuDialog: false,
|
showMoreMenuDialog: false,
|
||||||
coverRgb: 'rgb(55, 56, 56)',
|
coverRgb: 'rgb(55, 56, 56)',
|
||||||
@@ -272,7 +274,7 @@ export default {
|
|||||||
return this.playbackSession ? this.playbackSession.localLibraryItem || null : null
|
return this.playbackSession ? this.playbackSession.localLibraryItem || null : null
|
||||||
},
|
},
|
||||||
localLibraryItemCoverSrc() {
|
localLibraryItemCoverSrc() {
|
||||||
var localItemCover = this.localLibraryItem ? this.localLibraryItem.coverContentUrl : null
|
var localItemCover = this.localLibraryItem?.coverContentUrl || null
|
||||||
if (localItemCover) return Capacitor.convertFileSrc(localItemCover)
|
if (localItemCover) return Capacitor.convertFileSrc(localItemCover)
|
||||||
return null
|
return null
|
||||||
},
|
},
|
||||||
@@ -286,6 +288,7 @@ export default {
|
|||||||
return this.playMethod == this.$constants.PlayMethod.DIRECTPLAY
|
return this.playMethod == this.$constants.PlayMethod.DIRECTPLAY
|
||||||
},
|
},
|
||||||
title() {
|
title() {
|
||||||
|
if (this.currentChapterTitle && this.showFullscreen) return this.currentChapterTitle
|
||||||
if (this.playbackSession) return this.playbackSession.displayTitle
|
if (this.playbackSession) return this.playbackSession.displayTitle
|
||||||
return this.mediaMetadata ? this.mediaMetadata.title : 'Title'
|
return this.mediaMetadata ? this.mediaMetadata.title : 'Title'
|
||||||
},
|
},
|
||||||
@@ -317,17 +320,20 @@ export default {
|
|||||||
return this.$secondsToTimestamp(this.totalDuration)
|
return this.$secondsToTimestamp(this.totalDuration)
|
||||||
},
|
},
|
||||||
currentTimePretty() {
|
currentTimePretty() {
|
||||||
return this.$secondsToTimestamp(this.currentTime)
|
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
|
||||||
|
return this.$secondsToTimestamp(currentTimeToUse / this.currentPlaybackRate)
|
||||||
},
|
},
|
||||||
timeRemaining() {
|
timeRemaining() {
|
||||||
|
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
|
||||||
if (this.useChapterTrack && this.currentChapter) {
|
if (this.useChapterTrack && this.currentChapter) {
|
||||||
var currChapTime = this.currentTime - this.currentChapter.start
|
var currChapTime = currentTimeToUse - this.currentChapter.start
|
||||||
return (this.currentChapterDuration - currChapTime) / this.currentPlaybackRate
|
return (this.currentChapterDuration - currChapTime) / this.currentPlaybackRate
|
||||||
}
|
}
|
||||||
return this.totalTimeRemaining
|
return this.totalTimeRemaining
|
||||||
},
|
},
|
||||||
totalTimeRemaining() {
|
totalTimeRemaining() {
|
||||||
return (this.totalDuration - this.currentTime) / this.currentPlaybackRate
|
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
|
||||||
|
return (this.totalDuration - currentTimeToUse) / this.currentPlaybackRate
|
||||||
},
|
},
|
||||||
totalTimeRemainingPretty() {
|
totalTimeRemainingPretty() {
|
||||||
if (this.totalTimeRemaining < 0) {
|
if (this.totalTimeRemaining < 0) {
|
||||||
@@ -341,10 +347,6 @@ export default {
|
|||||||
}
|
}
|
||||||
return '-' + this.$secondsToTimestamp(this.timeRemaining)
|
return '-' + this.$secondsToTimestamp(this.timeRemaining)
|
||||||
},
|
},
|
||||||
timeLeftInChapter() {
|
|
||||||
if (!this.currentChapter) return 0
|
|
||||||
return this.currentChapter.end - this.currentTime
|
|
||||||
},
|
|
||||||
sleepTimeRemainingPretty() {
|
sleepTimeRemainingPretty() {
|
||||||
if (!this.sleepTimeRemaining) return '0s'
|
if (!this.sleepTimeRemaining) return '0s'
|
||||||
var secondsRemaining = Math.round(this.sleepTimeRemaining)
|
var secondsRemaining = Math.round(this.sleepTimeRemaining)
|
||||||
@@ -369,6 +371,10 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
clickChaptersBtn() {
|
||||||
|
if (!this.chapters.length) return
|
||||||
|
this.showChapterModal = true
|
||||||
|
},
|
||||||
async coverImageLoaded(fullCoverUrl) {
|
async coverImageLoaded(fullCoverUrl) {
|
||||||
if (!fullCoverUrl) return
|
if (!fullCoverUrl) return
|
||||||
|
|
||||||
@@ -391,11 +397,6 @@ export default {
|
|||||||
this.showFullscreen = false
|
this.showFullscreen = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async touchstartTrack(e) {
|
|
||||||
await this.$hapticsImpact()
|
|
||||||
if (!e || !e.touches || !this.$refs.track || !this.showFullscreen || this.lockUi) return
|
|
||||||
this.touchTrackStart = true
|
|
||||||
},
|
|
||||||
async selectChapter(chapter) {
|
async selectChapter(chapter) {
|
||||||
await this.$hapticsImpact()
|
await this.$hapticsImpact()
|
||||||
this.seek(chapter.start)
|
this.seek(chapter.start)
|
||||||
@@ -449,9 +450,9 @@ export default {
|
|||||||
this.$emit('showSleepTimer')
|
this.$emit('showSleepTimer')
|
||||||
},
|
},
|
||||||
async 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
|
||||||
|
this.updateTimestamp()
|
||||||
AbsAudioPlayer.setPlaybackSpeed({ value: speed })
|
AbsAudioPlayer.setPlaybackSpeed({ value: speed })
|
||||||
},
|
},
|
||||||
restart() {
|
restart() {
|
||||||
@@ -502,19 +503,18 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
updateTimestamp() {
|
updateTimestamp() {
|
||||||
var ts = this.$refs.currentTimestamp
|
const ts = this.$refs.currentTimestamp
|
||||||
if (!ts) {
|
if (!ts) {
|
||||||
console.error('No timestamp el')
|
console.error('No timestamp el')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var currTimeStr = ''
|
|
||||||
|
let currentTime = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
|
||||||
if (this.useChapterTrack && this.currentChapter) {
|
if (this.useChapterTrack && this.currentChapter) {
|
||||||
var currChapTime = Math.max(0, this.currentTime - this.currentChapter.start)
|
currentTime = Math.max(0, currentTime - this.currentChapter.start)
|
||||||
currTimeStr = this.$secondsToTimestamp(currChapTime)
|
|
||||||
} else {
|
|
||||||
currTimeStr = this.$secondsToTimestamp(this.currentTime)
|
|
||||||
}
|
}
|
||||||
ts.innerText = currTimeStr
|
|
||||||
|
ts.innerText = this.$secondsToTimestamp(currentTime / this.currentPlaybackRate)
|
||||||
},
|
},
|
||||||
timeupdate() {
|
timeupdate() {
|
||||||
if (!this.$refs.playedTrack) {
|
if (!this.$refs.playedTrack) {
|
||||||
@@ -536,22 +536,24 @@ export default {
|
|||||||
},
|
},
|
||||||
updateTrack() {
|
updateTrack() {
|
||||||
// Update progress track UI
|
// Update progress track UI
|
||||||
let percentDone = this.currentTime / this.totalDuration
|
let currentTimeToUse = this.isDraggingCursor ? this.draggingCurrentTime : this.currentTime
|
||||||
|
let percentDone = currentTimeToUse / this.totalDuration
|
||||||
const totalPercentDone = percentDone
|
const totalPercentDone = percentDone
|
||||||
let bufferedPercent = this.bufferedTime / this.totalDuration
|
let bufferedPercent = this.bufferedTime / this.totalDuration
|
||||||
const totalBufferedPercent = bufferedPercent
|
const totalBufferedPercent = bufferedPercent
|
||||||
|
|
||||||
if (this.useChapterTrack && this.currentChapter) {
|
if (this.useChapterTrack && this.currentChapter) {
|
||||||
const currChapTime = this.currentTime - this.currentChapter.start
|
const currChapTime = currentTimeToUse - 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))
|
||||||
}
|
}
|
||||||
|
|
||||||
const 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'
|
||||||
|
|
||||||
if (this.$refs.trackCursor) {
|
if (this.$refs.trackCursor) {
|
||||||
this.$refs.trackCursor.style.left = ptWidth - 8 + 'px'
|
this.$refs.trackCursor.style.left = ptWidth - 14 + 'px'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.useChapterTrack) {
|
if (this.useChapterTrack) {
|
||||||
@@ -572,35 +574,23 @@ export default {
|
|||||||
AbsAudioPlayer.seek({ value: Math.floor(time) })
|
AbsAudioPlayer.seek({ value: Math.floor(time) })
|
||||||
|
|
||||||
if (this.$refs.playedTrack) {
|
if (this.$refs.playedTrack) {
|
||||||
var perc = time / this.totalDuration
|
const perc = time / this.totalDuration
|
||||||
var ptWidth = Math.round(perc * this.trackWidth)
|
const ptWidth = Math.round(perc * this.trackWidth)
|
||||||
this.$refs.playedTrack.style.width = ptWidth + 'px'
|
this.$refs.playedTrack.style.width = ptWidth + 'px'
|
||||||
|
|
||||||
this.$refs.playedTrack.classList.remove('bg-gray-200')
|
this.$refs.playedTrack.classList.remove('bg-gray-200')
|
||||||
this.$refs.playedTrack.classList.add('bg-yellow-300')
|
this.$refs.playedTrack.classList.add('bg-yellow-300')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clickTrack(e) {
|
async touchstartCursor(e) {
|
||||||
if (this.isLoading || this.lockUi) return
|
if (!e || !e.touches || !this.$refs.track || !this.showFullscreen || this.lockUi) return
|
||||||
if (!this.showFullscreen) {
|
|
||||||
// Track not clickable on mini-player
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (e) e.stopPropagation()
|
|
||||||
|
|
||||||
var offsetX = e.offsetX
|
await this.$hapticsImpact()
|
||||||
var perc = offsetX / this.trackWidth
|
this.isDraggingCursor = true
|
||||||
var time = 0
|
this.draggingTouchStartX = e.touches[0].pageX
|
||||||
if (this.useChapterTrack && this.currentChapter) {
|
this.draggingTouchStartTime = this.currentTime
|
||||||
time = perc * this.currentChapterDuration + this.currentChapter.start
|
this.draggingCurrentTime = this.currentTime
|
||||||
} else {
|
this.updateTrack()
|
||||||
time = perc * this.totalDuration
|
|
||||||
}
|
|
||||||
if (isNaN(time) || time === null) {
|
|
||||||
console.error('Invalid time', perc, time)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.seek(time)
|
|
||||||
},
|
},
|
||||||
async playPauseClick() {
|
async playPauseClick() {
|
||||||
await this.$hapticsImpact()
|
await this.$hapticsImpact()
|
||||||
@@ -653,24 +643,11 @@ export default {
|
|||||||
touchend(e) {
|
touchend(e) {
|
||||||
if (!e.changedTouches) return
|
if (!e.changedTouches) return
|
||||||
|
|
||||||
if (this.touchTrackStart) {
|
if (this.isDraggingCursor) {
|
||||||
var touch = e.changedTouches[0]
|
if (this.draggingCurrentTime !== this.currentTime) {
|
||||||
const touchOnTrackPos = touch.pageX - 12
|
this.seek(this.draggingCurrentTime)
|
||||||
const dragPercent = Math.max(0, Math.min(1, touchOnTrackPos / this.trackWidth))
|
|
||||||
|
|
||||||
var seekToTime = 0
|
|
||||||
if (this.useChapterTrack && this.currentChapter) {
|
|
||||||
const currChapTime = dragPercent * this.currentChapterDuration
|
|
||||||
seekToTime = this.currentChapter.start + currChapTime
|
|
||||||
} else {
|
|
||||||
seekToTime = dragPercent * this.totalDuration
|
|
||||||
}
|
}
|
||||||
this.seek(seekToTime)
|
this.isDraggingCursor = false
|
||||||
|
|
||||||
if (this.$refs.draggingTrack) {
|
|
||||||
this.$refs.draggingTrack.style.width = '0px'
|
|
||||||
}
|
|
||||||
this.touchTrackStart = false
|
|
||||||
} else if (this.showFullscreen) {
|
} else if (this.showFullscreen) {
|
||||||
this.touchEndY = e.changedTouches[0].screenY
|
this.touchEndY = e.changedTouches[0].screenY
|
||||||
var touchDuration = Date.now() - this.touchStartTime
|
var touchDuration = Date.now() - this.touchStartTime
|
||||||
@@ -682,29 +659,24 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
touchmove(e) {
|
touchmove(e) {
|
||||||
if (!this.touchTrackStart) return
|
if (!this.isDraggingCursor || !e.touches) return
|
||||||
|
|
||||||
var touch = e.touches[0]
|
const distanceMoved = e.touches[0].pageX - this.draggingTouchStartX
|
||||||
const touchOnTrackPos = touch.pageX - 12
|
let duration = this.totalDuration
|
||||||
const dragPercent = Math.max(0, Math.min(1, touchOnTrackPos / this.trackWidth))
|
let minTime = 0
|
||||||
this.dragPercent = dragPercent
|
let maxTime = duration
|
||||||
|
if (this.useChapterTrack && this.currentChapter) {
|
||||||
if (this.$refs.draggingTrack) {
|
duration = this.currentChapterDuration
|
||||||
this.$refs.draggingTrack.style.width = this.dragPercent * this.trackWidth + 'px'
|
minTime = this.currentChapter.start
|
||||||
|
maxTime = minTime + duration
|
||||||
}
|
}
|
||||||
|
|
||||||
var ts = this.$refs.currentTimestamp
|
const timePerPixel = duration / this.trackWidth
|
||||||
if (ts) {
|
const newTime = this.draggingTouchStartTime + timePerPixel * distanceMoved
|
||||||
var currTimeStr = ''
|
this.draggingCurrentTime = Math.min(maxTime, Math.max(minTime, newTime))
|
||||||
if (this.useChapterTrack && this.currentChapter) {
|
|
||||||
const currChapTime = dragPercent * this.currentChapterDuration
|
this.updateTimestamp()
|
||||||
currTimeStr = this.$secondsToTimestamp(currChapTime)
|
this.updateTrack()
|
||||||
} else {
|
|
||||||
const dragTime = dragPercent * this.totalDuration
|
|
||||||
currTimeStr = this.$secondsToTimestamp(dragTime)
|
|
||||||
}
|
|
||||||
ts.innerText = currTimeStr
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async clickMenuAction(action) {
|
async clickMenuAction(action) {
|
||||||
await this.$hapticsImpact()
|
await this.$hapticsImpact()
|
||||||
@@ -723,6 +695,7 @@ export default {
|
|||||||
this.updateTimestamp()
|
this.updateTimestamp()
|
||||||
this.updateTrack()
|
this.updateTrack()
|
||||||
this.updateReadyTrack()
|
this.updateReadyTrack()
|
||||||
|
this.$localStore.setUseTotalTrack(this.useTotalTrack)
|
||||||
this.$localStore.setUseChapterTrack(this.useChapterTrack)
|
this.$localStore.setUseChapterTrack(this.useChapterTrack)
|
||||||
} else if (action === 'total_track') {
|
} else if (action === 'total_track') {
|
||||||
this.useTotalTrack = !this.useTotalTrack
|
this.useTotalTrack = !this.useTotalTrack
|
||||||
@@ -732,6 +705,7 @@ export default {
|
|||||||
this.updateTrack()
|
this.updateTrack()
|
||||||
this.updateReadyTrack()
|
this.updateReadyTrack()
|
||||||
this.$localStore.setUseTotalTrack(this.useTotalTrack)
|
this.$localStore.setUseTotalTrack(this.useTotalTrack)
|
||||||
|
this.$localStore.setUseChapterTrack(this.useChapterTrack)
|
||||||
} else if (action === 'close') {
|
} else if (action === 'close') {
|
||||||
this.closePlayback()
|
this.closePlayback()
|
||||||
}
|
}
|
||||||
@@ -747,7 +721,7 @@ export default {
|
|||||||
AbsAudioPlayer.closePlayback()
|
AbsAudioPlayer.closePlayback()
|
||||||
},
|
},
|
||||||
endPlayback() {
|
endPlayback() {
|
||||||
this.$store.commit('setPlayerItem', null)
|
this.$store.commit('setPlaybackSession', null)
|
||||||
this.showFullscreen = false
|
this.showFullscreen = false
|
||||||
this.isEnded = false
|
this.isEnded = false
|
||||||
this.isLoading = false
|
this.isLoading = false
|
||||||
@@ -783,8 +757,6 @@ export default {
|
|||||||
|
|
||||||
console.log('received metadata update', data)
|
console.log('received metadata update', data)
|
||||||
|
|
||||||
if (data.currentRate && data.currentRate > 0) this.playbackSpeed = data.currentRate
|
|
||||||
|
|
||||||
this.timeupdate()
|
this.timeupdate()
|
||||||
},
|
},
|
||||||
// When a playback session is started the native android/ios will send the session
|
// When a playback session is started the native android/ios will send the session
|
||||||
@@ -795,7 +767,7 @@ export default {
|
|||||||
this.isEnded = false
|
this.isEnded = false
|
||||||
this.isLoading = true
|
this.isLoading = true
|
||||||
this.syncStatus = 0
|
this.syncStatus = 0
|
||||||
this.$store.commit('setPlayerItem', this.playbackSession)
|
this.$store.commit('setPlaybackSession', this.playbackSession)
|
||||||
|
|
||||||
// Set track width
|
// Set track width
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -815,8 +787,14 @@ export default {
|
|||||||
this.$toast.error(`Playback Failed: ${errorMessage}`)
|
this.$toast.error(`Playback Failed: ${errorMessage}`)
|
||||||
this.endPlayback()
|
this.endPlayback()
|
||||||
},
|
},
|
||||||
|
onPlaybackSpeedChanged(data) {
|
||||||
|
if (!data.value || isNaN(data.value)) return
|
||||||
|
this.currentPlaybackRate = Number(data.value)
|
||||||
|
this.updateTimestamp()
|
||||||
|
},
|
||||||
async init() {
|
async init() {
|
||||||
this.useChapterTrack = await this.$localStore.getUseChapterTrack()
|
this.useChapterTrack = await this.$localStore.getUseChapterTrack()
|
||||||
|
this.useTotalTrack = await this.$localStore.getUseTotalTrack()
|
||||||
this.lockUi = await this.$localStore.getPlayerLock()
|
this.lockUi = await this.$localStore.getPlayerLock()
|
||||||
|
|
||||||
this.onPlaybackSessionListener = AbsAudioPlayer.addListener('onPlaybackSession', this.onPlaybackSession)
|
this.onPlaybackSessionListener = AbsAudioPlayer.addListener('onPlaybackSession', this.onPlaybackSession)
|
||||||
@@ -826,6 +804,7 @@ export default {
|
|||||||
this.onMetadataListener = AbsAudioPlayer.addListener('onMetadata', this.onMetadata)
|
this.onMetadataListener = AbsAudioPlayer.addListener('onMetadata', this.onMetadata)
|
||||||
this.onProgressSyncFailing = AbsAudioPlayer.addListener('onProgressSyncFailing', this.showProgressSyncIsFailing)
|
this.onProgressSyncFailing = AbsAudioPlayer.addListener('onProgressSyncFailing', this.showProgressSyncIsFailing)
|
||||||
this.onProgressSyncSuccess = AbsAudioPlayer.addListener('onProgressSyncSuccess', this.showProgressSyncSuccess)
|
this.onProgressSyncSuccess = AbsAudioPlayer.addListener('onProgressSyncSuccess', this.showProgressSyncSuccess)
|
||||||
|
this.onPlaybackSpeedChangedListener = AbsAudioPlayer.addListener('onPlaybackSpeedChanged', this.onPlaybackSpeedChanged)
|
||||||
},
|
},
|
||||||
screenOrientationChange() {
|
screenOrientationChange() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -846,10 +825,10 @@ export default {
|
|||||||
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-width-collapsed', coverImageWidthCollapsed + 'px')
|
||||||
document.documentElement.style.setProperty('--cover-image-height-collapsed', 46 + 'px')
|
document.documentElement.style.setProperty('--cover-image-height-collapsed', 46 + 'px')
|
||||||
document.documentElement.style.setProperty('--title-author-left-offset-collapsed', 24 + coverImageWidthCollapsed + 'px')
|
document.documentElement.style.setProperty('--title-author-left-offset-collapsed', 30 + coverImageWidthCollapsed + 'px')
|
||||||
},
|
},
|
||||||
minimizePlayerEvt() {
|
minimizePlayerEvt() {
|
||||||
this.showFullscreen = false
|
this.collapseFullscreen()
|
||||||
},
|
},
|
||||||
showProgressSyncIsFailing() {
|
showProgressSyncIsFailing() {
|
||||||
this.syncStatus = this.$constants.SyncStatus.FAILED
|
this.syncStatus = this.$constants.SyncStatus.FAILED
|
||||||
@@ -901,6 +880,7 @@ export default {
|
|||||||
if (this.onPlaybackFailedListener) this.onPlaybackFailedListener.remove()
|
if (this.onPlaybackFailedListener) this.onPlaybackFailedListener.remove()
|
||||||
if (this.onProgressSyncFailing) this.onProgressSyncFailing.remove()
|
if (this.onProgressSyncFailing) this.onProgressSyncFailing.remove()
|
||||||
if (this.onProgressSyncSuccess) this.onProgressSyncSuccess.remove()
|
if (this.onProgressSyncSuccess) this.onProgressSyncSuccess.remove()
|
||||||
|
if (this.onPlaybackSpeedChangedListener) this.onPlaybackSpeedChangedListener.remove()
|
||||||
clearInterval(this.playInterval)
|
clearInterval(this.playInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -912,11 +892,11 @@ export default {
|
|||||||
--cover-image-height: 0px;
|
--cover-image-height: 0px;
|
||||||
--cover-image-width-collapsed: 46px;
|
--cover-image-width-collapsed: 46px;
|
||||||
--cover-image-height-collapsed: 46px;
|
--cover-image-height-collapsed: 46px;
|
||||||
--title-author-left-offset-collapsed: 70px;
|
--title-author-left-offset-collapsed: 80px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.playerContainer {
|
.playerContainer {
|
||||||
height: 100px;
|
height: 120px;
|
||||||
}
|
}
|
||||||
.fullscreen .playerContainer {
|
.fullscreen .playerContainer {
|
||||||
height: 200px;
|
height: 200px;
|
||||||
@@ -931,21 +911,22 @@ export default {
|
|||||||
#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;
|
||||||
bottom: 20px;
|
bottom: 35px;
|
||||||
}
|
}
|
||||||
.fullscreen #playerTrack {
|
.fullscreen #playerTrack {
|
||||||
top: 20px;
|
|
||||||
bottom: unset;
|
bottom: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cover-wrapper {
|
.cover-wrapper {
|
||||||
bottom: 48px;
|
bottom: 68px;
|
||||||
left: 12px;
|
left: 24px;
|
||||||
height: var(--cover-image-height-collapsed);
|
height: var(--cover-image-height-collapsed);
|
||||||
width: var(--cover-image-width-collapsed);
|
width: var(--cover-image-width-collapsed);
|
||||||
transition: all 0.25s cubic-bezier(0.39, 0.575, 0.565, 1);
|
transition: all 0.25s cubic-bezier(0.39, 0.575, 0.565, 1);
|
||||||
transition-property: left, bottom, width, height;
|
transition-property: left, bottom, width, height;
|
||||||
transform-origin: left bottom;
|
transform-origin: left bottom;
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.total-track {
|
.total-track {
|
||||||
@@ -960,7 +941,7 @@ export default {
|
|||||||
transform-origin: left bottom;
|
transform-origin: left bottom;
|
||||||
|
|
||||||
width: 40%;
|
width: 40%;
|
||||||
bottom: 56px;
|
bottom: 76px;
|
||||||
left: var(--title-author-left-offset-collapsed);
|
left: var(--title-author-left-offset-collapsed);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
@@ -986,20 +967,18 @@ 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.5rem);
|
font-size: clamp(0.8rem, calc(var(--cover-image-height) / 260 * 20), 1.3rem);
|
||||||
}
|
}
|
||||||
.fullscreen .title-author-texts .author-text {
|
.fullscreen .title-author-texts .author-text {
|
||||||
font-size: clamp(0.6rem, calc(var(--cover-image-height) / 260 * 16), 1.1rem);
|
font-size: clamp(0.6rem, calc(var(--cover-image-height) / 260 * 16), 1rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
#playerControls {
|
#playerControls {
|
||||||
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: width, bottom;
|
transition-property: width, bottom;
|
||||||
height: 48px;
|
width: 128px;
|
||||||
width: 140px;
|
padding-right: 24px;
|
||||||
padding-left: 12px;
|
bottom: 70px;
|
||||||
padding-right: 12px;
|
|
||||||
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);
|
||||||
@@ -1016,8 +995,7 @@ export default {
|
|||||||
width: 40px;
|
width: 40px;
|
||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
margin: 0px 14px;
|
margin: 0px 7px;
|
||||||
/* padding: 8px; */
|
|
||||||
}
|
}
|
||||||
#playerControls .play-btn .material-icons {
|
#playerControls .play-btn .material-icons {
|
||||||
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);
|
||||||
@@ -1032,27 +1010,28 @@ export default {
|
|||||||
width: var(--cover-image-width);
|
width: var(--cover-image-width);
|
||||||
left: calc(50% - (calc(var(--cover-image-width)) / 2));
|
left: calc(50% - (calc(var(--cover-image-width)) / 2));
|
||||||
bottom: calc(50% + 120px - (calc(var(--cover-image-height)) / 2));
|
bottom: calc(50% + 120px - (calc(var(--cover-image-height)) / 2));
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fullscreen #playerControls {
|
.fullscreen #playerControls {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
bottom: 94px;
|
padding-left: 24px;
|
||||||
|
padding-right: 24px;
|
||||||
|
bottom: 78px;
|
||||||
|
left: 0;
|
||||||
}
|
}
|
||||||
.fullscreen #playerControls .jump-icon {
|
.fullscreen #playerControls .jump-icon {
|
||||||
margin: 0px 18px;
|
|
||||||
font-size: 2.4rem;
|
font-size: 2.4rem;
|
||||||
}
|
}
|
||||||
.fullscreen #playerControls .next-icon {
|
.fullscreen #playerControls .next-icon {
|
||||||
margin: 0px 20px;
|
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
.fullscreen #playerControls .play-btn {
|
.fullscreen #playerControls .play-btn {
|
||||||
/* padding: 16px; */
|
|
||||||
height: 65px;
|
height: 65px;
|
||||||
width: 65px;
|
width: 65px;
|
||||||
min-width: 65px;
|
min-width: 65px;
|
||||||
min-height: 65px;
|
min-height: 65px;
|
||||||
margin: 0px 26px;
|
|
||||||
}
|
}
|
||||||
.fullscreen #playerControls .play-btn .material-icons {
|
.fullscreen #playerControls .play-btn .material-icons {
|
||||||
font-size: 2.1rem;
|
font-size: 2.1rem;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<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" />
|
<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" :is-auto="isAutoSleepTimer" @change="selectSleepTimeout" @cancel="cancelSleepTimer" @increase="increaseSleepTimer" @decrease="decreaseSleepTimer" />
|
||||||
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="currentTime" :library-item-id="serverLibraryItemId" @select="selectBookmark" />
|
<modals-bookmarks-modal v-model="showBookmarksModal" :bookmarks="bookmarks" :current-time="currentTime" :library-item-id="serverLibraryItemId" @select="selectBookmark" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -28,6 +28,7 @@ export default {
|
|||||||
isSleepTimerRunning: false,
|
isSleepTimerRunning: false,
|
||||||
sleepTimerEndTime: 0,
|
sleepTimerEndTime: 0,
|
||||||
sleepTimeRemaining: 0,
|
sleepTimeRemaining: 0,
|
||||||
|
isAutoSleepTimer: false,
|
||||||
onLocalMediaProgressUpdateListener: null,
|
onLocalMediaProgressUpdateListener: null,
|
||||||
onSleepTimerEndedListener: null,
|
onSleepTimerEndedListener: null,
|
||||||
onSleepTimerSetListener: null,
|
onSleepTimerSetListener: null,
|
||||||
@@ -42,6 +43,9 @@ export default {
|
|||||||
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)
|
||||||
|
},
|
||||||
|
isIos() {
|
||||||
|
return this.$platform === 'ios'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -62,8 +66,9 @@ export default {
|
|||||||
console.log('Sleep Timer Ended Current Position: ' + currentPosition)
|
console.log('Sleep Timer Ended Current Position: ' + currentPosition)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSleepTimerSet({ value: sleepTimeRemaining }) {
|
onSleepTimerSet(payload) {
|
||||||
console.log('SLEEP TIMER SET', sleepTimeRemaining)
|
const { value: sleepTimeRemaining, isAuto } = payload
|
||||||
|
console.log('SLEEP TIMER SET', JSON.stringify(payload))
|
||||||
if (sleepTimeRemaining === 0) {
|
if (sleepTimeRemaining === 0) {
|
||||||
console.log('Sleep timer canceled')
|
console.log('Sleep timer canceled')
|
||||||
this.isSleepTimerRunning = false
|
this.isSleepTimerRunning = false
|
||||||
@@ -71,6 +76,7 @@ export default {
|
|||||||
this.isSleepTimerRunning = true
|
this.isSleepTimerRunning = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.isAutoSleepTimer = !!isAuto
|
||||||
this.sleepTimeRemaining = sleepTimeRemaining
|
this.sleepTimeRemaining = sleepTimeRemaining
|
||||||
},
|
},
|
||||||
showSleepTimer() {
|
showSleepTimer() {
|
||||||
@@ -104,7 +110,7 @@ export default {
|
|||||||
},
|
},
|
||||||
streamProgress(data) {
|
streamProgress(data) {
|
||||||
if (!data.numSegments) return
|
if (!data.numSegments) return
|
||||||
var chunks = data.chunks
|
const chunks = data.chunks
|
||||||
if (this.$refs.audioPlayer) {
|
if (this.$refs.audioPlayer) {
|
||||||
this.$refs.audioPlayer.setChunksReady(chunks, data.numSegments)
|
this.$refs.audioPlayer.setChunksReady(chunks, data.numSegments)
|
||||||
}
|
}
|
||||||
@@ -185,6 +191,7 @@ export default {
|
|||||||
const libraryItemId = payload.libraryItemId
|
const libraryItemId = payload.libraryItemId
|
||||||
const episodeId = payload.episodeId
|
const episodeId = payload.episodeId
|
||||||
const startTime = payload.startTime
|
const startTime = payload.startTime
|
||||||
|
const startWhenReady = !payload.paused
|
||||||
|
|
||||||
// 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
|
||||||
@@ -207,6 +214,8 @@ export default {
|
|||||||
if (startTime !== undefined && startTime !== null) {
|
if (startTime !== undefined && startTime !== null) {
|
||||||
// seek to start time
|
// seek to start time
|
||||||
AbsAudioPlayer.seek({ value: Math.floor(startTime) })
|
AbsAudioPlayer.seek({ value: Math.floor(startTime) })
|
||||||
|
} else if (this.$refs.audioPlayer) {
|
||||||
|
this.$refs.audioPlayer.play()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -220,7 +229,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log('Called playLibraryItem', libraryItemId)
|
console.log('Called playLibraryItem', libraryItemId)
|
||||||
const preparePayload = { libraryItemId, episodeId, playWhenReady: true, playbackRate }
|
const preparePayload = { libraryItemId, episodeId, playWhenReady: startWhenReady, playbackRate }
|
||||||
if (startTime !== undefined && startTime !== null) preparePayload.startTime = startTime
|
if (startTime !== undefined && startTime !== null) preparePayload.startTime = startTime
|
||||||
AbsAudioPlayer.prepareLibraryItem(preparePayload)
|
AbsAudioPlayer.prepareLibraryItem(preparePayload)
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
@@ -256,8 +265,7 @@ export default {
|
|||||||
this.$store.commit('globals/updateLocalMediaProgress', localMediaProgress)
|
this.$store.commit('globals/updateLocalMediaProgress', localMediaProgress)
|
||||||
},
|
},
|
||||||
onMediaPlayerChanged(data) {
|
onMediaPlayerChanged(data) {
|
||||||
var mediaPlayer = data.value
|
this.$store.commit('setMediaPlayer', data.value)
|
||||||
this.$store.commit('setMediaPlayer', mediaPlayer)
|
|
||||||
},
|
},
|
||||||
onReady() {
|
onReady() {
|
||||||
// The UI is reporting elsewhere we are ready
|
// The UI is reporting elsewhere we are ready
|
||||||
@@ -265,11 +273,18 @@ export default {
|
|||||||
this.notifyOnReady()
|
this.notifyOnReady()
|
||||||
},
|
},
|
||||||
notifyOnReady() {
|
notifyOnReady() {
|
||||||
|
// TODO: iOS opens last active playback session on app launch. Should be consistent with Android
|
||||||
|
if (!this.isIos) return
|
||||||
|
|
||||||
// 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 && this.$store.state.isFirstAudioLoad) {
|
||||||
|
this.$store.commit('setIsFirstAudioLoad', false) // Only run this once on app launch
|
||||||
AbsAudioPlayer.onReady()
|
AbsAudioPlayer.onReady()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
playbackTimeUpdate(currentTime) {
|
||||||
|
this.$refs.audioPlayer?.seek(currentTime)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -287,6 +302,7 @@ export default {
|
|||||||
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.$eventBus.$on('user-settings', this.settingsUpdated)
|
this.$eventBus.$on('user-settings', this.settingsUpdated)
|
||||||
|
this.$eventBus.$on('playback-time-update', this.playbackTimeUpdate)
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
if (this.onLocalMediaProgressUpdateListener) this.onLocalMediaProgressUpdateListener.remove()
|
if (this.onLocalMediaProgressUpdateListener) this.onLocalMediaProgressUpdateListener.remove()
|
||||||
@@ -300,6 +316,7 @@ export default {
|
|||||||
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.$eventBus.$off('user-settings', this.settingsUpdated)
|
this.$eventBus.$off('user-settings', this.settingsUpdated)
|
||||||
|
this.$eventBus.$off('playback-time-update', this.playbackTimeUpdate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -50,6 +50,9 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
showBookshelfListView(newVal) {
|
showBookshelfListView(newVal) {
|
||||||
this.resetEntities()
|
this.resetEntities()
|
||||||
|
},
|
||||||
|
seriesId() {
|
||||||
|
this.resetEntities()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -85,6 +88,12 @@ export default {
|
|||||||
filterBy() {
|
filterBy() {
|
||||||
return this.$store.getters['user/getUserSetting']('mobileFilterBy')
|
return this.$store.getters['user/getUserSetting']('mobileFilterBy')
|
||||||
},
|
},
|
||||||
|
collapseSeries() {
|
||||||
|
return this.$store.getters['user/getUserSetting']('collapseSeries')
|
||||||
|
},
|
||||||
|
collapseBookSeries() {
|
||||||
|
return this.$store.getters['user/getUserSetting']('collapseBookSeries')
|
||||||
|
},
|
||||||
isCoverSquareAspectRatio() {
|
isCoverSquareAspectRatio() {
|
||||||
return this.bookCoverAspectRatio === 1
|
return this.bookCoverAspectRatio === 1
|
||||||
},
|
},
|
||||||
@@ -155,7 +164,7 @@ export default {
|
|||||||
|
|
||||||
const entityPath = this.entityName === 'books' || this.entityName === 'series-books' ? `items` : this.entityName
|
const entityPath = this.entityName === 'books' || this.entityName === 'series-books' ? `items` : this.entityName
|
||||||
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
const sfQueryString = this.currentSFQueryString ? this.currentSFQueryString + '&' : ''
|
||||||
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1`
|
const fullQueryString = `?${sfQueryString}limit=${this.booksPerFetch}&page=${page}&minified=1&include=rssfeed`
|
||||||
|
|
||||||
const 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)
|
||||||
@@ -356,6 +365,9 @@ 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)}`)
|
||||||
|
if (this.collapseBookSeries) {
|
||||||
|
searchParams.set('collapseseries', 1)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this.filterBy && this.filterBy !== 'all') {
|
if (this.filterBy && this.filterBy !== 'all') {
|
||||||
searchParams.set('filter', this.filterBy)
|
searchParams.set('filter', this.filterBy)
|
||||||
@@ -452,6 +464,12 @@ export default {
|
|||||||
this.libraryItemUpdated(ab)
|
this.libraryItemUpdated(ab)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
screenOrientationChange() {
|
||||||
|
setTimeout(() => {
|
||||||
|
console.log('LazyBookshelf Screen orientation change')
|
||||||
|
this.resetEntities()
|
||||||
|
}, 50)
|
||||||
|
},
|
||||||
initListeners() {
|
initListeners() {
|
||||||
const bookshelf = document.getElementById('bookshelf-wrapper')
|
const bookshelf = document.getElementById('bookshelf-wrapper')
|
||||||
if (bookshelf) {
|
if (bookshelf) {
|
||||||
@@ -466,6 +484,13 @@ export default {
|
|||||||
this.$socket.$on('item_removed', this.libraryItemRemoved)
|
this.$socket.$on('item_removed', this.libraryItemRemoved)
|
||||||
this.$socket.$on('items_updated', this.libraryItemsUpdated)
|
this.$socket.$on('items_updated', this.libraryItemsUpdated)
|
||||||
this.$socket.$on('items_added', this.libraryItemsAdded)
|
this.$socket.$on('items_added', this.libraryItemsAdded)
|
||||||
|
|
||||||
|
if (screen.orientation) {
|
||||||
|
// Not available on ios
|
||||||
|
screen.orientation.addEventListener('change', this.screenOrientationChange)
|
||||||
|
} else {
|
||||||
|
document.addEventListener('orientationchange', this.screenOrientationChange)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
removeListeners() {
|
removeListeners() {
|
||||||
const bookshelf = document.getElementById('bookshelf-wrapper')
|
const bookshelf = document.getElementById('bookshelf-wrapper')
|
||||||
@@ -481,6 +506,13 @@ export default {
|
|||||||
this.$socket.$off('item_removed', this.libraryItemRemoved)
|
this.$socket.$off('item_removed', this.libraryItemRemoved)
|
||||||
this.$socket.$off('items_updated', this.libraryItemsUpdated)
|
this.$socket.$off('items_updated', this.libraryItemsUpdated)
|
||||||
this.$socket.$off('items_added', this.libraryItemsAdded)
|
this.$socket.$off('items_added', this.libraryItemsAdded)
|
||||||
|
|
||||||
|
if (screen.orientation) {
|
||||||
|
// Not available on ios
|
||||||
|
screen.orientation.removeEventListener('change', this.screenOrientationChange)
|
||||||
|
} else {
|
||||||
|
document.removeEventListener('orientationchange', this.screenOrientationChange)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
updated() {
|
updated() {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="!altViewEnabled" class="absolute text-center categoryPlacard font-book transform z-30 bottom-0.5 left-4 md:left-8 w-36 rounded-md" style="height: 18px">
|
<div v-if="!altViewEnabled" class="absolute text-center categoryPlacardtransform z-30 bottom-0.5 left-4 md:left-8 w-36 rounded-md" style="height: 18px">
|
||||||
<div class="w-full h-full flex items-center justify-center rounded-sm border shinyBlack">
|
<div class="w-full h-full flex items-center justify-center rounded-sm border shinyBlack">
|
||||||
<p class="transform text-xs">{{ label }}</p>
|
<p class="transform text-xs">{{ label }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,26 +7,6 @@
|
|||||||
<p class="truncate text-sm">{{ name }}</p>
|
<p class="truncate text-sm">{{ name }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="flex h-full px-1 overflow-hidden shadow-sm">
|
|
||||||
<div style="max-height: 48px; max-width: 48px" class="w-12 h-12 bg-primary overflow-hidden rounded">
|
|
||||||
<svg width="140%" height="140%" style="margin-left: -20%; margin-top: -20%; opacity: 0.6" viewBox="0 0 177 266" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path fill="white" d="M40.7156 165.47C10.2694 150.865 -31.5407 148.629 -38.0532 155.529L63.3191 204.159L76.9443 190.899C66.828 181.394 54.006 171.846 40.7156 165.47Z" stroke="white" stroke-width="4" transform="translate(-2 -1)" />
|
|
||||||
<path d="M-38.0532 155.529C-31.5407 148.629 10.2694 150.865 40.7156 165.47C54.006 171.846 66.828 181.394 76.9443 190.899L95.0391 173.37C80.6681 159.403 64.7526 149.155 51.5747 142.834C21.3549 128.337 -46.2471 114.563 -60.6897 144.67L-71.5489 167.307L44.5864 223.019L63.3191 204.159L-38.0532 155.529Z" fill="white" />
|
|
||||||
<path
|
|
||||||
d="M105.87 29.6508C80.857 17.6515 50.8784 28.1923 38.879 53.2056C26.8797 78.219 37.4205 108.198 62.4338 120.197C87.4472 132.196 117.426 121.656 129.425 96.6422C141.425 71.6288 130.884 41.6502 105.87 29.6508ZM106.789 85.783C112.761 73.3329 107.461 58.2599 95.0112 52.2874C82.5611 46.3148 67.4881 51.6147 61.5156 64.0648C55.543 76.5149 60.8429 91.5879 73.293 97.5604C85.7431 103.533 100.816 98.2331 106.789 85.783Z"
|
|
||||||
fill="white"
|
|
||||||
/>
|
|
||||||
<path
|
|
||||||
d="M151.336 159.01L159.048 166.762L82.7048 242.703L74.973 242.683L74.9934 234.951L151.336 159.01ZM181.725 108.497C179.624 108.491 177.436 109.326 175.835 110.918L160.415 126.257L191.848 157.856L207.268 142.517C210.554 139.248 210.568 133.954 207.299 130.667L187.685 110.95C186.009 109.264 183.91 108.502 181.725 108.497ZM151.399 135.226L58.2034 227.931L58.1203 259.447L89.6359 259.53L182.831 166.825L151.399 135.226Z"
|
|
||||||
fill="white"
|
|
||||||
/>
|
|
||||||
<path d="M151.336 159.01L159.048 166.762L82.7048 242.703L74.973 242.683L74.9934 234.951L151.336 159.01Z" fill="white" stroke="white" stroke-width="10px" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="flex-grow px-2 authorSearchCardContent h-full">
|
|
||||||
<p class="truncate text-sm">{{ author }}</p>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||