Compare commits

...
1680 Commits
Author SHA1 Message Date
advplyr 7014e04e6f iOS version bump 0.14.2 2026-09-27 17:50:38 -05:00
advplyr a00032beda Version bump v0.14.2-beta 2026-09-27 17:23:31 -05:00
advplyr 02acaf1159 Add Arabic, Hebrew, Korean and Slovak language options 2026-09-27 17:17:45 -05:00
advplyr 6f923ef1d8 Merge pull request #2017 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2026-09-27 17:06:19 -05:00
advplyr 679ebb16f8 Merge pull request #2027 from nichwall/download-clear-invalid-queue
Download clear invalid queue
2026-09-27 17:06:02 -05:00
advplyr ed356a8d99 Add try/catch around android cleanupExpired downloads call 2026-09-27 17:03:25 -05:00
Amirali Dehestani 33e187678d Translated using Weblate (Persian)
Currently translated at 14.6% (56 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fa/
2026-09-26 23:52:38 +02:00
Mateusz Lesiak 5fc952a9e5 Translated using Weblate (Polish)
Currently translated at 100.0% (381 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2026-09-24 17:51:37 +00:00
oddhap a408c55cff Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (381 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2026-09-24 17:51:34 +00:00
Juan Benites a3d482e3f7 Translated using Weblate (Spanish)
Currently translated at 100.0% (381 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2026-09-24 17:51:30 +00:00
Mediokris 2f8c1731e9 Translated using Weblate (Norwegian Bokmål)
Currently translated at 99.7% (380 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2026-09-22 09:51:35 +00:00
Danol b5269f52c4 Translated using Weblate (Czech)
Currently translated at 100.0% (381 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2026-09-22 09:51:33 +00:00
Daniel Nylander 9ebc7da984 Translated using Weblate (Swedish)
Currently translated at 100.0% (381 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2026-09-18 23:29:28 +02:00
Martynas c8c4b8b059 Translated using Weblate (Lithuanian)
Currently translated at 59.8% (228 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/lt/
2026-09-17 19:51:30 +00:00
Martynas c1f3e236f8 Translated using Weblate (Lithuanian)
Currently translated at 56.4% (215 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/lt/
2026-09-15 18:51:58 +00:00
Kim Josefsen 128e23fabf Translated using Weblate (Danish)
Currently translated at 98.4% (375 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2026-09-15 18:51:52 +00:00
Nicholas Wallace 989cf66715 Revert "Debug: add forced bad download queue entry"
This reverts commit 108f34bfb7.
2026-09-13 15:03:15 -07:00
Nicholas Wallace ce6883331e Fix: download queue null check before restoring queue 2026-09-13 15:03:04 -07:00
Nicholas Wallace 108f34bfb7 Debug: add forced bad download queue entry 2026-09-13 14:48:55 -07:00
J. Lavoie 869873acfd Translated using Weblate (Greek)
Currently translated at 72.1% (275 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/el/
2026-09-11 21:19:03 +00:00
samia shanta 96a44bcce7 Translated using Weblate (Bengali)
Currently translated at 100.0% (381 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/bn/
2026-09-11 21:19:03 +00:00
J. Lavoie ca2c023486 Translated using Weblate (Italian)
Currently translated at 99.7% (380 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2026-09-11 21:19:02 +00:00
J. Lavoie 46850159de Translated using Weblate (French)
Currently translated at 100.0% (381 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2026-09-11 21:19:02 +00:00
J. Lavoie bd4c3be210 Translated using Weblate (German)
Currently translated at 100.0% (381 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2026-09-11 21:19:01 +00:00
advplyr da4745217c Merge pull request #2022 from nichwall/weblate-workflow
Add weblate translation credit workflow
2026-09-11 16:18:48 -05:00
advplyr 3048237a4b Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2026-09-11 16:14:01 -05:00
advplyr e473155646 Update gh issue templates with 0.14.1 2026-09-11 16:13:56 -05:00
Nicholas Wallace 643603c073 Add weblate translation credit workflow 2026-09-11 14:09:09 -07:00
advplyr 2ac4de4d58 Bump iOS build number 2026-09-11 16:06:49 -05:00
advplyr 68931febf6 Fix crash on cleanExpiredLogs for iOS 2026-09-11 15:57:59 -05:00
advplyr 8561ada1d2 Merge pull request #2019 from nichwall/ios-workflow
Ios debug build workflow
2026-09-11 15:00:22 -05:00
Nicholas Wallace 06f4ba2e82 Delete iOS release workflow for separate PR 2026-09-11 06:43:42 -07:00
Nicholas Wallace 353e2f02df Reapply "Fix package-lock for macOS runner"
This reverts commit 94db1f72a7.
2026-09-11 06:38:58 -07:00
advplyr 2927ab92ae Bump iOS build number 2026-09-11 06:22:42 -05:00
Nicholas Wallace 94db1f72a7 Revert "Fix package-lock for macOS runner"
This reverts commit 607e5fc670.
2026-09-10 17:18:10 -07:00
Nicholas Wallace bf92d18b48 Change workflows to exclude optional dependencies 2026-09-10 17:16:19 -07:00
Nicholas Wallace c4aed2587f Initial IPA and Testflight push workflow 2026-09-10 16:36:23 -07:00
Nicholas Wallace 7a5e4f559c Add quiet flag to reduce amount of logs printed 2026-09-10 16:31:20 -07:00
Nicholas Wallace fe14ec0f45 Delete temporary workflow 2026-09-10 16:15:51 -07:00
Nicholas Wallace 607e5fc670 Fix package-lock for macOS runner 2026-09-10 16:15:22 -07:00
Nicholas Wallace a34a1bb2b9 Dummy workflow to get macOS npm dependencies 2026-09-10 16:13:23 -07:00
Nicholas Wallace e48f2f0da2 Initial iOS build workflow 2026-09-10 16:02:27 -07:00
Nicholas Wallace 32a8212653 Add inverted iOS icons for debug builds 2026-09-10 16:01:50 -07:00
advplyr 9a2f0c3e4a Add one time wipe of logs for iOS 2026-09-10 17:03:20 -05:00
advplyr 7292e367d2 iOS version bump 0.14.1 2026-09-08 17:35:53 -05:00
advplyr fe8f5de891 Update android target sdk to 36 2026-09-08 17:35:36 -05:00
advplyr c6d0f0e8b5 Version bump v0.14.1-beta 2026-09-08 17:22:02 -05:00
advplyr 14885467a4 Add padding on report bugs message on account page 2026-09-08 17:13:49 -05:00
advplyr 87f1c83cbf Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2026-09-08 17:11:14 -05:00
advplyr b706337852 Fix iOS expired logs never clearing 2026-09-08 17:10:30 -05:00
advplyr 34f2165ef6 Merge pull request #1956 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2026-09-08 16:36:14 -05:00
Pavel Miniutka 88ab456ce5 Translated using Weblate (Belarusian)
Currently translated at 100.0% (381 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2026-09-08 23:28:15 +02:00
J. Lavoie 022a9c5875 Translated using Weblate (Greek)
Currently translated at 71.9% (274 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/el/
2026-09-08 23:28:15 +02:00
J. Lavoie 4cb4183a12 Translated using Weblate (Finnish)
Currently translated at 98.9% (377 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2026-09-08 23:28:15 +02:00
J. Lavoie 62e80cb469 Translated using Weblate (Italian)
Currently translated at 99.7% (380 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2026-09-08 23:28:15 +02:00
J. Lavoie 37b19e9a7b Translated using Weblate (French)
Currently translated at 100.0% (381 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2026-09-08 23:28:15 +02:00
J. Lavoie 961e66b863 Translated using Weblate (German)
Currently translated at 100.0% (381 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2026-09-08 23:28:15 +02:00
samia shanta 6c56003c26 Translated using Weblate (Bengali)
Currently translated at 87.6% (334 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/bn/
2026-09-08 23:28:15 +02:00
Martynas d44dbd770a Translated using Weblate (Lithuanian)
Currently translated at 55.9% (213 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/lt/
2026-09-08 23:28:15 +02:00
kuci-JK ca04bb7a3e Translated using Weblate (Czech)
Currently translated at 99.2% (378 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2026-09-08 23:28:15 +02:00
ugyes c832c86ca4 Translated using Weblate (Hungarian)
Currently translated at 100.0% (381 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2026-09-08 23:28:15 +02:00
Jan Paulzen 64cbb41b0b Translated using Weblate (German)
Currently translated at 100.0% (381 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2026-09-08 23:28:15 +02:00
advplyr d11d9860b6 iOS update refresh token failure to only logout when request returns 401/403 2026-09-08 16:28:01 -05:00
advplyr c1a0b96ae4 iOS version bump 0.14.0 2026-09-08 16:27:27 -05:00
epipenRX a09326b5b2 Pause instead of close playback on KEYCODE_MEDIA_STOP (#1913)
Car head units send KEYCODE_MEDIA_STOP when powering off, which was
calling closePlayback() and destroying the session entirely. This removed
the media notification from the shade and required manually reopening the
app to resume listening.

Change to pause() so the session and notification stay alive, matching
the behavior of YouTube, Plex, and Jellyfin. When the car powers back on
and sends KEYCODE_MEDIA_PLAY, playback resumes in the same session.
2026-09-07 17:41:37 -05:00
advplyr 1a290b4d07 Merge pull request #2000 from JorenVerbaandert/fix/playback-session-bug
Fix playback session bug
2026-09-07 17:32:42 -05:00
advplyr 8d8147ffea Merge pull request #2003 from ljlongwing/fix-1711-1712-library-switch
Leave playlist/collection detail pages when the library changes (#1711, #1712)
2026-09-07 17:20:56 -05:00
advplyr 17fcec4401 Merge pull request #1972 from Michael-128/fix/ipados-resize
Fix Stage Manager resize issue on iPadOS (#1563)
2026-09-07 17:18:05 -05:00
advplyr 79a925088f Merge pull request #2008 from huggenknubbel/cast-sleep-timer-volume
Let the sleep timer fade out on a cast device too
2026-09-07 17:13:49 -05:00
advplyr 2fc8735643 Merge pull request #2009 from huggenknubbel/cast-availability
Report receiver availability from the framework as well
2026-09-07 17:13:21 -05:00
advplyr 0b1dfa4c34 Merge pull request #2002 from nichwall/android-download-followup
Android download followup
2026-09-06 18:22:08 -05:00
advplyr 30c4fa7e16 Add try catch around scan download item 2026-09-06 18:11:49 -05:00
advplyr 71bd354bc7 Refactor token refresh to better handle types of failures and retries during download 2026-09-06 17:53:46 -05:00
advplyr e06ce4a868 Only clear refresh token and logout when refresh request explicitly rejects, prevents transient request errors from causing a logout 2026-09-06 17:16:36 -05:00
advplyr 90f8d99287 Merge pull request #2001 from JorenVerbaandert/docs/update-bug-report-template
Add 0.14 to bug report template, remove 0.12
2026-09-06 16:46:10 -05:00
huggenknubbel 1cdf0a59b1 cast: report receiver availability from the framework as well
ChromecastListener has always implemented CastStateListener, but nothing
ever registered it. Whether a receiver was reported as available hung
entirely on the media router callback, which does not always fire - and
then the cast button never appears even though devices are there.

The framework's own signal is registered now and the current state is
reported straight away. It is not taken at face value in one direction:
the framework drops back to NO_DEVICES_AVAILABLE when a session ends,
because it stops discovering at that point, which would hide the button
right after someone stops casting. The devices the media router still
knows decide that case.

The listener is dropped again when the activity goes.
2026-09-03 08:15:20 +02:00
Nicholas Wallace 6560038495 Use actual file size from server it size does not match metadata 2026-09-02 13:30:04 -07:00
Nicholas Wallace 4b8abe7deb Refresh auth token on download 401 2026-09-02 10:39:15 -07:00
Nicholas Wallace d230b16d5e Restore fullname check 2026-09-02 10:37:59 -07:00
huggenknubbel 0e71eb0308 cast: let the sleep timer fade out on a cast device too
The sleep timer fades playback down to 10% over its last minute by setting the
player's volume. CastPlayer.setVolume was an empty stub and getVolume returned
zero, so while casting the fade did nothing at all.

Both now go through the media stream volume the Cast SDK offers. That is the
receiver's volume for this stream only: the device volume stays where the user
put it, and on a cast group the balance between its members is untouched,
because the receiver applies the stream volume to the group's output.
2026-08-30 23:38:27 +02:00
LJ 4a1cf0ea4a Leave playlist/collection detail pages when the library changes (#1711, #1712)
The item detail page already listens for the app-wide `library-changed`
event and navigates away; the playlist and collection detail pages did
not, so switching libraries left them showing stale content with no way
back to the list.

- playlist/_id.vue: route to /bookshelf/playlists on library change
- collection/_id.vue: route to /bookshelf/collections unless the new
  library is the collection's own library
2026-08-28 13:03:59 -06:00
Nicholas Wallace d2196ce035 Add download complete to logs 2026-08-28 07:36:53 -07:00
Nicholas Wallace a100226a85 Add AbsLogger to download paths 2026-08-27 20:38:48 -07:00
Nicholas Wallace ba66869502 Remove unnecessary changes 2026-08-27 15:31:32 -07:00
Nicholas Wallace 02e3f3541f Simplify and revert Capacitor changes 2026-08-27 15:12:32 -07:00
Nicholas Wallace f25ff6344e Inline download resume policy 2026-08-27 15:12:04 -07:00
Nicholas Wallace c800ed727a Fix concurrent podcast download cover issue 2026-08-27 14:52:14 -07:00
Joren Verbaandert 4eb848ae5f Update the feature request template 2026-08-27 22:53:48 +02:00
Joren Verbaandert 9dfa68ea8b Add 0.14 to bug report template, remove 0.12 2026-08-27 22:35:29 +02:00
Nicholas Wallace 7989607313 Retry from beginning of file on failure and restore 2026-08-27 13:26:51 -07:00
Nicholas Wallace 61c314a531 Initial download service startup fixes and range request fix 2026-08-27 13:08:46 -07:00
Joren Verbaandert a709e994a0 Fix playback session bug 2026-08-27 21:26:12 +02:00
Michael-128 96fc9adbbf Fix Stage Manager resize issue on iPadOS (#1563) 2026-08-24 18:12:06 +02:00
advplyr 12025ab59a Version bump 0.14.0-beta 2026-08-04 17:26:09 -05:00
advplyr f66840e8e4 Merge pull request #1948 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2026-08-04 17:17:03 -05:00
violawang0401-cloud 4c3e098785 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 100.0% (381 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hant/
2026-08-04 23:59:01 +02:00
violawang0401-cloud 441086e773 Translated using Weblate (Chinese (Traditional Han script))
Currently translated at 94.7% (361 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hant/
2026-08-04 23:59:00 +02:00
FiendFEARing 1408a4c3ff Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (381 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2026-08-04 23:59:00 +02:00
advplyrandadvplyr fe67a221ac Render a higher resolution cover in the Android notification background (#1917)
* Render a higher resolution bitmap in the notification background art

Previously only METADATA_KEY_ALBUM_ART_URI/ART_URI were set, leaving notification/lock-screen/Android Auto consumers to resolve the cover art themselves. Likely due to internal scaling, a lower quality bitmap was loaded.

This commit adds a shared resolveUriAsBitmap() helper (extracted from AbMediaDescriptionAdapter.kt) and uses it to:
  - Resolve and cache a bitmap on PlaybackSession once per
  session (synchronously for local covers, asynchronously for server covers), then set it on METADATA_KEY_ALBUM_ART/ART once available, falling back to the URI keys until it resolves.
  - Reuse the same helper in AbMediaDescriptionAdapter so the notification icon is resolved consistently.

  PlayerNotificationService tracks the resolution Job and cancels/replaces it on each new playback session, invalidating the media session metadata once the bitmap is ready.

* Remove the size override in Glide

The override was not needed. It wasn't in the original code, so I'll remove it for consistency.

* Update notification icon to reuse cover bitmap if already loaded on session

---------

Co-authored-by: advplyr <advplyr@protonmail.com>
2026-08-04 16:58:47 -05:00
advplyr f087cbed6f Update notification icon to reuse cover bitmap if already loaded on session 2026-08-04 16:57:00 -05:00
databoy2kanddataboy2k 9820db0ef6 fix(android-auto): run server connection check off the main thread (#1920)
Android Auto browsing could freeze and crash the app (phone app "closes",
Android Auto shows a generic connection error) whenever the Capacitor
webview had not yet established the server connection.

Root cause: onLoadChildren runs on the media browser service main thread.
It calls loadAndroidAutoItems -> checkSetValidServerConnectionConfig, which
used runBlocking to ping every saved server config and authorize the user.
That blocked the main thread on network I/O. Android Auto (plus Assistant)
re-request the browse root every ~2s, and every onGetRoot flags a reload
while disconnected, so the blocking work was triggered repeatedly and
re-entrantly, producing an ANR.

Changes:
- checkSetValidServerConnectionConfig now runs on a dedicated
  Dispatchers.IO scope instead of runBlocking, so pings/authorize never
  block the caller. The callback contract is unchanged; downstream Android
  Auto callbacks already ran off the main thread.
- loadAndroidAutoItems coalesces overlapping calls into a single in-flight
  load and fires all queued callbacks on completion, so the browse-root
  polling storm can no longer spawn concurrent loads racing shared state.
- Fix latent bug in checkResetServerItems: server config id was compared
  with !== (reference identity) instead of != (value), causing spurious
  cache resets that fed the reload storm.

Co-authored-by: databoy2k <18686442+databoy2k@users.noreply.github.com>
2026-08-04 16:26:09 -05:00
advplyr 9664f79b1f Merge pull request #1947 from hypnotoad08/fix/finished-book-restarts
Restart finished local media from the beginning
2026-08-03 17:36:59 -05:00
advplyr 533396b855 Android clear current time on local media progress when marking as finished 2026-08-03 17:32:19 -05:00
advplyr 005a051cd0 Merge pull request #1906 from jucor/fix/audiotrack-nullable-mimetype
Make AudioTrack.mimeType nullable to prevent Android playback stall (fixes #1905)
2026-08-03 17:06:18 -05:00
hypnotoad08 f06168fdc2 Restart finished local media from the beginning 2026-08-02 20:15:20 -04:00
advplyr 26c6b9149b Merge pull request #1938 from cosandr/fix-304
Remember sort & filter options for podcast episodes
2026-08-02 17:49:02 -05:00
advplyr 8b70d02332 Fix getUserSetting incorrectly returning null for all falsy values 2026-08-02 17:43:45 -05:00
advplyr 4370b19e12 Merge pull request #1879 from OguzIronCode/fix/search-result-limit
Remove hardcoded search result limit of 5
2026-08-02 17:28:34 -05:00
advplyr f708e4dc07 Merge pull request #1862 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2026-08-02 07:28:09 -05:00
Vladimir Makeev 4bb809c1b9 Translated using Weblate (Russian)
Currently translated at 100.0% (381 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2026-08-01 22:54:17 +00:00
Igor Dobrača ccf57bb016 Translated using Weblate (Croatian)
Currently translated at 100.0% (381 of 381 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2026-08-01 22:54:11 +00:00
Pavel Miniutka 65b2a1a9a4 Translated using Weblate (Belarusian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2026-08-01 22:54:06 +00:00
Zarakkas 8e6ef8e718 Translated using Weblate (German)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2026-08-01 22:54:04 +00:00
Mona Lisa cbea6229b5 Translated using Weblate (Swedish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2026-08-01 22:54:02 +00:00
Axel Björnsson 05205eb646 Translated using Weblate (Icelandic)
Currently translated at 2.6% (10 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/is/
2026-08-01 22:54:00 +00:00
Mona Lisa 32fc866d8a Translated using Weblate (Swedish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2026-08-01 22:53:57 +00:00
Mona Lisa da089338dc Translated using Weblate (Swedish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2026-08-01 22:53:55 +00:00
FiendFEARing e19f47b291 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2026-08-01 22:53:53 +00:00
FiendFEARing e7ed54439b Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2026-08-01 22:53:51 +00:00
FiendFEARing 3db3bbd756 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2026-08-01 22:53:48 +00:00
FiendFEARing 24ab13e27e Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2026-08-01 22:53:46 +00:00
FiendFEARing 195d1d1ef4 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2026-08-01 22:53:45 +00:00
burghy86 5383e91249 Translated using Weblate (Italian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2026-08-01 22:53:44 +00:00
தமிழ்நேரம் c02e45b703 Translated using Weblate (Tamil)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ta/
2026-08-01 22:53:41 +00:00
தமிழ்நேரம் 991dd997ee Translated using Weblate (Tamil)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ta/
2026-08-01 22:53:37 +00:00
Gabriel daddd5c3c2 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2026-08-01 22:53:34 +00:00
Gabriel 8b77fae668 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2026-08-01 22:53:33 +00:00
advplyr bba15dacf8 Added translation using Weblate (Tamil) 2026-08-01 22:53:32 +00:00
Hans Henrik Juhl bfaeb6ea42 Translated using Weblate (Danish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2026-08-01 22:53:31 +00:00
Hans Henrik Juhl 5f66f65d6f Translated using Weblate (Danish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2026-08-01 22:53:30 +00:00
Mads Kvist Møller 8b9098c877 Translated using Weblate (Danish)
Currently translated at 99.4% (372 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2026-08-01 22:53:27 +00:00
no1ta 7044925791 Translated using Weblate (Greek)
Currently translated at 72.1% (270 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/el/
2026-08-01 22:53:23 +00:00
advplyr bcc292b33a Added translation using Weblate (Uzbek) 2026-08-01 22:53:22 +00:00
Pavel Miniutka ae08206c3e Translated using Weblate (Belarusian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2026-08-01 22:53:20 +00:00
EteranlK 91315dd9b5 Translated using Weblate (Arabic)
Currently translated at 99.4% (372 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2026-08-01 22:53:18 +00:00
advplyr 8a253e7679 Added translation using Weblate (Latvian) 2026-08-01 22:53:17 +00:00
advplyr 04e9c8a329 Merge pull request #1869 from AndrolGenhald/reduce-data-usage
Send less data to server with progress update.
2026-08-01 17:53:00 -05:00
advplyr 1d163df773 Update iOS to send partial playback session on local syncs 2026-08-01 17:47:35 -05:00
advplyr 4cfa22004c Merge pull request #1861 from lkiesow/landscape-player
Player view in landscape mode
2026-07-31 18:06:31 -04:00
advplyr e0747e3f19 Fix chapter track overflow and switch to percentage width 2026-07-31 17:00:35 -05:00
advplyr b14e7b7357 Merge branch 'master' into landscape-player 2026-07-31 16:42:19 -05:00
advplyr 0123df99d2 Improve logging around progress sync 2026-07-31 16:38:15 -05:00
advplyr 4a48cb0a9f Merge pull request #1848 from Kyrluckechuck/fix/seek-to-exact-time-rather-than-floored-second
Seek to millisecond rather than flooring to second
2026-07-31 17:18:55 -04:00
advplyr 425ef4c3b6 Merge pull request #1937 from nichwall/download-overhaul-again
Android download rewrite, try 2
2026-07-30 18:36:32 -04:00
advplyr e5ef38295d Update download notification cancel to clear download queue in UI 2026-07-30 17:31:23 -05:00
advplyr b084e54dcc Update download notification id to 11 2026-07-30 17:08:24 -05:00
advplyr d3e55cabf4 Version guard download notification channel API 26+ 2026-07-30 16:51:48 -05:00
advplyr 2cc9ff5a41 Add POST_NOTIFICATION to manifest and start foreground service with type 2026-07-29 17:07:27 -05:00
Andrei Costescu 9562e8f252 Fix ascending bug 2026-07-21 21:05:27 +02:00
Andrei Costescu 7726192008 Align with codebase 2026-07-21 20:53:58 +02:00
Nicholas Wallace f8424c03fd Increase download chunk size to 512 KB 2026-07-20 20:05:18 -07:00
Nicholas Wallace 58fbfca645 Add translation strings for download dialogs and notification 2026-07-20 20:01:02 -07:00
Nicholas Wallace 6282fd1f95 Remove deleted migration call 2026-07-20 19:54:47 -07:00
Nicholas Wallace 245df5d934 Remove unnecessary migration, autoformat 2026-07-20 19:06:35 -07:00
Nicholas Wallace dc6184f41d Remove old and slow simplePath from SAF, also autoformatted 2026-07-20 18:59:19 -07:00
Nicholas Wallace b3c3950c55 Check if completed file exists in shared storage before downloading again 2026-07-19 22:31:32 -07:00
Nicholas Wallace 034a134319 Remove retry stubs 2026-07-19 17:54:46 -07:00
Nicholas Wallace 8142d626c0 Ensure token is not part of download URL 2026-07-19 10:24:12 -07:00
Nicholas Wallace 8b20730b6f Cleaning up retry logic and unused return types 2026-07-19 10:24:01 -07:00
Nicholas Wallace d27f431f6f Comment cleanup 2026-07-19 09:23:31 -07:00
Nicholas Wallace 0df0a206e7 Delete partial files after 24 hours 2026-07-19 09:16:35 -07:00
Nicholas Wallace 6aeb31b591 Add support for background download and resumption 2026-07-18 22:56:12 -07:00
Nicholas Wallace eb2483d039 Initial rewrite of 1587 by codex 2026-07-18 17:32:37 -07:00
Andrei Costescu 67fbeeeb6c Remember sort & filter options for podcast episodes 2026-07-18 18:07:14 +02:00
photown 5099e39f7b Remove the size override in Glide
The override was not needed. It wasn't in the original code, so I'll remove it for consistency.
2026-07-05 23:32:28 -04:00
photown 34710dbddc Render a higher resolution bitmap in the notification background art
Previously only METADATA_KEY_ALBUM_ART_URI/ART_URI were set, leaving notification/lock-screen/Android Auto consumers to resolve the cover art themselves. Likely due to internal scaling, a lower quality bitmap was loaded.

This commit adds a shared resolveUriAsBitmap() helper (extracted from AbMediaDescriptionAdapter.kt) and uses it to:
  - Resolve and cache a bitmap on PlaybackSession once per
  session (synchronously for local covers, asynchronously for server covers), then set it on METADATA_KEY_ALBUM_ART/ART once available, falling back to the URI keys until it resolves.
  - Reuse the same helper in AbMediaDescriptionAdapter so the notification icon is resolved consistently.

  PlayerNotificationService tracks the resolution Job and cancels/replaces it on each new playback session, invalidating the media session metadata once the bitmap is ready.
2026-07-05 23:32:28 -04:00
Julien Cornebise 4798644bea Make AudioTrack.mimeType nullable to prevent Android playback stall
The POST /api/items/<id>/play/<episodeId> response inlines every episode
of a podcast with an audioTrack per episode. Server-side, audioTrack.mimeType
is derived from audioFile.mimeType, which is null when the row was created
without a successful audio probe (direct DB inserts, import scripts,
partial probe failures).

The Jackson deserializer hits one null mimeType among the inlined
episodes, throws MissingKotlinParameterException, and the entire
play-session response fails to deserialize. ExoPlayer is never called,
the UI silently stalls.

Make mimeType nullable on the data class to tolerate this. ExoPlayer's
MediaItem.Builder.setMimeType already accepts nullable. Defensively
coerce null to empty string for Cast's MediaInfo.Builder.setContentType,
whose Java signature is non-null.

Fixes #1905
2026-06-23 15:58:18 +01:00
OguzHAN bd0cbf8bf0 Remove hardcoded search result limit of 5
The search API call had a hardcoded limit=5 parameter which restricted
search results to only 5 items regardless of how many matches exist.
Removing this limit allows the server's default pagination to apply,
showing all relevant search results to the user.

Fixes #1872
2026-05-25 21:12:38 +03:00
AndrolGenhald f562a1a5dc Send less data to server with progress update. 2026-05-13 21:18:05 -05:00
advplyr 185cba16eb Merge pull request #1868 from pavel-miniutka/master
Belarusian language option
2026-05-13 16:32:34 -05:00
pavel-miniutka 547d086228 Belarusian language option 2026-05-13 09:11:27 +03:00
advplyr 3a2d5647d4 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2026-05-12 08:22:20 -05:00
advplyr 0a67c9b802 Update gh issue templates for 0.13.0 2026-05-12 08:22:15 -05:00
advplyr 0254afd5d8 iOS version bump 0.13.0 2026-05-10 15:18:30 -05:00
advplyr 853546f3d2 Fix cover image sampling not working when webview strips origin header, instead fetch image from server with CapacitorHttp 2026-05-10 15:15:13 -05:00
advplyr b8ac073247 Version bump 0.13.0-beta 2026-05-10 14:29:52 -05:00
advplyr 56cb4df31a Cleanup unnecessary logs 2026-05-10 14:23:17 -05:00
advplyr 7285b0bf26 iOS update RealmSwift to 10.54.6 2026-05-10 14:12:58 -05:00
advplyr 3fdc2e6dcf Merge pull request #1864 from advplyr/fix_open_session_progress_sync
Fix external progress sync for open sessions
2026-05-10 11:24:38 -05:00
advplyr ed2f9d2e45 Prevent focus and socket reconnect from both fetching server media progress 2026-05-10 11:12:17 -05:00
advplyr 68dafbba75 Sync local media open in player when socket reconnects after 30s+ 2026-05-10 11:05:17 -05:00
Lars Kiesow 7d17367c1e Adjust title and author size in landscape mode 2026-05-10 00:55:53 +02:00
advplyr 8be08702c1 Fix regain focus check server progress for local media items open in player & sync more recent 2026-05-09 16:48:55 -05:00
Lars Kiesow 91bc56e1e1 Player view in landscape mode
Rotating your phone to landscape mode and opening the player, the view
wasn't using the available space very efficiently. Especially the cover
image is not very helpful being pushed to a minimal size.

This patch redesigned the view in landscape mode, pushing the cover
image to the left half of the screen while the controls are moved to the
right half of the screen.
2026-05-07 02:05:04 +02:00
advplyr 2c2e77f6cd Merge pull request #1824 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2026-05-05 15:55:32 -05:00
LvanAlphen 12d02645fd Translated using Weblate (Dutch)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2026-05-04 22:12:49 +00:00
Pim 4578339594 Translated using Weblate (Dutch)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2026-05-04 22:12:48 +00:00
LvanAlphen b81f78755d Translated using Weblate (Dutch)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2026-05-04 22:12:47 +00:00
Naoto Ishikawa 82664d6c81 Translated using Weblate (Japanese)
Currently translated at 76.4% (286 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ja/
2026-05-04 22:12:47 +00:00
Pavel Miniutka 9e06db58d2 Translated using Weblate (Belarusian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2026-05-04 22:12:46 +00:00
ugyes de3461af52 Translated using Weblate (Hungarian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2026-05-04 22:12:46 +00:00
Serhiy Bobrov 61f676d106 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2026-05-04 22:12:45 +00:00
Владимир Макеев 82627b85a5 Translated using Weblate (Russian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2026-05-04 22:12:45 +00:00
Pavel Miniutka 21bc6cc8b6 Translated using Weblate (Belarusian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2026-05-04 22:12:44 +00:00
Plazec 2295b1071e Translated using Weblate (Czech)
Currently translated at 98.6% (369 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2026-05-04 22:12:43 +00:00
Vadzim Kurdzesau c75e376533 Translated using Weblate (Russian)
Currently translated at 97.5% (365 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2026-05-04 22:12:43 +00:00
A L 3a5e2af9e4 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/bg/
2026-05-04 22:12:42 +00:00
Alex 30f1414ea5 Translated using Weblate (Slovak)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2026-05-04 22:12:42 +00:00
tfr tint 737364aa3a Translated using Weblate (Italian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2026-05-04 22:12:41 +00:00
xxzp3 fd988afad1 Translated using Weblate (Danish)
Currently translated at 98.9% (370 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2026-05-04 22:12:41 +00:00
kuci-JK 56dc7f856c Translated using Weblate (Czech)
Currently translated at 98.1% (367 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2026-05-04 22:12:40 +00:00
Paolo Ricci 5e4a41b0b5 Translated using Weblate (Italian)
Currently translated at 97.5% (365 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2026-05-04 22:12:40 +00:00
Pavel Miniutka 66f426f6bb Translated using Weblate (Belarusian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2026-05-04 22:12:39 +00:00
Francisco Serrador 03efa352f5 Translated using Weblate (Spanish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2026-05-04 22:12:39 +00:00
Francisco Serrador 5c147955b7 Translated using Weblate (Spanish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2026-05-04 22:12:38 +00:00
Pavel Miniutka 0a55573bd0 Translated using Weblate (Belarusian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2026-05-04 22:12:37 +00:00
Pavel Miniutka de3812ac33 Translated using Weblate (Belarusian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2026-05-04 22:12:37 +00:00
Abdellatif Ahammad b6de52eb91 Translated using Weblate (Arabic)
Currently translated at 96.5% (361 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2026-05-04 22:12:36 +00:00
advplyr 65ec1ec5d4 Update search to include podcast episodes #1793 2026-05-04 17:12:29 -05:00
advplyr 614118d1d3 Merge pull request #1856 from EndToEndDisciple/patch-1
Update i18n.js to add Bulgarian language
2026-05-04 16:23:39 -05:00
advplyr e3999360f0 Update github template app versions 2026-04-24 05:18:19 -05:00
Radoslav Mandev 50d1b62b95 Update i18n.js to add Bulgarian language 2026-04-24 12:03:37 +02:00
Kyrluckechuck d4e7d0abf9 Seek to millisecond rather than flooring to second 2026-04-14 09:56:28 -04:00
advplyr 1571bcd9f3 Merge pull request #1806 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2026-03-08 17:31:40 -05:00
Felix Groll 5531ced129 Translated using Weblate (German)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2026-03-04 05:09:49 +00:00
Grzegorz Orlowski 2b258de5b8 Translated using Weblate (Polish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2026-03-04 05:09:48 +00:00
Artur 4e3d3f7752 Translated using Weblate (Polish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2026-03-04 05:09:47 +00:00
Torstein Eide 63863a8800 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2026-03-02 09:09:49 +01:00
thehijacker d594fcb08b Translated using Weblate (Slovenian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2026-03-02 09:09:47 +01:00
Pavel Miniutka 85aab8df06 Translated using Weblate (Belarusian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2026-02-26 10:09:46 +01:00
Pavel Miniutka ec1585f7c1 Translated using Weblate (Belarusian)
Currently translated at 99.1% (371 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2026-02-24 19:09:50 +00:00
Pavel Miniutka 3b3bda117c Translated using Weblate (Belarusian)
Currently translated at 96.5% (361 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/be/
2026-02-20 19:09:51 +01:00
lambolighting c37f2b71d4 Translated using Weblate (Greek)
Currently translated at 61.7% (231 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/el/
2026-02-20 19:09:50 +01:00
Plazec 04de5960f1 Translated using Weblate (Czech)
Currently translated at 97.0% (363 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2026-02-19 03:09:52 +01:00
Gneb dad7ac4efd Translated using Weblate (Czech)
Currently translated at 97.0% (363 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2026-02-19 03:09:51 +01:00
Silviu Bajenaru d2830c1603 Translated using Weblate (Romanian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ro/
2026-02-19 03:09:50 +01:00
peter cerny 6091fd6f79 Translated using Weblate (Slovak)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2026-02-19 03:09:48 +01:00
herny ucet c9935eca2b Translated using Weblate (Slovak)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2026-02-19 03:09:47 +01:00
Gneb 38eacdd7b4 Translated using Weblate (Slovak)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2026-02-19 03:09:46 +01:00
Igor Dobrača 1bde917b09 Translated using Weblate (Croatian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2026-02-16 06:11:22 +01:00
ugyes 0859e219fa Translated using Weblate (Hungarian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2026-02-16 06:11:22 +01:00
Karl Bernstål dac9279320 Translated using Weblate (Swedish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2026-02-16 06:11:21 +01:00
advplyr 4fb9f6b92d Version bump 0.12.0-beta 2026-02-09 17:28:57 -06:00
advplyr fabd579247 Merge pull request #1797 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2026-02-09 17:14:58 -06:00
Yusuke Sakai b4ee3b80a8 Translated using Weblate (Japanese)
Currently translated at 54.8% (205 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ja/
2026-02-10 00:14:41 +01:00
Bjorn Jorgensen a75bdce9b4 Translated using Weblate (Danish)
Currently translated at 97.0% (363 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2026-02-10 00:14:41 +01:00
Luis Landeiro d48ac952a5 Translated using Weblate (Portuguese (Brazil))
Currently translated at 98.1% (367 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2026-02-10 00:14:40 +01:00
advplyr d83b2e032b Merge pull request #1749 from danyhoron/master
feat: implement download queue on iOS
2026-02-09 17:14:30 -06:00
advplyr 46c9c3f006 Fix downloading page filename overflow 2026-02-08 17:06:23 -06:00
advplyr 4199add608 Merge pull request #1738 from eagleeyetom/chromecast-volume-fix-updated
Android cast: stabilize MediaSession volume handling; simplify Google…
2026-02-08 16:37:15 -06:00
advplyr 8311643e73 Merge pull request #1774 from FreedomBen/dont-queue-background-display-events
Significantly improve Android UI performance when resuming from background
2026-02-08 16:08:49 -06:00
advplyr 7775d5216f Merge pull request #1758 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2026-02-03 17:27:10 -06:00
Lluís Forns 6f2fc6b5c1 Translated using Weblate (Catalan)
Currently translated at 79.6% (298 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ca/
2026-02-03 20:10:04 +01:00
Napitauki 235cac7ab4 Translated using Weblate (Finnish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2026-02-03 20:10:04 +01:00
dapitch666 93b4acfe13 Translated using Weblate (French)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2026-02-02 20:02:04 +00:00
Jan-Eric Myhrgren bd7c73c48d Translated using Weblate (Swedish)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2026-02-02 20:02:02 +00:00
Dawid Kuźnicki 45ecca7ce4 Translated using Weblate (Polish)
Currently translated at 96.7% (362 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2026-02-02 20:02:00 +00:00
FiendFEARing 0a8c67cf13 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2026-02-02 20:01:59 +00:00
ugyes 316d3d8324 Translated using Weblate (Hungarian)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2026-02-02 20:01:57 +00:00
enosh 182d45b07c Translated using Weblate (Hebrew)
Currently translated at 94.9% (355 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/he/
2026-02-02 20:01:55 +00:00
Delta Umhöfer 58d8afca38 Translated using Weblate (German)
Currently translated at 100.0% (374 of 374 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2026-02-01 00:26:04 +01:00
dv4yGY2U 136709d7ae Translated using Weblate (Turkish)
Currently translated at 98.8% (359 of 363 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/tr/
2026-02-01 00:26:04 +01:00
bittin1ddc447d824349b2 b0d02aa769 Translated using Weblate (Swedish)
Currently translated at 100.0% (363 of 363 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2026-02-01 00:26:03 +01:00
Samuel Guerrero ca66a45ba4 Translated using Weblate (Spanish)
Currently translated at 99.1% (360 of 363 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2026-02-01 00:26:02 +01:00
FiendFEARing d7d4114ee6 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (363 of 363 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2026-02-01 00:26:02 +01:00
Mantas e589c0a7ea Translated using Weblate (Lithuanian)
Currently translated at 52.3% (189 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/lt/
2026-02-01 00:26:01 +01:00
Kabika82 eb2625a2df Translated using Weblate (Hungarian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2026-02-01 00:26:01 +01:00
N Visi aacbacf557 Translated using Weblate (Japanese)
Currently translated at 55.9% (202 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ja/
2026-02-01 00:26:00 +01:00
Henrik Lynge 0a7b129db2 Translated using Weblate (Danish)
Currently translated at 100.0% (361 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2026-02-01 00:25:59 +01:00
herny ucet c927264b62 Translated using Weblate (Slovak)
Currently translated at 100.0% (361 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sk/
2026-02-01 00:25:59 +01:00
xxzp3 808225351e Translated using Weblate (Danish)
Currently translated at 99.1% (358 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2026-02-01 00:25:58 +01:00
J. Lavoie 40ee8d5221 Translated using Weblate (Italian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2026-02-01 00:25:58 +01:00
J. Lavoie 8cc80e8378 Translated using Weblate (French)
Currently translated at 100.0% (361 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2026-02-01 00:25:57 +01:00
Marcin db5f249c14 Translated using Weblate (Polish)
Currently translated at 99.7% (360 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2026-02-01 00:25:56 +01:00
Mario e86028e577 Translated using Weblate (German)
Currently translated at 100.0% (361 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2026-02-01 00:25:56 +01:00
Jan-Eric Myhrgren 54bed2bee1 Translated using Weblate (Swedish)
Currently translated at 100.0% (361 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2026-02-01 00:25:55 +01:00
Petri Hämäläinen 1de78165d0 Translated using Weblate (Finnish)
Currently translated at 100.0% (361 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2026-02-01 00:25:55 +01:00
Ivan Smoliakov 4c9ed5e2bf Translated using Weblate (Russian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2026-02-01 00:25:54 +01:00
lambolighting a036a8bda6 Translated using Weblate (Greek)
Currently translated at 60.6% (219 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/el/
2026-02-01 00:25:53 +01:00
biuklija 5a482e603e Translated using Weblate (Croatian)
Currently translated at 100.0% (361 of 361 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2026-02-01 00:25:53 +01:00
FiendFEARing d5eb8a341e Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (361 of 361 strings)

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

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2026-02-01 00:25:51 +01:00
Ahetek 8584bebe6c Translated using Weblate (Polish)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2026-02-01 00:25:50 +01:00
advplyr 39cd6c65f2 Merge pull request #1763 from randomdude583/fix/android-aac-seek-issue
Enable constant bitrate seeking to fix AAC seek reset in ExoPlayer
2026-01-31 17:25:39 -06:00
advplyr 1056541dd2 Merge pull request #1772 from KiwiHour/sort-by-progress
added 'sort by progress' entries
2026-01-30 17:31:26 -06:00
advplyr f9a3a4a218 Merge pull request #1775 from anstosa/ansel/playnextepisodesort
Play Next Episode correctly for serial podcasts
2026-01-30 17:11:54 -06:00
advplyr 80bafd9281 Merge pull request #1755 from sir-wilhelm/localize
Localized various strings.
2026-01-30 16:59:16 -06:00
advplyr c06c17d3ae Update HeaderWelcome to include username 2026-01-30 16:50:40 -06:00
advplyr 2a7969fe68 Fix backButton dialog i18n strings 2026-01-30 16:47:51 -06:00
advplyr decda748ba Fix unnecessary $ in getString call 2026-01-30 16:22:57 -06:00
advplyr 427f858be5 Merge pull request #1767 from FreedomBen/add-more-backwards-forward-options
Expand time interval options for jump controls
2026-01-29 17:38:42 -06:00
advplyr 1043fb3f3e fix i18n string order 2026-01-29 17:33:58 -06:00
advplyr 7c5e8cb046 Revert audio player playerControls padding 2026-01-29 17:32:34 -06:00
advplyr 5fb8baa186 Remove icons from jump options, remove unused computed properties 2026-01-29 17:17:29 -06:00
advplyr f63b04451b Update jump label to only show on fullscreen audio player 2026-01-29 17:08:12 -06:00
advplyr a80722ff79 Fix auth re-login error updating vuex state 2026-01-29 16:41:16 -06:00
Daniel Horon 9d5cbd37c8 Merge branch 'advplyr:master' into master 2026-01-29 17:35:37 +02:00
KiwiHour fada8fccb1 made order-by menu scrollable 2026-01-12 00:07:34 +00:00
Ansel Santosa 9d650019cd Play Next Episode correctly for serial podcasts
The default episode sort correctly takes podcast type into account (oldest to newest for serial, newest to oldest for episodic). However the Play Next Episode button uses episodic sorting for all podcast types. This minimal change fixes that.
2026-01-07 22:19:27 -08:00
KiwiHour 915f3c88bc Removed whitespaces around LabelLibrarySortByProgress 2026-01-06 01:48:36 +00:00
Benjamin Porter 140bc7fbc5 Skip progress updates when backgrounded
When we are backgrounded, these queue events build up and can cause
performance issues as the queue is processed.  We don't need to process
these when backgrounded, so skip them.

Also add small delay when resuming for webview to load before sending it
update events
2026-01-05 16:58:10 -07:00
KiwiHour 2a12c1e811 added 'sort by progress' entries, strings added for en-us 2026-01-05 23:50:15 +00:00
Benjamin Porter 5250adb36e Don't queue display events while backgrounded
When we are backgrounded, don't queue display events.  This prevents
a hang and sluggish replay of display events once we are re-foregrounded
later
2026-01-04 10:03:50 -07:00
Benjamin Porter f548163689 Make jump labels compatible with i18n/translation 2025-12-29 17:08:37 -07:00
Benjamin Porter 589ffcecca Move jump back/forward labels below the icon
Move jump back/forward labels below the icon.  Previously they
were on the side which looked a little awkward
2025-12-29 14:09:01 -07:00
Benjamin Porter d9c591b0ea Expand time interval options for Make jump controls
Fixes #1445

- Added additional options for customizing jump forward/backward.  Instead of just 5,
  10, and 30 seconds, we now have 5s, 10s, 15s, 30s, 60s, 2m, and 5m.
- Switched the jump interval to a list in the settings menu for better UX
- Reordered "Disable auto rewind time" for aesthetics in settings menu (it looked
  out of place after backwards time and forwards time were turned to dropdowns

The only downside of this (afaict), and what created the biggest challenge,
is the lack of numeric glyphs in the Material Symbols font.  They only have 5, 10,
and 30, which we were previously using.  Given that they don't have others, that means
we had to change strategies for displaying.  I switched all to generic backward and
forward icons and added the time interval as a label next to it so the user can still
tell at a glance what the setting is.  For consistency we don't use the numbered
glyphs anymore.  For more info on the Material Symbols font, see:
https://fonts.google.com/icons?icon.query=forward

However this does feel like two steps forward, one step back, though I can't think
of good options.  Other possibility would be to create SVGs, though I'm not skilled
enough to do that.  Personally this feels like a good compromise, but others will
probably differ. I considered using the numbered glyphs when the selected value was
5, 10, or 30, but when testing I found the inconsistency jarring and kind of janky.
2025-12-29 12:55:18 -07:00
sir-wilhelm 462021225f Localized various strings.
Confirm title, corresponding messages, welcome, and media type dropdown.
2025-12-28 10:36:17 -06:00
Alex 3f0bc74c21 Enable constant bitrate seeking to fix AAC seek reset in ExoPlayer 2025-12-24 13:03:27 -06:00
advplyr b2c2b625e3 Cleanup ItemSearchCard remove unused match properties, add LabelByAuthor string 2025-12-16 17:44:34 -06:00
advplyr d3bd3427e3 Merge pull request #1733 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-12-14 16:35:05 -06:00
lambolighting a040a73174 Translated using Weblate (Greek)
Currently translated at 24.4% (88 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/el/
2025-12-14 22:00:20 +00:00
lambolighting 332d9f203e Translated using Weblate (Greek)
Currently translated at 8.3% (30 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/el/
2025-12-12 23:13:10 +00:00
Lucas Jaksys 2d97329836 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2025-12-12 23:13:10 +00:00
kfctatertot 730d030623 Translated using Weblate (Arabic)
Currently translated at 99.7% (359 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-12-12 23:13:09 +00:00
kfctatertot 2e29daab1d Translated using Weblate (Spanish)
Currently translated at 99.7% (359 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2025-12-12 23:13:08 +00:00
zard Kim d7b48b4f61 Translated using Weblate (Korean)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ko/
2025-12-12 23:13:08 +00:00
advplyr 24b98f10e3 Added translation using Weblate (Greek) 2025-12-12 23:13:07 +00:00
Igor Dobrača 5fd6f5e4cc Translated using Weblate (Croatian)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2025-12-12 23:13:06 +00:00
thehijacker c21af649c3 Translated using Weblate (Slovenian)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sl/
2025-12-12 23:13:06 +00:00
Alessandro Burzio 1d8a23922e Translated using Weblate (Italian)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2025-12-12 23:13:05 +00:00
J. Lavoie 68893ab1f4 Translated using Weblate (German)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-12-12 23:13:04 +00:00
Petri Hämäläinen dcf172bd98 Translated using Weblate (Finnish)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2025-12-12 23:13:04 +00:00
J. Lavoie 6dd466a9ce Translated using Weblate (French)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2025-12-12 23:13:03 +00:00
advplyr ca222d4110 Added translation using Weblate (Icelandic) 2025-12-12 23:13:02 +00:00
Ivan Smoliakov c9af9582e3 Translated using Weblate (Russian)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2025-12-12 23:13:01 +00:00
zard Kim a3028127ce Translated using Weblate (Korean)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ko/
2025-12-12 23:13:01 +00:00
Plazec 23133194b2 Translated using Weblate (Czech)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2025-12-12 23:13:00 +00:00
Renaldas Repečka b84555db6c Translated using Weblate (Lithuanian)
Currently translated at 51.9% (187 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/lt/
2025-12-12 23:12:59 +00:00
Artur 229298df1b Translated using Weblate (Polish)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-12-12 23:12:59 +00:00
Cèlia Garriga fb270cb50b Translated using Weblate (Catalan)
Currently translated at 78.0% (281 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ca/
2025-12-12 23:12:58 +00:00
B0rax a861c880ba Translated using Weblate (German)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2025-12-12 23:12:57 +00:00
Jan-Eric Myhrgren 9712d3f527 Translated using Weblate (Swedish)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2025-12-12 23:12:57 +00:00
ugyes 51a7ce41de Translated using Weblate (Hungarian)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2025-12-12 23:12:56 +00:00
Максим Горпиніч 4c657c66d0 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2025-12-12 23:12:55 +00:00
FiendFEARing 61c3fafd94 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (360 of 360 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2025-12-12 23:12:54 +00:00
advplyr bab95530c8 Update WrappingMarquee to use textContent 2025-12-12 17:12:39 -06:00
Dani Horon 68afba45bc feat: implement download queue on iOS 2025-12-10 17:55:43 +02:00
eagleeyetom 1a52f19b32 Android cast: stabilize MediaSession volume handling; simplify Google Cast volume 2025-11-27 00:32:33 +01:00
advplyr aaef0a7e8d Update github issue templates with 0.11.0 2025-11-25 17:28:10 -06:00
advplyr 02d9023f9d Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2025-11-24 14:52:04 -06:00
advplyr 59a27cdd4d Add Turkish language option #1734 2025-11-24 14:51:59 -06:00
advplyr 9440782027 iOS version bump 0.11.0 2025-11-23 17:20:32 -06:00
advplyr f19ec05780 Version bump v0.11.0-beta 2025-11-23 17:00:45 -06:00
advplyr d95b75e317 Increase padding on reader toolbar for ios 2025-11-23 16:54:36 -06:00
advplyr a6dec31461 Revert #1727 2025-11-23 16:23:53 -06:00
advplyr b12427daa6 Merge pull request #1727 from indiefan/lost-progress-fix
Pause and end playback if the Vue layer receives a progress update wh…
2025-11-23 15:50:41 -06:00
advplyr 892686b213 Update components/app/AudioPlayer.vue 2025-11-23 15:44:41 -06:00
advplyr 81e71cd7a2 Fix disconnect in side drawer causing audio playing with player closed #1385 2025-11-23 15:16:44 -06:00
advplyr 38a5e3637d Merge pull request #1730 from eagleeyetom/feature/cast-volume-fix
Android: fix Cast volume to control receiver
2025-11-23 14:31:15 -06:00
advplyr eb16e769fe Remove logo shown on server connect loading 2025-11-22 17:31:54 -06:00
advplyr 9417f0dd3d Merge pull request #1732 from jamerst/days-in-a-row-today-optional
Don't require listening today for 'Days in a row' stat
2025-11-22 17:27:57 -06:00
advplyr 68d82f7a3a Fix unable to delete servers that are unreachable, include delete icon btn on server item #1703 2025-11-21 17:24:16 -06:00
James Tattersall b8fa041dcb Don't require listening today for 'Days in a row' stat 2025-11-21 16:54:13 +00:00
advplyr 1df07dc58d Merge pull request #1688 from weilBaum9/master
Add font selection setting in epub e-reader to match web-client (#1373)
2025-11-19 17:01:39 -06:00
advplyr e63358f776 Fix e-reader settings modal on landscape 2025-11-19 17:01:06 -06:00
advplyr 9518189898 Add font family Sans and Serif translations 2025-11-19 16:46:39 -06:00
advplyr 70a9cab6f2 Merge pull request #1706 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-11-19 08:06:56 -06:00
advplyr 495ec80ebd Added translation using Weblate (Korean) 2025-11-19 15:03:24 +01:00
Mikael Engström 34454fee10 Translated using Weblate (Swedish)
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/sv/
2025-11-18 23:28:18 +00:00
Milo Ivir fbac4be4a5 Translated using Weblate (Croatian)
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/hr/
2025-11-18 23:28:17 +00:00
phewi a150e6109e Translated using Weblate (Finnish)
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/fi/
2025-11-18 23:28:16 +00:00
Lucas Gonçalves Jaksys f392075836 Translated using Weblate (Portuguese (Brazil))
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/pt_BR/
2025-11-18 23:28:15 +00:00
Petri Hämäläinen 5d9dd394ea Translated using Weblate (Finnish)
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/fi/
2025-11-18 23:28:14 +00:00
Aleksandr Zakirov 162c189d4f Translated using Weblate (Estonian)
Currently translated at 49.5% (177 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/et/
2025-11-18 23:28:13 +00:00
advplyr 01902f8a79 Merge pull request #1679 from clayreimann/feat-ios-logs
feat: add logs on iOS
2025-11-18 17:28:00 -06:00
advplyr 399a01a937 iOS fix onLog listener event for realtime logs 2025-11-18 17:24:49 -06:00
eagleeyetom dcaa19071f Android: fix Cast volume to control receiver
- Route hardware volume keys to Cast via MediaSession VolumeProvider
- Use CastSession.volume (0–1) mapped to 0–100; clamp and step by 1
- Update provider with actual device volume after each change (prevents stalls)
- Drop stream-volume/polling attempts and other leftovers
- Expose get/set/adjust device volume commands to the player
2025-11-18 20:57:27 +01:00
advplyr a6280805e6 Merge pull request #1685 from sir-wilhelm/chapter_duration_player_modal
Adds a player preference to use chapter duration in the chapters modal view
2025-11-16 17:51:11 -06:00
advplyr 3d8843109a Update chapters modal to always show both start and duration, add sticky header 2025-11-16 17:44:11 -06:00
advplyr 16eaccc8e3 Merge pull request #1683 from sir-wilhelm/chapter_duration_chapters_table
Add chapter duration to chapters table on book page
2025-11-16 17:00:14 -06:00
advplyr 9df996a507 Merge pull request #1666 from mfulgo/master
Allows widget on Samsung cover screens
2025-11-16 16:22:06 -06:00
Marke Hallowell fa0b9ed6ab Pause and end playback if the Vue layer receives a progress update while having no played track ref. 2025-11-15 16:49:00 -08:00
sir-wilhelm 9b8536a581 Adds a player setting to use chapter duration in the player Chapter modal.
Allows users to view chapter duration instead of start time in the player chapter list.
2025-11-04 19:37:31 -06:00
sir-wilhelm 9158f4f8f1 Add chapter duration next to start time.
This adds the duration to the chapter table on the book page.
2025-11-03 17:39:44 -06:00
advplyr 072f19e157 Merge pull request #1647 from IzzySoft/fastlane
initial fastlane structures from IzzyOnDroid
2025-10-28 08:31:26 -05:00
advplyr 44198fa17a Merge pull request #1701 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-10-21 17:27:00 -05:00
Petri Hämäläinen 83ccb398c5 Translated using Weblate (Finnish)
Currently translated at 97.7% (349 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2025-10-17 00:06:25 +02:00
burghy86 824e335dec Translated using Weblate (Italian)
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/it/
2025-10-17 00:06:24 +02:00
nlqog e28e9d71ae Translated using Weblate (Portuguese (Brazil))
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/pt_BR/
2025-10-14 05:07:24 +00:00
pmangro 46acc76f07 Translated using Weblate (Portuguese (Brazil))
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/pt_BR/
2025-10-11 13:58:53 +02:00
Coxe 356fa4eb7c Translated using Weblate (Danish)
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/da/
2025-10-11 13:58:53 +02:00
pryszczoskor 09a6c631cb Translated using Weblate (Polish)
Currently translated at 99.4% (355 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-10-10 09:07:24 +00:00
Hezha cf19816e06 Translated using Weblate (Arabic)
Currently translated at 98.5% (352 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ar/
2025-10-08 22:55:30 +02:00
advplyr 2c4b9f5a50 Merge pull request #1691 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-10-07 16:25:36 -05:00
Ahetek 9e29d45fc3 Translated using Weblate (Polish)
Currently translated at 99.4% (355 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-10-07 03:02:02 +00:00
Kabika82 b4e350a308 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-10-07 03:02:01 +00:00
SmileFate 8274072b6a Translated using Weblate (Turkish)
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/tr/
2025-10-01 09:02:06 +00:00
DR 1ddd146558 Translated using Weblate (Hebrew)
Currently translated at 98.5% (352 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/he/
2025-10-01 09:02:05 +00:00
Jan 2d76fc6e8c Translated using Weblate (German)
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/de/
2025-10-01 09:02:03 +00:00
B0rax e19e541a70 Translated using Weblate (German)
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/de/
2025-10-01 09:02:02 +00:00
kuci-JK 3a56c82501 Translated using Weblate (Czech)
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/cs/
2025-10-01 09:02:00 +00:00
Petri Hämäläinen 2f0e385ca2 Translated using Weblate (Finnish)
Currently translated at 97.4% (348 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2025-09-29 05:02:00 +00:00
Amirhossein Ghorbanmehr 9241a4729c Translated using Weblate (Persian)
Currently translated at 15.4% (55 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fa/
2025-09-25 05:02:01 +02:00
SmileFate ea331bd4ef Translated using Weblate (Turkish)
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/tr/
2025-09-25 05:02:00 +02:00
advplyr c2b598d98e Added translation using Weblate (Persian) 2025-09-23 15:38:29 +02:00
advplyr 2fde05412c Merge pull request #1681 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-09-22 09:41:31 -05:00
Gernomaly d8436d3273 Translated using Weblate (German)
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/de/
2025-09-21 02:02:00 +02:00
weilBaum9 e662dae396 Add font selection setting in epub e-reader to match web-client (#1373) 2025-09-16 17:32:00 +02:00
Milo Ivir e74c5d8609 Translated using Weblate (Croatian)
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/hr/
2025-09-11 10:01:59 +00:00
Satanowski 3f05a8408d Translated using Weblate (Polish)
Currently translated at 99.4% (355 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2025-09-09 21:02:05 +00:00
Milo Ivir 61f0ba4643 Translated using Weblate (Croatian)
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/hr/
2025-09-09 21:02:04 +00:00
Clay Jensen-Reimann b773ec7690 feat: usable logs on iOS
- enable logs page on iOS
- centralize all logging
- wrap long identifiers in logs UI
- update editorconfig for swift files
- update podfile
2025-09-07 16:31:59 -05:00
peter cerny df294730a0 Translated using Weblate (Slovak)
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/sk/
2025-09-06 21:01:58 +00:00
Troj@ ee1cd38fa2 Translated using Weblate (Belarusian)
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/be/
2025-09-06 21:01:57 +00:00
jhonthan a012328f4c Translated using Weblate (Portuguese (Brazil))
Currently translated at 84.8% (303 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2025-09-06 21:01:56 +00:00
kuci-JK 1df9656191 Translated using Weblate (Czech)
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/cs/
2025-09-06 21:01:55 +00:00
petr-prikryl fde2d0887e Translated using Weblate (Czech)
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/cs/
2025-09-06 21:01:54 +00:00
advplyr 356e439715 Update app versions in github issue templates 0.10.1 2025-09-04 10:47:04 -05:00
advplyr 00ee7b662e iOS version bump 0.10.1 2025-09-03 17:27:41 -05:00
advplyr b222b2a5e5 Version bump v0.10.1-beta 2025-09-03 16:19:04 -05:00
advplyr 15b212734e Merge pull request #1669 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2025-09-03 17:00:11 -04:00
icutehunter 631ba52ffb Translated using Weblate (Turkish)
Currently translated at 76.7% (274 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/tr/
2025-09-03 01:01:59 +02:00
kuci-JK a4b58a893d Translated using Weblate (Czech)
Currently translated at 98.8% (353 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2025-09-03 01:01:57 +02:00
Hnatiucb 47a3fd9462 Translated using Weblate (Romanian)
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/ro/
2025-08-31 23:41:26 +02:00
Hnatiucb abcd56f0c5 Translated using Weblate (Romanian)
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/ro/
2025-08-31 23:41:25 +02:00
Hnatiucb b498b5eafa Translated using Weblate (Romanian)
Currently translated at 23.2% (83 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ro/
2025-08-31 23:41:25 +02:00
tngch be114e7789 Translated using Weblate (Japanese)
Currently translated at 45.3% (162 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ja/
2025-08-31 23:41:24 +02:00
Jan-Eric Myhrgren 8c44142e70 Translated using Weblate (Swedish)
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/sv/
2025-08-31 23:41:23 +02:00
Hnatiucb 5a655f77d2 Translated using Weblate (Romanian)
Currently translated at 19.3% (69 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ro/
2025-08-31 23:41:23 +02:00
Jan-Eric Myhrgren c825f791d0 Translated using Weblate (Swedish)
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/sv/
2025-08-31 23:41:22 +02:00
Jan-Eric Myhrgren 83a9d8a162 Translated using Weblate (Swedish)
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/sv/
2025-08-31 23:41:22 +02:00
Mathias Franco acfa2e2183 Translated using Weblate (Dutch)
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/nl/
2025-08-31 23:41:21 +02:00
Jan-Eric Myhrgren 591933f3d8 Translated using Weblate (Swedish)
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/sv/
2025-08-31 23:41:21 +02:00
idojius86 982f94d955 Translated using Weblate (Spanish)
Currently translated at 98.5% (352 of 357 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2025-08-31 23:41:20 +02:00
advplyr 95bf28d7c6 Fix tab focus for appbar and sidenav, add tabindex to book card 2025-08-31 16:41:13 -05:00
advplyr 48a299de45 Merge pull request #1675 from laurisvr/remove-busy-loops
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
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
Matt Fulgo 941e823374 Allows widget on Samsung cover screens
Adds meta-data to the widget, allowing it to be used on the cover screen
of Samsung devices such as the Flip.

The widget renders as a 4x4 grid (not an optimal layout) but functions
for launching the most recent audiobook or controlling playback.
2025-08-19 22:24:58 -04: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 2025-08-16 17:55:00 -05:00
advplyr 7aebcd92c3 Fix refresh token not persisted on new server connections #1634 2025-08-15 17:41:23 -05:00
advplyr bd8668f0bf Fix rss feed modal not showing full URL once open #1652 2025-08-08 17:39:27 -05:00
advplyr c6a7c6fec2 Merge pull request #1637 from weblate/weblate-audiobookshelf-abs-mobile-app
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
Izzy f74a1ceb47 initial fastlane structures from IzzyOnDroid 2025-08-03 01:11:32 +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
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 2025-07-21 08:57:56 -05:00
advplyr 04c70f54aa iOS version bump 0.10.0 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
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 2025-07-17 17:37:41 -05:00
advplyr 224d75fac5 Update old auth alert messages, add link to github discussion 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 2025-07-11 16:58:50 -05:00
advplyr beb654825f Merge pull request #1623 from weblate/weblate-audiobookshelf-abs-mobile-app
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 2025-07-10 17:18:38 -05:00
advplyr beb5e1a56c OIDC to support new access tokens 2025-07-07 17:20:22 -05:00
advplyr be08efeca3 iOS update retry request handler to handle string bodies 2025-07-07 06:15:51 -05:00
advplyr 4534ffaead Handle re-authenticating socket 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 2025-07-05 18:06:24 -05:00
advplyr fab94cd363 Handle native app token refresh failure notification 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 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
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 2025-07-04 17:41:19 -05:00
advplyr ba4e9ab7e3 Add explicit library filters 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 2025-06-29 17:32:29 -05:00
advplyr b9c084a442 Merge pull request #1612 from advplyr/create_bookmarks_focus
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
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
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
advplyrandISO-B 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
RaHoniandadvplyr 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
advplyr 8bc5932889 Version bump v0.9.76-beta 2024-08-13 10:26:08 -05:00
advplyr 3cd597d71e Merge pull request #1286 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-08-13 10:19:04 -05:00
Fredrik Lindqvist 48d5ad462e Translated using Weblate (Swedish)
Currently translated at 81.1% (249 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2024-08-13 17:18:39 +02:00
advplyr 40cab87640 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2024-08-13 10:18:28 -05:00
advplyr f405947ad8 Add:Random library sort #1285 2024-08-13 10:18:23 -05:00
advplyr fd550c15a5 iOS version bump 0.9.75 2024-08-12 17:38:44 -05:00
advplyr d18537cf04 Version bump v0.9.75-beta & targetSdkVersion bump to 34 2024-08-12 17:24:05 -05:00
advplyr f01f0229a8 Merge pull request #1278 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-08-12 16:40:47 -05:00
Charlie b369eb2d40 Translated using Weblate (French)
Currently translated at 100.0% (307 of 307 strings)

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

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-08-12 23:34:46 +02:00
tonttula 1043f4ecd7 Translated using Weblate (Finnish)
Currently translated at 72.3% (222 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2024-08-12 23:34:46 +02:00
SunSpring 4f17a32373 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-08-12 23:34:46 +02:00
Charlie 2203aae6e6 Translated using Weblate (French)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-08-12 23:34:46 +02:00
advplyr 7b60c7b6c5 Merge pull request #1279 from nichwall/release_close_issue
Add: workflow to automatically close issues on release
2024-08-12 16:34:41 -05:00
Nicholas Wallace be98e06efc Add: workflow to automatically close issues on release 2024-08-06 21:02:27 -07:00
advplyr 45239bca34 Merge pull request #1258 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-08-04 10:09:21 -05:00
ISO-B 84509ec1a8 Added ability to download whole serie from serie view 2024-07-27 23:26:57 +03:00
burghy86 2493ce7ed7 Translated using Weblate (Italian)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2024-07-26 04:45:02 +00:00
advplyr 2b1e53b371 Remove references to matrix server 2024-07-23 16:22:24 -05:00
weblate.user.1274 d643a1769f Translated using Weblate (Norwegian Bokmål)
Currently translated at 84.0% (258 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2024-07-23 23:19:53 +02:00
Mario a63b7a188c Translated using Weblate (German)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-07-23 23:19:53 +02:00
gallegonovato f9c0162b8a Translated using Weblate (Spanish)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-07-23 23:19:52 +02:00
Petteri Hjort cda3cb3512 Translated using Weblate (Finnish)
Currently translated at 49.8% (153 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2024-07-23 23:19:52 +02:00
Marcin Martela 766f6938a7 Translated using Weblate (Polish)
Currently translated at 96.0% (295 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2024-07-23 23:19:51 +02:00
tonttula 8e1ccb517b Translated using Weblate (Finnish)
Currently translated at 48.2% (148 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2024-07-23 23:19:51 +02:00
Mario 540aab6f83 Translated using Weblate (German)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-07-23 23:19:50 +02:00
Charlie d1969d7897 Translated using Weblate (French)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-07-23 23:19:50 +02:00
Ahetek b69e8fd649 Translated using Weblate (Polish)
Currently translated at 93.4% (287 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2024-07-23 23:19:49 +02:00
Lode Smets 2e95cf908d Translated using Weblate (Dutch)
Currently translated at 64.8% (199 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2024-07-23 23:19:49 +02:00
Mario b589bae2b0 Translated using Weblate (German)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-07-23 23:19:48 +02:00
Ahetek e7e4891d82 Translated using Weblate (Polish)
Currently translated at 93.4% (287 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2024-07-23 23:19:48 +02:00
shaun 79df676a13 split groups of save history items that have different status results 2024-07-16 18:01:09 +10:00
advplyr 03a46decba Fix:Server connect form press enter to submit #1158 2024-07-08 17:21:31 -05:00
advplyr d80482f443 Merge pull request #1250 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-06-28 15:50:19 -05:00
J. Lavoie 735cce7614 Translated using Weblate (Finnish)
Currently translated at 39.0% (120 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2024-06-27 04:45:17 +00:00
J. Lavoie 8ea35a0b36 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.3% (305 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2024-06-27 04:45:16 +00:00
J. Lavoie ad90bd3d05 Translated using Weblate (Norwegian Bokmål)
Currently translated at 81.1% (249 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2024-06-27 04:45:15 +00:00
J. Lavoie fa3856e845 Translated using Weblate (Italian)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2024-06-27 04:45:15 +00:00
dex girl 752d4ddc8c Translated using Weblate (Croatian)
Currently translated at 69.0% (212 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-06-27 04:45:05 +00:00
J. Lavoie fd2d7a05d1 Translated using Weblate (French)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-06-27 04:45:04 +00:00
J. Lavoie ccf4675867 Translated using Weblate (Spanish)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-06-27 04:45:03 +00:00
J. Lavoie bc043c73ca Translated using Weblate (German)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-06-27 04:45:02 +00:00
Allan Nordhøy 291ab247bc Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-06-25 16:34:50 +02:00
Allan Nordhøy d901f470a1 Translated using Weblate (Vietnamese)
Currently translated at 85.6% (263 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/vi/
2024-06-25 16:34:50 +02:00
Allan Nordhøy 0ebb01e39d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2024-06-25 16:34:50 +02:00
Allan Nordhøy 20ac99bf8d Translated using Weblate (Swedish)
Currently translated at 59.2% (182 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/sv/
2024-06-25 16:34:50 +02:00
Allan Nordhøy 90e86e1575 Translated using Weblate (Russian)
Currently translated at 99.3% (305 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2024-06-25 16:34:50 +02:00
Allan Nordhøy 28652031e7 Translated using Weblate (Portuguese (Brazil))
Currently translated at 99.3% (305 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2024-06-25 16:34:50 +02:00
Allan Nordhøy cd8be40cc8 Translated using Weblate (Polish)
Currently translated at 90.2% (277 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pl/
2024-06-25 16:34:50 +02:00
Allan Nordhøy 52fc937f96 Translated using Weblate (Norwegian Bokmål)
Currently translated at 81.1% (249 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2024-06-25 16:34:49 +02:00
Allan Nordhøy 68e4722387 Translated using Weblate (Dutch)
Currently translated at 57.0% (175 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nl/
2024-06-25 16:34:49 +02:00
Allan Nordhøy 5f5b38b2f9 Translated using Weblate (Lithuanian)
Currently translated at 59.2% (182 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/lt/
2024-06-25 16:34:49 +02:00
Allan Nordhøy 65f8d3ce81 Translated using Weblate (Italian)
Currently translated at 96.4% (296 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2024-06-25 16:34:49 +02:00
Allan Nordhøy ac571fd63d Translated using Weblate (Hungarian)
Currently translated at 91.5% (281 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hu/
2024-06-25 16:34:49 +02:00
Allan Nordhøy ea2d6e9f31 Translated using Weblate (Croatian)
Currently translated at 46.5% (143 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-06-25 16:34:49 +02:00
Allan Nordhøy 80b999d648 Translated using Weblate (Hindi)
Currently translated at 24.1% (74 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hi/
2024-06-25 16:34:49 +02:00
Allan Nordhøy 54720039e4 Translated using Weblate (Gujarati)
Currently translated at 23.4% (72 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/gu/
2024-06-25 16:34:49 +02:00
Allan Nordhøy 8c6717c201 Translated using Weblate (French)
Currently translated at 99.6% (306 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-06-25 16:34:49 +02:00
Allan Nordhøy 6610754e25 Translated using Weblate (Spanish)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-06-25 16:34:49 +02:00
Allan Nordhøy d38622d8a2 Translated using Weblate (German)
Currently translated at 99.6% (306 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-06-25 16:34:49 +02:00
Allan Nordhøy cd2750a489 Translated using Weblate (Danish)
Currently translated at 64.1% (197 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2024-06-25 16:34:48 +02:00
Allan Nordhøy 30140629ae Translated using Weblate (Czech)
Currently translated at 96.0% (295 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-06-25 16:34:48 +02:00
Allan Nordhøy 0ce29337ef Translated using Weblate (Norwegian Bokmål)
Currently translated at 69.7% (214 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2024-06-25 15:36:41 +02:00
Allan Nordhøy 6715f29ebb Translated using Weblate (Norwegian Bokmål)
Currently translated at 68.7% (211 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/nb_NO/
2024-06-25 13:32:12 +00:00
phewi b4a8dc9e26 Translated using Weblate (Finnish)
Currently translated at 36.4% (112 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2024-06-25 13:32:12 +00:00
advplyr c349a0bf00 Merge pull request #1248 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-06-25 08:32:05 -05:00
SunSpring 0dc6c142be Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-06-21 04:45:03 +00:00
Plazec e46c1d9120 Translated using Weblate (Czech)
Currently translated at 96.4% (296 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-06-20 04:45:02 +00:00
advplyr 27ad554ea1 Merge pull request #1245 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-06-19 15:18:59 -05:00
Charlie 5660772b46 Translated using Weblate (French)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-06-19 06:45:03 +02:00
kuci-JK dab80642ab Translated using Weblate (Czech)
Currently translated at 95.7% (294 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-06-19 06:45:02 +02:00
Nicholas W da46191208 Translated using Weblate (Bengali)
Currently translated at 57.6% (177 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/bn/
2024-06-18 22:01:23 +02:00
JL 6484c4de79 Translated using Weblate (Danish)
Currently translated at 64.1% (197 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/da/
2024-06-18 22:01:23 +02:00
kuci-JK 6716b6042a Translated using Weblate (Czech)
Currently translated at 94.1% (289 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-06-18 22:01:23 +02:00
Plazec 0686859fa7 Translated using Weblate (Czech)
Currently translated at 94.1% (289 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-06-18 22:01:23 +02:00
advplyr 4180862c3a Fix: Sleep Timer modal defaults to Custom Time during first access #1224 2024-06-17 16:44:06 -05:00
Plazec 7aa20d3211 Translated using Weblate (Czech)
Currently translated at 92.8% (285 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/cs/
2024-06-17 23:42:10 +02:00
pmangro b694b55710 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2024-06-17 23:42:10 +02:00
pmangro 024b613063 Translated using Weblate (Portuguese (Brazil))
Currently translated at 93.8% (288 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/pt_BR/
2024-06-17 23:42:10 +02:00
Smoukus 8768d11a20 Translated using Weblate (Croatian)
Currently translated at 46.5% (143 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/hr/
2024-06-17 23:42:10 +02:00
Valentin 5898303ba4 Translated using Weblate (German)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-06-17 23:42:10 +02:00
advplyr e79d09941c Update:Editor config/vscode settings/prettierrc 2024-06-16 09:49:05 -05:00
advplyr c52a60fe77 Add:Finnish language option 2024-06-16 09:44:09 -05:00
advplyr e64f9aa427 Merge pull request #1239 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-06-16 09:40:59 -05:00
Petteri Hjort ee343f78ef Translated using Weblate (Finnish)
Currently translated at 27.3% (84 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fi/
2024-06-16 06:45:04 +02:00
Vito0912 1f20d5821f Translated using Weblate (German)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-06-16 06:45:03 +02:00
Nicholas W b0c512e946 Added translation using Weblate (Finnish) 2024-06-15 16:32:35 +02:00
Charlie 0d13b7abd1 Translated using Weblate (French)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-06-15 14:31:50 +00:00
SunSpring 43fd8285c9 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-06-15 06:45:09 +02:00
Illia Pyshniak 2f7e8d618a Translated using Weblate (Ukrainian)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/uk/
2024-06-15 06:45:08 +02:00
Dmitry a0b910deeb Translated using Weblate (Russian)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/ru/
2024-06-15 06:45:08 +02:00
Petras Šukys e9dc45613c Translated using Weblate (Lithuanian)
Currently translated at 59.2% (182 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/lt/
2024-06-15 06:45:07 +02:00
Pierrick Guillaume b5e90ec3d2 Translated using Weblate (French)
Currently translated at 87.9% (270 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/fr/
2024-06-15 06:45:06 +02:00
DiamondtipDR 3d9f42c1e1 Translated using Weblate (Spanish)
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-06-15 06:45:06 +02:00
SunSpring e62f430f37 Translated using Weblate (Chinese (Simplified))
Currently translated at 100.0% (307 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/zh_Hans/
2024-06-14 11:19:29 +02:00
burghy86 9c53564c46 Translated using Weblate (Italian)
Currently translated at 96.7% (297 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/it/
2024-06-14 11:19:29 +02:00
Daniel Schosser f7add6f6cd Translated using Weblate (German)
Currently translated at 99.6% (306 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-06-14 11:19:29 +02:00
Mario 7786e6a827 Translated using Weblate (German)
Currently translated at 99.6% (306 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-06-14 11:19:29 +02:00
apineiro97 b213e2aabf Translated using Weblate (Spanish)
Currently translated at 99.6% (306 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-06-14 06:45:03 +02:00
Vito0912 ebca0fafca Translated using Weblate (German)
Currently translated at 95.1% (292 of 307 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/de/
2024-06-14 06:45:02 +02:00
advplyr ac8a2ca985 Merge pull request #1235 from mfcar/mf/downloadTweaks
Tweak the download page (add library size and play episode)
2024-06-12 18:14:35 -05:00
advplyr 6380d7cc02 Fix local episode play-item event, set PodcastEpisode size and AudioTrack FileMetadata for downloads 2024-06-12 18:01:44 -05:00
advplyr 9056ba6b8d Merge pull request #1234 from nichwall/localization_workflow_update
Update i18n workflow to 1.3.0
2024-06-11 17:03:21 -05:00
advplyr 23801363c4 Merge pull request #1233 from weblate/weblate-audiobookshelf-abs-mobile-app
Translations update from Hosted Weblate
2024-06-11 17:03:01 -05:00
Marcos Carvalho 1cdcd7327f Add play downloaded episode and library size 2024-06-11 19:31:10 +01:00
Nicholas Wallace 50ec50003e Update i18n workflow to 1.3.0 2024-06-10 19:25:34 -07:00
gallegonovato 5ff4db1a3f Translated using Weblate (Spanish)
Currently translated at 100.0% (304 of 304 strings)

Translation: Audiobookshelf/Abs Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-mobile-app/es/
2024-06-10 21:56:03 +00:00
advplyr 394b44c343 Merge pull request #1222 from BimBimSalaBim/master
Update font settings in EpubReader and Reader components
2024-06-10 16:55:55 -05:00
advplyr eb2aa6f773 Map locally stored ebook settings onto defaults 2024-06-10 16:38:29 -05:00
advplyr 81130e04e5 Merge master 2024-06-10 16:32:09 -05:00
advplyr c0ec0758f4 Merge pull request #1228 from weblate/weblate-audiobookshelf-test-abs-mobile-app
Translations update from Hosted Weblate
2024-06-10 16:23:07 -05:00
Nicholas W e21f7c545e Translated using Weblate (Chinese (Simplified))
Currently translated at 96.0% (292 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/zh_Hans/
2024-06-09 21:51:48 +02:00
Nicholas W c16dd2fcf6 Translated using Weblate (Vietnamese)
Currently translated at 86.5% (263 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/vi/
2024-06-09 21:51:48 +02:00
Nicholas W 90974e6d73 Translated using Weblate (Ukrainian)
Currently translated at 95.7% (291 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/uk/
2024-06-09 21:51:48 +02:00
Nicholas W 8e4f75d85b Translated using Weblate (Swedish)
Currently translated at 59.5% (181 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/sv/
2024-06-09 21:51:48 +02:00
Nicholas W 792ee2adb8 Translated using Weblate (Russian)
Currently translated at 94.0% (286 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/ru/
2024-06-09 21:51:47 +02:00
Nicholas W 2c57489c07 Translated using Weblate (Portuguese (Brazil))
Currently translated at 94.0% (286 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/pt_BR/
2024-06-09 21:51:47 +02:00
Nicholas W e3321770fd Translated using Weblate (Polish)
Currently translated at 91.4% (278 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/pl/
2024-06-09 21:51:47 +02:00
Nicholas W 848673f028 Translated using Weblate (Norwegian Bokmål)
Currently translated at 57.8% (176 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/nb_NO/
2024-06-09 21:51:47 +02:00
Nicholas W e6d3003ecf Translated using Weblate (Dutch)
Currently translated at 57.5% (175 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/nl/
2024-06-09 21:51:47 +02:00
Nicholas W 90551b7ce7 Translated using Weblate (Lithuanian)
Currently translated at 59.5% (181 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/lt/
2024-06-09 21:51:46 +02:00
Nicholas W aabac5da13 Translated using Weblate (Italian)
Currently translated at 92.4% (281 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/it/
2024-06-09 21:51:46 +02:00
Nicholas W 4fad77f9c1 Translated using Weblate (Hungarian)
Currently translated at 92.7% (282 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/hu/
2024-06-09 21:51:46 +02:00
Nicholas W 0fb6d746eb Translated using Weblate (Croatian)
Currently translated at 45.7% (139 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/hr/
2024-06-09 21:51:46 +02:00
Nicholas W 7fc712d19b Translated using Weblate (Hindi)
Currently translated at 24.3% (74 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/hi/
2024-06-09 21:51:46 +02:00
Nicholas W d6018cfe10 Translated using Weblate (Gujarati)
Currently translated at 23.3% (71 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/gu/
2024-06-09 21:51:45 +02:00
Nicholas W 5b8f231a98 Translated using Weblate (French)
Currently translated at 86.8% (264 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/fr/
2024-06-09 21:51:45 +02:00
Nicholas W cb503f84e1 Translated using Weblate (Spanish)
Currently translated at 93.0% (283 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/es/
2024-06-09 21:51:45 +02:00
Nicholas W a34f9b80ac Translated using Weblate (Danish)
Currently translated at 58.5% (178 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/da/
2024-06-09 21:51:44 +02:00
Nicholas W 3e6696b516 Translated using Weblate (Czech)
Currently translated at 93.0% (283 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/cs/
2024-06-09 21:51:44 +02:00
Nicholas W 9cd8bd439a Translated using Weblate (English)
Currently translated at 20.3% (62 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/en/
2024-06-09 21:51:44 +02:00
Nicholas W ca5b99e23d Added translation using Weblate (Bengali) 2024-06-09 21:44:05 +02:00
Nicholas W c44fd74f5b Translated using Weblate (German)
Currently translated at 99.0% (301 of 304 strings)

Translation: Audiobookshelf/ABS Mobile App
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf-test/abs-mobile-app/de/
2024-06-09 21:40:09 +02:00
advplyr f82c4834c3 Merge pull request #1225 from lukasitaly/patch-1
Update de.json
2024-06-08 16:03:45 -05:00
advplyr dda95bd69c Merge pull request #1217 from mfcar/mf/networkStatusSwift
Implement network monitoring on iOS
2024-06-08 16:02:53 -05:00
Lukas a623cb57a7 Update de.json
Changed "Mobilfunknetz" to "mobile Daten", but I wouldn't really change the remaining strings with "Mobilfunknetz" just for the sake of uniformity, since "Mobilfunknetz" definitely fits better than "mobile Daten".
2024-06-08 17:01:49 +00:00
Lukas e798d930fb Update de.json
Translation of not yet translated strings
2024-06-07 08:42:05 +02:00
advplyr f91b0c9e2f Merge pull request #1210 from mfcar/mf/limitCellularData
Add settings to limit Cellular Data (Download/Streaming)
2024-06-04 16:56:01 -05:00
advplyr b6824cf242 Fix:Latest podcast episodes to play local 2024-06-04 16:50:55 -05:00
advplyr 41477fdac1 Merge translation strings 2024-06-04 16:28:35 -05:00
advplyr d4e8afc2f1 Map translation strings 2024-06-04 16:23:23 -05:00
advplyr d42cbfe3e4 Revert podcast episode local playback event 2024-06-04 16:21:28 -05:00
advplyr c7c3ef52e6 Update issue templates to 0.9.73 and 0.9.74 2024-06-04 10:25:01 -05:00
advplyr 92344325b6 Add 0.9.64 to issue templates 2024-06-04 10:23:21 -05:00
Faizan Zafar a8172fb3f9 Update font settings in EpubReader and Reader components to include a "Font Boldness" slider which applies a stroke to the text #3020 on main repo 2024-06-03 20:25:11 -07:00
advplyr a73ec783d5 Merge pull request #1221 from c-franco/update-spanish-translation
Update Spanish translation
2024-06-03 17:20:00 -05:00
Christian Franco 5c9fc4c9e4 Update Spanish translation 2024-06-03 16:31:40 +01:00
advplyr 494ff38ef3 Fix:Epub rendition resize on screen orientation change #1213 2024-06-02 16:20:41 -05:00
advplyr b7e6cbf1ab Fix:PDF ereader flickering 2024-06-02 15:54:50 -05:00
advplyr b33623b74f Merge pull request #1215 from Dalabad/patch-1
Update de.json
2024-05-28 17:24:57 -05:00
Marcos Carvalho 4f939ce6fc Implement network monitoring on iOS 2024-05-27 22:16:49 +01:00
Daniel Schosser 4f45f360df Update de.json 2024-05-27 22:50:11 +02:00
Marcos Carvalho 4ad54f34fe Improvements and fixes on the cellular permissions 2024-05-26 22:08:53 +01:00
Marcos Carvalho 574dda5286 Fix android build 2024-05-24 08:10:25 +01:00
Marcos Carvalho d788623509 Initial work 2024-05-23 22:33:06 +01:00
advplyr f047f6a23b Merge pull request #1208 from mfcar/mf/detectableTimestamp
feat: Add Time Marker Detection in Episode Descriptions
2024-05-21 17:11:49 -05:00
advplyr 48b30f6b03 iOS version bump 0.9.74 2024-05-20 09:42:43 -05:00
Marcos Carvalho 67d32164d9 Update the parse description to identify time marker in anchor links 2024-05-20 08:33:02 +01:00
advplyr 6672a21049 Version bump v0.9.74-beta 2024-05-19 16:58:20 -05:00
Marcos Carvalho b833019a14 Change href link to span, to avoid change the page and improve code 2024-05-19 14:14:47 +01:00
Marcos Carvalho 00eeeb2a5a Add detectable timestamp 2024-05-19 10:40:04 +01:00
advplyr 06a0a68143 Merge pull request #1205 from indiefan/command-target-duplication-fix
Fix bug iOS AudioPlayer where multiple calls to setupRemoteTransportC…
2024-05-17 17:05:14 -05:00
advplyr 1a7ae468bd Merge pull request #1206 from cor-bee/patch-1
Update uk.json
2024-05-17 16:50:49 -05:00
Illia Pyshniak e8f74d7358 Update uk.json 2024-05-17 14:00:58 +03:00
Marke Hallowell 984f01da23 Fix bug iOS AudioPlayer where multiple calls to setupRemoteTransportControls resulted in duplicate MPRemoteCommand targets being applied for the same action. 2024-05-16 19:11:38 -07:00
advplyr d6e75410a5 Merge pull request #1201 from nichwall/bug_report
App bug report format
2024-05-12 12:41:45 -05:00
Nicholas Wallace 7627e1b58c App bug report format 2024-05-11 18:24:38 -07:00
advplyr 5fb31b18ae Merge pull request #1199 from nichwall/feature_request
Add: feature request
2024-05-10 17:55:03 -05:00
Nicholas W 20903e8217 App selection allows for multiple 2024-05-09 07:12:02 -07:00
Nicholas W 8fe348d4eb Update feature_request.yaml 2024-05-09 06:40:06 -07:00
Nicholas Wallace 24cda8a179 Add: feature request 2024-05-08 23:30:13 -07:00
advplyr fc954a968b Remove old missing parts references 2024-04-28 16:38:40 -05:00
advplyr aae53a54cd Update:UI/UX: Add explicit indicator next to titles #1181 2024-04-28 16:34:31 -05:00
advplyr cfedcffc98 Add:No series filter option for book libraries #1184 2024-04-28 16:00:57 -05:00
advplyr 0e74c74a6c Merge pull request #1179 from indiefan/rate-manager
Refactor AudioPlayer to use a Rate Manager protocol.
2024-04-27 16:49:27 -05:00
advplyr fb86c45e78 iOS add defaultRate fixing merge 2024-04-27 16:41:29 -05:00
advplyr 124300b215 Merge branch 'master' into rate-manager 2024-04-27 16:06:20 -05:00
advplyr 6746c0627b Merge pull request #1178 from indiefan/master
Update Now Playing info with correct default rate.
2024-04-27 16:01:06 -05:00
Marke Hallowell 0f2cd3b132 Update Now Playing info with correct default rate so that third party apps don't mistake current status as scrubbing. 2024-04-21 15:39:37 -07:00
advplyr e94d6fdc1e Update:Android notification image to not resize #657 2024-04-19 16:32:16 -05:00
advplyr e301b3071c Update:Add 1.7x playback speed to modal #1180 2024-04-19 14:39:00 -05:00
Marke Hallowell 3d3dad5e4a Adding new sources to project file. 2024-04-14 16:47:38 -07:00
Marke Hallowell f8a9033738 Add an AudioPlayerRateManager implementation for iOS 16+. 2024-04-14 16:12:55 -07:00
Marke Hallowell edb25f5bcd Refactor AudioPlayer to use an AudioPlayerRateManager protocol for managing rate/defaultRate state management. Implements a LegacyAudioPlayerRateManager implementation with existing (pre-iOS 16) behavior. 2024-04-14 11:45:03 -07:00
Marke Hallowell bcb239efd1 Update Now Playing info with correct default rate so that third party apps don't mistake current status as scrubbing. 2024-04-13 23:10:03 -07:00
advplyr d208211c6f Merge pull request #1176 from Fymyte/patch-4
Update French Translation
2024-04-12 15:22:06 -05:00
Pierrick Guillaume 0b20b72241 Update fr.json 2024-04-12 20:32:47 +02:00
Pierrick Guillaume 5c29459af2 Update fr.json 2024-04-12 20:31:15 +02:00
advplyr e54fd38784 Merge pull request #1174 from nichwall/i18n-update
Update i18n workflow to use 1.2.0
2024-04-11 17:17:23 -05:00
Nicholas Wallace 94774f13df Update i18n workflow to use 1.2.0 2024-04-10 18:38:11 -07:00
advplyr 43ec35fe67 Fix:Android device info client version 2024-04-05 16:39:24 -05:00
advplyr b2645dc444 Merge pull request #1162 from springsunx/patch-1
Update zh-cn.json
2024-04-03 18:16:14 -05:00
SunX c79624a82c Update zh-cn.json 2024-04-03 09:06:13 +08:00
advplyr 8201f3c9a6 Fix:Close sleep timer custom timer sub modal on sleep timer close #1112 2024-04-01 17:35:22 -05:00
advplyr e0ac6a1c38 Merge pull request #1160 from pmangro/master
[PR-BR] Go to Web Client
2024-04-01 17:18:33 -05:00
pmangro 400da5566d [PR-BR] Go to Web Client 2024-03-31 19:50:45 -03:00
advplyr 9b67037774 Update:Support for comic files with webp images https://github.com/advplyr/audiobookshelf/issues/2792 2024-03-27 16:13:13 -05:00
advplyr 48596542c7 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2024-03-25 16:33:36 -05:00
advplyr 9715d65d88 Update:Go to web client buttons visible to all users #1079 2024-03-25 16:33:30 -05:00
advplyr 2e14b31a69 Merge pull request #1153 from burghy86/patch-4
Update it.json
2024-03-25 14:51:30 -05:00
burghy86 1c8ce6c3db Update it.json
monthly translation of new strings
2024-03-25 16:29:07 +01:00
advplyr dc74a6213d Merge pull request #1150 from nichwall/translation_faq_link
Translation guide link added
2024-03-24 05:06:47 -05:00
Nicholas Wallace e9a66bb7a7 Translation guide link added 2024-03-23 20:08:41 -07:00
advplyr 3c6d4b5f19 Add:Nav button in side drawer and item more menu to go to web client #1079 2024-03-22 17:54:56 -05:00
advplyr f04eebe0d8 Merge pull request #1142 from nichwall/translation_workflow_check
Add: i18n workflow check
2024-03-20 16:43:26 -05:00
advplyr 63b5227ccc Merge pull request #1140 from cor-bee/master
Update Ukrainian translation
2024-03-20 16:31:00 -05:00
Nicholas Wallace 934d9b1df4 Fixed release number of action 2024-03-19 21:19:38 -07:00
Nicholas Wallace 8994cb3a6e Added newline to force check 2024-03-19 21:12:52 -07:00
Nicholas Wallace 4a32de482b Example: German keys alphabetized 2024-03-19 21:09:49 -07:00
Nicholas Wallace 95f47793d2 Example: German strings out of order 2024-03-19 21:08:43 -07:00
Nicholas Wallace f27048618d Add: newline to end of file 2024-03-19 21:05:53 -07:00
Nicholas Wallace c90ad419e8 Add: i18n workflow check 2024-03-19 21:05:09 -07:00
Illia Pyshniak 2cdc3ecb9d Update uk.json 2024-03-19 23:09:35 +02:00
advplyr 9f89fcd968 Fix:Podcast library playlists page incorrect label in toolbar #1135 2024-03-19 12:52:14 -05:00
advplyr 7c2d51f811 Merge pull request #1136 from cor-bee/master
Ukrainian translation
2024-03-18 17:44:49 -05:00
advplyr 41fe7a29cd Merge 2024-03-18 17:41:13 -05:00
advplyr 54f3eec303 Merge pull request #1127 from nichwall/vietnamese_translations
Vietnamese translations
2024-03-18 17:36:58 -05:00
advplyr cd91013f69 Update Vietnamese datefnslocale code 2024-03-18 17:37:04 -05:00
advplyr 0af16ef05f Merge pull request #1139 from pmangro/master
[PT-BR] Remove from Playlist
2024-03-18 11:57:25 -05:00
pmangro 5ba830d56e [PT-BR] Remove from Playlist 2024-03-18 09:45:46 -03:00
Illia Pyshniak 81919e963a Add Ukrainian language to i18n.js 2024-03-15 22:28:00 +02:00
Illia Pyshniak 8a1d4bb958 Ukrainian app translation 2024-03-15 22:26:52 +02:00
Nicholas Wallace eabac0e709 Add: Vietnamese to language code map 2024-03-09 16:23:25 -07:00
Nicholas Wallace d90783d7c8 Fixed whitespace 2024-03-09 16:20:24 -07:00
Nicholas Wallace faf8211e5c Add: Vietnamese app translations 2024-03-09 16:20:00 -07:00
advplyr 65d90947c5 Fix:Remove from playlist button #987 2024-03-04 14:56:58 -06:00
advplyr 752e918d26 iOS version bump 0.9.73 & update dependencies 2024-03-03 16:28:55 -06:00
advplyr ad08cc69f4 Version bump 0.9.73-beta 2024-03-03 14:15:32 -06:00
advplyr da3882fd5e Fix:Playlist tab not shown after adding first playlist #1092 2024-03-03 13:29:58 -06:00
advplyr c04c7a55d9 Fix:iOS progress bar overflowing edge of screen 2024-03-03 12:52:17 -06:00
advplyr 3318846b43 Add:Remove from playlist option in more menu for items on playlist page #987 2024-03-02 15:25:18 -06:00
advplyr e0860feeb9 Merge pull request #1109 from megamegax/hu
feat(i18n): add Hungarian localization
2024-03-01 15:39:15 -06:00
Hunyady Mihály c0253a69b9 feat(i18n): replace NoItemsFound translation to a more general one 2024-03-01 22:09:33 +01:00
Hunyady Mihály 9242c7e6e0 feat(i18n): small grammar adjustments 2024-03-01 22:04:09 +01:00
Hunyady Mihály 0bfc579ae4 feat(i18n): adjust translation 2024-03-01 21:39:06 +01:00
megamegax 5fad06756b feat(i18n): add Hungarian localization 2024-03-01 20:03:21 +01:00
advplyr 5be6afa6c1 Merge pull request #1106 from pmangro/0.9.72.a-PT-BR
[PT-BR] Translated LabelAllowSeekingOnMediaControls
2024-02-27 15:58:36 -06:00
pmangro 5ba3b8be7a [PT-BR] Translated LabelAllowSeekingOnMediaControls 2024-02-27 14:39:21 -03:00
advplyr 0eb62f541d Fix:Local media item page long titles overflowing screen pushing play & more menu button offscreen #1103 2024-02-25 15:35:23 -06:00
advplyr 6baff881c7 Merge pull request #1094 from fidoriel/disable-widget-ios-scrubing
Disable ios/android scrubbing in MPRemoteCommandCenter/MediaBrowserServiceCompat
2024-02-25 15:29:52 -06:00
fidoriel 8c23c1e795 german translation for LabelAllowSeekingOnMediaControls 2024-02-25 22:25:17 +01:00
advplyr 9706c66a27 iOS Update Realm schema version for new setting, update remote transport controls on device settings updated 2024-02-25 15:04:54 -06:00
advplyr f4f7252537 Update wording for enable seeking on media controls settings, remove restart required help text, map translations 2024-02-25 14:44:40 -06:00
advplyr 430d200151 Update android disable media control seek setting to update in realtime 2024-02-25 14:11:28 -06:00
advplyr 7be82d03f3 Merge branch 'master' into disable-widget-ios-scrubing 2024-02-25 12:51:58 -06:00
advplyr 15aaef742d Fix:Ignore swiping for audio player when modal is open #1099 2024-02-24 15:00:29 -06:00
advplyr e443ba09da Merge pull request #1095 from pmangro/0.9.72.a-PT-BR
Added PT-BR Strings
2024-02-24 14:49:35 -06:00
luca 8235d1f1f7 reorder setting and add info label 2024-02-22 20:27:33 +01:00
pmangro 9a39dcceee Update i18n.js to add pt-br 2024-02-21 08:55:45 -03:00
pmangro a707b7fc86 Typos corrected 2024-02-21 08:53:34 -03:00
fidoriel b6fea85ef4 fix compile android 2024-02-21 10:16:47 +01:00
advplyr 7e3b3f55ae Fix:Android auto not loading server items #1081 2024-02-20 17:05:07 -06:00
fidoriel d2d3bbe8a1 fix default false 2024-02-20 22:08:47 +01:00
fidoriel 41a65c0f9d add setting for widget scrubbing 2024-02-20 21:58:31 +01:00
pmangro 165ddd58a6 Update pt-br.json - initial file 2024-02-20 17:29:03 -03:00
pmangro 09dc04c0de Created pt-br.json 2024-02-20 15:40:29 -03:00
advplyr 57d5dbc0d0 Merge pull request #1082 from arcao/patch-1
Translate cs.json
2024-02-14 18:42:25 -06:00
Martin Sloup 11af75e127 Translate cs.json
Translate untranslated texts to Czech and fix already translated texts.
2024-02-14 02:10:33 +01:00
advplyr f8d82a3020 iOS version bump 0.9.72 2024-02-11 08:54:54 -06:00
advplyr 81d7b1cfd7 Version bump 0.9.72-beta 2024-02-10 16:35:01 -06:00
advplyr e15c1bcb32 Fix:Continue listening shelves show on initial failed connection #1038 2024-02-09 17:52:24 -06:00
advplyr 826d414d9f Update:iOS do not resume playback after app suspended. Attempted fix for #973 2024-02-07 18:02:25 -06:00
advplyr 0da6fca727 Fix:iOS 0 second listening sessions #970 and include device info with offline sessions 2024-02-05 19:04:11 -06:00
advplyr 2327bc1705 Update playMethod to fallback to 0 2024-02-04 18:14:24 -06:00
advplyr 3bb76cecac Fix:Android respect offline mode on app restart 2024-02-04 17:46:56 -06:00
advplyr e3f82bb237 Fix:Include device info with sync local-all request 2024-02-04 17:27:57 -06:00
advplyr fcd6270f2d Fix:Hide library list button from app bar when no network connection 2024-02-04 15:53:05 -06:00
advplyr d5411703ca Update:Add 10 second timeout when loading home page shelves 2024-02-04 15:48:37 -06:00
advplyr 8fc2058db8 Update:Handle navigating to library item pages when server connection goes down
- Added loading indicator to library item page on initial request to server
- Redirect to downloaded item page if server item request fails
2024-02-04 15:46:26 -06:00
advplyr d03949f5d1 Update:Audio player title scrolls when overflow #740
- increase width of the title on minimized player to scale with screen width
- mini player shows media title and chapter title
2024-02-03 17:31:53 -06:00
advplyr ff01786063 Remove unused fastSort.js 2024-02-03 13:28:41 -06:00
advplyr bb117ab4da Update:Authors page increase size to fill width with 2 per row #1026 2024-02-02 18:33:23 -06:00
advplyr f5ec247b47 Update:Minimized audio player allow swipe up to fullscreen #1046 2024-02-01 18:20:10 -06:00
advplyr 6e6dd00523 Update:Allow swiping down anywhere on fullscreen audio player to minimize #1046 2024-02-01 17:51:10 -06:00
advplyr c9fb98f033 Fix:Android crash on startup due to SimpleStorage #1040
- Update SimpleStorage to 1.5.5-SNAPSHOT
2024-01-29 16:46:39 -06:00
advplyr 75ecff5e6d Remove @nuxtjs/tailwindcss dependency and replace with standalone tailwindcss 2024-01-25 17:39:22 -06:00
advplyr 91a9f918fb Merge pull request #1052 from bloodscript/issue-1051-german-localization-improvement
[issue-1051] German Localization
2024-01-24 14:18:17 -06:00
Steffen Meiselbach 03305f90ca corrected ereader based on current Duden entry 2024-01-23 12:38:31 +01:00
Steffen Meiselbach 9428bc07c5 reverted to uniformly uncapitalized pronouns 2024-01-23 12:37:03 +01:00
Steffen Meiselbach f38f9c3502 ebook written as used in Duden and uniformly capitalized version of Du 2024-01-23 12:33:01 +01:00
Steffen Meiselbach 64a796aece standardized addressing (du), missing translations and uniformly starting with capital character 2024-01-23 07:24:40 +01:00
advplyr 63039fe8c9 Fix pl.json formatting 2024-01-22 17:34:59 -06:00
Steffen Meiselbach 3f2b723d0f (sleep) timer is typically used in german parlance 2024-01-22 22:55:28 +01:00
advplyr ffd7fb1cf9 Update workflows to use node 20 2024-01-21 15:56:24 -06:00
advplyr b049b206cf Merge pull request #1049 from Bogumio/patch-2
Update pl.json
2024-01-21 15:31:27 -06:00
advplyr 35f56dc8a9 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2024-01-21 15:32:13 -06:00
advplyr a71cd326cc Update readme node version to 20 2024-01-21 15:32:06 -06:00
Bogumio cc841d1c7c Update pl.json
The rest of the translations have been completed. Minor corrections and unifications.
2024-01-21 12:04:06 +01:00
advplyr 16ecdcf96e Merge pull request #1042 from Bogumio/patch-1
Update pl.json
2024-01-14 11:15:48 -06:00
Bogumio 96ec56d1ad Update pl.json
Most of the missing phrases have been completed. I will try to translate the rest soon.
2024-01-14 18:07:55 +01:00
advplyr 9b15bafc9f Fix:Settings page button/toggle touch area #1021 2024-01-11 17:27:41 -06:00
advplyr aa02f2e465 Update readme for iOS beta full 2024-01-06 14:15:19 -06:00
advplyr 15464617ef Merge pull request #1032 from burghy86/patch-3
Update it.json
2024-01-05 10:21:43 -06:00
burghy86 60ecceb59d Update it.json
all string translate
2024-01-05 10:18:48 +01:00
advplyr 3fe943e989 Add:Player setting to scale elapsed time by playback speed #1028
- Moved player settings to new playerSettings key in storage preferences
2024-01-04 15:28:25 -06:00
advplyr 5e7d039454 iOS version bump 0.9.71 2024-01-01 13:55:13 -06:00
advplyr f928df146a Version bump 0.9.71-beta 2024-01-01 12:56:12 -06:00
advplyr 6e57cb49c4 Update:Move modal titles down to not overlap with ios status bar 2024-01-01 12:24:41 -06:00
advplyr 2f8494b013 Add:Tags to item page & allow wrapping to multiple lines #1014 2024-01-01 12:08:15 -06:00
advplyr 6f1b93bd9f Update:Default to alt view so that wooden shelf does not show on initial load 2024-01-01 11:55:57 -06:00
advplyr 19b928d7f6 Fix:Download marker overlapping series sequence on book cover #996 2024-01-01 11:53:28 -06:00
advplyr b849be6f5e Add:Epub reader setting to force single page #1018 2024-01-01 11:31:45 -06:00
advplyr 86096df1a4 Merge pull request #1003 from benonymity/playerSwipeBack
Disable iOS swipeBack in fullscreen audio player
2024-01-01 10:52:35 -06:00
advplyr 98a465c7b4 Update components/app/AudioPlayer.vue 2024-01-01 10:51:46 -06:00
advplyr 3315f61ae4 Merge branch 'master' into playerSwipeBack 2024-01-01 10:46:01 -06:00
advplyr f977445d7a Merge pull request #1004 from benonymity/statusBarTweaks
iOS Status Bar Tweaks
2024-01-01 10:31:58 -06:00
advplyr 095d5e1cec Fix item page gradient for light mode 2024-01-01 10:26:06 -06:00
advplyr 69fbfa4b33 Update nuxtjs/tailwindcss and update tailwind config 2024-01-01 10:19:55 -06:00
advplyr 1c89d33726 Update item page gradient to be absolute and below cover image, add transitions 2024-01-01 09:57:51 -06:00
advplyr acee907932 Merge branch 'master' into statusBarTweaks 2024-01-01 09:00:47 -06:00
advplyr 08ad216fe2 Merge pull request #1002 from benonymity/iosChapterTrack
iOS Now Playing Chapter Track (updated #372)
2024-01-01 08:51:59 -06:00
advplyr 3cc7a0937c Fix seeking in NowPlaying to adjust for chapter start time 2024-01-01 08:49:39 -06:00
advplyr 322cad5548 Move AbsAudioPlayer.setChapterTrack to AudioPlayer instead of localStore 2024-01-01 08:22:20 -06:00
advplyr 73d70dd480 Merge branch 'master' into iosChapterTrack 2024-01-01 08:11:59 -06:00
advplyr 56bc569d4a Merge pull request #1016 from KaiStarkk/hotfix-pr-1005
[Fix] Prevent crash from force unwrapping nil value in PR-1005
2023-12-31 16:06:50 -06:00
advplyr 981ae58f6a Update:Podfile for capacitor filesharer 2023-12-31 16:02:29 -06:00
Kieran Hannigan b2bfa4c002 [Fix] Prevent crash from force unwrapping nil value in PR #1005 2023-12-30 11:16:10 +08:00
advplyr 8b6003c5e3 Merge pull request #1005 from benonymity/playPauseFix
Add togglePlayPauseCommand route to iOS
2023-12-29 15:53:20 -06:00
advplyr 017b6237ca Fix:Filter dialog clear filter button overflow notch on ios #999 2023-12-27 17:23:55 -06:00
advplyr d5496c2e9a Fix:Library list view long titles overflowing, clamped to 2 lines #1013 2023-12-27 17:10:57 -06:00
advplyr 5ef8961c33 Fix:Comma separated links on item page 2023-12-27 17:03:36 -06:00
advplyr e8bea48577 Map shelf translation strings 2023-12-26 16:40:28 -06:00
advplyr 6cae78f5c2 Merge pull request #1007 from Jafeyyu/master
Update zh-cn.json
2023-12-26 16:34:01 -06:00
Jafeyyu d9d31a0620 Update zh-cn.json 2023-12-26 18:09:17 +08:00
advplyr 3682c5fca2 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2023-12-25 15:35:34 -06:00
advplyr 14b04a14b6 Add:Year in review cards to stats page 2023-12-25 15:35:29 -06:00
advplyr f218d90bfb Merge pull request #997 from Jafeyyu/master
Update zh-cn.json
2023-12-24 12:02:17 -06:00
advplyr 020f936ad4 Merge pull request #991 from Hallo951/master
Update de.json
2023-12-24 12:01:58 -06:00
benonymity 224e5efe48 add: togglePlayPause route 2023-12-23 01:51:37 -05:00
benonymity 439ab2c8c0 fix: item page background conflict 2023-12-23 01:45:40 -05:00
benonymity 26d0209763 fix: ios status bar mirror theme 2023-12-23 00:00:50 -05:00
benonymity 29086fc783 fix: no swipeback when audio player fullscreen 2023-12-22 22:34:24 -05:00
benonymity bb656a265f fix: broken isIos 2023-12-22 21:36:49 -05:00
benonymity b11ad9dc5b fix: only call on ios and remove...very professional logging 2023-12-22 21:18:55 -05:00
benonymity 2b1667e532 feat: now playing chapter track 2023-12-22 21:01:37 -05:00
Jafeyyu 228a198598 Update zh-cn.json 2023-12-21 16:09:21 +08:00
Hallo951 2843308fb8 Update de.json 2023-12-19 09:03:06 +01:00
advplyr 793f0c05f7 iOS version bump 0.9.70 2023-12-18 17:15:22 -06:00
advplyr cf7271dc57 Version bump 0.9.70 2023-12-18 17:10:07 -06:00
advplyr 54664b0c32 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2023-12-18 17:05:40 -06:00
advplyr ecf910dc55 Fix:Play button loading indicator when offline #990 2023-12-18 17:05:34 -06:00
advplyr 460daec72e iOS version bump 0.9.69 2023-12-16 17:21:43 -06:00
advplyr 4dd67adcab Fix:Prevent pressing download buttons twice causing crash in iOS 2023-12-16 17:21:19 -06:00
advplyr 023c83ee78 Version bump 0.9.69-beta 2023-12-16 16:24:07 -06:00
advplyr 2b8ed16616 Update:UI for playlist & rss feed modal to add more padding on top and bottom to prevent iOS cutoff 2023-12-16 15:06:49 -06:00
advplyr 6ae4d72b68 Update:Settings UI cleanup and light theme updates 2023-12-16 14:43:48 -06:00
advplyr c6bd93ef5c Light theme updates 2023-12-16 14:37:05 -06:00
advplyr e1c02ce74c Update:Show loading indicator on play buttons when starting playback 2023-12-15 17:35:37 -06:00
advplyr efc6d68403 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2023-12-15 16:05:53 -06:00
advplyr 7412d0899f Fix:Syncing when media is open in audio player and playing on another device #984
- Local playback session IDs have been updated to UUIDs instead of prefixing local_play_
2023-12-15 16:05:19 -06:00
advplyr 42eb0a8384 Merge pull request #988 from burghy86/patch-2
Update it.json
2023-12-15 15:03:25 -06:00
burghy86 27e83e8ed9 Update it.json 2023-12-15 22:02:26 +01:00
advplyr dd66042766 Update:Dont show chapter number infront of chapter title #981 2023-12-14 16:59:47 -06:00
advplyr dc790d5450 Fix light mode for new podcast page 2023-12-14 16:55:17 -06:00
advplyr 01ac844024 Fix light theme on add podcast page 2023-12-14 16:39:55 -06:00
advplyr c8064ac7bc Rename page name from search to add-podcast 2023-12-14 16:25:59 -06:00
advplyr f5da44dc6f Merge pull request #986 from Hallo951/master
Update de.json
2023-12-14 08:15:26 -06:00
Hallo951 cbdff29684 Update de.json 2023-12-14 08:19:27 +01:00
advplyr 19d6c84113 Update:Light theme colors for track progress bar and ereader settings #916 2023-12-13 16:57:44 -06:00
advplyr d9f4ed3d54 Merge pull request #983 from Nab0y/master
Fix Russian localization
2023-12-12 17:50:06 -06:00
advplyr fdb26b7700 Update:Set device language to match the first server connected to #448 2023-12-12 17:50:04 -06:00
Dmitry Naboychenko 94cb7a65a2 Fix Russian localization 2023-12-12 22:34:53 +03:00
advplyr 9298065934 Add:Theme option setting #916 2023-12-11 18:11:17 -06:00
advplyr 1cf01a5d12 Add home page shelf translation strings #448 2023-12-11 17:42:29 -06:00
advplyr e3aa96cbd5 Fix:Setting device language code 2023-12-11 17:27:26 -06:00
advplyr c7678da664 Update:Setup css colors for themes, add light theme colors #916 2023-12-10 17:53:27 -06:00
advplyr 52701048ad Merge pull request #967 from tonyedwardspz/master
Feature - Add a low feedback mode
2023-12-09 15:14:52 -06:00
advplyr 776c02828f Fix latest page to show download indicator, update download button to be responsive right away 2023-12-09 14:57:44 -06:00
advplyr 69308413f6 Fix bad merge with master 2023-12-08 17:04:03 -06:00
advplyr 84e376a7cf Remove commented out toasts 2023-12-08 17:03:08 -06:00
advplyr 1822b21af8 Remove socketConnected check on local items alert 2023-12-08 16:49:12 -06:00
advplyr 8b222435e6 Fix:Load local settings before attempting server connection #976 2023-12-08 16:10:57 -06:00
advplyr fdd251ec2c Merge pull request #975 from Jafeyyu/master
Improve Chinese translation
2023-12-08 16:01:24 -06:00
Tony Edwards 78170d4e10 Potential toasts for removal 2023-12-08 09:59:22 +00:00
Tony Edwards 837981b8b6 Removes download prompt on podcast ep page 2023-12-08 09:56:07 +00:00
Tony Edwards be23c6703f Shows media link status when connected to a server 2023-12-08 09:08:51 +00:00
Tony Edwards 0c72889917 Remove successfully connected media link message 2023-12-08 07:56:45 +00:00
Tony Edwards 02746bc35f Fixes my introduced errors 2023-12-08 07:53:35 +00:00
Qi, Xiaoqiang 85a5c33bd8 Improve Chinese translation 2023-12-08 15:33:37 +08:00
Tony Edwards 5067c0f6d7 Removes download confirmation prompt 2023-12-08 07:30:54 +00:00
Tony Edwards 89938ab120 Removes download success toast 2023-12-08 07:25:51 +00:00
Tony Edwards 9a32396daa Removes low feedback toggle 2023-12-08 07:19:23 +00:00
advplyr ddb1796891 Update serverconfig to remove version before saving 2023-12-07 17:02:50 -06:00
advplyr 76253b7b9d Merge pull request #969 from Sapd/sso-redirect-ri
SSO: Add redirect_uri to oAuth auth request
2023-12-07 16:08:19 -06:00
advplyr 74fef1d2f4 Make backwards compatible with server v2.6.0 2023-12-07 11:07:37 -06:00
advplyr 7306ffa709 Merge pull request #972 from JannikGohr/german_translation
Add German Translations
2023-12-06 17:45:41 -06:00
JannikGohr 6f289293cc Continue german translation in strings/de.json 2023-12-07 00:09:34 +01:00
advplyr 783f924bb9 Add:Ebook library filters #971 2023-12-05 16:14:20 -06:00
Denis Arnst 044a35aacf SSO: Removed isRest as requirement
Removed in the backend to increase compatibility with possible 3rd party oauth libraries people might use
2023-12-05 09:44:20 +01:00
Tony Edwards 78448894d2 Adds hiding audio book feedback promt/toast 2023-12-05 07:31:03 +00:00
Tony Edwards aca348e783 Adds low feedback mode to podcast downloads 2023-12-05 07:31:03 +00:00
Tony Edwards e6c1874cbb Adds download success toasts to low feedback mode 2023-12-05 07:31:03 +00:00
Tony Edwards 1b06dbd78c Adds basic low feedback mode implementation 2023-12-05 07:31:03 +00:00
advplyr a26f37aa49 Add more translation strings, clean out unused strings #448 2023-12-04 17:53:36 -06:00
Denis Arnst 8af3c09458 SSO: Add redirect_uri to oAuth auth request
Also:
- Add client_id with the name of the app (for possible future use)
- Add response_type simply to provide all required oauth2 parameters for reference
- For the URL to be opened in Browser: Forward the callback/redirect_uri URL provided by the server
- Use a hardcoded variable for HTTPS enforcement, for easier debugging
2023-12-04 22:43:14 +01:00
advplyr fed6579e08 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2023-12-03 17:37:14 -06:00
advplyr 6621f8b2ee Add:Language code setting and translations #448 2023-12-03 17:37:01 -06:00
advplyr 737b6d7c26 Merge pull request #964 from Sapd/seek-fix
Fix track skipping on iOS (Fixes #686)
2023-11-30 17:06:33 -06:00
Denis Arnst 59c71b71fb Fix track skipping on iOS (Fixes #686)
When we seek into a different chapter, we will have to reset the progress of the current one before seeking to the new one.
2023-11-29 17:30:34 +01:00
advplyr f89148b92e iOS version bump 0.9.68 2023-11-25 15:03:14 -06:00
advplyr 88fbebe5c4 Fix:Error going to local library item page that is storing an old server library item id when server is now using new ids #953 2023-11-25 14:38:40 -06:00
advplyr e8abb18350 Version bump v0.9.68-beta 2023-11-25 13:26:12 -06:00
advplyr cc9dead1eb Remove unused query property 2023-11-25 13:10:46 -06:00
advplyr f9dd8188ee Update:Clicking local podcast from home page will filter by downloaded episodes #958 2023-11-25 13:09:43 -06:00
advplyr 579785d2c6 Add:Downloaded filter for podcast episodes #959
Fix:More menu button not working for podcasts with some episodes downloaded
2023-11-25 12:56:29 -06:00
advplyr 257a1ceb51 Fix:Check network connection before redirecting local item to server item #958 2023-11-25 12:28:28 -06:00
advplyr 5c60cd136c Fix password login to send empty string instead of null to support passwordless root user login 2023-11-25 11:17:35 -06:00
advplyr 2bfe548c52 Add:Monochrome app icon & capitalize app name #786 2023-11-22 14:00:55 -06:00
advplyr 164fd92a18 Fix:Android auto unable to play podcast episodes #832 2023-11-21 11:33:45 -06:00
advplyr 0036a19659 Fix:Downloaded item server user id mismatch alert to support connections with old user id #945 2023-11-21 10:55:26 -06:00
advplyr ef4d7ba1bf Fix openid path that is still expecting a trailing slash 2023-11-19 14:45:35 -06:00
advplyr 8dbffa7993 iOS version bump 0.9.67 2023-11-17 15:44:33 -06:00
advplyr 17ff2ed5f4 Version bump 0.9.67-beta 2023-11-17 15:27:02 -06:00
advplyr 6fe470cfc1 Update:Android remove folder scanning and ffmpegkit 2023-11-16 14:52:11 -06:00
advplyr 301e9b213f Update message alerts for locally downloaded media. Added alert for same server address but different user. 2023-11-16 13:28:12 -06:00
advplyr 2b533524fc Add:Tags in search results #940 2023-11-15 14:30:57 -06:00
advplyr 632b1f526b Update:Library item pages show alerts on if the item is linked to the current server
- Local library item pages redirect to server item page if connected to the same server address
2023-11-14 17:13:49 -06:00
advplyr 81b11bac93 Fix:iOS downloading not saving progress for local copy #825 2023-11-13 16:33:19 -06:00
advplyr a88e404ccf Update:Trim trailing slash from server address input 2023-11-13 15:22:15 -06:00
advplyr a7cbdbfbbb Update:iOS remove paused timer that synced with the server when current media was paused 2023-11-12 13:49:20 -06:00
advplyr ad5146e2ee Update:iOS remove unused logic of opening last active session on app launch 2023-11-12 13:36:23 -06:00
advplyr 36be91962c Update:iOS using new sync local sessions endpoint
- remove local session sync function call on starting playback
- Add User model and getCurrentUser api function
2023-11-12 13:19:36 -06:00
advplyr ff5a1bb09f Update:Server connect page shows message informing new users #912 2023-11-12 10:30:39 -06:00
advplyr 5060554640 Merge pull request #887 from atoghyani/ios-sleep-timer-reset-after-resume
Adding a check so chapter sleep timer is set after resume only when a…
2023-11-12 08:35:44 -06:00
advplyr 93cb319e97 Merge pull request #906 from Sapd/oauth2-support
OAuth2 Support
2023-11-11 14:49:49 -06:00
Denis Arnst a42dfa5649 Don't check version for now
- Be more explicit on error 404, to not confuse users
- Error for when there is no authCode
2023-11-11 21:30:10 +01:00
advplyr 6a938b8da1 Update button UI to be full width 2023-11-11 14:00:48 -06:00
advplyr 4f994072ad Check for duplicate config when authenticating with openid 2023-11-11 13:55:32 -06:00
advplyr 7c0ef179ec Update:Podcast search page renamed to add #936 2023-11-10 16:36:07 -06:00
Denis Arnst 5fdd0c6672 oauth2: Fix PKCE
- Send challenge to the SSO provider itself too
- Fix challenge generation
- Removed semicolons
2023-11-10 22:30:55 +01:00
advplyr e9f523d7cb Fix:Android downloading finished item not marked as finished #903 2023-11-07 16:04:01 -06:00
advplyr c1b13dcd24 Update:Hide duration on collection items that have no duration 2023-11-06 16:21:52 -06:00
Denis Arnst 0daa043f14 oauth: Customizable OpenID Buttontext and auto-launch 2023-11-05 21:35:42 +01:00
Denis Arnst 1a6b716046 oauth2: Add comments
- Move appUrlOpen to represent the correct order of flow
2023-11-05 21:01:51 +01:00
Denis Arnst 945baa24f0 Connect: Check if audiobookshelf and check for version 2023-11-05 20:32:14 +01:00
advplyr 30d4e709f0 Merge branch 'master' into oauth2-support 2023-11-05 10:58:17 -06:00
Denis Arnst 8d8782a5a9 oauth2: Implement PKCE
- Also fixed URLs, the / after ${this.serverConfig.address} had to be removed, because during Connection flow the URL is now (re)set correctly uniformly
- Removed now uneccessary callback? parameter from first auth request
2023-11-05 17:27:04 +01:00
advplyr 087e8553c4 Update:Bookmarks modal is not closed on updating/deleting and does not show a toast #882 2023-11-04 16:08:19 -05:00
Denis Arnst 2984398051 ServerConnect: Improve connect flow
* Prefer HTTPS if no protocol given
* Retry with HTTP if no protocol given and HTTPS TCP establishment was not successful
* Do validation checks, i.e. control for unexpected redirects. Don't allow for protocol downgrade
* Provide good user messages on failure
* Fix a bug where it would use the wrong protocol after connecting bc. of unchecked redirect
* Reworked getRequest(...) to get information about the resulting URL and detailed errors
2023-11-04 15:25:03 +01:00
advplyr e9251db647 Add more menu on playlist item row to show library item more menu #914 2023-11-03 15:53:38 -05:00
Denis Arnst 6c88337180 oauth2: Force HTTPS, check state
Also improve error handling
2023-11-03 21:29:37 +01:00
advplyr 74b488ad0f Update playlist table design to include progress bar on items #914 2023-11-03 13:00:45 -05:00
advplyr fc7af6d1fc Add:Send ebook to device button #909 2023-11-02 16:10:55 -05:00
advplyr 737d8f19b3 Move appUrlOpen listener to plugin and listen for eventBus event 2023-11-01 10:16:31 -05:00
advplyr d613d8954d Fix host path extra port, fix formatting 2023-11-01 09:30:52 -05:00
advplyr 04e468b43d Fix:Android seeking from notification widget on multi-track books #894 2023-10-31 16:20:28 -05:00
advplyr dc8178769b Fix:Series page sort order by name #925 2023-10-31 16:00:08 -05:00
Denis Arnst 8b631f7860 oauth2 support on Android 2023-10-13 23:14:52 +02:00
Denis Arnst e521ddfab6 OAuth2 Support
Using CapacitorHttp and in-app link
2023-10-08 18:36:28 +02:00
advplyr 6b164bdb27 Fix:Clicking series item in list view when collapsing series #877 2023-10-07 13:39:47 -05:00
Arian Toghyani 2eb399b8a7 Adding a check so chapter sleep timer is set after resume only when a chapter sleep timer was set before 2023-09-26 22:35:01 -07:00
advplyr 46f558d6e0 Update axios requests to nativeHttp, add openid connect auth button 2023-09-25 17:30:39 -05:00
advplyr 00bed39733 iOS version bump 0.9.66 2023-09-17 14:12:08 -05:00
advplyr 2048795c92 Version bump 0.9.66-beta 2023-09-17 13:35:08 -05:00
advplyr 8c9bd53934 Hide lock orientation setting on iOS because it is not supported 2023-09-17 12:53:38 -05:00
advplyr 01178d00bf Replace existing axios calls with native https calls 2023-09-17 12:43:50 -05:00
advplyr 7541e4b660 Fix:Android mp3 index seeking setting not sticking #638 2023-09-17 11:51:15 -05:00
advplyr 2366c938b5 Update:Remove non-breaking spaces from comma separated name strings 2023-09-17 11:37:31 -05:00
advplyr 59480ad114 Update:Add switch server/user button in side drawer and change button text on account page. Update logout to disconnect #628 2023-09-17 10:23:35 -05:00
advplyr ce7779607f Update:Bookmarks modal UI #876 2023-09-17 09:28:43 -05:00
advplyr 04bc74babe Update:Epub ereader show location at bottom of page #766 2023-09-16 15:12:20 -05:00
advplyr ac2b674ba5 Fix:Epub ereader swiping moving 2 pages #802 2023-09-16 09:38:20 -05:00
advplyr e53901e7a4 Fix:Incorrect line-height on epub titles #874 2023-09-16 09:30:51 -05:00
advplyr 3ede41f496 Add java version to github workflow 2023-09-16 08:18:13 -05:00
advplyr 0adeaaa04d Add java version to github workflow 2023-09-16 08:08:45 -05:00
advplyr 57d1fbfa83 Disable CapacitorHttp and add plugin to replace axios calls #781 2023-09-15 17:35:44 -05:00
advplyr 27bbe40874 Update to capacitor v5, update to latest gradle and android target 34 2023-09-14 18:05:49 -05:00
advplyr 60da6093f3 Update:Android auto include explicit indicator #611 2023-09-12 17:14:28 -05:00
advplyr 259c9cef20 Update:Podcast items show number of incomplete episodes #315 2023-09-11 17:55:59 -05:00
advplyr 6d61b2acaf Fix:Switch to library of item when clicking on title of fullscreen audio player #803 2023-09-11 17:08:15 -05:00
advplyr 72f687b079 Fix:Android auto loading podcast episodes #832 2023-09-11 12:25:51 -05:00
advplyr 0ce3c8bcbd Fix:Remove local playback sessions started offline, refresh open player timestamps when device gains focus 2023-09-10 17:51:53 -05:00
advplyr f9621d2f60 Fix:Progress lost when app in background for period of time #828 2023-09-10 15:10:26 -05:00
advplyr e634132a39 Fix:Android sync server progress to local progress #808 2023-09-10 13:37:41 -05:00
advplyr 59b428100b Sync local sessions when network reconnects 2023-09-10 12:46:57 -05:00
advplyr 36e0c91e27 Merge pull request #851 from lkiesow/author-separator
Fix separator between authors
2023-09-10 08:39:20 -05:00
advplyr 03225957aa Update initial server connection to not block the home page 2023-09-10 08:38:11 -05:00
Lars Kiesow 77f70e48de Fix separator between authors
This patch fixes a small problem with the separator between several
authors which is `Author A , Author B`, but should be instead
`Author A, Author B` like with the other metadata fields.
2023-08-30 23:36:18 +02:00
advplyr cbdc3513c7 Fix:Comic reader not detecting file sort order when number is more than 5-digit 2023-08-29 16:06:20 -05:00
advplyr 394479bea0 Update readme, add matrix link 2023-07-27 18:19:49 -05:00
advplyr b9592e46a7 Merge pull request #813 from casrou/master
Add missing space for authors as well #777
2023-07-26 18:02:38 -05:00
advplyr 5810a83c66 Fix:Android internal storage downloads audio files multiple times #791 2023-07-26 18:02:25 -05:00
Casper Roursgaard Christensen 389c7d23ca Revert to index as key 2023-07-27 00:56:47 +02:00
Casper Roursgaard Christensen defc7d216a Add missing space for authors as well #777 2023-07-26 23:29:54 +02:00
advplyr 852702112a Update:Enable CapacitorHttp to patch XMLHttpRequest to use native libraries, fixes cors #781 2023-07-12 17:13:57 -05:00
advplyr dd91bc6667 Fix:Item page comma separated lists space before comma #777 2023-07-12 16:56:23 -05:00
400 changed files with 37895 additions and 29893 deletions

No files matched your search

+4 -5
View File
@@ -1,13 +1,12 @@
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.swift]
indent_style = space
indent_size = 4
-37
View File
@@ -1,37 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
labels: bug
---
### Steps to reproduce
1.
2.
3.
### Expected behaviour
- Tell us what should happen
### Actual behaviour
- Tell us what happens
### Environment data
Audiobookshelf Version:
- [ ] Android App?
- [ ] iOS App?
#### Android Issue
Android version:
Device model:
Stock or customized system:
#### iOS Issue
iOS Version:
iPhone model:
+98
View File
@@ -0,0 +1,98 @@
name: 🐞 ABS App Bug Report
description: File a bug/issue and help us improve the Audiobookshelf mobile apps.
title: '[Bug]: '
labels: ['bug', 'triage']
body:
- type: markdown
attributes:
value: '## App Bug Description'
- type: markdown
attributes:
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.'
- 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:
label: What was the Problem?
description: Please help us understand the problem.
placeholder: Describe the problem you saw or experienced in detail! Include a screenshot or video, if applicable.
validations:
required: true
- type: textarea
id: steps-to-reproduce
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...'
validations:
required: true
- type: textarea
id: what-was-expected
attributes:
label: What was Expected?
description: Please help us understand what should have happened.
placeholder: Please help us visualize by including a doodle or screenshot.
validations:
required: true
- type: markdown
attributes:
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, Samsung Galaxy s23, etc
validations:
required: true
- type: input
id: phone-os
attributes:
label: Phone OS
description: What operating system? If Android, is it stock or a custom system?
placeholder: e.g. Android 14, iOS 17.4, etc
validations:
required: true
- type: dropdown
id: app-version
attributes:
label: Audiobookshelf App Version
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.14.1'
- 'iOS App - 0.14.1'
- 'Android App - 0.14.0'
- 'iOS App - 0.14.0'
validations:
required: true
- type: dropdown
id: install-source
attributes:
label: Installation Source
multiple: true
options:
- 'Google Play Store'
- 'Testflight'
- 'SideStore'
- 'Other (List in "Additional Notes")'
validations:
required: true
- type: textarea
id: additional-notes
attributes:
label: Additional Notes
description: Anything else you want to add?
placeholder: 'e.g. I have tried X, Y, and Z.'
+5
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.
-18
View File
@@ -1,18 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
labels: enhancement
---
### Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
### Describe the solution you'd like
A clear and concise description of what you want to happen.
### Describe alternatives you've considered
A clear and concise description of any alternative solutions or features you've considered.
### Additional context
Add any other context or screenshots about the feature request here.
@@ -0,0 +1,57 @@
name: 🚀 App Feature Request
description: Request a feature/enhancement
title: '[Enhancement]: '
labels: ['enhancement']
body:
- type: markdown
attributes:
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.'
- type: textarea
id: describe
attributes:
label: Describe the Feature/Enhancement
description: Please help us understand what you want.
placeholder: What is your vision?
validations:
required: true
- type: textarea
id: the-why
attributes:
label: Why would this be helpful?
description: Please help us understand why this would enhance your experience.
placeholder: Explain the "why" or "use case".
validations:
required: true
- type: textarea
id: image
attributes:
label: Future Implementation (Screenshot)
description: Please help us visualize by including a doodle or screenshot.
placeholder: How could this look or where could it go?
validations:
required: true
- type: markdown
attributes:
value: '## App Current Implementation'
- type: dropdown
id: version
attributes:
label: Audiobookshelf App Version
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.14.1'
- 'iOS App - 0.14.1'
- 'Android App - 0.14.0'
- 'iOS App - 0.14.0'
validations:
required: true
- type: textarea
id: current-image
attributes:
label: Current Implementation (Screenshot)
description: What screen were you looking at when you thought of this enhancement?
placeholder: If an image is not applicable, please explain why.
+41
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. -->
+31 -25
View File
@@ -13,35 +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 16.x
uses: actions/setup-node@v3
with:
node-version: 16
- name: use Node.js 20.x
uses: actions/setup-node@v3
with:
node-version: 20
- name: install dependencies
run: npm ci
- name: Set up Java
uses: actions/setup-java@v2
with:
distribution: 'temurin'
java-version: 21
- name: build Nuxt project
run: npm run generate
- name: install dependencies
run: npm ci --omit=optional
- name: copy to Android project
run: npx cap sync
- name: build Nuxt project
run: npm run generate
- name: build Android app
run: ./android/gradlew assembleDebug -p android --no-daemon
- name: copy to Android project
run: npx cap sync
- 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: build Android app
run: ./android/gradlew assembleDebug -p android --no-daemon
- name: upload app
uses: actions/upload-artifact@v3
with:
name: audiobookshelf-apk
path: android/app/build/outputs/apk/debug/*.apk
- 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@v4
with:
name: audiobookshelf-apk
path: android/app/build/outputs/apk/debug/*.apk
+68
View File
@@ -0,0 +1,68 @@
name: Build iOS
on:
push:
paths-ignore:
- 'android/**'
- '.github/workflows/build-apk.yml'
- '.github/workflows/deploy-apk.yml'
- 'readme.md'
pull_request:
paths-ignore:
- 'android/**'
- '.github/workflows/build-apk.yml'
- '.github/workflows/deploy-apk.yml'
- 'readme.md'
permissions:
contents: read
jobs:
build:
name: Build iOS simulator app
runs-on: macos-latest
steps:
- name: Check out sources
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
- name: Install JavaScript dependencies
run: npm ci --omit=optional
- name: Build shared web app
run: npm run generate
- name: Sync Capacitor iOS project
run: npx cap sync ios
- name: Install CocoaPods dependencies
working-directory: ios/App
run: pod install --deployment
- name: Build iOS simulator app
working-directory: ios/App
run: |
xcodebuild -quiet \
-workspace App.xcworkspace \
-scheme App \
-configuration Debug \
-sdk iphonesimulator \
-destination 'generic/platform=iOS Simulator' \
-derivedDataPath build \
ASSETCATALOG_COMPILER_APPICON_NAME=IconsDebug \
CODE_SIGNING_ALLOWED=NO \
build
echo "iOS simulator build succeeded."
- name: Upload iOS simulator app
uses: actions/upload-artifact@v4
with:
name: audiobookshelf-ios-simulator
path: ios/App/build/Build/Products/Debug-iphonesimulator/Audiobookshelf.app
if-no-files-found: error
@@ -0,0 +1,20 @@
name: Close fixed issues on release.
on:
release:
types: [published]
permissions:
contents: read
issues: write
jobs:
comment:
runs-on: ubuntu-latest
steps:
- name: Close issues marked as fixed upon a release.
uses: gcampbell-msft/fixed-pending-release@7fa1b75a0c04bcd4b375110522878e5f6100cff5
with:
label: 'awaiting release'
removeLabel: true
applyToAll: true
message: Fixed in [${releaseTag}](${releaseUrl}).
+42
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"
});
}
+43 -37
View File
@@ -12,51 +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 16.x
uses: actions/setup-node@v3
with:
node-version: 16
- name: use Node.js 20.x
uses: actions/setup-node@v3
with:
node-version: 20
- name: install dependencies
run: npm ci
- name: Set up Java
uses: actions/setup-java@v2
with:
distribution: 'temurin'
java-version: 21
- name: build Nuxt project
run: npm run generate
- name: install dependencies
run: npm ci --omit=optional
- name: copy to Android project
run: npx cap sync
- name: build Nuxt project
run: npm run generate
- name: build Android app
run: ./android/gradlew assembleDebug -p android --no-daemon
- name: copy to Android project
run: npx cap sync
- 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: build Android app
run: ./android/gradlew assembleDebug -p android --no-daemon
- 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: 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: 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: 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: upload test page artifact
uses: actions/upload-pages-artifact@v1
with:
path: ./ghpages
- 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@v3
with:
path: ./ghpages
deploy:
needs: build
@@ -73,4 +79,4 @@ jobs:
steps:
- name: deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v1
uses: actions/deploy-pages@v4
+28
View File
@@ -0,0 +1,28 @@
name: Verify all i18n files are alphabetized
on:
push:
pull_request:
paths:
- strings/** # Should only check if any strings changed
jobs:
update_translations:
runs-on: ubuntu-latest
steps:
# Check out the repository
- name: Checkout repository
uses: actions/checkout@v4
# Set up node to run the javascript
- name: Set up node
uses: actions/setup-node@v4
with:
node-version: '20'
# The only argument is the `directory`, which is where the i18n files are
# stored.
- name: Run Update JSON Files action
uses: audiobookshelf/audiobookshelf-i18n-updater@v1.3.0
with:
directory: 'strings/' # Adjust the directory path as needed
+25
View File
@@ -0,0 +1,25 @@
name: Prepare translation credits
on:
push:
branches:
- master
workflow_dispatch:
jobs:
release-notes:
if: github.event_name == 'workflow_dispatch' || contains(github.event.head_commit.message, 'weblate')
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: translator-credits
uses: audiobookshelf/get-translator-credits@v1.1.0
- name: Show translator credits
run: |
printf '%s\n' "${{ steps.translator-credits.outputs.credits }}"
+17
View File
@@ -0,0 +1,17 @@
{
"semi": false,
"singleQuote": true,
"printWidth": 400,
"proseWrap": "never",
"trailingComma": "none",
"overrides": [
{
"files": ["*.html"],
"options": {
"singleQuote": false,
"wrapAttributes": false,
"sortAttributes": false
}
}
]
}
+7
View File
@@ -0,0 +1,7 @@
{
"recommendations": [
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode",
"octref.vetur"
]
}
+27
View File
@@ -0,0 +1,27 @@
{
"vetur.format.defaultFormatterOptions": {
"prettier": {
"semi": false,
"singleQuote": true,
"printWidth": 400,
"proseWrap": "never",
"trailingComma": "none"
},
"prettyhtml": {
"printWidth": 400,
"singleQuote": false,
"wrapAttributes": false,
"sortAttributes": false
}
},
"editor.formatOnSave": true,
"editor.detectIndentation": true,
"editor.tabSize": 2,
"javascript.format.semicolons": "remove",
"[javascript][json][jsonc]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "octref.vetur"
}
}
+6
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"
}
+39 -38
View File
@@ -18,57 +18,62 @@ 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 121
versionName "0.14.2-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 96
versionName "0.9.65-beta"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
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()
}
configurations.all {
configurations.configureEach {
resolutionStrategy {
force("com.google.android.gms:play-services-base:18.0.1")
force("androidx.appcompat:appcompat:$androidxAppCompatVersion")
force("androidx.core:core-ktx:$androidx_core_ktx_version")
force("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version")
force("androidx.media:media:$androidx_media_version")
force("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version")
force("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version")
@@ -76,19 +81,19 @@ configurations.all {
}
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.3'
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 "androidx.work:work-runtime-ktx:2.9.1"
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"
@@ -111,22 +116,18 @@ dependencies {
// Glide for images
implementation "com.github.bumptech.glide:glide:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version"
// Paper NoSQL Db
implementation 'io.github.pilgr:paperdb:2.7.2'
// Simple Storage
implementation "com.anggrayudi:storage:1.5.4"
implementation "com.anggrayudi:storage:1.5.6"
// OK HTTP
implementation 'com.squareup.okhttp3:okhttp:4.9.2'
// Jackson for JSON
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.12.2'
// FFMPEG-Kit
implementation 'com.arthenica:ffmpeg-kit-min:4.5.1'
}
apply from: 'capacitor.build.gradle'
+6 -2
View File
@@ -2,14 +2,18 @@
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
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')
implementation project(':capacitor-dialog')
implementation project(':capacitor-haptics')
@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
@@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
@@ -0,0 +1,14 @@
<?xml version='1.0' encoding='utf-8'?>
<resources>
<string name="app_name">Audiobookshelf</string>
<string name="title_activity_main">Audiobookshelf</string>
<string name="package_name">com.audiobookshelf.app</string>
<string name="custom_url_scheme">audiobookshelf</string>
<string name="add_widget">Add widget</string>
<string name="app_widget_description">Simple widget for audiobookshelf playback</string>
<string name="action_jump_forward">Jump Forward</string>
<string name="action_jump_backward">Jump Backward</string>
<string name="action_skip_forward">Skip Forward</string>
<string name="action_skip_backward">Skip Backward</string>
<string name="action_change_speed">Change Playback Speed</string>
</resources>
+15 -2
View File
@@ -6,19 +6,22 @@
<!-- Permissions -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
@@ -35,6 +38,10 @@
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/media_player_widget_info" />
<meta-data
android:name="com.samsung.android.appwidget.provider"
android:resource="@xml/samsung_cover_widget_info" />
</receiver>
<!-- Used by Android Auto -->
@@ -50,7 +57,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"
@@ -93,6 +100,12 @@
</intent-filter>
</service>
<service
android:name=".services.DownloadService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
@@ -2,5 +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"
}
}
@@ -1,8 +1,24 @@
[
{
"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"
},
{
"pkg": "@capacitor/browser",
"classpath": "com.capacitorjs.plugins.browser.BrowserPlugin"
},
{
"pkg": "@capacitor/clipboard",
"classpath": "com.capacitorjs.plugins.clipboard.ClipboardPlugin"
@@ -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
@@ -34,40 +40,75 @@ class MainActivity : BridgeActivity() {
val storage = SimpleStorage(this)
val REQUEST_PERMISSIONS = 1
var PERMISSIONS_ALL = arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE
)
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)
val permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
if (permission != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
PERMISSIONS_ALL,
REQUEST_PERMISSIONS)
// 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
}
}
requestNeededPermissions()
}
private fun requestNeededPermissions() {
val needed = mutableListOf<String>()
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
needed.add(Manifest.permission.READ_EXTERNAL_STORAGE)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
needed.add(Manifest.permission.POST_NOTIFICATIONS)
}
if (needed.isNotEmpty()) {
ActivityCompat.requestPermissions(this, needed.toTypedArray(), REQUEST_PERMISSIONS)
}
}
@@ -103,6 +144,9 @@ class MainActivity : BridgeActivity() {
}
}
fun isPlayerNotificationServiceInitialized():Boolean {
return ::foregroundService.isInitialized
}
fun stopMyService() {
if (mBounded) {
@@ -116,6 +160,7 @@ class MainActivity : BridgeActivity() {
override fun onSaveInstanceState(outState: Bundle) {
storageHelper.onSaveInstanceState(outState)
super.onSaveInstanceState(outState)
outState.clear()
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
@@ -95,7 +95,7 @@ internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManage
Log.i(tag, "Update App Widget | Is Playing=$isPlaying | isAppClosed=$isAppClosed")
val playPauseResource = if (isPlaying) R.drawable.ic_media_pause_dark else R.drawable.ic_media_play_dark
val playPauseResource = if (isPlaying) androidx.mediarouter.R.drawable.ic_media_pause_dark else androidx.mediarouter.R.drawable.ic_media_play_dark
views.setImageViewResource(R.id.widgetPlayPauseButton, playPauseResource)
// Instruct the widget manager to update the widget
@@ -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() }
}
}
@@ -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)
}
}
@@ -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()
}
}
@@ -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)
@@ -24,8 +27,9 @@ open class MediaType(var metadata:MediaTypeMetadata, var coverPath:String?) {
@JsonIgnore
open fun removeAudioTrack(localFileId:String) { }
@JsonIgnore
open fun getLocalCopy():MediaType { return MediaType(MediaTypeMetadata(""),null) }
open fun getLocalCopy():MediaType { return MediaType(MediaTypeMetadata("", false),null) }
@JsonIgnore
open fun checkHasTracks():Boolean { return false }
}
@JsonIgnoreProperties(ignoreUnknown = true)
@@ -93,10 +97,15 @@ 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
val newEpisode = PodcastEpisode(localEpisodeId,(episodes?.size ?: 0) + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,null,null,audioTrack,episode.chapters,audioTrack.duration,0, episode.id, localEpisodeId)
val newEpisode = PodcastEpisode(localEpisodeId,(episodes?.size ?: 0) + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,null,null,audioTrack,episode.chapters,audioTrack.duration,episode.size, episode.id, localEpisodeId)
episodes?.add(newEpisode)
var index = 1
@@ -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
@@ -190,7 +204,7 @@ class Book(
JsonSubTypes.Type(BookMetadata::class),
JsonSubTypes.Type(PodcastMetadata::class)
)
open class MediaTypeMetadata(var title:String) {
open class MediaTypeMetadata(var title:String, var explicit:Boolean) {
@JsonIgnore
open fun getAuthorDisplayName():String { return "Unknown" }
}
@@ -209,13 +223,15 @@ class BookMetadata(
var isbn:String?,
var asin:String?,
var language:String?,
var explicit:Boolean,
explicit:Boolean,
// In toJSONExpanded
var authorName:String?,
var authorNameLF:String?,
var narratorName:String?,
var seriesName:String?
) : MediaTypeMetadata(title) {
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" }
}
@@ -225,8 +241,9 @@ class PodcastMetadata(
title:String,
var author:String?,
var feedUrl:String?,
var genres:MutableList<String>
) : MediaTypeMetadata(title) {
var genres:MutableList<String>,
explicit:Boolean
) : MediaTypeMetadata(title, explicit) {
@JsonIgnore
override fun getAuthorDisplayName():String { return author ?: "Unknown" }
}
@@ -298,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 {
@@ -341,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,
@@ -386,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)
@@ -1,59 +1,102 @@
package com.audiobookshelf.app.data
import android.content.Context
import android.net.Uri
import android.support.v4.media.MediaDescriptionCompat
import android.util.Log
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonSubTypes
import com.fasterxml.jackson.annotation.JsonTypeInfo
import java.io.File
enum class LockOrientationSetting {
NONE, PORTRAIT, LANDSCAPE
NONE,
PORTRAIT,
LANDSCAPE
}
enum class HapticFeedbackSetting {
OFF, LIGHT, MEDIUM, HEAVY
OFF,
LIGHT,
MEDIUM,
HEAVY
}
enum class ShakeSensitivitySetting {
VERY_LOW, LOW, MEDIUM, HIGH, VERY_HIGH
VERY_LOW,
LOW,
MEDIUM,
HIGH,
VERY_HIGH
}
enum class DownloadUsingCellularSetting {
ASK,
ALWAYS,
NEVER
}
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,
var userId:String,
var username:String,
var token:String,
var customHeaders:Map<String, String>?
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,
var customHeaders: Map<String, String>?
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class LocalFile(
var id:String,
var filename:String?,
var contentUrl:String,
var basePath:String,
var absolutePath:String,
var simplePath:String,
var mimeType:String?,
var size:Long
var id: String,
var filename: String?,
var contentUrl: String,
var basePath: String,
var absolutePath: String,
var mimeType: String?,
var size: Long
) {
@JsonIgnore
fun isAudioFile():Boolean {
fun exists(ctx: Context): Boolean {
if (contentUrl.startsWith("content:")) {
return try {
ctx.contentResolver.openFileDescriptor(Uri.parse(contentUrl), "r")?.use { true } ?: false
} catch (e: Exception) {
Log.w("LocalFile", "Cannot access SAF file $contentUrl", e)
false
}
}
return File(absolutePath).exists()
}
@JsonIgnore
fun isAudioFile(): Boolean {
if (mimeType == "application/octet-stream") return true
if (mimeType == "video/mp4") return true
return mimeType?.startsWith("audio") == true
}
@JsonIgnore
fun isEBookFile():Boolean {
fun isEBookFile(): Boolean {
return getEBookFormat() != null
}
@JsonIgnore
fun getEBookFormat():String? {
fun getEBookFormat(): String? {
if (mimeType == "application/epub+zip") return "epub"
if (mimeType == "application/pdf") return "pdf"
if (mimeType == "application/x-mobipocket-ebook") return "mobi"
@@ -66,107 +109,127 @@ data class LocalFile(
@JsonIgnoreProperties(ignoreUnknown = true)
data class LocalFolder(
var id:String,
var name:String,
var contentUrl:String,
var basePath:String,
var absolutePath:String,
var simplePath:String,
var storageType:String,
var mediaType:String
var id: String,
var name: String,
var contentUrl: String,
var basePath: String,
var absolutePath: String,
var storageType: String,
var mediaType: String
)
@JsonTypeInfo(use= JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes(
JsonSubTypes.Type(LibraryItem::class),
JsonSubTypes.Type(LocalLibraryItem::class)
)
open class LibraryItemWrapper(var id:String) {
@JsonTypeInfo(use = JsonTypeInfo.Id.DEDUCTION)
@JsonSubTypes(JsonSubTypes.Type(LibraryItem::class), JsonSubTypes.Type(LocalLibraryItem::class))
open class LibraryItemWrapper(var id: String) {
@JsonIgnore
open fun getMediaDescription(progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat { return MediaDescriptionCompat.Builder().build() }
open fun getMediaDescription(
progress: MediaProgressWrapper?,
ctx: Context
): MediaDescriptionCompat {
return MediaDescriptionCompat.Builder().build()
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class DeviceInfo(
var deviceId:String,
var manufacturer:String,
var model:String,
var sdkVersion:Int,
var clientVersion: String
var deviceId: String,
var manufacturer: String,
var model: String,
var sdkVersion: Int,
var clientVersion: String
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class PlayItemRequestPayload(
var mediaPlayer:String,
var forceDirectPlay:Boolean,
var forceTranscode:Boolean,
var deviceInfo:DeviceInfo
var mediaPlayer: String,
var forceDirectPlay: Boolean,
var forceTranscode: Boolean,
var deviceInfo: DeviceInfo
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class DeviceSettings(
var disableAutoRewind:Boolean,
var enableAltView:Boolean,
var jumpBackwardsTime:Int,
var jumpForwardTime:Int,
var enableMp3IndexSeeking:Boolean,
var disableShakeToResetSleepTimer:Boolean,
var shakeSensitivity: ShakeSensitivitySetting,
var lockOrientation: LockOrientationSetting,
var hapticFeedback: HapticFeedbackSetting,
var autoSleepTimer: Boolean,
var autoSleepTimerStartTime: String,
var autoSleepTimerEndTime: String,
var autoSleepTimerAutoRewind: Boolean,
var autoSleepTimerAutoRewindTime: Long, //Time in milliseconds
var sleepTimerLength: Long, // Time in milliseconds
var disableSleepTimerFadeOut: Boolean,
var disableSleepTimerResetFeedback: Boolean
var disableAutoRewind: Boolean,
var enableAltView: Boolean,
var allowSeekingOnMediaControls: Boolean,
var jumpBackwardsTime: Int,
var jumpForwardTime: Int,
var enableMp3IndexSeeking: Boolean,
var disableShakeToResetSleepTimer: Boolean,
var shakeSensitivity: ShakeSensitivitySetting,
var lockOrientation: LockOrientationSetting,
var hapticFeedback: HapticFeedbackSetting,
var autoSleepTimer: Boolean,
var autoSleepTimerStartTime: String,
var autoSleepTimerEndTime: String,
var autoSleepTimerAutoRewind: Boolean,
var autoSleepTimerAutoRewindTime: Long, // Time in milliseconds
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 androidAutoBrowseLimitForGrouping: Int,
var androidAutoBrowseSeriesSequenceOrder: AndroidAutoBrowseSeriesSequenceOrderSetting
) {
companion object {
// Static method to get default device settings
fun default():DeviceSettings {
fun default(): DeviceSettings {
return DeviceSettings(
disableAutoRewind = false,
enableAltView = true,
jumpBackwardsTime = 10,
jumpForwardTime = 10,
enableMp3IndexSeeking = false,
disableShakeToResetSleepTimer = false,
shakeSensitivity = ShakeSensitivitySetting.MEDIUM,
lockOrientation = LockOrientationSetting.NONE,
hapticFeedback = HapticFeedbackSetting.LIGHT,
autoSleepTimer = false,
autoSleepTimerStartTime = "22:00",
autoSleepTimerEndTime = "06:00",
sleepTimerLength = 900000L, // 15 minutes
autoSleepTimerAutoRewind = false,
autoSleepTimerAutoRewindTime = 300000L, // 5 minutes
disableSleepTimerFadeOut = false,
disableSleepTimerResetFeedback = false
disableAutoRewind = false,
enableAltView = true,
allowSeekingOnMediaControls = false,
jumpBackwardsTime = 10,
jumpForwardTime = 10,
enableMp3IndexSeeking = false,
disableShakeToResetSleepTimer = false,
shakeSensitivity = ShakeSensitivitySetting.MEDIUM,
lockOrientation = LockOrientationSetting.NONE,
hapticFeedback = HapticFeedbackSetting.LIGHT,
autoSleepTimer = false,
autoSleepTimerStartTime = "22:00",
autoSleepTimerEndTime = "06:00",
sleepTimerLength = 900000L, // 15 minutes
autoSleepTimerAutoRewind = false,
autoSleepTimerAutoRewindTime = 300000L, // 5 minutes
disableSleepTimerFadeOut = false,
disableSleepTimerResetFeedback = false,
enableSleepTimerAlmostDoneChime = false,
languageCode = "en-us",
downloadUsingCellular = DownloadUsingCellularSetting.ALWAYS,
streamingUsingCellular = StreamingUsingCellularSetting.ALWAYS,
androidAutoBrowseLimitForGrouping = 100,
androidAutoBrowseSeriesSequenceOrder = AndroidAutoBrowseSeriesSequenceOrderSetting.ASC
)
}
}
@get:JsonIgnore
val jumpBackwardsTimeMs get() = jumpBackwardsTime * 1000L
val jumpBackwardsTimeMs
get() = jumpBackwardsTime * 1000L
@get:JsonIgnore
val jumpForwardTimeMs get() = jumpForwardTime * 1000L
val jumpForwardTimeMs
get() = jumpForwardTime * 1000L
@get:JsonIgnore
val autoSleepTimerStartHour get() = autoSleepTimerStartTime.split(":")[0].toInt()
val autoSleepTimerStartHour
get() = autoSleepTimerStartTime.split(":")[0].toInt()
@get:JsonIgnore
val autoSleepTimerStartMinute get() = autoSleepTimerStartTime.split(":")[1].toInt()
val autoSleepTimerStartMinute
get() = autoSleepTimerStartTime.split(":")[1].toInt()
@get:JsonIgnore
val autoSleepTimerEndHour get() = autoSleepTimerEndTime.split(":")[0].toInt()
val autoSleepTimerEndHour
get() = autoSleepTimerEndTime.split(":")[0].toInt()
@get:JsonIgnore
val autoSleepTimerEndMinute get() = autoSleepTimerEndTime.split(":")[1].toInt()
val autoSleepTimerEndMinute
get() = autoSleepTimerEndTime.split(":")[1].toInt()
@JsonIgnore
fun getShakeThresholdGravity() : Float { // Used in ShakeDetector
return if (shakeSensitivity == ShakeSensitivitySetting.VERY_HIGH) 1.2f
else if (shakeSensitivity == ShakeSensitivitySetting.HIGH) 1.4f
else if (shakeSensitivity == ShakeSensitivitySetting.MEDIUM) 1.6f
fun getShakeThresholdGravity(): Float { // Used in ShakeDetector
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 {
@@ -177,10 +240,10 @@ data class DeviceSettings(
}
data class DeviceData(
var serverConnectionConfigs:MutableList<ServerConnectionConfig>,
var lastServerConnectionConfigId:String?,
var deviceSettings: DeviceSettings?,
var lastPlaybackSession: PlaybackSession?
var serverConnectionConfigs: MutableList<ServerConnectionConfig>,
var lastServerConnectionConfigId: String?,
var deviceSettings: DeviceSettings?,
var lastPlaybackSession: PlaybackSession?
) {
@JsonIgnore
fun getLastServerConnectionConfig(): ServerConnectionConfig? {
@@ -189,4 +252,3 @@ data class DeviceData(
}
}
}
@@ -1,15 +0,0 @@
package com.audiobookshelf.app.data
data class FolderScanResult(
var itemsAdded:Int,
var itemsUpdated:Int,
var itemsRemoved:Int,
var itemsUpToDate:Int,
val localFolder:LocalFolder,
val localLibraryItems:List<LocalLibraryItem>,
)
data class LocalLibraryItemScanResult(
val updated:Boolean,
val localLibraryItem:LocalLibraryItem,
)
@@ -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
@@ -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)
}
}
@@ -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()
}
}
@@ -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,58 +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
)
}
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)
}
}
@@ -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)
}
}
@@ -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,15 +79,39 @@ class LocalLibraryItem(
}
}
@JsonIgnore
fun hasTracks(ctx: Context, 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
}
if (!trackExists(ctx, it.contentUrl, it.metadata!!.path)) {
return false
}
}
return true
}
@JsonIgnore
private fun trackExists(ctx: Context, contentUrl: String?, path: String): Boolean {
return LocalFile("", null, contentUrl ?: "", "", path, null, 0).exists(ctx)
}
@JsonIgnore
fun getPlaybackSession(episode:PodcastEpisode?, deviceInfo:DeviceInfo):PlaybackSession {
val localEpisodeId = episode?.id
val sessionId = "play_local_${UUID.randomUUID()}"
val sessionId = "${UUID.randomUUID()}"
// Get current progress for local media
val mediaProgressId = if (localEpisodeId.isNullOrEmpty()) id else "$id-$localEpisodeId"
val mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(mediaProgressId)
val currentTime = mediaProgress?.currentTime ?: 0.0
val currentTime = if (mediaProgress?.isFinished == true) 0.0 else mediaProgress?.currentTime ?: 0.0
val mediaMetadata = media.metadata
@@ -152,6 +177,10 @@ class LocalLibraryItem(
)
}
if (media.metadata.explicit) {
extras.putLong(MediaConstants.METADATA_KEY_IS_EXPLICIT, MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT)
}
val mediaDescriptionBuilder = MediaDescriptionCompat.Builder()
.setMediaId(id)
.setTitle(title)
@@ -4,70 +4,64 @@ 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 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())
}
}
@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)
}
}
}
@@ -46,6 +46,10 @@ class LocalMediaProgress(
progress = if (finished) 1.0 else 0.0
}
if (finished) {
currentTime = 0.0
}
isFinished = finished
lastUpdate = System.currentTimeMillis()
finishedAt = if (isFinished) lastUpdate else null
@@ -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>,
)
@@ -1,6 +1,7 @@
package com.audiobookshelf.app.data
import android.content.Context
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.net.Uri
import android.os.Build
@@ -12,6 +13,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 +21,73 @@ 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.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
@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 +98,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,125 +109,204 @@ 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}")
}
/** Bitmap for the cover art, once resolved */
@JsonIgnore
private var resolvedCoverBitmap: Bitmap? = null
/**
* Builds the current session metadata, including the cover art bitmap if it has already been resolved.
*/
@JsonIgnore
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)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, totalDurationMs)
.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)
}
metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap)
metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, bitmap)
if (resolvedCoverBitmap != null) {
metadataBuilder
.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, resolvedCoverBitmap)
.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, resolvedCoverBitmap)
} else {
metadataBuilder
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, coverUri.toString())
.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, coverUri.toString())
}
return metadataBuilder.build()
}
/**
* Resolves the cover art bitmap (local covers are decoded synchronously, server-side covers
* are fetched asynchronously) and calls `onArtResolved` once it's available
*
* Returns the Job for the async fetch, or `null` if the bitmap was resolved synchronously.
*/
@JsonIgnore
fun getExoMediaMetadata(ctx:Context): MediaMetadata {
fun resolveCoverBitmapAsync(
ctx: Context,
coroutineScope: CoroutineScope,
onArtResolved: () -> Unit
): Job? {
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)
// Local covers get bitmap synchronously, no async fetch needed
if (localLibraryItem?.coverContentUrl != null) {
resolvedCoverBitmap =
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)
}
onArtResolved()
return null
}
// Server-side cover: resolve the art bitmap async
return coroutineScope.launch {
val bitmap = resolveUriAsBitmap(ctx, coverUri)
bitmap?.let {
resolvedCoverBitmap = it
onArtResolved()
}
}
}
@JsonIgnore
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)
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 +314,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 +425,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
)
}
}
@@ -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,19 +65,96 @@ 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) {
deviceData.deviceSettings?.languageCode = "en-us"
}
if (deviceData.deviceSettings?.downloadUsingCellular == null) {
deviceData.deviceSettings?.downloadUsingCellular = DownloadUsingCellularSetting.ALWAYS
}
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)) {
@@ -77,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
)
}
}
})
}
}
@@ -5,700 +5,308 @@ import android.net.Uri
import android.util.Log
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.file.*
import com.arthenica.ffmpegkit.FFmpegKitConfig
import com.arthenica.ffmpegkit.FFprobeKit
import com.arthenica.ffmpegkit.Level
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.models.DownloadItem
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.getcapacitor.JSObject
import org.json.JSONException
import com.audiobookshelf.app.models.DownloadItemPart
import java.io.File
class FolderScanner(var ctx: Context) {
/** Creates local-library records from a completed download manifest. */
class FolderScanner(private val ctx: Context) {
private val tag = "FolderScanner"
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 {
return "local_" + DeviceManager.getBase64Id(mediaItemId)
private fun localLibraryItemId(mediaItemId: String) =
"local_${DeviceManager.getBase64Id(mediaItemId)}"
private fun createLocalFile(
part: DownloadItemPart,
externalFile: DocumentFile? = null
): LocalFile? {
if (part.isInternalStorage) {
val file = File(part.finalDestinationPath)
if (!file.exists()) return null
return LocalFile(
DeviceManager.getBase64Id(file.name),
file.name,
Uri.fromFile(file).toString(),
file.getBasePath(ctx),
file.absolutePath,
file.mimeType,
file.length()
)
}
part.completedDestinationUri?.let { contentUrl ->
val uri = Uri.parse(contentUrl)
val size =
try {
ctx.contentResolver.openFileDescriptor(uri, "r")?.use { descriptor ->
descriptor.statSize.coerceAtLeast(0L)
}
?: 0L
} catch (e: Exception) {
Log.e(tag, "Could not open completed SAF file: $contentUrl", e)
return null
}
// Android 10 DownloadsProvider may not reconstruct a DocumentFile for an audio URI even
// though the URI remains readable. Keep the URI as the authoritative local-file location.
return LocalFile(
DeviceManager.getBase64Id(contentUrl),
part.filename,
contentUrl,
part.localFolderName,
part.finalDestinationPath,
mimeTypeFor(part),
size
)
}
// Do not reconstruct a DocumentFile from an absolute path: on Android 10 that becomes a
// file:// URI, which DocumentsContract rejects. The caller resolves this from the persisted
// SAF tree grant instead.
val document = externalFile
if (document == null || !document.exists()) {
Log.e(tag, "Could not resolve downloaded SAF file: ${part.finalDestinationPath}")
return null
}
return LocalFile(
DeviceManager.getBase64Id(document.id),
document.name,
document.uri.toString(),
document.getBasePath(ctx),
document.getAbsolutePath(ctx),
document.mimeType,
document.length()
)
}
enum class ItemScanResult {
ADDED, REMOVED, UPDATED, UPTODATE
}
private fun newLocalLibraryItem(
id: String,
downloadItem: DownloadItem,
basePath: String,
absolutePath: String,
contentUrl: String
) =
LocalLibraryItem(
id,
downloadItem.localFolder.id,
basePath,
absolutePath,
contentUrl,
false,
downloadItem.mediaType,
downloadItem.media.getLocalCopy(),
mutableListOf(),
null,
null,
true,
downloadItem.serverConnectionConfigId,
downloadItem.serverAddress,
downloadItem.serverUserId,
downloadItem.libraryItemId
)
// TODO: CLEAN this monster! Divide into bite-size methods
fun scanForMediaItems(localFolder:LocalFolder, forceAudioProbe:Boolean):FolderScanResult? {
FFmpegKitConfig.enableLogCallback { log ->
if (log.level != Level.AV_LOG_STDERR) { // STDERR is filled with junk
Log.d(tag, "FFmpeg-Kit Log: (${log.level}) ${log.message}")
}
}
private fun scanParts(
item: DownloadItem,
localItem: LocalLibraryItem,
externalFolder: DocumentFile? = null,
callback: (DownloadItemScanResult?) -> Unit
) {
val tracks = mutableListOf<AudioTrack>()
var foundEbook = false
var localEpisodeId: String? = null
val df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(localFolder.contentUrl))
if (df == null) {
Log.e(tag, "Folder Doc File Invalid $localFolder.contentUrl")
return null
}
var mediaItemsUpdated = 0
var mediaItemsAdded = 0
var mediaItemsRemoved = 0
var mediaItemsUpToDate = 0
// Search for files in media item folder
val foldersFound = df.search(true, DocumentFileType.FOLDER)
// Match folders found with local library items already saved in db
var existingLocalLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id)
// Remove existing items no longer there
existingLocalLibraryItems = existingLocalLibraryItems.filter { lli ->
Log.d(tag, "scanForMediaItems Checking Existing LLI ${lli.id}")
val fileFound = foldersFound.find { f -> lli.id == getLocalLibraryItemId(f.id) }
if (fileFound == null) {
Log.d(tag, "Existing local library item is no longer in file system ${lli.media.metadata.title}")
DeviceManager.dbManager.removeLocalLibraryItem(lli.id)
mediaItemsRemoved++
}
fileFound != null
}
foldersFound.forEach { itemFolder ->
Log.d(tag, "Iterating over Folder Found ${itemFolder.name} | ${itemFolder.getSimplePath(ctx)} | URI: ${itemFolder.uri}")
val existingItem = existingLocalLibraryItems.find { emi -> emi.id == getLocalLibraryItemId(itemFolder.id) }
val filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/*"))
// Do not scan folders that have no media items and not an existing item already
if (existingItem != null || filesInFolder.isNotEmpty()) {
when (scanLibraryItemFolder(itemFolder, filesInFolder, localFolder, existingItem, forceAudioProbe)) {
ItemScanResult.REMOVED -> mediaItemsRemoved++
ItemScanResult.UPDATED -> mediaItemsUpdated++
ItemScanResult.ADDED -> mediaItemsAdded++
else -> mediaItemsUpToDate++
}
}
}
Log.d(tag, "Folder $${localFolder.name} scan Results: $mediaItemsAdded Added | $mediaItemsUpdated Updated | $mediaItemsRemoved Removed | $mediaItemsUpToDate Up-to-date")
return if (mediaItemsAdded > 0 || mediaItemsUpdated > 0 || mediaItemsRemoved > 0) {
val folderLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(localFolder.id) // Get all local media items
FolderScanResult(mediaItemsAdded, mediaItemsUpdated, mediaItemsRemoved, mediaItemsUpToDate, localFolder, folderLibraryItems)
} else {
Log.d(tag, "No Media Items to save")
FolderScanResult(mediaItemsAdded, mediaItemsUpdated, mediaItemsRemoved, mediaItemsUpToDate, localFolder, mutableListOf())
}
}
private fun scanLibraryItemFolder(itemFolder:DocumentFile, filesInFolder:List<DocumentFile>, localFolder:LocalFolder, existingItem:LocalLibraryItem?, forceAudioProbe:Boolean):ItemScanResult {
val itemFolderName = itemFolder.name ?: ""
val itemId = getLocalLibraryItemId(itemFolder.id)
val existingLocalFiles = existingItem?.localFiles ?: mutableListOf()
val existingAudioTracks = existingItem?.media?.getAudioTracks() ?: mutableListOf()
var isNewOrUpdated = existingItem == null
val audioTracks = mutableListOf<AudioTrack>()
val localFiles = mutableListOf<LocalFile>()
var index = 1
var startOffset = 0.0
var coverContentUrl:String? = null
var coverAbsolutePath:String? = null
var hasEBookFile = false
var newEBookFile:EBookFile? = null
val existingLocalFilesRemoved = existingLocalFiles.filter { elf ->
filesInFolder.find { fif -> DeviceManager.getBase64Id(fif.id) == elf.id } == null // File was not found in media item folder
}
if (existingLocalFilesRemoved.isNotEmpty()) {
Log.d(tag, "${existingLocalFilesRemoved.size} Local files were removed from local media item ${existingItem?.media?.metadata?.title}")
isNewOrUpdated = true
}
filesInFolder.forEach { file ->
val mimeType = file.mimeType ?: ""
val filename = file.name ?: ""
Log.d(tag, "Found $mimeType file $filename in folder $itemFolderName")
val localFileId = DeviceManager.getBase64Id(file.id)
val localFile = LocalFile(localFileId,filename,file.uri.toString(),file.getBasePath(ctx), file.getAbsolutePath(ctx),file.getSimplePath(ctx),mimeType,file.length())
localFiles.add(localFile)
Log.d(tag, "File attributes Id:${localFileId}|ContentUrl:${localFile.contentUrl}|isDownloadsDocument:${file.isDownloadsDocument}")
if (localFile.isAudioFile()) {
val audioTrackToAdd:AudioTrack?
val existingAudioTrack = existingAudioTracks.find { eat -> eat.localFileId == localFileId }
if (existingAudioTrack != null) { // Update existing audio track
if (existingAudioTrack.index != index) {
Log.d(tag, "scanLibraryItemFolder Updating Audio track index from ${existingAudioTrack.index} to $index")
existingAudioTrack.index = index
isNewOrUpdated = true
}
if (existingAudioTrack.startOffset != startOffset) {
Log.d(tag, "scanLibraryItemFolder Updating Audio track startOffset ${existingAudioTrack.startOffset} to $startOffset")
existingAudioTrack.startOffset = startOffset
isNewOrUpdated = true
item.downloadItemParts.forEach { part ->
val externalFile =
if (part.isInternalStorage) {
null
} else {
part.completedDestinationUri?.let { DocumentFileCompat.fromUri(ctx, Uri.parse(it)) }
?: resolveExternalFile(externalFolder, part)
}
Log.d(tag, "Resolve part ${part.filename}: externalFile=${externalFile?.uri}")
val localFile = createLocalFile(part, externalFile) ?: return@forEach
when {
part.audioTrack != null -> {
val serverTrack = part.audioTrack
localItem.localFiles.removeAll { it.id == localFile.id }
localItem.localFiles.add(localFile)
val metadata =
FileMetadata(
localFile.filename ?: "",
File(localFile.filename ?: "").extension,
localFile.absolutePath,
localFile.basePath,
localFile.size
)
val track =
AudioTrack(
serverTrack.index,
serverTrack.startOffset,
serverTrack.duration,
localFile.filename ?: "",
localFile.contentUrl,
localFile.mimeType ?: "",
metadata,
true,
localFile.id,
serverTrack.index
)
tracks.add(track)
Log.d(tag, "Added local audio track ${track.contentUrl} (${track.metadata?.path})")
part.episode?.let { episode ->
val podcast = localItem.media as Podcast
localEpisodeId = podcast.addEpisode(track, episode).id
}
}
if (existingAudioTrack == null || forceAudioProbe) {
Log.d(tag, "scanLibraryItemFolder Scanning Audio File Path ${localFile.absolutePath} | ForceAudioProbe=${forceAudioProbe}")
// TODO: Make asynchronous
val audioProbeResult = probeAudioFile(localFile.absolutePath)
if (existingAudioTrack != null) {
// Update audio probe data on existing audio track
existingAudioTrack.audioProbeResult = audioProbeResult
audioTrackToAdd = existingAudioTrack
} else {
// Create new audio track
val track = AudioTrack(index, startOffset, audioProbeResult?.duration ?: 0.0, filename, localFile.contentUrl, mimeType, null, true, localFileId, audioProbeResult, null)
audioTrackToAdd = track
}
startOffset += audioProbeResult?.duration ?: 0.0
isNewOrUpdated = true
} else {
audioTrackToAdd = existingAudioTrack
part.ebookFile != null -> {
foundEbook = true
localItem.localFiles.removeAll { it.id == localFile.id }
localItem.localFiles.add(localFile)
(localItem.media as Book).ebookFile =
EBookFile(
part.ebookFile.ino,
part.ebookFile.metadata,
part.ebookFile.ebookFormat,
true,
localFile.id,
localFile.contentUrl
)
}
startOffset += audioTrackToAdd.duration
index++
audioTracks.add(audioTrackToAdd)
} else if (localFile.isEBookFile()) {
val existingLocalFile = existingLocalFiles.find { elf -> elf.id == localFileId }
if (localFolder.mediaType == "book") {
hasEBookFile = true
if (existingLocalFile == null) {
newEBookFile = EBookFile(localFileId, null, localFile.getEBookFormat() ?: "", true, localFileId, localFile.contentUrl)
}
}
} else {
val existingLocalFile = existingLocalFiles.find { elf -> elf.id == localFileId }
if (existingLocalFile == null) {
Log.d(tag, "scanLibraryItemFolder new local file found ${localFile.absolutePath}")
isNewOrUpdated = true
}
if (existingItem != null && existingItem.coverContentUrl == null) {
// Existing media item did not have a cover - cover found on scan
Log.d(tag, "scanLibraryItemFolder setting cover ${localFile.absolutePath}")
isNewOrUpdated = true
existingItem.coverAbsolutePath = localFile.absolutePath
existingItem.coverContentUrl = localFile.contentUrl
existingItem.media.coverPath = localFile.absolutePath
}
// First image file use as cover path
if (coverContentUrl == null) {
coverContentUrl = localFile.contentUrl
coverAbsolutePath = localFile.absolutePath
else -> {
localItem.coverAbsolutePath = localFile.absolutePath
localItem.coverContentUrl = localFile.contentUrl
localItem.localFiles.removeAll { it.id == localFile.id }
localItem.localFiles.add(localFile)
}
}
}
if (existingItem != null && audioTracks.isEmpty() && !hasEBookFile) {
Log.d(tag, "Local library item ${existingItem.media.metadata.title} no longer has audio tracks - removing item")
DeviceManager.dbManager.removeLocalLibraryItem(existingItem.id)
return ItemScanResult.REMOVED
} else if (existingItem != null && !isNewOrUpdated) {
Log.d(tag, "Local library item ${existingItem.media.metadata.title} has no updates")
return ItemScanResult.UPTODATE
} else if (existingItem != null) {
Log.d(tag, "Updating local library item ${existingItem.media.metadata.title}")
existingItem.updateFromScan(audioTracks,localFiles)
DeviceManager.dbManager.saveLocalLibraryItem(existingItem)
return ItemScanResult.UPDATED
} else if (audioTracks.isNotEmpty() || newEBookFile != null) {
Log.d(tag, "Found local media item named $itemFolderName with ${audioTracks.size} tracks and ${localFiles.size} local files")
val localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, itemFolder.uri.toString(), itemFolder.getSimplePath(ctx), itemFolder.getBasePath(ctx), itemFolder.getAbsolutePath(ctx),audioTracks,newEBookFile,localFiles,coverContentUrl,coverAbsolutePath)
val localLibraryItem = localMediaItem.getLocalLibraryItem()
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
return ItemScanResult.ADDED
} else {
return ItemScanResult.UPTODATE
if (tracks.isEmpty() && !foundEbook) {
callback(null)
return
}
if (item.mediaType == "book") {
tracks.sortBy { it.index }
var expectedIndex = 1
var offset = 0.0
tracks.forEach { track ->
track.index = expectedIndex++
track.startOffset = offset
offset += track.duration
}
localItem.media.setAudioTracks(tracks)
}
val result = DownloadItemScanResult(localItem, null)
item.userMediaProgress?.let { progress ->
val progressId =
if (item.episodeId.isNullOrEmpty()) localItem.id
else "${localItem.id}-$localEpisodeId"
result.localMediaProgress =
LocalMediaProgress(
progressId,
localItem.id,
localEpisodeId,
progress.duration,
progress.progress,
progress.currentTime,
progress.isFinished,
progress.ebookLocation,
progress.ebookProgress,
progress.lastUpdate,
progress.startedAt,
progress.finishedAt,
item.serverConnectionConfigId,
item.serverAddress,
item.serverUserId,
item.libraryItemId,
item.episodeId
)
DeviceManager.dbManager.saveLocalMediaProgress(result.localMediaProgress!!)
}
DeviceManager.dbManager.saveLocalLibraryItem(localItem)
callback(result)
}
private fun findFolderByPath(root: DocumentFile, subPath: String): DocumentFile? {
if (subPath.isBlank()) return root
var current = root
subPath.split('/').filter { it.isNotBlank() }.forEach { segment ->
if (segment == "." || segment == "..") return null
current = current.findFile(segment) ?: return null
}
return current
}
/**
* DownloadsProvider on Android 10 may expose an audio document without its extension through
* DocumentFile.findFile(). Match the manifest first, then match the provider-normalized base
* filename. MIME type and server-reported size are unreliable for Opus on this platform.
*/
private fun resolveExternalFile(folder: DocumentFile?, part: DownloadItemPart): DocumentFile? {
if (folder == null) return null
folder.findFile(part.filename)?.let {
return it
}
val expectedBaseName = part.filename.substringBeforeLast('.')
return folder.listFiles().firstOrNull { document ->
document.name == part.filename ||
document.fullName == part.filename ||
(part.audioTrack != null &&
document.isFile &&
(document.name ?: "").substringBeforeLast('.') == expectedBaseName) ||
(part.audioTrack != null &&
document.isFile &&
document.fullName.substringBeforeLast('.') == expectedBaseName)
}
}
private fun scanInternalDownloadItem(downloadItem:DownloadItem, cb: (DownloadItemScanResult?) -> Unit) {
val localLibraryItemId = "local_${downloadItem.libraryItemId}"
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)
} 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)
}
}
val audioTracks:MutableList<AudioTrack> = mutableListOf()
var foundEBookFile = false
downloadItem.downloadItemParts.forEach { downloadItemPart ->
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}")
} else {
if (downloadItemPart.audioTrack != null) {
val audioTrackFromServer = downloadItemPart.audioTrack
Log.d(
tag,
"scanInternalDownloadItem: Audio Track from Server index = ${audioTrackFromServer.index}"
)
val localFileId = DeviceManager.getBase64Id(file.name)
Log.d(tag, "Scan internal file localFileId=$localFileId")
val localFile = LocalFile(
localFileId,
file.name,
downloadItemPart.finalDestinationUri.toString(),
file.getBasePath(ctx),
file.absolutePath,
file.getSimplePath(ctx),
file.mimeType,
file.length()
)
localLibraryItem.localFiles.add(localFile)
// Create new audio track
val track = AudioTrack(
audioTrackFromServer.index,
audioTrackFromServer.startOffset,
audioTrackFromServer.duration,
localFile.filename ?: "",
localFile.contentUrl,
localFile.mimeType ?: "",
null,
true,
localFileId,
null,
audioTrackFromServer.index
)
audioTracks.add(track)
Log.d(
tag,
"scanInternalDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}"
)
// Add podcast episodes to library
downloadItemPart.episode?.let { podcastEpisode ->
val podcast = localLibraryItem.media as Podcast
val newEpisode = podcast.addEpisode(track, podcastEpisode)
localEpisodeId = newEpisode.id
Log.d(
tag,
"scanInternalDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}"
)
}
} else if (downloadItemPart.ebookFile != null) {
foundEBookFile = true
Log.d(tag, "scanInternalDownloadItem: Ebook file found with mimetype=${file.mimeType}")
val localFileId = DeviceManager.getBase64Id(file.name)
val localFile = LocalFile(
localFileId,
file.name,
Uri.fromFile(file).toString(),
file.getBasePath(ctx),
file.absolutePath,
file.getSimplePath(ctx),
file.mimeType,
file.length()
)
localLibraryItem.localFiles.add(localFile)
val ebookFile = EBookFile(
downloadItemPart.ebookFile.ino,
downloadItemPart.ebookFile.metadata,
downloadItemPart.ebookFile.ebookFormat,
true,
localFileId,
localFile.contentUrl
)
(localLibraryItem.media as Book).ebookFile = ebookFile
Log.d(tag, "scanInternalDownloadItem: Ebook file added to lli ${localFile.contentUrl}")
} else {
val localFileId = DeviceManager.getBase64Id(file.name)
val localFile = LocalFile(localFileId,file.name,Uri.fromFile(file).toString(),file.getBasePath(ctx),file.absolutePath,file.getSimplePath(ctx),file.mimeType,file.length())
localLibraryItem.coverAbsolutePath = localFile.absolutePath
localLibraryItem.coverContentUrl = localFile.contentUrl
localLibraryItem.localFiles.add(localFile)
}
}
}
if (audioTracks.isEmpty() && !foundEBookFile) {
Log.d(tag, "scanDownloadItem did not find any audio tracks or ebook file in folder for ${downloadItem.itemFolderPath}")
return cb(null)
}
// For books sort audio tracks then set
if (downloadItem.mediaType == "book") {
audioTracks.sortBy { it.index }
var indexCheck = 1
var startOffset = 0.0
audioTracks.forEach { audioTrack ->
if (audioTrack.index != indexCheck || audioTrack.startOffset != startOffset) {
audioTrack.index = indexCheck
audioTrack.startOffset = startOffset
}
indexCheck++
startOffset += audioTrack.duration
}
localLibraryItem.media.setAudioTracks(audioTracks)
}
val downloadItemScanResult = DownloadItemScanResult(localLibraryItem,null)
// If library item had media progress then make local media progress and save
downloadItem.userMediaProgress?.let { mediaProgress ->
val localMediaProgressId = if (downloadItem.episodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
val newLocalMediaProgress = LocalMediaProgress(
id = localMediaProgressId,
localLibraryItemId = localLibraryItemId,
localEpisodeId = localEpisodeId,
duration = mediaProgress.duration,
progress = mediaProgress.progress,
currentTime = mediaProgress.currentTime,
isFinished = false,
ebookLocation = mediaProgress.ebookLocation,
ebookProgress = mediaProgress.ebookProgress,
lastUpdate = mediaProgress.lastUpdate,
startedAt = mediaProgress.startedAt,
finishedAt = mediaProgress.finishedAt,
serverConnectionConfigId = downloadItem.serverConnectionConfigId,
serverAddress = downloadItem.serverAddress,
serverUserId = downloadItem.serverUserId,
libraryItemId = downloadItem.libraryItemId,
episodeId = downloadItem.episodeId)
Log.d(tag, "scanLibraryItemFolder: Saving local media progress ${newLocalMediaProgress.id} at progress ${newLocalMediaProgress.progress}")
DeviceManager.dbManager.saveLocalMediaProgress(newLocalMediaProgress)
downloadItemScanResult.localMediaProgress = newLocalMediaProgress
}
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
cb(downloadItemScanResult)
private fun mimeTypeFor(part: DownloadItemPart): String? {
return part.audioTrack?.mimeType
?: when (part.ebookFile?.ebookFormat?.lowercase()) {
"epub" -> "application/epub+zip"
"pdf" -> "application/pdf"
else -> "image/jpeg"
}
}
// Scan item after download and create local library item
fun scanDownloadItem(downloadItem: DownloadItem, cb: (DownloadItemScanResult?) -> Unit) {
// If downloading to internal storage handle separately
if (downloadItem.isInternalStorage) {
scanInternalDownloadItem(downloadItem, cb)
fun scanDownloadItem(item: DownloadItem, callback: (DownloadItemScanResult?) -> Unit) {
if (item.isInternalStorage) {
val id = "local_${item.libraryItemId}"
val localItem =
DeviceManager.dbManager.getLocalLibraryItem(id)
?: newLocalLibraryItem(id, item, item.itemFolderPath, item.itemFolderPath, "")
scanParts(item, localItem, callback = callback)
return
}
val folderDf = DocumentFileCompat.fromUri(ctx, Uri.parse(downloadItem.localFolder.contentUrl))
val foldersFound = folderDf?.search(true, DocumentFileType.FOLDER) ?: mutableListOf()
var itemFolderId = ""
var itemFolderUrl = ""
var itemFolderBasePath = ""
var itemFolderAbsolutePath = ""
foldersFound.forEach {
// e.g. absolute path is "storage/emulated/0/Audiobooks/Orson Scott Card/Enders Game"
// and itemSubfolder is "Orson Scott Card/Enders Game"
if (it.getAbsolutePath(ctx).endsWith(downloadItem.itemSubfolder)) {
itemFolderId = it.id
itemFolderUrl = it.uri.toString()
itemFolderBasePath = it.getBasePath(ctx)
itemFolderAbsolutePath = it.getAbsolutePath(ctx)
}
val root = DocumentFileCompat.fromUri(ctx, Uri.parse(item.localFolder.contentUrl))
if (root == null) {
Log.e(tag, "Invalid SAF root: ${item.localFolder.contentUrl}")
callback(null)
return
}
if (itemFolderUrl == "") {
Log.d(tag, "scanDownloadItem failed to find media folder")
return cb(null)
}
val df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(itemFolderUrl))
if (df == null) {
Log.e(tag, "Folder Doc File Invalid ${downloadItem.itemFolderPath}")
return cb(null)
}
val localLibraryItemId = getLocalLibraryItemId(itemFolderId)
Log.d(tag, "scanDownloadItem starting for ${downloadItem.itemFolderPath} | ${df.uri} | Item Folder Id:$itemFolderId | LLI Id:$localLibraryItemId")
// Search for files in media item folder
// m4b files showing as mimeType application/octet-stream on Android 10 and earlier see #154
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/*"))
Log.d(tag, "scanDownloadItem ${filesFound.size} files found in ${downloadItem.itemFolderPath}")
var localEpisodeId:String? = null
var localLibraryItem:LocalLibraryItem?
if (downloadItem.mediaType == "book") {
localLibraryItem = LocalLibraryItem(localLibraryItemId, downloadItem.localFolder.id, itemFolderBasePath, itemFolderAbsolutePath, itemFolderUrl, false, downloadItem.mediaType, downloadItem.media.getLocalCopy(), mutableListOf(), null, null, true, downloadItem.serverConnectionConfigId, downloadItem.serverAddress, downloadItem.serverUserId, downloadItem.libraryItemId)
} else {
// Lookup or create podcast local library item
localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
if (localLibraryItem == null) {
Log.d(tag, "[FolderScanner] Podcast local library item not created yet for ${downloadItem.media.metadata.title}")
localLibraryItem = LocalLibraryItem(localLibraryItemId, downloadItem.localFolder.id, itemFolderBasePath, itemFolderAbsolutePath, itemFolderUrl, false, downloadItem.mediaType, downloadItem.media.getLocalCopy(), mutableListOf(), null, null, true,downloadItem.serverConnectionConfigId,downloadItem.serverAddress,downloadItem.serverUserId,downloadItem.libraryItemId)
}
}
val audioTracks:MutableList<AudioTrack> = mutableListOf()
var foundEBookFile = false
filesFound.forEach { docFile ->
val itemPart = downloadItem.downloadItemParts.find { itemPart ->
itemPart.filename == docFile.name
}
if (itemPart == null) {
if (downloadItem.mediaType == "book") { // for books every download item should be a file found
Log.e(tag, "scanDownloadItem: Item part not found for doc file ${docFile.name} | ${docFile.getAbsolutePath(ctx)} | ${docFile.uri}")
}
} else if (itemPart.audioTrack != null) { // Is audio track
val audioTrackFromServer = itemPart.audioTrack
Log.d(tag, "scanDownloadItem: Audio Track from Server index = ${audioTrackFromServer.index}")
val localFileId = DeviceManager.getBase64Id(docFile.id)
val localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
localLibraryItem.localFiles.add(localFile)
// Create new audio track
val track = AudioTrack(audioTrackFromServer.index, audioTrackFromServer.startOffset, audioTrackFromServer.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, null, audioTrackFromServer.index)
audioTracks.add(track)
Log.d(tag, "scanDownloadItem: Created Audio Track with index ${track.index} from local file ${localFile.absolutePath}")
// Add podcast episodes to library
itemPart.episode?.let { podcastEpisode ->
val podcast = localLibraryItem.media as Podcast
val newEpisode = podcast.addEpisode(track, podcastEpisode)
localEpisodeId = newEpisode.id
Log.d(tag, "scanDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}")
}
} else if (itemPart.ebookFile != null) { // Ebook
foundEBookFile = true
Log.d(tag, "scanDownloadItem: Ebook file found with mimetype=${docFile.mimeType}")
val localFileId = DeviceManager.getBase64Id(docFile.id)
val localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
localLibraryItem.localFiles.add(localFile)
val ebookFile = EBookFile(itemPart.ebookFile.ino, itemPart.ebookFile.metadata, itemPart.ebookFile.ebookFormat, true, localFileId, localFile.contentUrl)
(localLibraryItem.media as Book).ebookFile = ebookFile
Log.d(tag, "scanDownloadItem: Ebook file added to lli ${localFile.contentUrl}")
} else { // Cover image
val localFileId = DeviceManager.getBase64Id(docFile.id)
val localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
localLibraryItem.coverAbsolutePath = localFile.absolutePath
localLibraryItem.coverContentUrl = localFile.contentUrl
localLibraryItem.localFiles.add(localFile)
}
}
if (audioTracks.isEmpty() && !foundEBookFile) {
Log.d(tag, "scanDownloadItem did not find any audio tracks or ebook file in folder for ${downloadItem.itemFolderPath}")
return cb(null)
}
// For books sort audio tracks then set
if (downloadItem.mediaType == "book") {
audioTracks.sortBy { it.index }
var indexCheck = 1
var startOffset = 0.0
audioTracks.forEach { audioTrack ->
if (audioTrack.index != indexCheck || audioTrack.startOffset != startOffset) {
audioTrack.index = indexCheck
audioTrack.startOffset = startOffset
}
indexCheck++
startOffset += audioTrack.duration
}
localLibraryItem.media.setAudioTracks(audioTracks)
}
val downloadItemScanResult = DownloadItemScanResult(localLibraryItem,null)
// If library item had media progress then make local media progress and save
downloadItem.userMediaProgress?.let { mediaProgress ->
val localMediaProgressId = if (downloadItem.episodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
val newLocalMediaProgress = LocalMediaProgress(
id = localMediaProgressId,
localLibraryItemId = localLibraryItemId,
localEpisodeId = localEpisodeId,
duration = mediaProgress.duration,
progress = mediaProgress.progress,
currentTime = mediaProgress.currentTime,
isFinished = false,
ebookLocation = mediaProgress.ebookLocation,
ebookProgress = mediaProgress.ebookProgress,
lastUpdate = mediaProgress.lastUpdate,
startedAt = mediaProgress.startedAt,
finishedAt = mediaProgress.finishedAt,
serverConnectionConfigId = downloadItem.serverConnectionConfigId,
serverAddress = downloadItem.serverAddress,
serverUserId = downloadItem.serverUserId,
libraryItemId = downloadItem.libraryItemId,
episodeId = downloadItem.episodeId)
Log.d(tag, "scanLibraryItemFolder: Saving local media progress ${newLocalMediaProgress.id} at progress ${newLocalMediaProgress.progress}")
DeviceManager.dbManager.saveLocalMediaProgress(newLocalMediaProgress)
downloadItemScanResult.localMediaProgress = newLocalMediaProgress
}
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
cb(downloadItemScanResult)
}
fun scanLocalLibraryItem(localLibraryItem:LocalLibraryItem, forceAudioProbe:Boolean):LocalLibraryItemScanResult? {
val df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(localLibraryItem.contentUrl))
if (df == null) {
Log.e(tag, "Item Folder Doc File Invalid ${localLibraryItem.absolutePath}")
return null
}
Log.d(tag, "scanLocalLibraryItem starting for ${localLibraryItem.absolutePath} | ${df.uri}")
var wasUpdated = false
// Search for files in media item folder
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4", "application/*"))
Log.d(tag, "scanLocalLibraryItem ${filesFound.size} files found in ${localLibraryItem.absolutePath}")
filesFound.forEach {
try {
Log.d(tag, "Checking file found ${it.name} | ${it.id}")
}catch(e:Exception) {
Log.d(tag, "Check file found exception", e)
}
}
val existingAudioTracks = localLibraryItem.media.getAudioTracks()
// Remove any files no longer found in library item folder
val existingLocalFileIds = localLibraryItem.localFiles.map { it.id }
existingLocalFileIds.forEach { localFileId ->
Log.d(tag, "Checking local file id is there $localFileId")
if (filesFound.find { DeviceManager.getBase64Id(it.id) == localFileId } == null) {
Log.d(tag, "scanLocalLibraryItem file $localFileId was removed from ${localLibraryItem.absolutePath}")
localLibraryItem.localFiles.removeIf { it.id == localFileId }
if (existingAudioTracks.find { it.localFileId == localFileId } != null) {
Log.d(tag, "scanLocalLibraryItem audio track file $localFileId was removed from ${localLibraryItem.absolutePath}")
localLibraryItem.media.removeAudioTrack(localFileId)
}
wasUpdated = true
}
}
filesFound.forEach { docFile ->
val localFileId = DeviceManager.getBase64Id(docFile.id)
val existingLocalFile = localLibraryItem.localFiles.find { it.id == localFileId }
if (existingLocalFile == null || (existingLocalFile.isAudioFile() && forceAudioProbe)) {
val localFile = existingLocalFile ?: LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx), docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
if (existingLocalFile == null) {
localLibraryItem.localFiles.add(localFile)
Log.d(tag, "scanLocalLibraryItem new file found ${localFile.filename}")
}
if (localFile.isAudioFile()) {
// TODO: Make asynchronous
val audioProbeResult = probeAudioFile(localFile.absolutePath)
val existingTrack = existingAudioTracks.find { audioTrack ->
audioTrack.localFileId == localFileId
}
if (existingTrack == null) {
// Create new audio track
val lastTrack = existingAudioTracks.lastOrNull()
val startOffset = (lastTrack?.startOffset ?: 0.0) + (lastTrack?.duration ?: 0.0)
val track = AudioTrack(existingAudioTracks.size, startOffset, audioProbeResult?.duration ?: 0.0, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, null)
localLibraryItem.media.addAudioTrack(track)
Log.d(tag, "Added New Audio Track ${track.title}")
wasUpdated = true
} else {
existingTrack.audioProbeResult = audioProbeResult
// TODO: Update data found from probe
Log.d(tag, "Updated Audio Track Probe Data ${existingTrack.title}")
wasUpdated = true
}
} else if (localFile.isEBookFile()) {
if (localLibraryItem.mediaType == "book") {
val existingEbookFile = (localLibraryItem.media as Book).ebookFile
if (existingEbookFile == null || existingEbookFile.localFileId != localFileId) {
val ebookFile = EBookFile(localFileId, null, localFile.getEBookFormat() ?: "", true, localFileId, localFile.contentUrl)
(localLibraryItem.media as Book).ebookFile = ebookFile
Log.d(tag, "scanLocalLibraryItem: Ebook file added to lli ${localFile.contentUrl}")
wasUpdated = true
}
}
} else { // Check if cover is empty
if (localLibraryItem.coverContentUrl == null) {
Log.d(tag, "scanLocalLibraryItem setting cover for ${localLibraryItem.media.metadata.title}")
localLibraryItem.coverContentUrl = localFile.contentUrl
localLibraryItem.coverAbsolutePath = localFile.absolutePath
wasUpdated = true
}
}
}
}
if (wasUpdated) {
Log.d(tag, "Local library item was updated - saving it")
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
} else {
Log.d(tag, "Local library item was up-to-date")
}
return LocalLibraryItemScanResult(wasUpdated, localLibraryItem)
}
private fun probeAudioFile(absolutePath:String):AudioProbeResult? {
val session = FFprobeKit.execute("-i \"${absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet")
var probeObject:JSObject? = null
try {
probeObject = JSObject(session.output)
} catch(error:JSONException) {
Log.e(tag, "Failed to parse probe result $error")
}
Log.d(tag, "FFprobe output $probeObject")
return if (probeObject == null || !probeObject.has("streams")) { // Check if output is empty
Log.d(tag, "probeAudioFile Probe audio file $absolutePath failed or invalid")
null
} else {
val audioProbeResult = jacksonMapper.readValue<AudioProbeResult>(session.output)
Log.d(tag, "Probe Result DATA ${audioProbeResult.duration} | ${audioProbeResult.size} | ${audioProbeResult.title} | ${audioProbeResult.artist}")
audioProbeResult
val itemFolder = findFolderByPath(root, item.itemSubfolder)
if (itemFolder == null) {
Log.e(tag, "SAF item folder not found: ${item.itemSubfolder}")
callback(null)
return
}
val id = localLibraryItemId(itemFolder.id)
val localItem =
DeviceManager.dbManager.getLocalLibraryItem(id)
?: newLocalLibraryItem(
id,
item,
itemFolder.getBasePath(ctx),
itemFolder.getAbsolutePath(ctx),
itemFolder.uri.toString()
)
scanParts(item, localItem, itemFolder, callback)
}
}
@@ -4,8 +4,9 @@ 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
class DbManager {
val tag = "DbManager"
@@ -22,45 +23,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 +71,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 +84,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 +109,108 @@ 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)
/**
* Validate persisted download queue entries before restoring them.
*
* Queue entries can become incompatible or malformed, for example after an app update. If any
* entry cannot be read or fails the checks required for queue restoration, the entire persisted
* queue is cleared so startup can continue without restoring invalid data.
*
* @return true if the queue passes validation or was successfully cleared; false if an invalid
* queue could not be cleared
*/
fun ensureValidDownloadQueue(): Boolean {
val downloadBook = Paper.book("downloadItems")
val queueIsValid =
try {
downloadBook.allKeys
.map { downloadItemId ->
val item =
downloadBook.read<DownloadItem>(downloadItemId)
?: throw IllegalStateException(
"Download queue entry $downloadItemId could not be read"
)
if (item.id != downloadItemId) {
throw IllegalStateException(
"Download queue entry $downloadItemId has a mismatched item ID"
)
}
item
}
.all { item ->
listOf<String?>(item.id, item.libraryItemId, item.localFolder.id).all {
!it.isNullOrBlank()
} &&
item.downloadItemParts.isNotEmpty() &&
item.downloadItemParts.all { part ->
listOf<String?>(
part.id,
part.downloadItemId,
part.filename,
part.destinationPath,
part.finalDestinationPath,
part.serverPath,
part.localFolderId
)
.all { !it.isNullOrBlank() } &&
part.downloadItemId == item.id
}
}
} catch (readError: Exception) {
AbsLogger.error(
tag,
"Could not read persisted download queue; clearing queue (${readError.message})"
)
false
}
if (queueIsValid) return true
AbsLogger.error(tag, "Persisted download queue contains invalid data; clearing queue")
return try {
downloadBook.destroy()
val remainingKeys = downloadBook.allKeys
if (remainingKeys.isEmpty()) {
AbsLogger.info(tag, "Cleared invalid persisted download queue")
true
} else {
AbsLogger.error(
tag,
"Invalid persisted download queue still contains ${remainingKeys.size} entries after clearing"
)
false
}
} catch (clearError: Exception) {
AbsLogger.error(
tag,
"Could not clear invalid persisted download queue (${clearError.message})"
)
false
}
}
// For books this will just be the localLibraryItemId for podcast episodes this will be "{localLibraryItemId}-{episodeId}"
fun getLocalMediaProgress(localMediaProgressId:String): LocalMediaProgress? {
fun saveLocalMediaProgress(mediaProgress: LocalMediaProgress) {
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? {
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 +218,7 @@ class DbManager {
}
return mediaProgress
}
fun removeLocalMediaProgress(localMediaProgressId:String) {
fun removeLocalMediaProgress(localMediaProgressId: String) {
Paper.book("localMediaProgress").delete(localMediaProgressId)
}
@@ -151,57 +227,82 @@ class DbManager {
}
// Make sure all local file ids still exist
fun cleanLocalLibraryItems() {
fun cleanLocalLibraryItems(context: Context) {
val localLibraryItems = getLocalLibraryItems()
localLibraryItems.forEach { lli ->
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 exists = localFile.exists(context)
if (!exists) {
Log.d(
tag,
"cleanLocalLibraryItems: Local file ${localFile.absolutePath} was removed from library item ${lli.media.metadata.title}"
)
hasUpdates = true
}
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
lli.coverAbsolutePath?.let {
val coverFile = File(it)
if (!coverFile.exists()) {
Log.d(tag, "cleanLocalLibraryItems: Cover $it was removed from library item ${lli.media.metadata.title}")
val coverExists =
lli.localFiles.any { localFile ->
localFile.absolutePath == it && localFile.exists(context)
}
if (!coverExists) {
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 +316,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 +337,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 +349,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 +370,36 @@ 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"
)
}
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,89 @@
package com.audiobookshelf.app.managers
import android.content.Context
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.models.DownloadItem
import com.audiobookshelf.app.plugins.AbsLogger
import java.io.File
import java.util.concurrent.TimeUnit
/** Removes only staging data for terminally failed downloads after their retention window. */
object IncompleteDownloadCleanup {
private const val tag = "IncompleteDownloadCleanup"
private const val RETENTION_MS = 24L * 60L * 60L * 1000L
private const val WORK_PREFIX = "incomplete-download-"
fun schedule(context: Context, item: DownloadItem) {
val failedAt = item.terminalFailureAt ?: return
if (item.stagingCleanupAt != null) return
val delay = (failedAt + RETENTION_MS - System.currentTimeMillis()).coerceAtLeast(0L)
val request = OneTimeWorkRequestBuilder<IncompleteDownloadCleanupWorker>()
.setInitialDelay(delay, TimeUnit.MILLISECONDS)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
WORK_PREFIX + item.id, ExistingWorkPolicy.REPLACE, request)
}
fun cancel(context: Context, itemId: String) {
WorkManager.getInstance(context).cancelUniqueWork(WORK_PREFIX + itemId)
}
/** Cleans staging data retained longer than 24 hours when scheduled work did not run. */
@Synchronized
fun cleanupExpired(context: Context) {
val now = System.currentTimeMillis()
DeviceManager.dbManager.getDownloadItems()
.filter { item -> isEligible(item, now) }
.forEach { item ->
deleteItem(context, item)
}
}
private fun isEligible(item: DownloadItem, now: Long): Boolean {
val failedAt = item.terminalFailureAt ?: return false
if (item.stagingCleanupAt != null) return false
if (now - failedAt < RETENTION_MS) return false
return item.downloadItemParts.all { part ->
part.moved || (part.failed && !part.isMoving)
}
}
private fun deleteItem(context: Context, item: DownloadItem) {
item.downloadItemParts.forEach { part ->
deleteAppOwnedFile(context, File(part.destinationPath))
if (!part.moved) {
part.bytesDownloaded = 0L
part.completed = false
}
}
item.stagingCleanupAt = System.currentTimeMillis()
DeviceManager.dbManager.saveDownloadItem(item)
cancel(context, item.id)
AbsLogger.info(tag, "Deleted staging files for terminally failed download item ${item.id}")
}
private fun deleteAppOwnedFile(context: Context, file: File) {
val path = file.absolutePath
val internal = context.filesDir.absolutePath
val external = context.getExternalFilesDir(null)?.absolutePath
if (path.startsWith(internal) || (external != null && path.startsWith(external))) {
if (file.exists() && !file.delete()) AbsLogger.error(tag, "Could not delete expired staging file $path")
file.parentFile?.takeIf { it.isDirectory && it.list()?.isEmpty() == true }?.delete()
} else {
AbsLogger.error(tag, "Refusing to delete non-app-owned path $path")
}
}
}
class IncompleteDownloadCleanupWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
DbManager.initialize(applicationContext)
IncompleteDownloadCleanup.cleanupExpired(applicationContext)
return Result.success()
}
}
@@ -1,74 +1,203 @@
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 com.audiobookshelf.app.plugins.AbsLogger
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
import java.util.concurrent.TimeUnit
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
class InternalDownloadManager(outputStream:FileOutputStream, private val progressCallback: DownloadItemManager.InternalProgressCallback) : AutoCloseable {
/** Streams a download into an app-owned staging file. */
class InternalDownloadManager(
private val destinationFile: File,
private val expectedSize: Long,
private val progressCallback: DownloadItemManager.InternalProgressCallback,
private val hasAvailableSpace: () -> Boolean
) {
private val tag = "InternalDownloadManager"
private val client: OkHttpClient = OkHttpClient()
private val writer = BinaryFileWriter(outputStream, progressCallback)
@Throws(IOException::class)
fun download(url:String) {
val request: Request = Request.Builder().url(url).build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
Log.e(tag, "download URL $url FAILED")
progressCallback.onComplete(true)
}
override fun onResponse(call: Call, response: Response) {
val responseBody: ResponseBody = response.body
?: throw IllegalStateException("Response doesn't contain a file")
val length: Long = (response.header(CONTENT_LENGTH, "1") ?: "0").toLong()
writer.write(responseBody.byteStream(), length)
}
})
interface DownloadHandle {
fun cancel()
}
@Throws(Exception::class)
override fun close() {
writer.close()
}
}
private class ActiveDownloadHandle : DownloadHandle {
private val cancelled = AtomicBoolean(false)
private val activeCall = AtomicReference<Call?>()
class BinaryFileWriter(outputStream: OutputStream, progressCallback: DownloadItemManager.InternalProgressCallback) :
AutoCloseable {
private val outputStream: OutputStream
private val progressCallback: DownloadItemManager.InternalProgressCallback
fun setCall(call: Call) {
activeCall.set(call)
if (cancelled.get()) call.cancel()
}
init {
this.outputStream = outputStream
this.progressCallback = progressCallback
}
@Throws(IOException::class)
fun write(inputStream: InputStream?, length: Long): Long {
BufferedInputStream(inputStream).use { input ->
val dataBuffer = ByteArray(CHUNK_SIZE)
var readBytes: Int
var totalBytes: Long = 0
while (input.read(dataBuffer).also { readBytes = it } != -1) {
totalBytes += readBytes.toLong()
outputStream.write(dataBuffer, 0, readBytes)
progressCallback.onProgress(totalBytes, (totalBytes * 100L) / length)
}
progressCallback.onComplete(false)
return totalBytes
override fun cancel() {
cancelled.set(true)
activeCall.get()?.cancel()
}
}
@Throws(IOException::class)
override fun close() {
outputStream.close()
/**
* Starts or resumes a download.
*
* @param url download URL
* @param token access token sent in the Authorization header
* @return logical handle used to cancel the active request, including a restarted request
*/
fun download(url: String, token: String): DownloadHandle {
destinationFile.parentFile?.mkdirs()
val handle = ActiveDownloadHandle()
startRequest(url, token, handle, allowRestart = true)
return handle
}
companion object {
private const val CHUNK_SIZE = 1024
private fun startRequest(
url: String,
token: String,
handle: ActiveDownloadHandle,
allowRestart: Boolean
) {
var existingBytes = destinationFile.takeIf { it.exists() }?.length() ?: 0L
AbsLogger.info(
tag,
"Starting ${if (existingBytes > 0L) "resumed" else "new"} download for ${destinationFile.name} at byte $existingBytes")
val request =
Request.Builder()
.url(url)
.addHeader("Accept-Encoding", "identity")
.addHeader("Authorization", "Bearer $token")
.apply { if (existingBytes > 0L) header("Range", "bytes=$existingBytes-") }
.build()
val call = client.newCall(request)
handle.setCall(call)
call.enqueue(
object : Callback {
override fun onFailure(call: Call, e: IOException) {
AbsLogger.error(tag, "Download request failed for ${destinationFile.name}: ${e.message}")
progressCallback.onComplete(true)
}
override fun onResponse(call: Call, response: Response) {
response.use {
try {
if (response.code == 401) {
AbsLogger.error(tag, "Download unauthorized (401) for ${destinationFile.name}")
progressCallback.onAuthError()
return
}
if (response.code == 416) {
val serverSize =
response.header("Content-Range")
?.removePrefix("bytes */")
?.toLongOrNull()
if (serverSize != null) progressCallback.onSizeResolved(serverSize)
if (serverSize != null && existingBytes == serverSize) {
progressCallback.onProgress(existingBytes, 100L)
AbsLogger.info(tag, "Download completed for ${destinationFile.name} ($existingBytes bytes)")
progressCallback.onComplete(false)
} else if (allowRestart && destinationFile.delete()) {
AbsLogger.info(tag, "Restarting stale range from byte zero for ${destinationFile.name}")
startRequest(url, token, handle, allowRestart = false)
} else {
AbsLogger.error(tag, "Could not recover invalid range for ${destinationFile.name} at byte $existingBytes")
progressCallback.onComplete(true)
}
return
}
val append =
existingBytes > 0L &&
response.code == 206 &&
hasExpectedRange(response, existingBytes)
if (existingBytes > 0L && !append && response.code != 200) {
AbsLogger.error(
tag,
"Invalid resume response ${response.code} for ${destinationFile.name} at byte $existingBytes"
)
progressCallback.onComplete(true)
return
}
if (!response.isSuccessful || response.body == null) {
AbsLogger.error(tag, "Download HTTP failure ${response.code} for ${destinationFile.name}")
progressCallback.onComplete(true)
return
}
val startingBytes = if (append) existingBytes else 0L
val responseLength = response.body!!.contentLength()
val serverSize =
if (append) contentRangeTotal(response)
else responseLength.takeIf { it >= 0L }
if (serverSize != null) progressCallback.onSizeResolved(serverSize)
val totalLength =
serverSize ?: if (expectedSize > 0L) expectedSize else 0L
FileOutputStream(destinationFile, append).use { output ->
response.body!!.byteStream().use { input ->
val buffer = ByteArray(CHUNK_SIZE)
var totalBytes = startingBytes
while (true) {
val read = input.read(buffer)
if (read < 0) break
if (!hasAvailableSpace())
throw IOException("Download paused to preserve free storage")
output.write(buffer, 0, read)
totalBytes += read
val progress =
if (totalLength > 0L) (totalBytes * 100L) / totalLength else 0L
progressCallback.onProgress(totalBytes, progress.coerceAtMost(100L))
}
}
}
val downloadedSize = destinationFile.length()
if (serverSize != null && downloadedSize != serverSize) {
AbsLogger.error(
tag,
"Downloaded size for ${destinationFile.name} was $downloadedSize, expected server size $serverSize"
)
progressCallback.onComplete(true)
} else {
AbsLogger.info(
tag,
"Download completed for ${destinationFile.name} ($downloadedSize bytes)"
)
progressCallback.onComplete(false)
}
} catch (e: IOException) {
AbsLogger.error(tag, "Could not write staging file ${destinationFile.name}: ${e.message}")
progressCallback.onComplete(true)
}
}
}
}
)
}
private fun hasExpectedRange(response: Response, offset: Long): Boolean {
val range = response.header("Content-Range") ?: return false
val match = CONTENT_RANGE.matchEntire(range) ?: return false
return match.groupValues[1].toLongOrNull() == offset &&
match.groupValues[2].toLongOrNull()?.let { it >= offset } == true
}
private fun contentRangeTotal(response: Response): Long? =
CONTENT_RANGE.matchEntire(response.header("Content-Range") ?: "")
?.groupValues
?.get(3)
?.takeUnless { it == "*" }
?.toLongOrNull()
private companion object {
const val CHUNK_SIZE = 512 * 1024 // 512 KB
val CONTENT_RANGE = Regex("bytes (\\d+)-(\\d+)/(\\d+|\\*)")
val client =
OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.build()
}
}
@@ -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()
}
}
@@ -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,49 +477,68 @@ 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()
}
/**
* Sends the current sleep timer state to the client.
* Called when app resumes from background to sync UI state.
*/
fun sendCurrentSleepTimerState() {
if (sleepTimerRunning) {
val timeRemaining = getSleepTimerTimeRemainingSeconds(getPlaybackSpeed())
playerNotificationService.clientEventEmitter?.onSleepTimerSet(timeRemaining, isAutoSleepTimer)
} else {
// No timer running - send 0 to clear any stale UI state
playerNotificationService.clientEventEmitter?.onSleepTimerSet(0, false)
}
}
}
@@ -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()
)
}
}
@@ -9,24 +9,38 @@ import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.server.ApiHandler
import com.getcapacitor.JSObject
import java.util.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.launch
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()
@@ -35,10 +49,49 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
var userSettingsPlaybackRate:Float? = null
// Android Auto browses on the media browser service main thread, so the server pings
// and authorize call must not run there or they block it and trigger an ANR
private val androidAutoIOScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
// Android Auto re-requests the browse root every ~2s and multiple clients do so at once,
// so overlapping loads are coalesced into one to avoid racing on the shared server state
private var isLoadingAndroidAutoItems = false
private val pendingAndroidAutoLoadCallbacks = mutableListOf<() -> Unit>()
private val androidAutoLoadLock = Any()
fun getIsLibrary(id:String) : Boolean {
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 +144,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) {
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 +185,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 +681,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 +703,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)
}
}
}
@@ -246,15 +743,22 @@ class MediaManager(private var apiHandler: ApiHandler, var ctx: Context) {
return mediaProgress
}
private fun checkSetValidServerConnectionConfig(cb: (Boolean) -> Unit) = runBlocking {
Log.d(tag, "checkSetValidServerConnectionConfig | $serverConfigIdUsed")
// Runs on [androidAutoIOScope] rather than blocking the caller: this is reached from
// onLoadChildren on the media browser service main thread, and the pings/authorize below
// are network calls, so runBlocking here caused ANRs while browsing in Android Auto
private fun checkSetValidServerConnectionConfig(cb: (Boolean) -> Unit) = androidAutoIOScope.launch {
Log.d(tag, "checkSetValidServerConnectionConfig | serverConfigIdUsed=$serverConfigIdUsed | lastServerConnectionConfigId=${DeviceManager.deviceData.lastServerConnectionConfigId}")
coroutineScope {
if (!DeviceManager.checkConnectivity(ctx)) {
serverUserMediaProgress = mutableListOf()
Log.d(tag, "checkSetValidServerConnectionConfig: No connectivity")
cb(false)
} else if (DeviceManager.deviceData.lastServerConnectionConfigId.isNullOrBlank()) { // If in offline mode last server connection config is unset
serverUserMediaProgress = mutableListOf()
Log.d(tag, "checkSetValidServerConnectionConfig: No last server connection config")
cb(false)
} else {
var hasValidConn = false
var lookupMediaProgress = true
@@ -325,40 +829,124 @@ 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")
// Coalesce overlapping calls into a single load, every caller still gets its callback
synchronized(androidAutoLoadLock) {
pendingAndroidAutoLoadCallbacks.add(cb)
if (isLoadingAndroidAutoItems) {
Log.d(tag, "loadAndroidAutoItems: Load already in progress, queued callback")
return
}
isLoadingAndroidAutoItems = true
}
val onLoadFinished = {
val callbacks: List<() -> Unit>
synchronized(androidAutoLoadLock) {
isLoadingAndroidAutoItems = false
callbacks = pendingAndroidAutoLoadCallbacks.toList()
pendingAndroidAutoLoadCallbacks.clear()
}
callbacks.forEach { it() }
}
// Check if any valid server connection if not use locally downloaded books
checkSetValidServerConnectionConfig { isConnected ->
if (isConnected) {
serverConfigIdUsed = DeviceManager.serverConnectionConfigId
Log.d(tag, "loadAndroidAutoItems: Connected to server config id=$serverConfigIdUsed")
loadLibraries { libraries ->
if (libraries.isEmpty()) {
Log.w(tag, "No libraries returned from server request")
cb()
onLoadFinished()
} 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
}
onLoadFinished() // Fully loaded
}
}
} else { // Not connected to server
cb()
Log.d(tag, "loadAndroidAutoItems: Not connected to server")
onLoadFinished()
}
}
}
/**
* 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)
}
}
}
@@ -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,34 +258,50 @@ class MediaProgressSyncer(val playerNotificationService: PlayerNotificationServi
saveLocalProgress(it)
lastSyncTime = System.currentTimeMillis()
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()
@@ -276,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 {
@@ -304,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
@@ -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))
}
@@ -6,34 +6,40 @@ import com.audiobookshelf.app.data.MediaType
import com.fasterxml.jackson.annotation.JsonIgnore
data class DownloadItem(
val id: String,
val libraryItemId:String,
val episodeId:String?,
val userMediaProgress: MediaProgress?,
val serverConnectionConfigId:String,
val serverAddress:String,
val serverUserId:String,
val mediaType: String,
val itemFolderPath:String,
val localFolder: LocalFolder,
val itemTitle: String,
val itemSubfolder: String,
val media: MediaType,
val downloadItemParts: MutableList<DownloadItemPart>
val id: String,
val libraryItemId: String,
val episodeId: String?,
val userMediaProgress: MediaProgress?,
val serverConnectionConfigId: String,
val serverAddress: String,
val serverUserId: String,
val mediaType: String,
val itemFolderPath: String,
val localFolder: LocalFolder,
val itemTitle: String,
val itemSubfolder: String,
val media: MediaType,
val downloadItemParts: MutableList<DownloadItemPart>,
@JsonIgnore var terminalFailureAt: Long? = null,
@JsonIgnore var stagingCleanupAt: Long? = null
) {
@get:JsonIgnore
val isInternalStorage get() = localFolder.id.startsWith("internal-")
val isInternalStorage
get() = localFolder.id.startsWith("internal-")
@get:JsonIgnore
val isDownloadFinished get() = !downloadItemParts.any { !it.completed || it.isMoving }
val isDownloadFinished
get() = downloadItemParts.isNotEmpty() && downloadItemParts.all {
it.completed && it.moved && !it.isMoving && !it.failed
}
@JsonIgnore
fun getNextDownloadItemParts(limit:Int): MutableList<DownloadItemPart> {
fun getNextDownloadItemParts(limit: Int): MutableList<DownloadItemPart> {
val itemParts = mutableListOf<DownloadItemPart>()
if (limit == 0) return itemParts
for (it in downloadItemParts) {
if (!it.completed && it.downloadId == null) {
if (!it.completed && !it.failed && it.downloadId == null) {
itemParts.add(it)
if (itemParts.size >= limit) break
}
@@ -1,6 +1,5 @@
package com.audiobookshelf.app.models
import android.app.DownloadManager
import android.net.Uri
import android.util.Log
import com.audiobookshelf.app.data.AudioTrack
@@ -15,7 +14,8 @@ data class DownloadItemPart(
val id: String,
val downloadItemId: String,
val filename: String,
val fileSize: Long,
var fileSize: Long,
@JsonIgnore val destinationPath: String,
val finalDestinationPath:String,
val serverPath: String,
val localFolderName: String,
@@ -31,25 +31,31 @@ data class DownloadItemPart(
@JsonIgnore val uri: Uri,
@JsonIgnore val destinationUri: Uri,
@JsonIgnore val finalDestinationUri: Uri,
@JsonIgnore var completedDestinationUri: String?,
val finalDestinationSubfolder: String,
var downloadId: Long?,
@JsonIgnore var lastUpdateTime: Long?,
var progress: Long,
var bytesDownloaded: Long
var bytesDownloaded: Long,
@JsonIgnore var retryCount: Int = 0,
@JsonIgnore var authRetryCount: Int = 0,
@JsonIgnore var waitingForSpace: Boolean = false,
@JsonIgnore var reusedExistingFile: Boolean = false
) {
companion object {
fun make(downloadItemId:String, filename:String, fileSize: Long, destinationFile: File, finalDestinationFile: File, subfolder:String, serverPath:String, localFolder: LocalFolder, ebookFile: EBookFile?, audioTrack: AudioTrack?, episode: PodcastEpisode?) :DownloadItemPart {
val destinationUri = Uri.fromFile(destinationFile)
val finalDestinationUri = Uri.fromFile(finalDestinationFile)
val rawCover = if (serverPath.endsWith("/cover")) "?raw=1" else ""
val downloadUri = Uri.parse("${DeviceManager.serverAddress}${serverPath}$rawCover")
var downloadUrl = "${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}"
if (serverPath.endsWith("/cover")) downloadUrl += "&format=jpeg&raw=1" // For cover images force to jpeg
val downloadUri = Uri.parse(downloadUrl)
Log.d("DownloadItemPart", "Audio File Destination Uri: $destinationUri | Final Destination Uri: $finalDestinationUri | Download URI $downloadUri")
Log.d("DownloadItemPart", "Audio File Destination Uri: $destinationUri | Final Destination Uri: $finalDestinationUri | Server Path $serverPath")
return DownloadItemPart(
id = DeviceManager.getBase64Id(finalDestinationFile.absolutePath),
downloadItemId,
filename = filename,
fileSize = fileSize,
destinationPath = destinationFile.absolutePath,
finalDestinationPath = finalDestinationFile.absolutePath,
serverPath = serverPath,
localFolderName = localFolder.name,
@@ -65,10 +71,13 @@ data class DownloadItemPart(
uri = downloadUri,
destinationUri = destinationUri,
finalDestinationUri = finalDestinationUri,
completedDestinationUri = null,
finalDestinationSubfolder = subfolder,
downloadId = null,
lastUpdateTime = null,
progress = 0,
bytesDownloaded = 0
bytesDownloaded = 0,
reusedExistingFile = false
)
}
}
@@ -76,16 +85,4 @@ data class DownloadItemPart(
@get:JsonIgnore
val isInternalStorage get() = localFolderId.startsWith("internal-")
@get:JsonIgnore
val serverUrl get() = "${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}"
@JsonIgnore
fun getDownloadRequest(): DownloadManager.Request {
val dlRequest = DownloadManager.Request(uri)
dlRequest.setTitle(filename)
dlRequest.setDescription("Downloading to $localFolderName with filename $filename")
dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE)
dlRequest.setDestinationUri(destinationUri)
return dlRequest
}
}
@@ -6,18 +6,13 @@ import android.graphics.ImageDecoder
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.support.v4.media.MediaMetadataCompat
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
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import kotlinx.coroutines.*
const val NOTIFICATION_LARGE_ICON_SIZE = 144 // px
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
@@ -38,12 +33,19 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
callback: PlayerNotificationManager.BitmapCallback
): Bitmap? {
val albumArtUri = controller.metadata.description.iconUri
val albumBitmap = controller.metadata.description.iconBitmap
?: controller.metadata.getBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART)
// Reuse bitmap from queue navigator (local) or PlaybackSession.resolveCoverBitmapAsync (streaming)
// 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) {
@@ -57,37 +59,14 @@ class AbMediaDescriptionAdapter constructor(private val controller: MediaControl
} else {
serviceScope.launch {
currentBitmap = albumArtUri?.let {
resolveUriAsBitmap(it)
resolveUriAsBitmap(playerNotificationService, it)
}
currentBitmap?.let { callback.onBitmap(it) }
}
null
}
} else {
currentBitmap
}
}
private suspend fun resolveUriAsBitmap(uri: Uri): Bitmap? {
return withContext(Dispatchers.IO) {
try {
Glide.with(playerNotificationService)
.asBitmap()
.load(uri)
.placeholder(R.drawable.icon)
.error(R.drawable.icon)
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)
.get()
} catch (e: Exception) {
e.printStackTrace()
Glide.with(playerNotificationService)
.asBitmap()
.load(Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon))
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)
.get()
}
}
}
}
@@ -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__"
@@ -21,6 +21,31 @@ class CastManager constructor(val mainActivity:Activity) {
private var playerNotificationService:PlayerNotificationService? = null
private var newConnectionListener: SessionListener? = null
private var castStateListener: CastStateListener? = null
/**
* The framework reports NO_DEVICES_AVAILABLE when a session ends, because it stops discovering -
* taken at face value that hides the cast button. The media router is the second opinion.
*/
private fun isReceiverAvailable(castState: Int): Boolean {
if (castState != CastState.NO_DEVICES_AVAILABLE) return true
// The framework's own selector, which carries the receiver id this app is configured with -
// building one by hand here would ask about a different receiver than the one being cast to
val selector = getContext().mergedSelector ?: return false
return getMediaRouter()?.isRouteAvailable(
selector,
MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE or
MediaRouter.AVAILABILITY_FLAG_REQUIRE_MATCH
) == true
}
/** Drops what this manager registered for the activity, which does not outlive it. */
fun detach() {
castStateListener?.let { getContext().removeCastStateListener(it) }
castStateListener = null
}
private fun switchToPlayer(useCastPlayer:Boolean) {
Handler(Looper.getMainLooper()).post() {
@@ -123,6 +148,13 @@ class CastManager constructor(val mainActivity:Activity) {
}
fun startRouteScan(connListener:ChromecastListener) {
// ChromecastListener has always implemented CastStateListener, but nothing registered it, so
// availability hung entirely on the media router callback below - which does not always fire
val stateListener = CastStateListener { state -> connListener.onReceiverAvailableUpdate(isReceiverAvailable(state)) }
castStateListener = stateListener
getContext().addCastStateListener(stateListener)
connListener.onReceiverAvailableUpdate(isReceiverAvailable(getContext().castState))
val callback = object : ScanCallback() {
override fun onRouteUpdate(routes: List<MediaRouter.RouteInfo>?) {
Log.d(tag, "CAST On ROUTE UPDATED ${routes?.size} | ${getContext().castState}")
@@ -3,6 +3,7 @@ package com.audiobookshelf.app.player
import android.annotation.SuppressLint
import android.os.Looper
import android.util.Log
import kotlin.math.roundToInt
import android.view.Surface
import android.view.SurfaceHolder
import android.view.SurfaceView
@@ -53,6 +54,10 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
private var myCurrentTrackGroups: TrackGroupArray
private var myCurrentTrackSelections: TrackSelectionArray
// Cache for pending volume to handle async setVolume
private var pendingVolume: Int? = null
private var pendingVolumeSetAt: Long = 0L
var currentMediaItems:List<MediaItem> = mutableListOf()
var remoteMediaClient:RemoteMediaClient? = null
var sessionAvailabilityListener:SessionAvailabilityListener? = null
@@ -76,7 +81,10 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
COMMAND_GET_MEDIA_ITEMS_METADATA,
COMMAND_SET_MEDIA_ITEMS_METADATA,
COMMAND_CHANGE_MEDIA_ITEMS,
COMMAND_GET_TRACKS)
COMMAND_GET_TRACKS,
COMMAND_GET_DEVICE_VOLUME,
COMMAND_SET_DEVICE_VOLUME,
COMMAND_ADJUST_DEVICE_VOLUME)
.build()
var currentPlaybackState = Player.STATE_IDLE
@@ -807,12 +815,21 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
return AudioAttributes.DEFAULT
}
/**
* The volume of the media stream, which is what the sleep timer fades - not the volume of the
* receiver. The device volume stays where the user put it, and on a cast group the balance
* between its members is left alone, because the receiver applies this to the group's output.
*/
override fun setVolume(audioVolume: Float) {
try {
remoteMediaClient?.setStreamVolume(audioVolume.toDouble().coerceIn(0.0, 1.0))
} catch (e: Exception) {
Log.e(tag, "Failed to set the cast stream volume", e)
}
}
override fun getVolume(): Float {
return 0F
return remoteMediaClient?.mediaStatus?.streamVolume?.toFloat() ?: 1F
}
override fun clearVideoSurface() {
@@ -861,27 +878,66 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
}
override fun getDeviceVolume(): Int {
return 0
// Short-lived cache without even-value quantization
val now = System.currentTimeMillis()
if (pendingVolume != null && (now - pendingVolumeSetAt) < 100) {
return pendingVolume!!
}
val session = castContext.sessionManager.currentCastSession
val vol = if (session != null && !session.isMute) {
(session.volume * 100).roundToInt().coerceIn(0, 100)
} else {
0
}
if (pendingVolume != null && vol == pendingVolume) {
pendingVolume = null
}
return vol
}
override fun isDeviceMuted(): Boolean {
return false
val session = castContext.sessionManager.currentCastSession
return session?.isMute ?: false
}
override fun setDeviceVolume(volume: Int) {
val session = castContext.sessionManager.currentCastSession
try {
val clamped = volume.coerceIn(0, 100)
val target = (clamped / 100.0).let { kotlin.math.round(it * 100.0) / 100.0 }
// Cache for 100ms to return in getDeviceVolume during async period
pendingVolume = clamped
pendingVolumeSetAt = System.currentTimeMillis()
session?.setVolume(target)
} catch (e: Exception) {
Log.e(tag, "Failed to set cast device volume", e)
}
}
override fun increaseDeviceVolume() {
val current = getDeviceVolume()
val next = current + 1
setDeviceVolume(next.coerceAtMost(100))
}
override fun decreaseDeviceVolume() {
val current = getDeviceVolume()
val next = current - 1
setDeviceVolume(next.coerceAtLeast(0))
}
override fun setDeviceMuted(muted: Boolean) {
val session = castContext.sessionManager.currentCastSession
try {
session?.isMute = muted
} catch (e: Exception) {
Log.e(tag, "Failed to set cast device mute state", e)
}
}
inner class StatusListener() : RemoteMediaClient.Callback(), SessionManagerListener<CastSession>, RemoteMediaClient.ProgressListener {
@@ -0,0 +1,36 @@
package com.audiobookshelf.app.player
import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.util.Log
import com.audiobookshelf.app.BuildConfig
import com.audiobookshelf.app.R
import com.bumptech.glide.Glide
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
private const val TAG = "CoverImageLoader"
/** Loads [uri] as a bitmap via Glide, falling back to the app icon if the load fails. */
suspend fun resolveUriAsBitmap(context: Context, uri: Uri): Bitmap? {
return withContext(Dispatchers.IO) {
try {
Glide.with(context)
.asBitmap()
.load(uri)
.placeholder(R.drawable.icon)
.error(R.drawable.icon)
.submit()
.get()
} catch (e: Exception) {
Log.e(TAG, "Failed to load cover bitmap for uri: $uri", e)
Glide.with(context)
.asBitmap()
.load(Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon))
.submit()
.get()
}
}
}
@@ -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
@@ -86,7 +85,8 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
}
override fun onSeekTo(pos: Long) {
playerNotificationService.seekPlayer(pos)
val currentTrackStartOffset = playerNotificationService.getCurrentTrackStartOffsetMs()
playerNotificationService.seekPlayer(currentTrackStartOffset + pos)
}
private fun onChangeSpeed() {
@@ -115,15 +115,16 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
if (mediaId.isNullOrEmpty()) {
libraryItemWrapper = playerNotificationService.mediaManager.getFirstItem()
} else if (mediaId.startsWith("ep_") || mediaId.startsWith("local_ep_")) { // Playing podcast episode
val libraryItemWithEpisode = playerNotificationService.mediaManager.getPodcastWithEpisodeByEpisodeId(mediaId)
libraryItemWrapper = libraryItemWithEpisode?.libraryItemWrapper
podcastEpisode = libraryItemWithEpisode?.episode
} else {
libraryItemWrapper = playerNotificationService.mediaManager.getById(mediaId)
if (libraryItemWrapper == null) {
Log.e(tag, "onPlayFromMediaId: Media item not found $mediaId")
val libraryItemWithEpisode = playerNotificationService.mediaManager.getPodcastWithEpisodeByEpisodeId(mediaId)
if (libraryItemWithEpisode != null) {
libraryItemWrapper = libraryItemWithEpisode.libraryItemWrapper
podcastEpisode = libraryItemWithEpisode.episode
} else {
libraryItemWrapper = playerNotificationService.mediaManager.getById(mediaId)
if (libraryItemWrapper == null) {
Log.e(tag, "onPlayFromMediaId: Media item not found $mediaId")
}
}
}
@@ -233,7 +234,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
playerNotificationService.jumpBackward()
}
KeyEvent.KEYCODE_MEDIA_STOP -> {
playerNotificationService.closePlayback()
playerNotificationService.pause()
}
else -> {
Log.d(tag, "KeyCode:${keyEvent.keyCode}")
@@ -49,10 +49,10 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
val libraryItemWrapper: LibraryItemWrapper?
var podcastEpisode: PodcastEpisode? = null
if (mediaId.startsWith("ep_") || mediaId.startsWith("local_ep_")) { // Playing podcast episode
val libraryItemWithEpisode = playerNotificationService.mediaManager.getPodcastWithEpisodeByEpisodeId(mediaId)
libraryItemWrapper = libraryItemWithEpisode?.libraryItemWrapper
podcastEpisode = libraryItemWithEpisode?.episode
val libraryItemWithEpisode = playerNotificationService.mediaManager.getPodcastWithEpisodeByEpisodeId(mediaId)
if (libraryItemWithEpisode != null) {
libraryItemWrapper = libraryItemWithEpisode.libraryItemWrapper
podcastEpisode = libraryItemWithEpisode.episode
} else {
libraryItemWrapper = playerNotificationService.mediaManager.getById(mediaId)
}
File diff suppressed because it is too large. Load diff
@@ -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)
@@ -33,6 +33,9 @@ class AbsAudioPlayer : Plugin() {
private var isCastAvailable:Boolean = false
// Track foreground state to avoid flooding WebView with events while backgrounded
private var isInForeground: Boolean = true
override fun load() {
mainActivity = (activity as MainActivity)
apiHandler = ApiHandler(mainActivity)
@@ -60,6 +63,8 @@ class AbsAudioPlayer : Plugin() {
}
override fun onMetadata(metadata: PlaybackMetadata) {
// Skip frequent metadata updates when app is backgrounded to prevent event queue buildup
if (!isInForeground) return
notifyListeners("onMetadata", JSObject(jacksonMapper.writeValueAsString(metadata)))
}
@@ -68,6 +73,8 @@ class AbsAudioPlayer : Plugin() {
}
override fun onSleepTimerSet(sleepTimeRemaining: Int, isAutoSleepTimer:Boolean) {
// Skip sleep timer updates when app is backgrounded to prevent event queue buildup
if (!isInForeground) return
val ret = JSObject()
ret.put("value", sleepTimeRemaining)
ret.put("isAuto", isAutoSleepTimer)
@@ -75,6 +82,8 @@ class AbsAudioPlayer : Plugin() {
}
override fun onLocalMediaProgressUpdate(localMediaProgress: LocalMediaProgress) {
// Skip progress updates when app is backgrounded to prevent event queue buildup
if (!isInForeground) return
notifyListeners("onLocalMediaProgressUpdate", JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
}
@@ -118,6 +127,32 @@ class AbsAudioPlayer : Plugin() {
notifyListeners(evtName, ret)
}
override fun handleOnDestroy() {
castManager?.detach()
super.handleOnDestroy()
}
override fun handleOnPause() {
super.handleOnPause()
isInForeground = false
}
override fun handleOnResume() {
super.handleOnResume()
isInForeground = true
// Send current state to UI after resume to sync up (with small delay to let WebView fully resume)
if (::playerNotificationService.isInitialized && playerNotificationService.currentPlaybackSession != null) {
Handler(Looper.getMainLooper()).postDelayed({
playerNotificationService.sendClientMetadata(PlayerState.READY)
playerNotificationService.sleepTimerManager.sendCurrentSleepTimerState()
playerNotificationService.mediaProgressSyncer.currentLocalMediaProgress?.let {
playerNotificationService.clientEventEmitter?.onLocalMediaProgressUpdate(it)
}
}, 100)
}
}
private fun initCastManager() {
val googleApi = GoogleApiAvailability.getInstance()
val statusCode = googleApi.isGooglePlayServicesAvailable(mainActivity)
@@ -180,7 +215,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 +234,9 @@ class AbsAudioPlayer : Plugin() {
return call.resolve(JSObject("{\"error\":\"Podcast episode not found\"}"))
}
}
if (!it.hasTracks(mainActivity, 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)}")
@@ -291,10 +330,10 @@ class AbsAudioPlayer : Plugin() {
@PluginMethod
fun seek(call: PluginCall) {
val time:Int = call.getInt("value", 0) ?: 0 // Value in seconds
val time: Double = call.getDouble("value", 0.0) ?: 0.0 // Value in seconds, fractional
Log.d(tag, "seek action to $time")
Handler(Looper.getMainLooper()).post {
playerNotificationService.seekPlayer(time * 1000L) // convert to ms
playerNotificationService.seekPlayer((time * 1000L).toLong())
call.resolve()
}
}
@@ -1,11 +1,14 @@
package com.audiobookshelf.app.plugins
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.audiobookshelf.app.MainActivity
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
@@ -20,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.cleanLocalLibraryItems(mainActivity)
DeviceManager.dbManager.cleanLogs()
}
@PluginMethod
@@ -111,13 +119,15 @@ class AbsDatabase : Plugin() {
@PluginMethod
fun setCurrentServerConnectionConfig(call:PluginCall) {
Log.d(tag, "setCurrentServerConnectionConfig ${call.data}")
Log.d(tag, "setCurrentServerConnectionConfig called")
val serverConfigPayload = jacksonMapper.readValue<ServerConnConfigPayload>(call.data.toString())
var serverConnectionConfig = DeviceManager.deviceData.serverConnectionConfigs.find { it.id == serverConfigPayload.id }
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
@@ -126,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!!)
@@ -134,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
@@ -160,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
@@ -172,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) {
@@ -218,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()) {
@@ -231,10 +296,16 @@ 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)
}
call.resolve()
}
}
} else {
AbsLogger.info("AbsDatabase", "syncLocalSessionsWithServer: No saved local playback sessions to send to server.")
call.resolve()
}
}
@@ -487,18 +558,25 @@ class AbsDatabase : Plugin() {
@PluginMethod
fun updateDeviceSettings(call:PluginCall) { // Returns device data
Log.d(tag, "updateDeviceSettings ${call.data}")
Log.d(tag, "updateDeviceSettings called")
val newDeviceSettings = jacksonMapper.readValue<DeviceSettings>(call.data.toString())
GlobalScope.launch(Dispatchers.IO) {
Handler(Looper.getMainLooper()).post {
DeviceManager.deviceData.deviceSettings = newDeviceSettings
DeviceManager.dbManager.saveDeviceData(DeviceManager.deviceData)
// Updates playback actions for media notification (handles media control seek locking setting)
if (mainActivity.isPlayerNotificationServiceInitialized()) {
mainActivity.foregroundService.setMediaSessionConnectorPlaybackActions()
}
call.resolve(JSObject(jacksonMapper.writeValueAsString(DeviceManager.deviceData)))
}
}
@PluginMethod
fun getMediaItemHistory(call:PluginCall) { // Returns device data
Log.d(tag, "getMediaItemHistory ${call.data}")
Log.d(tag, "getMediaItemHistory called")
val mediaId = call.getString("mediaId") ?: ""
GlobalScope.launch(Dispatchers.IO) {
@@ -1,17 +1,14 @@
package com.audiobookshelf.app.plugins
import android.app.DownloadManager
import android.content.Context
import android.os.Environment
import android.util.Log
import com.audiobookshelf.app.MainActivity
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.device.FolderScanner
import com.audiobookshelf.app.models.DownloadItem
import com.audiobookshelf.app.models.DownloadItemPart
import com.audiobookshelf.app.server.ApiHandler
import com.audiobookshelf.app.managers.DownloadItemManager
import com.audiobookshelf.app.services.DownloadServiceHost
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.JSObject
@@ -27,9 +24,7 @@ class AbsDownloader : Plugin() {
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
lateinit var mainActivity: MainActivity
lateinit var downloadManager: DownloadManager
lateinit var apiHandler: ApiHandler
lateinit var folderScanner: FolderScanner
lateinit var downloadItemManager: DownloadItemManager
private val clientEventEmitter = (object : DownloadItemManager.DownloadEventEmitter {
@@ -42,14 +37,44 @@ class AbsDownloader : Plugin() {
override fun onDownloadItemComplete(jsobj:JSObject) {
notifyListeners("onItemDownloadComplete", jsobj)
}
override fun onQueueChanged(hasWork: Boolean) {
notifyListeners("onQueueChanged", JSObject().put("hasWork", hasWork))
}
})
override fun load() {
mainActivity = (activity as MainActivity)
downloadManager = activity.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
folderScanner = FolderScanner(mainActivity)
apiHandler = ApiHandler(mainActivity)
downloadItemManager = DownloadItemManager(downloadManager, folderScanner, mainActivity, clientEventEmitter)
downloadItemManager = DownloadServiceHost.ensure(mainActivity)
DownloadServiceHost.attachBridge(mainActivity, clientEventEmitter)
}
override fun handleOnDestroy() {
DownloadServiceHost.detachBridge()
super.handleOnDestroy()
}
@PluginMethod
fun setDownloadNotificationStrings(call: PluginCall) {
DownloadServiceHost.setNotificationStrings(
mainActivity,
call.getString("preparing") ?: "Preparing downloads",
call.getString("downloadingFile") ?: "Downloading {0}",
call.getString("waitingForStorage") ?: "Waiting for available storage",
call.getString("downloads") ?: "Downloads",
call.getString("cancel") ?: "Cancel")
call.resolve()
}
/** Replays restored queue items when the frontend subscribes to download events. */
@PluginMethod(returnType = PluginMethod.RETURN_NONE)
override fun addListener(call: PluginCall) {
super.addListener(call)
if (call.getString("eventName") == "onDownloadItem" && ::downloadItemManager.isInitialized) {
downloadItemManager.downloadItemQueue.forEach { item ->
notifyListeners("onDownloadItem", JSObject(jacksonMapper.writeValueAsString(item)))
}
}
}
@PluginMethod
@@ -58,57 +83,60 @@ class AbsDownloader : Plugin() {
var episodeId = call.data.getString("episodeId").toString()
if (episodeId == "null") episodeId = ""
var localFolderId = call.data.getString("localFolderId", "").toString()
Log.d(tag, "Download library item $libraryItemId to folder $localFolderId / episode: $episodeId")
AbsLogger.info(tag, "Requested download for item $libraryItemId${if (episodeId.isEmpty()) "" else " / episode $episodeId"}")
val downloadId = if (episodeId.isEmpty()) libraryItemId else "$libraryItemId-$episodeId"
if (downloadItemManager.downloadItemQueue.find { it.id == downloadId } != null) {
Log.d(tag, "Download already started for this media entity $downloadId")
return call.resolve(JSObject("{\"error\":\"Download already started for this media entity\"}"))
}
apiHandler.getLibraryItemWithProgress(libraryItemId, episodeId) { libraryItem ->
if (libraryItem == null) {
call.resolve(JSObject("{\"error\":\"Server request failed\"}"))
} else {
Log.d(tag, "Got library item from server ${libraryItem.id}")
if (localFolderId == "") {
localFolderId = "internal-${libraryItem.mediaType}"
DownloadServiceHost.retryExisting(mainActivity, downloadId) { result ->
when (result) {
DownloadServiceHost.ExistingDownloadResult.RETRIED -> call.resolve()
DownloadServiceHost.ExistingDownloadResult.ACTIVE -> {
AbsLogger.info(tag, "Download already active for $downloadId")
call.resolve(JSObject("{\"error\":\"Download already started for this media entity\"}"))
}
var localFolder = DeviceManager.dbManager.getLocalFolder(localFolderId)
if (localFolder == null && localFolderId.startsWith("internal-")) {
Log.d(tag, "Creating new App Storage internal LocalFolder $localFolderId")
localFolder = LocalFolder(localFolderId, "Internal App Storage", "", "", "", "", "internal", libraryItem.mediaType)
DeviceManager.dbManager.saveLocalFolder(localFolder)
}
if (localFolder != null) {
if (episodeId.isNotEmpty() && libraryItem.mediaType != "podcast") {
Log.e(tag, "Library item is not a podcast but episode was requested")
call.resolve(JSObject("{\"error\":\"Invalid library item not a podcast\"}"))
} else if (episodeId.isNotEmpty()) {
val podcast = libraryItem.media as Podcast
val episode = podcast.episodes?.find { podcastEpisode ->
podcastEpisode.id == episodeId
}
if (episode == null) {
call.resolve(JSObject("{\"error\":\"Invalid podcast episode not found\"}"))
DownloadServiceHost.ExistingDownloadResult.SERVICE_START_FAILED ->
call.resolve(JSObject("{\"error\":\"Unable to start the Android download service\"}"))
DownloadServiceHost.ExistingDownloadResult.NOT_FOUND -> {
apiHandler.getLibraryItemWithProgress(libraryItemId, episodeId) { libraryItem ->
if (libraryItem == null) {
call.resolve(JSObject("{\"error\":\"Server request failed\"}"))
} else {
startLibraryItemDownload(libraryItem, localFolder, episode)
call.resolve()
AbsLogger.info(tag, "Preparing download for ${libraryItem.id}")
if (localFolderId == "") localFolderId = "internal-${libraryItem.mediaType}"
var localFolder = DeviceManager.dbManager.getLocalFolder(localFolderId)
if (localFolder == null && localFolderId.startsWith("internal-")) {
AbsLogger.info(tag, "Creating internal download folder $localFolderId")
localFolder = LocalFolder(localFolderId, "Internal App Storage", "", "", "", "internal", libraryItem.mediaType)
DeviceManager.dbManager.saveLocalFolder(localFolder)
}
if (localFolder == null) {
call.resolve(JSObject("{\"error\":\"Local Folder Not Found\"}"))
} else if (episodeId.isNotEmpty() && libraryItem.mediaType != "podcast") {
call.resolve(JSObject("{\"error\":\"Invalid library item not a podcast\"}"))
} else if (episodeId.isNotEmpty()) {
val podcast = libraryItem.media as Podcast
val episode = podcast.episodes?.find { it.id == episodeId }
if (episode == null) {
call.resolve(JSObject("{\"error\":\"Invalid podcast episode not found\"}"))
} else {
startLibraryItemDownload(libraryItem, localFolder, episode) { error -> resolveDownloadCall(call, error) }
}
} else {
startLibraryItemDownload(libraryItem, localFolder, null) { error -> resolveDownloadCall(call, error) }
}
}
} else {
startLibraryItemDownload(libraryItem, localFolder, null)
call.resolve()
}
} else {
call.resolve(JSObject("{\"error\":\"Local Folder Not Found\"}"))
}
}
}
}
private fun resolveDownloadCall(call: PluginCall, error: String?) {
if (error == null) call.resolve()
else call.resolve(JSObject().put("error", error))
}
// Item filenames could be the same if they are in sub-folders, this will make them unique
private fun getFilenameFromRelPath(relPath: String): String {
var cleanedRelPath = relPath.replace("\\", "_").replace("/", "_")
@@ -129,21 +157,31 @@ class AbsDownloader : Plugin() {
return newTitle
}
private fun startLibraryItemDownload(libraryItem: LibraryItem, localFolder: LocalFolder, episode:PodcastEpisode?) {
private fun startLibraryItemDownload(
libraryItem: LibraryItem,
localFolder: LocalFolder,
episode: PodcastEpisode?,
callback: (String?) -> Unit
) {
val isInternal = localFolder.id.startsWith("internal-")
val tempFolderPath = if (isInternal) "${mainActivity.filesDir}/downloads/${libraryItem.id}" else mainActivity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
val finalInternalFolderPath = "${mainActivity.filesDir}/downloads/${libraryItem.id}"
val tempFolderPath =
if (isInternal) {
"${mainActivity.filesDir}/download-staging/${libraryItem.id}"
} else {
"${mainActivity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS) ?: mainActivity.filesDir}/download-staging/${libraryItem.id}"
}
Log.d(tag, "downloadCacheDirectory=$tempFolderPath")
if (libraryItem.mediaType == "book") {
val bookTitle = cleanStringForFileSystem(libraryItem.media.metadata.title)
val bookAuthor = cleanStringForFileSystem(libraryItem.media.metadata.getAuthorDisplayName())
val tracks = libraryItem.media.getAudioTracks()
Log.d(tag, "Starting library item download with ${tracks.size} tracks")
AbsLogger.info(tag, "Queueing library item download with ${tracks.size} files")
val itemSubfolder = "$bookAuthor/$bookTitle"
val itemFolderPath = if (isInternal) "$tempFolderPath" else "${localFolder.absolutePath}/$itemSubfolder"
val itemFolderPath = if (isInternal) finalInternalFolderPath else "${localFolder.absolutePath}/$itemSubfolder"
val downloadItem = DownloadItem(libraryItem.id, libraryItem.id, null, libraryItem.userMediaProgress,DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, itemSubfolder, libraryItem.media, mutableListOf())
val book = libraryItem.media as Book
@@ -152,17 +190,7 @@ class AbsDownloader : Plugin() {
val serverPath = "/api/items/${libraryItem.id}/file/${ebookFile.ino}/download"
val destinationFilename = getFilenameFromRelPath(ebookFile.metadata?.relPath ?: "")
val finalDestinationFile = File("$itemFolderPath/$destinationFilename")
val destinationFile = File("$tempFolderPath/$destinationFilename")
if (destinationFile.exists()) {
Log.d(tag, "TEMP ebook file already exists, removing it from ${destinationFile.absolutePath}")
destinationFile.delete()
}
if (finalDestinationFile.exists()) {
Log.d(tag, "ebook file already exists, removing it from ${finalDestinationFile.absolutePath}")
finalDestinationFile.delete()
}
val destinationFile = File("$tempFolderPath/$destinationFilename.part")
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, fileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,ebookFile,null,null)
downloadItem.downloadItemParts.add(downloadItemPart)
@@ -178,20 +206,9 @@ class AbsDownloader : Plugin() {
val serverPath = "/api/items/${libraryItem.id}/file/${audioFileIno}/download"
val destinationFilename = getFilenameFromRelPath(audioTrack.relPath)
Log.d(tag, "Audio File Server Path $serverPath | AF RelPath ${audioTrack.relPath} | LocalFolder Path ${localFolder.absolutePath} | DestName $destinationFilename")
val finalDestinationFile = File("$itemFolderPath/$destinationFilename")
val destinationFile = File("$tempFolderPath/$destinationFilename")
if (destinationFile.exists()) {
Log.d(tag, "TEMP Audio file already exists, removing it from ${destinationFile.absolutePath}")
destinationFile.delete()
}
if (finalDestinationFile.exists()) {
Log.d(tag, "Audio file already exists, removing it from ${finalDestinationFile.absolutePath}")
finalDestinationFile.delete()
}
val destinationFile = File("$tempFolderPath/$destinationFilename.part")
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, fileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,null,audioTrack,null)
downloadItem.downloadItemParts.add(downloadItemPart)
@@ -205,25 +222,15 @@ class AbsDownloader : Plugin() {
val serverPath = "/api/items/${libraryItem.id}/cover"
val destinationFilename = "cover-${libraryItem.id}.jpg"
val destinationFile = File("$tempFolderPath/$destinationFilename")
val destinationFile = File("$tempFolderPath/$destinationFilename.part")
val finalDestinationFile = File("$itemFolderPath/$destinationFilename")
if (destinationFile.exists()) {
Log.d(tag, "TEMP Audio file already exists, removing it from ${destinationFile.absolutePath}")
destinationFile.delete()
}
if (finalDestinationFile.exists()) {
Log.d(tag, "Cover already exists, removing it from ${finalDestinationFile.absolutePath}")
finalDestinationFile.delete()
}
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, coverFileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,null,null,null)
downloadItem.downloadItemParts.add(downloadItemPart)
}
downloadItemManager.addDownloadItem(downloadItem)
}
DownloadServiceHost.enqueue(mainActivity, downloadItem, callback)
} else callback("No downloadable files found")
} else {
// Podcast episode download
val podcastTitle = cleanStringForFileSystem(libraryItem.media.metadata.title)
@@ -232,22 +239,16 @@ class AbsDownloader : Plugin() {
val audioFileIno = episode?.audioFile?.ino
val fileSize = audioTrack?.metadata?.size ?: 0
Log.d(tag, "Starting podcast episode download")
val itemFolderPath = if (isInternal) "$tempFolderPath" else "${localFolder.absolutePath}/$podcastTitle"
AbsLogger.info(tag, "Queueing podcast episode download")
val itemFolderPath = if (isInternal) finalInternalFolderPath else "${localFolder.absolutePath}/$podcastTitle"
val downloadItemId = "${libraryItem.id}-${episode?.id}"
val downloadItem = DownloadItem(downloadItemId, libraryItem.id, episode?.id, libraryItem.userMediaProgress, DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, podcastTitle, podcastTitle, libraryItem.media, mutableListOf())
var serverPath = "/api/items/${libraryItem.id}/file/${audioFileIno}/download"
var destinationFilename = getFilenameFromRelPath(audioTrack?.relPath ?: "")
Log.d(tag, "Audio File Server Path $serverPath | AF RelPath ${audioTrack?.relPath} | LocalFolder Path ${localFolder.absolutePath} | DestName $destinationFilename")
var destinationFile = File("$tempFolderPath/$destinationFilename")
var destinationFile = File("$tempFolderPath/$destinationFilename.part")
var finalDestinationFile = File("$itemFolderPath/$destinationFilename")
if (finalDestinationFile.exists()) {
Log.d(tag, "Audio file already exists, removing it from ${finalDestinationFile.absolutePath}")
finalDestinationFile.delete()
}
var downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,fileSize, destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,audioTrack,episode)
downloadItem.downloadItemParts.add(downloadItemPart)
@@ -258,18 +259,14 @@ class AbsDownloader : Plugin() {
serverPath = "/api/items/${libraryItem.id}/cover"
destinationFilename = "cover.jpg"
destinationFile = File("$tempFolderPath/$destinationFilename")
destinationFile = File("$tempFolderPath/$destinationFilename.part")
finalDestinationFile = File("$itemFolderPath/$destinationFilename")
if (finalDestinationFile.exists()) {
Log.d(tag, "Podcast cover already exists - not downloading cover again")
} else {
downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,coverFileSize,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,null,null)
downloadItem.downloadItemParts.add(downloadItemPart)
}
downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,coverFileSize,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,null,null)
downloadItem.downloadItemParts.add(downloadItemPart)
}
downloadItemManager.addDownloadItem(downloadItem)
DownloadServiceHost.enqueue(mainActivity, downloadItem, callback)
}
}
}
@@ -1,6 +1,7 @@
package com.audiobookshelf.app.plugins
import android.app.AlertDialog
import android.content.Context
import android.net.Uri
import android.os.Build
import android.util.Log
@@ -12,56 +13,73 @@ import com.anggrayudi.storage.callback.StorageAccessCallback
import com.anggrayudi.storage.file.*
import com.audiobookshelf.app.MainActivity
import com.audiobookshelf.app.data.LocalFolder
import com.audiobookshelf.app.data.LocalLibraryItem
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.device.FolderScanner
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.*
import com.getcapacitor.annotation.CapacitorPlugin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.File
@CapacitorPlugin(name = "AbsFileSystem")
class AbsFileSystem : Plugin() {
private val TAG = "AbsFileSystem"
private val tag = "AbsFileSystem"
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
private var jacksonMapper =
jacksonObjectMapper()
.enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
lateinit var mainActivity: MainActivity
override fun load() {
mainActivity = (activity as MainActivity)
mainActivity.storage.storageAccessCallback = object : StorageAccessCallback {
override fun onRootPathNotSelected(
requestCode: Int,
rootPath: String,
uri: Uri,
selectedStorageType: StorageType,
expectedStorageType: StorageType
) {
Log.d(TAG, "STORAGE ACCESS CALLBACK")
}
mainActivity.storage.storageAccessCallback =
object : StorageAccessCallback {
override fun onRootPathNotSelected(
requestCode: Int,
rootPath: String,
uri: Uri,
selectedStorageType: StorageType,
expectedStorageType: StorageType
) {
Log.d(TAG, "STORAGE ACCESS CALLBACK")
}
override fun onCanceledByUser(requestCode: Int) {
Log.d(TAG, "STORAGE ACCESS CALLBACK")
}
override fun onCanceledByUser(requestCode: Int) {
Log.d(TAG, "STORAGE ACCESS CALLBACK")
}
override fun onExpectedStorageNotSelected(requestCode: Int, selectedFolder: DocumentFile, selectedStorageType: StorageType, expectedBasePath: String, expectedStorageType: StorageType) {
Log.d(TAG, "STORAGE ACCESS CALLBACK")
}
override fun onExpectedStorageNotSelected(
requestCode: Int,
selectedFolder: DocumentFile,
selectedStorageType: StorageType,
expectedBasePath: String,
expectedStorageType: StorageType
) {
Log.d(TAG, "STORAGE ACCESS CALLBACK")
}
override fun onStoragePermissionDenied(requestCode: Int) {
Log.d(TAG, "STORAGE ACCESS CALLBACK")
}
override fun onStoragePermissionDenied(requestCode: Int) {
Log.d(TAG, "STORAGE ACCESS CALLBACK")
}
override fun onRootPathPermissionGranted(requestCode: Int, root: DocumentFile) {
Log.d(TAG, "STORAGE ACCESS CALLBACK")
}
}
override fun onRootPathPermissionGranted(requestCode: Int, root: DocumentFile) {
Log.d(TAG, "STORAGE ACCESS CALLBACK")
}
}
}
@PluginMethod
fun setFolderPickerStrings(call: PluginCall) {
mainActivity.getSharedPreferences(FOLDER_PICKER_PREFERENCES, Context.MODE_PRIVATE)
.edit()
.putString(KEY_WRITE_ACCESS_REQUIRED, call.getString("writeAccessRequired"))
.putString(KEY_ALLOW, call.getString("allow"))
.putString(KEY_CANCEL, call.getString("cancel"))
.putString(KEY_ACCESS_DENIED, call.getString("accessDenied"))
.putString(KEY_PERMISSION_DENIED, call.getString("permissionDenied"))
.apply()
call.resolve()
}
@PluginMethod
@@ -70,60 +88,74 @@ class AbsFileSystem : Plugin() {
val REQUEST_CODE_SELECT_FOLDER = 6
val REQUEST_CODE_SDCARD_ACCESS = 7
mainActivity.storage.folderPickerCallback = object : FolderPickerCallback {
override fun onFolderSelected(requestCode: Int, folder: DocumentFile) {
Log.d(TAG, "ON FOLDER SELECTED ${folder.uri} ${folder.name}")
val absolutePath = folder.getAbsolutePath(activity)
val storageType = folder.getStorageType(activity)
val simplePath = folder.getSimplePath(activity)
val basePath = folder.getBasePath(activity)
val folderId = android.util.Base64.encodeToString(folder.id.toByteArray(), android.util.Base64.DEFAULT)
mainActivity.storage.folderPickerCallback =
object : FolderPickerCallback {
override fun onFolderSelected(requestCode: Int, folder: DocumentFile) {
Log.d(TAG, "ON FOLDER SELECTED ${folder.uri} ${folder.name}")
val absolutePath = folder.getAbsolutePath(activity)
val storageType = folder.getStorageType(activity)
val basePath = folder.getBasePath(activity)
val folderId =
android.util.Base64.encodeToString(
folder.id.toByteArray(),
android.util.Base64.DEFAULT
)
val localFolder = LocalFolder(folderId, folder.name ?: "", folder.uri.toString(),basePath,absolutePath, simplePath, storageType.toString(), mediaType)
val localFolder =
LocalFolder(
folderId,
folder.name ?: "",
folder.uri.toString(),
basePath,
absolutePath,
storageType.toString(),
mediaType
)
DeviceManager.dbManager.saveLocalFolder(localFolder)
call.resolve(JSObject(jacksonMapper.writeValueAsString(localFolder)))
}
DeviceManager.dbManager.saveLocalFolder(localFolder)
call.resolve(JSObject(jacksonMapper.writeValueAsString(localFolder)))
}
override fun onStorageAccessDenied(
requestCode: Int,
folder: DocumentFile?,
storageType: StorageType,
storageId: String
) {
Log.e(tag, "Storage Access Denied ${folder?.getAbsolutePath(mainActivity)}")
override fun onStorageAccessDenied(
requestCode: Int,
folder: DocumentFile?,
storageType: StorageType,
storageId: String
) {
Log.e(tag, "Storage Access Denied ${folder?.getAbsolutePath(mainActivity)}")
val jsobj = JSObject()
if (requestCode == REQUEST_CODE_SELECT_FOLDER) {
val jsobj = JSObject()
if (requestCode == REQUEST_CODE_SELECT_FOLDER) {
val builder: AlertDialog.Builder = AlertDialog.Builder(mainActivity)
builder.setMessage(
"You have no write access to this storage, thus selecting this folder is useless." +
"\nWould you like to grant access to this folder?")
builder.setNegativeButton("Dont Allow") { _, _ ->
run {
jsobj.put("error", "User Canceled, Access Denied")
call.resolve(jsobj)
val builder: AlertDialog.Builder = AlertDialog.Builder(mainActivity)
builder.setMessage(folderPickerString(KEY_WRITE_ACCESS_REQUIRED, DEFAULT_WRITE_ACCESS_REQUIRED))
builder.setNegativeButton(folderPickerString(KEY_CANCEL, DEFAULT_CANCEL)) { _, _ ->
run {
jsobj.put("error", folderPickerString(KEY_ACCESS_DENIED, DEFAULT_ACCESS_DENIED))
call.resolve(jsobj)
}
}
builder.setPositiveButton(folderPickerString(KEY_ALLOW, DEFAULT_ALLOW)) { _, _ ->
mainActivity.storageHelper.requestStorageAccess(
REQUEST_CODE_SDCARD_ACCESS,
initialPath = FileFullPath(mainActivity, storageId, "")
)
}
builder.show()
} else {
Log.d(TAG, "STORAGE ACCESS DENIED $requestCode")
jsobj.put("error", folderPickerString(KEY_ACCESS_DENIED, DEFAULT_ACCESS_DENIED))
call.resolve(jsobj)
}
}
override fun onStoragePermissionDenied(requestCode: Int) {
Log.d(TAG, "STORAGE PERMISSION DENIED $requestCode")
val jsobj = JSObject()
jsobj.put("error", folderPickerString(KEY_PERMISSION_DENIED, DEFAULT_PERMISSION_DENIED))
call.resolve(jsobj)
}
}
}
builder.setPositiveButton("Allow.") { _, _ -> mainActivity.storageHelper.requestStorageAccess(REQUEST_CODE_SDCARD_ACCESS, initialPath = FileFullPath(mainActivity, storageId, "")) }
builder.show()
} else {
Log.d(TAG, "STORAGE ACCESS DENIED $requestCode")
jsobj.put("error", "Access Denied")
call.resolve(jsobj)
}
}
override fun onStoragePermissionDenied(requestCode: Int) {
Log.d(TAG, "STORAGE PERMISSION DENIED $requestCode")
val jsobj = JSObject()
jsobj.put("error", "Permission Denied")
call.resolve(jsobj)
}
}
mainActivity.storage.openFolderPicker(REQUEST_CODE_SELECT_FOLDER)
}
@@ -157,7 +189,7 @@ class AbsFileSystem : Plugin() {
val folderUrl = call.data.getString("folderUrl", "").toString()
Log.d(TAG, "Check Folder Permissions for $folderUrl")
val hasAccess = SimpleStorage.hasStorageAccess(context,folderUrl,true)
val hasAccess = SimpleStorage.hasStorageAccess(context, folderUrl, true)
val jsobj = JSObject()
jsobj.put("value", hasAccess)
@@ -171,26 +203,6 @@ class AbsFileSystem : Plugin() {
call.resolve(jsObject)
}
@PluginMethod
fun scanFolder(call: PluginCall) {
val folderId = call.data.getString("folderId", "").toString()
val forceAudioProbe = call.data.getBoolean("forceAudioProbe")
Log.d(TAG, "Scan Folder $folderId | Force Audio Probe $forceAudioProbe")
val folder: LocalFolder? = DeviceManager.dbManager.getLocalFolder(folderId)
folder?.let {
val folderScanner = FolderScanner(context)
val folderScanResult = folderScanner.scanForMediaItems(it, forceAudioProbe)
if (folderScanResult == null) {
Log.d(TAG, "NO Scan DATA")
return call.resolve(JSObject())
} else {
Log.d(TAG, "Scan DATA ${jacksonMapper.writeValueAsString(folderScanResult)}")
return call.resolve(JSObject(jacksonMapper.writeValueAsString(folderScanResult)))
}
} ?: call.resolve(JSObject())
}
@PluginMethod
fun removeFolder(call: PluginCall) {
val folderId = call.data.getString("folderId", "").toString()
@@ -205,27 +217,6 @@ class AbsFileSystem : Plugin() {
call.resolve()
}
@PluginMethod
fun scanLocalLibraryItem(call: PluginCall) {
val localLibraryItemId = call.data.getString("localLibraryItemId", "").toString()
val forceAudioProbe = call.data.getBoolean("forceAudioProbe")
Log.d(TAG, "Scan Local library item $localLibraryItemId | Force Audio Probe $forceAudioProbe")
GlobalScope.launch(Dispatchers.IO) {
val localLibraryItem: LocalLibraryItem? = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
localLibraryItem?.let {
val folderScanner = FolderScanner(context)
val scanResult = folderScanner.scanLocalLibraryItem(it, forceAudioProbe)
if (scanResult == null) {
Log.d(TAG, "NO Scan DATA")
call.resolve(JSObject())
} else {
Log.d(TAG, "Scan DATA ${jacksonMapper.writeValueAsString(scanResult)}")
call.resolve(JSObject(jacksonMapper.writeValueAsString(scanResult)))
}
} ?: call.resolve(JSObject())
}
}
@PluginMethod
fun deleteItem(call: PluginCall) {
val localLibraryItemId = call.data.getString("id", "").toString()
@@ -242,29 +233,41 @@ 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 ->
val folder = DeviceManager.dbManager.getLocalFolder(folderId)
folder?.absolutePath?.let { folderPath ->
val splitAbsolutePath = absolutePath.split("/")
val fullSubDir = splitAbsolutePath.subList(0, splitAbsolutePath.size - 1).joinToString("/")
val fullSubDir =
splitAbsolutePath.subList(0, splitAbsolutePath.size - 1).joinToString("/")
if (fullSubDir != folderPath) {
val subdirHasAnItem = DeviceManager.dbManager.getLocalLibraryItems().any { _localLibraryItem ->
if (_localLibraryItem.id == localLibraryItemId) {
false
} else {
_localLibraryItem.absolutePath.startsWith(fullSubDir)
}
}
val subdirHasAnItem =
DeviceManager.dbManager.getLocalLibraryItems().any { _localLibraryItem ->
if (_localLibraryItem.id == localLibraryItemId) {
false
} else {
_localLibraryItem.absolutePath.startsWith(fullSubDir)
}
}
subfolderPathToDelete = if (subdirHasAnItem) "" else fullSubDir
}
}
}
val docfile = DocumentFileCompat.fromUri(mainActivity, Uri.parse(contentUrl))
success = docfile?.delete() == true
if (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")
@@ -303,4 +306,23 @@ class AbsFileSystem : Plugin() {
call.resolve(JSObject("{\"success\":false}"))
}
}
private fun folderPickerString(key: String, defaultValue: String): String =
mainActivity.getSharedPreferences(FOLDER_PICKER_PREFERENCES, Context.MODE_PRIVATE)
.getString(key, defaultValue) ?: defaultValue
private companion object {
const val FOLDER_PICKER_PREFERENCES = "folder_picker"
const val KEY_WRITE_ACCESS_REQUIRED = "write_access_required"
const val KEY_ALLOW = "allow"
const val KEY_CANCEL = "cancel"
const val KEY_ACCESS_DENIED = "access_denied"
const val KEY_PERMISSION_DENIED = "permission_denied"
const val DEFAULT_WRITE_ACCESS_REQUIRED =
"You do not have write access to this folder. Would you like to grant access?"
const val DEFAULT_ALLOW = "Allow"
const val DEFAULT_CANCEL = "Cancel"
const val DEFAULT_ACCESS_DENIED = "Access denied"
const val DEFAULT_PERMISSION_DENIED = "Permission denied"
}
}
@@ -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()
}
}
@@ -1,6 +1,10 @@
package com.audiobookshelf.app.server
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
@@ -8,8 +12,12 @@ import com.audiobookshelf.app.media.MediaEventManager
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.databind.node.ObjectNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import com.getcapacitor.JSArray
@@ -27,11 +35,20 @@ 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>)
@JsonIgnoreProperties(ignoreUnknown = true)
data class LocalSessionSyncResult(val id:String, val success:Boolean, val progressSynced:Boolean?, val error:String?)
data class LocalSessionsSyncResponsePayload(val results:List<LocalSessionSyncResult>)
@@ -40,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) {
@@ -53,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")
@@ -82,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")
@@ -114,6 +160,236 @@ 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, fail the request ([refreshAuthTokens] owns clearing the session)
*
* @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) {
val serverConnectionConfigId = DeviceManager.serverConnectionConfigId
refreshAuthTokens(serverConnectionConfigId, httpClient) { result ->
if (result is RefreshResult.Success) {
retryOriginalRequest(originalRequest, result.accessToken, httpClient, callback)
} else {
callback(JSObject().put("error", "Authentication failed - login again"))
}
}
}
sealed interface RefreshResult {
data class Success(val accessToken: String) : RefreshResult
/** The server rejected the refresh token, so the session has already been cleared. */
data object Rejected : RefreshResult
/** The refresh could not be completed. Credentials are untouched and the caller may retry. */
data object Transient : RefreshResult
}
/** Refreshes tokens for a specific saved server, including downloads queued while another server is active. */
fun refreshAuthTokens(
serverConnectionConfigId: String,
httpClient: OkHttpClient? = null,
onResult: (RefreshResult) -> Unit
) {
val config = DeviceManager.getServerConnectionConfig(serverConnectionConfigId)
val refreshToken = secureStorage.getRefreshToken(serverConnectionConfigId)
if (config == null || refreshToken.isNullOrEmpty()) {
AbsLogger.error(tag, "No refresh token or server configuration for $serverConnectionConfigId")
handleRefreshRejected(serverConnectionConfigId)
onResult(RefreshResult.Rejected)
return
}
val request = try {
Request.Builder()
.url("${config.address}/auth/refresh")
.addHeader("x-refresh-token", refreshToken)
.addHeader("Content-Type", "application/json")
.post(EMPTY_REQUEST)
.build()
} catch (e: Exception) {
AbsLogger.error(tag, "Could not create refresh request for ${config.name}: ${e.message}")
onResult(RefreshResult.Transient)
return
}
(httpClient ?: defaultClient).newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
AbsLogger.error(tag, "Token refresh failed for ${config.name}: ${e.message}")
onResult(RefreshResult.Transient)
}
override fun onResponse(call: Call, response: Response) {
response.use {
if (!it.isSuccessful) {
AbsLogger.error(tag, "Token refresh returned ${it.code} for ${config.name}")
if (it.code != 401 && it.code != 403) {
onResult(RefreshResult.Transient)
return
}
handleRefreshRejected(serverConnectionConfigId)
onResult(RefreshResult.Rejected)
return
}
try {
val user = JSONObject(it.body!!.string()).optJSONObject("user")
val accessToken = user?.optString("accessToken").orEmpty()
if (accessToken.isEmpty()) {
AbsLogger.error(tag, "Refresh response had no access token for ${config.name}")
onResult(RefreshResult.Transient)
return
}
updateTokens(accessToken, user?.optString("refreshToken").orEmpty().ifEmpty { refreshToken }, serverConnectionConfigId)
onResult(RefreshResult.Success(accessToken))
} catch (e: Exception) {
AbsLogger.error(tag, "Could not parse refresh response for ${config.name}: ${e.message}")
onResult(RefreshResult.Transient)
}
}
}
})
}
/**
* Clears only the server that rejected the refresh token; queued downloads can target a non-active server.
*
* Only call this when the server explicitly rejected the refresh token. Transient failures should not log the user out
*/
private fun handleRefreshRejected(serverConnectionConfigId: String) {
// Must not throw: callers still have to report the refresh result to an in-flight request or download.
try {
secureStorage.removeRefreshToken(serverConnectionConfigId)
if (DeviceManager.serverConnectionConfigId != serverConnectionConfigId) return
DeviceManager.serverConnectionConfig = null
DeviceManager.deviceData.lastServerConnectionConfigId = null
DeviceManager.dbManager.saveDeviceData(DeviceManager.deviceData)
if (checkAbsDatabaseNotifyListenersInitted()) {
absDatabaseNotifyListeners(
"onTokenRefreshFailure",
JSObject().put("error", "Token refresh failed").put("serverConnectionConfigId", serverConnectionConfigId))
}
} catch (e: Exception) {
AbsLogger.error(tag, "Could not clear session for $serverConnectionConfigId: ${e.message}")
}
}
/**
* 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")
}
// The refreshed connection may be queued in the downloader rather than currently active.
DeviceManager.getServerConnectionConfig(serverConnectionConfigId)?.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 (DeviceManager.serverConnectionConfigId == serverConnectionConfigId && 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)
}
}
fun getCurrentUser(cb: (User?) -> Unit) {
getRequest("/api/me", null, null) {
if (it.has("error")) {
@@ -128,7 +404,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()
@@ -145,6 +421,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")) {
@@ -185,6 +478,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>()
@@ -192,8 +582,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)
}
}
@@ -230,10 +619,29 @@ class ApiHandler(var ctx:Context) {
}
}
fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: (Boolean, String?) -> Unit) {
val payload = JSObject(jacksonMapper.writeValueAsString(playbackSession))
private fun createPartialPlaybackSession(playbackSession: PlaybackSession): ObjectNode {
val json = jacksonMapper.createObjectNode()
json.put("id", playbackSession.id)
json.put("userId", playbackSession.userId)
json.put("libraryItemId", playbackSession.libraryItemId)
json.put("episodeId", playbackSession.episodeId)
json.put("mediaType", playbackSession.mediaType)
json.put("displayTitle", playbackSession.displayTitle)
json.put("displayAuthor", playbackSession.displayAuthor)
json.put("duration", playbackSession.duration)
json.put("playMethod", playbackSession.playMethod)
json.put("startedAt", playbackSession.startedAt)
json.put("updatedAt", playbackSession.updatedAt)
json.put("timeListening", playbackSession.timeListening)
json.put("currentTime", playbackSession.currentTime)
json.put("mediaPlayer", playbackSession.mediaPlayer)
return json
}
postRequest("/api/session/local", payload, null) {
fun sendLocalProgressSync(playbackSession:PlaybackSession, cb: (Boolean, String?) -> Unit) {
val partialSession = createPartialPlaybackSession(playbackSession)
partialSession.set<ObjectNode>("deviceInfo", jacksonMapper.valueToTree(playbackSession.deviceInfo))
postRequest("/api/session/local", JSObject(partialSession.toString()), null) {
if (!it.getString("error").isNullOrEmpty()) {
cb(false, it.getString("error"))
} else {
@@ -293,9 +701,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)
}
}
@@ -322,23 +730,35 @@ class ApiHandler(var ctx:Context) {
}
fun sendSyncLocalSessions(playbackSessions:List<PlaybackSession>, cb: (Boolean, String?) -> Unit) {
val payload = JSObject(jacksonMapper.writeValueAsString(LocalSessionsSyncRequestPayload(playbackSessions)))
@SuppressLint("HardwareIds")
val deviceId = Settings.Secure.getString(ctx.contentResolver, Settings.Secure.ANDROID_ID)
val deviceInfo = DeviceInfo(deviceId, Build.MANUFACTURER, Build.MODEL, Build.VERSION.SDK_INT, BuildConfig.VERSION_NAME)
postRequest("/api/session/local-all", payload, null) {
val json = jacksonMapper.createObjectNode();
json.putArray("sessions").addAll(playbackSessions.map(::createPartialPlaybackSession))
json.set<ObjectNode>("deviceInfo", jacksonMapper.valueToTree(deviceInfo))
AbsLogger.info("ApiHandler", "sendSyncLocalSessions: Sending ${playbackSessions.size} saved local playback sessions to server (${DeviceManager.serverConnectionConfigName})")
postRequest("/api/session/local-all", JSObject(json.toString()), null) {
if (!it.getString("error").isNullOrEmpty()) {
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}")
}
DeviceManager.dbManager.removePlaybackSession(session.id)
}
}
cb(true, null)
@@ -347,36 +767,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()
}
@@ -0,0 +1,102 @@
package com.audiobookshelf.app.services
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import com.audiobookshelf.app.R
import com.audiobookshelf.app.models.DownloadItemPart
/** Android-owned foreground lifecycle for transfers that must outlive the WebView and Activity. */
class DownloadService : Service() {
override fun onCreate() {
super.onCreate()
createChannel()
startForegroundWithType(DownloadServiceHost.notificationStrings(this).preparing)
DownloadServiceHost.attachService(this)
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_CANCEL -> DownloadServiceHost.cancelAll(this)
else -> {
startForegroundWithType(DownloadServiceHost.notificationStrings(this).preparing)
DownloadServiceHost.startWork(this)
}
}
return START_STICKY
}
override fun onDestroy() {
DownloadServiceHost.detachService(this)
super.onDestroy()
}
override fun onBind(intent: Intent?): IBinder? = null
fun onPartUpdate(part: DownloadItemPart) {
val strings = DownloadServiceHost.notificationStrings(this)
val text =
if (part.waitingForSpace) strings.waitingForStorage
else strings.downloadingFile.replace("{0}", part.filename)
val progress = part.progress.coerceIn(0L, 100L).toInt()
val notification = notification(text, progress, part.fileSize > 0L)
(getSystemService(NOTIFICATION_SERVICE) as NotificationManager).notify(NOTIFICATION_ID, notification)
}
fun onQueueChanged(hasWork: Boolean) {
if (!hasWork) {
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
}
private fun startForegroundWithType(text: String) {
val notification = notification(text)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} else {
startForeground(NOTIFICATION_ID, notification)
}
}
private fun notification(text: String, progress: Int = 0, determinate: Boolean = false): Notification {
val cancelIntent = PendingIntent.getService(
this, 1, Intent(this, DownloadService::class.java).setAction(ACTION_CANCEL), pendingIntentFlags())
return NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.icon)
.setContentTitle(DownloadServiceHost.notificationStrings(this).downloads)
.setContentText(text)
.setOnlyAlertOnce(true)
.setOngoing(true)
.setProgress(100, progress, !determinate)
.addAction(0, DownloadServiceHost.notificationStrings(this).cancel, cancelIntent)
.build()
}
private fun createChannel() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
manager.createNotificationChannel(
NotificationChannel(
CHANNEL_ID,
DownloadServiceHost.notificationStrings(this).downloads,
NotificationManager.IMPORTANCE_LOW))
}
private fun pendingIntentFlags(): Int = PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
companion object {
private const val CHANNEL_ID = "downloads"
private const val NOTIFICATION_ID = 11
private const val ACTION_CANCEL = "com.audiobookshelf.app.download.CANCEL"
fun intent(context: Context) = Intent(context, DownloadService::class.java)
}
}
@@ -0,0 +1,225 @@
package com.audiobookshelf.app.services
import android.content.Context
import androidx.core.content.ContextCompat
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.device.FolderScanner
import com.audiobookshelf.app.managers.DbManager
import com.audiobookshelf.app.managers.DownloadItemManager
import com.audiobookshelf.app.managers.IncompleteDownloadCleanup
import com.audiobookshelf.app.models.DownloadItem
import com.audiobookshelf.app.plugins.AbsLogger
import com.getcapacitor.JSObject
import java.util.Collections
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
/** Shared process owner used by the foreground service and the Capacitor bridge. */
object DownloadServiceHost {
enum class ExistingDownloadResult { NOT_FOUND, ACTIVE, RETRIED, SERVICE_START_FAILED }
data class NotificationStrings(
val preparing: String,
val downloadingFile: String,
val waitingForStorage: String,
val downloads: String,
val cancel: String
)
private var manager: DownloadItemManager? = null
private var bridgeEmitter: DownloadItemManager.DownloadEventEmitter = NoopEmitter
@Volatile private var service: DownloadService? = null
@Volatile private var bridgeReady = false
private val deferredCompletions = Collections.synchronizedList(mutableListOf<JSObject>())
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private var restoreJob: Job? = null
@Synchronized
fun ensure(context: Context): DownloadItemManager {
if (manager == null) {
val appContext = context.applicationContext
DbManager.initialize(appContext)
manager = DownloadItemManager(FolderScanner(appContext), appContext, ForwardingEmitter)
restoreJob = scope.launch {
try {
if (DeviceManager.dbManager.ensureValidDownloadQueue()) {
IncompleteDownloadCleanup.cleanupExpired(appContext)
manager!!.restoreQueue()
} else {
AbsLogger.error(TAG, "Skipping invalid download queue because it could not be cleared")
}
onRestoreComplete(appContext)
} catch (restoreError: Exception) {
AbsLogger.error(
TAG,
"Could not restore download queue (${restoreError.javaClass.simpleName}: ${restoreError.message})"
)
}
}
}
return manager!!
}
/** Attaches the frontend after restored queue items have been emitted. */
@Synchronized
fun attachBridge(context: Context, emitter: DownloadItemManager.DownloadEventEmitter) {
bridgeReady = false
bridgeEmitter = emitter
ensure(context).setEventEmitter(ForwardingEmitter)
bridgeReady = true
val completions = synchronized(deferredCompletions) {
deferredCompletions.toList().also { deferredCompletions.clear() }
}
completions.forEach(bridgeEmitter::onDownloadItemComplete)
}
@Synchronized
fun detachBridge() {
bridgeReady = false
bridgeEmitter = NoopEmitter
}
fun enqueue(context: Context, item: DownloadItem, callback: (String?) -> Unit) {
val queue = ensure(context)
scope.launch {
restoreJob?.join()
queue.addDownloadItem(item)
if (startService(context)) callback(null)
else callback("Unable to start the Android download service")
}
}
fun retryExisting(
context: Context,
downloadItemId: String,
callback: (ExistingDownloadResult) -> Unit
) {
val queue = ensure(context)
scope.launch {
restoreJob?.join()
val existing = queue.downloadItemQueue.find { it.id == downloadItemId }
if (existing == null) {
callback(ExistingDownloadResult.NOT_FOUND)
} else if (!queue.retryDownloadItem(downloadItemId)) {
callback(ExistingDownloadResult.ACTIVE)
} else if (startService(context)) {
callback(ExistingDownloadResult.RETRIED)
} else {
callback(ExistingDownloadResult.SERVICE_START_FAILED)
}
}
}
fun cancelAll(context: Context) {
val queue = ensure(context)
scope.launch {
restoreJob?.join()
queue.cancelAll()
}
}
fun setNotificationStrings(
context: Context,
preparing: String,
downloadingFile: String,
waitingForStorage: String,
downloads: String,
cancel: String
) {
context.getSharedPreferences(NOTIFICATION_PREFERENCES, Context.MODE_PRIVATE)
.edit()
.putString(KEY_PREPARING, preparing)
.putString(KEY_DOWNLOADING_FILE, downloadingFile)
.putString(KEY_WAITING_FOR_STORAGE, waitingForStorage)
.putString(KEY_DOWNLOADS, downloads)
.putString(KEY_CANCEL, cancel)
.apply()
}
fun notificationStrings(context: Context): NotificationStrings {
val preferences = context.getSharedPreferences(NOTIFICATION_PREFERENCES, Context.MODE_PRIVATE)
return NotificationStrings(
preferences.getString(KEY_PREPARING, DEFAULT_PREPARING) ?: DEFAULT_PREPARING,
preferences.getString(KEY_DOWNLOADING_FILE, DEFAULT_DOWNLOADING_FILE)
?: DEFAULT_DOWNLOADING_FILE,
preferences.getString(KEY_WAITING_FOR_STORAGE, DEFAULT_WAITING_FOR_STORAGE)
?: DEFAULT_WAITING_FOR_STORAGE,
preferences.getString(KEY_DOWNLOADS, DEFAULT_DOWNLOADS) ?: DEFAULT_DOWNLOADS,
preferences.getString(KEY_CANCEL, DEFAULT_CANCEL) ?: DEFAULT_CANCEL)
}
fun attachService(downloadService: DownloadService) {
synchronized(this) { service = downloadService }
startWork(downloadService)
}
@Synchronized
fun detachService(downloadService: DownloadService) {
if (service === downloadService) service = null
}
fun startWork(context: Context) {
val queue = ensure(context)
scope.launch {
restoreJob?.join()
queue.resumeWork()
}
}
private fun onRestoreComplete(context: Context) {
val attachedService = synchronized(this) { service }
if (attachedService != null) {
manager?.resumeWork()
} else if (bridgeReady && manager?.hasWork() == true) {
startService(context)
}
}
private fun startService(context: Context): Boolean {
return try {
ContextCompat.startForegroundService(context, DownloadService.intent(context))
true
} catch (e: RuntimeException) {
AbsLogger.error(TAG, "Could not start download foreground service: ${e.message}")
false
}
}
private object ForwardingEmitter : DownloadItemManager.DownloadEventEmitter {
override fun onDownloadItem(downloadItem: DownloadItem) { bridgeEmitter.onDownloadItem(downloadItem) }
override fun onDownloadItemPartUpdate(downloadItemPart: com.audiobookshelf.app.models.DownloadItemPart) {
if (bridgeReady) bridgeEmitter.onDownloadItemPartUpdate(downloadItemPart)
service?.onPartUpdate(downloadItemPart)
}
override fun onDownloadItemComplete(jsobj: JSObject) {
if (bridgeReady) bridgeEmitter.onDownloadItemComplete(jsobj) else deferredCompletions.add(jsobj)
}
override fun onQueueChanged(hasWork: Boolean) {
bridgeEmitter.onQueueChanged(hasWork)
service?.onQueueChanged(hasWork)
}
}
private object NoopEmitter : DownloadItemManager.DownloadEventEmitter {
override fun onDownloadItem(downloadItem: DownloadItem) = Unit
override fun onDownloadItemPartUpdate(downloadItemPart: com.audiobookshelf.app.models.DownloadItemPart) = Unit
override fun onDownloadItemComplete(jsobj: JSObject) = Unit
override fun onQueueChanged(hasWork: Boolean) = Unit
}
private const val NOTIFICATION_PREFERENCES = "download_notifications"
private const val KEY_PREPARING = "preparing"
private const val KEY_DOWNLOADING_FILE = "downloading_file"
private const val KEY_WAITING_FOR_STORAGE = "waiting_for_storage"
private const val KEY_DOWNLOADS = "downloads"
private const val KEY_CANCEL = "cancel"
private const val DEFAULT_PREPARING = "Preparing downloads"
private const val DEFAULT_DOWNLOADING_FILE = "Downloading {0}"
private const val DEFAULT_WAITING_FOR_STORAGE = "Waiting for available storage"
private const val DEFAULT_DOWNLOADS = "Downloads"
private const val DEFAULT_CANCEL = "Cancel"
private const val TAG = "DownloadServiceHost"
}
@@ -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.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
Loaded 100 of 400 files, more files were not shown because too many files have changed in this diff. Show more