Compare commits

...

779 commits

Author SHA1 Message Date
advplyr
48a299de45
Merge pull request #1675 from laurisvr/remove-busy-loops
Some checks are pending
Build APK / main (push) Waiting to run
Publish Test App / build (push) Waiting to run
Publish Test App / deploy (push) Blocked by required conditions
Verify all i18n files are alphabetized / update_translations (push) Waiting to run
Remove busy loops
2025-08-31 16:12:07 -04:00
Lauris van Rijn
719e517dda fix(androidauto): async handling of browseTree init instead of busy‑loop
Removed blocking `while (!browseTree.isInitialized){}` in
`onLoadChildren`. Added `waitForBrowseTree` and `onBrowseTreeInitialized`
helpers to queue pending results until browseTree is ready. All
browseTree assignments now call `onBrowseTreeInitialized()`. This avoids
ANRs and high CPU when Android Auto requests children before init.
2025-08-29 00:43:02 +02:00
Lauris van Rijn
361c55c5ac fix(media): remove busy‑wait loop in library personalization loading
Replaced the infinite `while(libraryPersonalizationsDone > 0){}` spin‑loop
with an async counter callback. This prevents pegging the CPU if one
personalization never completes, and allows completion to trigger via
AtomicInteger decrement. Now the final callback fires only when all
libraries have finished loading.
2025-08-29 00:42:48 +02:00
advplyr
b157fff229
Merge pull request #1653 from weblate/weblate-audiobookshelf-abs-mobile-app
Some checks failed
Build APK / main (push) Has been cancelled
Publish Test App / build (push) Has been cancelled
Verify all i18n files are alphabetized / update_translations (push) Has been cancelled
Publish Test App / deploy (push) Has been cancelled
Translations update from Hosted Weblate
2025-08-23 17:46:51 -04:00
thehijacker
e23e6417ee
Translated using Weblate (Slovenian)
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2025-08-23 05:01:54 +00:00
Yurt Page
ad77209832
Translated using Weblate (Russian)
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2025-08-23 05:01:53 +00:00
Zhelyan Radoev
4ceb427d9c
Translated using Weblate (Bulgarian)
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/bg/
2025-08-20 05:02:15 +00:00
Ivan Smoliakov
16a26130f2
Translated using Weblate (Russian)
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2025-08-20 05:02:14 +00:00
numerfolt
e7773c9e88
Translated using Weblate (German)
Currently translated at 99.7% (356 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-08-20 05:02:13 +00:00
B0rax
1b38b92177
Translated using Weblate (German)
Currently translated at 99.7% (356 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-08-20 05:02:12 +00:00
Laurin Sorgend
ef180f08b0
Translated using Weblate (German)
Currently translated at 99.7% (356 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-08-20 05:02:11 +00:00
Zhelyan Radoev
6de45a3b88
Translated using Weblate (Bulgarian)
Currently translated at 99.7% (356 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/bg/
2025-08-18 17:02:14 +02:00
FiendFEARing
ceb9537b73
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-08-18 17:02:12 +02:00
Максим Горпиніч
976bf73388
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-08-18 17:02:10 +02:00
ugyes
6c7f2d96f3
Translated using Weblate (Hungarian)
Currently translated at 100.0% (357 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2025-08-18 17:02:09 +02:00
Charlie
ae2fc1fcfb
Translated using Weblate (French)
Currently translated at 99.7% (356 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2025-08-18 17:02:07 +02:00
owlcollector
3aad7e9e07
Translated using Weblate (Japanese)
Currently translated at 16.5% (59 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ja/
2025-08-17 00:55:15 +02:00
Zhelyan Radoev
e43f1e75c8
Translated using Weblate (Bulgarian)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/bg/
2025-08-17 00:55:14 +02:00
Максим Горпиніч
bc5505e12d
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-08-17 00:55:13 +02:00
Hang Pham
19a2703ea4
Translated using Weblate (Vietnamese)
Currently translated at 80.0% (285 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/vi/
2025-08-17 00:55:13 +02:00
Sneaky
8a8f7d4f27
Translated using Weblate (Swedish)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-08-17 00:55:12 +02:00
Paolo Ricci
e350b4970f
Translated using Weblate (Italian)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2025-08-17 00:55:11 +02:00
owlcollector
4be28bc579
Translated using Weblate (Japanese)
Currently translated at 7.8% (28 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ja/
2025-08-17 00:55:11 +02:00
J. Lavoie
13855a8682
Translated using Weblate (German)
Currently translated at 99.7% (355 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-08-17 00:55:10 +02:00
advplyr
4be1598eca Fix oidc button not showing on re-login, fix oidc re-login showing config already exists #1638 #1634
Some checks failed
Build APK / main (push) Has been cancelled
Publish Test App / build (push) Has been cancelled
Verify all i18n files are alphabetized / update_translations (push) Has been cancelled
Publish Test App / deploy (push) Has been cancelled
2025-08-16 17:55:00 -05:00
advplyr
7aebcd92c3 Fix refresh token not persisted on new server connections #1634
Some checks are pending
Verify all i18n files are alphabetized / update_translations (push) Waiting to run
2025-08-15 17:41:23 -05:00
advplyr
bd8668f0bf Fix rss feed modal not showing full URL once open #1652
Some checks failed
Verify all i18n files are alphabetized / update_translations (push) Has been cancelled
Publish Test App / build (push) Has been cancelled
Build APK / main (push) Has been cancelled
Publish Test App / deploy (push) Has been cancelled
2025-08-08 17:39:27 -05:00
advplyr
c6a7c6fec2
Merge pull request #1637 from weblate/weblate-audiobookshelf-abs-mobile-app
Some checks are pending
Build APK / main (push) Waiting to run
Publish Test App / deploy (push) Blocked by required conditions
Publish Test App / build (push) Waiting to run
Verify all i18n files are alphabetized / update_translations (push) Waiting to run
Translations update from Hosted Weblate
2025-08-07 18:45:51 -04:00
Aleksandr Zakirov
e367fa8a86
Translated using Weblate (Estonian)
Currently translated at 49.1% (175 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/et/
2025-08-07 03:01:53 +02:00
weblate.user.1274
ecdbabfa17
Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2025-08-07 03:01:52 +02:00
Kent Henriksen
796189ad48
Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2025-08-07 03:01:51 +02:00
Ashish Wadekar
76d1f2b29b
Translated using Weblate (Hindi)
Currently translated at 13.2% (47 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hi/
2025-08-07 03:01:50 +02:00
Camille de Lune
73599815ba
Translated using Weblate (French)
Currently translated at 99.7% (355 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2025-08-07 03:01:49 +02:00
Grzegorz Orlowski
66372e9743
Translated using Weblate (Polish)
Currently translated at 99.4% (354 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-08-05 04:02:09 +02:00
Grzegorz Orlowski
0d7f92129b
Translated using Weblate (Polish)
Currently translated at 98.0% (349 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-08-03 12:02:20 +02:00
Remco Schrijver
9da49d6116
Translated using Weblate (Dutch)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2025-07-31 20:02:32 +02:00
Troj@
7eeb9095e8
Translated using Weblate (Belarusian)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2025-07-30 02:08:19 +00:00
thehijacker
58281bb5ce
Translated using Weblate (Slovenian)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2025-07-30 02:08:18 +00:00
ugyes
f24dd3b289
Translated using Weblate (Hungarian)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2025-07-30 02:08:17 +00:00
B0rax
51f2e5e7ec
Translated using Weblate (German)
Currently translated at 99.7% (355 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-07-30 02:08:16 +00:00
numerfolt
a937800ef0
Translated using Weblate (German)
Currently translated at 99.7% (355 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-07-30 02:08:16 +00:00
Mikkel Dupont Olesen
c34d14edb5
Translated using Weblate (Danish)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2025-07-27 09:04:25 +00:00
Dmitry
8722e91c0f
Translated using Weblate (Russian)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2025-07-23 08:03:14 +02:00
kuci-JK
8038a8ac66
Translated using Weblate (Czech)
Currently translated at 99.1% (353 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2025-07-23 08:03:14 +02:00
Pavel Vachek
69a54c5fa9
Translated using Weblate (Czech)
Currently translated at 99.1% (353 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2025-07-23 08:03:14 +02:00
advplyr
aa508887f3
Merge pull request #1636 from advplyr/fix_android_transcode_track_url
Some checks failed
Verify all i18n files are alphabetized / update_translations (push) Has been cancelled
Build APK / main (push) Has been cancelled
Publish Test App / build (push) Has been cancelled
Publish Test App / deploy (push) Has been cancelled
Fix track URL used for transcodes on Android #1635
2025-07-22 15:15:47 -05:00
advplyr
a53a96ecf5 Update web player track index to fallback to 1 2025-07-22 14:46:31 -05:00
advplyr
82cddde15f Fix track URL used for transcodes on Android #1635 2025-07-22 14:16:35 -05:00
advplyr
def47fd5e2 Update gh issue template app versions
Some checks failed
Build APK / main (push) Has been cancelled
Publish Test App / build (push) Has been cancelled
Verify all i18n files are alphabetized / update_translations (push) Has been cancelled
Publish Test App / deploy (push) Has been cancelled
2025-07-21 08:57:56 -05:00
advplyr
04c70f54aa iOS version bump 0.10.0
Some checks failed
Build APK / main (push) Has been cancelled
Publish Test App / build (push) Has been cancelled
Verify all i18n files are alphabetized / update_translations (push) Has been cancelled
Publish Test App / deploy (push) Has been cancelled
2025-07-19 17:17:27 -05:00
advplyr
03e2feddb3 Version bump v0.10.0-beta 2025-07-19 16:55:54 -05:00
advplyr
fc69909dd0
Merge pull request #1625 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-07-19 16:47:20 -05:00
FiendFEARing
15fa42986a
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-07-19 15:03:05 +02:00
SunSpring
d8b4cf98dd
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-07-19 15:03:04 +02:00
Максим Горпиніч
ce62f3b273
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-07-19 15:03:03 +02:00
biuklija
8be2e3af0c
Translated using Weblate (Croatian)
Currently translated at 100.0% (356 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-07-19 15:03:01 +02:00
Gernomaly
e44c574068
Translated using Weblate (German)
Currently translated at 99.4% (354 of 356 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-07-19 15:03:01 +02:00
SunSpring
c31aa17ce2
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-07-17 22:45:49 +00:00
Максим Горпиніч
decac596d0
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-07-17 22:45:48 +00:00
Grzegorz Orlowski
2d9604aa07
Translated using Weblate (Polish)
Currently translated at 98.8% (349 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-07-17 22:45:47 +00:00
Fredrik Lindqvist
79c4ffa6c5
Translated using Weblate (Swedish)
Currently translated at 99.7% (352 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-07-17 22:45:47 +00:00
Jannik
571b3a2c86
Translated using Weblate (German)
Currently translated at 99.7% (352 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-07-17 22:45:46 +00:00
FiendFEARing
fa31fba586
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-07-17 22:45:45 +00:00
Simple16
2f3b261392
Translated using Weblate (Russian)
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2025-07-17 22:45:45 +00:00
biuklija
afd3cd6716
Translated using Weblate (Croatian)
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-07-17 22:45:44 +00:00
FiendFEARing
be99ef87ad
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-07-17 22:45:44 +00:00
Kabika82
572fc8545f
Translated using Weblate (Hungarian)
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2025-07-17 22:45:43 +00:00
advplyr
239a943172
Merge pull request #1618 from advplyr/new_jwt_auth
Some checks failed
Build APK / main (push) Has been cancelled
Publish Test App / build (push) Has been cancelled
Verify all i18n files are alphabetized / update_translations (push) Has been cancelled
Publish Test App / deploy (push) Has been cancelled
Update auth to handle refresh tokens
2025-07-17 17:45:33 -05:00
advplyr
87614bc78a Update auth message, update force re-login to pull auth methods to support oidc
Some checks failed
Build APK / main (push) Has been cancelled
Verify all i18n files are alphabetized / update_translations (push) Has been cancelled
2025-07-17 17:37:41 -05:00
advplyr
224d75fac5 Update old auth alert messages, add link to github discussion
Some checks failed
Build APK / main (push) Has been cancelled
Verify all i18n files are alphabetized / update_translations (push) Has been cancelled
2025-07-15 17:41:30 -05:00
advplyr
f4e0a6121f Update readers to handle token refresh 2025-07-15 17:22:45 -05:00
advplyr
80ee88b488 Merge branch 'master' into new_jwt_auth
Some checks failed
Build APK / main (push) Has been cancelled
Verify all i18n files are alphabetized / update_translations (push) Has been cancelled
2025-07-11 16:58:50 -05:00
advplyr
beb654825f
Merge pull request #1623 from weblate/weblate-audiobookshelf-abs-mobile-app
Some checks failed
Build APK / main (push) Has been cancelled
Publish Test App / build (push) Has been cancelled
Verify all i18n files are alphabetized / update_translations (push) Has been cancelled
Publish Test App / deploy (push) Has been cancelled
Translations update from Hosted Weblate
2025-07-11 16:58:32 -05:00
advplyr
79d8ccbf52 Update login query param to x-return-tokens header 2025-07-11 16:00:48 -05:00
thehijacker
b48c74eca0
Translated using Weblate (Slovenian)
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2025-07-11 00:18:44 +02:00
FiendFEARing
19e3dee706
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-07-11 00:18:44 +02:00
SunSpring
e1be86c3ce
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-07-11 00:18:44 +02:00
SunSpring
11f7eddd7b
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-07-11 00:18:44 +02:00
Raj
7f28d2ea70
Translated using Weblate (Gujarati)
Currently translated at 9.3% (33 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/gu/
2025-07-11 00:18:44 +02:00
Richard Požgay
4607a8e274
Translated using Weblate (Czech)
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2025-07-11 00:18:44 +02:00
Michal
26c559104a
Translated using Weblate (Slovak)
Currently translated at 99.7% (352 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2025-07-11 00:18:44 +02:00
Максим Горпиніч
e15e884e65
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (353 of 353 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-07-11 00:18:43 +02:00
advplyr
b29401909f Fix comic extracted metadata icon
Some checks failed
Build APK / main (push) Has been cancelled
Publish Test App / build (push) Has been cancelled
Verify all i18n files are alphabetized / update_translations (push) Has been cancelled
Publish Test App / deploy (push) Has been cancelled
2025-07-10 17:18:38 -05:00
advplyr
beb5e1a56c OIDC to support new access tokens
Some checks failed
Build APK / main (push) Has been cancelled
Verify all i18n files are alphabetized / update_translations (push) Has been cancelled
2025-07-07 17:20:22 -05:00
advplyr
be08efeca3 iOS update retry request handler to handle string bodies
Some checks are pending
Verify all i18n files are alphabetized / update_translations (push) Waiting to run
2025-07-07 06:15:51 -05:00
advplyr
4534ffaead Handle re-authenticating socket
Some checks failed
Verify all i18n files are alphabetized / update_translations (push) Waiting to run
Build APK / main (push) Has been cancelled
2025-07-06 11:32:21 -05:00
advplyr
d35dd2df1a Merge branch 'master' into new_jwt_auth 2025-07-06 09:07:17 -05:00
advplyr
2f3a9a5d96 Update play button on list book to check if player is starting
Some checks failed
Build APK / main (push) Has been cancelled
Publish Test App / build (push) Has been cancelled
Verify all i18n files are alphabetized / update_translations (push) Has been cancelled
Publish Test App / deploy (push) Has been cancelled
2025-07-05 18:06:24 -05:00
advplyr
fab94cd363 Handle native app token refresh failure notification
Some checks are pending
Build APK / main (push) Waiting to run
Verify all i18n files are alphabetized / update_translations (push) Waiting to run
2025-07-05 17:40:57 -05:00
advplyr
b06274866d iOS update to use new track endpoint and remove token from cover url depending on version 2025-07-05 17:31:25 -05:00
advplyr
5766c49f61 Update iOS ApiClient to handle token refresh 2025-07-05 16:46:37 -05:00
advplyr
bc927d4c35 Add SecureStorage to iOS and implement refresh token methods 2025-07-05 14:52:41 -05:00
advplyr
5804c54656 Add version to ServerConnectionConfig on iOS 2025-07-05 14:28:02 -05:00
advplyr
52f86cbce9 Fix plugin listener handlers, add message for configs using old server auth, show server version on account page
Some checks are pending
Build APK / main (push) Waiting to run
Verify all i18n files are alphabetized / update_translations (push) Waiting to run
2025-07-05 12:20:27 -05:00
advplyr
f6e2e4010f Update to not logout from server when switching servers, force users to re-login if using old auth 2025-07-05 11:21:20 -05:00
advplyr
44613e12f1 Update serverConnectionConfig to include server version, update server track URL and server cover image URL based on server version 2025-07-05 09:28:40 -05:00
advplyr
b99e0b112b Merge branch 'master' into new_jwt_auth 2025-07-05 08:06:40 -05:00
advplyr
80b565c23f
Merge pull request #1622 from advplyr/play_from_library_list_rows
Some checks are pending
Build APK / main (push) Waiting to run
Publish Test App / build (push) Waiting to run
Publish Test App / deploy (push) Blocked by required conditions
Verify all i18n files are alphabetized / update_translations (push) Waiting to run
Add play button to book library list view
2025-07-04 18:22:30 -05:00
advplyr
bee88c43e5 Add play button to book library list view 2025-07-04 18:07:05 -05:00
advplyr
467fedbfe7 Update to use x-refresh-token header, update logout to clear refresh token, add AbsLogger logs for android refresh
Some checks are pending
Build APK / main (push) Waiting to run
Verify all i18n files are alphabetized / update_translations (push) Waiting to run
2025-07-04 17:41:19 -05:00
advplyr
ba4e9ab7e3 Add explicit library filters
Some checks are pending
Build APK / main (push) Waiting to run
Publish Test App / build (push) Waiting to run
Publish Test App / deploy (push) Blocked by required conditions
Verify all i18n files are alphabetized / update_translations (push) Waiting to run
2025-07-03 17:40:02 -05:00
advplyr
1b4fa5e44a
Merge pull request #1610 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-07-03 17:32:02 -05:00
FiendFEARing
e92c9b2990
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-07-02 20:16:41 +02:00
FiendFEARing
f23f8e9ade
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-07-02 20:16:39 +02:00
DavevanIersel
dc00aeece4
Translated using Weblate (Dutch)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2025-07-02 20:16:37 +02:00
advplyr
d8cdb7073e Update auth to handle refresh tokens 2025-07-01 11:33:51 -05:00
Vito0912
4fdea9b0ec
Translated using Weblate (German)
Currently translated at 99.7% (351 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-06-30 00:32:39 +02:00
Vito0912
3ded7687f7
Translated using Weblate (German)
Currently translated at 99.7% (351 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-06-30 00:32:38 +02:00
Eigen_art
42119425af
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-06-30 00:32:38 +02:00
Dan Johansen
93c7e1c44a
Translated using Weblate (Danish)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2025-06-30 00:32:37 +02:00
Michael Förster
381fd84839
Translated using Weblate (German)
Currently translated at 99.7% (351 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-06-30 00:32:36 +02:00
advplyr
67bab72783 Update podcast library latest page number of episodes to 50 #1615
Some checks failed
Build APK / main (push) Has been cancelled
Publish Test App / build (push) Has been cancelled
Verify all i18n files are alphabetized / update_translations (push) Has been cancelled
Publish Test App / deploy (push) Has been cancelled
2025-06-29 17:32:29 -05:00
advplyr
b9c084a442
Merge pull request #1612 from advplyr/create_bookmarks_focus
Some checks failed
Build APK / main (push) Has been cancelled
Publish Test App / build (push) Has been cancelled
Verify all i18n files are alphabetized / update_translations (push) Has been cancelled
Publish Test App / deploy (push) Has been cancelled
Update create bookmark to not autofocus input and use placeholder
2025-06-22 17:51:26 -05:00
advplyr
a77f345596 Update create bookmark to not autofocus input and leave empty with placeholder 2025-06-22 17:36:09 -05:00
advplyr
6a2f487ed5
Merge pull request #1608 from NickSkier/ui_black_theme
Some checks failed
Build APK / main (push) Has been cancelled
Publish Test App / build (push) Has been cancelled
Verify all i18n files are alphabetized / update_translations (push) Has been cancelled
Publish Test App / deploy (push) Has been cancelled
Add: UI Black/OLED theme
2025-06-20 16:55:41 -05:00
advplyr
616cf9029d Fix audio player colors for black theme 2025-06-20 16:51:19 -05:00
NickSkier
3f21b4172b Add: UI Black/OLED theme 2025-06-20 01:05:46 +03:00
advplyr
62f6a11522
Merge pull request #1589 from weblate/weblate-audiobookshelf-abs-mobile-app
Some checks are pending
Build APK / main (push) Waiting to run
Publish Test App / build (push) Waiting to run
Publish Test App / deploy (push) Blocked by required conditions
Verify all i18n files are alphabetized / update_translations (push) Waiting to run
Translations update from Hosted Weblate
2025-06-19 16:24:42 -05:00
burghy86
ae1dce455d
Translated using Weblate (Italian)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2025-06-19 23:01:48 +02:00
B0rax
7b5dc52620
Translated using Weblate (German)
Currently translated at 99.7% (351 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-06-19 23:01:47 +02:00
Mathias Franco
0bf50de54d
Translated using Weblate (Dutch)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2025-06-17 22:01:57 +02:00
biuklija
1eec470902
Translated using Weblate (Croatian)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-06-17 22:01:56 +02:00
petr-prikryl
867dc69479
Translated using Weblate (Czech)
Currently translated at 99.1% (349 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2025-06-17 22:01:54 +02:00
ugyes
9792f4ebc1
Translated using Weblate (Hungarian)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2025-06-12 11:02:03 +02:00
Dawid Kuźnicki
a44fc41caa
Translated using Weblate (Polish)
Currently translated at 98.2% (346 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-06-10 09:06:17 +00:00
Grzegorz Orlowski
1927dbf0ad
Translated using Weblate (Polish)
Currently translated at 98.2% (346 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-06-10 09:06:16 +00:00
Rekentek
7052674f32
Translated using Weblate (Dutch)
Currently translated at 98.0% (345 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2025-06-10 09:06:16 +00:00
Daniel Schosser
9a487f3bb6
Translated using Weblate (German)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-06-10 09:06:15 +00:00
David Havndrup Munch
d6242c350d
Translated using Weblate (Danish)
Currently translated at 98.0% (345 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2025-06-10 09:06:14 +00:00
Plazec
4cc4aceb41
Translated using Weblate (Czech)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2025-06-10 09:06:13 +00:00
peter cerny
dd8f4d6201
Translated using Weblate (Slovak)
Currently translated at 99.7% (351 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2025-06-07 21:01:56 +02:00
Usama Khalil
f44dcdf124
Translated using Weblate (Arabic)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-06-07 21:01:54 +02:00
thehijacker
65e8f2317e
Translated using Weblate (Slovenian)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2025-06-07 21:01:51 +02:00
SunSpring
1c4f43db7c
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-06-07 21:01:49 +02:00
max grakov
24b9df6fbd
Translated using Weblate (Russian)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2025-06-07 21:01:47 +02:00
Максим Горпиніч
c46891ecb0
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (352 of 352 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-06-06 00:01:47 +02:00
Anders Norman
e723601f6b
Translated using Weblate (Norwegian Bokmål)
Currently translated at 71.5% (251 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2025-06-04 00:17:08 +02:00
DR
77545df174
Translated using Weblate (Hebrew)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/he/
2025-06-04 00:17:07 +02:00
Grzegorz Orlowski
7e136b17a5
Translated using Weblate (Polish)
Currently translated at 98.2% (345 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-06-04 00:17:07 +02:00
Adolfo Jayme Barrientos
a970a06f57
Translated using Weblate (Catalan)
Currently translated at 80.0% (281 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ca/
2025-06-04 00:17:06 +02:00
ABS translator
95dd78d4aa
Translated using Weblate (Arabic)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-06-04 00:17:05 +02:00
Grzegorz Orlowski
eeaf33be20
Translated using Weblate (Polish)
Currently translated at 96.8% (340 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-06-04 00:17:04 +02:00
Adolfo Jayme Barrientos
7ed0cd26fe
Translated using Weblate (Spanish)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2025-06-04 00:17:03 +02:00
advplyr
d26403c800 Add book/podcast library filter for RSS Feed Open 2025-06-03 17:16:54 -05:00
advplyr
5ed6e3a6a3
Merge pull request #1573 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-05-24 17:11:31 -05:00
peter cerny
7e83405370
Translated using Weblate (Slovak)
Currently translated at 99.7% (350 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2025-05-24 00:19:32 +02:00
biuklija
a5de31da2b
Translated using Weblate (Croatian)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-05-24 00:19:31 +02:00
peter cerny
39c06005ed
Translated using Weblate (Slovak)
Currently translated at 93.7% (329 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2025-05-24 00:19:30 +02:00
Usama Khalil
9479160044
Translated using Weblate (Arabic)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-05-24 00:19:30 +02:00
Usama Khalil
46ec7369e5
Translated using Weblate (Arabic)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-05-24 00:19:29 +02:00
ABS translator
424e7d742b
Translated using Weblate (Arabic)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-05-24 00:19:28 +02:00
Usama Khalil
44036399b9
Translated using Weblate (Arabic)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-05-24 00:19:28 +02:00
Adolfo Jayme Barrientos
e4394da467
Translated using Weblate (Catalan)
Currently translated at 79.4% (279 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ca/
2025-05-24 00:19:27 +02:00
Usama Khalil
b837ada3f8
Translated using Weblate (Arabic)
Currently translated at 76.6% (269 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-05-24 00:19:27 +02:00
Adolfo Jayme Barrientos
a623de89aa
Translated using Weblate (Spanish)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2025-05-24 00:19:26 +02:00
peter cerny
2768407907
Translated using Weblate (Slovak)
Currently translated at 85.4% (300 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2025-05-24 00:19:26 +02:00
polarwood
64ed35d2b1
Translated using Weblate (Turkish)
Currently translated at 77.7% (273 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/tr/
2025-05-24 00:19:25 +02:00
J. Lavoie
b92f4dfa02
Translated using Weblate (Italian)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2025-05-24 00:19:25 +02:00
J. Lavoie
e4f02b7a51
Translated using Weblate (French)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2025-05-24 00:19:24 +02:00
J. Lavoie
24c3f45018
Translated using Weblate (German)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-05-24 00:19:23 +02:00
Michal
fcf55eb581
Translated using Weblate (Slovak)
Currently translated at 54.4% (191 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2025-05-24 00:19:23 +02:00
peter cerny
9ade7add32
Translated using Weblate (Slovak)
Currently translated at 54.4% (191 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2025-05-24 00:19:22 +02:00
Jaakko Rantamäki
d16dfcb4b4
Translated using Weblate (Finnish)
Currently translated at 98.2% (345 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2025-05-24 00:19:22 +02:00
Максим Горпиніч
37c58a5d32
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-05-24 00:19:21 +02:00
peter cerny
e6cdde4f70
Translated using Weblate (Slovak)
Currently translated at 13.6% (48 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2025-05-24 00:19:20 +02:00
Oleg Ivasenko
93c0adecbd
Translated using Weblate (Russian)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2025-05-24 00:19:20 +02:00
advplyr
33d44a5181
Merge pull request #1584 from advplyr/abridged_indicator
Add abridged indicator #1477
2025-05-23 17:19:11 -05:00
advplyr
80e6fa7e7a Add abridged indicator #1477 2025-05-23 17:04:26 -05:00
advplyr
b8eed48112
Merge pull request #1582 from advplyr/handle_android_edgetoedge
Fix android system bars overlapping UI #1564 #1574
2025-05-21 17:52:19 -05:00
advplyr
8d563dcfed Remove unnecessary styles.xml 2025-05-21 17:38:05 -05:00
advplyr
1cf36e5549 Fix android system bars overlapping UI #1564 #1574 2025-05-21 17:26:31 -05:00
advplyr
1357a0628f
Merge pull request #1577 from golinski/jackson-reuse
Reuse the existing ObjectMapper
2025-05-18 17:13:58 -05:00
advplyr
c4fe0680f3 Remove unused imports 2025-05-18 17:06:06 -05:00
advplyr
16694aa932 Update progress bar box shadow for visibility #1576 2025-05-17 17:06:28 -05:00
Michał Goliński
ea59ad2953 Reuse the existing ObjectMapper 2025-05-17 20:37:59 +02:00
advplyr
cf2f684e80
Merge pull request #1557 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-05-10 16:56:47 -05:00
Nicholas W
01522eda43
Added translation using Weblate (Slovak) 2025-05-10 15:44:43 +02:00
Adolfo Jayme Barrientos
a200ee1310
Translated using Weblate (Catalan)
Currently translated at 78.9% (277 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ca/
2025-05-10 15:44:42 +02:00
Adolfo Jayme Barrientos
579a07baa2
Translated using Weblate (Catalan)
Currently translated at 78.6% (276 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ca/
2025-05-09 14:11:21 +02:00
Adolfo Jayme Barrientos
a82dd88b1f
Translated using Weblate (Spanish)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2025-05-09 14:11:21 +02:00
thehijacker
36ba98920a
Translated using Weblate (Slovenian)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2025-05-07 00:01:46 +02:00
Adolfo Jayme Barrientos
47cc0c34eb
Translated using Weblate (Spanish)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2025-05-07 00:01:45 +02:00
Adolfo Jayme Barrientos
53eaca8724
Translated using Weblate (Spanish)
Currently translated at 96.8% (340 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2025-05-04 23:19:23 +02:00
ABS translator
8a2814b391
Translated using Weblate (Arabic)
Currently translated at 69.8% (245 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-05-04 23:19:23 +02:00
burghy86
345c8ab217
Translated using Weblate (Italian)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2025-05-04 23:19:23 +02:00
ABS translator
7c010895e0
Translated using Weblate (Arabic)
Currently translated at 59.8% (210 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-05-04 23:19:23 +02:00
SunSpring
e716cfd19b
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-05-04 23:19:23 +02:00
Sergey Ponomarev
461a6c8be5
Translated using Weblate (Russian)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2025-05-04 23:19:23 +02:00
Максим Горпиніч
3360f1a3f6
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-05-04 23:19:23 +02:00
Robin Stolpe
8b939e8a02
Translated using Weblate (Swedish)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-05-04 23:19:23 +02:00
Fredrik Lindqvist
3a8998081c
Translated using Weblate (Swedish)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-05-04 23:19:23 +02:00
dvc05
9b986a7b15
Translated using Weblate (Norwegian Bokmål)
Currently translated at 71.5% (251 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2025-05-04 23:19:23 +02:00
Kabika82
69ab395798
Translated using Weblate (Hungarian)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2025-05-04 23:19:23 +02:00
biuklija
7aa3c33348
Translated using Weblate (Croatian)
Currently translated at 99.7% (350 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-05-04 23:19:23 +02:00
johes00
734328dd1f
Translated using Weblate (German)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-05-04 23:19:23 +02:00
kuci-JK
7d6c30e733
Translated using Weblate (Czech)
Currently translated at 100.0% (351 of 351 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2025-05-04 23:19:23 +02:00
advplyr
3e682dfe50 Fix local cover images not showing in android auto #1565 2025-05-04 16:19:16 -05:00
advplyr
50efda3858
Merge pull request #1561 from nichwall/android_vscode_subproject
Add: settings for gradle project in subfolder
2025-04-30 17:15:56 -05:00
advplyr
ca1b9e2bdb
Merge pull request #1562 from nichwall/workflow_updates
Template updates
2025-04-28 09:17:12 -05:00
Nicholas Wallace
aa3a91b9d9 Add pull request template 2025-04-27 16:58:50 -07:00
Nicholas Wallace
7117608445 Update app versions in issue template 2025-04-27 16:56:02 -07:00
Nicholas Wallace
6de640f53f Add: settings for gradle project in subfolder 2025-04-27 16:47:44 -07:00
advplyr
75627ea40f iOS version bump 0.9.81 & pod update 2025-04-26 10:03:57 -05:00
advplyr
0fa04d7d9a Version bump v0.9.81-beta 2025-04-25 16:56:34 -05:00
advplyr
1e76ebe075
Merge pull request #1556 from Zibbp/master
fix(InternalDownloadManager): add accept-encoding identity to requests
2025-04-25 16:25:07 -05:00
advplyr
965da2fee3
Merge pull request #1547 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-04-25 16:24:51 -05:00
Zibbp
b03f59ace3 fix(InternalDownloadManager): set accept-encoding header on just the download task 2025-04-25 15:51:46 -05:00
SunSpring
79c7244b24
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (349 of 349 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-04-25 00:29:31 +02:00
Pim
0843be3f4e
Translated using Weblate (Dutch)
Currently translated at 97.9% (342 of 349 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2025-04-25 00:29:31 +02:00
Kabika82
6911f368e5
Translated using Weblate (Hungarian)
Currently translated at 100.0% (349 of 349 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2025-04-25 00:29:31 +02:00
cebo29
42167ea738
Translated using Weblate (German)
Currently translated at 100.0% (349 of 349 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-04-25 00:29:31 +02:00
thehijacker
1c57d3506a
Translated using Weblate (Slovenian)
Currently translated at 100.0% (349 of 349 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2025-04-25 00:29:31 +02:00
Максим Горпиніч
8adc6fae4c
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (349 of 349 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-04-25 00:29:31 +02:00
NickSkier
e9746577ba
Translated using Weblate (Russian)
Currently translated at 100.0% (349 of 349 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2025-04-25 00:29:31 +02:00
max grakov
9f68730622
Translated using Weblate (Russian)
Currently translated at 100.0% (349 of 349 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2025-04-25 00:29:31 +02:00
biuklija
f019b67b0c
Translated using Weblate (Croatian)
Currently translated at 99.7% (348 of 349 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-04-25 00:29:31 +02:00
advplyr
0f650c0572
Merge pull request #1555 from advplyr/sleep_timer_chime_android
Add:Android sleep timer setting to play a chime when almost finished
2025-04-24 17:29:25 -05:00
Zibbp
dfc77ea0d0 fix(InternalDownloadManager): add accept-encoding identity to requests 2025-04-24 17:04:08 -05:00
advplyr
67f514524f Remove unused imports and release log 2025-04-24 17:03:14 -05:00
advplyr
d97c6a0872 Add:Android sleep timer setting to play a chime when almost finished #600 2025-04-24 16:42:55 -05:00
advplyr
e7ad62760f Fix android cover image not showing in notification for downloaded media #1493, upgrade glide 2025-04-23 17:01:52 -05:00
advplyr
fd34ea8124 Show change library dropdown in app bar when socket is not connected #1494 2025-04-23 15:46:19 -05:00
advplyr
5db94bf5b8 Fix series sequence text color on book cards in light mode #1549 2025-04-22 16:50:21 -05:00
advplyr
796d6d79d4 Add logs for android auto 2025-04-21 17:34:30 -05:00
advplyr
03aafafe1c Fix bug with AbsLogger not initialized 2025-04-21 17:11:23 -05:00
advplyr
3f303abc19 iOS version bump 0.9.80 2025-04-20 17:17:18 -05:00
advplyr
85d6958025 Version bump v0.9.80-beta 2025-04-20 16:54:49 -05:00
advplyr
669bd7827b
Merge pull request #1545 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-04-20 16:17:36 -05:00
petr-prikryl
2b48f0c6a9
Translated using Weblate (Czech)
Currently translated at 100.0% (345 of 345 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2025-04-20 21:11:10 +00:00
ABS translator
2c44d38906
Translated using Weblate (Arabic)
Currently translated at 52.1% (180 of 345 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-04-20 21:11:10 +00:00
Максим Горпиніч
750726ff6a
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (345 of 345 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-04-20 21:11:09 +00:00
Jakob Zoll
dafab492fe
Translated using Weblate (German)
Currently translated at 100.0% (345 of 345 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-04-20 21:11:08 +00:00
advplyr
6419c8dc3a Update deploy-apk workflow for java 21 2025-04-20 16:10:54 -05:00
advplyr
3bb5ce5924
Merge pull request #1546 from advplyr/abslogger
Logs page with AbsLogger plugin
2025-04-20 16:09:14 -05:00
advplyr
882c2749ab Add AbsLogger stub to iOS, update iOS plugins for capacitor 7 2025-04-20 16:06:52 -05:00
advplyr
26b0fae0fb Setup onLog event, add app version & platform to logs and share filename 2025-04-20 14:33:48 -05:00
advplyr
fe921fd5b1 Update AbsLogger to have a tag with logs 2025-04-20 12:36:27 -05:00
advplyr
88e1877742 More AbsLogs and clean logs older than 48 hours on init 2025-04-20 12:18:10 -05:00
advplyr
74758c7762 Add clear logs, use more menu dialog 2025-04-20 10:32:52 -05:00
advplyr
2000534e37 Update logs to mask server address, add share txt file button 2025-04-20 10:06:52 -05:00
advplyr
390388fe83 Add AbsLogger plugin, persist logs to db, logs page for android 2025-04-19 17:26:32 -05:00
advplyr
b9e3ccd0c1
Merge pull request #1543 from advplyr/capacitor_v7
Update to capacitor v7
2025-04-17 17:57:22 -05:00
advplyr
23e0a44e54 Merge branch 'capacitor_v7' of https://github.com/advplyr/audiobookshelf-app into capacitor_v7 2025-04-17 17:45:26 -05:00
advplyr
e788f8767a Update build-apk workflow to use java 21 2025-04-17 17:45:22 -05:00
advplyr
d09e88f138 Podfile update 2025-04-17 17:38:17 -05:00
advplyr
6e1eee48f0 Update to capacitor v7 2025-04-17 17:16:00 -05:00
advplyr
99c8949861
Merge pull request #1533 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-04-15 17:32:21 -05:00
polarwood
ce44ee8514
Translated using Weblate (Turkish)
Currently translated at 77.9% (268 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/tr/
2025-04-16 00:30:42 +02:00
Marc Casalprim
490b45d476
Translated using Weblate (Catalan)
Currently translated at 77.3% (266 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ca/
2025-04-16 00:30:41 +02:00
polarwood
e403304e46
Translated using Weblate (Turkish)
Currently translated at 76.1% (262 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/tr/
2025-04-16 00:30:40 +02:00
Ricky Tigg
2bc00bb7f7
Translated using Weblate (Finnish)
Currently translated at 100.0% (344 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2025-04-16 00:30:40 +02:00
Coxe
2785b1d09a
Translated using Weblate (Danish)
Currently translated at 100.0% (344 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2025-04-16 00:30:39 +02:00
advplyr
1d703f8c87 iOS update Podfile version to 14 2025-04-15 17:30:30 -05:00
advplyr
93748a917f Android update target SDK to 35, update androidx versions 2025-04-15 17:21:48 -05:00
advplyr
46d5e1d96c Update max socket reconnection to 15s, add socket reconnection logs 2025-04-14 16:17:09 -05:00
advplyr
1e791f9601 Update gradle versions and dependencies 2025-04-13 17:35:48 -05:00
advplyr
ea2e8c7cc9 Update bookshelf item width to more accurately fill shelf for smaller screens #1540 2025-04-11 17:46:19 -05:00
advplyr
9ca160f1da
Merge pull request #1529 from nichwall/replace_icons_with_symbols
Replace Material icons with symbols
2025-04-05 17:22:26 -05:00
advplyr
1b0843d12e Update Year in review icons, update some icons to be larger, fix read more/less, audio player use keyboard arrow down 2025-04-05 17:14:43 -05:00
Nicholas Wallace
b1c4ceb40a Replace: seek forward 10 seconds 2025-03-31 00:16:58 -07:00
Nicholas Wallace
ff626a3609 Fix sizing for bookshelf toolbar 2025-03-31 00:07:31 -07:00
Nicholas Wallace
0862aecfc9 Fix: fill for icons in side drawer 2025-03-31 00:04:45 -07:00
Nicholas Wallace
e119672336 Fix: icon sizing and alignment issues 2025-03-31 00:02:45 -07:00
Nicholas Wallace
71f6f53111 Fix: app bar alignment 2025-03-30 23:33:12 -07:00
Nicholas Wallace
8cf757c080 Replace all material-icons with material-symbols 2025-03-30 23:26:14 -07:00
Nicholas Wallace
6e0f67b19c Add woff2 fonts 2025-03-30 17:55:21 -07:00
advplyr
fa2400871a
Merge pull request #1518 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-03-30 04:56:59 -05:00
Augusto Massini Pinto
7ef3f1895e
Translated using Weblate (Portuguese (Brazil))
Currently translated at 87.7% (302 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2025-03-30 03:00:16 +02:00
Adolfo Jayme Barrientos
637911e6b7
Translated using Weblate (Catalan)
Currently translated at 75.0% (258 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ca/
2025-03-28 04:46:34 +01:00
Adolfo Jayme Barrientos
1b0d7c2463
Translated using Weblate (Spanish)
Currently translated at 98.8% (340 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2025-03-28 04:46:33 +01:00
Adolfo Jayme Barrientos
dfb793247c
Translated using Weblate (Catalan)
Currently translated at 74.7% (257 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ca/
2025-03-27 02:52:16 +01:00
Adolfo Jayme Barrientos
a730ca38f2
Translated using Weblate (Catalan)
Currently translated at 74.7% (257 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ca/
2025-03-24 04:09:53 +01:00
ConfusedAlex
e59a955e6b
Translated using Weblate (German)
Currently translated at 100.0% (344 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-03-23 15:59:59 +01:00
Jan-Eric Myhrgren
0fbe5f6c86
Translated using Weblate (Swedish)
Currently translated at 95.6% (329 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-03-23 10:03:24 +01:00
Adolfo Jayme Barrientos
312b04acc9
Translated using Weblate (Catalan)
Currently translated at 72.9% (251 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ca/
2025-03-22 22:56:26 +00:00
Adolfo Jayme Barrientos
8c20b7985b
Translated using Weblate (Catalan)
Currently translated at 72.9% (251 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ca/
2025-03-22 22:56:25 +00:00
Adolfo Jayme Barrientos
c10a82842e
Translated using Weblate (Spanish)
Currently translated at 98.8% (340 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2025-03-22 22:56:25 +00:00
Adolfo Jayme Barrientos
7a608bb2b2
Translated using Weblate (Catalan)
Currently translated at 72.6% (250 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ca/
2025-03-22 22:56:24 +00:00
Adolfo Jayme Barrientos
a8493b2778
Translated using Weblate (Italian)
Currently translated at 99.7% (343 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2025-03-22 22:56:23 +00:00
Adolfo Jayme Barrientos
02a6dd71d4
Translated using Weblate (Spanish)
Currently translated at 98.5% (339 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2025-03-22 22:56:23 +00:00
Adolfo Jayme Barrientos
bb5e1e6d71
Translated using Weblate (Catalan)
Currently translated at 57.8% (199 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ca/
2025-03-22 22:56:22 +00:00
Adolfo Jayme Barrientos
bfbb04dc93
Translated using Weblate (Spanish)
Currently translated at 98.5% (339 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2025-03-22 22:56:21 +00:00
Adolfo Jayme Barrientos
8f1b92add5
Translated using Weblate (Catalan)
Currently translated at 56.6% (195 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ca/
2025-03-22 22:56:21 +00:00
Adolfo Jayme Barrientos
cab98eff3d
Translated using Weblate (Spanish)
Currently translated at 98.5% (339 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2025-03-22 22:56:20 +00:00
Adolfo Jayme Barrientos
4877d41ff2
Translated using Weblate (Catalan)
Currently translated at 56.3% (194 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ca/
2025-03-22 22:56:20 +00:00
Adolfo Jayme Barrientos
01c5c03653
Translated using Weblate (Spanish)
Currently translated at 98.5% (339 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2025-03-22 22:56:19 +00:00
Adolfo Jayme Barrientos
be31f265b8
Translated using Weblate (Spanish)
Currently translated at 98.2% (338 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2025-03-22 22:56:18 +00:00
Jan-Eric Myhrgren
6ef9721b7d
Translated using Weblate (Swedish)
Currently translated at 95.6% (329 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-03-22 22:56:18 +00:00
Jan-Eric Myhrgren
13a50c990b
Translated using Weblate (Swedish)
Currently translated at 95.6% (329 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-03-22 22:56:17 +00:00
J. Lavoie
67e2f2366d
Translated using Weblate (Italian)
Currently translated at 99.7% (343 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2025-03-22 22:56:16 +00:00
J. Lavoie
0947c2fe6f
Translated using Weblate (French)
Currently translated at 100.0% (344 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2025-03-22 22:56:16 +00:00
J. Lavoie
7c8d5658f3
Translated using Weblate (German)
Currently translated at 99.7% (343 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-03-22 22:56:15 +00:00
SunSpring
66cf4639e6
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (344 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-03-22 22:56:14 +00:00
advplyr
8b370da5b7
Merge pull request #1523 from complacentsee/ios_seekbacktrackbugfix
iOS: Bugfix: resolve race condition when rebuilding queue for seeking back
2025-03-22 17:56:07 -05:00
advplyr
a6d49fc926
Merge pull request #1522 from 4ch1m/sort_podcasts_by_filename
sort podcasts by filename; display filename in table row
2025-03-22 17:54:54 -05:00
advplyr
8aedd1cd95 Update filename style on episode row 2025-03-22 17:46:45 -05:00
Adam Traeger
647bd8193b
iOS: resolve race condition when rebuilding queue for seeking back 2025-03-22 10:21:26 -05:00
Achim
b6349fb3a7
sort podcasts by filename; display filename in table row 2025-03-22 12:50:08 +01:00
advplyr
675c10f7f7
Merge pull request #1500 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-03-15 17:44:16 -05:00
ejlaner
5ab5609397
Translated using Weblate (Japanese)
Currently translated at 3.4% (12 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ja/
2025-03-15 22:31:25 +00:00
Xeratone
6de4bc14af
Translated using Weblate (Japanese)
Currently translated at 0.8% (3 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ja/
2025-03-15 22:31:23 +00:00
Jan-Eric Myhrgren
8579536eff
Translated using Weblate (Swedish)
Currently translated at 95.6% (329 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-03-15 22:31:22 +00:00
thehijacker
36ec19e25e
Translated using Weblate (Slovenian)
Currently translated at 100.0% (344 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2025-03-15 22:31:22 +00:00
Simple16
b6b54e04b8
Translated using Weblate (Russian)
Currently translated at 100.0% (344 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2025-03-15 22:31:21 +00:00
biuklija
5e4ac06493
Translated using Weblate (Croatian)
Currently translated at 100.0% (344 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-03-15 22:31:20 +00:00
Ricky Tigg
1f05e18ce4
Translated using Weblate (Finnish)
Currently translated at 99.7% (343 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2025-03-15 22:31:20 +00:00
Максим Горпиніч
cd059542c6
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (344 of 344 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-03-15 22:31:19 +00:00
Jan Schoenfeld
b207c18abe
Translated using Weblate (German)
Currently translated at 99.4% (341 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-03-15 22:31:18 +00:00
Krissse10
6b7d3775e4
Translated using Weblate (Swedish)
Currently translated at 95.9% (329 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-03-15 22:31:18 +00:00
Miró Allard
7e1d942e0a
Translated using Weblate (Swedish)
Currently translated at 95.9% (329 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-03-15 22:31:17 +00:00
Peter
5cb49daaba
Translated using Weblate (German)
Currently translated at 98.8% (339 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-03-15 22:31:16 +00:00
Troj@
4e132dcc28
Translated using Weblate (Belarusian)
Currently translated at 100.0% (343 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2025-03-15 22:31:16 +00:00
Troj@
288ef8f368
Translated using Weblate (Belarusian)
Currently translated at 100.0% (343 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2025-03-15 22:31:15 +00:00
Andreas Morell-Reng
66206adbe0
Translated using Weblate (Danish)
Currently translated at 100.0% (343 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2025-03-15 22:31:14 +00:00
thehijacker
c6529ed8a3
Translated using Weblate (Slovenian)
Currently translated at 100.0% (343 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2025-03-15 22:31:13 +00:00
Максим Горпиніч
6ddb44afb6
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (343 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-03-15 22:31:13 +00:00
Simple16
7f84bbe43c
Translated using Weblate (Russian)
Currently translated at 100.0% (343 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2025-03-15 22:31:12 +00:00
Krissse10
51b8eece7e
Translated using Weblate (Swedish)
Currently translated at 95.3% (327 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-03-15 22:31:11 +00:00
Fredrik Lindqvist
c891c227a6
Translated using Weblate (Swedish)
Currently translated at 95.3% (327 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-03-15 22:31:11 +00:00
Prashant Mhatre
03457ef138
Translated using Weblate (Hindi)
Currently translated at 8.4% (29 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hi/
2025-03-15 22:31:10 +00:00
advplyr
36fb117f89
Merge pull request #1514 from complacentsee/ios_seek_issue_452
Bugfix issue 452:  iOS seek issue at end of book
2025-03-15 17:31:00 -05:00
Adam Traeger
605b52df0e
Fix getCurrentTime for streaming session.
This simplifies logic to  use session current time when session current track start offset is unavailable.
2025-03-14 20:07:33 -05:00
Adam Traeger
45d3a15c68
Rebuild track queue while seeking if player does not have a current track. 2025-03-13 23:00:53 -05:00
Adam Traeger
78d7ba69df
Fallback to session details if player has no active track.
Provides better handling if end of book has been reached.
2025-03-13 22:50:30 -05:00
Adam Traeger
c72f7cddc8
Mark session as inactive if end of queue is reached.
Ensures UI shows paused and stops incrementing session time.
2025-03-13 22:48:06 -05:00
advplyr
e7c3242765
Merge pull request #1512 from complacentsee/ios_autorewind
IOS: Support autoRewind and disableAutoRewind setting
2025-03-12 17:12:58 -05:00
Adam Traeger
35d60ecaa0
Match rewind times to android 2025-03-11 19:52:07 -05:00
advplyr
bcf40c0b76
Merge pull request #1508 from complacentsee/ios_sleep_fadeout
IOS: Add support for fading out on sleep timer end
2025-03-11 17:30:57 -05:00
Adam Traeger
922601eab7
IOS: Support autoRewind and disableAutoRewind setting 2025-03-10 20:27:57 -05:00
Adam Traeger
bc4267ea1d
Merge branch 'advplyr:master' into ios_sleep_fadeout 2025-03-09 20:23:44 -05:00
advplyr
f65d3213fe
Merge pull request #1507 from NickSkier/black_theme_epub_reader
Add:Black/oled theme for epub reader
2025-03-09 17:26:40 -05:00
advplyr
bf2107c01b Use tailwind class for bg-black 2025-03-09 17:25:59 -05:00
Adam Traeger
33c738873f
Add support for fading out on sleep timer end
Playback will start to fadeout during last 60 seconds of the sleep timer. Once faded out, playback will be paused, volume reset, and playback seeked to start of fadeout.
2025-03-09 13:14:15 -05:00
NickSkier
5bf724b2a2 Add:Black/oled theme for epub reader 2025-03-09 18:08:10 +03:00
advplyr
769ce0ade9 Update comic reader bottom page number bar to only show when top bar is showing #1485 2025-03-05 17:49:20 -06:00
advplyr
6f01eafd30 Fix download location modal for light mode #1504 2025-03-03 17:28:07 -06:00
advplyr
5ec27932b0
Merge pull request #1502 from nichwall/bug_report_template_issue
Fix: validation and strings for options
2025-03-01 17:55:23 -06:00
Nicholas Wallace
d79ff6e6d5 Fix: validation and strings for options 2025-03-01 16:49:30 -07:00
advplyr
b0e7d3af2c
Merge pull request #1499 from nichwall/report_issue
Add question to confirm they have checked for awaiting release
2025-03-01 17:37:47 -06:00
advplyr
f6966efff7
Merge pull request #1501 from nichwall/typo_fix
Fix: bitrate typo
2025-03-01 17:37:09 -06:00
Nicholas Wallace
79e71a6978 Fix: bitrate typo 2025-03-01 15:50:04 -07:00
Nicholas Wallace
5beed3e824 Add question to confirm they have checked for awaiting release 2025-02-27 18:25:17 -07:00
advplyr
c6ea9daa3a
Merge pull request #1484 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-02-27 18:04:20 -06:00
Troj@
95440951e5
Translated using Weblate (Belarusian)
Currently translated at 100.0% (343 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2025-02-27 19:02:09 +01:00
phewi
7a45da2293
Translated using Weblate (Finnish)
Currently translated at 100.0% (343 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2025-02-27 19:02:09 +01:00
Ricky Tigg
8ae8390339
Translated using Weblate (Finnish)
Currently translated at 100.0% (343 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2025-02-27 19:02:08 +01:00
Jan-Eric Myhrgren
27bc1f4a66
Translated using Weblate (Swedish)
Currently translated at 92.1% (316 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-02-27 19:02:08 +01:00
Troj@
b887052d65
Translated using Weblate (Belarusian)
Currently translated at 90.9% (312 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2025-02-26 02:05:07 +01:00
Krissse10
90bd2fc54c
Translated using Weblate (Swedish)
Currently translated at 92.1% (316 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-02-26 02:05:06 +01:00
Troja
e433f7173f
Translated using Weblate (Belarusian)
Currently translated at 66.4% (228 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2025-02-24 11:06:37 +01:00
burghy86
a1ce605719
Translated using Weblate (Italian)
Currently translated at 100.0% (343 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2025-02-24 11:06:36 +01:00
Olivier Turcot
665cbddcf5
Translated using Weblate (French)
Currently translated at 100.0% (343 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2025-02-24 11:06:35 +01:00
Troja
6c0a39099f
Translated using Weblate (Belarusian)
Currently translated at 47.2% (162 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2025-02-22 16:05:53 +01:00
Jan-Eric Myhrgren
b2576d0b35
Translated using Weblate (Swedish)
Currently translated at 90.6% (311 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-02-22 16:05:52 +01:00
Michał Rączka-Dudek
7107ea4aff
Translated using Weblate (Polish)
Currently translated at 97.6% (335 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-02-21 14:21:18 +01:00
Nicholas W
2c43a71850
Added translation using Weblate (Romanian) 2025-02-20 14:32:49 +01:00
Milo Ivir
16b93bc490
Translated using Weblate (Croatian)
Currently translated at 100.0% (343 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-02-20 14:32:49 +01:00
polarwood
d56a77acdf
Translated using Weblate (Turkish)
Currently translated at 76.6% (263 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/tr/
2025-02-19 19:01:53 +01:00
thehijacker
5da1dd1756
Translated using Weblate (Slovenian)
Currently translated at 100.0% (343 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2025-02-19 19:01:53 +01:00
SunSpring
66e57e8d8f
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (343 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-02-19 19:01:52 +01:00
Максим Горпиніч
db1d7ae549
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (343 of 343 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-02-19 19:01:51 +01:00
advplyr
3d85c4085b
Merge pull request #1466 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-02-18 17:18:02 -06:00
polarwood
da23b2cc51
Translated using Weblate (Turkish)
Currently translated at 76.3% (261 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/tr/
2025-02-18 00:48:26 +00:00
Armanc Keser
38926a5b4f
Translated using Weblate (Turkish)
Currently translated at 63.4% (217 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/tr/
2025-02-18 00:48:25 +00:00
Troja
b39003d519
Translated using Weblate (Belarusian)
Currently translated at 24.2% (83 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2025-02-18 00:48:24 +00:00
biuklija
c79ea6ffd2
Translated using Weblate (Croatian)
Currently translated at 100.0% (342 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-02-18 00:48:23 +00:00
polarwood
a7f1ead7fe
Translated using Weblate (Turkish)
Currently translated at 59.9% (205 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/tr/
2025-02-18 00:48:22 +00:00
Jan-Eric Myhrgren
3c56087432
Translated using Weblate (Swedish)
Currently translated at 90.6% (310 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-02-18 00:48:22 +00:00
skrido
1e98dea091
Translated using Weblate (German)
Currently translated at 98.8% (338 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-02-18 00:48:21 +00:00
Troja
d239d06c3e
Translated using Weblate (Belarusian)
Currently translated at 23.3% (80 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2025-02-18 00:48:20 +00:00
Ivan Penchev
339393e904
Translated using Weblate (Bulgarian)
Currently translated at 100.0% (342 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/bg/
2025-02-18 00:48:19 +00:00
Troja
40cfb2593c
Translated using Weblate (Belarusian)
Currently translated at 21.9% (75 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2025-02-18 00:48:18 +00:00
burghy86
346b418908
Translated using Weblate (Italian)
Currently translated at 100.0% (342 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2025-02-18 00:48:17 +00:00
Jan-Eric Myhrgren
03c2850c38
Translated using Weblate (Swedish)
Currently translated at 90.6% (310 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-02-18 00:48:17 +00:00
Jan-Eric Myhrgren
1c29a08679
Translated using Weblate (Swedish)
Currently translated at 90.3% (309 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-02-18 00:48:16 +00:00
advplyr
db89ac7743
Merge pull request #1475 from nichwall/replace_cast_icon
Use `cast-connected` icon instead of green icon
2025-02-17 18:01:53 -06:00
advplyr
412333cfc4
Merge pull request #1473 from nichwall/local_folders_ui_update
Add description for local folder usage
2025-02-17 17:58:16 -06:00
advplyr
033b0b6ebf Update cast icon to black on light covers 2025-02-17 17:58:03 -06:00
advplyr
ab02ce5601 Update info icon alignment 2025-02-17 17:37:52 -06:00
advplyr
d46777595d
Merge pull request #1471 from nichwall/remove_local_only_android
Remove local only android
2025-02-17 17:29:31 -06:00
Nicholas Wallace
669ced862e Use cast-connected icon instead of green icon 2025-02-09 14:06:47 -07:00
Nicholas Wallace
ba66cc02b7 Reworded local folder description 2025-02-08 23:01:07 -07:00
Nicholas Wallace
1e840250b9 Add: info bubble for local folders 2025-02-08 19:07:32 -07:00
Nicholas Wallace
2b615a51fb Remove: unused AudioProbeResult.kt 2025-02-08 12:45:05 -07:00
Nicholas Wallace
f0c92be5f2 Auto format FolderScanner.kt 2025-02-08 12:41:30 -07:00
Nicholas Wallace
20206d6e14 Remove: unused functions from LocalMediaItem.kt 2025-02-08 12:01:38 -07:00
Nicholas Wallace
7c6e098014 Remove: local library only item from playback and syncing media 2025-02-08 11:18:46 -07:00
Nicholas Wallace
1e0f1f329f Clean local only when checking if files exist 2025-02-08 10:41:58 -07:00
Nicholas Wallace
b2ebeafed5 Autoformatting files 2025-02-08 10:36:03 -07:00
Nicholas Wallace
9492975a74 Remove: local only items from frontend 2025-02-08 09:09:18 -07:00
advplyr
e194df455b
Merge pull request #1469 from nichwall/download_manager_cleanup
Download manager cleanup
2025-02-07 17:13:51 -06:00
advplyr
1e01139028
Merge pull request #1468 from nichwall/devicemanager_settings_revert
Devicemanager settings revert
2025-02-07 17:12:11 -06:00
Nicholas Wallace
1141c6f7a5 Remove read timeout for connection 2025-02-06 18:28:22 -07:00
Nicholas Wallace
5a1951b495 InternalDownloadManager autoformatting and comments 2025-02-06 18:26:13 -07:00
Nicholas Wallace
3b3f94124a Iniital refactor and adding comments 2025-02-06 18:16:56 -07:00
Nicholas Wallace
bb56a55143 Fix: end time default value 2025-02-06 17:58:10 -07:00
Nicholas Wallace
853a12b0dd Revert "Simplify default value assignment"
This reverts commit d4090d15be.
2025-02-06 17:45:33 -07:00
advplyr
840641681e
Merge pull request #1459 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-02-04 17:37:55 -06:00
Jan-Eric Myhrgren
022951f406
Translated using Weblate (Swedish)
Currently translated at 90.3% (309 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-02-04 15:11:12 +01:00
biuklija
b6c9df62eb
Translated using Weblate (Croatian)
Currently translated at 100.0% (342 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-02-04 15:11:12 +01:00
advplyr
27a14a6e3b
Added translation using Weblate (Turkish) 2025-02-02 22:05:07 +00:00
SunSpring
45e3620634
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (342 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-02-02 22:05:06 +00:00
Simple16
d770d6aac7
Translated using Weblate (Russian)
Currently translated at 100.0% (342 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2025-02-02 22:05:06 +00:00
Andreas Morell-Reng
37d60252fd
Translated using Weblate (Danish)
Currently translated at 100.0% (342 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2025-02-02 22:05:05 +00:00
Jan-Eric Myhrgren
1cdcb48c1f
Translated using Weblate (Swedish)
Currently translated at 90.3% (309 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-02-02 22:05:04 +00:00
Will Forde
f9920a0b6b
Translated using Weblate (Japanese)
Currently translated at 0.2% (1 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ja/
2025-02-02 22:05:03 +00:00
thehijacker
00f524db16
Translated using Weblate (Slovenian)
Currently translated at 100.0% (342 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2025-02-02 22:05:03 +00:00
Максим Горпиніч
51401e333f
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (342 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-02-02 22:05:02 +00:00
ugyes
7692c29a81
Translated using Weblate (Hungarian)
Currently translated at 99.4% (340 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2025-02-02 22:05:01 +00:00
biuklija
d7c755235e
Translated using Weblate (Croatian)
Currently translated at 100.0% (342 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-02-02 22:05:01 +00:00
Andreas Morell-Reng
8589efdf6e
Translated using Weblate (Danish)
Currently translated at 100.0% (342 of 342 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2025-02-02 22:05:00 +00:00
advplyr
02b83f02d6
Merge pull request #1464 from nichwall/sleep_timer_cleanup_2
Android Sleep Timer cleanup part 2
2025-02-02 16:04:52 -06:00
Nicholas Wallace
e4e7679414 Fix: sleep timer gets stuck at 1 second 2025-02-02 14:23:58 -07:00
advplyr
abdf51d045
Merge pull request #1462 from nichwall/andriod_deviceManager_cleanup
Andriod `DeviceManager.kt` cleanup
2025-02-02 15:04:49 -06:00
Nicholas Wallace
5fd21c8393 Change: sleep timer EoC cutoff to be 10 seconds 2025-02-02 13:00:28 -07:00
Nicholas Wallace
e7e03697d6 Fix: shake to reset only during grace period or while playing 2025-02-02 12:49:08 -07:00
Nicholas Wallace
9e7a76bd97 Simplify nullable return 2025-02-01 15:32:34 -07:00
Nicholas Wallace
d4090d15be Simplify default value assignment 2025-02-01 15:29:35 -07:00
Nicholas Wallace
973dca83a2 Add: function comments, autoformatting applied 2025-02-01 15:01:12 -07:00
advplyr
c7f51e815c
Merge pull request #1461 from nichwall/build_apk_workflow_update
Build apk workflow update
2025-01-31 16:40:49 -06:00
Nicholas Wallace
1c302a7ac1 Update deploy APK workflow 2025-01-29 21:03:50 -07:00
Nicholas Wallace
5d29efe1d5 Update: upload_artifact to v4 due to deprecation
https://github.blog/changelog/2024-04-16-deprecation-notice-v3-of-the-artifact-actions/
2025-01-29 20:52:41 -07:00
advplyr
b066df4efc Update issue template app versions 2025-01-29 17:09:12 -06:00
advplyr
fa6b71afae
Merge pull request #1460 from nichwall/close_blank_issue
Add: workflow to close issues without template
2025-01-29 17:07:35 -06:00
advplyr
3d0c064d41 Github issue template add config.yaml 2025-01-29 17:04:39 -06:00
Nicholas Wallace
50755ead18 Add: workflow to close issues without template 2025-01-28 20:11:23 -07:00
advplyr
79f7fa32ab iOS version bump 0.9.79 2025-01-26 17:11:49 -06:00
advplyr
703ab710e9 Version bump v0.9.79-beta 2025-01-26 16:41:06 -06:00
advplyr
bc782ba9ee
Merge pull request #1451 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-01-26 16:16:20 -06:00
Максим Горпиніч
f2eef64d84
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (341 of 341 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-01-26 22:35:04 +01:00
Simple16
abb3a7a3a9
Translated using Weblate (Russian)
Currently translated at 100.0% (340 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2025-01-26 22:35:03 +01:00
advplyr
ef61e020b6
Added translation using Weblate (Japanese) 2025-01-26 22:35:03 +01:00
Jan-Eric Myhrgren
66c87f37e6
Translated using Weblate (Swedish)
Currently translated at 91.1% (310 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-26 22:35:02 +01:00
Jan-Eric Myhrgren
56caab19f1
Translated using Weblate (Swedish)
Currently translated at 91.1% (310 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-26 22:35:02 +01:00
Jan-Eric Myhrgren
27eb5f72f7
Translated using Weblate (Swedish)
Currently translated at 91.1% (310 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-26 22:35:01 +01:00
advplyr
23a80a60b9 Show missing button with dialog, show missing indicator on covers 2025-01-26 15:34:54 -06:00
advplyr
0520cbd538 Update collection/playlist play button to show pause when playing #1394, update collections to play local item if available 2025-01-26 15:11:49 -06:00
advplyr
c79ecbb92e Fix crash downloading with old server, check server version to append token on image requests #1450 2025-01-26 14:22:35 -06:00
advplyr
08651d28ef
Merge pull request #1455 from anstosa/defaultpodcastsort
Sensible default sort for podcasts
2025-01-26 12:49:48 -06:00
advplyr
3060d186a1
Merge pull request #1453 from nichwall/android_sleep_timer_cleanup
Android sleep timer cleanup
2025-01-26 12:47:09 -06:00
advplyr
0509d7105e Update confirm disable auto timer message 2025-01-26 12:40:39 -06:00
advplyr
b6ab7dc8a7 Update sleep timer modal to not show negative time remaining 2025-01-26 12:36:11 -06:00
Ansel Santosa
822ca65349 Sensible default sort for podcasts 2025-01-26 07:43:44 -08:00
advplyr
4b4a2b46c1 Merge branch 'master' into android_sleep_timer_cleanup 2025-01-26 09:36:48 -06:00
Nicholas Wallace
13b020732f General cleanup, only disable auto-sleep temporarily
Auto sleep timer is only disabled until the end of
the current time period (e.g. when the sleep timer
would be disabled automatically).
2025-01-25 19:00:43 -07:00
advplyr
fb9ca7f5f3 Update item description to support rich text #1281 2025-01-25 14:10:35 -06:00
advplyr
e21d37b20f Remove con.android.systemui from valid media browsers #1446 2025-01-25 12:27:43 -06:00
Nicholas Wallace
161614f6c9 Fix: scale end of chapter time by playback speed 2025-01-24 22:18:03 -07:00
Nicholas Wallace
16472e1de8 Simplify increase/decrease sleep timer 2025-01-24 20:05:43 -07:00
Nicholas Wallace
d81e47204c Auto formatted code and added function comments 2025-01-24 19:55:03 -07:00
advplyr
7ec93cd91e
Merge pull request #1444 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-01-23 04:12:43 -06:00
Jan-Eric Myhrgren
af516991a9
Translated using Weblate (Swedish)
Currently translated at 91.1% (310 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-23 08:55:12 +01:00
Jan-Eric Myhrgren
2cac88cf7c
Translated using Weblate (Swedish)
Currently translated at 91.1% (310 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-23 08:49:26 +01:00
Milo Ivir
1546a85195
Translated using Weblate (Croatian)
Currently translated at 100.0% (340 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-01-23 02:01:54 +01:00
Lucas
9eba3e4dc4
Translated using Weblate (Spanish)
Currently translated at 100.0% (340 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2025-01-23 02:01:53 +01:00
akynr
b568e2ecf1
Translated using Weblate (German)
Currently translated at 99.1% (337 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-01-23 02:01:53 +01:00
Lukas Eßmann
9dce119530
Translated using Weblate (German)
Currently translated at 99.1% (337 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-01-23 02:01:53 +01:00
Andreas Morell-Reng
24f9b134cd
Translated using Weblate (Danish)
Currently translated at 100.0% (340 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2025-01-21 13:07:16 +01:00
Andreas Morell-Reng
c3e6bdd73d
Translated using Weblate (Danish)
Currently translated at 100.0% (340 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2025-01-21 12:34:57 +01:00
Andreas Morell-Reng
80e11c1492
Translated using Weblate (Danish)
Currently translated at 100.0% (340 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2025-01-21 11:59:05 +01:00
Andreas Morell-Reng
bdebb4972e
Translated using Weblate (Danish)
Currently translated at 100.0% (340 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2025-01-21 11:59:00 +01:00
Andreas Morell-Reng
9b5f9e00d9
Translated using Weblate (Danish)
Currently translated at 100.0% (340 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2025-01-21 11:42:19 +01:00
Jan-Eric Myhrgren
57167b8a10
Translated using Weblate (Swedish)
Currently translated at 91.1% (310 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-21 11:41:37 +01:00
RaHoni
98889a0baf
Translated using Weblate (German)
Currently translated at 97.9% (333 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-01-21 11:41:37 +01:00
Nicky Larstrup
0c535bb2d8
Translated using Weblate (Danish)
Currently translated at 100.0% (340 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2025-01-21 11:41:37 +01:00
Andreas Morell-Reng
f4c34a3102
Translated using Weblate (Danish)
Currently translated at 100.0% (340 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2025-01-21 11:41:37 +01:00
Nicky Larstrup
1f4b4d30ee
Translated using Weblate (Danish)
Currently translated at 97.6% (332 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2025-01-20 15:13:32 +01:00
Jan-Eric Myhrgren
1b1dc91c72
Translated using Weblate (Swedish)
Currently translated at 91.1% (310 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-20 15:12:53 +01:00
Petter Schaug-Pettersen
5a6a76bd63
Translated using Weblate (Norwegian Bokmål)
Currently translated at 69.7% (237 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2025-01-20 15:12:53 +01:00
ugyes
e5263f7719
Translated using Weblate (Hungarian)
Currently translated at 99.7% (339 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2025-01-20 15:12:53 +01:00
Nicky Larstrup
267229f929
Translated using Weblate (Danish)
Currently translated at 97.6% (332 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2025-01-20 15:12:53 +01:00
Jan-Eric Myhrgren
79ee8b09d6
Translated using Weblate (Swedish)
Currently translated at 90.8% (309 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-20 11:58:16 +01:00
thehijacker
7824693254
Translated using Weblate (Slovenian)
Currently translated at 100.0% (340 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2025-01-20 11:56:48 +01:00
SunSpring
0b2319fbaa
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (340 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-01-20 11:56:48 +01:00
Jan-Eric Myhrgren
dea3844090
Translated using Weblate (Swedish)
Currently translated at 90.5% (308 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-20 11:56:48 +01:00
Илья Червонный
be2c5759f2
Translated using Weblate (Russian)
Currently translated at 96.1% (327 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2025-01-20 11:56:48 +01:00
J. Lavoie
c8b14d7822
Translated using Weblate (Italian)
Currently translated at 96.4% (328 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2025-01-20 11:56:48 +01:00
biuklija
1b54016035
Translated using Weblate (Croatian)
Currently translated at 100.0% (340 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-01-20 11:56:48 +01:00
J. Lavoie
50fe370585
Translated using Weblate (French)
Currently translated at 100.0% (340 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2025-01-20 11:56:48 +01:00
J. Lavoie
e423ad168a
Translated using Weblate (German)
Currently translated at 97.6% (332 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-01-20 11:56:48 +01:00
Vito0912
1d7670830c
Translated using Weblate (German)
Currently translated at 97.6% (332 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-01-20 11:56:48 +01:00
Losicek
d05dbfd5fb
Translated using Weblate (Czech)
Currently translated at 96.4% (328 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2025-01-20 11:56:47 +01:00
advplyr
7e3f1f1ffa Update issue template versions 2025-01-19 12:52:23 -06:00
advplyr
882ed1871a iOS version bump 0.9.78 2025-01-18 17:45:04 -06:00
advplyr
7c5c7d5632 Version bump v0.9.78-beta 2025-01-18 16:56:36 -06:00
advplyr
240c0da577
Merge pull request #1438 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-01-18 16:42:07 -06:00
Максим Горпиніч
7472ef01a8
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (340 of 340 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-01-18 22:24:44 +00:00
Максим Горпиніч
9907095a2d
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (337 of 337 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-01-18 22:24:44 +00:00
Максим Горпиніч
658a527d93
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (336 of 336 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-01-18 22:24:43 +00:00
Максим Горпиніч
3d25f3dd62
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (336 of 336 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-01-18 22:24:42 +00:00
Bezruchenko Simon
6290e983e9
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (336 of 336 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-01-18 22:24:41 +00:00
Максим Горпиніч
10ae92ec41
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (333 of 333 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-01-18 22:24:41 +00:00
Jan-Eric Myhrgren
2c25286155
Translated using Weblate (Swedish)
Currently translated at 92.4% (307 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-18 22:24:40 +00:00
Jan-Eric Myhrgren
656ede98b6
Translated using Weblate (Swedish)
Currently translated at 92.4% (307 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-18 22:24:40 +00:00
Kieli Puoli
5fb0118f60
Translated using Weblate (Finnish)
Currently translated at 88.5% (294 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2025-01-18 22:24:39 +00:00
Jan-Eric Myhrgren
27e47b2b4b
Translated using Weblate (Swedish)
Currently translated at 92.4% (307 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-18 22:24:39 +00:00
Kieli Puoli
f02e3358aa
Translated using Weblate (Finnish)
Currently translated at 88.2% (293 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2025-01-18 22:24:38 +00:00
Jan-Eric Myhrgren
7caae3171c
Translated using Weblate (Swedish)
Currently translated at 92.4% (307 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-18 22:24:38 +00:00
Kieli Puoli
0cded599b1
Translated using Weblate (Finnish)
Currently translated at 87.9% (292 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2025-01-18 22:24:37 +00:00
Kieli Puoli
4e48240158
Translated using Weblate (Finnish)
Currently translated at 87.6% (291 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2025-01-18 22:24:36 +00:00
Jan-Eric Myhrgren
b488e24798
Translated using Weblate (Swedish)
Currently translated at 92.4% (307 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-18 22:24:36 +00:00
Jan-Eric Myhrgren
7e7d87c147
Translated using Weblate (Swedish)
Currently translated at 92.4% (307 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-18 22:24:35 +00:00
Jan-Eric Myhrgren
8b6389cf3a
Translated using Weblate (Swedish)
Currently translated at 92.4% (307 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-18 22:24:34 +00:00
Kieli Puoli
8a2725b48b
Translated using Weblate (Finnish)
Currently translated at 83.7% (278 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2025-01-18 22:24:34 +00:00
Jan-Eric Myhrgren
a7e8fe12f4
Translated using Weblate (Swedish)
Currently translated at 92.4% (307 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-18 22:24:33 +00:00
Jan-Eric Myhrgren
67a5473e27
Translated using Weblate (Swedish)
Currently translated at 92.4% (307 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-18 22:24:33 +00:00
Jan-Eric Myhrgren
e16fba8eab
Translated using Weblate (Swedish)
Currently translated at 92.4% (307 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-18 22:24:32 +00:00
Jan-Eric Myhrgren
cdfbafc789
Translated using Weblate (Swedish)
Currently translated at 92.4% (307 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-18 22:24:31 +00:00
Jan-Eric Myhrgren
c318f5d57b
Translated using Weblate (Swedish)
Currently translated at 92.4% (307 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-18 22:24:30 +00:00
Jan-Eric Myhrgren
556ab85b0d
Translated using Weblate (Swedish)
Currently translated at 91.8% (305 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-18 22:24:30 +00:00
Jan-Eric Myhrgren
20018aed27
Translated using Weblate (Swedish)
Currently translated at 91.8% (305 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-18 22:24:29 +00:00
Jan-Eric Myhrgren
e2facc87f0
Translated using Weblate (Swedish)
Currently translated at 91.8% (305 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-18 22:24:29 +00:00
Jan-Eric Myhrgren
07e951039c
Translated using Weblate (Swedish)
Currently translated at 91.8% (305 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-18 22:24:28 +00:00
Jan-Eric Myhrgren
386d9ebc52
Translated using Weblate (Swedish)
Currently translated at 91.8% (305 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-18 22:24:28 +00:00
Jan-Eric Myhrgren
d8e058a416
Translated using Weblate (Swedish)
Currently translated at 91.8% (305 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-18 22:24:27 +00:00
Jan-Eric Myhrgren
f5d9c6076c
Translated using Weblate (Swedish)
Currently translated at 91.8% (305 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-18 22:24:26 +00:00
Jan-Eric Myhrgren
e870855602
Translated using Weblate (Swedish)
Currently translated at 91.8% (305 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-18 22:24:26 +00:00
Jan-Eric Myhrgren
a3a6b4618b
Translated using Weblate (Swedish)
Currently translated at 91.8% (305 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-01-18 22:24:25 +00:00
advplyr
3ecae3e16b Fix ereader settings scroll #1374 2025-01-18 16:24:18 -06:00
advplyr
111e8d38dc Remove token from image requests & fix download raw cover image #1328 2025-01-18 15:42:40 -06:00
advplyr
d5fa36b11a Update error icon on audio player cover to show progress sync alert dialog when tapped #1396 2025-01-18 14:46:12 -06:00
advplyr
a35c94cf03 Update numbers to use Intl.NumberFormat with selected locale #1427 2025-01-18 14:24:44 -06:00
advplyr
b2eff46c38 Add # of episodes sort option for podcast libraries 2025-01-18 14:19:14 -06:00
advplyr
07c0187423 Update podcast items incomplete episodes display 2025-01-18 14:15:34 -06:00
advplyr
0074078539 Update server connections list to show warning for connections with an old user id #1411 2025-01-18 13:44:16 -06:00
advplyr
d1641ac0e8 Update epub reader settings to be 75vh 2025-01-18 11:48:41 -06:00
advplyr
13d3489cde
Merge pull request #1439 from advplyr/reader-keep-screen-awake
Add epub reader setting to keep screen awake #1207
2025-01-18 11:37:52 -06:00
advplyr
30efe6bd0a Update podfile 2025-01-18 11:35:06 -06:00
advplyr
5d67c71791 Add epub reader setting to keep screen awake #1207
-----------

Co-authored-by: ISO-B <3048685+ISO-B@users.noreply.github.com>
2025-01-18 10:38:24 -06:00
advplyr
c8d9887070 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2025-01-17 17:06:57 -06:00
advplyr
8268592e8e Update cordova-plugin-screen-orientation, update postcss in nuxt config 2025-01-17 17:06:50 -06:00
advplyr
3b63c5c3f9
Merge pull request #1423 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-01-17 09:01:50 -06:00
ugyes
d3918df4e3
Translated using Weblate (Hungarian)
Currently translated at 99.3% (330 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2025-01-16 23:16:32 +00:00
Milo Ivir
7e586da521
Translated using Weblate (Croatian)
Currently translated at 100.0% (332 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-01-16 23:16:31 +00:00
Milo Ivir
2f5407736c
Translated using Weblate (Croatian)
Currently translated at 100.0% (332 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-01-16 23:16:31 +00:00
SunSpring
b29a98e680
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (332 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-01-16 23:16:30 +00:00
Mathias Franco
239a996846
Translated using Weblate (Dutch)
Currently translated at 100.0% (332 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2025-01-16 23:16:30 +00:00
Milo Ivir
421ecf527d
Translated using Weblate (Croatian)
Currently translated at 99.3% (330 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-01-16 23:16:29 +00:00
Vito0912
c60e55832e
Translated using Weblate (German)
Currently translated at 98.4% (327 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-01-16 23:16:28 +00:00
Rasmus Enevoldsen
b1f9568f8a
Translated using Weblate (Danish)
Currently translated at 100.0% (332 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2025-01-16 23:16:28 +00:00
thehijacker
dd1adb433b
Translated using Weblate (Slovenian)
Currently translated at 100.0% (332 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2025-01-16 23:16:27 +00:00
Bezruchenko Simon
5a4bcfbac5
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (332 of 332 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-01-16 23:16:27 +00:00
Mohamad Dahhan
179642fa8f
Translated using Weblate (Arabic)
Currently translated at 20.2% (66 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-01-16 23:16:26 +00:00
ugyes
33111c4f31
Translated using Weblate (Hungarian)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2025-01-16 23:16:26 +00:00
Troja
03721e8090
Translated using Weblate (Belarusian)
Currently translated at 16.8% (55 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2025-01-16 23:16:25 +00:00
David
0c025e0497
Translated using Weblate (Catalan)
Currently translated at 58.8% (192 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ca/
2025-01-16 23:16:24 +00:00
Dawid Kuźnicki
8cec40f672
Translated using Weblate (Polish)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-01-16 23:16:24 +00:00
Soaibuzzaman
4f7044bffa
Translated using Weblate (Bengali)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/bn/
2025-01-16 23:16:23 +00:00
advplyr
2c6c857e55 Update fullscreen ereader settings title to respect safe area 2025-01-16 16:00:53 -06:00
advplyr
ede8c4ebad Update bookmark create button to sticky at the bottom of modal #1413 2025-01-16 15:57:14 -06:00
advplyr
88dc85c401 Fix bookmark item id 2025-01-15 17:27:55 -06:00
advplyr
7a94f78d78 Update bookmark timestamps to be relative to playback speed #1148 2025-01-13 17:08:22 -06:00
advplyr
6cacf6b2c5 Update create bookmark to auto focus and select input 2025-01-13 16:48:51 -06:00
advplyr
69d198117e Remove babel dev dependency 2025-01-13 16:31:27 -06:00
advplyr
eb035c1023 Android gradle variables remove unused 2025-01-12 15:18:29 -06:00
advplyr
6dabc7a331 Update androidxCoreVersion to 1.12.0 and remove unnecessary gradle dependency 2025-01-12 15:03:29 -06:00
advplyr
58fe29e526 iOS Capacitor 6 updates 2025-01-12 14:58:49 -06:00
advplyr
20688d6395 Update to Capacitor 6 for package.json and android 2025-01-12 13:59:55 -06:00
advplyr
044dd7fea9 Update gradle, add jvmtoolchain, update kotlin version, address gradle warnings 2025-01-12 13:38:56 -06:00
advplyr
2c1f5081f2
Merge pull request #1322 from ISO-B/feat_android_auto_browse
Added better library browsing for Android Auto
2025-01-11 16:59:29 -06:00
advplyr
9243e90e90 Android atuo Reset serverItemsInProgress 2025-01-11 16:49:20 -06:00
advplyr
0dc7813c40 Fix force reload when server changes 2025-01-11 16:33:38 -06:00
advplyr
847bedb65c Fix drawdown for paging with many of the same first letters, update series sequence ascending/descending string, universalize default drawdown grouping limit 2025-01-11 15:33:43 -06:00
advplyr
8e6e0cf673 Update loading library stats, filter non-audio items from search, prevent recent episodes from loading same podcast library item multiple times 2025-01-11 14:55:30 -06:00
advplyr
0da3045c73 Merge branch 'master' into feat_android_auto_browse 2025-01-11 11:10:24 -06:00
advplyr
d8accce9e7
Merge pull request #1410 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-12-27 17:13:30 -06:00
J. Lavoie
dbfd27301c
Translated using Weblate (French)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-12-27 23:11:22 +00:00
pranelio
5ef5d8c343
Translated using Weblate (Lithuanian)
Currently translated at 55.5% (181 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/lt/
2024-12-27 23:11:21 +00:00
pranelio
6ff8282f9f
Translated using Weblate (Lithuanian)
Currently translated at 52.7% (172 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/lt/
2024-12-27 23:11:21 +00:00
Tamanegii
e3930e0588
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-12-27 23:11:20 +00:00
MODI NAVON
129db0ad21
Translated using Weblate (Hebrew)
Currently translated at 9.8% (32 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/he/
2024-12-27 23:11:20 +00:00
thehijacker
13a9ed902c
Translated using Weblate (Slovenian)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2024-12-27 23:11:19 +00:00
Plazec
4e9435418e
Translated using Weblate (Czech)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-12-27 23:11:18 +00:00
Troja
9d990644dd
Translated using Weblate (Belarusian)
Currently translated at 13.4% (44 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2024-12-27 23:11:18 +00:00
Dmitry
c733cab39e
Translated using Weblate (Russian)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2024-12-27 23:11:17 +00:00
ugyes
5a004eeed4
Translated using Weblate (Hungarian)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2024-12-27 23:11:16 +00:00
Troja
012ce933d3
Translated using Weblate (Belarusian)
Currently translated at 12.5% (41 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2024-12-27 23:11:16 +00:00
ugyes
9a2c6bc1ff
Translated using Weblate (Hungarian)
Currently translated at 99.0% (323 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2024-12-27 23:11:15 +00:00
gallegonovato
f0877e192d
Translated using Weblate (Spanish)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-12-27 23:11:15 +00:00
ABS translator
a04175a106
Translated using Weblate (Arabic)
Currently translated at 12.2% (40 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2024-12-27 23:11:14 +00:00
Bezruchenko Simon
0cdd4cd6ed
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2024-12-27 23:11:13 +00:00
biuklija
c64d2a4fa4
Translated using Weblate (Croatian)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-12-27 23:11:12 +00:00
Vito0912
0b4b5a0b56
Translated using Weblate (German)
Currently translated at 100.0% (326 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-12-27 23:11:12 +00:00
advplyr
42ff4e877a
Added translation using Weblate (Belarusian) 2024-12-27 23:11:11 +00:00
Troja
96f206e1a0
Translated using Weblate (Bulgarian)
Currently translated at 0.0% (0 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/bg/
2024-12-27 23:11:10 +00:00
Troja
d2bce7a1e2
Translated using Weblate (Bulgarian)
Currently translated at 5.5% (18 of 326 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/bg/
2024-12-27 23:11:10 +00:00
Hosted Weblate
f1f64eaba1
Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/
2024-12-27 23:11:09 +00:00
advplyr
817eaf1bb2
Merge pull request #1417 from mikiher/subdirectory-support
Support servers with subdirectory
2024-12-27 17:11:02 -06:00
mikiher
16de3fdb97 Support servers with subdirectory 2024-12-23 20:26:23 +02:00
RaHoni
2452f09714
Turn page while playing (#1383) fixes #1365
* Add an EReader setting for turning when playing

This allows to allow the navigation with the Volume buttons even when
audiobooks are playing.

---------

Co-authored-by: advplyr <advplyr@protonmail.com>
2024-12-13 16:20:13 -06:00
advplyr
b02c1311ff
Merge pull request #1407 from jaumet/Catalan_version
Catalan Translation
2024-12-12 08:16:08 -06:00
advplyr
21c35f7744 Add Catalan language option 2024-12-12 08:08:17 -06:00
advplyr
bd2f22e9d7
Merge pull request #1378 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-12-12 08:00:18 -06:00
Bezruchenko Simon
d27776fd26
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2024-12-11 09:01:51 +01:00
ugyes
416ce15d1d
Translated using Weblate (Hungarian)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2024-12-11 09:01:50 +01:00
Jaume
9f9abc6d3e
Catalan Translation
This is the Calan translation , a file in
7stings/ca.json
2024-12-05 21:45:01 +01:00
Milo Ivir
995f042fc8
Translated using Weblate (Croatian)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-12-05 06:04:58 +01:00
Artur
e905a50ed0
Translated using Weblate (Polish)
Currently translated at 91.9% (298 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2024-11-30 12:00:26 +01:00
shdw
5e036992b0
Translated using Weblate (French)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-11-28 13:00:32 +01:00
Soaibuzzaman
cb81a87200
Translated using Weblate (Bengali)
Currently translated at 96.6% (313 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/bn/
2024-11-26 22:00:22 +01:00
Dmitry
fad2ba23b7
Translated using Weblate (Russian)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2024-11-26 22:00:22 +01:00
Bezruchenko Simon
e093c0820d
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2024-11-25 00:00:37 +00:00
biuklija
a65c729906
Translated using Weblate (Croatian)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-11-25 00:00:36 +00:00
DR
1b7fc89d8b
Translated using Weblate (Hebrew)
Currently translated at 7.4% (24 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/he/
2024-11-22 23:00:18 +01:00
burghy86
246b8512e4
Translated using Weblate (Italian)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2024-11-22 23:00:18 +01:00
Mohamad Dahhan
4209f0e86d
Translated using Weblate (Arabic)
Currently translated at 12.0% (39 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2024-11-19 23:30:18 +01:00
DR
653f57ea5d
Translated using Weblate (Hebrew)
Currently translated at 6.4% (21 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/he/
2024-11-19 23:30:18 +01:00
ISO-B
1766111e1d Android Auto: Added comments to code 2024-11-18 20:20:00 +02:00
ISO-B
8caa08843e Android Auto: Prevent crashing loop in case that app restarts while browsing content 2024-11-18 19:47:49 +02:00
ISO-B
a08ae6f977 Android Auto: Ensure that podcast are listed from newest to oldest 2024-11-18 19:46:22 +02:00
Mohamad Dahhan
db246b38f2
Translated using Weblate (Arabic)
Currently translated at 10.1% (33 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2024-11-18 13:00:26 +01:00
thehijacker
1119d47a80
Translated using Weblate (Slovenian)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2024-11-18 13:00:25 +01:00
Julio Cesar de jesus
d4ddfad838
Translated using Weblate (Spanish)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-11-18 13:00:23 +01:00
Clara Papke
e2d0799d17
Translated using Weblate (German)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-11-18 13:00:23 +01:00
ISO-B
f68f31c80f Android Auto: Streamlined browsing settings to single option 2024-11-17 21:16:39 +02:00
ISO-B
b7c8e72ce2 Android Auto: Podcast episodes show publish date 2024-11-17 21:15:16 +02:00
Mohamad Dahhan
9f5b286a45
Translated using Weblate (Arabic)
Currently translated at 4.0% (13 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2024-11-16 22:04:25 +01:00
DR
1de204ff02
Translated using Weblate (Hebrew)
Currently translated at 2.1% (7 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/he/
2024-11-16 22:04:25 +01:00
SunSpring
cc4d364f75
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-11-16 22:04:25 +01:00
Paulo Henrique Dos Santos Garcia
cd629065b8
Translated using Weblate (Portuguese (Brazil))
Currently translated at 92.5% (300 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2024-11-16 22:04:24 +01:00
isogashiiman
46dfd9c44e
Translated using Weblate (French)
Currently translated at 96.9% (314 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-11-16 22:04:24 +01:00
gallegonovato
edc797035f
Translated using Weblate (Spanish)
Currently translated at 100.0% (324 of 324 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-11-16 22:04:24 +01:00
Bezruchenko Simon
c1eb7d70b7
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (319 of 319 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2024-11-15 00:51:30 +01:00
biuklija
921636ea69
Translated using Weblate (Croatian)
Currently translated at 100.0% (319 of 319 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-11-15 00:51:29 +01:00
gallegonovato
40c2262f47
Translated using Weblate (Spanish)
Currently translated at 100.0% (319 of 319 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-11-15 00:51:28 +01:00
Hosted Weblate
b91e8fd0a1
Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/
2024-11-15 00:51:27 +01:00
advplyr
ad7ca59532 Add more localization #1375 2024-11-14 17:51:16 -06:00
ISO-B
802c16c0df Android Auto improvements. Icons, Start up
Icons:
- Static browsable list items everywhere have hardcoded icons
- Libraries use icons that are defined on server

Start up / Initial loading:
- Initial loading first loads libraries from server. After that personalized shelves and items in progress are fetched simultaneously. Top menu items are update after every stage.
- If network connection is not available when android auto starts app tries to do initial loading again when network connection becomes available again
2024-11-14 19:23:55 +02:00
advplyr
efbb0e1b1c Fix string order 2024-11-13 17:30:38 -06:00
advplyr
607f6e9b6c Fix string order 2024-11-13 17:28:32 -06:00
advplyr
72b775e179 Add more localization #1375 2024-11-13 17:25:26 -06:00
advplyr
491f312036
Merge pull request #1368 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-11-13 17:09:25 -06:00
thehijacker
31221062dd
Translated using Weblate (Slovenian)
Currently translated at 100.0% (315 of 315 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2024-11-13 19:05:41 +00:00
burghy86
73e6fa24a8
Translated using Weblate (Italian)
Currently translated at 100.0% (315 of 315 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2024-11-13 19:05:41 +00:00
ISO-B
b335fd30d1 Android Auto improvements
General:
 - New top menu item Recent is added
 - Library caches are cleared when switching server

Search:
 - Is done using server API
 - Latest search is cache to prevent need to make new request when returning from browsable item.
 - Results are grouped by book, series, author and split by library
 - Only searches libraries with audio content

Library personalized shelves:
 - Recent books, series, authors, podcasts and episodes shelves are listed under Recent top menu
 - Discovery shelves can be found under library many from corresponding library
2024-11-13 09:20:03 +02:00
Nicholas W
37395003cb
Deleted translation using Weblate (English (United States)) 2024-11-12 04:14:22 +01:00
Tamanegii
39b360b132
Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (315 of 315 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hant/
2024-11-11 23:41:58 +01:00
John Joseph A. Gatchalian
34b16c2da9
Translated using Weblate (English (United States))
Currently translated at 100.0% (315 of 315 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/en_US/
2024-11-11 23:41:57 +01:00
thehijacker
525108bf26
Translated using Weblate (Slovenian)
Currently translated at 100.0% (315 of 315 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2024-11-11 23:41:56 +01:00
Tamanegii
7563bdf900
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (315 of 315 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-11-11 23:41:56 +01:00
Pavel Vachek
64785be947
Translated using Weblate (Czech)
Currently translated at 99.3% (313 of 315 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-11-11 23:41:55 +01:00
SunSpring
a714e9b985
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (315 of 315 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-11-11 23:41:54 +01:00
Nicholas W
bc69a8768c
Added translation using Weblate (Arabic) 2024-11-11 23:41:54 +01:00
Bezruchenko Simon
7d67761ed8
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (315 of 315 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2024-11-11 23:41:53 +01:00
biuklija
3d9b508335
Translated using Weblate (Croatian)
Currently translated at 100.0% (315 of 315 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-11-11 23:41:53 +01:00
advplyr
78f0c8823d Add Bengali and Slovenian to language setting dropdown 2024-11-11 16:41:21 -06:00
advplyr
145a3c44b7 Add localization chart to readme 2024-11-08 08:03:09 -06:00
advplyr
2063f51289 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2024-11-08 07:56:02 -06:00
advplyr
da2bfd8fc9 Update version in gh templates 2024-11-08 07:55:58 -06:00
ISO-B
eedcd188c3 Android Auto: Fixed and improved search
Search now queries data from server. Results are grouped by books, series and authors.
2024-11-08 12:49:47 +02:00
advplyr
af8b5b63d5 iOS version bump 0.9.77 2024-11-05 16:33:01 -06:00
advplyr
4485d0833e Version bump v0.9.77-beta 2024-11-05 16:26:10 -06:00
advplyr
a163a6af88 Fix:Android allow deleting local library item that doesnt exist on device #105 2024-11-04 16:45:21 -06:00
advplyr
38bb5af04b
Merge pull request #1358 from ISO-B/fix_check_files_before_playing
Ensure that there is files available before playing local content
2024-11-04 16:43:59 -06:00
ISO-B
11804d1cb8 Enhanced check for local audio tracks
When check is triggered code now checks that are files really exists
2024-11-04 13:54:02 +02:00
advplyr
22529fc1d2
Merge pull request #1352 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-11-03 15:51:05 -06:00
thehijacker
62b0c5fd62
Translated using Weblate (Slovenian)
Currently translated at 100.0% (315 of 315 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2024-11-03 22:17:33 +01:00
gallegonovato
1ed28a4fd4
Translated using Weblate (Spanish)
Currently translated at 100.0% (315 of 315 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-11-03 22:17:33 +01:00
Vito0912
5044aea1d7
Translated using Weblate (German)
Currently translated at 100.0% (315 of 315 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-11-03 22:17:33 +01:00
Frantisek Nagy
47fda0ccca
Translated using Weblate (Hungarian)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2024-11-03 22:17:33 +01:00
Ahetek
742490775a
Translated using Weblate (Polish)
Currently translated at 94.2% (294 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2024-11-03 22:17:33 +01:00
Mathias Franco
95bbed2b21
Translated using Weblate (Dutch)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2024-11-03 22:17:33 +01:00
biuklija
2b446e227d
Translated using Weblate (Croatian)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-11-03 22:17:33 +01:00
advplyr
ea6417dbb1
Merge pull request #1360 from ISO-B/fix_remove_internet_connection_check
iOS devices have always networkConnected status true
2024-11-03 15:17:28 -06:00
advplyr
d902417959 Fix:Android app crash when switching server while player open #1336
- Wrap requests in try/catch to prevent app crash for bad requests
2024-11-03 14:53:35 -06:00
ISO-B
358197db03 iOS devices have always networkConnected status true
Capacitor Network plugin only shows ios device connected if internet access is available. This fix allows iOS users to use local server without internet access. Socket is used to detect if connection to server is availabe.  networkConnected is only used for add server form and cellular permission check.

Cellular permissions for download and streaming wont work for iOS if device is connected to cellular, but without internet access to server that is used for connectivity check.
2024-11-03 21:45:05 +02:00
advplyr
102fd1f1a1
Merge pull request #1359 from ISO-B/fix_remove_internet_connection_check
Changed network connection check logic
2024-11-02 13:14:18 -05:00
ISO-B
fe9168c6cf Changed network connection check logic
Network connection no longer requires internet connection. Socket connection status is used instead for checking if server is reachable. If there is no socket connection available eq. before connecting to server then connection type is used to for netrork connection check.
2024-10-31 21:14:25 +02:00
advplyr
f7663fc17f
Merge pull request #1272 from ISO-B/master
Added ability to download whole series from series view
2024-10-31 09:05:01 -05:00
advplyr
e370ec36ab Merge branch 'master' into ISO-B/master 2024-10-31 08:30:04 -05:00
ISO-B
8f181c74d4 Ensure that there is files available before playing local content 2024-10-29 22:51:29 +02:00
ISO-B
8134ec84c6 Enchancements for Android Auto library
- Hide libraries without audiobooks
- Sort books in series by sequence value
- Added option for selecting ASC or DESC sorting for series
- Order authors alphabetically
2024-10-27 21:55:17 +02:00
advplyr
e2fc5bcbb1
Merge pull request #1331 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-10-16 17:43:02 -05:00
thehijacker
67af09c4bd
Translated using Weblate (Slovenian)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2024-10-16 01:15:53 +00:00
Mathias Franco
b81abf4da5
Translated using Weblate (Dutch)
Currently translated at 89.7% (280 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2024-10-16 01:15:52 +00:00
Ahetek
2c288bb34d
Translated using Weblate (Polish)
Currently translated at 94.2% (294 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2024-10-14 07:02:00 +02:00
Alexander Künzel
df762e8910
Translated using Weblate (German)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-10-14 07:02:00 +02:00
Languages add-on
253aa19701
Added translation using Weblate (Chinese (Traditional Han script)) 2024-10-12 06:45:53 +02:00
Languages add-on
ea608d875b
Added translation using Weblate (English (United States)) 2024-10-12 06:45:40 +02:00
Languages add-on
d4340a8b95
Added translation using Weblate (Hebrew) 2024-10-12 06:45:14 +02:00
Languages add-on
33b3aafb14
Added translation using Weblate (Bulgarian) 2024-10-12 06:45:05 +02:00
Languages add-on
6cedc2fe35
Added translation using Weblate (Estonian) 2024-10-12 06:45:05 +02:00
Mathias Franco
1bb03015d9
Translated using Weblate (Dutch)
Currently translated at 62.5% (195 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2024-10-12 06:45:04 +02:00
thehijacker
c5bef72c63
Translated using Weblate (Slovenian)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2024-10-11 04:45:02 +00:00
biuklija
8b7c00b337
Translated using Weblate (Croatian)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-10-11 04:45:02 +00:00
thehijacker
6199d4fab4
Translated using Weblate (Slovenian)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2024-10-10 04:45:02 +00:00
Petras Šukys
2063d8647a
Translated using Weblate (Lithuanian)
Currently translated at 52.2% (163 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/lt/
2024-10-09 06:45:02 +02:00
Alexander Künzel
c0a9f85f77
Translated using Weblate (German)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-10-08 06:45:02 +02:00
SunSpring
b1651c5b01
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-10-05 06:45:02 +02:00
kuci-JK
1a8af2caee
Translated using Weblate (Czech)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-10-03 06:45:02 +02:00
tonttula
a593070b9f
Translated using Weblate (Finnish)
Currently translated at 88.7% (277 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2024-10-01 04:45:05 +00:00
biuklija
1d700b0663
Translated using Weblate (Croatian)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-09-30 04:45:02 +00:00
Alexander Künzel
2233885142
Translated using Weblate (German)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-09-29 19:32:57 +02:00
tonttula
faf87d2942
Translated using Weblate (Finnish)
Currently translated at 78.2% (244 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2024-09-29 06:45:03 +02:00
Mihály Hunyady
706673aed0
Translated using Weblate (Hungarian)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2024-09-29 06:45:02 +02:00
Soaibuzzaman
eaea688e33
Translated using Weblate (Bengali)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/bn/
2024-09-26 16:42:37 +02:00
advplyr
256be3521d
Merge pull request #1325 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-09-25 17:02:48 -05:00
Vili Kangas
cec1e8593d
Translated using Weblate (Finnish)
Currently translated at 76.6% (239 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2024-09-25 04:45:02 +00:00
Charlie
e18f9be810
Translated using Weblate (French)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-09-24 20:17:38 +02:00
Philip Karlsson
69dca2db43
Translated using Weblate (Swedish)
Currently translated at 98.3% (307 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2024-09-24 06:45:02 +02:00
thehijacker
c03321b55e
Translated using Weblate (Slovenian)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2024-09-23 06:45:03 +02:00
SunSpring
f8b4287229
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-09-23 06:45:02 +02:00
Charlie
ce2fa0c116
Translated using Weblate (French)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-09-20 06:45:02 +02:00
SunSpring
ebe9a230fd
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-09-17 21:42:11 +02:00
RafalHo
a05ceea4bc
Translated using Weblate (Polish)
Currently translated at 94.2% (294 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2024-09-17 21:42:11 +02:00
advplyr
67c2ded1f0
Merge pull request #1315 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-09-16 17:07:36 -05:00
Milo Ivir
66d09c886e
Translated using Weblate (Croatian)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-09-17 00:00:24 +02:00
Lasse Slotmann
bbff7513f1
Translated using Weblate (Danish)
Currently translated at 78.5% (245 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2024-09-17 00:00:24 +02:00
Lasse Slotmann
4195279c5d
Translated using Weblate (Danish)
Currently translated at 73.7% (230 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2024-09-17 00:00:24 +02:00
Lasse Slotmann
464d967a0a
Translated using Weblate (Danish)
Currently translated at 58.6% (183 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2024-09-17 00:00:24 +02:00
gfbdrgng
4dd7bf8916
Translated using Weblate (Russian)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2024-09-17 00:00:24 +02:00
biuklija
4b134eaef9
Translated using Weblate (Croatian)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-09-17 00:00:24 +02:00
J. Lavoie
ece62c8b18
Translated using Weblate (Italian)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2024-09-17 00:00:24 +02:00
J. Lavoie
b430c185b1
Translated using Weblate (Italian)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2024-09-17 00:00:24 +02:00
Valentin
5c13ccf9e7
Translated using Weblate (German)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-09-17 00:00:24 +02:00
J. Lavoie
cb0e2ba305
Translated using Weblate (German)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-09-17 00:00:24 +02:00
thehijacker
011fbb38cb
Translated using Weblate (Slovenian)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2024-09-17 00:00:24 +02:00
Aron Pakarinen
0a69450bc0
Translated using Weblate (Finnish)
Currently translated at 71.1% (222 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2024-09-17 00:00:24 +02:00
Charlie
b4a97f4347
Translated using Weblate (French)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-09-17 00:00:24 +02:00
gallegonovato
96f7aa95ee
Translated using Weblate (Spanish)
Currently translated at 100.0% (312 of 312 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-09-17 00:00:24 +02:00
advplyr
e92f2177e8 Fix:Simple storage dependency version, update kotlin version to 2, update androidx dependencies 2024-09-16 16:59:12 -05:00
ISO-B
e4a3cc5290 Added Android Auto browsing settings 2024-09-16 23:06:49 +03:00
ISO-B
a3a58a25ef Added better library browsing for Android Auto
Each library has 3 options: Library, Series and Collection. Library is grouped by authors
2024-09-13 22:51:54 +03:00
advplyr
ab7bda402e
Merge pull request #1306 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-09-08 16:34:39 -05:00
Soaibuzzaman
d78c168e16
Translated using Weblate (Bengali)
Currently translated at 72.7% (224 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/bn/
2024-09-08 23:07:34 +02:00
thehijacker
2b04b4f008
Translated using Weblate (Slovenian)
Currently translated at 100.0% (308 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2024-09-08 23:07:34 +02:00
SunSpring
a6cc0d742c
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (308 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-09-08 23:07:34 +02:00
thehijacker
e7e9a2755c
Translated using Weblate (Slovenian)
Currently translated at 100.0% (308 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2024-09-08 23:07:34 +02:00
Soaibuzzaman
56ea4ce2f5
Translated using Weblate (Bengali)
Currently translated at 58.1% (179 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/bn/
2024-09-08 23:07:34 +02:00
Kamil Pomykała
05528f6dfd
Translated using Weblate (Polish)
Currently translated at 94.4% (291 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2024-09-08 23:07:34 +02:00
Andrej Kralj
49cd36c33e
Translated using Weblate (Slovenian)
Currently translated at 50.0% (154 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2024-09-08 23:07:34 +02:00
Andrej Kralj
898d96ce3a
Translated using Weblate (Slovenian)
Currently translated at 36.0% (111 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2024-09-08 23:07:34 +02:00
biuklija
b257836b76
Translated using Weblate (Croatian)
Currently translated at 100.0% (308 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-09-08 23:07:34 +02:00
gallegonovato
8fab4e56ef
Translated using Weblate (Spanish)
Currently translated at 100.0% (308 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-09-08 23:07:34 +02:00
biuklija
8e34c230d7
Translated using Weblate (Croatian)
Currently translated at 100.0% (308 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-09-08 23:07:34 +02:00
Nicholas W
b32f2e2973
Added translation using Weblate (Slovenian) 2024-09-08 23:07:34 +02:00
Dmitry
1047d1a598
Translated using Weblate (Russian)
Currently translated at 100.0% (308 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2024-09-08 23:07:34 +02:00
Nicholas W
1e5115b743
Translated using Weblate (Hungarian)
Currently translated at 89.9% (277 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2024-09-08 23:07:34 +02:00
Pierrick Guillaume
e1e12de91b
Translated using Weblate (French)
Currently translated at 100.0% (308 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-09-08 23:07:34 +02:00
Charlie
58950afb99
Translated using Weblate (French)
Currently translated at 100.0% (308 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-09-08 23:07:34 +02:00
Valentin
89b64b165f
Translated using Weblate (German)
Currently translated at 100.0% (308 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-09-08 23:07:34 +02:00
Nicholas W
90ab927bdc
Translated using Weblate (Swedish)
Currently translated at 74.0% (228 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2024-09-08 23:07:34 +02:00
Nicholas W
50ebf4eacb
Translated using Weblate (Norwegian Bokmål)
Currently translated at 75.6% (233 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2024-09-08 23:07:34 +02:00
Nicholas W
064ba080ab
Translated using Weblate (Gujarati)
Currently translated at 8.7% (27 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/gu/
2024-09-08 23:07:34 +02:00
Nicholas W
4ee0345217
Translated using Weblate (Danish)
Currently translated at 55.8% (172 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2024-09-08 23:07:34 +02:00
advplyr
d45cbbba98
Merge pull request #1302 from RaHoni/navigate_with_volume
Make it possible to turn the page with volume keys
2024-09-08 16:07:29 -05:00
advplyr
612a7a7063 Epub ereader volume navigate setting fixes and disable when player is open 2024-09-08 15:58:15 -05:00
ISO-B
62e3ca4068 Polished download series function
- File and size resets every time when download is triggered
- If everything is downloaded show popup to tell it to user
2024-09-08 00:06:55 +03:00
ISO-B
34dcdc89c3 Enhanced download series ui
- Move download button from menu to toolbar
- Changed download confirmation message
- Changed serie to series
2024-09-07 23:30:10 +03:00
advplyr
bcc202ff3b
Merge pull request #1305 from nichwall/translation_fixes
Remove untranslated strings
2024-08-31 15:28:48 -05:00
Nicholas Wallace
d4a56b3823 Remove untranslated strings 2024-08-31 13:11:23 -07:00
advplyr
626239038b
Merge pull request #1301 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-08-31 15:09:15 -05:00
Charlie
f7da431e61
Translated using Weblate (French)
Currently translated at 100.0% (308 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-08-31 21:09:53 +02:00
biuklija
6ff59c423b
Translated using Weblate (Croatian)
Currently translated at 100.0% (308 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-08-31 16:39:11 +02:00
biuklija
076d4977e3
Translated using Weblate (Croatian)
Currently translated at 100.0% (308 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-08-31 16:39:11 +02:00
advplyr
2a737a9d72
Merge pull request #1303 from Sp4rky001/Cover-Image-in-Downloaded-Audiobook-missing-in-Android-Auto-#141
Do not call setIconBitmap instead of setIconUri if book is local
2024-08-31 09:39:03 -05:00
advplyr
8c77a19eb6
Update android/app/src/main/java/com/audiobookshelf/app/player/PlayerNotificationService.kt 2024-08-31 09:33:24 -05:00
Rich T
3918696477 Do not call setIconBitmap instead of setIconUri if book is local 2024-08-30 18:15:47 -07:00
RaHoni
8a6a2b8577
Make it possible to turn the page with volume keys 2024-08-30 21:48:00 +02:00
advplyr
584023380c
Merge pull request #1288 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-08-28 04:55:24 -05:00
biuklija
1f4dd2bdb7
Translated using Weblate (Croatian)
Currently translated at 100.0% (308 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-08-28 06:45:02 +02:00
biuklija
f7d5a0732b
Translated using Weblate (Croatian)
Currently translated at 99.0% (305 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-08-27 04:45:03 +00:00
biuklija
2a6cc882a5
Translated using Weblate (Croatian)
Currently translated at 97.4% (300 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-08-27 00:01:25 +02:00
biuklija
f385339da3
Translated using Weblate (Croatian)
Currently translated at 74.3% (229 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-08-26 23:19:32 +02:00
Tom Redd
c37566a999
Translated using Weblate (Norwegian Bokmål)
Currently translated at 83.7% (258 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2024-08-26 23:19:31 +02:00
Ahetek
f90a4b1374
Translated using Weblate (Polish)
Currently translated at 96.1% (296 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2024-08-26 23:19:31 +02:00
Christian Wia
b649680d3e
Translated using Weblate (French)
Currently translated at 100.0% (308 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-08-26 23:19:30 +02:00
Mario
b84c2abe14
Translated using Weblate (German)
Currently translated at 100.0% (308 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-08-26 23:19:30 +02:00
Illia Pyshniak
58aff762c0
Translated using Weblate (Ukrainian)
Currently translated at 100.0% (308 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2024-08-26 23:19:29 +02:00
kuci-JK
e5eef69c91
Translated using Weblate (Czech)
Currently translated at 96.4% (297 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-08-26 23:19:29 +02:00
Fredrik Lindqvist
54f91b7a88
Translated using Weblate (Swedish)
Currently translated at 81.4% (251 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2024-08-26 23:19:28 +02:00
gallegonovato
17676173ef
Translated using Weblate (Spanish)
Currently translated at 100.0% (308 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-08-26 23:19:28 +02:00
Vito0912
31f4aa944a
Translated using Weblate (German)
Currently translated at 100.0% (308 of 308 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-08-26 23:19:27 +02:00
advplyr
b30bd495b7
Merge pull request #1257 from faush01/feature/history_split_saves
split groups of save history items that have different status results
2024-08-26 16:19:19 -05:00
advplyr
06f893436e Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2024-08-14 07:39:22 -05:00
advplyr
20a2b3ed90 Update app version on gh issue templates 2024-08-14 07:39:18 -05:00
advplyr
b1e3bc46c8 iOS version bump 0.9.76 2024-08-13 16:39:39 -05:00
ISO-B
84509ec1a8 Added ability to download whole serie from serie view 2024-07-27 23:26:57 +03:00
shaun
79df676a13 split groups of save history items that have different status results 2024-07-16 18:01:09 +10:00
265 changed files with 19136 additions and 10891 deletions

View file

@ -1,17 +1,27 @@
name: 🐞 ABS App Bug Report
description: File a bug/issue and help us improve the Audiobookshelf mobile apps.
title: "[Bug]: "
labels: ["bug", "triage"]
title: '[Bug]: '
labels: ['bug', 'triage']
body:
- type: markdown
attributes:
value: "## App Bug Description"
value: '## App Bug Description'
- type: markdown
attributes:
value: "Thank you for filing a bug report! 🐛"
value: 'Thank you for filing a bug report! 🐛'
- type: markdown
attributes:
value: "Join the [discord server](https://discord.gg/HQgCbd6E75) for questions or if you are not sure about a bug."
value: 'Join the [discord server](https://discord.gg/HQgCbd6E75) for questions or if you are not sure about a bug.'
- type: dropdown
id: confirm-check
attributes:
label: I have verified that the [bug is not already awaiting release](https://github.com/advplyr/audiobookshelf-app/issues?q=is%3Aissue%20label%3A%22awaiting%20release%22)
multiple: false
options:
- 'Yes'
- 'No'
validations:
required: true
- type: textarea
id: what-happened
attributes:
@ -25,7 +35,7 @@ body:
attributes:
label: Steps to Reproduce the Issue
description: Please help us understand how we can reliably reproduce the issue.
placeholder: "1. Go to the library page of a Podcast library and..."
placeholder: '1. Go to the library page of a Podcast library and...'
validations:
required: true
- type: textarea
@ -38,13 +48,13 @@ body:
required: true
- type: markdown
attributes:
value: "## Mobile Environment"
value: '## Mobile Environment'
- type: input
id: phone-model
attributes:
label: Phone Model
description: What kind of phone are you using?
placeholder: e.g. Pixel 6, iPhone 14, Samusung Galaxy s23, etc
placeholder: e.g. Pixel 6, iPhone 14, Samsung Galaxy s23, etc
validations:
required: true
- type: input
@ -62,10 +72,10 @@ body:
description: Please ensure your app is up to date. *If you are using a 3rd-party app, please reach out to them directly.*
multiple: true
options:
- Android App - 0.9.74
- iOS App - 0.9.74
- Android App - 0.9.73
- iOS App - 0.9.73
- 'Android App - 0.10.0'
- 'iOS App - 0.10.0'
- 'Android App - 0.9.81'
- 'iOS App - 0.9.81'
validations:
required: true
- type: dropdown
@ -74,10 +84,10 @@ body:
label: Installation Source
multiple: true
options:
- Google Play Store
- Testflight
- SideStore
- Other (List in "Additional Notes")
- 'Google Play Store'
- 'Testflight'
- 'SideStore'
- 'Other (List in "Additional Notes")'
validations:
required: true
- type: textarea
@ -85,4 +95,4 @@ body:
attributes:
label: Additional Notes
description: Anything else you want to add?
placeholder: "e.g. I have tried X, Y, and Z."
placeholder: 'e.g. I have tried X, Y, and Z.'

5
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Discord
url: https://discord.gg/HQgCbd6E75
about: Ask questions, get help troubleshooting, and join the Abs community here.

View file

@ -1,14 +1,14 @@
name: 🚀 App Feature Request
description: Request a feature/enhancement
title: "[Enhancement]: "
labels: ["enhancement"]
title: '[Enhancement]: '
labels: ['enhancement']
body:
- type: markdown
attributes:
value: "## App Feature Request Description"
value: '## App Feature Request Description'
- type: markdown
attributes:
value: "Please first search in both issues & discussions for your enhancement and make sure your app is up to date."
value: 'Please first search in both issues & discussions for your enhancement and make sure your app is up to date.'
- type: textarea
id: describe
attributes:
@ -35,7 +35,7 @@ body:
required: true
- type: markdown
attributes:
value: "## App Current Implementation"
value: '## App Current Implementation'
- type: dropdown
id: version
attributes:
@ -43,10 +43,10 @@ body:
description: Please ensure your app is up to date. *If you are using a 3rd-party app, please reach out to them directly.*
multiple: true
options:
- Android App - 0.9.74
- iOS App - 0.9.74
- Android App - 0.9.73
- iOS App - 0.9.73
- 'Android App - 0.10.0'
- 'iOS App - 0.10.0'
- 'Android App - 0.9.81'
- 'iOS App - 0.9.81'
validations:
required: true
- type: textarea

41
.github/pull_request_template.md vendored Normal file
View file

@ -0,0 +1,41 @@
<!--
For Work In Progress Pull Requests, please use the Draft PR feature,
see https://github.blog/2019-02-14-introducing-draft-pull-requests/ for further details.
If you do not follow this template, the PR may be closed without review.
Please ensure all checks pass.
If you are a new contributor, the workflows will need to be manually approved before they run.
-->
## Brief summary
<!-- Please provide a brief summary of what your PR attempts to achieve. -->
## Which issue is fixed?
<!-- Which issue number does this PR fix? Ex: "Fixes #1234" -->
## Pull Request Type
<!--
Does this affect only Android, only iOS, or both?
Does this change the frontend or the backend of the apps?
-->
## In-depth Description
<!--
Describe your solution in more depth.
How does it work? Why is this the best solution?
Does it solve a problem that affects multiple users or is this an edge case for your setup?
-->
## How have you tested this?
<!-- Please describe in detail with reproducible steps how you tested your changes. -->
## Screenshots
<!-- If your PR includes any changes to the front-end, please include screenshots or a
short video from before and after your changes. -->

View file

@ -13,41 +13,41 @@ jobs:
main:
runs-on: ubuntu-latest
steps:
- name: checkout sources
uses: actions/checkout@v3
- name: checkout sources
uses: actions/checkout@v3
- name: use Node.js 20.x
uses: actions/setup-node@v3
with:
node-version: 20
- name: use Node.js 20.x
uses: actions/setup-node@v3
with:
node-version: 20
- name: Set up Java
uses: actions/setup-java@v2
with:
distribution: "temurin"
java-version: 17
- name: Set up Java
uses: actions/setup-java@v2
with:
distribution: 'temurin'
java-version: 21
- name: install dependencies
run: npm ci
- name: install dependencies
run: npm ci
- name: build Nuxt project
run: npm run generate
- name: build Nuxt project
run: npm run generate
- name: copy to Android project
run: npx cap sync
- name: copy to Android project
run: npx cap sync
- name: build Android app
run: ./android/gradlew assembleDebug -p android --no-daemon
- 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: 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
- name: upload app
uses: actions/upload-artifact@v4
with:
name: audiobookshelf-apk
path: android/app/build/outputs/apk/debug/*.apk

View file

@ -0,0 +1,42 @@
name: Close Issues not using a template
on:
issues:
types:
- opened
permissions:
issues: write
jobs:
close_issue:
runs-on: ubuntu-latest
steps:
- name: Check issue headings
uses: actions/github-script@v6
with:
script: |
const issueBody = context.payload.issue.body || "";
// Match Markdown headings (e.g., # Heading, ## Heading)
const headingRegex = /^(#{1,6})\s.+/gm;
const headings = [...issueBody.matchAll(headingRegex)];
if (headings.length < 3) {
// Post a comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
body: "Thank you for opening an issue! To help us review your request efficiently, please use one of the provided issue templates. If you're seeking information or have a general question, consider opening a Discussion or joining the conversation on our Discord. Thanks!"
});
// Close the issue
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.issue.number,
state: "closed"
});
}

View file

@ -12,57 +12,57 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- name: checkout sources
uses: actions/checkout@v3
- name: checkout sources
uses: actions/checkout@v3
- name: use Node.js 20.x
uses: actions/setup-node@v3
with:
node-version: 20
- name: use Node.js 20.x
uses: actions/setup-node@v3
with:
node-version: 20
- name: Set up Java
uses: actions/setup-java@v2
with:
distribution: "temurin"
java-version: 17
- name: Set up Java
uses: actions/setup-java@v2
with:
distribution: 'temurin'
java-version: 21
- name: install dependencies
run: npm ci
- name: install dependencies
run: npm ci
- name: build Nuxt project
run: npm run generate
- name: build Nuxt project
run: npm run generate
- name: copy to Android project
run: npx cap sync
- name: copy to Android project
run: npx cap sync
- name: build Android app
run: ./android/gradlew assembleDebug -p android --no-daemon
- 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: 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: 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: 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
- name: upload test page artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./ghpages
deploy:
needs: build
@ -79,4 +79,4 @@ jobs:
steps:
- name: deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1
uses: actions/deploy-pages@v4

6
android/.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,6 @@
{
"java.project.root": "android",
"java.import.gradle.enabled": true,
"java.import.gradle.wrapper.enabled": true,
"java.import.gradle.user.home": "${workspaceFolder}/.gradle"
}

View file

@ -18,54 +18,55 @@ kotlin {
"--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
"--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED"
]
jvmToolchain(17)
}
android {
namespace 'com.audiobookshelf.app'
buildFeatures {
viewBinding true
buildConfig true
}
kotlinOptions {
freeCompilerArgs = ['-Xjvm-default=all']
freeCompilerArgs = ['-Xjvm-default=all']
}
compileSdk rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.audiobookshelf.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 113
versionName "0.10.0-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
manifestPlaceholders = [
"appAuthRedirectScheme": "com.audiobookshelf.app"
]
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.audiobookshelf.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 107
versionName "0.9.76-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
manifestPlaceholders = [
"appAuthRedirectScheme": "com.audiobookshelf.app"
]
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
}
}
buildTypes {
debug {
applicationIdSuffix ".debug"
versionNameSuffix "-debug"
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
buildTypes {
debug {
applicationIdSuffix ".debug"
versionNameSuffix "-debug"
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
repositories {
flatDir{
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
flatDir {
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
}
mavenCentral()
// TODO: Temporarily using SNAPSHOT version of Simple Storage that resolves https://github.com/anggrayudi/SimpleStorage/issues/133
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
}
configurations.configureEach {
@ -80,19 +81,18 @@ configurations.configureEach {
}
dependencies {
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
implementation project(':capacitor-android')
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
implementation project(':capacitor-cordova-android-plugins')
implementation "androidx.core:core-ktx:$androidx_core_ktx_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
@ -120,7 +120,7 @@ dependencies {
implementation 'io.github.pilgr:paperdb:2.7.2'
// Simple Storage
implementation "com.anggrayudi:storage:1.5.5-SNAPSHOT"
implementation "com.anggrayudi:storage:1.5.6"
// OK HTTP
implementation 'com.squareup.okhttp3:okhttp:4.9.2'

View file

@ -9,7 +9,9 @@ android {
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':byteowls-capacitor-filesharer')
implementation project(':webnativellc-capacitor-filesharer')
implementation project(':capacitor-community-keep-awake')
implementation project(':capacitor-community-volume-buttons')
implementation project(':capacitor-app')
implementation project(':capacitor-browser')
implementation project(':capacitor-clipboard')

View file

@ -51,7 +51,7 @@
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
android:exported="true"
android:label="@string/title_activity_main"
android:launchMode="singleTask"

View file

@ -2,10 +2,16 @@
"appId": "com.audiobookshelf.app",
"appName": "audiobookshelf-app",
"webDir": "dist",
"bundledWebRuntime": false,
"plugins": {
"CapacitorHttp": {
"enabled": false
},
"StatusBar": {
"backgroundColor": "#232323",
"style": "DARK"
}
},
"server": {
"androidScheme": "http"
}
}

View file

@ -1,8 +1,16 @@
[
{
"pkg": "@byteowls/capacitor-filesharer",
"pkg": "@webnativellc/capacitor-filesharer",
"classpath": "com.byteowls.capacitor.filesharer.FileSharerPlugin"
},
{
"pkg": "@capacitor-community/keep-awake",
"classpath": "com.getcapacitor.community.keepawake.KeepAwakePlugin"
},
{
"pkg": "@capacitor-community/volume-buttons",
"classpath": "com.ryltsov.alex.plugins.volume.buttons.VolumeButtonsPlugin"
},
{
"pkg": "@capacitor/app",
"classpath": "com.capacitorjs.plugins.app.AppPlugin"

View file

@ -6,10 +6,15 @@ import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.IBinder
import android.util.Log
import android.view.ViewGroup
import android.view.WindowInsets
import android.webkit.WebView
import androidx.core.app.ActivityCompat
import androidx.core.view.updateLayoutParams
import com.anggrayudi.storage.SimpleStorage
import com.anggrayudi.storage.SimpleStorageHelper
import com.audiobookshelf.app.managers.DbManager
@ -18,6 +23,7 @@ import com.audiobookshelf.app.plugins.AbsAudioPlayer
import com.audiobookshelf.app.plugins.AbsDatabase
import com.audiobookshelf.app.plugins.AbsDownloader
import com.audiobookshelf.app.plugins.AbsFileSystem
import com.audiobookshelf.app.plugins.AbsLogger
import com.getcapacitor.BridgeActivity
@ -39,29 +45,58 @@ class MainActivity : BridgeActivity() {
)
public override fun onCreate(savedInstanceState: Bundle?) {
// TODO: Optimize using strict mode logs
// StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
// .detectDiskReads()
// .detectDiskWrites().detectAll()
// .detectNetwork() // or .detectAll() for all detectable problems
// .penaltyLog()
// .build())
// StrictMode.setVmPolicy(VmPolicy.Builder()
// .detectLeakedSqlLiteObjects()
// .detectLeakedClosableObjects()
// .penaltyLog()
// .build())
DbManager.initialize(applicationContext)
registerPlugin(AbsAudioPlayer::class.java)
registerPlugin(AbsDownloader::class.java)
registerPlugin(AbsFileSystem::class.java)
registerPlugin(AbsDatabase::class.java)
registerPlugin(AbsLogger::class.java)
super.onCreate(savedInstanceState)
Log.d(tag, "onCreate")
// Update the margins to handle edge-to-edge enforced in SDK 35
// See: https://developer.android.com/develop/ui/views/layout/edge-to-edge
val webView: WebView = findViewById(R.id.webview)
webView.setOnApplyWindowInsetsListener { v, insets ->
val (left, top, right, bottom) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val sysInsets = insets.getInsets(WindowInsets.Type.systemBars())
Log.d(tag, "safe sysInsets: $sysInsets")
arrayOf(sysInsets.left, sysInsets.top, sysInsets.right, sysInsets.bottom)
} else {
arrayOf(
insets.systemWindowInsetLeft,
insets.systemWindowInsetTop,
insets.systemWindowInsetRight,
insets.systemWindowInsetBottom
)
}
// Inject as CSS variables
// NOTE: Possibly able to use in the future to support edge-to-edge better.
val js = """
document.documentElement.style.setProperty('--safe-area-inset-top', '${top}px');
document.documentElement.style.setProperty('--safe-area-inset-bottom', '${bottom}px');
document.documentElement.style.setProperty('--safe-area-inset-left', '${left}px');
document.documentElement.style.setProperty('--safe-area-inset-right', '${right}px');
""".trimIndent()
webView.evaluateJavascript(js, null)
// Set margins
v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
leftMargin = left
bottomMargin = bottom
rightMargin = right
topMargin = top
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
WindowInsets.CONSUMED
} else {
insets
}
}
val permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
if (permission != PackageManager.PERMISSION_GRANTED) {

View file

@ -1,74 +0,0 @@
package com.audiobookshelf.app.data
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class AudioProbeStream(
val index:Int,
val codec_name:String,
val codec_long_name:String,
val channels:Int,
val channel_layout:String,
val duration:Double,
val bit_rate:Double
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class AudioProbeChapterTags(
val title:String
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class AudioProbeChapter(
val id:Int,
val start:Long,
val end:Long,
val tags:AudioProbeChapterTags?
) {
@JsonIgnore
fun getBookChapter():BookChapter {
val startS = start / 1000.0
val endS = end / 1000.0
val title = tags?.title ?: "Chapter $id"
return BookChapter(id, startS, endS, title)
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class AudioProbeFormatTags(
val artist:String?,
val album:String?,
val comment:String?,
val date:String?,
val genre:String?,
val title:String?
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class AudioProbeFormat(
val filename:String,
val format_name:String,
val duration:Double,
val size:Long,
val bit_rate:Double,
val tags:AudioProbeFormatTags?
)
@JsonIgnoreProperties(ignoreUnknown = true)
class AudioProbeResult (
val streams:MutableList<AudioProbeStream>,
val chapters:MutableList<AudioProbeChapter>,
val format:AudioProbeFormat) {
val duration get() = format.duration
val size get() = format.size
val title get() = format.tags?.title ?: format.filename.split("/").last()
val artist get() = format.tags?.artist ?: ""
@JsonIgnore
fun getBookChapters(): List<BookChapter> {
if (chapters.isEmpty()) return mutableListOf()
return chapters.map { it.getBookChapter() }
}
}

View file

@ -5,30 +5,34 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class AudioTrack(
var index:Int,
var startOffset:Double,
var duration:Double,
var title:String,
var contentUrl:String,
var mimeType:String,
var metadata:FileMetadata?,
var isLocal:Boolean,
var localFileId:String?,
var audioProbeResult:AudioProbeResult?,
var serverIndex:Int? // Need to know if server track index is different
var index: Int,
var startOffset: Double,
var duration: Double,
var title: String,
var contentUrl: String,
var mimeType: String,
var metadata: FileMetadata?,
var isLocal: Boolean,
var localFileId: String?,
// TODO: This should no longer be necessary
var serverIndex: Int? // Need to know if server track index is different
) {
@get:JsonIgnore
val startOffsetMs get() = (startOffset * 1000L).toLong()
val startOffsetMs
get() = (startOffset * 1000L).toLong()
@get:JsonIgnore
val durationMs get() = (duration * 1000L).toLong()
val durationMs
get() = (duration * 1000L).toLong()
@get:JsonIgnore
val endOffsetMs get() = startOffsetMs + durationMs
val endOffsetMs
get() = startOffsetMs + durationMs
@get:JsonIgnore
val relPath get() = metadata?.relPath ?: ""
val relPath
get() = metadata?.relPath ?: ""
@JsonIgnore
fun getBookChapter():BookChapter {
return BookChapter(index + 1,startOffset, startOffset + duration, title)
fun getBookChapter(): BookChapter {
return BookChapter(index + 1, startOffset, startOffset + duration, title)
}
}

View file

@ -0,0 +1,36 @@
package com.audiobookshelf.app.data
import android.content.Context
import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
class CollapsedSeries(
id:String,
var libraryId:String?,
var name:String,
//var nameIgnorePrefix:String,
var sequence:String?,
var libraryItemIds:MutableList<String>
) : LibraryItemWrapper(id) {
@get:JsonIgnore
val title get() = name
@get:JsonIgnore
val numBooks get() = libraryItemIds.size
@JsonIgnore
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
val extras = Bundle()
val mediaId = "__LIBRARY__${libraryId}__SERIE__${id}"
return MediaDescriptionCompat.Builder()
.setMediaId(mediaId)
.setTitle(title)
//.setIconUri(getCoverUri())
.setSubtitle("${numBooks} books")
.setExtras(extras)
.build()
}
}

View file

@ -1,12 +1,15 @@
package com.audiobookshelf.app.data
import android.content.Context
import android.icu.text.DateFormat
import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat
import android.support.v4.media.MediaMetadataCompat
import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.media.MediaManager
import com.fasterxml.jackson.annotation.*
import com.audiobookshelf.app.media.getUriToAbsIconDrawable
import java.util.Date
// This auto-detects whether it is a Book or Podcast
@JsonTypeInfo(use=JsonTypeInfo.Id.DEDUCTION)
@ -25,7 +28,8 @@ open class MediaType(var metadata:MediaTypeMetadata, var coverPath:String?) {
open fun removeAudioTrack(localFileId:String) { }
@JsonIgnore
open fun getLocalCopy():MediaType { return MediaType(MediaTypeMetadata("", false),null) }
@JsonIgnore
open fun checkHasTracks():Boolean { return false }
}
@JsonIgnoreProperties(ignoreUnknown = true)
@ -93,6 +97,11 @@ class Podcast(
return Podcast(metadata as PodcastMetadata,coverPath,tags, mutableListOf(),autoDownloadEpisodes, 0)
}
@JsonIgnore
override fun checkHasTracks():Boolean {
return (episodes?.size ?: numEpisodes ?: 0) > 0
}
@JsonIgnore
fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode):PodcastEpisode {
val localEpisodeId = "local_ep_" + episode.id
@ -182,6 +191,11 @@ class Book(
override fun getLocalCopy(): Book {
return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(), ebookFile, null,null, 0)
}
@JsonIgnore
override fun checkHasTracks():Boolean {
return (tracks?.size ?: numTracks ?: 0) > 0
}
}
// This auto-detects whether it is a BookMetadata or PodcastMetadata
@ -214,7 +228,9 @@ class BookMetadata(
var authorName:String?,
var authorNameLF:String?,
var narratorName:String?,
var seriesName:String?
var seriesName:String?,
@JsonFormat(with=[JsonFormat.Feature.ACCEPT_SINGLE_VALUE_AS_ARRAY])
var series:List<SeriesType>?
) : MediaTypeMetadata(title, explicit) {
@JsonIgnore
override fun getAuthorDisplayName():String { return authorName ?: "Unknown" }
@ -299,11 +315,18 @@ data class PodcastEpisode(
val libraryItemDescription = libraryItem.getMediaDescription(null, ctx)
val mediaId = localEpisodeId ?: id
var subtitle = libraryItemDescription.title
if (publishedAt !== null) {
val sdf = DateFormat.getDateInstance()
val publishedAtDT = Date(publishedAt!!)
subtitle = "${sdf.format(publishedAtDT)} / $subtitle"
}
val mediaDescriptionBuilder = MediaDescriptionCompat.Builder()
.setMediaId(mediaId)
.setTitle(title)
.setIconUri(coverUri)
.setSubtitle(libraryItemDescription.title)
.setSubtitle(subtitle)
.setExtras(extras)
libraryItemDescription.iconBitmap?.let {
@ -342,18 +365,39 @@ data class Library(
var name:String,
var folders:MutableList<Folder>,
var icon:String,
var mediaType:String
var mediaType:String,
var stats: LibraryStats?
) {
@JsonIgnore
fun getMediaMetadata(): MediaMetadataCompat {
fun getMediaMetadata(context: Context, targetType: String? = null): MediaMetadataCompat {
var mediaId = id
if (targetType !== null) {
mediaId = "__RECENTLY__$id"
}
return MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, name)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, name)
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToAbsIconDrawable(context, icon).toString())
}.build()
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class LibraryStats(
var totalItems: Int,
var totalSize: Long,
var totalDuration: Double,
var numAudioFiles: Int
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class SeriesType(
var id: String,
var name: String,
var sequence: String?
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class Folder(
var id:String,
@ -387,3 +431,84 @@ data class LibraryItemWithEpisode(
var libraryItemWrapper:LibraryItemWrapper,
var episode:PodcastEpisode
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class LibraryItemSearchResultSeriesItemType(
var series: LibrarySeriesItem,
var books: List<LibraryItem>?
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class LibraryItemSearchResultLibraryItemType(
val libraryItem: LibraryItem
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class LibraryItemSearchResultType(
var book:List<LibraryItemSearchResultLibraryItemType>?,
var podcast:List<LibraryItemSearchResultLibraryItemType>?,
var series:List<LibraryItemSearchResultSeriesItemType>?,
var authors:List<LibraryAuthorItem>?
)
// For personalized shelves
@JsonTypeInfo(
use=JsonTypeInfo.Id.NAME,
property = "type",
include = JsonTypeInfo.As.PROPERTY,
visible = true
)
@JsonSubTypes(
JsonSubTypes.Type(LibraryShelfBookEntity::class, name = "book"),
JsonSubTypes.Type(LibraryShelfSeriesEntity::class, name = "series"),
JsonSubTypes.Type(LibraryShelfAuthorEntity::class, name = "authors"),
JsonSubTypes.Type(LibraryShelfEpisodeEntity::class, name = "episode"),
JsonSubTypes.Type(LibraryShelfPodcastEntity::class, name = "podcast")
)
@JsonIgnoreProperties(ignoreUnknown = true)
sealed class LibraryShelfType(
open val id: String,
open val label: String,
open val total: Int,
open val type: String,
)
data class LibraryShelfBookEntity(
override val id: String,
override val label: String,
override val total: Int,
override val type: String,
val entities: List<LibraryItem>?
) : LibraryShelfType(id, label, total, type)
data class LibraryShelfSeriesEntity(
override val id: String,
override val label: String,
override val total: Int,
override val type: String,
val entities: List<LibrarySeriesItem>?
) : LibraryShelfType(id, label, total, type)
data class LibraryShelfAuthorEntity(
override val id: String,
override val label: String,
override val total: Int,
override val type: String,
val entities: List<LibraryAuthorItem>?
) : LibraryShelfType(id, label, total, type)
data class LibraryShelfEpisodeEntity(
override val id: String,
override val label: String,
override val total: Int,
override val type: String,
val entities: List<LibraryItem>?
) : LibraryShelfType(id, label, total, type)
data class LibraryShelfPodcastEntity(
override val id: String,
override val label: String,
override val total: Int,
override val type: String,
val entities: List<LibraryItem>?
) : LibraryShelfType(id, label, total, type)

View file

@ -28,11 +28,18 @@ enum class StreamingUsingCellularSetting {
ASK, ALWAYS, NEVER
}
enum class AndroidAutoBrowseSeriesSequenceOrderSetting {
ASC, DESC
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class ServerConnectionConfig(
var id:String,
var index:Int,
var name:String,
var address:String,
// version added after 0.9.81-beta
var version:String?,
var userId:String,
var username:String,
var token:String,
@ -131,9 +138,12 @@ data class DeviceSettings(
var sleepTimerLength: Long, // Time in milliseconds
var disableSleepTimerFadeOut: Boolean,
var disableSleepTimerResetFeedback: Boolean,
var enableSleepTimerAlmostDoneChime: Boolean,
var languageCode: String,
var downloadUsingCellular: DownloadUsingCellularSetting,
var streamingUsingCellular: StreamingUsingCellularSetting
var streamingUsingCellular: StreamingUsingCellularSetting,
var androidAutoBrowseLimitForGrouping: Int,
var androidAutoBrowseSeriesSequenceOrder: AndroidAutoBrowseSeriesSequenceOrderSetting
) {
companion object {
// Static method to get default device settings
@ -157,9 +167,12 @@ data class DeviceSettings(
autoSleepTimerAutoRewindTime = 300000L, // 5 minutes
disableSleepTimerFadeOut = false,
disableSleepTimerResetFeedback = false,
enableSleepTimerAlmostDoneChime = false,
languageCode = "en-us",
downloadUsingCellular = DownloadUsingCellularSetting.ALWAYS,
streamingUsingCellular = StreamingUsingCellularSetting.ALWAYS
streamingUsingCellular = StreamingUsingCellularSetting.ALWAYS,
androidAutoBrowseLimitForGrouping = 100,
androidAutoBrowseSeriesSequenceOrder = AndroidAutoBrowseSeriesSequenceOrderSetting.ASC
)
}
}
@ -180,9 +193,9 @@ data class DeviceSettings(
@JsonIgnore
fun getShakeThresholdGravity() : Float { // Used in ShakeDetector
return if (shakeSensitivity == ShakeSensitivitySetting.VERY_HIGH) 1.2f
else if (shakeSensitivity == ShakeSensitivitySetting.HIGH) 1.4f
else if (shakeSensitivity == ShakeSensitivitySetting.MEDIUM) 1.6f
return if (shakeSensitivity == ShakeSensitivitySetting.VERY_HIGH) 1.1f
else if (shakeSensitivity == ShakeSensitivitySetting.HIGH) 1.3f
else if (shakeSensitivity == ShakeSensitivitySetting.MEDIUM) 1.5f
else if (shakeSensitivity == ShakeSensitivitySetting.LOW) 2f
else if (shakeSensitivity == ShakeSensitivitySetting.VERY_LOW) 2.7f
else {

View file

@ -5,8 +5,7 @@
package com.audiobookshelf.app.data
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import org.json.JSONObject
@ -18,8 +17,7 @@ data class ItemInProgress(
val isLocal: Boolean
) {
companion object {
fun makeFromServerObject(serverItem: JSONObject):ItemInProgress {
val jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
fun makeFromServerObject(serverItem: JSONObject, jacksonMapper: ObjectMapper):ItemInProgress {
val libraryItem = jacksonMapper.readValue<LibraryItem>(serverItem.toString())
var episode:PodcastEpisode? = null

View file

@ -0,0 +1,63 @@
package com.audiobookshelf.app.data
import android.content.Context
import android.net.Uri
import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat
import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.BuildConfig
import com.audiobookshelf.app.R
import com.audiobookshelf.app.device.DeviceManager
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
class LibraryAuthorItem(
id:String,
var libraryId:String,
var name:String,
var description:String?,
var imagePath:String?,
var addedAt:Long,
var updatedAt:Long,
var numBooks:Int?,
var libraryItems:MutableList<LibraryItem>?,
var series:MutableList<LibrarySeriesItem>?
) : LibraryItemWrapper(id) {
@get:JsonIgnore
val title get() = name
@get:JsonIgnore
val bookCount get() = if (numBooks != null) numBooks else libraryItems!!.size
@JsonIgnore
fun getPortraitUri(): Uri {
if (imagePath == null) {
return Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.md_account_outline)
}
return Uri.parse("${DeviceManager.serverAddress}/api/authors/$id/image?token=${DeviceManager.token}")
}
@JsonIgnore
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, groupTitle: String?): MediaDescriptionCompat {
val extras = Bundle()
if (groupTitle !== null) {
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, groupTitle)
}
val mediaId = "__LIBRARY__${libraryId}__AUTHOR__${id}"
return MediaDescriptionCompat.Builder()
.setMediaId(mediaId)
.setTitle(title)
.setIconUri(getPortraitUri())
.setSubtitle("${bookCount} books")
.setExtras(extras)
.build()
}
@JsonIgnore
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
return getMediaDescription(progress, ctx, null)
}
}

View file

@ -0,0 +1,40 @@
package com.audiobookshelf.app.data
import android.content.Context
import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
class LibraryCollection(
id:String,
var libraryId:String,
var name:String,
//var userId:String?,
var description:String?,
var books:MutableList<LibraryItem>?,
) : LibraryItemWrapper(id) {
@get:JsonIgnore
val title get() = name
@get:JsonIgnore
val bookCount get() = if (books != null) books!!.size else 0
@get:JsonIgnore
val audiobookCount get() = books?.filter { book -> (book.media as Book).getAudioTracks().isNotEmpty() }?.size ?: 0
@JsonIgnore
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
val extras = Bundle()
val mediaId = "__LIBRARY__${libraryId}__COLLECTION__${id}"
return MediaDescriptionCompat.Builder()
.setMediaId(mediaId)
.setTitle(title)
//.setIconUri(getCoverUri())
.setSubtitle("${bookCount} books")
.setExtras(extras)
.build()
}
}

View file

@ -32,10 +32,18 @@ class LibraryItem(
var media:MediaType,
var libraryFiles:MutableList<LibraryFile>?,
var userMediaProgress:MediaProgress?, // Only included when requesting library item with progress (for downloads)
var localLibraryItemId:String? // For Android Auto
var collapsedSeries: CollapsedSeries?,
var localLibraryItemId:String?, // For Android Auto
val recentEpisode: PodcastEpisode? // Podcast episode shelf uses this
) : LibraryItemWrapper(id) {
@get:JsonIgnore
val title get() = media.metadata.title
val title: String
get() {
if (collapsedSeries != null) {
return collapsedSeries!!.title
}
return media.metadata.title
}
@get:JsonIgnore
val authorName get() = media.metadata.getAuthorDisplayName()
@ -45,62 +53,126 @@ class LibraryItem(
return Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)
}
// As of v2.17.0 token is not needed with cover image requests
if (DeviceManager.isServerVersionGreaterThanOrEqualTo("2.17.0")) {
return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover")
}
return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}")
}
@JsonIgnore
fun checkHasTracks():Boolean {
return if (mediaType == "podcast") {
((media as Podcast).numEpisodes ?: 0) > 0
} else {
((media as Book).numTracks ?: 0) > 0
return media.checkHasTracks()
}
@get:JsonIgnore
val seriesSequence: String
get() {
if (mediaType != "podcast") {
return ((media as Book).metadata as BookMetadata).series?.get(0)?.sequence.orEmpty()
} else {
return ""
}
}
@get:JsonIgnore
val seriesSequenceParts: List<String>
get() {
if (seriesSequence.isEmpty()) {
return listOf("")
}
return seriesSequence.split(".", limit = 2)
}
@JsonIgnore
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, authorId: String?, showSeriesNumber: Boolean?, groupTitle: String?): MediaDescriptionCompat {
val extras = Bundle()
if (collapsedSeries == null) {
if (localLibraryItemId != null) {
extras.putLong(
MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
MediaDescriptionCompat.STATUS_DOWNLOADED
)
}
if (progress != null) {
if (progress.isFinished) {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
)
} else {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
)
extras.putDouble(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress.progress
)
}
} else if (mediaType != "podcast") {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
)
}
if (media.metadata.explicit) {
extras.putLong(
MediaConstants.METADATA_KEY_IS_EXPLICIT,
MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT
)
}
}
if (groupTitle !== null) {
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, groupTitle)
}
val mediaId = if (localLibraryItemId != null) {
localLibraryItemId
} else if (collapsedSeries != null) {
if (authorId != null) {
"__LIBRARY__${libraryId}__AUTHOR_SERIES__${authorId}__${collapsedSeries!!.id}"
} else {
"__LIBRARY__${libraryId}__SERIES__${collapsedSeries!!.id}"
}
} else {
id
}
var subtitle = authorName
if (collapsedSeries != null) {
subtitle = "${collapsedSeries!!.numBooks} books"
}
var itemTitle = title
if (showSeriesNumber == true && seriesSequence != "") {
itemTitle = "$seriesSequence. $itemTitle"
}
return MediaDescriptionCompat.Builder()
.setMediaId(mediaId)
.setTitle(itemTitle)
.setIconUri(getCoverUri())
.setSubtitle(subtitle)
.setExtras(extras)
.build()
}
@JsonIgnore
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, authorId: String?, showSeriesNumber: Boolean?): MediaDescriptionCompat {
return getMediaDescription(progress, ctx, authorId, showSeriesNumber, null)
}
@JsonIgnore
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, authorId: String?): MediaDescriptionCompat {
return getMediaDescription(progress, ctx, authorId, null, null)
}
@JsonIgnore
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
val extras = Bundle()
if (localLibraryItemId != null) {
extras.putLong(
MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
MediaDescriptionCompat.STATUS_DOWNLOADED
)
}
if (progress != null) {
if (progress.isFinished) {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
)
} else {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
)
extras.putDouble(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress.progress
)
}
} else if (mediaType != "podcast") {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
)
}
if (media.metadata.explicit) {
extras.putLong(MediaConstants.METADATA_KEY_IS_EXPLICIT, MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT)
}
val mediaId = localLibraryItemId ?: id
return MediaDescriptionCompat.Builder()
.setMediaId(mediaId)
.setTitle(title)
.setIconUri(getCoverUri())
.setSubtitle(authorName)
.setExtras(extras)
.build()
/*
This is needed so Android auto library hierarchy for author series can be implemented
*/
return getMediaDescription(progress, ctx, null, null, null)
}
}

View file

@ -0,0 +1,60 @@
package com.audiobookshelf.app.data
import android.content.Context
import android.os.Bundle
import android.support.v4.media.MediaDescriptionCompat
import androidx.media.utils.MediaConstants
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
class LibrarySeriesItem(
id:String,
var libraryId:String,
var name:String,
var description:String?,
var addedAt:Long,
var updatedAt:Long,
var books:MutableList<LibraryItem>?,
var localLibraryItemId:String? // For Android Auto
) : LibraryItemWrapper(id) {
@get:JsonIgnore
val title get() = name
@get:JsonIgnore
val audiobookCount: Int
get() {
if (books == null) return 0
val booksWithAudio = books?.filter { b -> b.media.checkHasTracks() }
return booksWithAudio?.size ?: 0
}
@JsonIgnore
fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context, groupTitle: String?): MediaDescriptionCompat {
val extras = Bundle()
if (localLibraryItemId != null) {
extras.putLong(
MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
MediaDescriptionCompat.STATUS_DOWNLOADED
)
}
if (groupTitle !== null) {
extras.putString(MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE, groupTitle)
}
val mediaId = "__LIBRARY__${libraryId}__SERIES__${id}"
return MediaDescriptionCompat.Builder()
.setMediaId(mediaId)
.setTitle(title)
//.setIconUri(getCoverUri())
.setSubtitle("$audiobookCount books")
.setExtras(extras)
.build()
}
@JsonIgnore
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
return getMediaDescription(progress, ctx, null)
}
}

View file

@ -18,6 +18,7 @@ import com.audiobookshelf.app.device.DeviceManager
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.audiobookshelf.app.player.PLAYMETHOD_LOCAL
import java.io.File
import java.util.*
@JsonIgnoreProperties(ignoreUnknown = true)
@ -78,6 +79,27 @@ class LocalLibraryItem(
}
}
@JsonIgnore
fun hasTracks(episode:PodcastEpisode?): Boolean {
var audioTracks = media.getAudioTracks() as MutableList<AudioTrack>
if (episode != null) { // Get podcast episode audio track
episode.audioTrack?.let { at -> mutableListOf(at) }?.let { tracks -> audioTracks = tracks }
}
if (audioTracks.size == 0) return false
audioTracks.forEach {
// Check that metadata is not null
if (it.metadata === null) {
return false
}
// Check that file exists
val file = File(it.metadata!!.path)
if (!file.exists()) {
return false
}
}
return true
}
@JsonIgnore
fun getPlaybackSession(episode:PodcastEpisode?, deviceInfo:DeviceInfo):PlaybackSession {
val localEpisodeId = episode?.id

View file

@ -4,70 +4,65 @@ import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
/*
Used as a helper class to generate LocalLibraryItem from scan results
*/
Used as a helper class to generate LocalLibraryItem from scan results
*/
@JsonIgnoreProperties(ignoreUnknown = true)
data class LocalMediaItem(
var id:String,
var name: String,
var mediaType:String,
var folderId:String,
var contentUrl:String,
var simplePath: String,
var basePath:String,
var absolutePath:String,
var audioTracks:MutableList<AudioTrack>,
var ebookFile:EBookFile?,
var localFiles:MutableList<LocalFile>,
var coverContentUrl:String?,
var coverAbsolutePath:String?
var id: String,
var name: String,
var mediaType: String,
var folderId: String,
var contentUrl: String,
var simplePath: String,
var basePath: String,
var absolutePath: String,
var audioTracks: MutableList<AudioTrack>,
var ebookFile: EBookFile?,
var localFiles: MutableList<LocalFile>,
var coverContentUrl: String?,
var coverAbsolutePath: String?
) {
@JsonIgnore
fun getDuration():Double {
fun getDuration(): Double {
var total = 0.0
audioTracks.forEach{ total += it.duration }
audioTracks.forEach { total += it.duration }
return total
}
@JsonIgnore
fun getTotalSize():Long {
fun getTotalSize(): Long {
var total = 0L
localFiles.forEach { total += it.size }
return total
}
@JsonIgnore
fun getMediaMetadata():MediaTypeMetadata {
fun getMediaMetadata(): MediaTypeMetadata {
return if (mediaType == "book") {
BookMetadata(name,null, mutableListOf(), mutableListOf(), mutableListOf(),null,null,null,null,null,null,null,false,null,null,null,null)
BookMetadata(
name,
null,
mutableListOf(),
mutableListOf(),
mutableListOf(),
null,
null,
null,
null,
null,
null,
null,
false,
null,
null,
null,
null,
null
)
} else {
PodcastMetadata(name,null,null, mutableListOf(), false)
}
}
@JsonIgnore
fun getAudiobookChapters():List<BookChapter> {
if (mediaType != "book" || audioTracks.isEmpty()) return mutableListOf()
if (audioTracks.size == 1) { // Single track audiobook look for chapters from ffprobe
return audioTracks[0].audioProbeResult?.getBookChapters() ?: mutableListOf()
}
// Multi-track make chapters from tracks
return audioTracks.map { it.getBookChapter() }
}
@JsonIgnore
fun getLocalLibraryItem():LocalLibraryItem {
val mediaMetadata = getMediaMetadata()
if (mediaType == "book") {
val chapters = getAudiobookChapters()
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)
} else {
val podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false, 0)
podcast.setAudioTracks(audioTracks) // Builds episodes from audio tracks
return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false, mediaType, podcast,localFiles,coverContentUrl, coverAbsolutePath, true, null,null,null,null)
PodcastMetadata(name, null, null, mutableListOf(), false)
}
}
}

View file

@ -4,14 +4,14 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
class MediaItemHistory(
var id: String, // media id
var mediaDisplayTitle: String,
var libraryItemId: String,
var episodeId: String?,
var isLocal:Boolean,
var serverConnectionConfigId:String?,
var serverAddress:String?,
var serverUserId:String?,
var createdAt: Long,
var events:MutableList<MediaItemEvent>,
var id: String, // media id
var mediaDisplayTitle: String,
var libraryItemId: String,
var episodeId: String?,
var isLocal: Boolean,
var serverConnectionConfigId: String?,
var serverAddress: String?,
var serverUserId: String?,
var createdAt: Long,
var events: MutableList<MediaItemEvent>,
)

View file

@ -12,6 +12,7 @@ import com.audiobookshelf.app.BuildConfig
import com.audiobookshelf.app.R
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.media.MediaProgressSyncData
import com.audiobookshelf.app.player.*
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.google.android.exoplayer2.MediaItem
@ -19,61 +20,70 @@ import com.google.android.exoplayer2.MediaMetadata
import com.google.android.gms.cast.MediaInfo
import com.google.android.gms.cast.MediaQueueItem
import com.google.android.gms.common.images.WebImage
import com.audiobookshelf.app.player.*
@JsonIgnoreProperties(ignoreUnknown = true)
class PlaybackSession(
var id:String,
var userId:String?,
var libraryItemId:String?,
var episodeId:String?,
var mediaType:String,
var mediaMetadata:MediaTypeMetadata,
var deviceInfo:DeviceInfo,
var chapters:List<BookChapter>,
var displayTitle: String?,
var displayAuthor: String?,
var coverPath:String?,
var duration:Double,
var playMethod:Int,
var startedAt:Long,
var updatedAt:Long,
var timeListening:Long,
var audioTracks:MutableList<AudioTrack>,
var currentTime:Double,
var libraryItem:LibraryItem?,
var localLibraryItem:LocalLibraryItem?,
var localEpisodeId:String?,
var serverConnectionConfigId:String?,
var serverAddress:String?,
var mediaPlayer:String?
var id: String,
var userId: String?,
var libraryItemId: String?,
var episodeId: String?,
var mediaType: String,
var mediaMetadata: MediaTypeMetadata,
var deviceInfo: DeviceInfo,
var chapters: List<BookChapter>,
var displayTitle: String?,
var displayAuthor: String?,
var coverPath: String?,
var duration: Double,
var playMethod: Int,
var startedAt: Long,
var updatedAt: Long,
var timeListening: Long,
var audioTracks: MutableList<AudioTrack>,
var currentTime: Double,
var libraryItem: LibraryItem?,
var localLibraryItem: LocalLibraryItem?,
var localEpisodeId: String?,
var serverConnectionConfigId: String?,
var serverAddress: String?,
var mediaPlayer: String?
) {
@get:JsonIgnore
val isHLS get() = playMethod == PLAYMETHOD_TRANSCODE
val isHLS
get() = playMethod == PLAYMETHOD_TRANSCODE
@get:JsonIgnore
val isDirectPlay get() = playMethod == PLAYMETHOD_DIRECTPLAY
val isDirectPlay
get() = playMethod == PLAYMETHOD_DIRECTPLAY
@get:JsonIgnore
val isLocal get() = playMethod == PLAYMETHOD_LOCAL
val isLocal
get() = playMethod == PLAYMETHOD_LOCAL
@get:JsonIgnore
val isPodcastEpisode get() = mediaType == "podcast"
val isPodcastEpisode
get() = mediaType == "podcast"
@get:JsonIgnore
val currentTimeMs get() = (currentTime * 1000L).toLong()
val currentTimeMs
get() = (currentTime * 1000L).toLong()
@get:JsonIgnore
val totalDurationMs get() = (getTotalDuration() * 1000L).toLong()
val totalDurationMs
get() = (getTotalDuration() * 1000L).toLong()
@get:JsonIgnore
val localLibraryItemId get() = localLibraryItem?.id ?: ""
val localLibraryItemId
get() = localLibraryItem?.id ?: ""
@get:JsonIgnore
val localMediaProgressId get() = if (localEpisodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
val localMediaProgressId
get() =
if (localEpisodeId.isNullOrEmpty()) localLibraryItemId
else "$localLibraryItemId-$localEpisodeId"
@get:JsonIgnore
val progress get() = currentTime / getTotalDuration()
val progress
get() = currentTime / getTotalDuration()
@get:JsonIgnore
val isLocalLibraryItemOnly get() = localLibraryItemId != "" && libraryItemId == null
@get:JsonIgnore
val mediaItemId get() = if (isLocalLibraryItemOnly) localMediaProgressId else if (episodeId.isNullOrEmpty()) libraryItemId ?: "" else "$libraryItemId-$episodeId"
val mediaItemId
get() = if (episodeId.isNullOrEmpty()) libraryItemId ?: "" else "$libraryItemId-$episodeId"
@JsonIgnore
fun getCurrentTrackIndex():Int {
fun getCurrentTrackIndex(): Int {
for (i in 0 until audioTracks.size) {
val track = audioTracks[i]
if (currentTimeMs >= track.startOffsetMs && (track.endOffsetMs > currentTimeMs)) {
@ -84,7 +94,7 @@ class PlaybackSession(
}
@JsonIgnore
fun getNextTrackIndex():Int {
fun getNextTrackIndex(): Int {
for (i in 0 until audioTracks.size) {
val track = audioTracks[i]
if (currentTimeMs < track.startOffsetMs) {
@ -95,68 +105,100 @@ class PlaybackSession(
}
@JsonIgnore
fun getChapterForTime(time:Long):BookChapter? {
fun getChapterForTime(time: Long): BookChapter? {
if (chapters.isEmpty()) return null
return chapters.find { time >= it.startMs && it.endMs > time}
return chapters.find { time >= it.startMs && it.endMs > time }
}
@JsonIgnore
fun getCurrentTrackEndTime():Long {
fun getCurrentTrackEndTime(): Long {
val currentTrack = audioTracks[this.getCurrentTrackIndex()]
return currentTrack.startOffsetMs + currentTrack.durationMs
}
@JsonIgnore
fun getNextChapterForTime(time:Long):BookChapter? {
fun getNextChapterForTime(time: Long): BookChapter? {
if (chapters.isEmpty()) return null
return chapters.find { time < it.startMs } // First chapter where start time is > then time
}
@JsonIgnore
fun getNextTrackEndTime():Long {
fun getNextTrackEndTime(): Long {
val currentTrack = audioTracks[this.getNextTrackIndex()]
return currentTrack.startOffsetMs + currentTrack.durationMs
}
@JsonIgnore
fun getCurrentTrackTimeMs():Long {
fun getCurrentTrackTimeMs(): Long {
val currentTrack = audioTracks[this.getCurrentTrackIndex()]
val time = currentTime - currentTrack.startOffset
return (time * 1000L).toLong()
}
@JsonIgnore
fun getTrackStartOffsetMs(index:Int):Long {
fun getTrackStartOffsetMs(index: Int): Long {
if (index < 0 || index >= audioTracks.size) return 0L
val currentTrack = audioTracks[index]
return (currentTrack.startOffset * 1000L).toLong()
}
@JsonIgnore
fun getTotalDuration():Double {
fun getTotalDuration(): Double {
var total = 0.0
audioTracks.forEach { total += it.duration }
return total
}
@JsonIgnore
fun getCoverUri(ctx:Context): Uri {
fun checkIsServerVersionGte(compareVersion: String): Boolean {
// Safety check this playback session is the same one currently connected (should always be)
if (DeviceManager.serverConnectionConfigId != serverConnectionConfigId) {
return false
}
return DeviceManager.isServerVersionGreaterThanOrEqualTo(compareVersion)
}
@JsonIgnore
fun getCoverUri(ctx: Context): Uri {
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())
coverUri =
FileProvider.getUriForFile(
ctx,
"${BuildConfig.APPLICATION_ID}.fileprovider",
coverUri.toFile()
)
}
return coverUri ?: Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + 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)
if (coverPath == null)
return Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)
// As of v2.17.0 token is not needed with cover image requests
if (checkIsServerVersionGte("2.17.0")) {
return Uri.parse("$serverAddress/api/items/$libraryItemId/cover")
}
return Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}")
}
@JsonIgnore
fun getContentUri(audioTrack:AudioTrack): Uri {
fun getContentUri(audioTrack: AudioTrack): Uri {
if (isLocal) return Uri.parse(audioTrack.contentUrl) // Local content url
// As of v2.22.0 tracks use a different endpoint
// See: https://github.com/advplyr/audiobookshelf/pull/4263
if (checkIsServerVersionGte("2.22.0")) {
return if (isDirectPlay) {
Uri.parse("$serverAddress/public/session/$id/track/${audioTrack.index}")
} else {
// Transcode uses HlsRouter on server
Uri.parse("$serverAddress${audioTrack.contentUrl}")
}
}
return Uri.parse("$serverAddress${audioTrack.contentUrl}?token=${DeviceManager.token}")
}
@ -164,28 +206,34 @@ class PlaybackSession(
fun getMediaMetadataCompat(ctx: Context): MediaMetadataCompat {
val coverUri = getCoverUri(ctx)
val metadataBuilder = MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, displayTitle)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, displayTitle)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, displayAuthor)
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, displayAuthor)
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, displayAuthor)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, displayAuthor)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, displayAuthor)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, displayAuthor)
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, coverUri.toString())
.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, coverUri.toString())
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, coverUri.toString())
val metadataBuilder =
MediaMetadataCompat.Builder()
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, displayTitle)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, displayTitle)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, displayAuthor)
.putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, displayAuthor)
.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, displayAuthor)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, displayAuthor)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, displayAuthor)
.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, displayAuthor)
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, coverUri.toString())
.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, coverUri.toString())
.putString(
MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,
coverUri.toString()
)
// Local covers get bitmap
if (localLibraryItem?.coverContentUrl != null) {
val bitmap = if (Build.VERSION.SDK_INT < 28) {
MediaStore.Images.Media.getBitmap(ctx.contentResolver, coverUri)
} else {
val source: ImageDecoder.Source = ImageDecoder.createSource(ctx.contentResolver, coverUri)
ImageDecoder.decodeBitmap(source)
}
val bitmap =
if (Build.VERSION.SDK_INT < 28) {
MediaStore.Images.Media.getBitmap(ctx.contentResolver, coverUri)
} else {
val source: ImageDecoder.Source =
ImageDecoder.createSource(ctx.contentResolver, coverUri)
ImageDecoder.decodeBitmap(source)
}
metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap)
metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, bitmap)
}
@ -194,26 +242,27 @@ class PlaybackSession(
}
@JsonIgnore
fun getExoMediaMetadata(ctx:Context): MediaMetadata {
fun getExoMediaMetadata(ctx: Context): MediaMetadata {
val coverUri = getCoverUri(ctx)
val metadataBuilder = MediaMetadata.Builder()
.setTitle(displayTitle)
.setDisplayTitle(displayTitle)
.setArtist(displayAuthor)
.setAlbumArtist(displayAuthor)
.setSubtitle(displayAuthor)
.setAlbumTitle(displayAuthor)
.setDescription(displayAuthor)
.setArtworkUri(coverUri)
.setMediaType(MediaMetadata.MEDIA_TYPE_AUDIO_BOOK)
val metadataBuilder =
MediaMetadata.Builder()
.setTitle(displayTitle)
.setDisplayTitle(displayTitle)
.setArtist(displayAuthor)
.setAlbumArtist(displayAuthor)
.setSubtitle(displayAuthor)
.setAlbumTitle(displayAuthor)
.setDescription(displayAuthor)
.setArtworkUri(coverUri)
.setMediaType(MediaMetadata.MEDIA_TYPE_AUDIO_BOOK)
return metadataBuilder.build()
}
@JsonIgnore
fun getMediaItems(ctx:Context):List<MediaItem> {
val mediaItems:MutableList<MediaItem> = mutableListOf()
fun getMediaItems(ctx: Context): List<MediaItem> {
val mediaItems: MutableList<MediaItem> = mutableListOf()
for (audioTrack in audioTracks) {
val mediaMetadata = this.getExoMediaMetadata(ctx)
@ -221,50 +270,107 @@ class PlaybackSession(
val mimeType = audioTrack.mimeType
val queueItem = getQueueItem(audioTrack) // Queue item used in exo player CastManager
val mediaItem = MediaItem.Builder().setUri(mediaUri).setTag(queueItem).setMediaMetadata(mediaMetadata).setMimeType(mimeType).build()
val mediaItem =
MediaItem.Builder()
.setUri(mediaUri)
.setTag(queueItem)
.setMediaMetadata(mediaMetadata)
.setMimeType(mimeType)
.build()
mediaItems.add(mediaItem)
}
return mediaItems
}
@JsonIgnore
fun getCastMediaMetadata(audioTrack:AudioTrack):com.google.android.gms.cast.MediaMetadata {
val castMetadata = com.google.android.gms.cast.MediaMetadata(com.google.android.gms.cast.MediaMetadata.MEDIA_TYPE_AUDIOBOOK_CHAPTER)
fun getCastMediaMetadata(audioTrack: AudioTrack): com.google.android.gms.cast.MediaMetadata {
val castMetadata =
com.google.android.gms.cast.MediaMetadata(
com.google.android.gms.cast.MediaMetadata.MEDIA_TYPE_AUDIOBOOK_CHAPTER
)
// As of v2.17.0 token is not needed with cover image requests
val coverUri = if (checkIsServerVersionGte("2.17.0")) {
Uri.parse("$serverAddress/api/items/$libraryItemId/cover")
} else {
Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}")
}
// Cast always uses server cover uri
coverPath?.let {
castMetadata.addImage(WebImage(Uri.parse("$serverAddress/api/items/$libraryItemId/cover?token=${DeviceManager.token}")))
castMetadata.addImage(WebImage(coverUri))
}
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_TITLE, displayTitle ?: "")
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_ARTIST, displayAuthor ?: "")
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_ALBUM_TITLE, displayAuthor ?: "")
castMetadata.putString(com.google.android.gms.cast.MediaMetadata.KEY_CHAPTER_TITLE, audioTrack.title)
castMetadata.putString(
com.google.android.gms.cast.MediaMetadata.KEY_ARTIST,
displayAuthor ?: ""
)
castMetadata.putString(
com.google.android.gms.cast.MediaMetadata.KEY_ALBUM_TITLE,
displayAuthor ?: ""
)
castMetadata.putString(
com.google.android.gms.cast.MediaMetadata.KEY_CHAPTER_TITLE,
audioTrack.title
)
castMetadata.putInt(com.google.android.gms.cast.MediaMetadata.KEY_TRACK_NUMBER, audioTrack.index)
castMetadata.putInt(
com.google.android.gms.cast.MediaMetadata.KEY_TRACK_NUMBER,
audioTrack.index
)
return castMetadata
}
@JsonIgnore
fun getQueueItem(audioTrack:AudioTrack):MediaQueueItem {
fun getQueueItem(audioTrack: AudioTrack): MediaQueueItem {
val castMetadata = getCastMediaMetadata(audioTrack)
val mediaUri = getContentUri(audioTrack)
val mediaInfo = MediaInfo.Builder(mediaUri.toString()).apply {
setContentUrl(mediaUri.toString())
setContentType(audioTrack.mimeType)
setMetadata(castMetadata)
setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
}.build()
val mediaInfo =
MediaInfo.Builder(mediaUri.toString())
.apply {
setContentUrl(mediaUri.toString())
setContentType(audioTrack.mimeType)
setMetadata(castMetadata)
setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
}
.build()
return MediaQueueItem.Builder(mediaInfo).apply {
setPlaybackDuration(audioTrack.duration)
}.build()
return MediaQueueItem.Builder(mediaInfo)
.apply { setPlaybackDuration(audioTrack.duration) }
.build()
}
@JsonIgnore
fun clone():PlaybackSession {
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)
fun clone(): PlaybackSession {
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
@ -275,7 +381,25 @@ class PlaybackSession(
}
@JsonIgnore
fun getNewLocalMediaProgress():LocalMediaProgress {
return LocalMediaProgress(localMediaProgressId,localLibraryItemId,localEpisodeId,getTotalDuration(),progress,currentTime,false,null,null,updatedAt,startedAt,null,serverConnectionConfigId,serverAddress,userId,libraryItemId,episodeId)
fun getNewLocalMediaProgress(): LocalMediaProgress {
return LocalMediaProgress(
localMediaProgressId,
localLibraryItemId,
localEpisodeId,
getTotalDuration(),
progress,
currentTime,
false,
null,
null,
updatedAt,
startedAt,
null,
serverConnectionConfigId,
serverAddress,
userId,
libraryItemId,
episodeId
)
}
}

View file

@ -12,11 +12,19 @@ import com.audiobookshelf.app.managers.DbManager
import com.audiobookshelf.app.player.PlayerNotificationService
import com.audiobookshelf.app.updateAppWidget
/** Interface for widget event handling. */
interface WidgetEventEmitter {
fun onPlayerChanged(pns:PlayerNotificationService)
/**
* Called when the player state changes.
* @param pns The PlayerNotificationService instance.
*/
fun onPlayerChanged(pns: PlayerNotificationService)
/** Called when the player is closed. */
fun onPlayerClosed()
}
/** Singleton object for managing device-related operations. */
object DeviceManager {
const val tag = "DeviceManager"
@ -25,20 +33,29 @@ object DeviceManager {
var serverConnectionConfig: ServerConnectionConfig? = null
val serverConnectionConfigId get() = serverConnectionConfig?.id ?: ""
val serverAddress get() = serverConnectionConfig?.address ?: ""
val serverUserId get() = serverConnectionConfig?.userId ?: ""
val token get() = serverConnectionConfig?.token ?: ""
val isConnectedToServer get() = serverConnectionConfig != null
val serverConnectionConfigName get() = serverConnectionConfig?.name ?: ""
val serverConnectionConfigString get() = serverConnectionConfig?.name ?: "No server connection"
val serverAddress
get() = serverConnectionConfig?.address ?: ""
val serverUserId
get() = serverConnectionConfig?.userId ?: ""
val token
get() = serverConnectionConfig?.token ?: ""
val serverVersion get() = serverConnectionConfig?.version ?: ""
val isConnectedToServer
get() = serverConnectionConfig != null
var widgetUpdater:WidgetEventEmitter? = null
var widgetUpdater: WidgetEventEmitter? = null
init {
Log.d(tag, "Device Manager Singleton invoked")
// Initialize new sleep timer settings and shake sensitivity added in v0.9.61
if (deviceData.deviceSettings?.autoSleepTimerStartTime == null || deviceData.deviceSettings?.autoSleepTimerEndTime == null) {
if (deviceData.deviceSettings?.autoSleepTimerStartTime == null ||
deviceData.deviceSettings?.autoSleepTimerEndTime == null
) {
deviceData.deviceSettings?.autoSleepTimerStartTime = "22:00"
deviceData.deviceSettings?.autoSleepTimerStartTime = "06:00"
deviceData.deviceSettings?.autoSleepTimerEndTime = "06:00"
deviceData.deviceSettings?.sleepTimerLength = 900000L
}
if (deviceData.deviceSettings?.shakeSensitivity == null) {
@ -48,6 +65,10 @@ object DeviceManager {
if (deviceData.deviceSettings?.autoSleepTimerAutoRewindTime == null) {
deviceData.deviceSettings?.autoSleepTimerAutoRewindTime = 300000L // 5 minutes
}
// Initialize sleep timer almost done chime added in v0.9.81
if (deviceData.deviceSettings?.enableSleepTimerAlmostDoneChime == null) {
deviceData.deviceSettings?.enableSleepTimerAlmostDoneChime = false
}
// Language added in v0.9.69
if (deviceData.deviceSettings?.languageCode == null) {
@ -61,19 +82,79 @@ object DeviceManager {
if (deviceData.deviceSettings?.streamingUsingCellular == null) {
deviceData.deviceSettings?.streamingUsingCellular = StreamingUsingCellularSetting.ALWAYS
}
if (deviceData.deviceSettings?.androidAutoBrowseLimitForGrouping == null) {
deviceData.deviceSettings?.androidAutoBrowseLimitForGrouping = 100
}
if (deviceData.deviceSettings?.androidAutoBrowseSeriesSequenceOrder == null) {
deviceData.deviceSettings?.androidAutoBrowseSeriesSequenceOrder =
AndroidAutoBrowseSeriesSequenceOrderSetting.ASC
}
}
fun getBase64Id(id:String):String {
return android.util.Base64.encodeToString(id.toByteArray(), android.util.Base64.URL_SAFE or android.util.Base64.NO_WRAP)
/**
* Encodes the given ID to a Base64 string.
* @param id The ID to encode.
* @return The Base64 encoded string.
*/
fun getBase64Id(id: String): String {
return android.util.Base64.encodeToString(
id.toByteArray(),
android.util.Base64.URL_SAFE or android.util.Base64.NO_WRAP
)
}
fun getServerConnectionConfig(id:String?):ServerConnectionConfig? {
if (id == null) return null
return deviceData.serverConnectionConfigs.find { it.id == id }
/**
* Retrieves the server connection configuration for the given ID.
* @param id The ID of the server connection configuration.
* @return The ServerConnectionConfig instance or null if not found.
*/
fun getServerConnectionConfig(id: String?): ServerConnectionConfig? {
return id?.let { deviceData.serverConnectionConfigs.find { it.id == id } }
}
fun checkConnectivity(ctx:Context): Boolean {
val connectivityManager = ctx.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
/**
* Check if the currently connected server version is >= compareVersion
* Abs server only uses major.minor.patch
* Note: Version is returned in Abs auth payloads starting v2.6.0
* Note: Version is saved with the server connection config starting after v0.9.81
*
* @example
* serverVersion=2.25.1
* isServerVersionGreaterThanOrEqualTo("2.26.0") = false
*
* serverVersion=2.26.1
* isServerVersionGreaterThanOrEqualTo("2.26.0") = true
*/
fun isServerVersionGreaterThanOrEqualTo(compareVersion:String):Boolean {
if (serverVersion == "") return false
if (compareVersion == "") return true
val serverVersionParts = serverVersion.split(".").map { it.toIntOrNull() ?: 0 }
val compareVersionParts = compareVersion.split(".").map { it.toIntOrNull() ?: 0 }
// Compare major, minor, and patch components
for (i in 0 until maxOf(serverVersionParts.size, compareVersionParts.size)) {
val serverVersionComponent = serverVersionParts.getOrElse(i) { 0 }
val compareVersionComponent = compareVersionParts.getOrElse(i) { 0 }
if (serverVersionComponent < compareVersionComponent) {
return false // Server version is less than compareVersion
} else if (serverVersionComponent > compareVersionComponent) {
return true // Server version is greater than compareVersion
}
}
return true // versions are equal in major, minor, and patch
}
/**
* Checks the network connectivity status.
* @param ctx The context to use for checking connectivity.
* @return True if connected to the internet, false otherwise.
*/
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)) {
@ -90,35 +171,58 @@ object DeviceManager {
return false
}
fun setLastPlaybackSession(playbackSession:PlaybackSession) {
/**
* Sets the last playback session.
* @param playbackSession The playback session to set.
*/
fun setLastPlaybackSession(playbackSession: PlaybackSession) {
deviceData.lastPlaybackSession = playbackSession
dbManager.saveDeviceData(deviceData)
}
fun initializeWidgetUpdater(context:Context) {
/**
* Initializes the widget updater.
* @param context The context to use for initializing the widget updater.
*/
fun initializeWidgetUpdater(context: Context) {
Log.d(tag, "Initializing widget updater")
widgetUpdater = (object : WidgetEventEmitter {
override fun onPlayerChanged(pns: PlayerNotificationService) {
widgetUpdater =
(object : WidgetEventEmitter {
override fun onPlayerChanged(pns: PlayerNotificationService) {
val isPlaying = pns.currentPlayer.isPlaying
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()
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
)
}
}
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)
}
}
})
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
)
}
}
})
}
}

View file

@ -13,83 +13,144 @@ import java.io.File
class FolderScanner(var ctx: Context) {
private val tag = "FolderScanner"
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
private var jacksonMapper =
jacksonObjectMapper()
.enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
data class DownloadItemScanResult(val localLibraryItem:LocalLibraryItem, var localMediaProgress:LocalMediaProgress?)
data class DownloadItemScanResult(
val localLibraryItem: LocalLibraryItem,
var localMediaProgress: LocalMediaProgress?
)
private fun getLocalLibraryItemId(mediaItemId:String):String {
private fun getLocalLibraryItemId(mediaItemId: String): String {
return "local_" + DeviceManager.getBase64Id(mediaItemId)
}
private fun scanInternalDownloadItem(downloadItem:DownloadItem, cb: (DownloadItemScanResult?) -> Unit) {
private fun scanInternalDownloadItem(
downloadItem: DownloadItem,
cb: (DownloadItemScanResult?) -> Unit
) {
val localLibraryItemId = "local_${downloadItem.libraryItemId}"
var localEpisodeId:String? = null
var localLibraryItem:LocalLibraryItem?
var localEpisodeId: String? = null
var localLibraryItem: LocalLibraryItem?
if (downloadItem.mediaType == "book") {
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)
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 {
// 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, downloadItem.itemFolderPath, downloadItem.itemFolderPath, "", false, downloadItem.mediaType, downloadItem.media.getLocalCopy(), mutableListOf(), null, null, true,downloadItem.serverConnectionConfigId,downloadItem.serverAddress,downloadItem.serverUserId,downloadItem.libraryItemId)
Log.d(
tag,
"[FolderScanner] Podcast local library item not created yet for ${downloadItem.media.metadata.title}"
)
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
downloadItem.downloadItemParts.forEach { downloadItemPart ->
Log.d(tag, "Scan internal storage item with finalDestinationUri=${downloadItemPart.finalDestinationUri}")
Log.d(
tag,
"Scan internal storage item with finalDestinationUri=${downloadItemPart.finalDestinationUri}"
)
val file = File(downloadItemPart.finalDestinationPath)
Log.d(tag, "Scan internal storage item created file ${file.name}")
if (file == null) {
Log.e(tag, "scanInternalDownloadItem: Null docFile for path ${downloadItemPart.finalDestinationPath}")
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}"
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()
)
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)
val trackFileMetadata = FileMetadata(file.name, file.extension, file.absolutePath, file.getBasePath(ctx), file.length())
val trackFileMetadata =
FileMetadata(
file.name,
file.extension,
file.absolutePath,
file.getBasePath(ctx),
file.length()
)
// Create new audio track
val track = AudioTrack(
audioTrackFromServer.index,
audioTrackFromServer.startOffset,
audioTrackFromServer.duration,
localFile.filename ?: "",
localFile.contentUrl,
localFile.mimeType ?: "",
trackFileMetadata,
true,
localFileId,
null,
audioTrackFromServer.index
)
val track =
AudioTrack(
audioTrackFromServer.index,
audioTrackFromServer.startOffset,
audioTrackFromServer.duration,
localFile.filename ?: "",
localFile.contentUrl,
localFile.mimeType ?: "",
trackFileMetadata,
true,
localFileId,
audioTrackFromServer.index
)
audioTracks.add(track)
Log.d(
tag,
"scanInternalDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}"
tag,
"scanInternalDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}"
)
// Add podcast episodes to library
@ -98,40 +159,51 @@ class FolderScanner(var ctx: Context) {
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}"
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()
)
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
)
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())
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
@ -141,7 +213,10 @@ class FolderScanner(var ctx: Context) {
}
if (audioTracks.isEmpty() && !foundEBookFile) {
Log.d(tag, "scanDownloadItem did not find any audio tracks or ebook file 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)
}
@ -163,30 +238,37 @@ class FolderScanner(var ctx: Context) {
localLibraryItem.media.setAudioTracks(audioTracks)
}
val downloadItemScanResult = DownloadItemScanResult(localLibraryItem,null)
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 = mediaProgress.isFinished,
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}")
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 = mediaProgress.isFinished,
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
@ -206,7 +288,7 @@ class FolderScanner(var ctx: Context) {
}
val folderDf = DocumentFileCompat.fromUri(ctx, Uri.parse(downloadItem.localFolder.contentUrl))
val foldersFound = folderDf?.search(true, DocumentFileType.FOLDER) ?: mutableListOf()
val foldersFound = folderDf?.search(true, DocumentFileType.FOLDER) ?: mutableListOf()
var itemFolderId = ""
var itemFolderUrl = ""
@ -235,72 +317,188 @@ class FolderScanner(var ctx: Context) {
}
val localLibraryItemId = getLocalLibraryItemId(itemFolderId)
Log.d(tag, "scanDownloadItem starting for ${downloadItem.itemFolderPath} | ${df.uri} | Item Folder Id:$itemFolderId | LLI Id:$localLibraryItemId")
Log.d(
tag,
"scanDownloadItem starting for ${downloadItem.itemFolderPath} | ${df.uri} | Item Folder Id:$itemFolderId | LLI Id:$localLibraryItemId"
)
// Search for files in media item folder
// 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/*"))
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?
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)
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)
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()
val audioTracks: MutableList<AudioTrack> = mutableListOf()
var foundEBookFile = false
filesFound.forEach { docFile ->
val itemPart = downloadItem.downloadItemParts.find { itemPart ->
itemPart.filename == docFile.name
}
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}")
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}")
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())
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 trackFileMetadata = FileMetadata(docFile.name ?: "", docFile.extension ?: "", docFile.getAbsolutePath(ctx), docFile.getBasePath(ctx), docFile.length())
val track = AudioTrack(audioTrackFromServer.index, audioTrackFromServer.startOffset, audioTrackFromServer.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", trackFileMetadata, true, localFileId, null, audioTrackFromServer.index)
val trackFileMetadata =
FileMetadata(
docFile.name ?: "",
docFile.extension ?: "",
docFile.getAbsolutePath(ctx),
docFile.getBasePath(ctx),
docFile.length()
)
val track =
AudioTrack(
audioTrackFromServer.index,
audioTrackFromServer.startOffset,
audioTrackFromServer.duration,
localFile.filename ?: "",
localFile.contentUrl,
localFile.mimeType ?: "",
trackFileMetadata,
true,
localFileId,
audioTrackFromServer.index
)
audioTracks.add(track)
Log.d(tag, "scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}")
Log.d(
tag,
"scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}"
)
// 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}")
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())
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)
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())
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
@ -309,7 +507,10 @@ class FolderScanner(var ctx: Context) {
}
if (audioTracks.isEmpty() && !foundEBookFile) {
Log.d(tag, "scanDownloadItem did not find any audio tracks or ebook file 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)
}
@ -331,30 +532,37 @@ class FolderScanner(var ctx: Context) {
localLibraryItem.media.setAudioTracks(audioTracks)
}
val downloadItemScanResult = DownloadItemScanResult(localLibraryItem,null)
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 = mediaProgress.isFinished,
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}")
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 = mediaProgress.isFinished,
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)

View file

@ -4,6 +4,8 @@ import android.content.Context
import android.util.Log
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.models.DownloadItem
import com.audiobookshelf.app.plugins.AbsLog
import com.audiobookshelf.app.plugins.AbsLogger
import io.paperdb.Paper
import java.io.File
@ -22,45 +24,47 @@ class DbManager {
}
fun getDeviceData(): DeviceData {
return Paper.book("device").read("data") ?: DeviceData(mutableListOf(), null, DeviceSettings.default(), null)
return Paper.book("device").read("data")
?: DeviceData(mutableListOf(), null, DeviceSettings.default(), null)
}
fun saveDeviceData(deviceData: DeviceData) {
Paper.book("device").write("data", deviceData)
}
fun getLocalLibraryItems(mediaType:String? = null):MutableList<LocalLibraryItem> {
val localLibraryItems:MutableList<LocalLibraryItem> = mutableListOf()
fun getLocalLibraryItems(mediaType: String? = null): MutableList<LocalLibraryItem> {
val localLibraryItems: MutableList<LocalLibraryItem> = mutableListOf()
Paper.book("localLibraryItems").allKeys.forEach {
val localLibraryItem: LocalLibraryItem? = Paper.book("localLibraryItems").read(it)
if (localLibraryItem != null && (mediaType.isNullOrEmpty() || mediaType == localLibraryItem.mediaType)) {
if (localLibraryItem != null &&
(mediaType.isNullOrEmpty() || mediaType == localLibraryItem.mediaType)
) {
localLibraryItems.add(localLibraryItem)
}
}
return localLibraryItems
}
fun getLocalLibraryItemsInFolder(folderId:String):List<LocalLibraryItem> {
fun getLocalLibraryItemsInFolder(folderId: String): List<LocalLibraryItem> {
val localLibraryItems = getLocalLibraryItems()
return localLibraryItems.filter {
it.folderId == folderId
}
return localLibraryItems.filter { it.folderId == folderId }
}
fun getLocalLibraryItemByLId(libraryItemId:String): LocalLibraryItem? {
fun getLocalLibraryItemByLId(libraryItemId: String): LocalLibraryItem? {
return getLocalLibraryItems().find { it.libraryItemId == libraryItemId }
}
fun getLocalLibraryItem(localLibraryItemId:String): LocalLibraryItem? {
fun getLocalLibraryItem(localLibraryItemId: String): LocalLibraryItem? {
return Paper.book("localLibraryItems").read(localLibraryItemId)
}
fun getLocalLibraryItemWithEpisode(podcastEpisodeId:String): LibraryItemWithEpisode? {
fun getLocalLibraryItemWithEpisode(podcastEpisodeId: String): LibraryItemWithEpisode? {
var podcastEpisode: PodcastEpisode? = null
val localLibraryItem = getLocalLibraryItems("podcast").find { localLibraryItem ->
val podcast = localLibraryItem.media as Podcast
podcastEpisode = podcast.episodes?.find { it.id == podcastEpisodeId }
podcastEpisode != null
}
val localLibraryItem =
getLocalLibraryItems("podcast").find { localLibraryItem ->
val podcast = localLibraryItem.media as Podcast
podcastEpisode = podcast.episodes?.find { it.id == podcastEpisodeId }
podcastEpisode != null
}
return if (localLibraryItem != null) {
LibraryItemWithEpisode(localLibraryItem, podcastEpisode!!)
} else {
@ -68,14 +72,12 @@ class DbManager {
}
}
fun removeLocalLibraryItem(localLibraryItemId:String) {
fun removeLocalLibraryItem(localLibraryItemId: String) {
Paper.book("localLibraryItems").delete(localLibraryItemId)
}
fun saveLocalLibraryItems(localLibraryItems:List<LocalLibraryItem>) {
localLibraryItems.map {
Paper.book("localLibraryItems").write(it.id, it)
}
fun saveLocalLibraryItems(localLibraryItems: List<LocalLibraryItem>) {
localLibraryItems.map { Paper.book("localLibraryItems").write(it.id, it) }
}
fun saveLocalLibraryItem(localLibraryItem: LocalLibraryItem) {
@ -83,28 +85,24 @@ class DbManager {
}
fun saveLocalFolder(localFolder: LocalFolder) {
Paper.book("localFolders").write(localFolder.id,localFolder)
Paper.book("localFolders").write(localFolder.id, localFolder)
}
fun getLocalFolder(folderId:String): LocalFolder? {
fun getLocalFolder(folderId: String): LocalFolder? {
return Paper.book("localFolders").read(folderId)
}
fun getAllLocalFolders():List<LocalFolder> {
val localFolders:MutableList<LocalFolder> = mutableListOf()
fun getAllLocalFolders(): List<LocalFolder> {
val localFolders: MutableList<LocalFolder> = mutableListOf()
Paper.book("localFolders").allKeys.forEach { localFolderId ->
Paper.book("localFolders").read<LocalFolder>(localFolderId)?.let {
localFolders.add(it)
}
Paper.book("localFolders").read<LocalFolder>(localFolderId)?.let { localFolders.add(it) }
}
return localFolders
}
fun removeLocalFolder(folderId:String) {
fun removeLocalFolder(folderId: String) {
val localLibraryItems = getLocalLibraryItemsInFolder(folderId)
localLibraryItems.forEach {
Paper.book("localLibraryItems").delete(it.id)
}
localLibraryItems.forEach { Paper.book("localLibraryItems").delete(it.id) }
Paper.book("localFolders").delete(folderId)
}
@ -112,29 +110,28 @@ class DbManager {
Paper.book("downloadItems").write(downloadItem.id, downloadItem)
}
fun removeDownloadItem(downloadItemId:String) {
fun removeDownloadItem(downloadItemId: String) {
Paper.book("downloadItems").delete(downloadItemId)
}
fun getDownloadItems():List<DownloadItem> {
val downloadItems:MutableList<DownloadItem> = mutableListOf()
fun getDownloadItems(): List<DownloadItem> {
val downloadItems: MutableList<DownloadItem> = mutableListOf()
Paper.book("downloadItems").allKeys.forEach { downloadItemId ->
Paper.book("downloadItems").read<DownloadItem>(downloadItemId)?.let {
downloadItems.add(it)
}
Paper.book("downloadItems").read<DownloadItem>(downloadItemId)?.let { downloadItems.add(it) }
}
return downloadItems
}
fun saveLocalMediaProgress(mediaProgress: LocalMediaProgress) {
Paper.book("localMediaProgress").write(mediaProgress.id,mediaProgress)
Paper.book("localMediaProgress").write(mediaProgress.id, mediaProgress)
}
// For books this will just be the localLibraryItemId for podcast episodes this will be "{localLibraryItemId}-{episodeId}"
fun getLocalMediaProgress(localMediaProgressId:String): LocalMediaProgress? {
// For books this will just be the localLibraryItemId for podcast episodes this will be
// "{localLibraryItemId}-{episodeId}"
fun getLocalMediaProgress(localMediaProgressId: String): LocalMediaProgress? {
return Paper.book("localMediaProgress").read(localMediaProgressId)
}
fun getAllLocalMediaProgress():List<LocalMediaProgress> {
val mediaProgress:MutableList<LocalMediaProgress> = mutableListOf()
fun getAllLocalMediaProgress(): List<LocalMediaProgress> {
val mediaProgress: MutableList<LocalMediaProgress> = mutableListOf()
Paper.book("localMediaProgress").allKeys.forEach { localMediaProgressId ->
Paper.book("localMediaProgress").read<LocalMediaProgress>(localMediaProgressId)?.let {
mediaProgress.add(it)
@ -142,7 +139,7 @@ class DbManager {
}
return mediaProgress
}
fun removeLocalMediaProgress(localMediaProgressId:String) {
fun removeLocalMediaProgress(localMediaProgressId: String) {
Paper.book("localMediaProgress").delete(localMediaProgressId)
}
@ -158,35 +155,50 @@ class DbManager {
var hasUpdates = false
// Check local files
lli.localFiles = lli.localFiles.filter { localFile ->
val file = File(localFile.absolutePath)
if (!file.exists()) {
Log.d(tag, "cleanLocalLibraryItems: Local file ${localFile.absolutePath} was removed from library item ${lli.media.metadata.title}")
hasUpdates = true
}
file.exists()
} as MutableList<LocalFile>
lli.localFiles =
lli.localFiles.filter { localFile ->
val file = File(localFile.absolutePath)
if (!file.exists()) {
Log.d(
tag,
"cleanLocalLibraryItems: Local file ${localFile.absolutePath} was removed from library item ${lli.media.metadata.title}"
)
hasUpdates = true
}
file.exists()
} as
MutableList<LocalFile>
// Check audio tracks and episodes
if (lli.isPodcast) {
val podcast = lli.media as Podcast
podcast.episodes = podcast.episodes?.filter { ep ->
if (lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } == null) {
Log.d(tag, "cleanLocalLibraryItems: Podcast episode ${ep.title} was removed from library item ${lli.media.metadata.title}")
hasUpdates = true
}
ep.audioTrack != null && lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } != null
} as MutableList<PodcastEpisode>
podcast.episodes =
podcast.episodes?.filter { ep ->
if (lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } == null) {
Log.d(
tag,
"cleanLocalLibraryItems: Podcast episode ${ep.title} was removed from library item ${lli.media.metadata.title}"
)
hasUpdates = true
}
ep.audioTrack != null &&
lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } != null
} as
MutableList<PodcastEpisode>
} else {
val book = lli.media as Book
book.tracks = book.tracks?.filter { track ->
if (lli.localFiles.find { lf -> lf.id == track.localFileId } == null) {
Log.d(tag, "cleanLocalLibraryItems: Audio track ${track.title} was removed from library item ${lli.media.metadata.title}")
hasUpdates = true
}
lli.localFiles.find { lf -> lf.id == track.localFileId } != null
} as MutableList<AudioTrack>
book.tracks =
book.tracks?.filter { track ->
if (lli.localFiles.find { lf -> lf.id == track.localFileId } == null) {
Log.d(
tag,
"cleanLocalLibraryItems: Audio track ${track.title} was removed from library item ${lli.media.metadata.title}"
)
hasUpdates = true
}
lli.localFiles.find { lf -> lf.id == track.localFileId } != null
} as
MutableList<AudioTrack>
}
// Check cover still there
@ -194,14 +206,22 @@ class DbManager {
val coverFile = File(it)
if (!coverFile.exists()) {
Log.d(tag, "cleanLocalLibraryItems: Cover $it was removed from library item ${lli.media.metadata.title}")
Log.d(
tag,
"cleanLocalLibraryItems: Cover $it was removed from library item ${lli.media.metadata.title}"
)
lli.coverAbsolutePath = null
lli.coverContentUrl = null
hasUpdates = true
}
}
if (hasUpdates) {
if (lli.serverConnectionConfigId == null) {
// Local-only item support was removed in app version 0.9.67, remove any remaining local
// only items beginning in 0.9.80
Log.d(tag, "cleanLocalLibraryItems: Local only item ${lli.id} - removing from ABS")
Paper.book("localLibraryItems").delete(lli.id)
} else if (hasUpdates) {
Log.d(tag, "cleanLocalLibraryItems: Saving local library item ${lli.id}")
Paper.book("localLibraryItems").write(lli.id, lli)
}
@ -215,11 +235,18 @@ class DbManager {
localMediaProgress.forEach {
val matchingLLI = localLibraryItems.find { lli -> lli.id == it.localLibraryItemId }
if (!it.id.startsWith("local")) {
// A bug on the server when syncing local media progress was replacing the media progress id causing duplicate progress. Remove them.
Log.d(tag, "cleanLocalMediaProgress: Invalid local media progress does not start with 'local' (fixed on server 2.0.24)")
// A bug on the server when syncing local media progress was replacing the media progress id
// causing duplicate progress. Remove them.
Log.d(
tag,
"cleanLocalMediaProgress: Invalid local media progress does not start with 'local' (fixed on server 2.0.24)"
)
Paper.book("localMediaProgress").delete(it.id)
} else if (matchingLLI == null) {
Log.d(tag, "cleanLocalMediaProgress: No matching local library item for local media progress ${it.id} - removing")
Log.d(
tag,
"cleanLocalMediaProgress: No matching local library item for local media progress ${it.id} - removing"
)
Paper.book("localMediaProgress").delete(it.id)
} else if (matchingLLI.isPodcast) {
if (it.localEpisodeId.isNullOrEmpty()) {
@ -229,7 +256,10 @@ class DbManager {
val podcast = matchingLLI.media as Podcast
val matchingLEp = podcast.episodes?.find { ep -> ep.id == it.localEpisodeId }
if (matchingLEp == null) {
Log.d(tag, "cleanLocalMediaProgress: Podcast media progress for episode ${it.localEpisodeId} not found - removing")
Log.d(
tag,
"cleanLocalMediaProgress: Podcast media progress for episode ${it.localEpisodeId} not found - removing"
)
Paper.book("localMediaProgress").delete(it.id)
}
}
@ -238,20 +268,20 @@ class DbManager {
}
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)
}
fun savePlaybackSession(playbackSession: PlaybackSession) {
Paper.book("playbackSession").write(playbackSession.id,playbackSession)
Paper.book("playbackSession").write(playbackSession.id, playbackSession)
}
fun removePlaybackSession(playbackSessionId:String) {
fun removePlaybackSession(playbackSessionId: String) {
Paper.book("playbackSession").delete(playbackSessionId)
}
fun getPlaybackSessions():List<PlaybackSession> {
val sessions:MutableList<PlaybackSession> = mutableListOf()
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)
@ -259,4 +289,35 @@ class DbManager {
}
return sessions
}
fun saveLog(log:AbsLog) {
Paper.book("log").write(log.id, log)
}
fun getAllLogs() : List<AbsLog> {
val logs:MutableList<AbsLog> = mutableListOf()
Paper.book("log").allKeys.forEach { logId ->
Paper.book("log").read<AbsLog>(logId)?.let {
logs.add(it)
}
}
return logs.sortedBy { it.timestamp }
}
fun removeAllLogs() {
Paper.book("log").destroy()
}
fun cleanLogs() {
val numberOfHoursToKeep = 48
val keepLogCutoff = System.currentTimeMillis() - (3600000 * numberOfHoursToKeep)
val allLogs = getAllLogs()
var logsRemoved = 0
allLogs.forEach {
if (it.timestamp < keepLogCutoff) {
Paper.book("log").delete(it.id)
logsRemoved++
}
}
if (logsRemoved > 0) {
AbsLogger.info("DbManager", "cleanLogs: Removed $logsRemoved logs older than $numberOfHoursToKeep hours")
}
}
}

View file

@ -18,18 +18,26 @@ import com.audiobookshelf.app.models.DownloadItemPart
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.JSObject
import java.io.File
import java.io.FileOutputStream
import java.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
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) {
/** Manages download items and their parts. */
class DownloadItemManager(
var downloadManager: DownloadManager,
private var folderScanner: FolderScanner,
var mainActivity: MainActivity,
private var clientEventEmitter: DownloadEventEmitter
) {
val tag = "DownloadItemManager"
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 {
InProgress,
@ -37,25 +45,28 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
Failed
}
var downloadItemQueue: MutableList<DownloadItem> = mutableListOf() // All pending and downloading items
var currentDownloadItemParts: MutableList<DownloadItemPart> = mutableListOf() // Item parts currently being downloaded
var downloadItemQueue: MutableList<DownloadItem> =
mutableListOf() // All pending and downloading items
var currentDownloadItemParts: MutableList<DownloadItemPart> =
mutableListOf() // Item parts currently being downloaded
interface DownloadEventEmitter {
fun onDownloadItem(downloadItem:DownloadItem)
fun onDownloadItemPartUpdate(downloadItemPart:DownloadItemPart)
fun onDownloadItemComplete(jsobj:JSObject)
fun onDownloadItem(downloadItem: DownloadItem)
fun onDownloadItemPartUpdate(downloadItemPart: DownloadItemPart)
fun onDownloadItemComplete(jsobj: JSObject)
}
interface InternalProgressCallback {
fun onProgress(totalBytesWritten:Long, progress: Long)
fun onProgress(totalBytesWritten: Long, progress: Long)
fun onComplete(failed: Boolean)
}
companion object {
var isDownloading:Boolean = false
var isDownloading: Boolean = false
}
fun addDownloadItem(downloadItem:DownloadItem) {
/** Adds a download item to the queue and starts processing the queue. */
fun addDownloadItem(downloadItem: DownloadItem) {
DeviceManager.dbManager.saveDownloadItem(downloadItem)
Log.i(tag, "Add download item ${downloadItem.media.metadata.title}")
@ -64,42 +75,18 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
checkUpdateDownloadQueue()
}
/** Checks and updates the download queue. */
private fun checkUpdateDownloadQueue() {
for (downloadItem in downloadItemQueue) {
val numPartsToGet = maxSimultaneousDownloads - currentDownloadItemParts.size
val nextDownloadItemParts = downloadItem.getNextDownloadItemParts(numPartsToGet)
Log.d(tag, "checkUpdateDownloadQueue: numPartsToGet=$numPartsToGet, nextDownloadItemParts=${nextDownloadItemParts.size}")
Log.d(
tag,
"checkUpdateDownloadQueue: numPartsToGet=$numPartsToGet, nextDownloadItemParts=${nextDownloadItemParts.size}"
)
if (nextDownloadItemParts.size > 0) {
nextDownloadItemParts.forEach {
if (it.isInternalStorage) {
val file = File(it.finalDestinationPath)
file.parentFile?.mkdirs()
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)
it.downloadId = 1
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)
}
}
if (nextDownloadItemParts.isNotEmpty()) {
processDownloadItemParts(nextDownloadItemParts)
}
if (currentDownloadItemParts.size >= maxSimultaneousDownloads) {
@ -107,9 +94,59 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
}
}
if (currentDownloadItemParts.size > 0) startWatchingDownloads()
if (currentDownloadItemParts.isNotEmpty()) startWatchingDownloads()
}
/** Processes the download item parts. */
private fun processDownloadItemParts(nextDownloadItemParts: List<DownloadItemPart>) {
nextDownloadItemParts.forEach {
if (it.isInternalStorage) {
startInternalDownload(it)
} else {
startExternalDownload(it)
}
}
}
/** Starts an internal download. */
private fun startInternalDownload(downloadItemPart: DownloadItemPart) {
val file = File(downloadItemPart.finalDestinationPath)
file.parentFile?.mkdirs()
val fileOutputStream = FileOutputStream(downloadItemPart.finalDestinationPath)
val internalProgressCallback =
object : InternalProgressCallback {
override fun onProgress(totalBytesWritten: Long, progress: Long) {
downloadItemPart.bytesDownloaded = totalBytesWritten
downloadItemPart.progress = progress
}
override fun onComplete(failed: Boolean) {
downloadItemPart.failed = failed
downloadItemPart.completed = true
}
}
Log.d(
tag,
"Start internal download to destination path ${downloadItemPart.finalDestinationPath} from ${downloadItemPart.serverUrl}"
)
InternalDownloadManager(fileOutputStream, internalProgressCallback)
.download(downloadItemPart.serverUrl)
downloadItemPart.downloadId = 1
currentDownloadItemParts.add(downloadItemPart)
}
/** Starts an external download. */
private fun startExternalDownload(downloadItemPart: DownloadItemPart) {
val dlRequest = downloadItemPart.getDownloadRequest()
val downloadId = downloadManager.enqueue(dlRequest)
downloadItemPart.downloadId = downloadId
Log.d(tag, "checkUpdateDownloadQueue: Starting download item part, downloadId=$downloadId")
currentDownloadItemParts.add(downloadItemPart)
}
/** Starts watching the downloads. */
private fun startWatchingDownloads() {
if (isDownloading) return // Already watching
@ -117,25 +154,13 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
Log.d(tag, "Starting watching downloads")
isDownloading = true
while (currentDownloadItemParts.size > 0) {
val itemParts = currentDownloadItemParts.filter { !it.isMoving }.map { it }
while (currentDownloadItemParts.isNotEmpty()) {
val itemParts = currentDownloadItemParts.filter { !it.isMoving }
for (downloadItemPart in itemParts) {
if (downloadItemPart.isInternalStorage) {
clientEventEmitter.onDownloadItemPartUpdate(downloadItemPart)
if (downloadItemPart.completed) {
val downloadItem = downloadItemQueue.find { it.id == downloadItemPart.downloadItemId }
downloadItem?.let {
checkDownloadItemFinished(it)
}
currentDownloadItemParts.remove(downloadItemPart)
}
handleInternalDownloadPart(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)
handleExternalDownloadPart(downloadItemPart)
}
}
@ -151,7 +176,29 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
}
}
private fun checkDownloadItemPart(downloadItemPart:DownloadItemPart):DownloadCheckStatus {
/** Handles an internal download part. */
private fun handleInternalDownloadPart(downloadItemPart: DownloadItemPart) {
clientEventEmitter.onDownloadItemPartUpdate(downloadItemPart)
if (downloadItemPart.completed) {
val downloadItem = downloadItemQueue.find { it.id == downloadItemPart.downloadItemId }
downloadItem?.let { checkDownloadItemFinished(it) }
currentDownloadItemParts.remove(downloadItemPart)
}
}
/** Handles an external download part. */
private fun handleExternalDownloadPart(downloadItemPart: DownloadItemPart) {
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)
}
/** Checks the status of a download item part. */
private fun checkDownloadItemPart(downloadItemPart: DownloadItemPart): DownloadCheckStatus {
val downloadId = downloadItemPart.downloadId ?: return DownloadCheckStatus.Failed
val query = DownloadManager.Query().setFilterById(downloadId)
@ -159,12 +206,17 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
if (it.moveToFirst()) {
val bytesColumnIndex = it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
val statusColumnIndex = it.getColumnIndex(DownloadManager.COLUMN_STATUS)
val bytesDownloadedColumnIndex = it.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
val bytesDownloadedColumnIndex =
it.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
val totalBytes = if (bytesColumnIndex >= 0) it.getInt(bytesColumnIndex) else 0
val downloadStatus = if (statusColumnIndex >= 0) it.getInt(statusColumnIndex) else 0
val bytesDownloadedSoFar = if (bytesDownloadedColumnIndex >= 0) it.getLong(bytesDownloadedColumnIndex) else 0
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} bytes $totalBytes | bytes dled $bytesDownloadedSoFar | downloadStatus $downloadStatus")
val bytesDownloadedSoFar =
if (bytesDownloadedColumnIndex >= 0) it.getLong(bytesDownloadedColumnIndex) else 0
Log.d(
tag,
"checkDownloads Download ${downloadItemPart.filename} bytes $totalBytes | bytes dled $bytesDownloadedSoFar | downloadStatus $downloadStatus"
)
return when (downloadStatus) {
DownloadManager.STATUS_SUCCESSFUL -> {
@ -183,8 +235,12 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
DownloadCheckStatus.Failed
}
else -> {
val percentProgress = if (totalBytes > 0) ((bytesDownloadedSoFar * 100L) / totalBytes) else 0
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Progress = $percentProgress%")
val percentProgress =
if (totalBytes > 0) ((bytesDownloadedSoFar * 100L) / totalBytes) else 0
Log.d(
tag,
"checkDownloads Download ${downloadItemPart.filename} Progress = $percentProgress%"
)
downloadItemPart.progress = percentProgress
downloadItemPart.bytesDownloaded = bytesDownloadedSoFar
@ -200,84 +256,120 @@ class DownloadItemManager(var downloadManager:DownloadManager, private var folde
}
}
private fun handleDownloadItemPartCheck(downloadCheckStatus:DownloadCheckStatus, downloadItemPart:DownloadItemPart) {
/** Handles the result of a download item part check. */
private fun handleDownloadItemPartCheck(
downloadCheckStatus: DownloadCheckStatus,
downloadItemPart: DownloadItemPart
) {
val downloadItem = downloadItemQueue.find { it.id == downloadItemPart.downloadItemId }
if (downloadItem == null) {
Log.e(tag, "Download item part finished but download item not found ${downloadItemPart.filename}")
Log.e(
tag,
"Download item part finished but download item not found ${downloadItemPart.filename}"
)
currentDownloadItemParts.remove(downloadItemPart)
} else if (downloadCheckStatus == DownloadCheckStatus.Successful) {
val file = DocumentFileCompat.fromUri(mainActivity, downloadItemPart.destinationUri)
Log.d(tag, "DOWNLOAD: DESTINATION URI ${downloadItemPart.destinationUri}")
val fcb = object : FileCallback() {
override fun onPrepare() {
Log.d(tag, "DOWNLOAD: PREPARING MOVE FILE")
}
override fun onFailed(errorCode: ErrorCode) {
Log.e(tag, "DOWNLOAD: FAILED TO MOVE FILE $errorCode")
downloadItemPart.failed = true
downloadItemPart.isMoving = false
file?.delete()
checkDownloadItemFinished(downloadItem)
currentDownloadItemParts.remove(downloadItemPart)
}
override fun onCompleted(result:Any) {
Log.d(tag, "DOWNLOAD: FILE MOVE COMPLETED")
val resultDocFile = result as DocumentFile
Log.d(tag, "DOWNLOAD: COMPLETED FILE INFO (name=${resultDocFile.name}) ${resultDocFile.getAbsolutePath(mainActivity)}")
// Rename to fix appended .mp3 on m4b/m4a files
// REF: https://github.com/anggrayudi/SimpleStorage/issues/94
val docNameLowerCase = resultDocFile.name?.lowercase(Locale.getDefault()) ?: ""
if (docNameLowerCase.endsWith(".m4b.mp3")|| docNameLowerCase.endsWith(".m4a.mp3")) {
resultDocFile.renameTo(downloadItemPart.filename)
}
downloadItemPart.moved = true
downloadItemPart.isMoving = false
checkDownloadItemFinished(downloadItem)
currentDownloadItemParts.remove(downloadItemPart)
}
}
val localFolderFile = DocumentFileCompat.fromUri(mainActivity, Uri.parse(downloadItemPart.localFolderUrl))
if (localFolderFile == null) {
// fAILED
downloadItemPart.failed = true
Log.e(tag, "Local Folder File from uri is null")
checkDownloadItemFinished(downloadItem)
currentDownloadItemParts.remove(downloadItemPart)
} else {
downloadItemPart.isMoving = true
val mimetype = if (downloadItemPart.audioTrack != null) MimeType.AUDIO else MimeType.IMAGE
val fileDescription = FileDescription(downloadItemPart.filename, downloadItemPart.finalDestinationSubfolder, mimetype)
file?.moveFileTo(mainActivity, localFolderFile, fileDescription, fcb)
}
moveDownloadedFile(downloadItem, downloadItemPart)
} else if (downloadCheckStatus != DownloadCheckStatus.InProgress) {
checkDownloadItemFinished(downloadItem)
currentDownloadItemParts.remove(downloadItemPart)
}
}
private fun checkDownloadItemFinished(downloadItem:DownloadItem) {
/** Moves the downloaded file to its final destination. */
private fun moveDownloadedFile(downloadItem: DownloadItem, downloadItemPart: DownloadItemPart) {
val file = DocumentFileCompat.fromUri(mainActivity, downloadItemPart.destinationUri)
Log.d(tag, "DOWNLOAD: DESTINATION URI ${downloadItemPart.destinationUri}")
val fcb =
object : FileCallback() {
override fun onPrepare() {
Log.d(tag, "DOWNLOAD: PREPARING MOVE FILE")
}
override fun onFailed(errorCode: ErrorCode) {
Log.e(tag, "DOWNLOAD: FAILED TO MOVE FILE $errorCode")
downloadItemPart.failed = true
downloadItemPart.isMoving = false
file?.delete()
checkDownloadItemFinished(downloadItem)
currentDownloadItemParts.remove(downloadItemPart)
}
override fun onCompleted(result: Any) {
Log.d(tag, "DOWNLOAD: FILE MOVE COMPLETED")
val resultDocFile = result as DocumentFile
Log.d(
tag,
"DOWNLOAD: COMPLETED FILE INFO (name=${resultDocFile.name}) ${resultDocFile.getAbsolutePath(mainActivity)}"
)
// Rename to fix appended .mp3 on m4b/m4a files
// REF: https://github.com/anggrayudi/SimpleStorage/issues/94
val docNameLowerCase = resultDocFile.name?.lowercase(Locale.getDefault()) ?: ""
if (docNameLowerCase.endsWith(".m4b.mp3") || docNameLowerCase.endsWith(".m4a.mp3")
) {
resultDocFile.renameTo(downloadItemPart.filename)
}
downloadItemPart.moved = true
downloadItemPart.isMoving = false
checkDownloadItemFinished(downloadItem)
currentDownloadItemParts.remove(downloadItemPart)
}
}
val localFolderFile =
DocumentFileCompat.fromUri(mainActivity, Uri.parse(downloadItemPart.localFolderUrl))
if (localFolderFile == null) {
// Failed
downloadItemPart.failed = true
Log.e(tag, "Local Folder File from uri is null")
checkDownloadItemFinished(downloadItem)
currentDownloadItemParts.remove(downloadItemPart)
} else {
downloadItemPart.isMoving = true
val mimetype = if (downloadItemPart.audioTrack != null) MimeType.AUDIO else MimeType.IMAGE
val fileDescription =
FileDescription(
downloadItemPart.filename,
downloadItemPart.finalDestinationSubfolder,
mimetype
)
file?.moveFileTo(mainActivity, localFolderFile, fileDescription, fcb)
}
}
/** Checks if a download item is finished and processes it. */
private fun checkDownloadItemFinished(downloadItem: DownloadItem) {
if (downloadItem.isDownloadFinished) {
Log.i(tag, "Download Item finished ${downloadItem.media.metadata.title}")
GlobalScope.launch(Dispatchers.IO) {
folderScanner.scanDownloadItem(downloadItem) { downloadItemScanResult ->
Log.d(tag, "Item download complete ${downloadItem.itemTitle} | local library item id: ${downloadItemScanResult?.localLibraryItem?.id}")
Log.d(
tag,
"Item download complete ${downloadItem.itemTitle} | local library item id: ${downloadItemScanResult?.localLibraryItem?.id}"
)
val jsobj = JSObject()
jsobj.put("libraryItemId", downloadItem.id)
jsobj.put("localFolderId", downloadItem.localFolder.id)
val jsobj =
JSObject().apply {
put("libraryItemId", downloadItem.id)
put("localFolderId", downloadItem.localFolder.id)
downloadItemScanResult?.localLibraryItem?.let { localLibraryItem ->
jsobj.put("localLibraryItem", JSObject(jacksonMapper.writeValueAsString(localLibraryItem)))
}
downloadItemScanResult?.localMediaProgress?.let { localMediaProgress ->
jsobj.put("localMediaProgress", JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
}
downloadItemScanResult?.localLibraryItem?.let { localLibraryItem ->
put(
"localLibraryItem",
JSObject(jacksonMapper.writeValueAsString(localLibraryItem))
)
}
downloadItemScanResult?.localMediaProgress?.let { localMediaProgress ->
put(
"localMediaProgress",
JSObject(jacksonMapper.writeValueAsString(localMediaProgress))
)
}
}
launch(Dispatchers.Main) {
clientEventEmitter.onDownloadItemComplete(jsobj)

View file

@ -1,60 +1,95 @@
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.*
import java.util.concurrent.TimeUnit
import okhttp3.*
/**
* Manages the internal download process.
*
* @property outputStream The output stream to write the downloaded data.
* @property progressCallback The callback to report download progress.
*/
class InternalDownloadManager(
private val outputStream: FileOutputStream,
private val progressCallback: DownloadItemManager.InternalProgressCallback
) : AutoCloseable {
class InternalDownloadManager(outputStream:FileOutputStream, private val progressCallback: DownloadItemManager.InternalProgressCallback) : AutoCloseable {
private val tag = "InternalDownloadManager"
private val client: OkHttpClient = OkHttpClient()
private val client: OkHttpClient =
OkHttpClient.Builder().connectTimeout(30, TimeUnit.SECONDS).build()
private val writer = BinaryFileWriter(outputStream, progressCallback)
/**
* Downloads a file from the given URL.
*
* @param url The URL to download the file from.
* @throws IOException If an I/O error occurs.
*/
@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)
}
fun download(url: String) {
val request: Request = Request.Builder().url(url).addHeader("Accept-Encoding", "identity").build()
client.newCall(request)
.enqueue(
object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e(tag, "Download URL $url FAILED", e)
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)
}
})
override fun onResponse(call: Call, response: Response) {
response.body?.let { responseBody ->
val length: Long = response.header("Content-Length")?.toLongOrNull() ?: 0L
writer.write(responseBody.byteStream(), length)
}
?: run {
Log.e(tag, "Response doesn't contain a file")
progressCallback.onComplete(true)
}
}
}
)
}
/**
* Closes the download manager and releases resources.
*
* @throws Exception If an error occurs during closing.
*/
@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
}
/**
* Writes binary data to an output stream.
*
* @property outputStream The output stream to write the data to.
* @property progressCallback The callback to report write progress.
*/
class BinaryFileWriter(
private val outputStream: OutputStream,
private val progressCallback: DownloadItemManager.InternalProgressCallback
) : AutoCloseable {
/**
* Writes data from the input stream to the output stream.
*
* @param inputStream The input stream to read the data from.
* @param length The total length of the data to be written.
* @return The total number of bytes written.
* @throws IOException If an I/O error occurs.
*/
@Throws(IOException::class)
fun write(inputStream: InputStream?, length: Long): Long {
fun write(inputStream: InputStream, length: Long): Long {
BufferedInputStream(inputStream).use { input ->
val dataBuffer = ByteArray(CHUNK_SIZE)
var readBytes: Int
var totalBytes: Long = 0
var readBytes: Int
while (input.read(dataBuffer).also { readBytes = it } != -1) {
totalBytes += readBytes.toLong()
totalBytes += readBytes
outputStream.write(dataBuffer, 0, readBytes)
progressCallback.onProgress(totalBytes, (totalBytes * 100L) / length)
}
@ -63,12 +98,17 @@ class BinaryFileWriter(outputStream: OutputStream, progressCallback: DownloadIte
}
}
/**
* Closes the writer and releases resources.
*
* @throws IOException If an error occurs during closing.
*/
@Throws(IOException::class)
override fun close() {
outputStream.close()
}
companion object {
private const val CHUNK_SIZE = 1024
private const val CHUNK_SIZE = 8192 // Increased chunk size for better performance
}
}

View file

@ -0,0 +1,124 @@
package com.audiobookshelf.app.managers
import android.content.Context
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
import android.util.Log
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
class SecureStorage(private val context: Context) {
companion object {
private const val TAG = "SecureStorage"
private const val KEYSTORE_PROVIDER = "AndroidKeyStore"
private const val KEY_ALIAS = "AudiobookshelfRefreshTokens"
private const val TRANSFORMATION = "AES/GCM/NoPadding"
private const val IV_LENGTH = 12
private const val TAG_LENGTH = 128
}
private val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply {
load(null)
}
/**
* Encrypts and stores a refresh token for a specific server connection
*/
fun storeRefreshToken(serverConnectionId: String, refreshToken: String): Boolean {
return try {
val key = getOrCreateKey()
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, key)
val encryptedBytes = cipher.doFinal(refreshToken.toByteArray(Charsets.UTF_8))
val combined = cipher.iv + encryptedBytes
val encoded = Base64.encodeToString(combined, Base64.DEFAULT)
val sharedPrefs = context.getSharedPreferences("SecureStorage", Context.MODE_PRIVATE)
sharedPrefs.edit().putString("refresh_token_$serverConnectionId", encoded).apply()
Log.d(TAG, "Successfully stored encrypted refresh token for server: $serverConnectionId")
true
} catch (e: Exception) {
Log.e(TAG, "Failed to store refresh token for server: $serverConnectionId", e)
false
}
}
/**
* Retrieves and decrypts a refresh token for a specific server connection
*/
fun getRefreshToken(serverConnectionId: String): String? {
return try {
val sharedPrefs = context.getSharedPreferences("SecureStorage", Context.MODE_PRIVATE)
val encoded = sharedPrefs.getString("refresh_token_$serverConnectionId", null) ?: return null
val combined = Base64.decode(encoded, Base64.DEFAULT)
val iv = combined.copyOfRange(0, IV_LENGTH)
val encryptedBytes = combined.copyOfRange(IV_LENGTH, combined.size)
val key = getOrCreateKey()
val cipher = Cipher.getInstance(TRANSFORMATION)
val spec = GCMParameterSpec(TAG_LENGTH, iv)
cipher.init(Cipher.DECRYPT_MODE, key, spec)
val decryptedBytes = cipher.doFinal(encryptedBytes)
String(decryptedBytes, Charsets.UTF_8)
} catch (e: Exception) {
Log.e(TAG, "Failed to retrieve refresh token for server: $serverConnectionId", e)
null
}
}
/**
* Removes a refresh token for a specific server connection
*/
fun removeRefreshToken(serverConnectionId: String): Boolean {
return try {
val sharedPrefs = context.getSharedPreferences("SecureStorage", Context.MODE_PRIVATE)
sharedPrefs.edit().remove("refresh_token_$serverConnectionId").apply()
Log.d(TAG, "Successfully removed refresh token for server: $serverConnectionId")
true
} catch (e: Exception) {
Log.e(TAG, "Failed to remove refresh token for server: $serverConnectionId", e)
false
}
}
/**
* Checks if a refresh token exists for a specific server connection
*/
fun hasRefreshToken(serverConnectionId: String): Boolean {
val sharedPrefs = context.getSharedPreferences("SecureStorage", Context.MODE_PRIVATE)
return sharedPrefs.contains("refresh_token_$serverConnectionId")
}
private fun getOrCreateKey(): SecretKey {
return if (keyStore.containsAlias(KEY_ALIAS)) {
keyStore.getKey(KEY_ALIAS, null) as SecretKey
} else {
createKey()
}
}
private fun createKey(): SecretKey {
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_PROVIDER)
val keyGenSpec = KeyGenParameterSpec.Builder(
KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(false)
.setRandomizedEncryptionRequired(true)
.build()
keyGenerator.init(keyGenSpec)
return keyGenerator.generateKey()
}
}

View file

@ -1,133 +1,221 @@
package com.audiobookshelf.app.managers
import android.content.Context
import android.media.metrics.PlaybackSession
import android.media.MediaPlayer
import android.os.*
import android.util.Log
import com.audiobookshelf.app.R
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.player.PlayerNotificationService
import com.audiobookshelf.app.player.SLEEP_TIMER_WAKE_UP_EXPIRATION
import java.text.SimpleDateFormat
import com.audiobookshelf.app.plugins.AbsLogger
import java.util.*
import kotlin.concurrent.schedule
import kotlin.math.roundToInt
class SleepTimerManager constructor(private val playerNotificationService: PlayerNotificationService) {
const val SLEEP_TIMER_CHIME_SOUND_VOLUME = 0.7f
class SleepTimerManager
constructor(private val playerNotificationService: PlayerNotificationService) {
private val tag = "SleepTimerManager"
private var sleepTimerTask:TimerTask? = null
private var sleepTimerRunning:Boolean = false
private var sleepTimerEndTime:Long = 0L
private var sleepTimerLength:Long = 0L
private var sleepTimerElapsed: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 var sleepTimerTask: TimerTask? = null
private var sleepTimerRunning: Boolean = false
private var sleepTimerEndTime: Long = 0L
private var sleepTimerLength: Long = 0L
private var sleepTimerElapsed: Long = 0L
private var sleepTimerFinishedAt: Long = 0L
private var isAutoSleepTimer: Boolean = false // When timer was auto-set
private var autoTimerDisabled: Boolean = false // Disable until out of auto timer period
private var sleepTimerSessionId: String = ""
private fun getCurrentTime():Long {
/**
* Gets the current time from the player notification service.
* @return Long - the current time in milliseconds.
*/
private fun getCurrentTime(): Long {
return playerNotificationService.getCurrentTime()
}
private fun getDuration():Long {
/**
* Gets the duration of the current playback.
* @return Long - the duration in milliseconds.
*/
private fun getDuration(): Long {
return playerNotificationService.getDuration()
}
private fun getIsPlaying():Boolean {
/**
* Checks if the player is currently playing.
* @return Boolean - true if the player is playing, false otherwise.
*/
private fun getIsPlaying(): Boolean {
return playerNotificationService.currentPlayer.isPlaying
}
private fun setVolume(volume:Float) {
/**
* Gets the playback speed of the player.
* @return Float - the playback speed.
*/
private fun getPlaybackSpeed(): Float {
return playerNotificationService.currentPlayer.playbackParameters.speed
}
/**
* Sets the volume of the player.
* @param volume Float - the volume level to set.
*/
private fun setVolume(volume: Float) {
playerNotificationService.currentPlayer.volume = volume
}
/** Pauses the player. */
private fun pause() {
playerNotificationService.currentPlayer.pause()
}
/** Plays the player. */
private fun play() {
playerNotificationService.currentPlayer.play()
}
private fun getSleepTimerTimeRemainingSeconds():Int {
/**
* Gets the remaining time of the sleep timer in seconds.
* @param speed Float - the playback speed of the player, default value is 1.
* @return Int - the remaining time in seconds.
*/
private fun getSleepTimerTimeRemainingSeconds(speed: Float = 1f): Int {
if (sleepTimerEndTime == 0L && sleepTimerLength > 0) { // For regular timer
return ((sleepTimerLength - sleepTimerElapsed) / 1000).toDouble().roundToInt()
}
// For chapter end timer
if (sleepTimerEndTime <= 0) return 0
return (((sleepTimerEndTime - getCurrentTime()) / 1000).toDouble()).roundToInt()
return (((sleepTimerEndTime - getCurrentTime()) / 1000).toDouble() / speed).roundToInt()
}
private fun setSleepTimer(time: Long, isChapterTime: Boolean) : Boolean {
Log.d(tag, "Setting Sleep Timer for $time is chapter time $isChapterTime")
/**
* Sets the sleep timer.
* @param time Long - the time to set the sleep timer for. When 0L, use end of chapter/track time.
* @return Boolean - true if the sleep timer was set successfully, false otherwise.
*/
private fun setSleepTimer(time: Long): Boolean {
Log.d(tag, "Setting Sleep Timer for $time")
sleepTimerTask?.cancel()
sleepTimerRunning = true
sleepTimerFinishedAt = 0L
sleepTimerElapsed = 0L
setVolume(1f)
// Register shake sensor
playerNotificationService.registerSensor()
if (time == 0L) {
// Get the current chapter time and set the sleep timer to the end of the chapter
val chapterEndTime = this.getChapterEndTime()
val currentTime = getCurrentTime()
if (isChapterTime) {
if (currentTime > time) {
Log.d(tag, "Invalid sleep timer - current time is already passed chapter time $time")
if (chapterEndTime == null) {
Log.e(tag, "Setting sleep timer to end of chapter/track but there is no current session")
return false
}
sleepTimerEndTime = time
sleepTimerLength = 0
val currentTime = getCurrentTime()
if (currentTime > chapterEndTime) {
Log.d(tag, "Invalid sleep timer - time is already past chapter time $chapterEndTime")
return false
}
sleepTimerEndTime = chapterEndTime
if (sleepTimerEndTime > getDuration()) {
sleepTimerEndTime = getDuration()
}
} else {
sleepTimerLength = time
sleepTimerEndTime = 0L
}
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds(), isAutoSleepTimer)
// Set sleep timer length. Will be 0L if using chapter end time
sleepTimerLength = time
sleepTimerTask = Timer("SleepTimer", false).schedule(0L, 1000L) {
Handler(Looper.getMainLooper()).post {
if (getIsPlaying()) {
sleepTimerElapsed += 1000L
// Register shake sensor
playerNotificationService.registerSensor()
val sleepTimeSecondsRemaining = getSleepTimerTimeRemainingSeconds()
Log.d(tag, "Timer Elapsed $sleepTimerElapsed | Sleep TIMER time remaining $sleepTimeSecondsRemaining s")
playerNotificationService.clientEventEmitter?.onSleepTimerSet(
getSleepTimerTimeRemainingSeconds(getPlaybackSpeed()),
isAutoSleepTimer
)
if (sleepTimeSecondsRemaining > 0) {
playerNotificationService.clientEventEmitter?.onSleepTimerSet(sleepTimeSecondsRemaining, isAutoSleepTimer)
}
sleepTimerTask =
Timer("SleepTimer", false).schedule(0L, 1000L) {
Handler(Looper.getMainLooper()).post {
if (getIsPlaying()) {
sleepTimerElapsed += 1000L
if (sleepTimeSecondsRemaining <= 0) {
Log.d(tag, "Sleep Timer Pausing Player on Chapter")
pause()
val sleepTimeSecondsRemaining =
getSleepTimerTimeRemainingSeconds(getPlaybackSpeed())
Log.d(
tag,
"Timer Elapsed $sleepTimerElapsed | Sleep TIMER time remaining $sleepTimeSecondsRemaining s"
)
playerNotificationService.clientEventEmitter?.onSleepTimerEnded(getCurrentTime())
clearSleepTimer()
sleepTimerFinishedAt = System.currentTimeMillis()
} else if (sleepTimeSecondsRemaining <= 60 && DeviceManager.deviceData.deviceSettings?.disableSleepTimerFadeOut != true) {
// Start fading out audio down to 10% volume
val percentToReduce = 1 - (sleepTimeSecondsRemaining / 60F)
val volume = 1f - (percentToReduce * 0.9f)
Log.d(tag, "SLEEP VOLUME FADE $volume | ${sleepTimeSecondsRemaining}s remaining")
setVolume(volume)
} else {
setVolume(1f)
}
}
}
}
if (sleepTimeSecondsRemaining > 0) {
playerNotificationService.clientEventEmitter?.onSleepTimerSet(
sleepTimeSecondsRemaining,
isAutoSleepTimer
)
}
if (sleepTimeSecondsRemaining == 30 && sleepTimerElapsed > 1 && DeviceManager.deviceData.deviceSettings?.enableSleepTimerAlmostDoneChime == true) {
playChimeSound()
}
if (sleepTimeSecondsRemaining <= 0) {
Log.d(tag, "Sleep Timer Pausing Player on Chapter")
pause()
playerNotificationService.clientEventEmitter?.onSleepTimerEnded(
getCurrentTime()
)
clearSleepTimer()
sleepTimerFinishedAt = System.currentTimeMillis()
} else if (sleepTimeSecondsRemaining <= 60 &&
DeviceManager.deviceData
.deviceSettings
?.disableSleepTimerFadeOut != true
) {
// Start fading out audio down to 10% volume
val percentToReduce = 1 - (sleepTimeSecondsRemaining / 60F)
val volume = 1f - (percentToReduce * 0.9f)
Log.d(
tag,
"SLEEP VOLUME FADE $volume | ${sleepTimeSecondsRemaining}s remaining"
)
setVolume(volume)
} else {
setVolume(1f)
}
}
}
}
return true
}
fun setManualSleepTimer(playbackSessionId:String, time: Long, isChapterTime:Boolean):Boolean {
/**
* Sets a manual sleep timer.
* @param playbackSessionId String - the playback session ID.
* @param time Long - the time to set the sleep timer for.
* @param isChapterTime Boolean - true if the time is for the end of a chapter, false otherwise.
* @return Boolean - true if the sleep timer was set successfully, false otherwise.
*/
fun setManualSleepTimer(playbackSessionId: String, time: Long, isChapterTime: Boolean): Boolean {
sleepTimerSessionId = playbackSessionId
isAutoSleepTimer = false
return setSleepTimer(time, isChapterTime)
if (isChapterTime) {
Log.d(tag, "Setting manual sleep timer for end of chapter")
return setSleepTimer(0L)
} else {
Log.d(tag, "Setting manual sleep timer for $time")
return setSleepTimer(time)
}
}
/** Clears the sleep timer. */
private fun clearSleepTimer() {
sleepTimerTask?.cancel()
sleepTimerTask = null
@ -138,32 +226,36 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
setVolume(1f)
}
fun getSleepTimerTime():Long {
/**
* Gets the sleep timer end time.
* @return Long - the sleep timer end time in milliseconds.
*/
fun getSleepTimerTime(): Long {
return sleepTimerEndTime
}
/** Cancels the sleep timer. */
fun cancelSleepTimer() {
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)
Log.i(tag, "Disabling auto sleep timer for this time period")
autoTimerDisabled = true
}
clearSleepTimer()
playerNotificationService.clientEventEmitter?.onSleepTimerSet(0, false)
}
// Vibrate when resetting sleep timer
/** Provides vibration feedback when resetting the sleep timer. */
private fun vibrateFeedback() {
if (DeviceManager.deviceData.deviceSettings?.disableSleepTimerResetFeedback == true) return
val context = playerNotificationService.getContext()
val vibrator:Vibrator
val vibrator: Vibrator
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager =
context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
vibrator = vibratorManager.defaultVibrator
} else {
@Suppress("DEPRECATION")
@ -172,18 +264,32 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
vibrator.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val vibrationEffect = VibrationEffect.createWaveform(longArrayOf(0, 150, 150, 150),-1)
val vibrationEffect = VibrationEffect.createWaveform(longArrayOf(0, 150, 150, 150), -1)
it.vibrate(vibrationEffect)
} else {
@Suppress("DEPRECATION")
it.vibrate(10)
@Suppress("DEPRECATION") it.vibrate(10)
}
}
}
// Get the chapter end time for use in End of Chapter timers
// if less than 2s remain in chapter then use the next chapter
private fun getChapterEndTime():Long? {
/** Plays chime sound */
private fun playChimeSound() {
AbsLogger.info(tag, "playChimeSound: Playing sleep timer chime sound")
val ctx = playerNotificationService.getContext()
val mediaPlayer = MediaPlayer.create(ctx, R.raw.bell)
mediaPlayer.setVolume(SLEEP_TIMER_CHIME_SOUND_VOLUME, SLEEP_TIMER_CHIME_SOUND_VOLUME)
mediaPlayer.start()
mediaPlayer.setOnCompletionListener {
mediaPlayer.release()
}
}
/**
* Gets the chapter end time for use in End of Chapter timers. If less than 10 seconds remain in
* the chapter, then use the next chapter.
* @return Long? - the chapter end time in milliseconds, or null if there is no current session.
*/
private fun getChapterEndTime(): Long? {
val currentChapterEndTimeMs = playerNotificationService.getEndTimeOfChapterOrTrack()
if (currentChapterEndTimeMs == null) {
Log.e(tag, "Getting chapter sleep timer end of chapter/track but there is no current session")
@ -191,11 +297,17 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
}
val timeLeftInChapter = currentChapterEndTimeMs - getCurrentTime()
return if (timeLeftInChapter < 2000L) {
Log.i(tag, "Getting chapter sleep timer time and current chapter has less than 2s remaining")
// If less than 10 seconds remain in the chapter, set the timer to the next chapter or track
// This handles the auto-rewind from not playing media for a little bit to select the next
// chapter
return if (timeLeftInChapter < 10000L) {
Log.i(tag, "Getting chapter sleep timer time and current chapter has less than 10s remaining")
val nextChapterEndTimeMs = playerNotificationService.getEndTimeOfNextChapterOrTrack()
if (nextChapterEndTimeMs == null || currentChapterEndTimeMs == nextChapterEndTimeMs) {
Log.e(tag, "Invalid next chapter time. No current session or equal to current chapter. $nextChapterEndTimeMs")
Log.e(
tag,
"Invalid next chapter time. No current session or equal to current chapter. $nextChapterEndTimeMs"
)
null
} else {
nextChapterEndTimeMs
@ -205,17 +317,35 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
}
}
private fun resetChapterTimer() {
this.getChapterEndTime()?.let { chapterEndTime ->
Log.d(tag, "Resetting stopped sleep timer to end of chapter $chapterEndTime")
vibrateFeedback()
setSleepTimer(chapterEndTime, true)
play()
/**
* Rewind auto sleep timer if setting enabled. To ensure the first rewind of the time period does
* not take place, make sure to set `isAutoSleepTimer` after calling this function.
*/
private fun tryRewindAutoSleepTimer() {
DeviceManager.deviceData.deviceSettings?.let { deviceSettings ->
if (isAutoSleepTimer && deviceSettings.autoSleepTimerAutoRewind) {
Log.i(
tag,
"Auto sleep timer auto rewind seeking back ${deviceSettings.autoSleepTimerAutoRewindTime}ms"
)
playerNotificationService.seekBackward(deviceSettings.autoSleepTimerAutoRewindTime)
}
}
}
/** Checks if the sleep timer should be reset. */
private fun checkShouldResetSleepTimer() {
if (!sleepTimerRunning) {
if (sleepTimerRunning) {
// Reset the sleep timer if it has been running for at least 3 seconds or it is an end of
// chapter/track timer
if (sleepTimerLength == 0L || sleepTimerElapsed > 3000L) {
Log.d(tag, "Resetting running sleep timer")
vibrateFeedback()
setSleepTimer(sleepTimerLength)
play()
}
} else {
if (sleepTimerFinishedAt <= 0L) return
val finishedAtDistance = System.currentTimeMillis() - sleepTimerFinishedAt
@ -226,51 +356,31 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
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
}
// If timer was cleared by going negative on time, clear the sleep timer length so pressing
// play allows playback to continue without the sleep timer continuously setting for 1 second.
if (sleepTimerLength == 1000L) {
Log.d(tag, "Sleep timer cleared by manually subtracting time, clearing sleep timer")
sleepTimerFinishedAt = 0L
return
}
// Automatically rewind in the book if settings are enabled
tryRewindAutoSleepTimer()
// Set sleep timer
// When sleepTimerLength is 0 then use end of chapter/track time
if (sleepTimerLength == 0L) {
Log.d(tag, "Resetting stopped chapter sleep timer")
resetChapterTimer()
} else {
Log.d(tag, "Resetting stopped sleep timer to length $sleepTimerLength")
vibrateFeedback()
setSleepTimer(sleepTimerLength, false)
play()
}
return
}
// Does not apply to chapter sleep timers and timer must be running for at least 3 seconds
if (sleepTimerLength > 0L && sleepTimerElapsed > 3000L) {
Log.d(tag, "Resetting running sleep timer to length $sleepTimerLength")
Log.d(tag, "Resetting stopped sleep timer")
vibrateFeedback()
setSleepTimer(sleepTimerLength, false)
} else if (sleepTimerLength == 0L) {
// When navigating to previous chapters make sure this is still the end of the current chapter
this.getChapterEndTime()?.let { chapterEndTime ->
if (chapterEndTime != sleepTimerEndTime) {
Log.d(tag, "Resetting chapter sleep timer to end of chapter $chapterEndTime from $sleepTimerEndTime")
vibrateFeedback()
setSleepTimer(chapterEndTime, true)
play()
}
}
setSleepTimer(sleepTimerLength)
play()
}
}
/**
* Handles the shake event to reset the sleep timer. Shaking to reset only works during the 2
* minute grace period after the timer ends or while media is playing.
*/
fun handleShake() {
if (sleepTimerRunning || sleepTimerFinishedAt > 0L) {
if ((sleepTimerRunning && getIsPlaying()) || sleepTimerFinishedAt > 0L) {
if (DeviceManager.deviceData.deviceSettings?.disableShakeToResetSleepTimer == true) {
Log.d(tag, "Shake to reset sleep timer is disabled")
return
@ -279,48 +389,63 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
}
}
/**
* Increases the sleep timer time.
* @param time Long - the time to increase the sleep timer by.
*/
fun increaseSleepTime(time: Long) {
Log.d(tag, "Increase Sleep time $time")
if (!sleepTimerRunning) return
// Increase the sleep timer time (if using fixed length) or end time (if using chapter end time)
// and ensure it doesn't go over the duration of the current playback item
if (sleepTimerEndTime == 0L) {
// Fixed length
sleepTimerLength += time
if (sleepTimerLength + getCurrentTime() > getDuration()) sleepTimerLength = getDuration() - getCurrentTime()
sleepTimerLength = minOf(sleepTimerLength, getDuration() - getCurrentTime())
} else {
val newSleepEndTime = sleepTimerEndTime + time
sleepTimerEndTime = if (newSleepEndTime >= getDuration()) {
getDuration()
} else {
newSleepEndTime
}
// Chapter end time
sleepTimerEndTime =
minOf(sleepTimerEndTime + (time * getPlaybackSpeed()).roundToInt(), getDuration())
}
setVolume(1F)
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds(), isAutoSleepTimer)
playerNotificationService.clientEventEmitter?.onSleepTimerSet(
getSleepTimerTimeRemainingSeconds(getPlaybackSpeed()),
isAutoSleepTimer
)
}
/**
* Decreases the sleep timer time.
* @param time Long - the time to decrease the sleep timer by.
*/
fun decreaseSleepTime(time: Long) {
Log.d(tag, "Decrease Sleep time $time")
if (!sleepTimerRunning) return
// Decrease the sleep timer time (if using fixed length) or end time (if using chapter end time)
// and ensure it doesn't go below 1 second
if (sleepTimerEndTime == 0L) {
sleepTimerLength -= time
if (sleepTimerLength <= 0) sleepTimerLength = 1000L
// Fixed length
sleepTimerLength = maxOf(sleepTimerLength - time, 1000L)
} else {
val newSleepEndTime = sleepTimerEndTime - time
sleepTimerEndTime = if (newSleepEndTime <= 1000) {
// End sleep timer in 1 second
getCurrentTime() + 1000
} else {
newSleepEndTime
}
// Chapter end time
sleepTimerEndTime =
maxOf(
sleepTimerEndTime - (time * getPlaybackSpeed()).roundToInt(),
getCurrentTime() + 1000
)
}
setVolume(1F)
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds(), isAutoSleepTimer)
playerNotificationService.clientEventEmitter?.onSleepTimerSet(
getSleepTimerTimeRemainingSeconds(getPlaybackSpeed()),
isAutoSleepTimer
)
}
/** Checks whether the auto sleep timer should be set, and set up auto sleep timer if so. */
fun checkAutoSleepTimer() {
if (sleepTimerRunning) { // Sleep timer already running
return
@ -337,10 +462,13 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
val currentCalendar = Calendar.getInstance()
// In cases where end time is before start time then we shift the time window forward or backward based on the current time.
// 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 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 (currentCalendar.before(startCalendar)) { // Shift start back a day
startCalendar.add(Calendar.DAY_OF_MONTH, -1)
@ -349,47 +477,52 @@ class SleepTimerManager constructor(private val playerNotificationService: Playe
}
}
val currentHour = SimpleDateFormat("HH:mm", Locale.getDefault()).format(currentCalendar.time)
if (currentCalendar.after(startCalendar) && currentCalendar.before(endCalendar)) {
Log.i(tag, "Current hour $currentHour is between ${deviceSettings.autoSleepTimerStartTime} and ${deviceSettings.autoSleepTimerEndTime} - starting sleep timer")
val isDuringAutoTime =
currentCalendar.after(startCalendar) && currentCalendar.before(endCalendar)
// 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
// When sleepTimerLength is 0 then use end of chapter/track time
if (deviceSettings.sleepTimerLength == 0L) {
val chapterEndTimeMs = this.getChapterEndTime()
if (chapterEndTimeMs == null) {
Log.e(tag, "Setting auto sleep timer to end of chapter/track but there is no current session")
} else {
isAutoSleepTimer = true
setSleepTimer(chapterEndTimeMs, true)
}
// Determine whether to set the auto sleep timer or not
if (autoTimerDisabled) {
if (!isDuringAutoTime) {
// Check if sleep timer was disabled during the previous period and enable again
Log.i(tag, "Leaving disabled auto sleep time period, enabling for next time period")
autoTimerDisabled = false
} else {
isAutoSleepTimer = true
setSleepTimer(deviceSettings.sleepTimerLength, false)
// Auto time is disabled, do not set sleep timer
Log.i(tag, "Auto sleep timer is disabled for this time period")
}
} else {
isFirstAutoSleepTimer = true
Log.d(tag, "Current hour $currentHour is NOT between ${deviceSettings.autoSleepTimerStartTime} and ${deviceSettings.autoSleepTimerEndTime}")
if (isDuringAutoTime) {
// Start an auto sleep timer
val currentHour = currentCalendar.get(Calendar.HOUR_OF_DAY)
val currentMin = currentCalendar.get(Calendar.MINUTE)
Log.i(tag, "Starting auto sleep timer at $currentHour:$currentMin")
// Automatically rewind in the book if settings is enabled
tryRewindAutoSleepTimer()
// Set `isAutoSleepTimer` to true to indicate that the timer was set automatically
// and to not cause the timer to rewind
isAutoSleepTimer = true
setSleepTimer(deviceSettings.sleepTimerLength)
} else {
Log.d(tag, "Not in auto sleep time period")
}
}
}
}
fun handleMediaPlayEvent(playbackSessionId:String) {
/**
* Handles the media play event and checks if the sleep timer should be reset or set.
* @param playbackSessionId String - the playback session ID.
*/
fun handleMediaPlayEvent(playbackSessionId: String) {
// 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
// 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
}
} else {}
sleepTimerSessionId = playbackSessionId
checkAutoSleepTimer()

View file

@ -36,76 +36,97 @@ object MediaEventManager {
}
fun seekEvent(playbackSession: PlaybackSession, syncResult: SyncResult?) {
Log.i(tag, "Seek Event for media \"${playbackSession.displayTitle}\", currentTime=${playbackSession.currentTime}")
Log.i(
tag,
"Seek Event for media \"${playbackSession.displayTitle}\", currentTime=${playbackSession.currentTime}"
)
addPlaybackEvent("Seek", playbackSession, syncResult)
}
fun syncEvent(mediaProgress: MediaProgressWrapper, description: String) {
Log.i(tag, "Sync Event for media item id \"${mediaProgress.mediaItemId}\", currentTime=${mediaProgress.currentTime}")
Log.i(
tag,
"Sync Event for media item id \"${mediaProgress.mediaItemId}\", currentTime=${mediaProgress.currentTime}"
)
addSyncEvent("Sync", mediaProgress, description)
}
private fun addSyncEvent(eventName:String, mediaProgress:MediaProgressWrapper, description: String) {
private fun addSyncEvent(
eventName: String,
mediaProgress: MediaProgressWrapper,
description: String
) {
val mediaItemHistory = getMediaItemHistoryMediaItem(mediaProgress.mediaItemId)
if (mediaItemHistory == null) {
Log.w(tag, "addSyncEvent: Media Item History not created yet for media item id ${mediaProgress.mediaItemId}")
Log.w(
tag,
"addSyncEvent: Media Item History not created yet for media item id ${mediaProgress.mediaItemId}"
)
return
}
val mediaItemEvent = MediaItemEvent(
name = eventName,
type = "Sync",
description = description,
currentTime = mediaProgress.currentTime,
serverSyncAttempted = false,
serverSyncSuccess = null,
serverSyncMessage = null,
timestamp = System.currentTimeMillis()
)
val mediaItemEvent =
MediaItemEvent(
name = eventName,
type = "Sync",
description = description,
currentTime = mediaProgress.currentTime,
serverSyncAttempted = false,
serverSyncSuccess = null,
serverSyncMessage = null,
timestamp = System.currentTimeMillis()
)
mediaItemHistory.events.add(mediaItemEvent)
DeviceManager.dbManager.saveMediaItemHistory(mediaItemHistory)
clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory)
}
private fun addPlaybackEvent(eventName:String, playbackSession:PlaybackSession, syncResult: SyncResult?) {
val mediaItemHistory = getMediaItemHistoryMediaItem(playbackSession.mediaItemId) ?: createMediaItemHistoryForSession(playbackSession)
private fun addPlaybackEvent(
eventName: String,
playbackSession: PlaybackSession,
syncResult: SyncResult?
) {
val mediaItemHistory =
getMediaItemHistoryMediaItem(playbackSession.mediaItemId)
?: createMediaItemHistoryForSession(playbackSession)
val mediaItemEvent = MediaItemEvent(
name = eventName,
type = "Playback",
description = "",
currentTime = playbackSession.currentTime,
serverSyncAttempted = syncResult?.serverSyncAttempted ?: false,
serverSyncSuccess = syncResult?.serverSyncSuccess,
serverSyncMessage = syncResult?.serverSyncMessage,
timestamp = System.currentTimeMillis()
)
val mediaItemEvent =
MediaItemEvent(
name = eventName,
type = "Playback",
description = "",
currentTime = playbackSession.currentTime,
serverSyncAttempted = syncResult?.serverSyncAttempted ?: false,
serverSyncSuccess = syncResult?.serverSyncSuccess,
serverSyncMessage = syncResult?.serverSyncMessage,
timestamp = System.currentTimeMillis()
)
mediaItemHistory.events.add(mediaItemEvent)
DeviceManager.dbManager.saveMediaItemHistory(mediaItemHistory)
clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory)
}
private fun getMediaItemHistoryMediaItem(mediaItemId: String) : MediaItemHistory? {
private fun getMediaItemHistoryMediaItem(mediaItemId: String): MediaItemHistory? {
return DeviceManager.dbManager.getMediaItemHistory(mediaItemId)
}
private fun createMediaItemHistoryForSession(playbackSession: PlaybackSession):MediaItemHistory {
private fun createMediaItemHistoryForSession(playbackSession: PlaybackSession): MediaItemHistory {
Log.i(tag, "Creating new media item history for media \"${playbackSession.displayTitle}\"")
val isLocalOnly = playbackSession.isLocalLibraryItemOnly
val libraryItemId = if (isLocalOnly) playbackSession.localLibraryItemId else playbackSession.libraryItemId ?: ""
val episodeId:String? = if (isLocalOnly && playbackSession.localEpisodeId != null) playbackSession.localEpisodeId else playbackSession.episodeId
val libraryItemId = playbackSession.libraryItemId ?: ""
val episodeId: String? = playbackSession.episodeId
return MediaItemHistory(
id = playbackSession.mediaItemId,
mediaDisplayTitle = playbackSession.displayTitle ?: "Unset",
libraryItemId,
episodeId,
isLocalOnly,
playbackSession.serverConnectionConfigId,
playbackSession.serverAddress,
playbackSession.userId,
createdAt = System.currentTimeMillis(),
events = mutableListOf())
id = playbackSession.mediaItemId,
mediaDisplayTitle = playbackSession.displayTitle ?: "Unset",
libraryItemId,
episodeId,
false, // local-only items are not supported
playbackSession.serverConnectionConfigId,
playbackSession.serverAddress,
playbackSession.userId,
createdAt = System.currentTimeMillis(),
events = mutableListOf()
)
}
}

View file

@ -15,18 +15,29 @@ import org.json.JSONException
import org.json.JSONObject
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import java.util.concurrent.atomic.AtomicInteger
class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
val tag = "MediaManager"
private var serverLibraryItems = mutableListOf<LibraryItem>() // Store all items here
private var selectedLibraryItems = mutableListOf<LibraryItem>()
private var selectedLibraryId = ""
private var cachedLibraryAuthors : MutableMap<String, MutableMap<String, LibraryAuthorItem>> = hashMapOf()
private var cachedLibraryAuthorItems : MutableMap<String, MutableMap<String, List<LibraryItem>>> = hashMapOf()
private var cachedLibraryAuthorSeriesItems : MutableMap<String, MutableMap<String, List<LibraryItem>>> = hashMapOf()
private var cachedLibrarySeries : MutableMap<String, List<LibrarySeriesItem>> = hashMapOf()
private var cachedLibrarySeriesItem : MutableMap<String, MutableMap<String, List<LibraryItem>>> = hashMapOf()
private var cachedLibraryCollections : MutableMap<String, MutableMap<String, LibraryCollection>> = hashMapOf()
private var cachedLibraryRecentShelves : MutableMap<String, MutableList<LibraryShelfType>> = hashMapOf()
private var cachedLibraryDiscovery : MutableMap<String, MutableList<LibraryItem>> = hashMapOf()
private var cachedLibraryPodcasts : MutableMap<String, MutableMap<String, LibraryItem>> = hashMapOf()
private var isLibraryPodcastsCached : MutableMap<String, Boolean> = hashMapOf()
var allLibraryPersonalizationsDone : Boolean = false
private var libraryPersonalizationsDone : Int = 0
private var selectedPodcast:Podcast? = null
private var selectedLibraryItemId:String? = null
private var podcastEpisodeLibraryItemMap = mutableMapOf<String, LibraryItemWithEpisode>()
private var serverLibraryCategories = listOf<LibraryCategory>()
private var serverConfigIdUsed:String? = null
private var serverConfigLastPing:Long = 0L
var serverUserMediaProgress:MutableList<MediaProgress> = mutableListOf()
@ -39,6 +50,35 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
return serverLibraries.find { it.id == id } != null
}
/**
* Check if there is discovery shelf for [libraryId]
* If personalized shelves are not yet populated for library then populate
*
*/
fun getHasDiscovery(libraryId: String) : Boolean {
if (cachedLibraryDiscovery.containsKey(libraryId)) {
if (cachedLibraryDiscovery[libraryId]!!.isNotEmpty()) {
return true
}
} else {
populatePersonalizedDataForLibrary(libraryId){}
}
return false
}
fun getLibrary(id:String) : Library? {
return serverLibraries.find { it.id == id }
}
/**
* Add [libraryItem] to [serverLibraryItems] if it is not already added
*/
private fun addServerLibrary(libraryItem: LibraryItem) {
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
}
fun getSavedPlaybackRate():Float {
if (userSettingsPlaybackRate != null) {
return userSettingsPlaybackRate ?: 1f
@ -91,19 +131,31 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
}
}
fun checkResetServerItems() {
fun checkResetServerItems():Boolean {
// When opening android auto need to check if still connected to server
// and reset any server data already set
val serverConnConfig = if (DeviceManager.isConnectedToServer) DeviceManager.serverConnectionConfig else DeviceManager.deviceData.getLastServerConnectionConfig()
if (!DeviceManager.isConnectedToServer || !DeviceManager.checkConnectivity(ctx) || serverConnConfig == null || serverConnConfig.id !== serverConfigIdUsed) {
podcastEpisodeLibraryItemMap = mutableMapOf()
serverLibraryCategories = listOf()
serverLibraries = listOf()
serverLibraryItems = mutableListOf()
selectedLibraryItems = mutableListOf()
selectedLibraryId = ""
cachedLibraryAuthors = hashMapOf()
cachedLibraryAuthorItems = hashMapOf()
cachedLibraryAuthorSeriesItems = hashMapOf()
cachedLibrarySeries = hashMapOf()
cachedLibrarySeriesItem = hashMapOf()
cachedLibraryCollections = hashMapOf()
cachedLibraryRecentShelves = hashMapOf()
cachedLibraryDiscovery = hashMapOf()
cachedLibraryPodcasts = hashMapOf()
isLibraryPodcastsCached = hashMapOf()
serverItemsInProgress = listOf()
allLibraryPersonalizationsDone = false
libraryPersonalizationsDone = 0
return true
}
return false
}
private fun loadItemsInProgressForAllLibraries(cb: (List<ItemInProgress>) -> Unit) {
@ -120,28 +172,457 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
}
}
fun loadLibraryItemsWithAudio(libraryId:String, cb: (List<LibraryItem>) -> Unit) {
if (selectedLibraryItems.isNotEmpty() && selectedLibraryId == libraryId) {
cb(selectedLibraryItems)
/**
* Load personalized shelves from server for all libraries.
* [cb] resolves when all libraries are processed
*/
fun populatePersonalizedDataForAllLibraries(cb: () -> Unit) {
val remaining = AtomicInteger(serverLibraries.size)
serverLibraries.forEach { lib ->
Log.d(tag, "Loading personalization for library ${lib.name}")
populatePersonalizedDataForLibrary(lib.id) {
Log.d(tag, "Loaded personalization for library ${lib.name}")
if (remaining.decrementAndGet() == 0) {
Log.d(tag, "Finished loading all library personalization data")
allLibraryPersonalizationsDone = true
cb()
}
}
}
}
/**
* Get personalized shelves from server for selected [libraryId].
* Populates [cachedLibraryRecentShelves] and [cachedLibraryDiscovery].
*/
private fun populatePersonalizedDataForLibrary(libraryId: String, cb: () -> Unit) {
apiHandler.getLibraryPersonalized(libraryId) { shelves ->
Log.d(tag, "populatePersonalizedDataForLibrary $libraryId")
if (shelves === null) return@getLibraryPersonalized
shelves.map { shelf ->
Log.d(tag, "$shelf")
if (shelf.type == "book") {
if (shelf.id == "continue-listening") return@map
else if (shelf.id == "listen-again") return@map
else if (shelf.id == "recently-added") {
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
cachedLibraryRecentShelves[libraryId] = mutableListOf()
}
if (cachedLibraryRecentShelves[libraryId]?.find { it.id == shelf.id } == null) {
cachedLibraryRecentShelves[libraryId]!!.add(shelf)
}
}
else if (shelf.id == "discover") {
if (!cachedLibraryDiscovery.containsKey(libraryId)) {
cachedLibraryDiscovery[libraryId] = mutableListOf()
}
(shelf as LibraryShelfBookEntity).entities?.map {
cachedLibraryDiscovery[libraryId]!!.add(it)
}
}
else if (shelf.id == "continue-reading") return@map
else if (shelf.id == "continue-series") return@map
shelf as LibraryShelfBookEntity
} else if (shelf.type == "series") {
if (shelf.id == "recent-series") {
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
cachedLibraryRecentShelves[libraryId] = mutableListOf()
}
if (cachedLibraryRecentShelves[libraryId]?.find { it.id == shelf.id } == null) {
cachedLibraryRecentShelves[libraryId]!!.add(shelf)
}
}
} else if (shelf.type == "episode") {
if (shelf.id == "continue-listening") return@map
else if (shelf.id == "listen-again") return@map
else if (shelf.id == "newest-episodes") {
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
cachedLibraryRecentShelves[libraryId] = mutableListOf()
}
if (cachedLibraryRecentShelves[libraryId]?.find { it.id == shelf.id } == null) {
cachedLibraryRecentShelves[libraryId]!!.add(shelf)
}
val podcastLibraryItemIds = mutableListOf<String>()
(shelf as LibraryShelfEpisodeEntity).entities?.forEach { libraryItem ->
if (!podcastLibraryItemIds.contains(libraryItem.id)) {
podcastLibraryItemIds.add(libraryItem.id)
loadPodcastItem(libraryItem.libraryId, libraryItem.id) {}
}
}
}
} else if (shelf.type == "podcast") {
if (shelf.id == "recently-added"){
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
cachedLibraryRecentShelves[libraryId] = mutableListOf()
}
if (cachedLibraryRecentShelves[libraryId]?.find { it.id == shelf.id } == null) {
cachedLibraryRecentShelves[libraryId]!!.add(shelf)
}
}
else if (shelf.id == "discover"){
return@map
}
} else if (shelf.type =="authors") {
if (shelf.id == "newest-authors") {
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
cachedLibraryRecentShelves[libraryId] = mutableListOf()
}
if (cachedLibraryRecentShelves[libraryId]?.find { it.id == shelf.id } == null) {
cachedLibraryRecentShelves[libraryId]!!.add(shelf)
}
}
}
}
Log.d(tag, "populatePersonalizedDataForLibrary $libraryId DONE")
cb()
}
}
/**
* Returns podcasts for selected library.
* If data is not found from local cache it is loaded from server
*/
fun loadLibraryPodcasts(libraryId:String, cb: (List<LibraryItem>?) -> Unit) {
// Without this there is possibility that only recent podcasts get loaded
// Loading recent podcasts will also create cachedLibraryPodcasts entry for library
if (!isLibraryPodcastsCached.containsKey(libraryId)) {
isLibraryPodcastsCached[libraryId] = false
}
// Ensure that there is map for library
if (!cachedLibraryPodcasts.containsKey(libraryId)) {
cachedLibraryPodcasts[libraryId] = mutableMapOf()
}
if (isLibraryPodcastsCached.getOrElse(libraryId) {false}) {
Log.d(tag, "loadLibraryPodcasts: Found from cache: $libraryId")
cb(cachedLibraryPodcasts[libraryId]?.values?.sortedBy { libraryItem -> (libraryItem.media as Podcast).metadata.title })
} else {
apiHandler.getLibraryItems(libraryId) { libraryItems ->
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
if (libraryItemsWithAudio.isNotEmpty()) {
selectedLibraryId = libraryId
}
selectedLibraryItems = mutableListOf()
libraryItemsWithAudio.forEach { libraryItem ->
selectedLibraryItems.add(libraryItem)
cachedLibraryPodcasts[libraryId]?.set(libraryItem.id, libraryItem)
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
}
isLibraryPodcastsCached[libraryId] = true
Log.d(tag, "loadLibraryPodcasts: loaded from server: $libraryId")
cb(libraryItemsWithAudio.sortedBy { libraryItem -> (libraryItem.media as Podcast).metadata.title })
}
}
}
/**
* Returns series with audio books from selected library.
* If data is not found from local cache then it will be fetched from server
*/
fun loadLibrarySeriesWithAudio(libraryId:String, cb: (List<LibrarySeriesItem>) -> Unit) {
// Check "cache" first
if (cachedLibrarySeries.containsKey(libraryId)) {
Log.d(tag, "Series with audio found from cache | Library $libraryId ")
cb(cachedLibrarySeries[libraryId] as List<LibrarySeriesItem>)
} else {
apiHandler.getLibrarySeries(libraryId) { seriesItems ->
Log.d(tag, "Series with audio loaded from server | Library $libraryId")
val seriesItemsWithAudio = seriesItems.filter { si -> si.audiobookCount > 0 }
cachedLibrarySeries[libraryId] = seriesItemsWithAudio
cb(seriesItemsWithAudio)
}
}
}
/**
* Returns series with audiobooks from selected library using filter for paging.
* If data is not found from local cache then it will be fetched from server
*/
fun loadLibrarySeriesWithAudio(libraryId:String, seriesFilter:String, cb: (List<LibrarySeriesItem>) -> Unit) {
// Check "cache" first
if (!cachedLibrarySeries.containsKey(libraryId)) {
loadLibrarySeriesWithAudio(libraryId) {}
} else {
Log.d(tag, "Series with audio found from cache | Library $libraryId ")
}
val seriesWithBooks = cachedLibrarySeries[libraryId]!!.filter { ls -> ls.title.uppercase().startsWith(seriesFilter) }.toList()
cb(seriesWithBooks)
}
/**
* Sorts books in series. Assumes that sequence is main.minor
*/
private fun sortSeriesBooks(seriesBooks: List<LibraryItem>) : List<LibraryItem> {
val sortingLogic = compareBy<LibraryItem> { it.seriesSequenceParts[0].length }
.thenBy { it.seriesSequenceParts[0].ifEmpty { "" } }
.thenBy { it.seriesSequenceParts.getOrElse(1) { "" }.length }
.thenBy { it.seriesSequenceParts.getOrElse(1) { "" } }
return seriesBooks.sortedWith(sortingLogic)
}
/**
* Returns books for series from library.
* If data is not found from local cache then it will be fetched from server
*/
fun loadLibrarySeriesItemsWithAudio(libraryId:String, seriesId:String, cb: (List<LibraryItem>) -> Unit) {
// Check "cache" first
if (!cachedLibrarySeriesItem.containsKey(libraryId)) {
cachedLibrarySeriesItem[libraryId] = hashMapOf()
}
if (cachedLibrarySeriesItem[libraryId]!!.containsKey(seriesId)) {
Log.d(tag, "Items for series $seriesId found from cache | Library $libraryId")
cachedLibrarySeriesItem[libraryId]!![seriesId]?.let { cb(it) }
} else {
apiHandler.getLibrarySeriesItems(libraryId, seriesId) { libraryItems ->
Log.d(tag, "Items for series $seriesId loaded from server | Library $libraryId")
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
val sortedLibraryItemsWithAudio = sortSeriesBooks(libraryItemsWithAudio)
cachedLibrarySeriesItem[libraryId]!![seriesId] = sortedLibraryItemsWithAudio
sortedLibraryItemsWithAudio.forEach { libraryItem ->
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
}
cb(sortedLibraryItemsWithAudio)
}
}
}
/**
* Returns authors with books from library.
* If data is not found from local cache then it will be fetched from server
*/
fun loadAuthorsWithBooks(libraryId:String, cb: (List<LibraryAuthorItem>) -> Unit) {
// Check "cache" first
if (cachedLibraryAuthors.containsKey(libraryId)) {
Log.d(tag, "Authors with books found from cache | Library $libraryId ")
cb(cachedLibraryAuthors[libraryId]!!.values.toList())
} else {
// Fetch data from server and add it to local "cache"
apiHandler.getLibraryAuthors(libraryId) { authorItems ->
Log.d(tag, "Authors with books loaded from server | Library $libraryId ")
// TO-DO: This check won't ensure that there is audiobooks. Current API won't offer ability to do so
var authorItemsWithBooks = authorItems.filter { li -> li.bookCount != null && li.bookCount!! > 0 }
authorItemsWithBooks = authorItemsWithBooks.sortedBy { it.name }
// Ensure that there is map for library
cachedLibraryAuthors[libraryId] = mutableMapOf()
// Cache authors
authorItemsWithBooks.forEach {
if (!cachedLibraryAuthors[libraryId]!!.containsKey(it.id)) {
cachedLibraryAuthors[libraryId]!![it.id] = it
}
}
cb(authorItemsWithBooks)
}
}
}
/**
* Returns authors with books from selected library using filter for paging.
* If data is not found from local cache then it will be fetched from server
*/
fun loadAuthorsWithBooks(libraryId:String, authorFilter: String, cb: (List<LibraryAuthorItem>) -> Unit) {
// Check "cache" first
if (cachedLibraryAuthors.containsKey(libraryId)) {
Log.d(tag, "Authors with books found from cache | Library $libraryId ")
} else {
loadAuthorsWithBooks(libraryId) {}
}
val authorsWithBooks = cachedLibraryAuthors[libraryId]!!.values.filter { lai -> lai.name.uppercase().startsWith(authorFilter) }.toList()
cb(authorsWithBooks)
}
/**
* Returns audiobooks for author from library
* If data is not found from local cache then it will be fetched from server
*/
fun loadAuthorBooksWithAudio(libraryId:String, authorId:String, cb: (List<LibraryItem>) -> Unit) {
// Ensure that there is map for library
if (!cachedLibraryAuthorItems.containsKey(libraryId)) {
cachedLibraryAuthorItems[libraryId] = mutableMapOf()
}
// Check "cache" first
if (cachedLibraryAuthorItems[libraryId]!!.containsKey(authorId)) {
Log.d(tag, "Items for author $authorId found from cache | Library $libraryId")
cachedLibraryAuthorItems[libraryId]!![authorId]?.let { cb(it) }
} else {
apiHandler.getLibraryItemsFromAuthor(libraryId, authorId) { libraryItems ->
Log.d(tag, "Items for author $authorId loaded from server | Library $libraryId")
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
cachedLibraryAuthorItems[libraryId]!![authorId] = libraryItemsWithAudio
libraryItemsWithAudio.forEach { libraryItem ->
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
}
cb(libraryItemsWithAudio)
}
}
}
/**
* Returns audiobooks for author from specified series within library
* If data is not found from local cache then it will be fetched from server
*/
fun loadAuthorSeriesBooksWithAudio(libraryId:String, authorId:String, seriesId: String, cb: (List<LibraryItem>) -> Unit) {
val authorSeriesKey = "$authorId|$seriesId"
// Ensure that there is map for library
if (!cachedLibraryAuthorSeriesItems.containsKey(libraryId)) {
cachedLibraryAuthorSeriesItems[libraryId] = mutableMapOf()
}
// Check "cache" first
if (cachedLibraryAuthorSeriesItems[libraryId]!!.containsKey(authorSeriesKey)) {
Log.d(tag, "Items for series $seriesId with author $authorId found from cache | Library $libraryId")
cachedLibraryAuthorSeriesItems[libraryId]!![authorSeriesKey]?.let { cb(it) }
} else {
apiHandler.getLibrarySeriesItems(libraryId, seriesId) { libraryItems ->
Log.d(tag, "Items for series $seriesId with author $authorId loaded from server | Library $libraryId")
val libraryItemsWithAudio = libraryItems.filter { li -> li.checkHasTracks() }
if (!cachedLibraryAuthors[libraryId]!!.containsKey(authorId)) {
Log.d(tag, "Author data is missing")
}
val authorName = cachedLibraryAuthors[libraryId]!![authorId]?.name ?: ""
Log.d(tag, "Using author name: $authorName")
val libraryItemsFromAuthorWithAudio = libraryItemsWithAudio.filter { li -> li.authorName.indexOf(authorName, ignoreCase = true) >= 0 }
val sortedLibraryItemsWithAudio = sortSeriesBooks(libraryItemsFromAuthorWithAudio)
cachedLibraryAuthorSeriesItems[libraryId]!![authorId] = sortedLibraryItemsWithAudio
sortedLibraryItemsWithAudio.forEach { libraryItem ->
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
}
cb(sortedLibraryItemsWithAudio)
}
}
}
/**
* Returns collections with audiobooks from library
* If data is not found from local cache then it will be fetched from server
*/
fun loadLibraryCollectionsWithAudio(libraryId:String, cb: (List<LibraryCollection>) -> Unit) {
if (cachedLibraryCollections.containsKey(libraryId)) {
Log.d(tag, "Collections with books found from cache | Library $libraryId ")
cb(cachedLibraryCollections[libraryId]!!.values.toList())
} else {
apiHandler.getLibraryCollections(libraryId) { libraryCollections ->
Log.d(tag, "Collections with books loaded from server | Library $libraryId ")
val libraryCollectionsWithAudio = libraryCollections.filter { lc -> lc.audiobookCount > 0 }
// Cache collections
cachedLibraryCollections[libraryId] = hashMapOf()
libraryCollectionsWithAudio.forEach {
if (!cachedLibraryCollections[libraryId]!!.containsKey(it.id)) {
cachedLibraryCollections[libraryId]!![it.id] = it
}
}
cb(libraryCollectionsWithAudio)
}
}
}
/**
* Returns audiobooks for collection from library
* If data is not found from local cache then it will be fetched from server
*/
fun loadLibraryCollectionBooksWithAudio(libraryId: String, collectionId: String, cb: (List<LibraryItem>) -> Unit) {
if (!cachedLibraryCollections.containsKey(libraryId)) {
loadLibraryCollectionsWithAudio(libraryId) {}
}
Log.d(tag, "Trying to find collection $collectionId items from from cache | Library $libraryId ")
if ( cachedLibraryCollections[libraryId]!!.containsKey(collectionId)) {
val libraryCollectionBookswithAudio = cachedLibraryCollections[libraryId]!![collectionId]?.books
libraryCollectionBookswithAudio?.forEach { libraryItem ->
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
}
cb(libraryCollectionBookswithAudio as List<LibraryItem>)
}
}
/**
* Returns audiobooks from discovery shelf for [libraryId]
* If data is not found from local cache then it will be fetched from server
*/
fun loadLibraryDiscoveryBooksWithAudio(libraryId: String, cb: (List<LibraryItem>) -> Unit) {
if (!cachedLibraryDiscovery.containsKey(libraryId)) {
cb(listOf())
}
val libraryItemsWithAudio = cachedLibraryDiscovery[libraryId]?.filter { li -> li.checkHasTracks() }
libraryItemsWithAudio?.forEach { libraryItem -> addServerLibrary(libraryItem) }
cb(libraryItemsWithAudio as List<LibraryItem>)
}
/**
* Returns recent shelves for [libraryId]
* If data is not shelves are found returns empty list
*/
fun getLibraryRecentShelfs(libraryId: String, cb: (List<LibraryShelfType>) -> Unit) {
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
Log.d(tag, "getLibraryRecentShelfs: No shelves $libraryId")
cb(listOf())
return
}
cb(cachedLibraryRecentShelves[libraryId] as List<LibraryShelfType>)
}
/**
* Returns recent shelf by [type] for [libraryId]
* If shelf is not found returns null
*/
fun getLibraryRecentShelfByType(libraryId: String, type:String, cb: (LibraryShelfType?) -> Unit) {
Log.d(tag, "getLibraryRecentShelfByType: $libraryId | $type")
if (!cachedLibraryRecentShelves.containsKey(libraryId)) {
cb(null)
return
}
for (shelf in cachedLibraryRecentShelves[libraryId]!!) {
if (shelf.type == type.lowercase()) {
cb(shelf)
return
}
}
cb(null)
}
/**
* Loads podcasts for newest episodes shelf
*/
private fun loadPodcastItem(libraryId: String, libraryItemId: String, cb: (LibraryItem?) -> Unit) {
// Ensure that there is map for library
if (!cachedLibraryPodcasts.containsKey(libraryId)) {
cachedLibraryPodcasts[libraryId] = mutableMapOf()
}
if (cachedLibraryPodcasts[libraryId]!!.containsKey(libraryItemId)) {
Log.d(tag, "loadPodcastItem: Podcast found from cache | Library $libraryItemId ")
cb(cachedLibraryPodcasts[libraryId]?.get(libraryItemId))
} else {
Log.d(tag, "loadPodcastItem: Calling getLibraryItem $libraryItemId")
apiHandler.getLibraryItem(libraryItemId) { libraryItem ->
if (libraryItem !== null) {
Log.d(tag, "loadPodcastItem: Got library item ${libraryItem.id} ${libraryItem.media.metadata.title}")
val podcast = libraryItem.media as Podcast
podcast.episodes?.forEach { podcastEpisode ->
podcastEpisodeLibraryItemMap[podcastEpisode.id] = LibraryItemWithEpisode(libraryItem, podcastEpisode)
}
cachedLibraryPodcasts[libraryId]?.set(libraryItemId, libraryItem)
cb(libraryItem)
}
}
}
}
private fun loadLibraryItem(libraryItemId:String, cb: (LibraryItemWrapper?) -> Unit) {
if (libraryItemId.startsWith("local")) {
cb(DeviceManager.dbManager.getLocalLibraryItem(libraryItemId))
@ -187,8 +668,8 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
}
selectedLibraryItemId = libraryItemWrapper.id
selectedPodcast = podcast
val children = podcast.episodes?.map { podcastEpisode ->
val episodes = podcast.episodes?.sortedByDescending { it.publishedAt }
val children = episodes?.map { podcastEpisode ->
val progress = serverUserMediaProgress.find { it.libraryItemId == libraryItemWrapper.id && it.episodeId == podcastEpisode.id }
@ -209,13 +690,16 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
}
}
/**
* Loads libraries for selected server with stats
*/
private fun loadLibraries(cb: (List<Library>) -> Unit) {
if (serverLibraries.isNotEmpty()) {
cb(serverLibraries)
} else {
apiHandler.getLibraries {
serverLibraries = it
cb(it)
apiHandler.getLibraries { loadedLibraries ->
serverLibraries = loadedLibraries
cb(serverLibraries)
}
}
}
@ -329,6 +813,25 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
}
}
fun initializeInProgressItems(cb: () -> Unit) {
Log.d(tag, "Initializing inprogress items")
loadItemsInProgressForAllLibraries { itemsInProgress ->
itemsInProgress.forEach {
val libraryItem = it.libraryItemWrapper as LibraryItem
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
if (it.episode != null) {
podcastEpisodeLibraryItemMap[it.episode.id] = LibraryItemWithEpisode(it.libraryItemWrapper, it.episode)
}
}
Log.d(tag, "Initializing inprogress items done")
cb()
}
}
fun loadAndroidAutoItems(cb: () -> Unit) {
Log.d(tag, "Load android auto items")
@ -343,23 +846,7 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
Log.w(tag, "No libraries returned from server request")
cb()
} else {
val library = libraries[0]
Log.d(tag, "Loading categories for library ${library.name} - ${library.id} - ${library.mediaType}")
loadItemsInProgressForAllLibraries { itemsInProgress ->
itemsInProgress.forEach {
val libraryItem = it.libraryItemWrapper as LibraryItem
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
if (it.episode != null) {
podcastEpisodeLibraryItemMap[it.episode.id] = LibraryItemWithEpisode(it.libraryItemWrapper, it.episode)
}
}
cb() // Fully loaded
}
cb() // Fully loaded
}
}
} else { // Not connected to server
@ -369,6 +856,65 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
}
}
/**
* Handles search requests.
* Searches from books, series and authors
*/
suspend fun doSearch(libraryId: String, queryString: String) : Map<String, List<MediaBrowserCompat.MediaItem>> {
return suspendCoroutine {
apiHandler.getSearchResults(libraryId, queryString) { searchResult ->
Log.d(tag, "searchLocalCache: $searchResult")
// Nothing found from server
if (searchResult === null) {
it.resume(mapOf())
return@getSearchResults
}
val foundItems: MutableMap<String, List<MediaBrowserCompat.MediaItem>> = mutableMapOf()
val serverLibrary = serverLibraries.find { sl -> sl.id == libraryId }
// Books
if (searchResult.book !== null && searchResult.book!!.isNotEmpty()) {
Log.d(tag, "searchLocalCache: found ${searchResult.book!!.size} books")
val children = searchResult.book!!.filter { it.libraryItem.checkHasTracks() }.map { bookResult ->
val libraryItem = bookResult.libraryItem
if (serverLibraryItems.find { li -> li.id == libraryItem.id } == null) {
serverLibraryItems.add(libraryItem)
}
val progress = serverUserMediaProgress.find { it.libraryItemId == libraryItem.id }
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItem.id)
libraryItem.localLibraryItemId = localLibraryItem?.id
val description = libraryItem.getMediaDescription(progress, ctx, null, null, "Books (${serverLibrary?.name})")
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE)
}
foundItems["book"] = children
}
if (searchResult.series !== null && searchResult.series!!.isNotEmpty()) {
Log.d(tag, "onSearch: found ${searchResult.series!!.size} series")
val children = searchResult.series!!.map { seriesResult ->
val seriesItem = seriesResult.series
seriesItem.books = seriesResult.books as MutableList<LibraryItem>
val description = seriesItem.getMediaDescription(null, ctx, "Series (${serverLibrary?.name})")
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
}
foundItems["series"] = children
}
if (searchResult.authors !== null && searchResult.authors!!.isNotEmpty()) {
Log.d(tag, "onSearch: found ${searchResult.authors!!.size} authors")
val children = searchResult.authors!!.map { authorItem ->
val description = authorItem.getMediaDescription(null, ctx, "Authors (${serverLibrary?.name})")
MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE)
}
foundItems["authors"] = children
}
it.resume(foundItems)
}
}
}
fun getFirstItem() : LibraryItemWrapper? {
if (serverLibraryItems.isNotEmpty()) {
return serverLibraryItems[0]

View file

@ -8,41 +8,49 @@ import com.audiobookshelf.app.data.MediaProgress
import com.audiobookshelf.app.data.PlaybackSession
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.player.PlayerNotificationService
import com.audiobookshelf.app.plugins.AbsLogger
import com.audiobookshelf.app.server.ApiHandler
import java.util.*
import kotlin.concurrent.schedule
data class MediaProgressSyncData(
var timeListened:Long, // seconds
var duration:Double, // seconds
var currentTime:Double // seconds
var timeListened: Long, // seconds
var duration: Double, // seconds
var currentTime: Double // seconds
)
data class SyncResult(
var serverSyncAttempted:Boolean,
var serverSyncSuccess:Boolean?,
var serverSyncMessage:String?
var serverSyncAttempted: Boolean,
var serverSyncSuccess: Boolean?,
var serverSyncMessage: String?
)
class MediaProgressSyncer(val playerNotificationService: PlayerNotificationService, private val apiHandler: ApiHandler) {
class MediaProgressSyncer(
val playerNotificationService: PlayerNotificationService,
private val apiHandler: ApiHandler
) {
private val tag = "MediaProgressSync"
private val METERED_CONNECTION_SYNC_INTERVAL = 60000
private var listeningTimerTask: TimerTask? = null
var listeningTimerRunning:Boolean = false
var listeningTimerRunning: Boolean = false
private var lastSyncTime:Long = 0
private var failedSyncs:Int = 0
private var lastSyncTime: Long = 0
private var failedSyncs: Int = 0
var currentPlaybackSession: PlaybackSession? = null // copy of pb session currently syncing
var currentLocalMediaProgress: LocalMediaProgress? = null
private val currentDisplayTitle get() = currentPlaybackSession?.displayTitle ?: "Unset"
val currentIsLocal get() = currentPlaybackSession?.isLocal == true
val currentSessionId get() = currentPlaybackSession?.id ?: ""
private val currentPlaybackDuration get() = currentPlaybackSession?.duration ?: 0.0
private val currentDisplayTitle
get() = currentPlaybackSession?.displayTitle ?: "Unset"
val currentIsLocal
get() = currentPlaybackSession?.isLocal == true
val currentSessionId
get() = currentPlaybackSession?.id ?: ""
private val currentPlaybackDuration
get() = currentPlaybackSession?.duration ?: 0.0
fun start(playbackSession:PlaybackSession) {
fun start(playbackSession: PlaybackSession) {
if (listeningTimerRunning) {
Log.d(tag, "start: Timer already running for $currentDisplayTitle")
if (playbackSession.id != currentSessionId) {
@ -62,40 +70,48 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi
listeningTimerRunning = true
lastSyncTime = System.currentTimeMillis()
currentPlaybackSession = playbackSession.clone()
Log.d(tag, "start: init last sync time $lastSyncTime with playback session id=${currentPlaybackSession?.id}")
Log.d(
tag,
"start: init last sync time $lastSyncTime with playback session id=${currentPlaybackSession?.id}"
)
listeningTimerTask = Timer("ListeningTimer", false).schedule(15000L, 15000L) {
Handler(Looper.getMainLooper()).post() {
if (playerNotificationService.currentPlayer.isPlaying) {
// Set auto sleep timer if enabled and within start/end time
playerNotificationService.sleepTimerManager.checkAutoSleepTimer()
listeningTimerTask =
Timer("ListeningTimer", false).schedule(15000L, 15000L) {
Handler(Looper.getMainLooper()).post() {
if (playerNotificationService.currentPlayer.isPlaying) {
// Set auto sleep timer if enabled and within start/end time
playerNotificationService.sleepTimerManager.checkAutoSleepTimer()
// Only sync with server on unmetered connection every 15s OR sync with server if last sync time is >= 60s
val shouldSyncServer = PlayerNotificationService.isUnmeteredNetwork || System.currentTimeMillis() - lastSyncTime >= METERED_CONNECTION_SYNC_INTERVAL
// Only sync with server on unmetered connection every 15s OR sync with server if
// last sync time is >= 60s
val shouldSyncServer =
PlayerNotificationService.isUnmeteredNetwork ||
System.currentTimeMillis() - lastSyncTime >=
METERED_CONNECTION_SYNC_INTERVAL
val currentTime = playerNotificationService.getCurrentTimeSeconds()
if (currentTime > 0) {
sync(shouldSyncServer, currentTime) { syncResult ->
Log.d(tag, "Sync complete")
val currentTime = playerNotificationService.getCurrentTimeSeconds()
if (currentTime > 0) {
sync(shouldSyncServer, currentTime) { syncResult ->
Log.d(tag, "Sync complete")
currentPlaybackSession?.let { playbackSession ->
MediaEventManager.saveEvent(playbackSession, syncResult)
currentPlaybackSession?.let { playbackSession ->
MediaEventManager.saveEvent(playbackSession, syncResult)
}
}
}
}
}
}
}
}
}
}
}
fun play(playbackSession:PlaybackSession) {
fun play(playbackSession: PlaybackSession) {
Log.d(tag, "play ${playbackSession.displayTitle}")
MediaEventManager.playEvent(playbackSession)
start(playbackSession)
}
fun stop(shouldSync:Boolean? = true, cb: () -> Unit) {
fun stop(shouldSync: Boolean? = true, cb: () -> Unit) {
if (!listeningTimerRunning) {
reset()
return cb()
@ -106,7 +122,8 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi
listeningTimerRunning = false
Log.d(tag, "stop: Stopping listening for $currentDisplayTitle")
val currentTime = if (shouldSync == true) playerNotificationService.getCurrentTimeSeconds() else 0.0
val currentTime =
if (shouldSync == true) playerNotificationService.getCurrentTimeSeconds() else 0.0
if (currentTime > 0) { // Current time should always be > 0 on stop
sync(true, currentTime) { syncResult ->
currentPlaybackSession?.let { playbackSession ->
@ -192,17 +209,21 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi
MediaEventManager.seekEvent(currentPlaybackSession!!, null)
}
// Currently unused
fun syncFromServerProgress(mediaProgress: MediaProgress) {
currentPlaybackSession?.let {
it.updatedAt = mediaProgress.lastUpdate
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"
)
saveLocalProgress(it)
}
}
fun sync(shouldSyncServer:Boolean, currentTime:Double, cb: (SyncResult?) -> Unit) {
fun sync(shouldSyncServer: Boolean, currentTime: Double, cb: (SyncResult?) -> Unit) {
if (lastSyncTime <= 0) {
Log.e(tag, "Last sync time is not set $lastSyncTime")
return cb(null)
@ -214,11 +235,14 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi
}
val listeningTimeToAdd = diffSinceLastSync / 1000L
val syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime)
val syncData = MediaProgressSyncData(listeningTimeToAdd, currentPlaybackDuration, currentTime)
currentPlaybackSession?.syncData(syncData)
if (currentPlaybackSession?.progress?.isNaN() == true) {
Log.e(tag, "Current Playback Session invalid progress ${currentPlaybackSession?.progress} | Current Time: ${currentPlaybackSession?.currentTime} | Duration: ${currentPlaybackSession?.getTotalDuration()}")
Log.e(
tag,
"Current Playback Session invalid progress ${currentPlaybackSession?.progress} | Current Time: ${currentPlaybackSession?.currentTime} | Duration: ${currentPlaybackSession?.getTotalDuration()}"
)
return cb(null)
}
@ -226,11 +250,7 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi
// 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)
}
}
currentPlaybackSession?.let { DeviceManager.dbManager.savePlaybackSession(it) }
if (currentIsLocal) {
// Save local progress sync
@ -238,36 +258,50 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi
saveLocalProgress(it)
lastSyncTime = System.currentTimeMillis()
Log.d(tag, "Sync local device current serverConnectionConfigId=${DeviceManager.serverConnectionConfig?.id}")
Log.d(
tag,
"Sync local device current serverConnectionConfigId=${DeviceManager.serverConnectionConfig?.id}"
)
AbsLogger.info("MediaProgressSyncer", "sync: Saved local progress (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: ${it.id})")
// 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
if (hasNetworkConnection && shouldSyncServer && !it.libraryItemId.isNullOrEmpty() && it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
// Send sync to server also if connected to this server and local item belongs to this
// server
val isConnectedToSameServer = it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId
if (hasNetworkConnection &&
shouldSyncServer &&
!it.libraryItemId.isNullOrEmpty() &&
isConnectedToSameServer
) {
apiHandler.sendLocalProgressSync(it) { syncSuccess, errorMsg ->
if (syncSuccess) {
failedSyncs = 0
playerNotificationService.alertSyncSuccess()
DeviceManager.dbManager.removePlaybackSession(it.id) // Remove session from db
AbsLogger.info("MediaProgressSyncer", "sync: Successfully synced local progress (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: ${it.id})")
} else {
failedSyncs++
if (failedSyncs == 2) {
playerNotificationService.alertSyncFailing() // Show alert in client
failedSyncs = 0
}
Log.e(tag, "Local Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime with session id=${it.id}")
AbsLogger.error("MediaProgressSyncer", "sync: Local progress sync failed (count: $failedSyncs) (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: ${it.id}) (${DeviceManager.serverConnectionConfigName})")
}
cb(SyncResult(true, syncSuccess, errorMsg))
}
} else {
AbsLogger.info("MediaProgressSyncer", "sync: Not sending local progress to server (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: ${it.id}) (hasNetworkConnection: $hasNetworkConnection) (isConnectedToSameServer: $isConnectedToSameServer)")
cb(SyncResult(false, null, null))
}
}
} else if (hasNetworkConnection && shouldSyncServer) {
Log.d(tag, "sync: currentSessionId=$currentSessionId")
AbsLogger.info("MediaProgressSyncer", "sync: Sending progress sync to server (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: ${currentSessionId}) (${DeviceManager.serverConnectionConfigName})")
apiHandler.sendProgressSync(currentSessionId, syncData) { syncSuccess, errorMsg ->
if (syncSuccess) {
Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime")
AbsLogger.info("MediaProgressSyncer", "sync: Successfully synced progress (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: ${currentSessionId}) (${DeviceManager.serverConnectionConfigName})")
failedSyncs = 0
playerNotificationService.alertSyncSuccess()
lastSyncTime = System.currentTimeMillis()
@ -278,18 +312,20 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi
playerNotificationService.alertSyncFailing() // Show alert in client
failedSyncs = 0
}
Log.e(tag, "Progress sync failed ($failedSyncs) to send to server $currentDisplayTitle for time $currentTime with session id=${currentSessionId}")
AbsLogger.error("MediaProgressSyncer", "sync: Progress sync failed (count: $failedSyncs) (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: $currentSessionId) (${DeviceManager.serverConnectionConfigName})")
}
cb(SyncResult(true, syncSuccess, errorMsg))
}
} else {
AbsLogger.info("MediaProgressSyncer", "sync: Not sending progress to server (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: $currentSessionId) (${DeviceManager.serverConnectionConfigName}) (hasNetworkConnection: $hasNetworkConnection)")
cb(SyncResult(false, null, null))
}
}
private fun saveLocalProgress(playbackSession:PlaybackSession) {
private fun saveLocalProgress(playbackSession: PlaybackSession) {
if (currentLocalMediaProgress == null) {
val mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(playbackSession.localMediaProgressId)
val mediaProgress =
DeviceManager.dbManager.getLocalMediaProgress(playbackSession.localMediaProgressId)
if (mediaProgress == null) {
currentLocalMediaProgress = playbackSession.getNewLocalMediaProgress()
} else {
@ -306,12 +342,14 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi
} else {
DeviceManager.dbManager.saveLocalMediaProgress(it)
playerNotificationService.clientEventEmitter?.onLocalMediaProgressUpdate(it)
Log.d(tag, "Saved Local Progress Current Time: ID ${it.id} | ${it.currentTime} | Duration ${it.duration} | Progress ${it.progressPercent}%")
Log.d(
tag,
"Saved Local Progress Current Time: ID ${it.id} | ${it.currentTime} | Duration ${it.duration} | Progress ${it.progressPercent}%"
)
}
}
}
fun reset() {
currentPlaybackSession = null
currentLocalMediaProgress = null

View file

@ -0,0 +1,55 @@
package com.audiobookshelf.app.media
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import androidx.annotation.AnyRes
import com.audiobookshelf.app.R
/**
* get uri to drawable or any other resource type if u wish
* @param drawableId - drawable res id
* @return - uri
*/
fun getUriToDrawable(context: Context, @AnyRes drawableId: Int): Uri {
return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE
+ "://" + context.resources.getResourcePackageName(drawableId)
+ '/' + context.resources.getResourceTypeName(drawableId)
+ '/' + context.resources.getResourceEntryName(drawableId))
}
/**
* get uri to drawable or any other resource type if u wish
* @param drawableId - drawable res id
* @return - uri
*/
fun getUriToAbsIconDrawable(context: Context, absIconName: String): Uri {
val drawableId = when(absIconName) {
"audiobookshelf" -> R.drawable.abs_audiobookshelf
"authors" -> R.drawable.abs_authors
"book-1" -> R.drawable.abs_book_1
"books-1" -> R.drawable.abs_books_1
"books-2" -> R.drawable.abs_books_2
"columns" -> R.drawable.abs_columns
"database" -> R.drawable.abs_database
"file-picture" -> R.drawable.abs_file_picture
"headphones" -> R.drawable.abs_headphones
"heart" -> R.drawable.abs_heart
"microphone_1" -> R.drawable.abs_microphone_1
"microphone_2" -> R.drawable.abs_microphone_2
"microphone_3" -> R.drawable.abs_microphone_3
"music" -> R.drawable.abs_music
"podcast" -> R.drawable.abs_podcast
"radio" -> R.drawable.abs_radio
"rocket" -> R.drawable.abs_rocket
"rss" -> R.drawable.abs_rss
"star" -> R.drawable.abs_star
else -> R.drawable.icon_library_folder
}
return Uri.parse(
ContentResolver.SCHEME_ANDROID_RESOURCE
+ "://" + context.resources.getResourcePackageName(drawableId)
+ '/' + context.resources.getResourceTypeName(drawableId)
+ '/' + context.resources.getResourceEntryName(drawableId))
}

View file

@ -42,7 +42,10 @@ data class DownloadItemPart(
val finalDestinationUri = Uri.fromFile(finalDestinationFile)
var downloadUrl = "${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}"
if (serverPath.endsWith("/cover")) downloadUrl += "&format=jpeg&raw=1" // For cover images force to jpeg
if (serverPath.endsWith("/cover")) {
downloadUrl += "&raw=1" // Download raw cover image
}
val downloadUri = Uri.parse(downloadUrl)
Log.d("DownloadItemPart", "Audio File Destination Uri: $destinationUri | Final Destination Uri: $finalDestinationUri | Download URI $downloadUri")
return DownloadItemPart(
@ -77,7 +80,7 @@ data class DownloadItemPart(
val isInternalStorage get() = localFolderId.startsWith("internal-")
@get:JsonIgnore
val serverUrl get() = "${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}"
val serverUrl get() = uri.toString()
@JsonIgnore
fun getDownloadRequest(): DownloadManager.Request {

View file

@ -7,7 +7,6 @@ import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.support.v4.media.session.MediaControllerCompat
import android.util.Log
import com.audiobookshelf.app.BuildConfig
import com.audiobookshelf.app.R
import com.bumptech.glide.Glide
@ -15,7 +14,7 @@ import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import kotlinx.coroutines.*
class AbMediaDescriptionAdapter constructor(private val controller: MediaControllerCompat, private val playerNotificationService: PlayerNotificationService) : PlayerNotificationManager.MediaDescriptionAdapter {
class AbMediaDescriptionAdapter (private val controller: MediaControllerCompat, private val playerNotificationService: PlayerNotificationService) : PlayerNotificationManager.MediaDescriptionAdapter {
private val tag = "MediaDescriptionAdapter"
private var currentIconUri: Uri? = null
@ -36,12 +35,17 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
callback: PlayerNotificationManager.BitmapCallback
): Bitmap? {
val albumArtUri = controller.metadata.description.iconUri
val albumBitmap = controller.metadata.description.iconBitmap
// For local cover images, bitmap is set in PlayerNotificationService TimelineQueueNavigator.getMediaDescription
if (albumBitmap != null) {
return albumBitmap
}
return if (currentIconUri != albumArtUri || currentBitmap == null) {
// Cache the bitmap for the current audiobook so that successive calls to
// `getCurrentLargeIcon` don't cause the bitmap to be recreated.
currentIconUri = albumArtUri
Log.d(tag, "ART $currentIconUri")
if (currentIconUri.toString().startsWith("content://")) {
currentBitmap = if (Build.VERSION.SDK_INT < 28) {
@ -61,7 +65,6 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
}
null
}
} else {
currentBitmap
}

View file

@ -1,51 +1,45 @@
package com.audiobookshelf.app.player
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import android.support.v4.media.MediaMetadataCompat
import androidx.annotation.AnyRes
import android.util.Log
import com.audiobookshelf.app.R
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.media.getUriToDrawable
class BrowseTree(
val context: Context,
itemsInProgress: List<ItemInProgress>,
libraries: List<Library>
libraries: List<Library>,
recentsLoaded: Boolean
) {
private val mediaIdToChildren = mutableMapOf<String, MutableList<MediaMetadataCompat>>()
/**
* get uri to drawable or any other resource type if u wish
* @param drawableId - drawable res id
* @return - uri
*/
private fun getUriToDrawable(@AnyRes drawableId: Int): Uri {
return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE
+ "://" + context.resources.getResourcePackageName(drawableId)
+ '/' + context.resources.getResourceTypeName(drawableId)
+ '/' + context.resources.getResourceEntryName(drawableId))
}
init {
val rootList = mediaIdToChildren[AUTO_BROWSE_ROOT] ?: mutableListOf()
val continueListeningMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, CONTINUE_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Listening")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.exo_icon_localaudio).toString())
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Continue")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_localaudio).toString())
}.build()
val recentMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, RECENTLY_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Recent")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.md_clock_outline).toString())
}.build()
val downloadsMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, DOWNLOADS_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Downloads")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.exo_icon_downloaddone).toString())
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_downloaddone).toString())
}.build()
val librariesMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, LIBRARIES_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Libraries")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(R.drawable.icon_library_folder).toString())
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.icon_library_folder).toString())
}.build()
if (itemsInProgress.isNotEmpty()) {
@ -53,13 +47,28 @@ class BrowseTree(
}
if (libraries.isNotEmpty()) {
if (recentsLoaded) {
rootList += recentMetadata
}
rootList += librariesMetadata
libraries.forEach { library ->
val libraryMediaMetadata = library.getMediaMetadata()
// Skip libraries without audio content
if (library.stats?.numAudioFiles == 0) return@forEach
Log.d("BrowseTree", "Library $library | ${library.icon}")
// Generate library list items for Libraries menu
val libraryMediaMetadata = library.getMediaMetadata(context)
val children = mediaIdToChildren[LIBRARIES_ROOT] ?: mutableListOf()
children += libraryMediaMetadata
mediaIdToChildren[LIBRARIES_ROOT] = children
if (recentsLoaded) {
// Generate library list items for Recent menu
val recentlyMediaMetadata = library.getMediaMetadata(context,"recently")
val childrenRecently = mediaIdToChildren[RECENTLY_ROOT] ?: mutableListOf()
childrenRecently += recentlyMediaMetadata
mediaIdToChildren[RECENTLY_ROOT] = childrenRecently
}
}
}
@ -75,3 +84,4 @@ const val AUTO_BROWSE_ROOT = "/"
const val CONTINUE_ROOT = "__CONTINUE__"
const val DOWNLOADS_ROOT = "__DOWNLOADS__"
const val LIBRARIES_ROOT = "__LIBRARIES__"
const val RECENTLY_ROOT = "__RECENTLY__"

View file

@ -8,7 +8,6 @@ import android.util.Log
import android.view.KeyEvent
import com.audiobookshelf.app.data.LibraryItemWrapper
import com.audiobookshelf.app.data.PodcastEpisode
import com.audiobookshelf.app.device.DeviceManager
import java.util.*
import kotlin.concurrent.schedule

View file

@ -5,6 +5,7 @@ import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.plugins.AbsLogger
import kotlin.math.sqrt
class ShakeDetector : SensorEventListener {
@ -46,6 +47,7 @@ class ShakeDetector : SensorEventListener {
if (mShakeTimestamp + SHAKE_COUNT_RESET_TIME_MS < now) {
mShakeCount = 0
}
AbsLogger.info("ShakeDetector", "Device shake above threshold ($gForce > $shakeThreshold)")
mShakeTimestamp = now
mShakeCount++
mListener!!.onShake(mShakeCount)

View file

@ -180,7 +180,8 @@ class AbsAudioPlayer : Plugin() {
val playWhenReady = call.getBoolean("playWhenReady") == true
val playbackRate = call.getFloat("playbackRate",1f) ?: 1f
val startTimeOverride = call.getDouble("startTime")
Log.d(tag, "prepareLibraryItem lid=$libraryItemId, startTimeOverride=$startTimeOverride, playbackRate=$playbackRate")
AbsLogger.info("AbsAudioPlayer", "prepareLibraryItem: lid=$libraryItemId, startTimeOverride=$startTimeOverride, playbackRate=$playbackRate")
if (libraryItemId.isEmpty()) {
Log.e(tag, "Invalid call to play library item no library item id")
@ -198,6 +199,9 @@ class AbsAudioPlayer : Plugin() {
return call.resolve(JSObject("{\"error\":\"Podcast episode not found\"}"))
}
}
if (!it.hasTracks(episode)) {
return call.resolve(JSObject("{\"error\":\"No audio files found on device. Download book again to fix.\"}"))
}
Handler(Looper.getMainLooper()).post {
Log.d(tag, "prepareLibraryItem: Preparing Local Media item ${jacksonMapper.writeValueAsString(it)}")

View file

@ -8,6 +8,7 @@ import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.media.MediaEventManager
import com.audiobookshelf.app.server.ApiHandler
import com.audiobookshelf.app.managers.SecureStorage
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
@ -22,20 +23,25 @@ class AbsDatabase : Plugin() {
val tag = "AbsDatabase"
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
lateinit var mainActivity: MainActivity
lateinit var apiHandler: ApiHandler
private lateinit var mainActivity: MainActivity
private lateinit var apiHandler: ApiHandler
private lateinit var secureStorage: SecureStorage
data class LocalMediaProgressPayload(val value:List<LocalMediaProgress>)
data class LocalLibraryItemsPayload(val value:List<LocalLibraryItem>)
data class LocalFoldersPayload(val value:List<LocalFolder>)
data class ServerConnConfigPayload(val id:String?, val index:Int, val name:String?, val userId:String, val username:String, val token:String, val address:String?, val customHeaders:Map<String,String>?)
data class ServerConnConfigPayload(val id:String?, val index:Int, val name:String?, val userId:String, val username:String, var version:String, val token:String, val refreshToken:String?, val address:String?, val customHeaders:Map<String,String>?)
override fun load() {
mainActivity = (activity as MainActivity)
apiHandler = ApiHandler(mainActivity)
ApiHandler.absDatabaseNotifyListeners = ::notifyListeners
secureStorage = SecureStorage(mainActivity)
DeviceManager.dbManager.cleanLocalMediaProgress()
DeviceManager.dbManager.cleanLocalLibraryItems()
DeviceManager.dbManager.cleanLogs()
}
@PluginMethod
@ -119,7 +125,9 @@ class AbsDatabase : Plugin() {
val userId = serverConfigPayload.userId
val username = serverConfigPayload.username
val token = serverConfigPayload.token
val serverVersion = serverConfigPayload.version
val accessToken = serverConfigPayload.token
val refreshToken = serverConfigPayload.refreshToken // Refresh only sent after login or refresh
GlobalScope.launch(Dispatchers.IO) {
if (serverConnectionConfig == null) { // New Server Connection
@ -128,7 +136,16 @@ class AbsDatabase : Plugin() {
// Create new server connection config
val sscId = DeviceManager.getBase64Id("$serverAddress@$username")
val sscIndex = DeviceManager.deviceData.serverConnectionConfigs.size
serverConnectionConfig = ServerConnectionConfig(sscId, sscIndex, "$serverAddress ($username)", serverAddress, userId, username, token, serverConfigPayload.customHeaders)
// Store refresh token securely if provided
val hasRefreshToken = if (!refreshToken.isNullOrEmpty()) {
secureStorage.storeRefreshToken(sscId, refreshToken)
} else {
false
}
Log.d(tag, "Refresh token secured = $hasRefreshToken")
serverConnectionConfig = ServerConnectionConfig(sscId, sscIndex, "$serverAddress ($username)", serverAddress, serverVersion, userId, username, accessToken, serverConfigPayload.customHeaders)
// Add and save
DeviceManager.deviceData.serverConnectionConfigs.add(serverConnectionConfig!!)
@ -136,14 +153,21 @@ class AbsDatabase : Plugin() {
DeviceManager.dbManager.saveDeviceData(DeviceManager.deviceData)
} else {
var shouldSave = false
if (serverConnectionConfig?.username != username || serverConnectionConfig?.token != token) {
if (serverConnectionConfig?.username != username || serverConnectionConfig?.token != accessToken || serverConnectionConfig?.version != serverVersion) {
serverConnectionConfig?.userId = userId
serverConnectionConfig?.username = username
serverConnectionConfig?.name = "${serverConnectionConfig?.address} (${serverConnectionConfig?.username})"
serverConnectionConfig?.token = token
serverConnectionConfig?.version = serverVersion
serverConnectionConfig?.token = accessToken
shouldSave = true
}
// Update refresh token if provided
if (!refreshToken.isNullOrEmpty()) {
val stored = secureStorage.storeRefreshToken(serverConnectionConfig!!.id, refreshToken)
Log.d(tag, "Refresh token secured = $stored")
}
// Set last connection config
if (DeviceManager.deviceData.lastServerConnectionConfigId != serverConfigPayload.id) {
DeviceManager.deviceData.lastServerConnectionConfigId = serverConfigPayload.id
@ -162,6 +186,10 @@ class AbsDatabase : Plugin() {
fun removeServerConnectionConfig(call:PluginCall) {
GlobalScope.launch(Dispatchers.IO) {
val serverConnectionConfigId = call.getString("serverConnectionConfigId", "").toString()
// Remove refresh token if it exists
secureStorage.removeRefreshToken(serverConnectionConfigId)
DeviceManager.deviceData.serverConnectionConfigs = DeviceManager.deviceData.serverConnectionConfigs.filter { it.id != serverConnectionConfigId } as MutableList<ServerConnectionConfig>
if (DeviceManager.deviceData.lastServerConnectionConfigId == serverConnectionConfigId) {
DeviceManager.deviceData.lastServerConnectionConfigId = null
@ -174,6 +202,42 @@ class AbsDatabase : Plugin() {
}
}
@PluginMethod
fun getRefreshToken(call:PluginCall) {
val serverConnectionConfigId = call.getString("serverConnectionConfigId", "").toString()
GlobalScope.launch(Dispatchers.IO) {
val refreshToken = secureStorage.getRefreshToken(serverConnectionConfigId)
if (refreshToken != null) {
val result = JSObject()
result.put("refreshToken", refreshToken)
call.resolve(result)
} else {
call.resolve()
}
}
}
@PluginMethod
fun clearRefreshToken(call:PluginCall) {
val serverConnectionConfigId = call.getString("serverConnectionConfigId", "").toString()
val refreshToken = secureStorage.removeRefreshToken(serverConnectionConfigId)
val result = JSObject()
result.put("success", refreshToken)
call.resolve(result)
}
@PluginMethod
fun getAccessToken(call:PluginCall) {
val serverConnectionConfigId = call.getString("serverConnectionConfigId", "").toString()
val serverConnectionConfig = DeviceManager.deviceData.serverConnectionConfigs.find { it.id == serverConnectionConfigId }
val token = serverConnectionConfig?.token ?: ""
val ret = JSObject()
ret.put("token", token)
call.resolve(ret)
}
@PluginMethod
fun logout(call:PluginCall) {
GlobalScope.launch(Dispatchers.IO) {
@ -220,12 +284,11 @@ class AbsDatabase : Plugin() {
@PluginMethod
fun syncLocalSessionsWithServer(call:PluginCall) {
if (DeviceManager.serverConnectionConfig == null) {
Log.e(tag, "syncLocalSessionsWithServer not connected to server")
AbsLogger.error("AbsDatabase", "syncLocalSessionsWithServer: not connected to server")
return call.resolve()
}
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()) {
@ -233,6 +296,7 @@ class AbsDatabase : Plugin() {
if (!success) {
call.resolve(JSObject("{\"error\":\"$errorMsg\"}"))
} else {
AbsLogger.info("AbsDatabase", "syncLocalSessionsWithServer: Finished sending local playback sessions to server. Removing ${savedSessions.size} saved sessions.")
// Remove all local sessions
savedSessions.forEach {
DeviceManager.dbManager.removePlaybackSession(it.id)
@ -241,6 +305,7 @@ class AbsDatabase : Plugin() {
}
}
} else {
AbsLogger.info("AbsDatabase", "syncLocalSessionsWithServer: No saved local playback sessions to send to server.")
call.resolve()
}
}

View file

@ -196,7 +196,11 @@ class AbsFileSystem : Plugin() {
if (localLibraryItem?.folderId?.startsWith("internal-") == true) {
Log.d(tag, "Deleting internal library item at absolutePath $absolutePath")
val file = File(absolutePath)
success = file.deleteRecursively()
success = if (file.exists()) {
file.deleteRecursively()
} else {
true
}
} else {
var subfolderPathToDelete = ""
localLibraryItem?.folderId?.let { folderId ->
@ -218,7 +222,12 @@ class AbsFileSystem : Plugin() {
}
val docfile = DocumentFileCompat.fromUri(mainActivity, Uri.parse(contentUrl))
success = docfile?.delete() == true
if (docfile?.exists() == true) {
success = docfile.delete() == true
} else {
Log.d(tag, "Folder $contentUrl doesn't exist")
success = true
}
if (subfolderPathToDelete != "") {
Log.d(tag, "Deleting empty subfolder at $subfolderPathToDelete")

View file

@ -0,0 +1,80 @@
package com.audiobookshelf.app.plugins
import android.util.Log
import com.audiobookshelf.app.device.DeviceManager
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.JSObject
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
import java.util.UUID
data class AbsLog(
var id:String,
var tag:String,
var level:String,
var message:String,
var timestamp:Long
)
data class AbsLogList(val value:List<AbsLog>)
@CapacitorPlugin(name = "AbsLogger")
class AbsLogger : Plugin() {
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
override fun load() {
onLogEmitter = { log:AbsLog ->
notifyListeners("onLog", JSObject(jacksonMapper.writeValueAsString(log)))
}
info("AbsLogger", "load: AbsLogger plugin initialized")
}
companion object {
var onLogEmitter:((log:AbsLog) -> Unit)? = null
fun log(level:String, tag:String, message:String) {
val absLog = AbsLog(id = UUID.randomUUID().toString(), tag, level, message, timestamp = System.currentTimeMillis())
DeviceManager.dbManager.saveLog(absLog)
onLogEmitter?.let { it(absLog) }
}
fun info(tag:String, message:String) {
Log.i("AbsLogger", message)
log("info", tag, message)
}
fun error(tag:String, message:String) {
Log.e("AbsLogger", message)
log("error", tag, message)
}
}
@PluginMethod
fun info(call: PluginCall) {
val msg = call.getString("message") ?: return call.reject("No message")
val tag = call.getString("tag") ?: ""
info(tag, msg)
call.resolve()
}
@PluginMethod
fun error(call: PluginCall) {
val msg = call.getString("message") ?: return call.reject("No message")
val tag = call.getString("tag") ?: ""
error(tag, msg)
call.resolve()
}
@PluginMethod
fun getAllLogs(call: PluginCall) {
val absLogs = DeviceManager.dbManager.getAllLogs()
call.resolve(JSObject(jacksonMapper.writeValueAsString(AbsLogList(absLogs))))
}
@PluginMethod
fun clearLogs(call: PluginCall) {
DeviceManager.dbManager.removeAllLogs()
call.resolve()
}
}

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.provider.Settings
import android.util.Base64
import android.util.Log
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager
@ -12,6 +13,8 @@ import com.audiobookshelf.app.media.MediaProgressSyncData
import com.audiobookshelf.app.media.SyncResult
import com.audiobookshelf.app.models.User
import com.audiobookshelf.app.BuildConfig
import com.audiobookshelf.app.plugins.AbsLogger
import com.audiobookshelf.app.managers.SecureStorage
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
@ -31,9 +34,19 @@ import java.util.concurrent.TimeUnit
class ApiHandler(var ctx:Context) {
val tag = "ApiHandler"
companion object {
// For sending data back to the Webview frontend
lateinit var absDatabaseNotifyListeners:(String, JSObject) -> Unit
fun checkAbsDatabaseNotifyListenersInitted():Boolean {
return ::absDatabaseNotifyListeners.isInitialized
}
}
private var defaultClient = OkHttpClient()
private var pingClient = OkHttpClient.Builder().callTimeout(3, TimeUnit.SECONDS).build()
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
private var secureStorage = SecureStorage(ctx)
data class LocalSessionsSyncRequestPayload(val sessions:List<PlaybackSession>, val deviceInfo:DeviceInfo)
@JsonIgnoreProperties(ignoreUnknown = true)
@ -44,10 +57,17 @@ class ApiHandler(var ctx:Context) {
val address = config?.address ?: DeviceManager.serverAddress
val token = config?.token ?: DeviceManager.token
val request = Request.Builder()
.url("${address}$endpoint").addHeader("Authorization", "Bearer $token")
.build()
makeRequest(request, httpClient, cb)
try {
val request = Request.Builder()
.url("${address}$endpoint").addHeader("Authorization", "Bearer $token")
.build()
makeRequest(request, httpClient, cb)
} catch(e: Exception) {
e.printStackTrace()
val jsobj = JSObject()
jsobj.put("error", "Request failed: ${e.message}")
cb(jsobj)
}
}
private fun postRequest(endpoint:String, payload: JSObject?, config:ServerConnectionConfig?, cb: (JSObject) -> Unit) {
@ -57,23 +77,38 @@ class ApiHandler(var ctx:Context) {
val requestBody = payload?.toString()?.toRequestBody(mediaType) ?: EMPTY_REQUEST
val requestUrl = "${address}$endpoint"
Log.d(tag, "postRequest to $requestUrl")
val request = Request.Builder().post(requestBody)
.url(requestUrl).addHeader("Authorization", "Bearer ${token}")
.build()
makeRequest(request, null, cb)
try {
val request = Request.Builder().post(requestBody)
.url(requestUrl).addHeader("Authorization", "Bearer ${token}")
.build()
makeRequest(request, null, cb)
} catch(e: Exception) {
e.printStackTrace()
val jsobj = JSObject()
jsobj.put("error", "Request failed: ${e.message}")
cb(jsobj)
}
}
private fun patchRequest(endpoint:String, payload: JSObject, cb: (JSObject) -> Unit) {
val mediaType = "application/json; charset=utf-8".toMediaType()
val requestBody = payload.toString().toRequestBody(mediaType)
val request = Request.Builder().patch(requestBody)
.url("${DeviceManager.serverAddress}$endpoint").addHeader("Authorization", "Bearer ${DeviceManager.token}")
.build()
makeRequest(request, null, cb)
try {
val request = Request.Builder().patch(requestBody)
.url("${DeviceManager.serverAddress}$endpoint").addHeader("Authorization", "Bearer ${DeviceManager.token}")
.build()
makeRequest(request, null, cb)
} catch(e: Exception) {
e.printStackTrace()
val jsobj = JSObject()
jsobj.put("error", "Request failed: ${e.message}")
cb(jsobj)
}
}
private fun makeRequest(request:Request, httpClient:OkHttpClient?, cb: (JSObject) -> Unit) {
val client = httpClient ?: defaultClient
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.d(tag, "FAILURE TO CONNECT")
@ -86,6 +121,13 @@ class ApiHandler(var ctx:Context) {
override fun onResponse(call: Call, response: Response) {
response.use {
if (it.code == 401) {
// Handle 401 Unauthorized by attempting token refresh
AbsLogger.info(tag, "makeRequest: 401 Unauthorized for request to \"${request.url}\" - attempt token refresh")
handleTokenRefresh(request, httpClient, cb)
return
}
if (!it.isSuccessful) {
val jsobj = JSObject()
jsobj.put("error", "Unexpected code $response")
@ -118,6 +160,269 @@ class ApiHandler(var ctx:Context) {
})
}
/**
* Handles token refresh when a 401 Unauthorized response is received
* This function will:
* 1. Get the refresh token from secure storage for the current server connection
* 2. Make a request to /auth/refresh endpoint with the refresh token
* 3. Update the stored tokens with the new access token
* 4. Retry the original request with the new access token
* 5. If refresh fails, handle logout
*
* @param originalRequest The original request that failed with 401
* @param httpClient The HTTP client to use for the request
* @param callback The callback to return the response
*/
private fun handleTokenRefresh(originalRequest: Request, httpClient: OkHttpClient?, callback: (JSObject) -> Unit) {
try {
AbsLogger.info(tag, "handleTokenRefresh: Attempting to refresh auth tokens for server ${DeviceManager.serverConnectionConfigString}")
// Get current server connection config ID
val serverConnectionConfigId = DeviceManager.serverConnectionConfigId
if (serverConnectionConfigId.isEmpty()) {
AbsLogger.error(tag, "handleTokenRefresh: Unable to refresh auth tokens. No server connection config ID")
val errorObj = JSObject()
errorObj.put("error", "No server connection available")
callback(errorObj)
return
}
// Get refresh token from secure storage
val refreshToken = secureStorage.getRefreshToken(serverConnectionConfigId)
if (refreshToken.isNullOrEmpty()) {
AbsLogger.error(tag, "handleTokenRefresh: Unable to refresh auth tokens. No refresh token available for server ${DeviceManager.serverConnectionConfigString}")
val errorObj = JSObject()
errorObj.put("error", "No refresh token available")
callback(errorObj)
return
}
Log.d(tag, "handleTokenRefresh: Retrieved refresh token, attempting to refresh access token")
// Create refresh token request
val refreshEndpoint = "${DeviceManager.serverAddress}/auth/refresh"
val refreshRequest = Request.Builder()
.url(refreshEndpoint)
.addHeader("x-refresh-token", refreshToken)
.addHeader("Content-Type", "application/json")
.post(EMPTY_REQUEST)
.build()
// Make the refresh request
val client = httpClient ?: defaultClient
client.newCall(refreshRequest).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e(tag, "handleTokenRefresh: Failed to connect to refresh endpoint", e)
AbsLogger.error(tag, "handleTokenRefresh: Failed to connect to refresh endpoint for server ${DeviceManager.serverConnectionConfigString} (error: ${e.message})")
handleRefreshFailure(callback)
}
override fun onResponse(call: Call, response: Response) {
response.use {
if (!it.isSuccessful) {
AbsLogger.error(tag, "handleTokenRefresh: Refresh request failed with status ${it.code} for server ${DeviceManager.serverConnectionConfigString}")
handleRefreshFailure(callback)
return
}
val bodyString = it.body!!.string()
try {
val responseJson = JSONObject(bodyString)
val userObj = responseJson.optJSONObject("user")
if (userObj == null) {
AbsLogger.error(tag, "handleTokenRefresh: No user object in refresh response for server ${DeviceManager.serverConnectionConfigString}")
handleRefreshFailure(callback)
return
}
val newAccessToken = userObj.optString("accessToken")
val newRefreshToken = userObj.optString("refreshToken")
if (newAccessToken.isEmpty()) {
AbsLogger.error(tag, "handleTokenRefresh: No access token in refresh response for server ${DeviceManager.serverConnectionConfigString}")
handleRefreshFailure(callback)
return
}
Log.d(tag, "handleTokenRefresh: Successfully obtained new access token")
// Update tokens in secure storage and device manager
updateTokens(newAccessToken, newRefreshToken.ifEmpty { refreshToken }, serverConnectionConfigId)
// Retry the original request with the new access token
Log.d(tag, "handleTokenRefresh: Retrying original request with new token")
retryOriginalRequest(originalRequest, newAccessToken, httpClient, callback)
} catch (e: Exception) {
Log.e(tag, "handleTokenRefresh: Failed to parse refresh response", e)
AbsLogger.error(tag, "handleTokenRefresh: Failed to parse refresh response for server ${DeviceManager.serverConnectionConfigString} (error: ${e.message})")
handleRefreshFailure(callback)
}
}
}
})
} catch (e: Exception) {
Log.e(tag, "handleTokenRefresh: Unexpected error during token refresh", e)
handleRefreshFailure(callback)
}
}
/**
* Updates the stored tokens with new access and refresh tokens
*
* @param newAccessToken The new access token
* @param newRefreshToken The new refresh token (or existing one if not provided)
*/
private fun updateTokens(newAccessToken: String, newRefreshToken: String, serverConnectionConfigId: String) {
try {
// Update the refresh token in secure storage if it's new
if (newRefreshToken != secureStorage.getRefreshToken(serverConnectionConfigId)) {
secureStorage.storeRefreshToken(serverConnectionConfigId, newRefreshToken)
Log.d(tag, "updateTokens: Updated refresh token in secure storage")
}
// Update the access token in the current server connection config
DeviceManager.serverConnectionConfig?.let { config ->
config.token = newAccessToken
DeviceManager.dbManager.saveDeviceData(DeviceManager.deviceData)
Log.d(tag, "updateTokens: Updated access token in server connection config")
}
// Send access token to Webview frontend
if (checkAbsDatabaseNotifyListenersInitted()) {
val tokenJsObject = JSObject()
tokenJsObject.put("accessToken", newAccessToken)
absDatabaseNotifyListeners("onTokenRefresh", tokenJsObject)
} else {
// Can happen if Webview is never run
Log.i(tag, "AbsDatabaseNotifyListeners is not initialized so cannot send new access token")
}
AbsLogger.info(tag, "updateTokens: Successfully refreshed auth tokens for server ${DeviceManager.serverConnectionConfigString}")
} catch (e: Exception) {
Log.e(tag, "updateTokens: Failed to update tokens", e)
AbsLogger.error(tag, "updateTokens: Failed to refresh auth tokens for server ${DeviceManager.serverConnectionConfigString} (error: ${e.message})")
}
}
/**
* Retries the original request with the new access token
*
* @param originalRequest The original request to retry
* @param newAccessToken The new access token to use
* @param httpClient The HTTP client to use
* @param callback The callback to return the response
*/
private fun retryOriginalRequest(originalRequest: Request, newAccessToken: String, httpClient: OkHttpClient?, callback: (JSObject) -> Unit) {
try {
// Create a new request with the updated authorization header
val newRequest = originalRequest.newBuilder()
.removeHeader("Authorization")
.addHeader("Authorization", "Bearer $newAccessToken")
.build()
Log.d(tag, "retryOriginalRequest: Retrying request to ${newRequest.url}")
// Make the retry request
val client = httpClient ?: defaultClient
client.newCall(newRequest).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e(tag, "retryOriginalRequest: Failed to retry request", e)
AbsLogger.error(tag, "retryOriginalRequest: Failed to retry request after token refresh for server ${DeviceManager.serverConnectionConfigString} (error: ${e.message})")
val errorObj = JSObject()
errorObj.put("error", "Failed to retry request after token refresh")
callback(errorObj)
}
override fun onResponse(call: Call, response: Response) {
response.use {
if (!it.isSuccessful) {
Log.e(tag, "retryOriginalRequest: Retry request failed with status ${it.code}")
AbsLogger.error(tag, "retryOriginalRequest: Retry request failed with status ${it.code} for server ${DeviceManager.serverConnectionConfigString}")
val errorObj = JSObject()
errorObj.put("error", "Retry request failed with status ${it.code}")
callback(errorObj)
return
}
val bodyString = it.body!!.string()
if (bodyString == "OK") {
callback(JSObject())
} else {
try {
var jsonObj = JSObject()
if (bodyString.startsWith("[")) {
val array = JSArray(bodyString)
jsonObj.put("value", array)
} else {
jsonObj = JSObject(bodyString)
}
callback(jsonObj)
} catch(je:JSONException) {
Log.e(tag, "retryOriginalRequest: Invalid JSON response ${je.localizedMessage} from body $bodyString")
val errorObj = JSObject()
errorObj.put("error", "Invalid response body")
callback(errorObj)
}
}
}
}
})
} catch (e: Exception) {
Log.e(tag, "retryOriginalRequest: Unexpected error during retry", e)
AbsLogger.error(tag, "retryOriginalRequest: Unexpected error during retry for server ${DeviceManager.serverConnectionConfigString}")
val errorObj = JSObject()
errorObj.put("error", "Failed to retry request")
callback(errorObj)
}
}
/**
* Handles the case when token refresh fails
* This will clear the current session and notify the callback
*
* @param callback The callback to return the error
*/
private fun handleRefreshFailure(callback: (JSObject) -> Unit) {
try {
Log.d(tag, "handleRefreshFailure: Token refresh failed, clearing session")
// Clear the current server connection
DeviceManager.serverConnectionConfig = null
DeviceManager.deviceData.lastServerConnectionConfigId = null
DeviceManager.dbManager.saveDeviceData(DeviceManager.deviceData)
// Remove refresh token from secure storage
val serverConnectionConfigId = DeviceManager.serverConnectionConfigId
if (serverConnectionConfigId.isNotEmpty()) {
secureStorage.removeRefreshToken(serverConnectionConfigId)
}
val errorObj = JSObject()
errorObj.put("error", "Authentication failed - please login again")
callback(errorObj)
if (checkAbsDatabaseNotifyListenersInitted()) {
val tokenJsObject = JSObject()
tokenJsObject.put("error", "Token refresh failed")
if (serverConnectionConfigId.isNotEmpty()) {
tokenJsObject.put("serverConnectionConfigId", serverConnectionConfigId)
}
absDatabaseNotifyListeners("onTokenRefreshFailure", tokenJsObject)
} else {
// Can happen if Webview is never run
Log.i(tag, "AbsDatabaseNotifyListeners is not initialized so cannot send token refresh failure notification")
}
} catch (e: Exception) {
Log.e(tag, "handleRefreshFailure: Error during failure handling", e)
val errorObj = JSObject()
errorObj.put("error", "Authentication failed")
callback(errorObj)
}
}
fun getCurrentUser(cb: (User?) -> Unit) {
getRequest("/api/me", null, null) {
if (it.has("error")) {
@ -132,7 +437,7 @@ class ApiHandler(var ctx:Context) {
fun getLibraries(cb: (List<Library>) -> Unit) {
val mapper = jacksonMapper
getRequest("/api/libraries", null,null) {
getRequest("/api/libraries?include=stats", null,null) {
val libraries = mutableListOf<Library>()
var array = JSONArray()
@ -149,6 +454,23 @@ class ApiHandler(var ctx:Context) {
}
}
fun getLibraryPersonalized(libraryItemId:String, cb: (List<LibraryShelfType>?) -> Unit) {
getRequest("/api/libraries/$libraryItemId/personalized", null, null) {
if (it.has("error")) {
Log.e(tag, it.getString("error") ?: "getLibraryStats Failed")
cb(null)
} else {
val items = mutableListOf<LibraryShelfType>()
val array = it.getJSONArray("value")
for (i in 0 until array.length()) {
val item = jacksonMapper.readValue<LibraryShelfType>(array.get(i).toString())
items.add(item)
}
cb(items)
}
}
}
fun getLibraryItem(libraryItemId:String, cb: (LibraryItem?) -> Unit) {
getRequest("/api/items/$libraryItemId?expanded=1", null, null) {
if (it.has("error")) {
@ -189,6 +511,103 @@ class ApiHandler(var ctx:Context) {
}
}
fun getLibrarySeries(libraryId:String, cb: (List<LibrarySeriesItem>) -> Unit) {
Log.d(tag, "Getting series")
getRequest("/api/libraries/$libraryId/series?minified=1&sort=name&limit=10000", null, null) {
val items = mutableListOf<LibrarySeriesItem>()
if (it.has("results")) {
val array = it.getJSONArray("results")
for (i in 0 until array.length()) {
val item = jacksonMapper.readValue<LibrarySeriesItem>(array.get(i).toString())
items.add(item)
}
}
cb(items)
}
}
fun getLibrarySeriesItems(libraryId:String, seriesId:String, cb: (List<LibraryItem>) -> Unit) {
Log.d(tag, "Getting items for series")
val seriesIdBase64 = Base64.encodeToString(seriesId.toByteArray(), Base64.DEFAULT)
getRequest("/api/libraries/$libraryId/items?minified=1&sort=media.metadata.title&filter=series.${seriesIdBase64}&limit=1000", null, null) {
val items = mutableListOf<LibraryItem>()
if (it.has("results")) {
val array = it.getJSONArray("results")
for (i in 0 until array.length()) {
val item = jacksonMapper.readValue<LibraryItem>(array.get(i).toString())
items.add(item)
}
}
cb(items)
}
}
fun getLibraryAuthors(libraryId:String, cb: (List<LibraryAuthorItem>) -> Unit) {
Log.d(tag, "Getting series")
getRequest("/api/libraries/$libraryId/authors", null, null) {
val items = mutableListOf<LibraryAuthorItem>()
if (it.has("authors")) {
val array = it.getJSONArray("authors")
for (i in 0 until array.length()) {
val item = jacksonMapper.readValue<LibraryAuthorItem>(array.get(i).toString())
items.add(item)
}
}else{
Log.e(tag, "No results")
}
cb(items)
}
}
fun getLibraryItemsFromAuthor(libraryId:String, authorId:String, cb: (List<LibraryItem>) -> Unit) {
Log.d(tag, "Getting author items")
val authorIdBase64 = Base64.encodeToString(authorId.toByteArray(), Base64.DEFAULT)
getRequest("/api/libraries/$libraryId/items?limit=1000&minified=1&filter=authors.${authorIdBase64}&sort=media.metadata.title&collapseseries=1", null, null) {
val items = mutableListOf<LibraryItem>()
if (it.has("results")) {
val array = it.getJSONArray("results")
for (i in 0 until array.length()) {
val item = jacksonMapper.readValue<LibraryItem>(array.get(i).toString())
if (item.collapsedSeries != null) {
item.collapsedSeries?.libraryId = libraryId
}
items.add(item)
}
}else{
Log.e(tag, "No results")
}
cb(items)
}
}
fun getLibraryCollections(libraryId:String, cb: (List<LibraryCollection>) -> Unit) {
Log.d(tag, "Getting collections")
getRequest("/api/libraries/$libraryId/collections?minified=1&sort=name&limit=1000", null, null) {
val items = mutableListOf<LibraryCollection>()
if (it.has("results")) {
val array = it.getJSONArray("results")
for (i in 0 until array.length()) {
val item = jacksonMapper.readValue<LibraryCollection>(array.get(i).toString())
items.add(item)
}
}
cb(items)
}
}
fun getSearchResults(libraryId:String, queryString:String, cb: (LibraryItemSearchResultType?) -> Unit) {
Log.d(tag, "Doing search for library $libraryId")
getRequest("/api/libraries/$libraryId/search?q=$queryString", null, null) {
if (it.has("error")) {
Log.e(tag, it.getString("error") ?: "getSearchResults Failed")
cb(null)
} else {
val librarySearchResults = jacksonMapper.readValue<LibraryItemSearchResultType>(it.toString())
cb(librarySearchResults)
}
}
}
fun getAllItemsInProgress(cb: (List<ItemInProgress>) -> Unit) {
getRequest("/api/me/items-in-progress", null, null) {
val items = mutableListOf<ItemInProgress>()
@ -196,8 +615,7 @@ class ApiHandler(var ctx:Context) {
val array = it.getJSONArray("libraryItems")
for (i in 0 until array.length()) {
val jsobj = array.get(i) as JSONObject
val itemInProgress = ItemInProgress.makeFromServerObject(jsobj)
val itemInProgress = ItemInProgress.makeFromServerObject(jsobj, jacksonMapper)
items.add(itemInProgress)
}
}
@ -297,9 +715,9 @@ class ApiHandler(var ctx:Context) {
}
}
fun closePlaybackSession(playbackSessionId:String, cb: (Boolean) -> Unit) {
fun closePlaybackSession(playbackSessionId:String, config:ServerConnectionConfig?, cb: (Boolean) -> Unit) {
Log.d(tag, "closePlaybackSession: playbackSessionId=$playbackSessionId")
postRequest("/api/session/$playbackSessionId/close", null, null) {
postRequest("/api/session/$playbackSessionId/close", null, config) {
cb(true)
}
}
@ -331,22 +749,27 @@ class ApiHandler(var ctx:Context) {
val deviceInfo = DeviceInfo(deviceId, Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
val payload = JSObject(jacksonMapper.writeValueAsString(LocalSessionsSyncRequestPayload(playbackSessions, deviceInfo)))
Log.d(tag, "Sending ${playbackSessions.size} saved local playback sessions to server")
AbsLogger.info("ApiHandler", "sendSyncLocalSessions: Sending ${playbackSessions.size} saved local playback sessions to server (${DeviceManager.serverConnectionConfigName})")
postRequest("/api/session/local-all", payload, null) {
if (!it.getString("error").isNullOrEmpty()) {
Log.e(tag, "Failed to sync local sessions")
AbsLogger.error("ApiHandler", "sendSyncLocalSessions: Failed to sync local sessions. (${it.getString("error")})")
cb(false, it.getString("error"))
} else {
val response = jacksonMapper.readValue<LocalSessionsSyncResponsePayload>(it.toString())
response.results.forEach { localSessionSyncResult ->
Log.d(tag, "Synced session result ${localSessionSyncResult.id}|${localSessionSyncResult.progressSynced}|${localSessionSyncResult.success}")
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")
AbsLogger.info("ApiHandler", "sendSyncLocalSessions: Synced session \"${session.displayTitle}\" with server, server progress was updated for item ${session.mediaItemId}")
} else if (!localSessionSyncResult.success) {
Log.e(tag, "Failed to sync session ${session.displayTitle} with server. Error: ${localSessionSyncResult.error}")
AbsLogger.error("ApiHandler", "sendSyncLocalSessions: Failed to sync session \"${session.displayTitle}\" with server. Error: ${localSessionSyncResult.error}")
} else {
AbsLogger.info("ApiHandler", "sendSyncLocalSessions: Synced session \"${session.displayTitle}\" with server. Server progress was up-to-date for item ${session.mediaItemId}")
}
}
}
@ -356,37 +779,72 @@ class ApiHandler(var ctx:Context) {
}
fun syncLocalMediaProgressForUser(cb: () -> Unit) {
AbsLogger.info("ApiHandler", "[ApiHandler] syncLocalMediaProgressForUser: Server connection ${DeviceManager.serverConnectionConfigName}")
// 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")
AbsLogger.info("ApiHandler", "[ApiHandler] syncLocalMediaProgressForUser: No local media progress to sync")
return cb()
}
getCurrentUser { _user ->
_user?.let { user->
AbsLogger.info("ApiHandler", "syncLocalMediaProgressForUser: Found ${allLocalMediaProgress.size} local media progress")
getCurrentUser { user ->
if (user == null) {
AbsLogger.error("ApiHandler", "syncLocalMediaProgressForUser: Failed to load user from server (${DeviceManager.serverConnectionConfigName})")
} else {
var numLocalMediaProgressUptToDate = 0
var numLocalMediaProgressUpdated = 0
// 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}")
val updateLogs = mutableListOf<String>()
if (mediaProgress.progress != localMediaProgress.progress) {
updateLogs.add("Updated progress from ${localMediaProgress.progress} to ${mediaProgress.progress}")
}
if (mediaProgress.currentTime != localMediaProgress.currentTime) {
updateLogs.add("Updated currentTime from ${localMediaProgress.currentTime} to ${mediaProgress.currentTime}")
}
if (mediaProgress.isFinished != localMediaProgress.isFinished) {
updateLogs.add("Updated isFinished from ${localMediaProgress.isFinished} to ${mediaProgress.isFinished}")
}
if (mediaProgress.ebookProgress != localMediaProgress.ebookProgress) {
updateLogs.add("Updated ebookProgress from ${localMediaProgress.isFinished} to ${mediaProgress.isFinished}")
}
if (updateLogs.isNotEmpty()) {
AbsLogger.info("ApiHandler", "syncLocalMediaProgressForUser: Server progress for item \"${mediaProgress.mediaItemId}\" is more recent than local (server lastUpdate=${mediaProgress.lastUpdate}, local lastUpdate=${localMediaProgress.lastUpdate}). ${updateLogs.joinToString()}")
}
localMediaProgress.updateFromServerMediaProgress(mediaProgress)
MediaEventManager.syncEvent(mediaProgress, "Sync on server connection")
// Only report sync if progress changed
if (updateLogs.isNotEmpty()) {
MediaEventManager.syncEvent(mediaProgress, "Sync on server connection")
}
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
numLocalMediaProgressUpdated++
} else if (localMediaProgress.lastUpdate > mediaProgress.lastUpdate && localMediaProgress.ebookLocation != null && localMediaProgress.ebookLocation != mediaProgress.ebookLocation) {
// Patch ebook progress to server
AbsLogger.info("ApiHandler", "syncLocalMediaProgressForUser: Local progress for ebook item \"${mediaProgress.mediaItemId}\" is more recent than server progress. Local progress last updated ${localMediaProgress.lastUpdate}, server progress last updated ${mediaProgress.lastUpdate}. Sending server request to update ebook progress from ${mediaProgress.ebookProgress} to ${localMediaProgress.ebookProgress}")
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")
AbsLogger.info("ApiHandler", "syncLocalMediaProgressForUser: Successfully updated server ebook progress for item item \"${mediaProgress.mediaItemId}\"")
}
} else {
numLocalMediaProgressUptToDate++
}
}
}
AbsLogger.info("ApiHandler", "syncLocalMediaProgressForUser: Finishing syncing local media progress with server. $numLocalMediaProgressUptToDate up-to-date, $numLocalMediaProgressUpdated updated")
}
cb()
}

View file

@ -0,0 +1 @@
<!-- drawable/clock_outline.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z" /></vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 798 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 707 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="27dp"
android:height="32dp"
android:viewportWidth="27"
android:viewportHeight="32">
<path
android:pathData="M26.719,15.761c-0.165,-0.138 -0.422,-0.341 -0.77,-0.581v-2.703c0,-6.891 -5.586,-12.477 -12.478,-12.477v0c-6.891,0 -12.477,5.586 -12.477,12.477v2.703c-0.348,0.241 -0.605,0.443 -0.77,0.581 -0.137,0.114 -0.223,0.285 -0.223,0.476 0,0 0,0 0,0.001v-0,3.238c0,0 0,0.001 0,0.001 0,0.191 0.086,0.361 0.222,0.475l0.001,0.001c0.385,0.321 1.269,0.993 2.645,1.683v0.315c0,0.849 0.548,1.537 1.223,1.537v0c0.675,0 1.223,-0.688 1.223,-1.537v-7.767c0,-0.849 -0.548,-1.537 -1.223,-1.537v0c-0.647,0 -1.177,0.632 -1.22,1.431l-0.003,0.002v-1.601c0,-5.856 4.747,-10.602 10.603,-10.602v0c5.856,0 10.603,4.747 10.603,10.602v1.601l-0.003,-0.002c-0.043,-0.799 -0.573,-1.431 -1.22,-1.431v0c-0.675,0 -1.223,0.688 -1.223,1.537v7.766c0,0.849 0.548,1.537 1.223,1.537v0c0.676,0 1.223,-0.688 1.223,-1.537v-0.315c1.376,-0.69 2.26,-1.362 2.645,-1.683 0.137,-0.114 0.223,-0.285 0.223,-0.476 0,-0 0,-0.001 0,-0.001v0,-3.237c0,-0 0,-0 0,-0 0,-0.191 -0.086,-0.361 -0.222,-0.475l-0.001,-0.001zM9.12,29.262c0.816,0 1.477,-0.661 1.477,-1.477v0,-16.543c0,-0 0,-0 0,-0 0,-0.816 -0.661,-1.477 -1.477,-1.477h-1.526c-0.816,0 -1.478,0.662 -1.478,1.478v0,16.543c0,0.816 0.661,1.477 1.477,1.477 0,0 0,0 0,0v0zM6.673,13.731h3.368v0.352h-3.368zM14.234,29.262c0.816,0 1.477,-0.661 1.477,-1.477v0,-16.543c0,-0 0,-0 0,-0 0,-0.816 -0.661,-1.477 -1.477,-1.477h-1.526c-0.816,0 -1.478,0.662 -1.478,1.478v0,16.543c0,0.816 0.661,1.477 1.477,1.477 0,0 0,0 0,0v0zM11.787,13.731h3.367v0.352h-3.367zM19.348,29.262c0.816,0 1.477,-0.661 1.477,-1.477v0,-16.543c0,-0 0,-0 0,-0 0,-0.816 -0.661,-1.477 -1.477,-1.477h-1.526c-0.816,0 -1.478,0.662 -1.478,1.478v0,16.543c0,0.816 0.661,1.477 1.477,1.477 0,0 0,0 0,0v0zM16.901,13.731h3.367v0.352h-3.367zM3.566,29.773h19.81c0.615,0 1.113,0.498 1.113,1.113v0c0,0.615 -0.498,1.113 -1.113,1.113h-19.81c-0.615,0 -1.113,-0.498 -1.113,-1.113v-0c0,-0.615 0.498,-1.113 1.113,-1.113z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M16,7.333c2.577,0 4.667,2.089 4.667,4.667v0c0,2.577 -2.089,4.667 -4.667,4.667v0c-2.577,0 -4.667,-2.089 -4.667,-4.667v0c0,-2.577 2.089,-4.667 4.667,-4.667v0zM6.667,10.667c0.747,0 1.44,0.2 2.04,0.56 -0.2,1.907 0.36,3.8 1.507,5.28 -0.667,1.28 -2,2.16 -3.547,2.16 -2.209,0 -4,-1.791 -4,-4v0c0,-2.209 1.791,-4 4,-4v0zM25.333,10.667c2.209,0 4,1.791 4,4v0c0,2.209 -1.791,4 -4,4v0c-1.547,0 -2.88,-0.88 -3.547,-2.16 1.147,-1.48 1.707,-3.373 1.507,-5.28 0.6,-0.36 1.293,-0.56 2.04,-0.56zM7.333,24.333c0,-2.76 3.88,-5 8.667,-5s8.667,2.24 8.667,5v2.333h-17.333v-2.333zM0,26.667v-2c0,-1.853 2.52,-3.413 5.933,-3.867 -0.787,0.907 -1.267,2.16 -1.267,3.533v2.333h-4.667zM32,26.667h-4.667v-2.333c0,-1.373 -0.48,-2.627 -1.267,-3.533 3.413,0.453 5.933,2.013 5.933,3.867v2z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M28,4v26h-21c-1.657,0 -3,-1.343 -3,-3s1.343,-3 3,-3h19v-24h-20c-2.2,0 -4,1.8 -4,4v24c0,2.2 1.8,4 4,4h24v-28h-2zM7.002,26v0c-0.001,0 -0.001,0 -0.002,0 -0.552,0 -1,0.448 -1,1s0.448,1 1,1c0.001,0 0.001,-0 0.002,-0v0h18.997v-2h-18.997z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M12,4v20h4v-20h-4zM16,6.667l5.333,17.333 4,-1.333 -5.333,-17.333 -4,1.333zM6.667,6.667v17.333h4v-17.333h-4zM4,25.333v2.667h24v-2.667h-24z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="32dp"
android:viewportWidth="36"
android:viewportHeight="32">
<path
android:pathData="M7,4h-6c-0.55,0 -1,0.45 -1,1v22c0,0.55 0.45,1 1,1h6c0.55,0 1,-0.45 1,-1v-22c0,-0.55 -0.45,-1 -1,-1zM6,10h-4v-2h4v2zM17,4h-6c-0.55,0 -1,0.45 -1,1v22c0,0.55 0.45,1 1,1h6c0.55,0 1,-0.45 1,-1v-22c0,-0.55 -0.45,-1 -1,-1zM16,10h-4v-2h4v2zM23.909,5.546l-5.358,2.7c-0.491,0.247 -0.691,0.852 -0.443,1.343l8.999,17.861c0.247,0.491 0.852,0.691 1.343,0.443l5.358,-2.7c0.491,-0.247 0.691,-0.852 0.443,-1.343l-8.999,-17.861c-0.247,-0.491 -0.852,-0.691 -1.343,-0.443z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M19.2,6h-6.4v19.2h6.4v-19.2zM22.4,6v19.2h6.4v-19.2h-6.4zM9.6,6h-6.4v19.2h6.4v-19.2zM0,2.8h32v25.6h-32v-25.6z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="27dp"
android:height="32dp"
android:viewportWidth="27"
android:viewportHeight="32">
<path
android:pathData="M13.68,0c7.518,0 13.612,2.854 13.612,6.37 0,3.518 -6.096,6.37 -13.612,6.37s-13.612,-2.854 -13.612,-6.37c0,-3.516 6.096,-6.37 13.612,-6.37v0zM0.068,21.31v4.891c2.422,8.602 26.349,6.94 27.227,-0.44v-4.885c-1.195,8.102 -25.313,8.685 -27.227,0.435v0,0zM0,8.578v4.776c2.422,8.401 26.482,7.266 27.362,0.06v-4.773c-1.198,7.914 -25.448,7.995 -27.362,-0.063v0zM0,14.75v4.891c2.422,8.602 26.482,7.44 27.362,0.06v-4.885c-1.198,8.102 -25.448,8.185 -27.362,-0.065v0z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M26,28h-20v-4l6,-10 8.219,10 5.781,-4v8zM26,15c0,1.657 -1.343,3 -3,3s-3,-1.343 -3,-3 1.343,-3 3,-3c1.657,0 3,1.343 3,3zM28.681,7.159c-0.694,-0.947 -1.662,-2.053 -2.724,-3.116s-2.169,-2.03 -3.116,-2.724c-1.612,-1.182 -2.393,-1.319 -2.841,-1.319h-15.5c-1.378,0 -2.5,1.121 -2.5,2.5v27c0,1.378 1.122,2.5 2.5,2.5h23c1.378,0 2.5,-1.122 2.5,-2.5v-19.5c0,-0.448 -0.137,-1.23 -1.319,-2.841zM24.543,5.457c0.959,0.959 1.712,1.825 2.268,2.543h-4.811v-4.811c0.718,0.556 1.584,1.309 2.543,2.268zM28,29.5c0,0.271 -0.229,0.5 -0.5,0.5h-23c-0.271,0 -0.5,-0.229 -0.5,-0.5v-27c0,-0.271 0.229,-0.5 0.5,-0.5 0,0 15.499,-0 15.5,0v7c0,0.552 0.448,1 1,1h7v19.5z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M9,18h-2v14h2c0.55,0 1,-0.45 1,-1v-12c0,-0.55 -0.45,-1 -1,-1zM23,18c-0.55,0 -1,0.45 -1,1v12c0,0.55 0.45,1 1,1h2v-14h-2zM32,16c0,-8.837 -7.163,-16 -16,-16s-16,7.163 -16,16c0,1.919 0.338,3.759 0.958,5.464 -0.609,1.038 -0.958,2.246 -0.958,3.536 0,3.526 2.608,6.443 6,6.929v-13.857c-0.997,0.143 -1.927,0.495 -2.742,1.012 -0.168,-0.835 -0.258,-1.699 -0.258,-2.584 0,-7.18 5.82,-13 13,-13s13,5.82 13,13c0,0.885 -0.088,1.749 -0.257,2.584 -0.816,-0.517 -1.745,-0.87 -2.743,-1.013v13.858c3.392,-0.485 6,-3.402 6,-6.929 0,-1.29 -0.349,-2.498 -0.958,-3.536 0.62,-1.705 0.958,-3.545 0.958,-5.465z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M23.6,2c-3.363,0 -6.258,2.736 -7.599,5.594 -1.342,-2.858 -4.237,-5.594 -7.601,-5.594 -4.637,0 -8.4,3.764 -8.4,8.401 0,9.433 9.516,11.906 16.001,21.232 6.13,-9.268 15.999,-12.1 15.999,-21.232 0,-4.637 -3.763,-8.401 -8.4,-8.401z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="32dp"
android:viewportWidth="24"
android:viewportHeight="32">
<path
android:pathData="M6,6v10c0,3.313 2.688,6 6,6s6,-2.688 6,-6h-5c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h5v-2h-5c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h5v-2h-5c-0.55,0 -1,-0.45 -1,-1s0.45,-1 1,-1h5c0,-3.313 -2.688,-6 -6,-6s-6,2.688 -6,6zM20,15v1c0,4.419 -3.581,8 -8,8s-8,-3.581 -8,-8v-2.5c0,-0.831 -0.669,-1.5 -1.5,-1.5s-1.5,0.669 -1.5,1.5v2.5c0,5.569 4.138,10.169 9.5,10.9v2.1h-3c-0.831,0 -1.5,0.669 -1.5,1.5s0.669,1.5 1.5,1.5h9c0.831,0 1.5,-0.669 1.5,-1.5s-0.669,-1.5 -1.5,-1.5h-3v-2.1c5.363,-0.731 9.5,-5.331 9.5,-10.9v-2.5c0,-0.831 -0.669,-1.5 -1.5,-1.5s-1.5,0.669 -1.5,1.5v1.5z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="32dp"
android:viewportWidth="22"
android:viewportHeight="32">
<path
android:pathData="M22.292,14.872c0,-0.836 -0.677,-1.51 -1.51,-1.51 -0.836,0 -1.51,0.677 -1.51,1.51 0,2.977 -0.714,5.435 -2.159,7.076 -1.31,1.484 -3.289,2.341 -5.964,2.341s-4.654,-0.854 -5.964,-2.339c-1.448,-1.641 -2.159,-4.099 -2.159,-7.078 0,-0.836 -0.677,-1.51 -1.51,-1.51 -0.836,0 -1.51,0.677 -1.51,1.51 0,3.711 0.961,6.857 2.914,9.073 1.438,1.63 3.375,2.734 5.815,3.164v2.479h-3.703c-0.661,0 -1.203,0.542 -1.203,1.203v1.206h14.646v-1.206c0,-0.661 -0.542,-1.203 -1.203,-1.203h-3.711v-2.479c2.44,-0.432 4.375,-1.534 5.815,-3.167 1.953,-2.214 2.917,-5.359 2.917,-9.07v0,0zM11.146,0c3.083,0 5.604,2.523 5.604,5.604v0.146h-3.013v3.57h3.016v1.818h-3.016v3.57h3.016v1.284c0,3.083 -2.523,5.604 -5.604,5.604 -3.083,0 -5.604,-2.523 -5.604,-5.604v-1.284h3.016v-3.57h-3.018v-1.818h3.016v-3.57h-3.016v-0.146c0,-3.081 2.521,-5.604 5.604,-5.604v0z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M15,22c2.761,0 5,-2.239 5,-5v-12c0,-2.761 -2.239,-5 -5,-5s-5,2.239 -5,5v12c0,2.761 2.239,5 5,5zM22,14v3c0,3.866 -3.134,7 -7,7s-7,-3.134 -7,-7v-3h-2v3c0,4.632 3.5,8.447 8,8.944v4.056h-4v2h10v-2h-4v-4.056c4.5,-0.497 8,-4.312 8,-8.944v-3h-2z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M30,0h2v23c0,2.761 -3.134,5 -7,5s-7,-2.239 -7,-5c0,-2.761 3.134,-5 7,-5 1.959,0 3.729,0.575 5,1.501v-11.501l-16,3.556v15.444c0,2.761 -3.134,5 -7,5s-7,-2.239 -7,-5c0,-2.761 3.134,-5 7,-5 1.959,0 3.729,0.575 5,1.501v-19.501l18,-4z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="33dp"
android:height="32dp"
android:viewportWidth="33"
android:viewportHeight="32">
<path
android:pathData="M18.289,18.549v13.45h-3.973v-13.45c-1.305,-0.706 -2.191,-2.087 -2.191,-3.676 0,-2.307 1.871,-4.178 4.178,-4.178s4.178,1.871 4.178,4.178c0,1.589 -0.887,2.97 -2.192,3.676v0zM10.434,2.963c0.774,-0.382 1.091,-1.319 0.709,-2.091s-1.318,-1.091 -2.091,-0.71c-3.158,1.558 -5.613,4.134 -7.165,7.169 -1.146,2.241 -1.8,4.734 -1.879,7.251 -0.079,2.538 0.421,5.101 1.585,7.46 1.433,2.906 3.863,5.491 7.44,7.322 0.767,0.392 1.706,0.088 2.098,-0.679s0.088,-1.706 -0.679,-2.097c-2.929,-1.498 -4.905,-3.589 -6.059,-5.926 -0.93,-1.887 -1.33,-3.942 -1.267,-5.981 0.064,-2.058 0.599,-4.098 1.536,-5.93 1.256,-2.455 3.234,-4.536 5.771,-5.786v0zM23.554,0.161c-0.773,-0.382 -1.71,-0.064 -2.091,0.71s-0.064,1.71 0.71,2.091c2.537,1.251 4.515,3.332 5.771,5.787 0.937,1.832 1.472,3.871 1.536,5.93 0.064,2.038 -0.336,4.094 -1.267,5.981 -1.153,2.337 -3.13,4.428 -6.059,5.926 -0.767,0.392 -1.07,1.331 -0.679,2.097s1.331,1.071 2.098,0.679c3.577,-1.83 6.007,-4.415 7.44,-7.322 1.164,-2.359 1.664,-4.922 1.585,-7.46 -0.079,-2.516 -0.732,-5.01 -1.878,-7.251 -1.552,-3.034 -4.008,-5.611 -7.165,-7.169v0zM21.968,7.033c-0.582,-0.42 -1.395,-0.287 -1.814,0.295s-0.287,1.395 0.295,1.814c0.235,0.169 0.465,0.359 0.691,0.566 1.319,1.216 2.101,2.838 2.266,4.553 0.166,1.731 -0.295,3.566 -1.464,5.188 -0.198,0.276 -0.427,0.555 -0.686,0.836 -0.487,0.529 -0.453,1.353 0.076,1.839s1.353,0.452 1.84,-0.076c0.313,-0.339 0.607,-0.701 0.88,-1.081 1.555,-2.16 2.167,-4.617 1.943,-6.951 -0.225,-2.349 -1.293,-4.566 -3.091,-6.224 -0.283,-0.261 -0.595,-0.514 -0.935,-0.76v0zM12.156,9.143c0.582,-0.42 0.715,-1.232 0.295,-1.814s-1.232,-0.715 -1.814,-0.296c-0.341,0.246 -0.652,0.499 -0.935,0.76 -1.799,1.658 -2.866,3.875 -3.091,6.224 -0.224,2.334 0.387,4.792 1.943,6.952 0.274,0.379 0.567,0.741 0.88,1.08 0.487,0.529 1.311,0.563 1.839,0.076s0.563,-1.311 0.076,-1.839c-0.259,-0.281 -0.487,-0.56 -0.686,-0.836 -1.169,-1.623 -1.63,-3.457 -1.464,-5.188 0.164,-1.715 0.947,-3.337 2.266,-4.553 0.226,-0.207 0.456,-0.397 0.691,-0.566v0z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M30.925,2.938c0.794,-0.231 1.25,-1.069 1.019,-1.863s-1.069,-1.25 -1.869,-1.012l-26.844,7.869c-0.587,0.169 -1.119,0.456 -1.569,0.825 -1.006,0.725 -1.663,1.906 -1.663,3.244v16c0,2.206 1.794,4 4,4h24c2.206,0 4,-1.794 4,-4v-16c0,-2.206 -1.794,-4 -4,-4h-14.344l17.269,-5.063zM23,25c-2.762,0 -5,-2.238 -5,-5s2.238,-5 5,-5 5,2.238 5,5 -2.238,5 -5,5zM5,16c0,-0.55 0.45,-1 1,-1h6c0.55,0 1,0.45 1,1s-0.45,1 -1,1h-6c-0.55,0 -1,-0.45 -1,-1zM4,20c0,-0.55 0.45,-1 1,-1h8c0.55,0 1,0.45 1,1s-0.45,1 -1,1h-8c-0.55,0 -1,-0.45 -1,-1zM5,24c0,-0.55 0.45,-1 1,-1h6c0.55,0 1,0.45 1,1s-0.45,1 -1,1h-6c-0.55,0 -1,-0.45 -1,-1z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M22,2l-10,10h-6l-6,8c0,0 6.357,-1.77 10.065,-0.94l-10.065,12.94 13.184,-10.255c1.839,4.208 -1.184,10.255 -1.184,10.255l8,-6v-6l10,-10 2,-10 -10,2z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M4.259,23.467c-2.35,0 -4.259,1.917 -4.259,4.252 0,2.349 1.909,4.244 4.259,4.244 2.358,0 4.265,-1.895 4.265,-4.244 -0,-2.336 -1.907,-4.252 -4.265,-4.252zM0.005,10.873v6.133c3.993,0 7.749,1.562 10.577,4.391 2.825,2.822 4.384,6.595 4.384,10.603h6.16c-0,-11.651 -9.478,-21.127 -21.121,-21.127zM0.012,0v6.136c14.243,0 25.836,11.604 25.836,25.864h6.152c0,-17.64 -14.352,-32 -31.988,-32z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M32,12.408l-11.056,-1.607 -4.944,-10.018 -4.944,10.018 -11.056,1.607 8,7.798 -1.889,11.011 9.889,-5.199 9.889,5.199 -1.889,-11.011 8,-7.798z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1 @@
<!-- drawable/account_outline.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" /></vector>

View file

@ -0,0 +1 @@
<!-- drawable/book_multiple_outline.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M19 2A2 2 0 0 1 21 4V16A2 2 0 0 1 19 18H9A2 2 0 0 1 7 16V4A2 2 0 0 1 9 2H19M19 4H16V10L13.5 7.75L11 10V4H9V16H19M3 20A2 2 0 0 0 5 22H17V20H5V6H3Z" /></vector>

View file

@ -0,0 +1 @@
<!-- drawable/book_open_blank_variant_outline.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M12 21.5C10.65 20.65 8.2 20 6.5 20C4.85 20 3.15 20.3 1.75 21.05C1.65 21.1 1.6 21.1 1.5 21.1C1.25 21.1 1 20.85 1 20.6V6C1.6 5.55 2.25 5.25 3 5C4.11 4.65 5.33 4.5 6.5 4.5C8.45 4.5 10.55 4.9 12 6C13.45 4.9 15.55 4.5 17.5 4.5C18.67 4.5 19.89 4.65 21 5C21.75 5.25 22.4 5.55 23 6V20.6C23 20.85 22.75 21.1 22.5 21.1C22.4 21.1 22.35 21.1 22.25 21.05C20.85 20.3 19.15 20 17.5 20C15.8 20 13.35 20.65 12 21.5M11 7.5C9.64 6.9 7.84 6.5 6.5 6.5C5.3 6.5 4.1 6.65 3 7V18.5C4.1 18.15 5.3 18 6.5 18C7.84 18 9.64 18.4 11 19V7.5M13 19C14.36 18.4 16.16 18 17.5 18C18.7 18 19.9 18.15 21 18.5V7C19.9 6.65 18.7 6.5 17.5 6.5C16.16 6.5 14.36 6.9 13 7.5V19Z" /></vector>

View file

@ -0,0 +1 @@
<!-- drawable/clock_outline.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z" /></vector>

View file

@ -0,0 +1 @@
<!-- drawable/telescope.xml --><vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24"><path android:fillColor="#FFFFFF" android:pathData="M21.9,8.9L20.2,9.9L16.2,3L17.9,2L21.9,8.9M9.8,7.9L12.8,13.1L18.9,9.6L15.9,4.4L9.8,7.9M11.4,12.7L9.4,9.2L5.1,11.7L7.1,15.2L11.4,12.7M2.1,14.6L3.1,16.3L5.7,14.8L4.7,13.1L2.1,14.6M12.1,14L11.8,13.6L7.5,16.1L7.8,16.5C8,16.8 8.3,17.1 8.6,17.3L7,22H9L10.4,17.7H10.5L12,22H14L12.1,16.4C12.6,15.7 12.6,14.8 12.1,14Z" /></vector>

View file

@ -7,6 +7,7 @@
tools:context=".MainActivity">
<WebView
android:id="@+id/webview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

Binary file not shown.

View file

@ -10,7 +10,7 @@
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
<item name="android:background">@color/background_dark</item>
<item name="android:statusBarColor">@color/background_dark</item>
<item name="android:navigationBarColor">@color/background_dark</item>
</style>

View file

@ -3,5 +3,5 @@
<color name="light_blue_200">#FF81D4FA</color>
<color name="light_blue_600">#FF039BE5</color>
<color name="light_blue_900">#FF01579B</color>
<color name="background_dark">#262626</color>
</resources>
<color name="background_dark">#232323</color>
</resources>

View file

@ -12,7 +12,7 @@
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
<item name="android:background">@color/background_dark</item>
<item name="android:statusBarColor">@color/background_dark</item>
<item name="android:navigationBarColor">@color/background_dark</item>
</style>

View file

@ -1,15 +1,15 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.7.20'
ext.kotlin_version = '2.0.0'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.google.gms:google-services:4.4.0'
classpath 'com.android.tools.build:gradle:8.1.1'
classpath 'com.google.gms:google-services:4.4.2'
classpath 'com.android.tools.build:gradle:8.8.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
@ -26,7 +26,7 @@ allprojects {
}
}
task clean(type: Delete) {
delete rootProject.buildDir
tasks.register('clean', Delete) {
delete rootProject.layout.buildDirectory
}

View file

@ -2,8 +2,14 @@
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
include ':byteowls-capacitor-filesharer'
project(':byteowls-capacitor-filesharer').projectDir = new File('../node_modules/@byteowls/capacitor-filesharer/android')
include ':webnativellc-capacitor-filesharer'
project(':webnativellc-capacitor-filesharer').projectDir = new File('../node_modules/@webnativellc/capacitor-filesharer/android')
include ':capacitor-community-keep-awake'
project(':capacitor-community-keep-awake').projectDir = new File('../node_modules/@capacitor-community/keep-awake/android')
include ':capacitor-community-volume-buttons'
project(':capacitor-community-volume-buttons').projectDir = new File('../node_modules/@capacitor-community/volume-buttons/android')
include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')

View file

@ -31,4 +31,6 @@ android.useAndroidX=true
kapt.use.worker.api=false
android.defaults.buildfeatures.buildconfig=true
android.buildfeatures.buildconfig=true
org.gradle.warning.mode=all

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View file

@ -1,3 +1,7 @@
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.9.0"
}
include ':app'
include ':capacitor-cordova-android-plugins'
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')

View file

@ -1,39 +1,24 @@
ext {
minSdkVersion = 24
compileSdkVersion = 34
targetSdkVersion = 34
androidxActivityVersion = '1.7.0'
androidxAppCompatVersion = '1.6.1'
androidxCoordinatorLayoutVersion = '1.2.0'
androidxCoreVersion = '1.10.0'
androidPlayCore = '1.9.0'
androidxFragmentVersion = '1.5.6'
compileSdkVersion = 35
targetSdkVersion = 35
androidxActivityVersion = '1.9.2'
androidxAppCompatVersion = '1.7.0'
androidxCoordinatorLayoutVersion = '1.3.0'
androidxCoreVersion = '1.15.0'
androidxFragmentVersion = '1.8.4'
junitVersion = '4.13.2'
androidxJunitVersion = '1.1.5'
androidxEspressoCoreVersion = '3.5.1'
androidxJunitVersion = '1.2.1'
androidxEspressoCoreVersion = '3.6.1'
cordovaAndroidVersion = '10.1.1'
androidx_car_version = '1.0.0-alpha7'
androidx_core_ktx_version = '1.12.0'
androidx_media_version = '1.6.0'
androidx_preference_version = '1.1.1'
androidx_test_runner_version = '1.3.0'
arch_lifecycle_version = '2.2.0'
constraint_layout_version = '2.0.1'
espresso_version = '3.3.0'
androidx_core_ktx_version = '1.16.0'
androidx_media_version = '1.7.0'
exoplayer_version = '2.18.7'
fragment_version = '1.2.5'
glide_version = '4.11.0'
gms_strict_version_matcher_version = '1.0.3'
gradle_version = '3.1.4'
gson_version = '2.8.5'
junit_version = '4.13'
kotlin_version = '1.8.10'
kotlin_coroutines_version = '1.6.4'
multidex_version = '1.0.3'
play_services_auth_version = '18.1.0'
recycler_view_version = '1.1.0'
robolectric_version = '4.2'
glide_version = '4.16.0'
junit_version = '4.13.2'
kotlin_version = '2.1.0'
kotlin_coroutines_version = '1.10.1'
test_runner_version = '1.1.0'
coreSplashScreenVersion = '1.0.1'
androidxWebkitVersion = '1.6.1'
androidxWebkitVersion = '1.12.1'
}

View file

@ -1,5 +1,5 @@
@import "./tailwind.css";
@import "./fonts.css";
@import './tailwind.css';
@import './fonts.css';
@import './defaultStyles.css';
@import './absicons.css';
@import './transitions.css';
@ -61,6 +61,10 @@ textarea {
box-shadow: 4px 1px 8px #11111166, -4px 1px 8px #11111166, 1px -4px 8px #11111166;
}
.box-shadow-progressbar {
box-shadow: 0px -1px 4px rgb(62, 50, 2, 0.5);
}
.shadow-height {
height: calc(100% - 4px);
}
@ -164,4 +168,4 @@ Bookshelf Label
.tracksTable th {
padding: 4px 8px;
font-size: 0.75rem;
}
}

View file

@ -52,4 +52,16 @@
text-indent: 0px !important;
text-align: start !important;
text-align-last: start !important;
}
}
.default-style.less-spacing p {
margin-block-start: 0;
}
.default-style.less-spacing ul {
margin-block-start: 0;
}
.default-style.less-spacing ol {
margin-block-start: 0;
}

View file

@ -1,19 +1,12 @@
@font-face {
font-family: 'Material Icons';
font-family: 'Material Symbols Rounded';
font-style: normal;
font-weight: 400;
src: url(/fonts/MaterialIcons-Regular.ttf) format('truetype');
src: url(/fonts/MaterialSymbolsRounded.woff2) format('woff2');
}
@font-face {
font-family: 'Material Icons Outlined';
font-style: normal;
font-weight: 400;
src: url(/fonts/MaterialIconsOutlined-Regular.otf) format('opentype');
}
.material-icons {
font-family: 'Material Icons';
.material-symbols {
font-family: 'Material Symbols Rounded';
font-weight: normal;
font-style: normal;
line-height: 1;
@ -24,31 +17,14 @@
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
vertical-align: top;
}
.material-icons:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl):not(.text-7xl):not(.text-8xl) {
font-size: 1.5rem;
.material-symbols.fill {
font-variation-settings:
'FILL' 1
}
.material-icons-outlined {
font-family: 'Material Icons Outlined';
font-weight: normal;
font-style: normal;
line-height: 1;
letter-spacing: normal;
text-transform: none;
display: inline-block;
white-space: nowrap;
word-wrap: normal;
direction: ltr;
-webkit-font-smoothing: antialiased;
}
.material-icons-outlined:not(.text-xs):not(.text-sm):not(.text-md):not(.text-base):not(.text-lg):not(.text-xl):not(.text-2xl):not(.text-3xl):not(.text-4xl):not(.text-5xl):not(.text-6xl):not(.text-7xl):not(.text-8xl) {
font-size: 1.5rem;
}
/* cyrillic-ext */
@font-face {
font-family: 'Source Sans Pro';
@ -317,4 +293,4 @@
font-display: swap;
src: url(/fonts/Ubuntu_Mono/UbuntuMono-Regular.ttf) format('ttf');
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
}

View file

@ -22,6 +22,25 @@
--gradient-minimized-audio-player: linear-gradient(145deg, rgba(38, 38, 38, 0.5) 0%, rgba(38, 38, 38, 0.9) 20%, rgb(38, 38, 38) 60%);
}
html[data-theme='black'] {
color: white;
--color-bg: 0 0 0;
--color-bg-hover: 0 0 0;
--color-fg: 230 237 243;
--color-fg-muted: 120 126 132;
--color-primary: 0 0 0;
--color-secondary: 0 0 0;
--color-border: 55 62 65;
--color-bg-toggle: 0 0 0;
--color-bg-toggle-selected: 35 35 35;
--color-track-cursor: 229 231 235;
--color-track: 107 114 128;
--color-track-buffered: 75 85 99;
--gradient-item-page: rgb(0, 0, 0);
--gradient-audio-player: rgb(0, 0, 0);
--gradient-minimized-audio-player: rgb(0, 0, 0);
}
html[data-theme='light'] {
color: black;
--color-bg: 255 255 255;

View file

@ -2,10 +2,16 @@
"appId": "com.audiobookshelf.app",
"appName": "audiobookshelf-app",
"webDir": "dist",
"bundledWebRuntime": false,
"plugins": {
"CapacitorHttp": {
"enabled": false
},
"StatusBar": {
"backgroundColor": "#232323",
"style": "DARK"
}
},
"server": {
"androidScheme": "http"
}
}
}

View file

@ -5,9 +5,9 @@
<img src="/Logo.png" class="h-10 w-10" />
</nuxt-link>
<a v-if="showBack" @click="back" class="rounded-full h-10 w-10 flex items-center justify-center mr-2 cursor-pointer">
<span class="material-icons text-3xl text-fg">arrow_back</span>
<span class="material-symbols text-3xl text-fg">arrow_back</span>
</a>
<div v-if="user && currentLibrary && networkConnected">
<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">
<ui-library-icon :icon="currentLibraryIcon" :size="4" font-size="base" />
<p class="text-sm leading-4 ml-2 mt-0.5 max-w-24 truncate">{{ currentLibraryName }}</p>
@ -21,16 +21,18 @@
<widgets-download-progress-indicator />
<!-- Must be connected to a server to cast, only supports media items on server -->
<div v-show="isCastAvailable && user" class="mx-2 cursor-pointer flex items-center pt-0.5" @click="castClick">
<span class="material-icons" :class="isCasting ? 'text-success' : ''">cast</span>
<div v-show="isCastAvailable && user" class="mx-2 cursor-pointer flex items-center" @click="castClick">
<span class="material-symbols text-2xl leading-none">
{{ isCasting ? 'cast_connected' : 'cast' }}
</span>
</div>
<nuxt-link v-if="user" class="h-7 mx-1.5" style="padding-top: 3px" to="/search">
<span class="material-icons">search</span>
<nuxt-link v-if="user" class="mx-1.5 flex items-center h-10" to="/search">
<span class="material-symbols text-2xl leading-none">search</span>
</nuxt-link>
<div class="h-7 mx-1.5">
<span class="material-icons" style="font-size: 1.75rem" @click="clickShowSideDrawer">menu</span>
<span class="material-symbols" style="font-size: 1.75rem" @click="clickShowSideDrawer">menu</span>
</div>
</div>
</div>
@ -54,9 +56,6 @@ export default {
this.$store.commit('setCastAvailable', val)
}
},
networkConnected() {
return this.$store.state.networkConnected
},
currentLibrary() {
return this.$store.getters['libraries/getCurrentLibrary']
},
@ -101,14 +100,14 @@ export default {
this.isCastAvailable = data && data.value
}
},
mounted() {
async mounted() {
AbsAudioPlayer.getIsCastAvailable().then((data) => {
this.isCastAvailable = data && data.value
})
this.onCastAvailableUpdateListener = AbsAudioPlayer.addListener('onCastAvailableUpdate', this.onCastAvailableUpdate)
this.onCastAvailableUpdateListener = await AbsAudioPlayer.addListener('onCastAvailableUpdate', this.onCastAvailableUpdate)
},
beforeDestroy() {
if (this.onCastAvailableUpdateListener) this.onCastAvailableUpdateListener.remove()
this.onCastAvailableUpdateListener?.remove()
}
}
</script>
@ -160,4 +159,4 @@ export default {
transform: translate(10px, 0);
}
}
</style>
</style>

Some files were not shown because too many files have changed in this diff Show more