Compare commits

...
2416 Commits
Author SHA1 Message Date
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
advplyr cdbbd56a58 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2023-06-25 14:29:09 -05:00
advplyr d6a1eba657 Fix:Android file provider to use application id 2023-06-25 14:29:02 -05:00
advplyr da899bfddd iOS version bump 0.9.65 2023-06-24 16:16:56 -05:00
advplyr b7b746fb2a Version bump 0.9.65-beta 2023-06-24 15:38:56 -05:00
advplyr c8053355df Add:Open/close RSS feed for library items 2023-06-24 14:45:25 -05:00
advplyr 1ddb6eca00 Fix:iOS local podcast episode progress #719 2023-06-24 10:37:56 -05:00
advplyr f42c624cba Update:iOS saving progress for offline ebooks 2023-06-24 09:41:14 -05:00
advplyr f8fbeef9b7 Fix:Add podcast form cutoff by open player #750 2023-06-21 17:23:51 -05:00
advplyr 46b82a81ce Update:iOS deleting downloaded item redirects to downloads page 2023-06-20 16:34:44 -05:00
advplyr 48342e5bd3 Fix:iOS offline reading and initialization 2023-06-20 16:29:56 -05:00
advplyr 147f89f870 Add:iOS offline ebooks 2023-06-19 17:42:15 -05:00
advplyr 8b39ba9c92 Fix dependency issue 2023-06-19 15:28:39 -05:00
advplyr 13d32dc1f7 Fix:Android internal file paths using file provider 2023-06-19 15:09:12 -05:00
advplyr 0d1312e497 Fix:Android auto filter out ebook only from downloaded tab 2023-06-19 14:03:29 -05:00
advplyr ff4f8324e7 Update:Syncing playback time when media item is open in player 2023-06-19 12:37:44 -05:00
advplyr b4bf10d409 Set epubjs version to 0.3.88 to work on ios, fix epub reader height 2023-06-18 17:11:22 -05:00
advplyr ae37861f7d Add:iOS support offline episode chapters #675 2023-06-18 15:34:56 -05:00
advplyr 1da03cdd65 Add:Podcast episode chapters for local eps #675 2023-06-18 15:12:07 -05:00
advplyr a7443d0992 Fix:Persist Total Track setting in audio player #663 2023-06-18 14:53:01 -05:00
advplyr 5a953464ab Update:Remove toast for mark as finished #718 2023-06-18 14:26:04 -05:00
advplyr 953bde5a42 Update:Add epub ereader settings and table of contents 2023-06-18 13:31:38 -05:00
advplyr 481d0d0285 Update:Show status bar when ereader toolbar is shown 2023-06-18 08:59:35 -05:00
advplyr 7b3a005ad6 Update:Ereader fullscreen #731 2023-06-17 17:34:08 -05:00
advplyr 4f9d341f69 Update:Delete local item/episode options in context menu #690, android: remove scan options for local server items & remove option 2023-06-17 12:49:24 -05:00
advplyr 9e7d1d1617 Add:Android setting to use mp3 index seeking #638 2023-06-17 11:27:44 -05:00
advplyr 4f03c2a35d Update:Cleanup chapters modal & increase width 2023-06-13 17:15:28 -05:00
advplyr db3c5396e9 Update:UI/UX improvements for playlists 2023-06-11 17:03:03 -05:00
advplyr 455af239a1 Update:Increasing touch size for seek bar cursor #713 2023-06-11 14:20:33 -05:00
advplyr d8bc26f5f8 Add:Ebook files table and supplementary ereader 2023-06-11 13:36:19 -05:00
advplyr 543ac209e4 Add:Progress for comics #738 2023-06-11 11:12:52 -05:00
advplyr c8b5cefeb5 Fix:Playlist items playing from server and not using local copy #734 2023-06-09 17:05:29 -05:00
advplyr f1411011e6 Update:Android include DeviceInfo with local playback session 2023-06-08 17:19:31 -05:00
advplyr f3964dda17 Update:Home screen server loading to not block local item access #708 2023-06-04 17:14:26 -05:00
advplyr d207e88e18 Update:Save last scroll position for item pages #717 2023-06-04 15:52:36 -05:00
advplyr 2643e98fc0 Fix:Back button closes ebook #709 2023-06-04 15:38:26 -05:00
advplyr 410648e316 Fix:Android auto filter out ebook only items 2023-06-04 15:12:56 -05:00
advplyr 373221703d Update:Android download to internal storage option #635 2023-06-04 14:59:55 -05:00
advplyr fbcb8620f9 Update:iOS stream and download URLs to use new API endpoints 2023-06-04 12:21:40 -05:00
advplyr a8ac4970cc Update:Android downloads to use new API endpoint 2023-06-04 11:34:34 -05:00
advplyr 195e33ae32 Update:EReaders to use new ebook API routes 2023-06-04 11:13:05 -05:00
advplyr 9acf695802 Remove file copy 2023-06-03 17:28:37 -05:00
advplyr e6aaccfc74 Update:Android support storing downloads to internal app storage #635 2023-06-03 17:24:32 -05:00
advplyr 87c74fe78b Fix:Playback speed not persisting #532 2023-06-02 16:57:17 -05:00
advplyr 7dc319c1bd Fix:Android app widget updating #658 2023-06-02 16:29:52 -05:00
advplyr c093548b71 Update:Scale chapter times with playback rate #726 2023-06-01 17:36:52 -05:00
advplyr b067a2d3d5 Fix:Libraries modal background colors #723 2023-06-01 17:22:28 -05:00
advplyr a9ae7bfdb8 Update:Fix epub3 background color, increase padding on top of ereader 2023-06-01 17:19:16 -05:00
advplyr 9dcf22614c Update:Android remove stop action when notification cancelled 2023-06-01 16:28:25 -05:00
advplyr c7564d439e Update:Android exoplayer version, target SDK to 33, remove stop button from notification 2023-05-31 17:43:54 -05:00
advplyr 157dc01673 Fix:Android close playback session on server when streaming 2023-05-27 17:20:01 -05:00
advplyr cce6e1d0ab Update:Android local media page to show ebook file 2023-05-25 17:59:24 -05:00
advplyr b935cb5cf1 Fix:Android remove local sessions after sync 2023-05-25 17:23:35 -05:00
advplyr 9ab37ac525 Add:Saving progress for PDF ebooks #712 2023-05-25 17:09:28 -05:00
advplyr 7031c895b5 iOS version bump 0.9.64 2023-05-21 18:41:18 -05:00
advplyr 7c739bf3bf Version bump 0.9.64-beta 2023-05-21 18:01:28 -05:00
advplyr facf56930e iOS add deviceId 2023-05-21 17:19:21 -05:00
advplyr 2c3dff3544 Add:EBook download and offline reading #187 #243 2023-05-21 15:02:49 -05:00
advplyr b1bf68b8cd Add:Series progress to series cards #410 2023-05-21 10:25:10 -05:00
advplyr 78aade7694 Fix:Android delete empty author folder when removing item #701 2023-05-20 18:13:19 -05:00
advplyr 83a4474375 Add:Podcast episode table sort options #376 2023-05-20 17:12:36 -05:00
advplyr 1b47c38efa Fix:Android sleep timer getting set to time left in media #659 2023-05-20 16:51:16 -05:00
advplyr 922d9f22ce Fix chapters button clickable when no chapters 2023-05-20 16:31:51 -05:00
advplyr d7feb52dd8 Fix:Discard progress for local and server media progress #646 2023-05-20 16:29:54 -05:00
advplyr fafaad8b5f Update:Play buttons on item pages show Play/Pause instead of disabled and showing Playing/Streaming #697 2023-05-20 14:49:55 -05:00
advplyr 928f33005a Update:Add play button to collections and playlist table #666 2023-05-20 14:22:53 -05:00
advplyr 0448d20583 Update:Android disable check for updated progress when playing 2023-05-20 13:22:04 -05:00
advplyr f9d408b33f Merge pull request #692 from HStep20/master
Added Auto Rewind on Sleep Timer setting to Android
2023-05-20 12:18:17 -05:00
advplyr 9386ae95a3 Fix set sleepTimerSessionId 2023-05-20 12:17:41 -05:00
advplyr c530e0f7e3 Remove test log 2023-05-20 12:00:19 -05:00
advplyr e39a5e1ba3 Fix saveSettings function call 2023-05-20 11:58:27 -05:00
advplyr cb9e317e74 Initialize auto rewind time and update description 2023-05-20 11:56:36 -05:00
advplyr 8061ac0ced Fix auto rewind when check resetting sleep timer 2023-05-20 11:41:39 -05:00
advplyr daff8355d7 Merge branch 'master' into HStep20/master 2023-05-20 10:18:16 -05:00
advplyr 231289e9ac Update:Add podcast type, episode type, episode number, season and support html episode description #677 2023-05-19 17:57:36 -05:00
advplyr 87df022af0 Add:Narrators in search results 2023-05-19 17:18:31 -05:00
advplyr 07fdac9746 Update:Podcast episodes on home page dont show an Episode number if none is set 2023-05-19 17:03:54 -05:00
advplyr 0d482aaa42 Update:Android remove auto-open most recent listened item 2023-05-19 16:59:10 -05:00
advplyr d48d1fdde3 Update:Audio player play method color for visibility #706 2023-05-17 17:59:47 -05:00
hstephens 65b69cc020 fixed if statement syntax 2023-05-12 20:19:18 -06:00
hstephens a3794a6a6a added check before setting state 2023-04-29 17:20:04 -06:00
hstephens 695ab84940 changed behavior for first timer set 2023-04-29 16:48:43 -06:00
hstephens 5399ec67d7 Added Auto Rewind on Sleep Timer setting 2023-04-29 15:38:51 -06:00
advplyr d9a6ec65df Add deviceId to DeviceInfo for Android 2023-04-22 16:48:22 -05:00
advplyr e842de78dd Fix:Audiobook tracks table for local only books 2023-03-28 17:30:54 -05:00
advplyr 260363bf4f Fix:Android media widget initialize DB 2023-03-28 17:30:20 -05:00
advplyr 6d7dffd4de Merge pull request #649 from maltejur/largeHeap
Add `android:largeHeap="true"` to `AndroidManifest.xml`
2023-03-27 17:02:09 -05:00
advplyr ef1d8a8eb4 Update:iOS HLS stream buffer set to 50s 2023-03-27 16:54:19 -05:00
Malte Jürgens cdd7a1a636 add android:largeHeap to AndroidManifest.xml 2023-03-27 21:00:52 +02:00
advplyr 6584cd1522 Fix:Android auto sleep timer window #648 2023-03-26 16:43:24 -05:00
advplyr d01b602306 Update epub reader media progress update 2023-03-26 13:50:21 -05:00
advplyr c90f2c136a Add:Support epub progress saving 2023-03-25 17:40:46 -05:00
advplyr 15ccc7192e Fix:Android sleep timer reset volume when shake #626 2023-03-25 16:51:24 -05:00
advplyr e4345a4438 Update:Raw cover on library item page, Android download raw cover #634 2023-03-19 17:13:33 -05:00
advplyr 3f4d564bad Fix:Add try/catch to initCastManager #480 2023-03-10 16:51:45 -06:00
advplyr 3311f66da8 Update readme screenshot 2023-03-10 16:13:04 -06:00
advplyr e711ada0b3 Fix:Center play button when locking ui #624 2023-03-10 15:37:42 -06:00
advplyr 840979d635 Fix play at start time 2023-03-06 16:59:49 -06:00
advplyr 53c9109534 iOS fix local episode duration and playback session title, version bump 0.9.63 2023-03-06 16:47:38 -06:00
advplyr 87ca6018d3 Version bump 0.9.63 2023-03-06 16:11:20 -06:00
advplyr c590087578 Fix description line clamp show more 2023-03-06 15:51:11 -06:00
advplyr 3963ffa0e2 iOS implement start time override 2023-03-06 15:46:07 -06:00
advplyr eaa0c9914a Merge pull request #571 from lkiesow/center-title
Center title on book details
2023-03-06 15:30:15 -06:00
advplyr d9b0b8c33d Add chapters & tracks table. Clamp description to 4 lines. Move size to more info modal 2023-03-06 15:27:12 -06:00
advplyr fb4e7e6b55 Merge branch 'master' into center-title 2023-03-06 13:48:30 -06:00
advplyr 78a063a669 Fix:iOS download books with CD subfolders #620 2023-03-06 13:27:57 -06:00
advplyr cb5d32bbb9 Fix epub reader 2023-03-06 13:09:09 -06:00
advplyr f594b734c0 Adding more top padding to ereader 2023-03-06 11:40:00 -06:00
advplyr bf62b82037 Fix ereader padding and swiping 2023-03-06 10:58:08 -06:00
advplyr 39fbb0902e Removing padding above cover on item page 2023-03-05 16:47:04 -06:00
advplyr 7818500f0f Fix:Podcast latest and book authors page refresh/redirect on library change 2023-03-05 11:12:17 -06:00
advplyr 1ea984b915 Update iOS disable touch callout and user select 2023-03-04 17:15:48 -06:00
advplyr f28405081a Update:Audio player controls max width 2023-03-04 16:42:06 -06:00
advplyr 317f00e1b8 Update:Audio player drag cursor to seek #457 2023-03-04 16:26:37 -06:00
advplyr f8ce0b8355 Fix collapse sub series show series name 2023-03-04 14:15:08 -06:00
advplyr 9fd6ff9fd2 Fix sub series navigation not refreshing entities 2023-03-04 14:12:16 -06:00
advplyr 3931020c83 Update:Audio player cover rounded corners and rounded track 2023-03-04 13:59:37 -06:00
advplyr 8bdb6b0b66 Update:Fullscreen audio player track padding 2023-03-04 13:49:58 -06:00
advplyr 33bdee66a3 Add:Collapse series and collapse sub-series settings #99 2023-03-03 17:05:23 -06:00
advplyr a81f50878e Add:Authors page #618 2023-03-02 17:59:11 -06:00
advplyr 322f7d75dd Fix:Show episode title on home page shelves #616 2023-03-02 17:17:49 -06:00
advplyr 2f34da4e99 Fix:Android playback rate set on initial load #532 2023-03-01 17:47:07 -06:00
advplyr 5a9e30b08c Update:Show raw cover image in audio player #614 2023-02-27 16:30:18 -06:00
advplyr 04e233816d Android fix for playing items with no cover in development 2023-02-26 17:21:03 -06:00
advplyr 3ad1042b13 Fix:Android bluetooth controls for fast forward/rewind 2023-02-26 15:56:39 -06:00
advplyr 4fb9201823 Update:Additional padding in local library items folder page #592 2023-02-26 14:57:04 -06:00
advplyr 02089e1489 Update:Increased padding on modals 2023-02-26 14:41:18 -06:00
advplyr da442f4a17 Fix:Clicking author card on home page filter/sort #607 2023-02-26 14:26:41 -06:00
advplyr a0f8e6fb93 Fix:Initializing haptic feedback setting on app launch #603 2023-02-26 12:54:56 -06:00
advplyr 20cf5a6132 Merge pull request #597 from KeyboardHammer/widget
Android Home Screen Widget
2023-02-26 11:30:16 -06:00
advplyr f5ca30620f Update android widget sizing, hide buttons on app close, and initialize when added 2023-02-26 11:26:34 -06:00
Lars Kiesow 72ec9d81b1 Singular & Plural 2023-02-26 13:19:23 +01:00
Lars Kiesow 29568412e6 four columns 2023-02-26 13:19:23 +01:00
Lars Kiesow f94140ec6b expand 2023-02-26 13:19:23 +01:00
Lars Kiesow 51062e7403 … 2023-02-26 13:19:22 +01:00
Lars Kiesow 0942bd2548 move title down 2023-02-26 13:19:22 +01:00
Lars Kiesow 5939fee66f one-big-table 2023-02-26 13:19:22 +01:00
Lars Kiesow 68560fd5f5 move series down 2023-02-26 13:19:22 +01:00
Lars Kiesow a38b826898 Center title on book details
This patch centers the title, author and series metadata on the book
details page below the cover. This makes this view look similar to the
places where the metadata are also places in the center.
2023-02-26 13:19:22 +01:00
advplyr 0158ccc4c8 Hide widget action buttons when app closes 2023-02-25 18:13:03 -06:00
Alex a5ba6c64c9 redo widget layout, hide buttons on force close 2023-02-25 18:02:20 -06:00
advplyr d67454db9a Fix:Update bookshelf on screen orientation change #602 2023-02-25 16:53:48 -06:00
advplyr fb35d8b1a0 Add:Podcast episode page and update episode lists to not show description #588 2023-02-25 16:33:06 -06:00
advplyr 960f75bfba Fix:Android cancel sleep timer resets volume #605 2023-02-25 09:28:22 -06:00
advplyr 83027a5787 Fix:Android local podcasts creating playback sessions with bad duration and displayTitle 2023-02-24 17:42:34 -06:00
Alex bc6ef96465 Android Home Screen Widget 2023-02-21 22:20:49 -06:00
advplyr ff77dbf3f8 iOS update pods 2023-02-19 16:53:43 -06:00
advplyr 3f78fb7a14 iOS version bump 0.9.62 2023-02-19 15:40:24 -06:00
advplyr 152904b7c7 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2023-02-19 14:51:41 -06:00
advplyr 9214fecc86 Version bump 0.9.62-beta 2023-02-19 14:51:34 -06:00
advplyr 049397f7ab Merge pull request #565 from jramer/master
fix: [iOS] seek pauses playback
2023-02-19 14:23:45 -06:00
advplyr 0f42a411a9 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2023-02-19 14:17:28 -06:00
advplyr 4575c61118 Update:Android foreground service updates 2023-02-19 14:17:22 -06:00
advplyr 7575a4f883 Merge branch 'master' into jramer/master 2023-02-19 11:47:20 -06:00
advplyr 8d4c322bce Fix:iOS fallback to transcoding #553 2023-02-19 11:47:05 -06:00
advplyr 28989f536a Set up for android widget 2023-02-19 11:14:30 -06:00
Joakim Ramer 3318f3501c fix: seek when changing track 2023-02-19 17:42:45 +01:00
Joakim Ramer c9ad0c7897 Merge branch 'master' of github.com:jramer/audiobookshelf-app 2023-02-19 15:04:46 +01:00
advplyr 018a988124 Merge branch 'master' into jramer/master 2023-02-18 10:20:01 -06:00
advplyr 750c370692 Fix:Android download file with capital M4B extension 2023-02-18 10:08:10 -06:00
advplyr 08ee08c698 Update:Show loading indicator when first connecting to server 2023-02-17 17:47:46 -06:00
advplyr d899fd4d89 Android & iOS only open most recent session on first load 2023-02-17 17:14:49 -06:00
advplyr aab44d8cee Fix:Alignment of recent sessions list #587 2023-02-17 16:51:11 -06:00
advplyr 2c2fac6e34 Update:Android download 3 audio files at a time #567 2023-02-17 16:24:08 -06:00
advplyr 47ccc54749 Fix:Reorder local audio files overlapping save button 2023-02-17 16:07:37 -06:00
advplyr 05d7d0d0d8 Fix:Android re-scan folder removing/unlinking downloads 2023-02-17 15:49:53 -06:00
advplyr 210fb7b88f Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2023-02-17 13:09:42 -06:00
advplyr 4eeb7f24d8 Fix:iOS downloads #537 2023-02-17 13:07:35 -06:00
Joakim Ramer 9496ee1d97 Merge branch 'master' of github.com:advplyr/audiobookshelf-app 2023-02-16 23:06:32 +01:00
Joakim Ramer f4b05a3236 Clarify state 2023-02-16 23:00:33 +01:00
Joakim Ramer b73763a9ec fix: Fixes streaming issues 2023-02-16 22:53:37 +01:00
advplyr e3aad8c44f Merge pull request #582 from lkiesow/build-on-workflow-change
Build APK on Workflow Change
2023-02-14 06:27:51 -06:00
Lars Kiesow 1e7c298aa2 Build APK on Workflow Change
This patch removes a few restriction about when the Android build
workflows run. In particular, we want to run workflows…

- If the workflows themselves change because that could mean that the
  output changes.
- If the deployment page template changes because that would mean that
  the published page needs to be updated.

We could still exclude a few specific files if we wanted to, but they
should rarely change and it's probably not worth the effort of keeping
those lists updated.
2023-02-14 13:16:02 +01:00
advplyr b9652c7cd8 Merge pull request #581 from lkiesow/test-page
Publish Android Test Page
2023-02-13 17:31:43 -06:00
advplyr 04f00d2994 Update .github/workflows/deploy-apk.yml 2023-02-13 17:31:13 -06:00
advplyr 3da6728f4c Add ignore paths to build-apk workflow 2023-02-13 17:27:57 -06:00
Lars Kiesow 5704ca2ae8 Publish Android Test Page
This page automatically builds the Android app from the latest commit on
the branch master, generates a test page and publishes it to GitHub
Pages as a nice way for non-developers to get the latest app version for
testing without having to build it themselves.

This page might need the pages source set to GitHub Actions at
Settings → Pages → Build and deployment → Source.
2023-02-13 21:42:54 +01:00
advplyr 6cd04e7098 Remove Gentium Book Basic font 2023-02-12 12:16:38 -06:00
advplyr 290c3560d2 Merge pull request #574 from lkiesow/easy-continue-listening
Easy way to play current audiobook
2023-02-12 08:55:15 -06:00
advplyr 40bda3d48b Auto play podcast episode & check for local copy 2023-02-12 08:47:38 -06:00
advplyr 63a838dc9d Merge pull request #577 from lkiesow/auto-build
Automatically test and build app
2023-02-12 08:03:43 -06:00
Lars Kiesow 127bf7d4e0 Automatic test and build app
This patch adds a GitHub Actions workflow which will automatically run
through all steps to build the Android version of the app. This is an
easy way of catching major build errors in pull requests or pushed
commits.

Additionally, this uploads the Android APK as build artifact. This
allows anyone to easily download any build for a while. That will make
it much easier for non-developers to participate in testing a new
version or a specific test since the no longer have to wait for a new
release.

The build artifacts are a bit tricky to find, but they are easy to point
out if anyone asks.
2023-02-12 01:21:16 +01:00
advplyr 9af4947232 Merge pull request #576 from lkiesow/account-settings
Account Setting Information
2023-02-11 17:29:25 -06:00
advplyr 3233a2a078 Merge pull request #575 from lkiesow/typeerror
Fix TypeError on Logout
2023-02-11 17:28:17 -06:00
Lars Kiesow 28ede56c7c Account Setting Information
The account setting list the connection name which is the server address
directly followed by the username. This is then followed by a field
repeating the username again.

This duplication of information is nor really helpful, which is why this
patch replaces this with two fields containing just the two information:
host address and username.
2023-02-11 23:53:01 +01:00
Lars Kiesow 5f6c1f57b0 Fix TypeError on Logout
Clicking on “logout”, you get the following TypeError:

```
TypeError: state.user is null
    getUserBookmarksForItem user.js:38
    bookmarks AudioPlayerContainer.vue:45
    VueJS 4
    render AudioPlayerContainer.vue:11
    VueJS 13
    logout user.js:115
    wrappedMutationHandler vuex.esm.js:844
    commitIterator vuex.esm.js:466
    commit vuex.esm.js:465
    _withCommit vuex.esm.js:624
    commit vuex.esm.js:464
    boundCommit vuex.esm.js:409
    _callee$ account.vue:67
    tryCatch pdf_viewer.js:1329
    invoke pdf_viewer.js:1502
    defineIteratorMethods pdf_viewer.js:1372
    Babel 10
    logout account.vue:56
    VueJS 4
    click Btn.vue:65
    VueJS 33
client.js:103
```

This patch fixes the problem by checking for `null` values.
2023-02-11 23:43:20 +01:00
Lars Kiesow ec704bf501 Easy way to play current audiobook
This patch implements a generic way of re-opening the player for the
book you last listened to. This is especially handy when first opening
the app and you can restart playback with a single click.

The player is opened only if no other book is already open in the
player. It will not overwrite or replace playbacks or player already in
progress.

The player is opened paused. No automatic playback since that could be
really annoying.

This closes #494
2023-02-11 23:14:41 +01:00
advplyr 5e0c740f02 Merge pull request #572 from lkiesow/hyphen-auto
Improve word wrapping
2023-02-10 17:48:44 -06:00
Lars Kiesow a185c3abe9 Improve word wrapping
This patch improves word wrapping for the description text in book
details by justifying the text block and allowing for words to be
wrapped by automatically inserting hyphens if necessary.

This causes the description box to look far less ragged on the right
edge which I think helps the overall cleanliness of the look.

Unfortunately the app's/browser's aren't as good as e.g. the TeX
algorithm for hyphening and there are JavaScript libraries which seem to
reimplement that. But this is already a significant step up and far less
work for something which is not the main focus of this app.
2023-02-10 23:27:33 +01:00
advplyr e8c6fc635f Merge pull request #569 from lkiesow/blurred-cover-bg
Blur Cover Image Background
2023-02-09 18:14:01 -06:00
advplyr f362db3855 Add prop to BookCover to not show a background 2023-02-09 18:13:30 -06:00
advplyr 9ae584665e Merge pull request #568 from emmertex/master
Disable ATS for iOS
2023-02-09 17:51:20 -06:00
Lars Kiesow 1aa852c3dc Blur Cover Image Background
This patch adds a heavily blurred version of the cover image as
background to the cover image area in the book details.

This looks especially nice on larger devices like tablets.
2023-02-09 23:57:59 +01:00
Joakim Ramer ef46ad7a93 fix: removes some warnings 2023-02-09 02:40:21 +01:00
Joakim Ramer cbd74d08e1 fix: seek pauses playback 2023-02-09 02:38:56 +01:00
advplyr 851d0357a0 Merge pull request #564 from lkiesow/lockfile-warning
Re-generate lock file
2023-02-08 17:09:48 -06:00
advplyr fb5c23d190 Merge pull request #563 from lkiesow/menu-padding
Increase padding for menus
2023-02-08 16:57:29 -06:00
advplyr 1743d0e13f Merge pull request #562 from lkiesow/book-details-menu-items
Icons for book details menu
2023-02-08 16:56:00 -06:00
advplyr 25af0c4170 Merge pull request #561 from lkiesow/simplify-progress
Simplify progress (re)sets
2023-02-08 16:53:24 -06:00
Lars Kiesow 4bcb77250d Re-generate lock file
Running `npm ci` you always get a warning about an old lock-file format
being used:

```
npm WARN old lockfile
npm WARN old lockfile The package-lock.json file was created with an old version of npm,
npm WARN old lockfile so supplemental metadata must be fetched from the registry.
npm WARN old lockfile
npm WARN old lockfile This is a one-time fix-up, please be patient...
npm WARN old lockfile
```

This patch simply re-generates the file by removing the old one and
running`npm install`. That gets rid of the warning.
2023-02-08 23:22:54 +01:00
Lars Kiesow 6114e03cb7 Increase passing for menus
This patch slightly increases the padding for menu dialogs. No real
reason for this except that I like the look of this better ;-)
2023-02-08 23:19:37 +01:00
Lars Kiesow 9866a787bd Icons for book details menu
This patch adds icons similar to those used in other app menus to the
book details menu.
2023-02-08 23:09:52 +01:00
Andrew Frahn 20e64523b2 Disable ATS for iOS 2023-02-09 08:38:50 +11:00
Lars Kiesow 50a53ec0d1 Simplify progress (re)sets
This patch is a follow up to the discussion from #559 about potentially
simplifying progress interactions.

This patch essentially reduces the interactions to two simple options
which allow users to always marks books as finished or discard the
progress and thus mark them as not yet started with a single
interaction:

- If users have already listed to the book, regardless of the state of
  the progress (finished, half way through, …), they get an option to
  discard the progress. This replaces the additional “mark as not
  finished” option but it should still be clear to users what happens.

- If a book is not yet finished, regardless of the state of the progress
  (not yet started, half way through), users get a “mark as finished”
  option which sets the progress to 100%.

This is hopefully a bit simpler than what we have now but also doesn't
require a user to figure out that you need to first mark a book as
finished to be able to discard the progress.
2023-02-08 22:27:56 +01:00
advplyr 40b2ff5a97 Merge pull request #560 from lkiesow/account-version-github
App info in account settings
2023-02-07 17:54:58 -06:00
Lars Kiesow 48cc000b95 App info in account settings
This patch updates the account settings view, trying to make it look a
bit cleaner by:

- Making the logout button look more like a button. The full width
  looked odd on large devices or in landscape mode.
- Removing the version number. It is already listed in the main menu
  anyway.
- Moving the hint about reporting bugs and the GitHub link to the
  bottom.

The hint is still a bit weird in this view. Maybe long-term, we want to
move all these information about the app (version, issues, …) to a
separate view. But that's something for another day.
2023-02-07 23:55:21 +01:00
advplyr 75aaf4d784 Merge pull request #559 from lkiesow/discard-progress-menu-option
Move Discard Progress to Menu
2023-02-07 16:45:04 -06:00
advplyr 23220ecae6 Fix podcast page returning item progress 2023-02-07 16:44:23 -06:00
advplyr 94b4dda94f Merge pull request #554 from KeyboardHammer/aaSpeed
Add playback speed adjustment for Android Auto
2023-02-07 16:38:13 -06:00
advplyr 86c5d1a3e9 Refresh media session connector actions when changing speed on app. Update rounding values for Android Auto speed icons 2023-02-07 16:10:02 -06:00
Lars Kiesow d7be01935f Move Discard Progress to Menu
This patch moves the discard progress functionality from the tiny icon
at the top right corner of the progress display to the options menu.

The reasoning for this is that resetting progress is an option which is
unlikely to be used very often and therefor does not need to be on the
main screen. In addition to that, the menu already holds the related
options to mark the book as finished and to discard the progress of a
finished book.

Finally, this removes the tiny icon which does not really match the rest
of the user interface of the app.
2023-02-07 22:42:06 +01:00
Alex b2b647e2f7 fix float conversion, rounding for icons 2023-02-06 20:41:35 -06:00
advplyr 405cd21c32 Emit playback speed change event when Android Auto changes speed 2023-02-06 17:20:39 -06:00
advplyr 17a894f5ef Update Android Auto playback speed string 2023-02-06 16:34:12 -06:00
Alex 23c90f4118 Add playback spped adjustment for Android Auto 2023-02-05 19:38:12 -06:00
advplyr f215efdcd0 Update:Syncing local sessions rewrite to support offline sessions #381 2023-02-05 16:54:46 -06:00
advplyr 2f243787ce Add:Android sleep timer setting to disable vibrate on reset #552 2023-02-04 17:52:31 -06:00
advplyr 39909a398e Add:Android sleep timer setting to disable audio fade out #320 2023-02-04 17:39:41 -06:00
advplyr 5e98a4ff2f Update:Android cancelling sleep timer when it was auto-enabled will also disable the auto sleep timer setting with alert #545 2023-02-04 16:57:55 -06:00
advplyr 8710775872 Fix:Android download freezing UI when a lot of files are downloaded #550 2023-02-04 15:52:06 -06:00
advplyr 99d60eaf9a Update:Adjust current playback time with playback speed #549 2023-02-04 13:50:54 -06:00
advplyr 55b7f05685 Fix:Android media play/pause event #429 #427 2023-02-04 11:13:19 -06:00
advplyr 944aa5de66 Fix:Android download book play times in history 2023-02-04 10:32:36 -06:00
advplyr a12eb7efdf Fix:Android sync fails when restarting playback session 2023-02-04 10:11:31 -06:00
advplyr 62a5a2d069 Update:Increase height of minimized audio player to accomadate iOS safe area #531 2023-01-31 15:30:13 -06:00
advplyr ebd65b4fa7 Fix:Back button closing modals and fullscreen player #535 2023-01-31 14:50:26 -06:00
advplyr 152cce2e9e Merge pull request #534 from lkiesow/player-progress-position
Player Progress Bar Position
2023-01-31 14:18:07 -06:00
advplyr ea847a62ff Update components/app/AudioPlayer.vue 2023-01-31 14:17:45 -06:00
advplyr 2a5ec618f0 Merge pull request #533 from lkiesow/metadata-separators
Fix Metadata List Separators
2023-01-31 14:10:35 -06:00
Lars Kiesow 3fc89377b4 Player Progress Bar Position
This patch moves the progress bar slightly up for a better separation
between playback controls and seeking.

The reasoning behind this is that I (and probably others as well) often
interact with this interface in a clumsy, half-asleep state. Due to the
closeness of the progress bar and the play/pause button, I have managed
it several times to accidentally seek instead of pause. That is really
annoying since you then have to manually find the previous playback
position again.

Long term, I would love to see more of the available space being used.
In particular in the player interface. There should be enough free space
available. Especially if you have just one of the progress bars active.
But for now, this is just a quick-fix to improve the current situation.
2023-01-31 15:31:06 +01:00
Lars Kiesow 93c16e6a62 Fix Metadata List Separators
This patch fixes the separators between metadata list entries in the book
detail view. Items were separated only by comma. A space was missing.

This just adds a space to both the genre as well as to the narrator
separators.
2023-01-31 14:43:49 +01:00
advplyr 84632c8a32 iOS version bump 0.9.61 2023-01-30 18:02:52 -06:00
advplyr df717fc87f Version bump 0.9.61-beta 2023-01-30 17:16:12 -06:00
advplyr 2b9f5c866b Fix home page shelves refreshing on network changes 2023-01-30 15:38:09 -06:00
advplyr 2de81130a1 Update:Podcast latest page show cover & podcast name #529 2023-01-30 11:39:01 -06:00
advplyr 2942d703ee Merge pull request #528 from blampe/seek
Handle skip track events on iOS
2023-01-30 08:18:01 -06:00
advplyr 3ef6c345e4 Fix Realm wrong thread crash by setting values from deviceSettings outside of commandCenter functions 2023-01-30 08:14:32 -06:00
advplyr 0a4052dec2 Fix manage local files redirect on iOS 2023-01-30 07:57:34 -06:00
Bryce Lampe ef4bb6c7a8 Handle skip track events on iOS 2023-01-29 19:54:12 -08:00
advplyr d9c38d1592 Update app bar progress indicator to not navigate to downloading on iOS 2023-01-29 17:53:03 -06:00
advplyr a7227acbc3 Update:Default to alt view and rename setting to use bookshelf view 2023-01-29 17:20:46 -06:00
advplyr bd5229941b Fix:Android sleep timer end of chapter use next chapter if within 2s of current chapter ending when reset #260 2023-01-29 17:11:15 -06:00
advplyr 0583ce617e Add:Continue listening shelf for offline books/episodes #112 2023-01-29 16:04:03 -06:00
advplyr 7462eb3bc4 Fix:iOS play audiobook tracks inside subfolders #483 2023-01-29 14:30:09 -06:00
advplyr 18e15ff056 Remove left & right padding of audio player timestamps on iOS 2023-01-29 14:12:14 -06:00
advplyr 4f6468c23a Merge pull request #519 from benonymity/playerUpdates
iOS Player Updates
2023-01-29 14:08:29 -06:00
advplyr 31531197a2 Add:Android auto sleep timer #260 2023-01-29 13:19:06 -06:00
advplyr 479de5f067 Android cleanup 2023-01-29 08:53:10 -06:00
advplyr 03240415b8 Update:Android sleep timer resets on play #260 2023-01-28 17:46:27 -06:00
advplyr f8a6b6a936 Update:Android sleep timer start fading out audio when 60s remaining on timer #320 #260 2023-01-28 17:33:15 -06:00
advplyr 28e50dd461 Add:Android device setting to adjust shake sensitivity for sleep timer #260 2023-01-28 17:31:52 -06:00
advplyr f06afbd08c Update:Sleep timer shake to reset anytime while timer is running #260 2023-01-28 16:07:03 -06:00
advplyr 860c7aac80 Fix:Refreshing server media progress after local sync 2023-01-28 14:20:00 -06:00
advplyr 8bab4ae383 Update:More accurate progress percentage using bytes, download 1 audio file at a time & currently downloading page #251 #360 #515 #274 2023-01-28 11:58:16 -06:00
advplyr 69171e5732 Update:Book downloads will go in author/title subfolders #194 2023-01-28 08:16:16 -06:00
Ben 31abaa019f Merge branch 'advplyr:master' into playerUpdates 2023-01-26 19:16:21 -05:00
advplyr ad57a90e4d Update iOS deployment target to 14 2023-01-26 17:35:37 -06:00
advplyr 925e8fd17e Update item page bg gradient 2023-01-26 17:14:06 -06:00
advplyr 7b857f04d4 Fix appbar z-index 2023-01-26 17:02:06 -06:00
advplyr 453744a637 Remove log 2023-01-25 17:48:31 -06:00
advplyr 062d4dc069 Fix:Render bookshelf on small screens 2023-01-25 17:32:52 -06:00
Ben 21bd98b367 Merge branch 'advplyr:master' into playerUpdates 2023-01-24 23:24:51 -05:00
advplyr ccd206e95d Fix cover height overflow on fullscreen audio player 2023-01-24 17:52:28 -06:00
benonymity bca5421dbc update: higher sync interval on low power mode 2023-01-24 17:35:12 -05:00
benonymity 20085c5eed update: increase update intervals 2023-01-24 17:34:53 -05:00
advplyr a8c66ff808 Update capacitor version, kotlin version, android dependencies, refactor some folders 2023-01-22 17:26:08 -06:00
advplyr b2d3edca81 Update:Android hide download notification when finished #495 2023-01-20 16:22:00 -06:00
advplyr 4b7fa7a543 Fix:Clearing filter refreshes library 2023-01-17 17:47:29 -06:00
advplyr a018dcf085 Update:Underline links on library item page 2023-01-17 17:38:48 -06:00
advplyr 091c1bcb9f Merge pull request #514 from lkiesow/toggle-total-track
Chapter and Total Track in Player
2023-01-17 15:50:59 -06:00
Lars Kiesow d89c97ba1a Chapter and Total Track in Player
This patch allows user to not only add a chapter track but also remove
the total track. This means that you can have only the total track or
only the chapter track.

This patch does not allow user to have no track at all. Disabling one
track will automatically enable the other one, if that one is already
disabled.

The reasoning behind this is that for long-ish audio books, users will
not very often look at the overall progress. In fact, I find the
progress bar mostly useful only for quickly seeking to a position when
jumping back a few seconds is not sufficient.

In the seldom occasion that I want the overall progress, I can easily
get it from the book details page.
2023-01-17 00:54:17 +01:00
advplyr 56fe350104 Update:Search input clearable #509 2023-01-16 17:44:32 -06:00
advplyr eb552170e3 Increase max playback speed to 10x 2023-01-16 17:34:42 -06:00
advplyr 29d022adcc Merge pull request #513 from lkiesow/player-menu-unified-style
Make Player Menu Items Look Alike
2023-01-16 17:04:02 -06:00
advplyr d9afdaf56f Update components/modals/Dialog.vue 2023-01-16 17:03:07 -06:00
Lars Kiesow 699976dcd8 Make Player Menu Items Look Alike
Instead of handling some items of the player menu as special cases,
adding icons and colors, this patch allows menu items to have icons in
general, allowing for the removal of the special cases.

This also makes it easy to actually use a homogeneous style for all of
the player menu items, letting them all have an icon and aligning them
visually.
2023-01-16 23:54:23 +01:00
advplyr acf85f4f09 Implement start time override 2023-01-15 16:33:51 -06:00
advplyr 8f6dd72df2 Local media sync events 2023-01-15 14:58:26 -06:00
advplyr 297eca6a86 MediaItemHistory and history page 2023-01-14 18:01:12 -06:00
advplyr b1805875b9 Update:Play/action button updates, show confirm dialog when mark as finished with progress 2023-01-13 17:30:48 -06:00
advplyr 16b06b5eb8 Fix bg gradient 2023-01-13 16:46:42 -06:00
advplyr dbbb5faa78 Fix button opacity 2023-01-12 17:12:14 -06:00
advplyr 5482b30324 Update:Play button color and increase cover size on item page 2023-01-12 17:04:47 -06:00
advplyr 5bae5411c8 Update:Library item page use bg gradient of primary cover image color 2023-01-11 18:00:05 -06:00
advplyr f75a8dfb86 Update:Redirect when changing libraries 2023-01-11 17:29:49 -06:00
advplyr 716d07c74e Fix:Open audio player covering up add new podcast button #502 2023-01-11 17:17:53 -06:00
advplyr 7de0304a3d Update:Audio player UI use primary cover image color as background gradient 2023-01-11 17:02:18 -06:00
advplyr df54094221 Fix:Android auto play downloaded media if available #385 2023-01-08 17:30:57 -06:00
advplyr 3207841384 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2023-01-08 16:04:20 -06:00
advplyr ae571d6b79 Merge pull request #499 from lkiesow/metadata-table
Metadata Table Alignment
2023-01-08 16:03:51 -06:00
advplyr f6ee0bf8df Fix:Lock orientation setting clicking on toggle button not working #465 2023-01-08 15:38:34 -06:00
advplyr d59f3ae0b6 Add:Haptic feedback device setting off/light/medium/heavy #472 2023-01-08 15:32:15 -06:00
advplyr 89041c4141 Fix:Show play button playing on local and non-local item pages #487 2023-01-08 14:50:21 -06:00
advplyr aac77949b4 Update:Audio player fullscreen move track above play button #408 #418 2023-01-08 14:31:37 -06:00
advplyr eb40f6e6e9 Fix:Library list view cover size #474 2023-01-08 14:07:48 -06:00
advplyr 34b3910ff8 Fix:Cover image for 1:1.6 covers in collapsed audio player #474 2023-01-08 13:50:18 -06:00
advplyr 83fdf4fd65 Update:Increase cover size on fullscreen audio player #491 2023-01-08 13:27:09 -06:00
Lars Kiesow ea6bb0a847 Metadata Table Alignment
This patch updates the metadata table in the book details view, turning
it into a CSS grid layout. This fixes a few issues with the current
layout:

- Keys and values are not vertically aligned with the value being
  displayed a little bit above the baseline of the key.
- There is no horizontal alignment of the table columns. In fact, it is
  technically not a table despite the CSS being tuned to almost look
  like one. Just turning this into a real grid looks cleaner.
- The code for making multi-line values not wrap is broken. This fixes
  the problem.

This also renames “Publish Year” to “Published”. Mainly because it is
shorter.
2023-01-08 17:17:59 +01:00
advplyr d85158bd70 Merge pull request #493 from lkiesow/chepter-idx
Make chapters identifiable
2023-01-05 16:57:02 -06:00
Lars Kiesow 22c5fee48e Make chapters identifiable
When long chapter names are being used, it may easily happen that parts
of the name is cut off. If this part identifies the latest chapter name
or number, it may make chapters indistinguishable from one another. This
makes it near imposible to select a specific chapter if the book has
many chapters.

This patch addresses the issue by starting each item in the list of
chapters with an index number.

This fixes #479
2023-01-05 23:07:11 +01:00
advplyr 0890a44c0b Update:Remove call to server for settings filter/sort/playback rate 2022-12-17 14:48:56 -06:00
advplyr 4fd4cc5604 iOS version bump 0.9.60 2022-12-11 17:11:37 -06:00
advplyr c73b71345a Version bump 0.9.60 & android target sdk version to 31 2022-12-11 17:03:56 -06:00
advplyr 11f0af1ebd Update:Android Auto play next podcast episode after finishing one 2022-12-11 16:09:00 -06:00
advplyr a532f988b0 Update:Local covers for queue items 2022-12-11 14:00:27 -06:00
advplyr bec2796771 Update cast icon position in app bar 2022-12-11 12:18:10 -06:00
advplyr feec7f7399 Add:Sync local media progress button on local media page 2022-12-11 12:01:54 -06:00
advplyr 067c2f84dd Fix:Bookmarks for downloaded books linked to server #163 2022-12-11 11:09:50 -06:00
advplyr 69996a4346 Fix:Changing orientation update audio player progress bar 2022-12-11 10:49:05 -06:00
advplyr 80cda129d2 Fix:Progress bar width on item cover image 2022-12-11 10:29:43 -06:00
advplyr dd0ff04155 Fix:Android Auto downloaded item cover image #141, Update:Lockscreen display title/subtitle #431 2022-12-10 18:09:27 -06:00
advplyr c69558aa7f Fix:Chapters modal overflowing title/time #460 2022-12-10 14:55:33 -06:00
advplyr 8fefea0d94 Fix:Library icons min width 2022-12-10 14:34:21 -06:00
advplyr 0ddee6f44e Fix:Show Manage Local Files & Add to Playlist in more menu correctly 2022-12-10 14:26:58 -06:00
advplyr feec1ab55a Update:Local books/podcasts shelves on home page 2022-12-10 14:17:18 -06:00
advplyr 49500aecd6 Fix network flag 2022-12-10 11:35:16 -06:00
advplyr 894bf100d2 Update:Disable pod search when no network connection #225 2022-12-10 10:44:21 -06:00
advplyr 7ba810adb8 Update:Show error when searching for episodes with no network connection 2022-12-10 10:32:16 -06:00
advplyr d10f33362e Add:Option to delete episodes from server #225 2022-12-10 10:29:56 -06:00
advplyr 82e8fbba2b Download episodes to server & see downloads queued/in progress #225 2022-12-10 10:12:58 -06:00
advplyr 64325fe2a6 Add:Feed podcast episodes select modal #225 2022-12-09 18:03:17 -06:00
advplyr 99217fee48 Merge pull request #459 from benonymity/hapticFeedback
Add haptic feedback
2022-12-09 17:08:17 -06:00
advplyr f12e64990c Update components/home/BookshelfToolbar.vue 2022-12-09 17:08:00 -06:00
advplyr ef7bb9a9d8 Update pages/settings.vue 2022-12-09 17:07:50 -06:00
advplyr 5ac925e151 Update pages/settings.vue 2022-12-09 17:07:45 -06:00
benonymity acc5bde33a fix: use native haptic plugin and remove semicolons 2022-12-08 19:09:46 -05:00
advplyr b521f37ec1 Update:Only show podcast search page for admin or up #225 2022-12-08 17:56:58 -06:00
advplyr 379fa21571 Add:Podcast create form #225 2022-12-08 17:54:56 -06:00
benonymity 1aa6a441f3 feat: add haptic feedback to all action buttons 2022-12-08 00:28:28 -05:00
advplyr 705bbaccc1 Add:Podcast search page #225 2022-12-07 17:57:42 -06:00
advplyr 8365e06762 Merge pull request #458 from benonymity/noTextSelect
Prevent selecting of text in the app
2022-12-07 16:37:47 -06:00
advplyr 139a0b3412 Merge pull request #456 from benonymity/listeningStatsFix
Fix Listening Stats
2022-12-07 16:28:37 -06:00
advplyr ea6b21169e Merge pull request #455 from benonymity/vertical-bounce
Don't allow webview to scroll out of bounds vertically
2022-12-07 16:13:17 -06:00
advplyr a262efe9da Fix lockOrientation setting and add Realm db migration; 2022-12-07 16:12:09 -06:00
benonymity ed5fb09007 fix: prevent selecting of text in the app 2022-12-07 15:41:32 -05:00
benonymity d3c6429fd6 fix: accurate time for streamed sessions 2022-12-07 14:40:50 -05:00
benonymity 371fcacc87 Fix: don't allow webview to scroll out of bounds vertically 2022-12-07 13:31:15 -05:00
advplyr 75666e523f Add:PDF EReader #420 2022-12-06 17:07:27 -06:00
advplyr 592ea7c533 Fix:EReader for ebooks in library folder root 2022-12-06 16:21:12 -06:00
advplyr b860dfc791 Fix:Library filter by progress #451 2022-12-05 17:30:15 -06:00
advplyr 96dde8cf31 Fix:Android seeking to invalid times 2022-12-04 16:57:03 -06:00
advplyr b4a37fed28 Update:Showing fullscreen cover uses raw cover 2022-12-04 16:30:18 -06:00
advplyr ad3fee8907 Fix:Save track order button hidden when player is open #380 2022-12-04 15:04:46 -06:00
advplyr 94db55598a Fix:Changing track sort order (Android) #380 2022-12-04 14:49:24 -06:00
advplyr 9687f47b6b Add:Device setting for locking screen orientation #449 2022-12-04 10:41:09 -06:00
advplyr af216d9b38 Update:Media session media button events for fast forward and rewind #423 2022-12-04 09:34:39 -06:00
advplyr c7879c2bc0 Update:Audio player UI updates #418 #408 2022-12-04 09:19:50 -06:00
advplyr 7db503a2cf Fix:EReader when player is open #442 2022-12-04 08:29:55 -06:00
advplyr d72295a0e6 Fix filter urls on items page 2022-12-03 17:58:22 -06:00
advplyr 638da4400f Update:Item page UI 2022-12-03 17:43:47 -06:00
advplyr af9a7aafae Add:Podcast latest page 2022-12-03 17:24:24 -06:00
advplyr ab9f7fed64 Add:Playlists management 2022-12-03 17:05:43 -06:00
advplyr b62ce27487 Add:Playlists 2022-12-03 15:20:27 -06:00
advplyr 6d9e902fe7 Update:Abs icon font and navbar to use icons 2022-12-03 12:06:24 -06:00
advplyr a0faf3f7d4 Update get libraries API call to support updated response payload 2022-12-02 17:39:45 -06:00
advplyr 32dfb3b40a Fix:Series books page sort order #443 2022-11-17 17:08:30 -06:00
advplyr 6bbcd300e4 iOS version bump 0.9.59 2022-11-05 16:41:45 -05:00
advplyr c54d9b3c5f Version bump 0.9.59 2022-11-05 16:29:30 -05:00
advplyr 9ab03fbcd4 fix & update comic reader for comics with subfolders and better sorting 2022-11-05 15:13:52 -05:00
advplyr a852114e9c Update:Use cover aspect ratio from library setting 2022-10-22 08:59:10 -05:00
advplyr 3c9b8c1c6f Fix read btn color 2022-10-21 16:29:46 -05:00
advplyr 4735b51dc0 Update:New library icons 2022-10-19 10:56:31 -05:00
advplyr 23927a1d59 Merge pull request #413 from wnhrt/master
Colored status and navigationbar
2022-10-16 12:08:55 -05:00
Niklas Weinhart 40b592f7c1 Colored status and navigationbar 2022-10-16 05:37:35 +02:00
advplyr 4da31f5850 Merge pull request #391 from ronaldheft/fix-ios-pause-during-seek
fix: Player sometimes pauses during seek on iOS
2022-09-23 15:37:50 -05:00
advplyr f79e96cbfe Merge pull request #397 from ronaldheft/ios-player-seek-back-time-fixes
feat: Groundwork for testable iOS swift code
2022-09-23 15:36:59 -05:00
ronaldheft 2dbce1e3bb fix: Negative currentTime after seek back 2022-09-21 20:16:29 -04:00
ronaldheft cdfbbcbf8a refactor: Use refactored seek back time class 2022-09-20 18:34:43 -04:00
ronaldheft 890852bef5 refactor: Pull out seek back calculation into testable class 2022-09-20 18:32:28 -04:00
advplyr 3b285db21a Merge pull request #393 from ronaldheft/fix-on-ready-multiple-calls
fix: onReady called multiple times
2022-09-19 16:46:00 -05:00
advplyr d37be88977 Merge pull request #392 from ronaldheft/fix-ios-chapter-sleep-timer
fix: iOS chapter sleep timer issues
2022-09-19 16:45:09 -05:00
ronaldheft e6d27a1487 fix: onReady called multiple times 2022-09-18 14:38:32 -04:00
ronaldheft 5d23c17d30 fix: iOS chapter sleep timer issues 2022-09-18 14:21:41 -04:00
ronaldheft 9c9c1c3b94 fix: Player sometimes pauses during seek 2022-09-18 13:38:16 -04:00
advplyr 4e94fd6ad0 Merge pull request #390 from ronaldheft/fix-ios-seek-back-time
fix: iOS seek back time not winding back
2022-09-18 11:58:50 -05:00
advplyr 81a8203cca Merge pull request #389 from ronaldheft/ios-bug-artwork
Fix local cover images not used on iOS
2022-09-18 11:57:12 -05:00
ronaldheft b6b206c288 fix: Seek back time not winding back 2022-09-18 12:44:48 -04:00
advplyr ed2310a2e7 Merge pull request #387 from ronaldheft/ios-realm-crash
fix: iOS 16 realm crash
2022-09-18 11:29:52 -05:00
ronaldheft bae5d97087 fix: Local artwork not loading 2022-09-18 12:24:19 -04:00
ronaldheft 3e12ff139b fix: Local artwork not used 2022-09-18 11:43:38 -04:00
ronaldheft 7ccd8db085 fix: iOS 16 realm crash 2022-09-17 17:47:18 -04:00
advplyr fdc829d06c Merge pull request #386 from ronaldheft/ios-general-fixes
Fix various iOS app startup issues
2022-09-16 16:14:44 -05:00
ronaldheft e7f61e34e8 Fix player automatically starting after WebKit reload 2022-09-15 20:49:25 -04:00
ronaldheft 141877f111 Fix Pods with iOS target change 2022-09-14 20:24:27 -04:00
ronaldheft b71a976e61 Merge remote-tracking branch 'upstream/master' into ios-general-fixes 2022-09-14 19:55:42 -04:00
advplyr 68c95ed6d5 Merge pull request #371 from ronaldheft/ios-logging
Improve iOS Logging
2022-09-14 17:54:31 -05:00
ronaldheft 7a68cc99af Convert remain NSLog statement to new logging framework 2022-09-13 21:16:55 -04:00
ronaldheft 7294bc7033 Audio player modifications should be on main queue 2022-09-13 20:47:07 -04:00
ronaldheft f8836be147 fix: Debug logging level does not persist to disk 2022-09-08 20:09:35 -04:00
ronaldheft 713999eb83 Fix deprecated and early window access warnings 2022-09-08 19:51:35 -04:00
ronaldheft cc4c9787c0 Rename ABS to Audiobookshelf 2022-09-06 21:56:10 -04:00
ronaldheft ecc5cf3e1b Rename app to provide better logging 2022-09-06 21:42:12 -04:00
ronaldheft ba7d2be03e Fix incorrect method call 2022-09-06 21:33:24 -04:00
ronaldheft 368f349c78 Use logging framework for AudioPlayer 2022-09-06 21:32:32 -04:00
ronaldheft 639d641c07 Fix usage of this 2022-09-06 21:32:10 -04:00
ronaldheft d9313a1654 Use os logging framework, so logs can be retrieved later 2022-09-06 21:26:48 -04:00
advplyr 9dd532285c Merge pull request #365 from vangorra/vangorra/aa-button-cleanup
Cleanup Android Auto buttons
2022-09-04 11:51:11 -05:00
Robert Van Gorkom 92f10fdb5c Merge branch 'advplyr:master' into vangorra/aa-button-cleanup 2022-09-04 09:23:51 -07:00
advplyr 9d0f43a664 Merge pull request #363 from vangorra/vangorra/debug-build
Add debug build to to allow prod and dev apps to work side by side.
2022-09-04 09:46:03 -05:00
Robbie Van Gorkom 453365fcd6 Fix buttons for Android Auto (and probably wear too). 2022-09-03 18:16:12 -07:00
Robbie Van Gorkom 6231c5fcc4 Add debug build to to allow prod and dev apps to work side by side. 2022-09-03 15:06:28 -07:00
advplyr 9821ddabcf iOS version bump 0.9.58-beta 2022-09-03 16:42:14 -05:00
advplyr 57777f2f4b Merge pull request #362 from ronaldheft/fix-ios-mark-as-read-crash
Fix marking local book as read crashing iOS app
2022-09-03 16:35:50 -05:00
advplyr 1f2c309d8c Merge pull request #361 from ronaldheft/ios-audio-events
Fix: Handle iOS audio events
2022-09-03 16:33:10 -05:00
Ron Heft c11977870d Merge branch 'advplyr:master' into ios-audio-events 2022-09-03 17:21:52 -04:00
ronaldheft 41753cd853 Fix marking as read crashing app 2022-09-03 17:18:54 -04:00
advplyr 003285f567 Merge pull request #359 from ronaldheft/bug-ios-sleep-timer
Rewrite iOS sleep timer to handle multi-track files
2022-09-03 16:17:28 -05:00
ronaldheft d1c1902cd3 Merge branch 'bug-ios-sleep-timer' into ios-audio-events 2022-09-03 16:53:03 -04:00
ronaldheft b697deac1c Fix cluegy way chapter sleep timer was being reset after skipping past 2022-09-03 16:44:01 -04:00
ronaldheft fb8e6408bb Fix memory leak 2022-09-03 16:34:31 -04:00
ronaldheft 836ffddd4f Fix race condition with chapter sleep timer 2022-09-02 18:36:57 -04:00
ronaldheft c8ff5a7817 Handle multi-track files for sleep timer 2022-09-02 18:31:16 -04:00
ronaldheft c14f6ec4c2 Rewrite sleep timer logic again 2022-09-02 18:22:42 -04:00
ronaldheft dc8852eb0d Guard against seeking during initialization 2022-09-02 16:52:08 -04:00
ronaldheft 586405f9de Rewrite sleep timer 2022-09-02 16:31:47 -04:00
advplyr 34e68593bc Merge pull request #355 from ronaldheft/ios-bug-seek-multi-track
Fixes iOS player seek issues with multi-track books
2022-09-02 08:21:39 -05:00
ronaldheft c02adfa326 Fix typo in if statement 2022-09-01 18:41:21 -04:00
ronaldheft 021d63d973 Fix seek jumping 2022-09-01 18:19:45 -04:00
ronaldheft 50f6b15d23 Refactor to ensure we aren't missing any update time 2022-09-01 17:40:49 -04:00
ronaldheft 6257c6488b Fix race condition with the player queue 2022-09-01 17:34:54 -04:00
advplyr defd895995 Merge pull request #347 from ronaldheft/ios-bg-downloads
feat: Handle iOS background downloads
2022-08-31 15:00:20 -05:00
ronaldheft 82159ee537 feat: Handle route change notifications 2022-08-30 22:59:59 -04:00
ronaldheft 11f22888d5 feat: Handle resuming iOS audio after pause 2022-08-30 22:47:55 -04:00
ronaldheft 20d932877e fix: Only mark audio session as active when playing 2022-08-30 22:33:55 -04:00
ronaldheft 35631fb211 feat: Handle iOS background downloads 2022-08-30 22:14:59 -04:00
advplyr b02dd18e0b Merge pull request #340 from ronaldheft/ios-crashes
fix: iOS app crashes when playback session not loaded from Realm
2022-08-30 16:29:51 -05:00
ronaldheft b583a8e6e2 Handle iOS crashes when no session 2022-08-30 17:05:06 -04:00
advplyr 5775a51731 Merge pull request #339 from ronaldheft/fix-ios-multitrack-currenttime
Fix: Session currentTime is inaccurate for books with multiple tracks
2022-08-30 04:34:25 -05:00
advplyr 9b94a11ab9 Merge pull request #338 from ronaldheft/ios-fix-playback-timer
Fix: Extra timeListening when player initializes on iOS
2022-08-30 04:33:34 -05:00
ronaldheft 9ee1203130 Fix multi-track currentTime value 2022-08-29 21:16:51 -04:00
ronaldheft 711b079963 Fix: Extra timeListening when player initializes 2022-08-29 20:39:55 -04:00
advplyr 91dbd73d45 iOS version bump 0.9.57 2022-08-27 17:03:31 -05:00
advplyr a2ac0e29e4 Version bump 0.9.57-beta 2022-08-27 16:00:20 -05:00
advplyr 4271d832f1 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2022-08-27 15:54:19 -05:00
advplyr 64147d065c Update:Book collection row item clickable #305 2022-08-27 15:54:13 -05:00
advplyr fb03c8e67f Merge pull request #335 from ronaldheft/ios-respect-skip-settings
Fix: Skip preferences not respected for iOS remote control
2022-08-27 14:23:25 -05:00
advplyr 75979960ab Merge pull request #330 from ronaldheft/bug-ios-background-currenttime
Improve iOS Player Sync Logic
2022-08-27 14:22:56 -05:00
ronaldheft 1411157bde Reindex server configs and fix bad data 2022-08-26 20:20:46 -04:00
ronaldheft eb7289c150 Fix the incorrect server config being persisted 2022-08-26 20:20:26 -04:00
ronaldheft c1f803bdd0 Fix configs sharing an index 2022-08-26 20:04:06 -04:00
ronaldheft ba1efedd79 Show currentTime in console 2022-08-26 19:48:54 -04:00
ronaldheft ac10997ed7 Fix: Skip preferences not respected for iOS remote control 2022-08-26 19:34:34 -04:00
ronaldheft 3c6f29bf3a Fix time observer crashing when already on main thread 2022-08-26 19:11:55 -04:00
advplyr f623393f8d Merge pull request #334 from ronaldheft/ios-failed-playback-session
Fix failed set category on initAudioSession
2022-08-26 18:02:17 -05:00
ronaldheft 66ab402a50 Fix failed playback session on initAudioSession 2022-08-26 18:58:08 -04:00
ronaldheft 2076b93e19 Fix edge case when seeking past chapter 2022-08-26 18:35:47 -04:00
advplyr a5aae3fce8 Fix:Audio player more menu close playback 2022-08-25 18:04:14 -05:00
ronaldheft eb7a241e94 Fix sleep timer chapter locking 2022-08-25 19:03:05 -04:00
advplyr 14ff9097c7 Update:Changing library while on item page redirects to home 2022-08-25 17:58:29 -05:00
advplyr 79f09b334c Add:Book library sort by published year, Fix:Check and set sort/filter when changing library to different media type 2022-08-25 17:50:55 -05:00
ronaldheft f5d1e992ef Fix edge case where incorrect progress was tracked 2022-08-25 18:46:13 -04:00
ronaldheft aed2c31f5a Use persisted session to inform seek back 2022-08-25 18:43:57 -04:00
ronaldheft 3e31e72734 Configure time observer on the main queue 2022-08-25 18:31:09 -04:00
ronaldheft 7cf36d829a Fix progress updating issues 2022-08-25 18:28:17 -04:00
ronaldheft 268cf67576 Fix lost sleep time on play/pause 2022-08-25 17:39:06 -04:00
ronaldheft 5c76158729 Fix holding onto frozen Realm reference 2022-08-25 16:55:35 -04:00
ronaldheft 8c87b31e56 Improved error handling 2022-08-25 15:42:37 -04:00
ronaldheft 01678f2c91 Remove DispatchQueue as that did not fix Realm crashes 2022-08-24 19:57:39 -04:00
ronaldheft b5e33b1707 Fix thread-safety with transaction on local media progress 2022-08-24 19:33:10 -04:00
advplyr bba388aefa Update:Android auto show in progress media items across all libraries #285 2022-08-24 17:55:07 -05:00
ronaldheft 46623d70a3 Realm modifications should occur on concurrent queue 2022-08-23 22:37:28 -04:00
ronaldheft 452b25057e Use the same Realm instead for updates of LocalMediaProgress 2022-08-23 22:07:07 -04:00
ronaldheft 67a6aec132 Move AudioPlayer tasks off the main queue 2022-08-23 22:06:41 -04:00
ronaldheft 10ddc1c911 Fix server config not factored in determining active session 2022-08-23 18:56:08 -04:00
ronaldheft 2a2ebefeb9 Fix thread safety issues 2022-08-23 18:51:30 -04:00
ronaldheft d5f39e5cb1 Fix async call not waiting for results 2022-08-23 18:22:11 -04:00
ronaldheft 099be648bf Fix server not sending updates every 10 seconds 2022-08-23 17:32:43 -04:00
ronaldheft 6a885b7241 Fix session time using milliseconds instead of seconds 2022-08-23 17:13:43 -04:00
ronaldheft 15cdff5aa2 Fix typo in comment 2022-08-22 20:36:15 -04:00
advplyr 8bd0e863ae Fix:iOS audio player in landscape mode 2022-08-22 17:18:46 -05:00
ronaldheft 94e261d7bf Fix bad data being encoded 2022-08-22 18:12:07 -04:00
ronaldheft 06f87d24a7 Fix merge conflict 2022-08-22 18:12:00 -04:00
ronaldheft cdd2f40ef9 Merge branch 'master' into bug-ios-background-currenttime 2022-08-22 18:04:02 -04:00
ronaldheft 2448b461f0 Re-implement the paused timer 2022-08-22 18:00:37 -04:00
advplyr 4f85827585 Add:Lock/unlock player ui setting #313, Update:Player track ui thinner with cursor 2022-08-22 16:50:20 -05:00
ronaldheft fe042f3f83 Fix NaN causing bad data 2022-08-22 17:27:55 -04:00
ronaldheft d084958f2d Scale time reporting by the rate 2022-08-22 17:15:20 -04:00
ronaldheft ccecba7a19 If adjusting sleep time from chapter, convert to regular sleep timer 2022-08-22 17:11:34 -04:00
ronaldheft d57fe44bcc Sleep timer using native time observer 2022-08-22 17:04:48 -04:00
advplyr a32bd9d74c Update:Audio player menu to modal instead of dropdown 2022-08-22 16:02:35 -05:00
advplyr 39250b56c0 Fix:Play local episode from home page playing from server #324, Update:Podcast cover UI for displaying episodes and showing local download indicator 2022-08-21 18:03:27 -05:00
advplyr 1c85e21e4f Merge pull request #329 from benonymity/download-page
Downloads page for iOS
2022-08-21 16:20:20 -05:00
advplyr d331e1f9ab iOS downloads page cleanup UI 2022-08-21 16:20:10 -05:00
advplyr d7c414f6c0 Merge 2022-08-21 15:59:44 -05:00
advplyr 523f861f77 Merge branch 'master' into download-page 2022-08-21 15:56:20 -05:00
advplyr 8a0a6f383f Merge pull request #322 from jakob42/manualSleepTimer
added manual sleep timer dialog
2022-08-21 15:52:39 -05:00
advplyr b8b2d73eab Sleep timer UI update and min custom time to 1 min 2022-08-21 15:52:06 -05:00
advplyr 683b4e753a Merge branch 'master' into manualSleepTimer 2022-08-21 15:38:38 -05:00
advplyr a3f2b02473 Merge pull request #328 from vangorra/vangorra/android_auto_buttons
Add additional controls to android auto
2022-08-21 15:20:44 -05:00
benonymity befe62f8a0 feat: downloads page for iOS 2022-08-21 15:02:22 -04:00
Robbie Van Gorkom 621a493c5b Add jump back and jump forward buttons to android auto. 2022-08-21 10:26:09 -07:00
ronaldheft 8952cbfd20 Start of refactor 2022-08-21 12:36:29 -04:00
advplyr a576eff700 Merge pull request #327 from ronaldheft/bug-ios-playback-on-session-resume
Fix playback session starting sometimes on app launch
2022-08-21 11:15:23 -05:00
advplyr 506e18a137 Merge pull request #326 from ronaldheft/bug-offline-plackback-no-progress
Fixes missing LocalMediaProgress for downloads without existing progress
2022-08-21 11:09:37 -05:00
ronaldheft 27d2ed2304 Convert PlayerHandler to shared instance 2022-08-21 12:06:37 -04:00
advplyr a670ba9f95 Merge pull request #277 from ronaldheft/ios-downloads
iOS Downloads  / Improved Progress Syncing
2022-08-21 10:55:18 -05:00
ronaldheft 2321312427 Revert "Fixes missing LocalMediaProgress for downloads without existing progress"
This reverts commit 7745ccefab.
2022-08-21 11:34:30 -04:00
ronaldheft 85382d2bb4 Fix incorrect logic for checking if progress object exists 2022-08-21 11:34:23 -04:00
ronaldheft 7745ccefab Fixes missing LocalMediaProgress for downloads without existing progress 2022-08-21 11:25:50 -04:00
ronaldheft 872931f507 Guard against player actions before player is initialized 2022-08-21 11:05:16 -04:00
ronaldheft f335d8735f Fix progress sync not updating playback object 2022-08-21 10:59:43 -04:00
ronaldheft fca0fd7bbb Remove dead code 2022-08-20 10:28:39 -04:00
ronaldheft a461ed43de Finish cleaning up some refactors 2022-08-20 10:15:45 -04:00
ronaldheft 0f8672fb92 Remove Unrealm 2022-08-20 10:11:30 -04:00
ronaldheft 972fbd42ee Refactor and clean up sync logic 2022-08-19 23:00:40 -04:00
ronaldheft 062a217946 Fix more edge cases on player initialization 2022-08-19 22:15:06 -04:00
ronaldheft a59f3bb957 Fix player reporting currentTime=0 2022-08-19 21:20:53 -04:00
ronaldheft 93cb27d324 Fix player session restoration events 2022-08-19 16:36:56 -04:00
jakob42 b4e70da1f5 check for negative timer values 2022-08-19 08:46:15 +02:00
ronaldheft bf46c46fc0 Fix race conditions on first launch 2022-08-18 19:31:32 -04:00
ronaldheft af835f2c43 Persist playback settings 2022-08-18 19:01:10 -04:00
ronaldheft ffd0d4da7d Bump session restoration to 1 second 2022-08-18 18:18:22 -04:00
ronaldheft e9226671c9 Fix updatedAt not updating 2022-08-18 17:38:01 -04:00
ronaldheft 5fd3f3c080 Sync player session with server media progress 2022-08-18 17:08:41 -04:00
ronaldheft 519969eee0 Restore playback session on startup 2022-08-18 16:20:28 -04:00
ronaldheft 1746ed6469 Refactor starting a playback session 2022-08-18 15:57:03 -04:00
ronaldheft 2912d442b5 Clean up progress syncing in PlayerHandler 2022-08-18 15:56:25 -04:00
ronaldheft a82474cf45 Fix server media progress JSON decoding 2022-08-18 15:55:50 -04:00
jakob42 56a3d8bb72 added manual sleep timer dialog 2022-08-18 13:44:23 +02:00
ronaldheft add64249cd Times in milliseconds 2022-08-17 20:42:53 -04:00
ronaldheft d3172ece9b Fix missing metadata 2022-08-17 20:42:45 -04:00
ronaldheft f655f89c49 Fix missing fields in PlaybackSession 2022-08-17 19:43:11 -04:00
ronaldheft 911e46c4f0 Fix crash when offline 2022-08-16 21:57:34 -04:00
ronaldheft ec4647e2bc Handle server success 2022-08-16 21:36:09 -04:00
ronaldheft 10f2da9e90 Fix progress syncing 2022-08-16 21:14:33 -04:00
ronaldheft 6aa0f2253b Sync offline playback progress 2022-08-16 20:45:29 -04:00
ronaldheft 7c5ee940d3 Merge branch 'master' into ios-downloads 2022-08-16 16:56:47 -04:00
ronaldheft d83e04c47b Refactor storage model to work with native Realm 2022-08-16 16:53:47 -04:00
ronaldheft b0905d0270 Fix crashes related to Realm threading 2022-08-16 12:32:22 -04:00
ronaldheft 8ce0d9ce56 Fix warnings 2022-08-15 17:00:39 -04:00
ronaldheft ac2a1ce079 Provide DAO methods for Realm objects 2022-08-15 17:00:22 -04:00
ronaldheft 5d31499fd6 Fix the downloader for using Realm native 2022-08-14 22:20:24 -04:00
ronaldheft 4c8217edf6 Fix merge conflict errors 2022-08-14 18:22:50 -04:00
ronaldheft 934a07a5ad Merge branch 'ios-downloads' into ios-downloads-realm-native 2022-08-14 17:48:31 -04:00
ronaldheft 7aae1edf3c Fix incorrect downloadUrl 2022-08-14 16:11:22 -04:00
ronaldheft 7cc35e4b96 Switch to timer for download task 2022-08-14 16:10:55 -04:00
ronaldheft 1f8a8d1ce5 Make timer easier to start for the future 2022-08-13 12:56:02 -04:00
ronaldheft e0e2f02e0b Fix all kinds of syncing errors 2022-08-13 12:41:20 -04:00
ronaldheft c029e519e9 Sync local progress to front-end 2022-08-13 10:28:28 -04:00
ronaldheft ef661bba37 Sync local progress with server progress 2022-08-12 23:11:09 -04:00
ronaldheft 8d38f3358e API functions to sync local progress 2022-08-12 21:58:54 -04:00
ronaldheft f4e39ec7ca Functions to update local progress 2022-08-11 20:38:51 -04:00
advplyr 72744dcd29 iOS version bump 0.9.56 2022-08-11 19:11:25 -05:00
advplyr 6b374d923b Version bump 0.9.56 2022-08-11 19:08:18 -05:00
advplyr 03a47b06ee Fix:iOS sleep timer chapter end #291 2022-08-11 18:49:04 -05:00
advplyr 77d0974782 Update:default podcast episode table sort descending 2022-08-11 17:37:13 -05:00
advplyr 95e070a897 Add:Podcast item page play button to play next unfinished episode #139 2022-08-11 17:36:27 -05:00
ronaldheft c32c77b963 Fix player display on local media 2022-08-11 18:34:59 -04:00
ronaldheft 9477860bca Working local file playback 2022-08-11 18:29:55 -04:00
advplyr a5f483f518 Fix:Home page podcast episode progress bar #264 2022-08-11 16:49:38 -05:00
ronaldheft 65fcc45bc6 Remove todos for file system 2022-08-11 16:45:34 -04:00
ronaldheft 2e6946e1f7 Disable track reordering on iOS 2022-08-11 16:39:59 -04:00
ronaldheft 64707b0928 Support local media progress 2022-08-11 16:26:32 -04:00
ronaldheft a107a25e43 Fix downloading in progress books 2022-08-11 16:11:53 -04:00
ronaldheft 0a989e7811 Handle deleting podcast episodes 2022-08-11 15:36:25 -04:00
ronaldheft 4e63d43679 Fix progress done percent 2022-08-11 15:36:06 -04:00
ronaldheft 60cb5f398c Handle deleting library items 2022-08-11 14:12:45 -04:00
ronaldheft 8edeefc1cd Prevent iCloud backups of downloads 2022-08-11 13:46:30 -04:00
ronaldheft 02eabb82c1 Hide UI options that iOS does not need / support 2022-08-11 13:16:25 -04:00
ronaldheft 999d434fe9 Fix download item not clearing for podcasts 2022-08-11 12:58:47 -04:00
ronaldheft 2b07c81e54 Use book/podcast helper 2022-08-11 12:32:58 -04:00
ronaldheft 5f4ff38035 Support downloading new podcast episodes 2022-08-11 12:30:45 -04:00
ronaldheft e52a5fd585 Fix incorrect episode id 2022-08-11 11:20:29 -04:00
ronaldheft 599a8e23cb Fix podcast episode downloading 2022-08-11 11:20:17 -04:00
ronaldheft 81a4e4ff7b Enable podcast downloading on iOS 2022-08-10 23:20:56 -04:00
ronaldheft 7d730dcafb Fix optionals 2022-08-10 22:56:42 -04:00
ronaldheft 446e54cb91 Fix data model issues 2022-08-10 22:17:12 -04:00
advplyr b9f428fd77 Fix:Android auto separate all library items list from selected library items list 2022-08-10 18:56:12 -05:00
advplyr 94e8978ce6 Fix:Unregister android network listener on destroy 2022-08-10 18:48:47 -05:00
advplyr 62376871e3 Fix:Android get track offset out of bounds crash 2022-08-10 18:33:30 -05:00
advplyr 40b731534c Fix:Android crash when attempting sync after exoplayer failed 2022-08-10 18:27:37 -05:00
advplyr 1aea0a18a9 Fix:Android catch json parse exception from ffprobe caused by XMP id3 tag 2022-08-10 18:02:16 -05:00
advplyr b1be594932 Fix:Android catch exception when grantUriPermission fails 2022-08-10 17:31:38 -05:00
ronaldheft a9d7fbc083 Convert objects to realm-native 2022-08-10 17:09:49 -04:00
ronaldheft a3e458fcc4 Make server config thread-safe 2022-08-10 17:08:19 -04:00
advplyr c1b8f8519f Fix:Android auto library items mixing with other libraries #309 2022-08-09 18:50:59 -05:00
advplyr 34d4a0b61b Fix:When stopping cast progress reverts back to initial progress #314 2022-08-09 18:14:07 -05:00
advplyr 7c3b6c9702 Fix:Starting cast player and stopping cast closing audio player in app #314 2022-08-09 17:35:42 -05:00
ronaldheft e275aa1699 More improvements to progress status 2022-08-08 20:05:09 -04:00
ronaldheft e9961f64a9 Handle a documents directory that can change
Thanks iOS
2022-08-08 19:25:59 -04:00
ronaldheft 8e2be4704e Clean up progress logic 2022-08-08 17:21:13 -04:00
ronaldheft 948cd3068a Fix more race conditions with progress reporting 2022-08-08 13:47:37 -04:00
ronaldheft 162eb9afad Fix race condition of reporting finished download 2022-08-08 12:39:55 -04:00
ronaldheft 30ae98c3de Throttle downloads 2022-08-07 21:17:13 -04:00
ronaldheft b1b10e846d Somewhat working progress under heavy load 2022-08-07 21:02:25 -04:00
advplyr 56c73c12db Update:Android auto show progress bar 2022-08-07 17:52:37 -05:00
ronaldheft ad802c16ea Remove blocking realmQueue 2022-08-07 17:46:13 -04:00
advplyr afc04d22bf Remove old android splash screen 2022-08-07 12:11:38 -05:00
advplyr b78f004f29 Update:Sort local books shelf show unfinished books first #302 2022-08-07 11:00:35 -05:00
ronaldheft 849b6303e8 Display downloaded cover on items 2022-08-07 11:59:02 -04:00
advplyr 4a82ba7c86 Add:Subtitle to item page 2022-08-07 10:35:01 -05:00
ronaldheft 5b7fcca800 Support downloading covers and podcast episodes 2022-08-07 11:23:50 -04:00
ronaldheft b549528e23 Handle download complete lifecycle 2022-08-07 10:27:08 -04:00
advplyr 719db8d3f6 Add:Setting to disable shake to reset sleep timer #298 #169 2022-08-07 09:13:10 -05:00
advplyr db98492d4f Update:Remove limit on home page local media shelves #301 2022-08-07 08:38:30 -05:00
advplyr 5430c10424 Add:Filter by language #312 2022-08-07 08:21:17 -05:00
ronaldheft 5495bcb945 Starting logic for handling completed downloads 2022-08-06 21:32:04 -04:00
ronaldheft 7fded5e105 Cleanup progress handling 2022-08-06 21:12:21 -04:00
ronaldheft dc8bc2fccd Clean up progress logic 2022-08-06 18:31:59 -04:00
ronaldheft af2c609405 Report download progress to the UI 2022-08-06 18:21:11 -04:00
ronaldheft d5d65e244b Rewrite downloader to use delegate and download item 2022-08-06 10:11:46 -04:00
advplyr 92a31c246c Update:Collections page show total duration #303 2022-08-04 18:23:09 -05:00
advplyr 9082b76707 Update:Collections table row book count center #306 2022-08-04 18:16:05 -05:00
ronaldheft 33041608f8 Switch to async logic 2022-08-04 18:25:27 -04:00
advplyr 408faf3665 Merge pull request #308 from benonymity/stats
Add Statistics
2022-08-04 17:23:51 -05:00
advplyr be6c675ded Show user stats nav only when connected to server, remove tooltip, remove icons on stats 2022-08-04 17:22:56 -05:00
ronaldheft 528854b285 Bump min ios version to support async 2022-08-04 16:24:41 -04:00
benonymity 87e277fad1 fix: lint comments 2022-08-03 22:20:20 -04:00
benonymity 014588180e feat: add stats page to mobile 2022-08-03 22:17:11 -04:00
ronaldheft e02edb9f9e Fix field collision 2022-08-02 21:05:28 -04:00
ronaldheft 37fa931672 Don't save in database until download is complete 2022-08-02 17:32:40 -04:00
ronaldheft a7424cc428 Fix persisting active config 2022-08-02 17:10:45 -04:00
advplyr 837df329e2 Update:List view/grid view icon swapped 2022-08-01 18:03:54 -05:00
ronaldheft f6c43e479d Rewrite of object model to use Unrealm
This addresses issues with JSON serialization
2022-08-01 09:40:28 -04:00
ronaldheft 0b46a9c9b1 Begin handling playback of local media 2022-07-31 13:33:36 -04:00
ronaldheft 76de92fe1f Fix id typos 2022-07-30 18:40:30 -04:00
ronaldheft 32550a75ec Return local items on DB lookup 2022-07-30 18:25:20 -04:00
ronaldheft 9eca03cfd7 Configure more local file data 2022-07-30 17:21:50 -04:00
ronaldheft fec1ec554b feat: Populate LocalFile with real info 2022-07-30 16:22:41 -04:00
ronaldheft db7a8cef77 Merge branch 'master' into ios-downloads 2022-07-30 15:16:58 -04:00
advplyr 5662c1983a Update:Series book list view show sequence before title 2022-07-30 08:42:49 -05:00
advplyr 8cfefdcc0f Fix:Show duration in alt bookshelf view sort 2022-07-30 08:39:50 -05:00
advplyr 7d91b95320 iOS version bump 0.9.55 2022-07-28 19:45:23 -05:00
advplyr beb790ad17 iOS add getAllLocalMediaProgress capacitor func 2022-07-28 19:44:18 -05:00
advplyr 073110376a Fix:iOS sending multiple sync requests at the same time, Update:iOS sync every 5s 2022-07-28 19:30:18 -05:00
advplyr 16f6f3b7e9 iOS Realm migration to add new altViewEnabled device setting 2022-07-28 19:14:29 -05:00
advplyr 4758f3e557 Version bump 0.9.55-beta 2022-07-28 18:36:43 -05:00
advplyr 38cc2adf0f Comment out widget in dev 2022-07-28 18:29:20 -05:00
advplyr 80fbbfc2ef Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2022-07-28 18:25:47 -05:00
advplyr ca45956479 Fix:Media progress syncer to not sync media with currentTime 0 and to not sync when lastSyncTime is not set, cancel timer right away on stop/pause syncer #268 2022-07-28 18:25:36 -05:00
advplyr 3d64c42b14 Merge pull request #299 from samblooms/master
Added alternative view setting
2022-07-27 18:24:25 -05:00
advplyr 97e7a12883 Updates to make alt bookshelf view look like the web app 2022-07-27 18:21:10 -05:00
Sam Bloomingdale e4d2b646e7 remove modified .gitignore 2022-07-27 15:01:30 -05:00
Sam Bloomingdale a5a8f3b6ac alt view series card 2022-07-27 14:54:29 -05:00
Sam Bloomingdale 41b0535213 fix typo in class name 2022-07-27 13:46:00 -05:00
Sam Bloomingdale a146a157b8 add alternative view to Shelf.vue 2022-07-27 13:37:20 -05:00
Sam Bloomingdale 96fff469fc add alternate view to LazybookShelf.vue 2022-07-27 13:29:07 -05:00
Sam Bloomingdale a60ff00f7e add getter method for 'enableAltView' 2022-07-27 13:15:03 -05:00
Sam Bloomingdale d52c326123 add 'Alternative Bookshelf View' setting 2022-07-27 12:58:06 -05:00
advplyr 76882780a4 Add:Start of app widget to control media playback 2022-07-23 18:09:10 -05:00
advplyr 97eb19da86 Fix:Load local media progress on launch before attempting server connection 2022-07-22 17:00:49 -05:00
advplyr 4ed9d702e9 Version bump 0.9.54-beta 2022-07-22 08:25:48 -05:00
advplyr 4c678836fb Update:Metered network connections to send progress syncs every 60s instead of every 5s and show metered/unmetered in connection status icon #238 2022-07-21 19:18:32 -05:00
advplyr fd134097a1 Fix:Android auto load libraries handle no libraries returned, Update:Local media items cover images width alignment #279 2022-07-21 18:06:33 -05:00
advplyr 2a87f1de28 Fix:Android check local media progress after a pause reverting to previous progress #290 2022-07-21 17:54:00 -05:00
advplyr 1f60a552e5 Version bump 0.9.53-beta 2022-07-20 18:02:07 -05:00
advplyr 3b34029ca2 Fix:M4B downloads adding MP3 extension by downloading to SimpleStorage v0.14.0 #292 2022-07-20 17:38:13 -05:00
advplyr 5f6a1ef7e9 Update:Show error icon on player cover when local media progress fails to sync & remove sync failure toast 2022-07-19 18:50:14 -05:00
advplyr b51f65d2a4 Fix:Closing playback first closes media progress syncer #268 2022-07-19 18:09:55 -05:00
advplyr dc9526beb3 Fix:Edit server config showing duplicate config already exists 2022-07-19 17:32:49 -05:00
advplyr 7188089f63 Fix:Set server settings after user logs in 2022-07-19 16:40:50 -05:00
ronaldheft 2ab9dbc72d Set encoder and init methods 2022-07-17 21:50:19 -04:00
ronaldheft 52d0890032 Refactor to use extensions for objects 2022-07-16 21:46:49 -04:00
ronaldheft b7725c455b Prototype response to app 2022-07-16 20:25:04 -04:00
advplyr db1d6da491 Version bump 0.9.52-beta 2022-07-15 19:59:51 -05:00
advplyr 329a51f886 Example of adding custom action icon for android auto 2022-07-15 19:48:36 -05:00
advplyr 0c0badbae4 Fix:Android Auto use saved playback speed #229 2022-07-15 18:40:20 -05:00
advplyr 2da272c270 Fix:Android auto starting items from continue listening #167 2022-07-15 18:13:04 -05:00
advplyr 25ec5b3dcb Fix:ANR with starting foreground service before starting session 2022-07-15 17:46:33 -05:00
advplyr d9cdc16ff9 Fix:Catch api handler invalid json response body 2022-07-15 16:22:44 -05:00
advplyr 066b72d54d Fix:Media progress sync wait for api request to return before stopping 2022-07-15 16:10:42 -05:00
advplyr e7c913643a Add:Last local media progress sync with server widget, Update:Remove local media progress with bad id 2022-07-13 19:17:34 -05:00
advplyr 9847ed9fcb Fix network connection indicator 2022-07-13 17:13:15 -05:00
advplyr 721f812df0 Update:Library widget UI 2022-07-13 17:10:47 -05:00
advplyr a58965306a Fix misleading plugin function name 2022-07-13 16:44:02 -05:00
advplyr 2b5377db04 Merge pull request #278 from Jnewbon/master
Added ability to Give write permission to the SD card
2022-07-11 14:57:38 -05:00
Jnewbon 7fe3849705 Re Added read Permission check as i probably shouldn't have removed it 2022-07-11 00:43:36 +01:00
Jnewbon f998deb725 Added Storage Media type Write access 2022-07-11 00:26:53 +01:00
ronaldheft 175e642081 First pass at attempting to save library items 2022-07-09 18:37:32 -04:00
ronaldheft e620f53705 Get something saving in the database 2022-07-09 17:35:59 -04:00
ronaldheft f56561b898 Refactor into smaller functions 2022-07-09 15:57:45 -04:00
ronaldheft a29a7669e1 Add unrealm to use Swift object in db 2022-07-09 15:19:20 -04:00
ronaldheft 29f08a5e5d Fix use of optionals 2022-07-09 15:18:59 -04:00
ronaldheft f40da562a6 Fix missing braces 2022-07-09 15:18:45 -04:00
ronaldheft affcdea09b Fix class name collisions 2022-07-09 15:18:34 -04:00
ronaldheft 16ca7460f4 Convert back to Swift objects 2022-07-09 15:18:15 -04:00
advplyr cb2aaede67 Fix:Socket reconnection on disconnect, Add:Connection indicator icon showing socket/cellular 2022-07-07 17:24:26 -05:00
benonymity 2ca9ce797d feat: iOS download groundwork 2022-07-06 10:09:17 -04:00
advplyr e07e7f70d6 Fix:Buffered progress bar for chapter track #250 2022-07-04 18:42:43 -05:00
advplyr 48313b60c1 iOS version bump 0.9.51 2022-07-04 16:56:04 -05:00
advplyr 3be13025a8 Fix:iOS swipe navigation to close modal 2022-07-04 16:55:34 -05:00
advplyr 505717e580 Version bump 0.9.51 2022-07-04 16:05:20 -05:00
advplyr b4bb6bf81b Fix:Back button to close modal #267 and back button to minimize fullscreen player #267 2022-07-04 15:42:59 -05:00
advplyr 6a8d901ce1 Update:Collections table row UI updates and include book duration #240 2022-07-04 14:59:56 -05:00
advplyr 68f3f0e276 Fix:Prevent crashes and catch exceptions for crash reports 2022-07-04 12:47:18 -05:00
advplyr ddf8e66a08 Fix:Remove server connection configs and prevent duplicate configs 2022-07-04 12:15:08 -05:00
advplyr 5b7a492a48 Update DB initializing to fix crashes for Paper db not initialized 2022-07-03 13:45:24 -05:00
advplyr 9be1ee1843 iOS version bump 0.9.50 2022-07-02 18:30:22 -05:00
advplyr b5c6acc2bc Add:iOS device settings for jump forward/backward 2022-07-02 18:29:41 -05:00
advplyr 39b137c7f1 Version bump 0.9.50 2022-07-02 16:40:31 -05:00
advplyr b16a7342aa Update:Tapping title/author in fullscreen player redirects to library item page #246 2022-07-02 15:40:37 -05:00
advplyr 3056c55d2f Fix:Update total track width on open fullscreen 2022-07-02 15:03:21 -05:00
advplyr 6b416e6fc1 Fix:Podcast author on item page and use html description for podcast episodes 2022-07-02 14:33:53 -05:00
advplyr e4c6093a82 Update:Notification player closed when closing in-app player and stop button on notification player #255, Add:Setting for disabling auto rewind 2022-07-02 14:18:21 -05:00
advplyr 3c1120ea48 Add settings page with adjustable jump forward/backward settings 2022-07-01 20:05:11 -05:00
advplyr a41e26e4c6 Fix:Fetch more library items per request to fill screen size #265 2022-07-01 17:01:40 -05:00
advplyr 317dc366e3 Add custom headers modal and model 2022-07-01 16:33:39 -05:00
advplyr cd4c280950 Add:Tap cover to show fullscreen cover image #224 2022-06-24 17:20:13 -05:00
advplyr 0ca8de5916 Fix:Audio player fullscreen in landscape mode #234 2022-06-23 19:27:41 -05:00
advplyr 3840f2f344 Update:Less aggressive seek back after pausing, first seek back starts on a 10s pause and jumps back 3s #230. Minimum seek back after a pause is the current chapter start time #244 2022-06-21 18:22:47 -05:00
advplyr f353a44fac Fix:Chromecast current track #253 2022-06-21 17:25:27 -05:00
advplyr a06796d9ca Fix initialize Paper for android auto 2022-06-19 12:03:50 -05:00
advplyr a69054fefa Fix crash when server requests fail 2022-06-19 11:57:19 -05:00
advplyr 6805d7eb96 Update readme.md 2022-06-11 21:46:20 -05:00
advplyr c77d0f1944 Merge pull request #233 from jmt-gh/add_server_version
Show server version in drawer
2022-06-09 17:36:56 -05:00
jmt-ghandadvplyr c457c8eaf7 Update components/app/SideDrawer.vue
Co-authored-by: advplyr <67830747+advplyr@users.noreply.github.com>
2022-06-09 15:28:49 -07:00
jmt-gh a391db5dc2 Change what is being called for version
If I tried to grab version directly in the computed section, it would
throw a null error on reload in browser (didn't see any issue on an
actual device). Doing it this way fixes that
2022-06-09 12:08:04 -07:00
jmt-gh 2180e2f649 Add server version to drawer
This commit adds the server version next to the URL. It also adds a new
style to ensure that the text wraps properly if the URL is too long.
Trying text-wrap didn't work.
2022-06-09 11:57:44 -07:00
advplyr fc6d16bdd9 Update:Make audio probe optional 2022-06-06 18:47:09 -05:00
advplyr 815529129a Fix:Save playback speed when using custom value #226 2022-06-06 17:19:02 -05:00
advplyr bad8d10a18 Fix:Android 9 and below scan folder for m4b #220 2022-06-05 16:46:38 -05:00
advplyr a4412da3ed Version bump 0.9.49-beta 2022-06-05 16:38:45 -05:00
advplyr 870774b408 Fix:Remove last saved library id when changing servers so home page shelves load on init 2022-06-05 16:32:28 -05:00
advplyr d7dcaa22a6 Fix:Logout button on account page #222 2022-06-05 16:01:05 -05:00
advplyr cc0b2943dd Fix:Hide play button for local podcast 2022-06-05 15:56:23 -05:00
advplyr 50ee0b2265 Fix:Local downloaded books crash when server progress check fails, fix check pause timeout length #219 2022-06-05 15:52:54 -05:00
advplyr 910b3a2a17 Version bump 0.9.48-beta 2022-06-05 09:55:22 -05:00
advplyr 916da91ccb Fix:Android auto check if server connection available and use first available server config, add ping api check with 3 second timeout #212 #167 2022-06-05 09:51:37 -05:00
advplyr d70a99254a Android version code bump 2022-06-04 17:20:14 -05:00
advplyr dcf5bb61a2 Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2022-06-04 17:12:14 -05:00
advplyr aa65bb10c1 Update:Android auto reset saved data if server config id changes 2022-06-04 17:12:10 -05:00
advplyr 158448f8a2 iOS version bump 0.9.47 2022-06-04 16:37:26 -05:00
advplyr f4be9b3e26 Update:Pass device info with play request 2022-06-04 16:36:49 -05:00
advplyr 15d68ca285 Version bump 0.9.47-beta 2022-06-04 15:11:49 -05:00
advplyr ad12e6a19d Add:Author, narrator and series links on item page #186 2022-06-04 10:19:31 -05:00
advplyr 1f1b2fe85a Add:Filter for podcast episodes 2022-06-03 19:46:43 -05:00
advplyr 480df58ce4 Fix:Check with server after pause of 1 minute or longer for updated media progress & show toast on client if progress sync is failing 2022-06-03 18:58:07 -05:00
advplyr 2decf532b2 Add:Not Finished progress filter 2022-06-02 18:20:52 -05:00
advplyr 3ba87419ae Update:Hide size on item landing page if not set 2022-06-02 18:14:29 -05:00
advplyr 1c78af37fa Update:Item page redesign with more details like narrator and chapters, decreasing cover size 2022-06-02 17:57:37 -05:00
advplyr 2b5373aedd Update:Rename Hard Delete 2022-06-02 17:02:30 -05:00
advplyr 99bf960b8a Add:Filter and sort for podcast episodes table, Update:Sync local media progress when media progress is updated on the server 2022-06-01 19:38:26 -05:00
advplyr c4aca22c28 Fix:Android auto reset server items if no longer connected to server #201 2022-05-29 18:43:53 -05:00
advplyr 58bd0e0cee Add:Click and drag player progress track #110 2022-05-29 18:13:25 -05:00
advplyr c1c56f8f52 Fix play request payload 2022-05-27 20:48:13 -05:00
advplyr f930ba1941 Update:Send android device info when opening playback sessions 2022-05-26 19:10:29 -05:00
advplyr 251116a5ce Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2022-05-24 09:08:28 -05:00
advplyr 06739c0401 Fix:Base64 encoding include URL_SAFE flag to prevent having / in local library item ids #192 2022-05-24 09:08:23 -05:00
advplyr 92f069d1e6 iOS version bump 0.9.46 2022-05-22 17:51:35 -05:00
advplyr 62c5042ecc Fix:Cover image stretch to fill 2022-05-22 17:49:10 -05:00
advplyr 604b086c0b Android version bump 0.9.46-beta 2022-05-22 17:04:19 -05:00
advplyr 0e0b356f6b Fix:Reset volume when playing from notification after sleep timer decreases volume #182 2022-05-22 16:24:38 -05:00
advplyr 573768e2b2 Fix:Select podcast folder when none is set yet 2022-05-22 15:56:32 -05:00
advplyr 236fd09c94 Fix:Hide download button if user does not have download permission #189 2022-05-22 15:49:42 -05:00
advplyr cb6cb5f637 Fix:M4B downloads on Android 9 & 10 not detected as audio files (mimeType is showing as application/octet-stream) #154 2022-05-22 15:31:25 -05:00
advplyr eb916a3ab0 Add:Duration sort 2022-05-22 14:47:07 -05:00
advplyr 61b8ca1510 Add:Persist scroll position for bookshelves #196, Fix:Hide clear filter button for series and collections 2022-05-22 14:19:13 -05:00
advplyr 5d2da97dc5 Update:Android auto to show libraries and browseable podcasts, Fix:Download podcast set episodes and cover art correctly 2022-05-22 13:40:01 -05:00
advplyr d626686614 Add start of downloading library item on iOS 2022-05-14 11:50:53 -05:00
advplyr 60ee33cb72 Update:Show download button on iOS and call downloadLibraryItem without a folderId 2022-05-14 09:08:52 -05:00
advplyr 8d2498e96d Merge branch 'master' of https://github.com/advplyr/audiobookshelf-app 2022-05-13 16:27:10 -05:00
advplyr 1794484bed Fix:Realtime updates for book cards when series sequence changes 2022-05-13 16:27:05 -05:00
advplyr f2b6331843 Add iOS AbsDownloader and AbsFileSystem plugins 2022-05-13 15:18:21 -05:00
advplyr 8d5f33245f Version bump 0.9.45 2022-05-13 13:53:37 -05:00
advplyr 1ee544b842 Fix:LazyBookshelf default to square cover before server settings are loaded 2022-05-13 13:42:38 -05:00
advplyr 0fd9463c7c Update:Set installLocation in android manifest to preferExternal for amazon app store 2022-05-13 11:04:52 -05:00
advplyr ad5edf3aee Fix:iOS audio player cutting off timestamps #173 2022-05-13 09:28:43 -05:00
advplyr d5fafd8cab Fix:Bookshelf list view wrong entity height #179 2022-05-12 18:24:36 -05:00
advplyr 92256f2fed Fix:Create bookmark was requiring a socket connection still 2022-05-11 18:59:24 -05:00
advplyr 5fa8d4c989 Fix:Check local library item is linked to a server item before sending playback session progress sync to server 2022-05-11 17:05:51 -05:00
advplyr 6b75f79f00 Fix wrong android version in comment 2022-05-08 15:01:36 -05:00
advplyr b0c9f29d90 Fix:Downloaded covers when used in media notification crashing android 11 #35 #164 2022-05-08 14:34:28 -05:00
advplyr 7721afc116 Remove unnecessary background-image 2022-05-07 20:19:49 -05:00
advplyr bc85e9bbfa Merge 2022-05-07 17:33:07 -05:00
advplyr e8b6602fe5 Remove app update package 2022-05-07 17:32:18 -05:00
558 changed files with 63945 additions and 19463 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.0'
- 'iOS App - 0.14.0'
- 'Android App - 0.13.0'
- 'iOS App - 0.13.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.0'
- 'iOS App - 0.14.0'
- 'Android App - 0.13.0'
- 'iOS App - 0.13.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. -->
+81
View File
@@ -0,0 +1,81 @@
<!DOCTYPE html>
<html lang=en>
<head>
<meta http-equiv=content-type content="text/html; charset=utf-8" />
<meta name=viewport content="width=device-width, initial-scale=1">
<title>Audiobookshelf :: Testers</title>
<style>
body {
background-color: rgb(55, 56, 56);
font-family: Sans;
margin: 40px auto;
max-width: 500px;
color: #ddd;
text-align: center;
padding: 5px;
}
img {
width: 100px;
}
h1 {
font-weight: 100;
}
a {
color: #fff;
}
#apk {
display: grid;
grid-template-columns: auto auto;
justify-content: center;
column-gap: 15px;
text-align: left;
font-family: monospace;
background-color: #222;
padding: 20px;
border-radius: 5px;
}
</style>
</head>
<body>
<a href="https://audiobookshelf.org">
<img src=logo.png />
</a>
<h1>Audiobookshelf :: Testers</h1>
<p>
Here you can download the latest version of Audiobookshelf for Android for <b>testing</b>.
The app is automatically built for every new patch added to the project.
</p>
<div id=apk>
<div>Built on</div>
<div>__DATE__</div>
<div>Git commit</div>
<a href="https://github.com/advplyr/audiobookshelf-app/commit/__COMMIT__">__COMMIT__</a>
<div>Download</div>
<a href="__APK__">__APK__</a>
</div>
<p>
Using this requires some technical knowledge to install the APK manually.
Regular users should use the releases published on the <a href="https://play.google.com/store/apps/details?id=com.audiobookshelf.app">Play Store</a>.
</p>
<hr />
<p>Report bugs, request features, provide feedback, and contribute on <a href="https://github.com/advplyr/audiobookshelf-app/issues">GitHub</a></p>
<p>
<a href="https://github.com/advplyr/audiobookshelf-app/issues">
<svg xmlns="http://www.w3.org/2000/svg" fill="currentColor" width=32 height=32 viewBox="0 0 24 24">
<path
d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"
/>
</svg>
</a>
</div>
</body>
</html>
+53
View File
@@ -0,0 +1,53 @@
name: Build APK
on:
push:
paths-ignore:
- 'ios/**'
- 'readme.md'
pull_request:
paths-ignore:
- 'ios/**'
- 'readme.md'
jobs:
main:
runs-on: ubuntu-latest
steps:
- name: checkout sources
uses: actions/checkout@v3
- name: use Node.js 20.x
uses: actions/setup-node@v3
with:
node-version: 20
- name: Set up Java
uses: actions/setup-java@v2
with:
distribution: 'temurin'
java-version: 21
- name: install dependencies
run: npm ci --omit=optional
- name: build Nuxt project
run: npm run generate
- name: copy to Android project
run: npx cap sync
- name: build Android app
run: ./android/gradlew assembleDebug -p android --no-daemon
- name: rename apk
working-directory: android/app/build/outputs/apk/debug/
run: |
build="$(date +%Y%m%d-%H%M%S)-$(git rev-parse --short HEAD)"
name="audiobookshelf-${build}.apk"
mv -v app-debug.apk "${name}"
- name: upload app
uses: actions/upload-artifact@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"
});
}
+82
View File
@@ -0,0 +1,82 @@
name: Publish Test App
on:
push:
branches:
- master
paths-ignore:
- 'ios/**'
- 'readme.md'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: checkout sources
uses: actions/checkout@v3
- name: use Node.js 20.x
uses: actions/setup-node@v3
with:
node-version: 20
- name: Set up Java
uses: actions/setup-java@v2
with:
distribution: 'temurin'
java-version: 21
- name: install dependencies
run: npm ci --omit=optional
- name: build Nuxt project
run: npm run generate
- name: copy to Android project
run: npx cap sync
- name: build Android app
run: ./android/gradlew assembleDebug -p android --no-daemon
- name: rename apk
working-directory: android/app/build/outputs/apk/debug/
run: |
build="$(date +%Y%m%d-%H%M%S)-$(git rev-parse --short HEAD)"
name="audiobookshelf-${build}.apk"
mv -v app-debug.apk "${name}"
- name: prepare test page ressources
run: |
mkdir ghpages
cp android/app/build/outputs/apk/debug/*apk ghpages/
cp static/Logo.png ghpages/logo.png
cp .github/testing-page-template.html ghpages/index.html
- name: build test page
working-directory: ghpages
run: |
sed -i "s/__DATE__/$(date)/g" index.html
sed -i "s/__COMMIT__/$(git rev-parse --short HEAD)/g" index.html
sed -i "s/__APK__/$(ls *apk)/g" index.html
- name: upload test page artifact
uses: actions/upload-pages-artifact@v3
with:
path: ./ghpages
deploy:
needs: build
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@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
+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"
}
+45 -38
View File
@@ -18,71 +18,82 @@ 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 {
kotlinOptions {
freeCompilerArgs = ['-Xjvm-default=all']
namespace 'com.audiobookshelf.app'
buildFeatures {
viewBinding true
buildConfig true
}
kotlinOptions {
freeCompilerArgs = ['-Xjvm-default=all']
}
compileSdk rootProject.ext.compileSdkVersion
defaultConfig {
applicationId "com.audiobookshelf.app"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 120
versionName "0.14.1-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 73
versionName "0.9.44-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 {
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")
// failOnVersionConflict()
}
}
dependencies {
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"
// testImplementation "junit:junit:$junitVersion"
// androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
// androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
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"
@@ -105,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:0.13.0"
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.1'
// FFMPEG-Kit
implementation 'com.arthenica:ffmpeg-kit-min:4.5.1'
implementation 'com.fasterxml.jackson.module:jackson-module-kotlin:2.12.2'
}
apply from: 'capacitor.build.gradle'
+8 -4
View File
@@ -2,20 +2,24 @@
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
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')
implementation project(':capacitor-network')
implementation project(':capacitor-preferences')
implementation project(':capacitor-status-bar')
implementation project(':capacitor-storage')
implementation project(':robingenz-capacitor-app-update')
}
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@mipmap/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 879 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -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>
+88 -50
View File
@@ -1,82 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:dist="http://schemas.android.com/apk/distribution"
xmlns:tools="http://schemas.android.com/tools"
package="com.audiobookshelf.app">
xmlns:dist="http://schemas.android.com/apk/distribution"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="preferExternal" >
<!-- 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.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:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config"
android:requestLegacyExternalStorage="true">
android:largeHeap="true" >
<!-- Android app widget -->
<receiver
android:name="MediaPlayerWidget"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<!--Used by Android Auto-->
<meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
android:resource="@drawable/icon" />
<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 -->
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc"/>
<!-- Support for Cast -->
<meta-data android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.audiobookshelf.app.CastOptionsProvider"/>
android:name="com.google.android.gms.car.notification.SmallIcon"
android:resource="@drawable/icon" />
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc" /> <!-- Support for Cast -->
<meta-data
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.audiobookshelf.app.CastOptionsProvider" />
<activity
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
android:name="com.audiobookshelf.app.MainActivity"
android:label="@string/title_activity_main"
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
android:exported="true"
android:theme="@style/AppTheme.NoActionBarLaunch"
android:launchMode="singleTask">
android:label="@string/title_activity_main"
android:launchMode="singleTask"
android:theme="@style/AppTheme.NoActionBarLaunch" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<intent-filter>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<!-- Register URL scheme -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="@string/custom_url_scheme" />
</intent-filter>
<!-- Register URL scheme -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="@string/custom_url_scheme" />
</intent-filter>
</activity>
<receiver android:name="androidx.media.session.MediaButtonReceiver" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<receiver android:name="androidx.media.session.MediaButtonReceiver" >
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<service
android:name=".player.PlayerNotificationService"
android:enabled="true"
android:exported="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<service
android:exported="true"
android:enabled="true"
android:name=".player.PlayerNotificationService">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService"/>
</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"
android:grantUriPermissions="true"
android:exported="false">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
@@ -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,28 @@
[
{
"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"
},
{
"pkg": "@capacitor/dialog",
"classpath": "com.capacitorjs.plugins.dialog.DialogPlugin"
@@ -15,16 +35,12 @@
"pkg": "@capacitor/network",
"classpath": "com.capacitorjs.plugins.network.NetworkPlugin"
},
{
"pkg": "@capacitor/preferences",
"classpath": "com.capacitorjs.plugins.preferences.PreferencesPlugin"
},
{
"pkg": "@capacitor/status-bar",
"classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin"
},
{
"pkg": "@capacitor/storage",
"classpath": "com.capacitorjs.plugins.storage.StoragePlugin"
},
{
"pkg": "@robingenz/capacitor-app-update",
"classpath": "dev.robingenz.capacitor.appupdate.AppUpdatePlugin"
}
]
@@ -1,21 +1,30 @@
package com.audiobookshelf.app
import android.Manifest
import android.app.DownloadManager
import android.content.*
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.content.pm.PackageManager
import android.os.*
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.data.AbsDatabase
import com.audiobookshelf.app.managers.DbManager
import com.audiobookshelf.app.player.PlayerNotificationService
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
import io.paperdb.Paper
class MainActivity : BridgeActivity() {
@@ -31,45 +40,76 @@ 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())
super.onCreate(savedInstanceState)
Log.d(tag, "onCreate")
// Grant full storage access for testing
// var ss = SimpleStorage(this)
// ss.requestFullStorageAccess()
var permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
if (permission != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
PERMISSIONS_ALL,
REQUEST_PERMISSIONS)
}
DbManager.initialize(applicationContext)
registerPlugin(AbsAudioPlayer::class.java)
registerPlugin(AbsDownloader::class.java)
registerPlugin(AbsFileSystem::class.java)
registerPlugin(AbsDatabase::class.java)
registerPlugin(AbsLogger::class.java)
Paper.init(applicationContext)
super.onCreate(savedInstanceState)
Log.d(tag, "onCreate")
// Update the margins to handle edge-to-edge enforced in SDK 35
// See: https://developer.android.com/develop/ui/views/layout/edge-to-edge
val webView: WebView = findViewById(R.id.webview)
webView.setOnApplyWindowInsetsListener { v, insets ->
val (left, top, right, bottom) = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
val sysInsets = insets.getInsets(WindowInsets.Type.systemBars())
Log.d(tag, "safe sysInsets: $sysInsets")
arrayOf(sysInsets.left, sysInsets.top, sysInsets.right, sysInsets.bottom)
} else {
arrayOf(
insets.systemWindowInsetLeft,
insets.systemWindowInsetTop,
insets.systemWindowInsetRight,
insets.systemWindowInsetBottom
)
}
// Inject as CSS variables
// NOTE: Possibly able to use in the future to support edge-to-edge better.
val js = """
document.documentElement.style.setProperty('--safe-area-inset-top', '${top}px');
document.documentElement.style.setProperty('--safe-area-inset-bottom', '${bottom}px');
document.documentElement.style.setProperty('--safe-area-inset-left', '${left}px');
document.documentElement.style.setProperty('--safe-area-inset-right', '${right}px');
""".trimIndent()
webView.evaluateJavascript(js, null)
// Set margins
v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
leftMargin = left
bottomMargin = bottom
rightMargin = right
topMargin = top
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
WindowInsets.CONSUMED
} else {
insets
}
}
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)
}
}
override fun onDestroy() {
@@ -94,9 +134,7 @@ class MainActivity : BridgeActivity() {
foregroundService = mLocalBinder.getService()
// Let NativeAudio know foreground service is ready and setup event listener
if (pluginCallback != null) {
pluginCallback()
}
pluginCallback()
}
}
@@ -106,6 +144,9 @@ class MainActivity : BridgeActivity() {
}
}
fun isPlayerNotificationServiceInitialized():Boolean {
return ::foregroundService.isInitialized
}
fun stopMyService() {
if (mBounded) {
@@ -119,6 +160,7 @@ class MainActivity : BridgeActivity() {
override fun onSaveInstanceState(outState: Bundle) {
storageHelper.onSaveInstanceState(outState)
super.onSaveInstanceState(outState)
outState.clear()
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
@@ -140,9 +182,4 @@ class MainActivity : BridgeActivity() {
// Mandatory for Activity, but not for Fragment & ComponentActivity
storageHelper.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
// override fun onUserInteraction() {
// super.onUserInteraction()
// Log.d(tag, "USER INTERACTION")
// }
}
@@ -0,0 +1,103 @@
package com.audiobookshelf.app
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import android.view.View
import android.widget.RemoteViews
import androidx.media.session.MediaButtonReceiver
import com.audiobookshelf.app.data.PlaybackSession
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.managers.DbManager
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.AppWidgetTarget
import com.bumptech.glide.request.transition.Transition
/**
* Implementation of App Widget functionality.
*/
class MediaPlayerWidget : AppWidgetProvider() {
val tag = "MediaPlayerWidget"
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
Log.d(tag, "onUpdate $appWidgetIds")
}
override fun onEnabled(context: Context) {
Log.i(tag, "onEnabled check context ${context.packageName}")
DbManager.initialize(context)
DeviceManager.deviceData.lastPlaybackSession?.let {
val appWidgetManager = AppWidgetManager.getInstance(context)
val componentName = ComponentName(context, MediaPlayerWidget::class.java)
val ids = appWidgetManager.getAppWidgetIds(componentName)
Log.d(tag, "Setting initial widget state with last playback session ${it.displayTitle}")
for (widgetId in ids) {
updateAppWidget(context, appWidgetManager, widgetId, it, false, true)
}
}
// Enter relevant functionality for when the first widget is created
DeviceManager.initializeWidgetUpdater(context)
}
}
internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, playbackSession: PlaybackSession?, isPlaying:Boolean, isAppClosed:Boolean) {
val tag = "MediaPlayerWidget"
val views = RemoteViews(context.packageName, R.layout.media_player_widget)
Log.i(tag, "updateAppWidget ${playbackSession?.displayTitle ?: "No Title"} isPlaying=$isPlaying isAppClosed=$isAppClosed")
val wholeWidgetClickI = Intent(context, MainActivity::class.java)
wholeWidgetClickI.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
val wholeWidgetClickPI = PendingIntent.getActivity(
context,
System.currentTimeMillis().toInt(),
wholeWidgetClickI,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
val playPausePI = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_PLAY_PAUSE)
views.setOnClickPendingIntent(R.id.widgetPlayPauseButton, playPausePI)
val fastForwardPI = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_FAST_FORWARD)
views.setOnClickPendingIntent(R.id.widgetFastForwardButton, fastForwardPI)
val rewindPI = MediaButtonReceiver.buildMediaButtonPendingIntent(context, PlaybackStateCompat.ACTION_REWIND)
views.setOnClickPendingIntent(R.id.widgetRewindButton, rewindPI)
// Show/Hide button container
views.setViewVisibility(R.id.widgetButtonContainer, if (isAppClosed) View.GONE else View.VISIBLE)
views.setOnClickPendingIntent(R.id.widgetBackground, wholeWidgetClickPI)
val imageUri = playbackSession?.getCoverUri(context) ?: Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)
val awt: AppWidgetTarget = object : AppWidgetTarget(context.applicationContext, R.id.widgetAlbumArt, views, appWidgetId) {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
super.onResourceReady(resource, transition)
}
}
val artist = playbackSession?.displayAuthor ?: "Unknown"
views.setTextViewText(R.id.widgetArtistText, artist)
val title = playbackSession?.displayTitle ?: "Unknown"
views.setTextViewText(R.id.widgetMediaTitle, title)
val options = RequestOptions().override(300, 300).placeholder(R.drawable.icon).error(R.drawable.icon)
Glide.with(context.applicationContext).asBitmap().load(imageUri).apply(options).into(awt)
Log.i(tag, "Update App Widget | Is Playing=$isPlaying | isAppClosed=$isAppClosed")
val playPauseResource = if (isPlaying) 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
appWidgetManager.updateAppWidget(appWidgetId, views)
}
@@ -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() }
}
}
@@ -0,0 +1,38 @@
package com.audiobookshelf.app.data
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class AudioTrack(
var index: Int,
var startOffset: Double,
var duration: Double,
var title: String,
var contentUrl: String,
var mimeType: String?,
var metadata: FileMetadata?,
var isLocal: Boolean,
var localFileId: String?,
// 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()
@get:JsonIgnore
val durationMs
get() = (duration * 1000L).toLong()
@get:JsonIgnore
val endOffsetMs
get() = startOffsetMs + durationMs
@get:JsonIgnore
val relPath
get() = metadata?.relPath ?: ""
@JsonIgnore
fun getBookChapter(): BookChapter {
return BookChapter(index + 1, startOffset, startOffset + duration, title)
}
}
@@ -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,61 +1,15 @@
package com.audiobookshelf.app.data
import android.net.Uri
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 com.audiobookshelf.app.R
import com.audiobookshelf.app.device.DeviceManager
import androidx.media.utils.MediaConstants
import com.audiobookshelf.app.media.MediaManager
import com.fasterxml.jackson.annotation.*
@JsonIgnoreProperties(ignoreUnknown = true)
data class LibraryItem(
var id:String,
var ino:String,
var libraryId:String,
var folderId:String,
var path:String,
var relPath:String,
var mtimeMs:Long,
var ctimeMs:Long,
var birthtimeMs:Long,
var addedAt:Long,
var updatedAt:Long,
var lastScan:Long?,
var scanVersion:String?,
var isMissing:Boolean,
var isInvalid:Boolean,
var mediaType:String,
var media:MediaType,
var libraryFiles:MutableList<LibraryFile>?,
var userMediaProgress:MediaProgress? // Only included when requesting library item with progress (for downloads)
) : LibraryItemWrapper() {
@get:JsonIgnore
val title get() = media.metadata.title
@get:JsonIgnore
val authorName get() = media.metadata.getAuthorDisplayName()
@JsonIgnore
fun getCoverUri():Uri {
if (media.coverPath == null) {
return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
}
return Uri.parse("${DeviceManager.serverAddress}/api/items/$id/cover?token=${DeviceManager.token}")
}
@JsonIgnore
fun getMediaMetadata(): MediaMetadataCompat {
return MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, authorName)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, getCoverUri().toString())
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getCoverUri().toString())
putString(MediaMetadataCompat.METADATA_KEY_ART_URI, getCoverUri().toString())
putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, authorName)
}.build()
}
}
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)
@@ -73,7 +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)
@@ -82,7 +38,8 @@ class Podcast(
coverPath:String?,
var tags:MutableList<String>,
var episodes:MutableList<PodcastEpisode>?,
var autoDownloadEpisodes:Boolean
var autoDownloadEpisodes:Boolean,
var numEpisodes:Int?
) : MediaType(metadata, coverPath) {
@JsonIgnore
override fun getAudioTracks():List<AudioTrack> {
@@ -99,7 +56,8 @@ class Podcast(
// Add new episodes
audioTracks.forEach { at ->
if (episodes?.find{ it.audioTrack?.localFileId == at.localFileId } == null) {
val newEpisode = PodcastEpisode("local_" + at.localFileId,episodes?.size ?: 0 + 1,null,null,at.title,null,null,null,at,at.duration,0, null)
val localEpisodeId = "local_ep_" + at.localFileId
val newEpisode = PodcastEpisode(localEpisodeId,(episodes?.size ?: 0) + 1,null,null,at.title,null,null,null, null, null, at,null,at.duration,0, null, localEpisodeId)
episodes?.add(newEpisode)
}
}
@@ -112,7 +70,8 @@ class Podcast(
}
@JsonIgnore
override fun addAudioTrack(audioTrack:AudioTrack) {
val newEpisode = PodcastEpisode("local_" + audioTrack.localFileId,episodes?.size ?: 0 + 1,null,null,audioTrack.title,null,null,null,audioTrack,audioTrack.duration,0, null)
val localEpisodeId = "local_ep_" + audioTrack.localFileId
val newEpisode = PodcastEpisode(localEpisodeId,(episodes?.size ?: 0) + 1,null,null,audioTrack.title,null,null,null, null, null,audioTrack,null,audioTrack.duration,0, null, localEpisodeId)
episodes?.add(newEpisode)
var index = 1
@@ -131,9 +90,22 @@ class Podcast(
index++
}
}
// Used for FolderScanner local podcast item to get copy of Podcast excluding episodes
@JsonIgnore
override fun getLocalCopy(): Podcast {
return Podcast(metadata as PodcastMetadata,coverPath,tags, mutableListOf(),autoDownloadEpisodes, 0)
}
@JsonIgnore
override fun checkHasTracks():Boolean {
return (episodes?.size ?: numEpisodes ?: 0) > 0
}
@JsonIgnore
fun addEpisode(audioTrack:AudioTrack, episode:PodcastEpisode):PodcastEpisode {
val newEpisode = PodcastEpisode("local_" + episode.id,episodes?.size ?: 0 + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,audioTrack,audioTrack.duration,0, episode.id)
val localEpisodeId = "local_ep_" + episode.id
val newEpisode = PodcastEpisode(localEpisodeId,(episodes?.size ?: 0) + 1,episode.episode,episode.episodeType,episode.title,episode.subtitle,episode.description,null,null,null,audioTrack,episode.chapters,audioTrack.duration,episode.size, episode.id, localEpisodeId)
episodes?.add(newEpisode)
var index = 1
@@ -144,10 +116,14 @@ class Podcast(
return newEpisode
}
// Used for FolderScanner local podcast item to get copy of Podcast excluding episodes
@JsonIgnore
override fun getLocalCopy(): Podcast {
return Podcast(metadata as PodcastMetadata,coverPath,tags, mutableListOf(),autoDownloadEpisodes)
fun getNextUnfinishedEpisode(libraryItemId:String, mediaManager: MediaManager):PodcastEpisode? {
val sortedEpisodes = episodes?.sortedByDescending { it.publishedAt }
val podcastEpisode = sortedEpisodes?.find { episode ->
val progress = mediaManager.serverUserMediaProgress.find { it.libraryItemId == libraryItemId && it.episodeId == episode.id }
progress == null || !progress.isFinished
}
return podcastEpisode
}
}
@@ -159,8 +135,10 @@ class Book(
var audioFiles:List<AudioFile>?,
var chapters:List<BookChapter>?,
var tracks:MutableList<AudioTrack>?,
var ebookFile: EBookFile?,
var size:Long?,
var duration:Double?
var duration:Double?,
var numTracks:Int?
) : MediaType(metadata, coverPath) {
@JsonIgnore
override fun getAudioTracks():List<AudioTrack> {
@@ -170,10 +148,13 @@ class Book(
override fun setAudioTracks(audioTracks:MutableList<AudioTrack>) {
tracks = audioTracks
tracks?.sortBy { it.index }
// TODO: Is it necessary to calculate this each time? check if can remove safely
var totalDuration = 0.0
var currentStartOffset = 0.0
tracks?.forEach {
totalDuration += it.duration
it.startOffset = currentStartOffset
currentStartOffset += it.duration
}
duration = totalDuration
}
@@ -206,10 +187,14 @@ class Book(
}
duration = totalDuration
}
@JsonIgnore
override fun getLocalCopy(): Book {
return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(),null,null)
return Book(metadata as BookMetadata,coverPath,tags, mutableListOf(),chapters,mutableListOf(), ebookFile, null,null, 0)
}
@JsonIgnore
override fun checkHasTracks():Boolean {
return (tracks?.size ?: numTracks ?: 0) > 0
}
}
@@ -219,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" }
}
@@ -238,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" }
}
@@ -254,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" }
}
@@ -276,12 +264,78 @@ data class PodcastEpisode(
var title:String?,
var subtitle:String?,
var description:String?,
var pubDate:String?,
var publishedAt:Long?,
var audioFile:AudioFile?,
var audioTrack:AudioTrack?,
var chapters:List<BookChapter>?,
var duration:Double?,
var size:Long?,
var serverEpisodeId:String? // For local podcasts to match with server podcasts
)
var serverEpisodeId:String?, // For local podcasts to match with server podcasts
var localEpisodeId:String? // For Android Auto server episodes with local copy
) {
@JsonIgnore
fun getMediaDescription(libraryItem:LibraryItemWrapper, progress:MediaProgressWrapper?, ctx: Context): MediaDescriptionCompat {
val coverUri = if (libraryItem is LocalLibraryItem) {
libraryItem.getCoverUri(ctx)
} else {
(libraryItem as LibraryItem).getCoverUri()
}
val extras = Bundle()
if (localEpisodeId != 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 {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
)
}
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(subtitle)
.setExtras(extras)
libraryItemDescription.iconBitmap?.let {
mediaDescriptionBuilder.setIconBitmap(it)
}
return mediaDescriptionBuilder.build()
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class LibraryFile(
@@ -311,7 +365,37 @@ 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(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, 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)
@@ -320,54 +404,111 @@ data class Folder(
var fullPath:String
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class AudioTrack(
var index:Int,
var startOffset:Double,
var duration:Double,
var title:String,
var contentUrl:String,
var mimeType:String,
var metadata:FileMetadata?,
var isLocal:Boolean,
var localFileId:String?,
var audioProbeResult:AudioProbeResult?,
var serverIndex:Int? // Need to know if server track index is different
) {
@get:JsonIgnore
val startOffsetMs get() = (startOffset * 1000L).toLong()
@get:JsonIgnore
val durationMs get() = (duration * 1000L).toLong()
@get:JsonIgnore
val endOffsetMs get() = startOffsetMs + durationMs
@get:JsonIgnore
val relPath get() = metadata?.relPath ?: ""
@JsonIgnore
fun getBookChapter():BookChapter {
return BookChapter(index + 1,startOffset, startOffset + duration, title)
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class BookChapter(
var id:Int,
var start:Double,
var end:Double,
var title:String?
) {
@get:JsonIgnore
val startMs get() = (start * 1000L).toLong()
@get:JsonIgnore
val endMs get() = (end * 1000L).toLong()
}
@JsonTypeInfo(use= JsonTypeInfo.Id.DEDUCTION, defaultImpl = MediaProgress::class)
@JsonSubTypes(
JsonSubTypes.Type(MediaProgress::class),
JsonSubTypes.Type(LocalMediaProgress::class)
)
open class MediaProgressWrapper(var isFinished:Boolean, var currentTime:Double, var progress:Double) {
open val mediaItemId get() = ""
}
// Helper class
data class LibraryItemWithEpisode(
var libraryItemWrapper:LibraryItemWrapper,
var episode:PodcastEpisode
)
@JsonIgnoreProperties(ignoreUnknown = true)
data class MediaProgress(
var id:String,
var libraryItemId:String,
var episodeId:String?,
var duration:Double, // seconds
var progress:Double, // 0 to 1
var currentTime:Double,
var isFinished:Boolean,
var lastUpdate:Long,
var startedAt:Long,
var finishedAt:Long?
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,215 +0,0 @@
package com.audiobookshelf.app.data
import android.util.Log
import com.audiobookshelf.app.plugins.AbsDownloader
import io.paperdb.Paper
import org.json.JSONObject
import java.io.File
class DbManager {
val tag = "DbManager"
fun getDeviceData(): DeviceData {
return Paper.book("device").read("data") ?: DeviceData(mutableListOf(), null, null)
}
fun saveDeviceData(deviceData:DeviceData) {
Paper.book("device").write("data", deviceData)
}
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)) {
localLibraryItems.add(localLibraryItem)
}
}
return localLibraryItems
}
fun getLocalLibraryItemsInFolder(folderId:String):List<LocalLibraryItem> {
val localLibraryItems = getLocalLibraryItems()
return localLibraryItems.filter {
it.folderId == folderId
}
}
fun getLocalLibraryItemByLLId(libraryItemId:String):LocalLibraryItem? {
return getLocalLibraryItems().find { it.libraryItemId == libraryItemId }
}
fun getLocalLibraryItem(localLibraryItemId:String):LocalLibraryItem? {
return Paper.book("localLibraryItems").read(localLibraryItemId)
}
fun removeLocalLibraryItem(localLibraryItemId:String) {
Paper.book("localLibraryItems").delete(localLibraryItemId)
}
fun saveLocalLibraryItems(localLibraryItems:List<LocalLibraryItem>) {
localLibraryItems.map {
Paper.book("localLibraryItems").write(it.id, it)
}
}
fun saveLocalLibraryItem(localLibraryItem:LocalLibraryItem) {
Paper.book("localLibraryItems").write(localLibraryItem.id, localLibraryItem)
}
fun saveLocalFolder(localFolder:LocalFolder) {
Paper.book("localFolders").write(localFolder.id,localFolder)
}
fun getLocalFolder(folderId:String):LocalFolder? {
return Paper.book("localFolders").read(folderId)
}
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)
}
}
return localFolders
}
fun removeLocalFolder(folderId:String) {
val localLibraryItems = getLocalLibraryItemsInFolder(folderId)
localLibraryItems.forEach {
Paper.book("localLibraryItems").delete(it.id)
}
Paper.book("localFolders").delete(folderId)
}
fun saveDownloadItem(downloadItem: AbsDownloader.DownloadItem) {
Paper.book("downloadItems").write(downloadItem.id, downloadItem)
}
fun removeDownloadItem(downloadItemId:String) {
Paper.book("downloadItems").delete(downloadItemId)
}
fun getDownloadItems():List<AbsDownloader.DownloadItem> {
val downloadItems:MutableList<AbsDownloader.DownloadItem> = mutableListOf()
Paper.book("downloadItems").allKeys.forEach { downloadItemId ->
Paper.book("downloadItems").read<AbsDownloader.DownloadItem>(downloadItemId)?.let {
downloadItems.add(it)
}
}
return downloadItems
}
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()
Paper.book("localMediaProgress").allKeys.forEach { localMediaProgressId ->
Paper.book("localMediaProgress").read<LocalMediaProgress>(localMediaProgressId)?.let {
mediaProgress.add(it)
}
}
return mediaProgress
}
fun removeLocalMediaProgress(localMediaProgressId:String) {
Paper.book("localMediaProgress").delete(localMediaProgressId)
}
fun removeAllLocalMediaProgress() {
Paper.book("localMediaProgress").destroy()
}
// Make sure all local file ids still exist
fun cleanLocalLibraryItems() {
val localLibraryItems = getLocalLibraryItems()
localLibraryItems.forEach { lli ->
var hasUpates = 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}")
hasUpates = true
}
file.exists()
} as MutableList<LocalFile>
// Check audio tracks and episodes
if (lli.isPodcast) {
val podcast = lli.media as Podcast
podcast.episodes = podcast.episodes?.filter { ep ->
if (lli.localFiles.find { lf -> lf.id == ep.audioTrack?.localFileId } == null) {
Log.d(tag, "cleanLocalLibraryItems: Podcast episode ${ep.title} was removed from library item ${lli.media.metadata.title}")
hasUpates = 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}")
hasUpates = 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}")
lli.coverAbsolutePath = null
lli.coverContentUrl = null
hasUpates = true
}
}
if (hasUpates) {
Log.d(tag, "cleanLocalLibraryItems: Saving local library item ${lli.id}")
Paper.book("localLibraryItems").write(lli.id, lli)
}
}
}
// Remove any local media progress where the local media item is not found
fun cleanLocalMediaProgress() {
val localMediaProgress = getAllLocalMediaProgress()
val localLibraryItems = getLocalLibraryItems()
localMediaProgress.forEach {
val matchingLLI = localLibraryItems.find { lli -> lli.id == it.localLibraryItemId }
if (matchingLLI == null) {
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()) {
Log.d(tag, "cleanLocalMediaProgress: Podcast media progress has no episode id - removing")
Paper.book("localMediaProgress").delete(it.id)
} else {
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")
Paper.book("localMediaProgress").delete(it.id)
}
}
}
}
}
fun saveLocalPlaybackSession(playbackSession:PlaybackSession) {
Paper.book("localPlaybackSession").write(playbackSession.id,playbackSession)
}
fun getLocalPlaybackSession(playbackSessionId:String):PlaybackSession? {
return Paper.book("localPlaybackSession").read(playbackSessionId)
}
}
@@ -1,66 +1,254 @@
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.util.*
import java.io.File
data class ServerConnectionConfig(
var id:String,
var index:Int,
var name:String,
var address:String,
var userId:String,
var username:String,
var token:String
)
enum class LockOrientationSetting {
NONE,
PORTRAIT,
LANDSCAPE
}
data class DeviceData(
var serverConnectionConfigs:MutableList<ServerConnectionConfig>,
var lastServerConnectionConfigId:String?,
var currentLocalPlaybackSession:PlaybackSession? // Stored to open up where left off for local media
) {
@JsonIgnore
fun getLastServerConnectionConfig():ServerConnectionConfig? {
return lastServerConnectionConfigId?.let { lsccid ->
return serverConnectionConfigs.find { it.id == lsccid }
}
}
enum class HapticFeedbackSetting {
OFF,
LIGHT,
MEDIUM,
HEAVY
}
enum class ShakeSensitivitySetting {
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,
// 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 {
return getEBookFormat() != null
}
@JsonIgnore
fun getEBookFormat(): String? {
if (mimeType == "application/epub+zip") return "epub"
if (mimeType == "application/pdf") return "pdf"
if (mimeType == "application/x-mobipocket-ebook") return "mobi"
if (mimeType == "application/vnd.comicbook+zip") return "cbz"
if (mimeType == "application/vnd.comicbook-rar") return "cbr"
if (mimeType == "application/vnd.amazon.mobi8-ebook") return "azw3"
return null
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
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)
@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()
}
}
@JsonIgnoreProperties(ignoreUnknown = true)
data class DeviceInfo(
var deviceId: String,
var manufacturer: String,
var model: String,
var sdkVersion: Int,
var clientVersion: String
)
open class LibraryItemWrapper()
@JsonIgnoreProperties(ignoreUnknown = true)
data class PlayItemRequestPayload(
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 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 {
return DeviceSettings(
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
@get:JsonIgnore
val jumpForwardTimeMs
get() = jumpForwardTime * 1000L
@get:JsonIgnore
val autoSleepTimerStartHour
get() = autoSleepTimerStartTime.split(":")[0].toInt()
@get:JsonIgnore
val autoSleepTimerStartMinute
get() = autoSleepTimerStartTime.split(":")[1].toInt()
@get:JsonIgnore
val autoSleepTimerEndHour
get() = autoSleepTimerEndTime.split(":")[0].toInt()
@get:JsonIgnore
val autoSleepTimerEndMinute
get() = autoSleepTimerEndTime.split(":")[1].toInt()
@JsonIgnore
fun getShakeThresholdGravity(): Float { // Used in ShakeDetector
return if (shakeSensitivity == ShakeSensitivitySetting.VERY_HIGH) 1.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 {
Log.e("DeviceSetting", "Invalid ShakeSensitivitySetting $shakeSensitivity")
1.6f
}
}
}
data class DeviceData(
var serverConnectionConfigs: MutableList<ServerConnectionConfig>,
var lastServerConnectionConfigId: String?,
var deviceSettings: DeviceSettings?,
var lastPlaybackSession: PlaybackSession?
) {
@JsonIgnore
fun getLastServerConnectionConfig(): ServerConnectionConfig? {
return lastServerConnectionConfigId?.let { lsccid ->
return serverConnectionConfigs.find { it.id == lsccid }
}
}
}
@@ -0,0 +1,13 @@
package com.audiobookshelf.app.data
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class EBookFile(
var ino:String,
var metadata:FileMetadata?,
var ebookFormat:String,
var isLocal:Boolean,
var localFileId:String?,
var contentUrl:String?
)
@@ -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,
)
@@ -0,0 +1,32 @@
/*
Used in Android Auto to represent a podcast episode or an audiobook in progress
*/
package com.audiobookshelf.app.data
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import org.json.JSONObject
@JsonIgnoreProperties(ignoreUnknown = true)
data class ItemInProgress(
val libraryItemWrapper: LibraryItemWrapper,
val episode: PodcastEpisode?,
val progressLastUpdate: Long,
val isLocal: Boolean
) {
companion object {
fun makeFromServerObject(serverItem: JSONObject, jacksonMapper: ObjectMapper):ItemInProgress {
val libraryItem = jacksonMapper.readValue<LibraryItem>(serverItem.toString())
var episode:PodcastEpisode? = null
if (serverItem.has("recentEpisode")) {
episode = jacksonMapper.readValue<PodcastEpisode>(serverItem.get("recentEpisode").toString())
}
val progressLastUpdate:Long = serverItem.getLong("progressLastUpdate")
return ItemInProgress(libraryItem, episode, progressLastUpdate, false)
}
}
}
@@ -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()
}
}
@@ -0,0 +1,178 @@
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 LibraryItem(
id:String,
var ino:String,
var libraryId:String,
var folderId:String,
var path:String,
var relPath:String,
var mtimeMs:Long,
var ctimeMs:Long,
var birthtimeMs:Long,
var addedAt:Long,
var updatedAt:Long,
var lastScan:Long?,
var scanVersion:String?,
var isMissing:Boolean,
var isInvalid:Boolean,
var mediaType:String,
var media:MediaType,
var libraryFiles:MutableList<LibraryFile>?,
var userMediaProgress:MediaProgress?, // Only included when requesting library item with progress (for downloads)
var collapsedSeries: CollapsedSeries?,
var localLibraryItemId:String?, // For Android Auto
val recentEpisode: PodcastEpisode? // Podcast episode shelf uses this
) : LibraryItemWrapper(id) {
@get:JsonIgnore
val title: String
get() {
if (collapsedSeries != null) {
return collapsedSeries!!.title
}
return media.metadata.title
}
@get:JsonIgnore
val authorName get() = media.metadata.getAuthorDisplayName()
@JsonIgnore
fun getCoverUri(): Uri {
if (media.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 (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 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 {
/*
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)
}
}
@@ -1,17 +1,29 @@
package com.audiobookshelf.app.data
import android.content.Context
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.net.Uri
import android.support.v4.media.MediaMetadataCompat
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.support.v4.media.MediaDescriptionCompat
import android.util.Log
import androidx.core.content.FileProvider
import androidx.core.net.toFile
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
import com.audiobookshelf.app.player.PLAYMETHOD_LOCAL
import java.io.File
import java.util.*
@JsonIgnoreProperties(ignoreUnknown = true)
data class LocalLibraryItem(
var id:String,
class LocalLibraryItem(
id:String,
var folderId:String,
var basePath:String,
var absolutePath:String,
@@ -28,7 +40,7 @@ data class LocalLibraryItem(
var serverAddress:String?,
var serverUserId:String?,
var libraryItemId:String?
) : LibraryItemWrapper() {
) : LibraryItemWrapper(id) {
@get:JsonIgnore
val title get() = media.metadata.title
@get:JsonIgnore
@@ -37,14 +49,17 @@ data class LocalLibraryItem(
val isPodcast get() = mediaType == "podcast"
@JsonIgnore
fun getCoverUri(): Uri {
return if (coverContentUrl != null) Uri.parse(coverContentUrl) else Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
fun getCoverUri(ctx:Context): Uri {
if (coverContentUrl?.startsWith("file:") == true) {
return FileProvider.getUriForFile(ctx, "${BuildConfig.APPLICATION_ID}.fileprovider", Uri.parse(coverContentUrl).toFile())
}
return if (coverContentUrl != null) Uri.parse(coverContentUrl) else Uri.parse("android.resource://${BuildConfig.APPLICATION_ID}/" + R.drawable.icon)
}
@JsonIgnore
fun getDuration():Double {
var total = 0.0
var audioTracks = media.getAudioTracks()
val audioTracks = media.getAudioTracks()
audioTracks.forEach{ total += it.duration }
return total
}
@@ -65,27 +80,55 @@ data class LocalLibraryItem(
}
@JsonIgnore
fun getPlaybackSession(episode:PodcastEpisode?):PlaybackSession {
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
// TODO: Clean up add mediaType methods for displayTitle and displayAuthor
val mediaMetadata = media.metadata
val chapters = if (mediaType == "book") (media as Book).chapters else mutableListOf()
var chapters = if (mediaType == "book") (media as Book).chapters else mutableListOf()
var audioTracks = media.getAudioTracks() as MutableList<AudioTrack>
val authorName = mediaMetadata.getAuthorDisplayName()
val displayTitle = episode?.title ?: mediaMetadata.title
var duration = getDuration()
if (episode != null) { // Get podcast episode audio track
episode.audioTrack?.let { at -> mutableListOf(at) }?.let { tracks -> audioTracks = tracks }
chapters = episode.chapters
duration = episode.audioTrack?.duration ?: 0.0
Log.d("LocalLibraryItem", "getPlaybackSession: Got podcast episode audio track ${audioTracks.size}")
}
val dateNow = System.currentTimeMillis()
return PlaybackSession(sessionId,serverUserId,libraryItemId,episode?.serverEpisodeId, mediaType, mediaMetadata, chapters ?: mutableListOf(), mediaMetadata.title, authorName,null,getDuration(),PLAYMETHOD_LOCAL,dateNow,0L,0L, audioTracks,currentTime,null,this,localEpisodeId,serverConnectionConfigId, serverAddress, "exo-player")
return PlaybackSession(sessionId,serverUserId,libraryItemId,episode?.serverEpisodeId, mediaType, mediaMetadata, deviceInfo,chapters ?: mutableListOf(), displayTitle, authorName,null,duration,PLAYMETHOD_LOCAL,dateNow,0L,0L, audioTracks,currentTime,null,this,localEpisodeId,serverConnectionConfigId, serverAddress, "exo-player")
}
@JsonIgnore
@@ -94,16 +137,61 @@ data class LocalLibraryItem(
}
@JsonIgnore
fun getMediaMetadata(): MediaMetadataCompat {
return MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, title)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, authorName)
putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, getCoverUri().toString())
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getCoverUri().toString())
putString(MediaMetadataCompat.METADATA_KEY_ART_URI, getCoverUri().toString())
putString(MediaMetadataCompat.METADATA_KEY_AUTHOR, authorName)
}.build()
override fun getMediaDescription(progress:MediaProgressWrapper?, ctx:Context): MediaDescriptionCompat {
val coverUri = getCoverUri(ctx)
var bitmap:Bitmap? = null
if (coverContentUrl != null) {
bitmap = if (Build.VERSION.SDK_INT < 28) {
MediaStore.Images.Media.getBitmap(ctx.contentResolver, coverUri)
} else {
val source: ImageDecoder.Source = ImageDecoder.createSource(ctx.contentResolver, coverUri)
ImageDecoder.decodeBitmap(source)
}
}
val extras = Bundle()
extras.putLong(
MediaDescriptionCompat.EXTRA_DOWNLOAD_STATUS,
MediaDescriptionCompat.STATUS_DOWNLOADED
)
if (progress != null) {
if (progress.isFinished) {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_FULLY_PLAYED
)
} else {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_PARTIALLY_PLAYED
)
extras.putDouble(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_PERCENTAGE, progress.progress
)
}
} else if (mediaType != "podcast") {
extras.putInt(
MediaConstants.DESCRIPTION_EXTRAS_KEY_COMPLETION_STATUS,
MediaConstants.DESCRIPTION_EXTRAS_VALUE_COMPLETION_STATUS_NOT_PLAYED
)
}
if (media.metadata.explicit) {
extras.putLong(MediaConstants.METADATA_KEY_IS_EXPLICIT, MediaConstants.METADATA_VALUE_ATTRIBUTE_PRESENT)
}
val mediaDescriptionBuilder = MediaDescriptionCompat.Builder()
.setMediaId(id)
.setTitle(title)
.setIconUri(coverUri)
.setSubtitle(authorName)
.setExtras(extras)
bitmap?.let {
mediaDescriptionBuilder.setIconBitmap(bitmap)
}
return mediaDescriptionBuilder.build()
}
}
@@ -4,69 +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 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 {
var mediaMetadata = getMediaMetadata()
if (mediaType == "book") {
var chapters = getAudiobookChapters()
var book = Book(mediaMetadata as BookMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), chapters,audioTracks,getTotalSize(),getDuration())
return LocalLibraryItem(id, folderId, basePath,absolutePath, contentUrl, false,mediaType, book, localFiles, coverContentUrl, coverAbsolutePath,true,null,null,null,null)
} else {
var podcast = Podcast(mediaMetadata as PodcastMetadata, coverAbsolutePath, mutableListOf(), mutableListOf(), false)
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)
}
}
}
@@ -2,16 +2,19 @@ package com.audiobookshelf.app.data
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import kotlin.math.roundToInt
@JsonIgnoreProperties(ignoreUnknown = true)
data class LocalMediaProgress(
class LocalMediaProgress(
var id:String,
var localLibraryItemId:String,
var localEpisodeId:String?,
var duration:Double,
var progress:Double, // 0 to 1
var currentTime:Double,
var isFinished:Boolean,
progress:Double, // 0 to 1
currentTime:Double,
isFinished:Boolean,
var ebookLocation:String?, // cfi tag
var ebookProgress:Double?, // 0 to 1
var lastUpdate:Long,
var startedAt:Long,
var finishedAt:Long?,
@@ -21,13 +24,32 @@ data class LocalMediaProgress(
var serverUserId:String?,
var libraryItemId:String?,
var episodeId:String?
) {
) : MediaProgressWrapper(isFinished, currentTime, progress) {
@get:JsonIgnore
val progressPercent get() = if (progress.isNaN()) 0 else (progress * 100).roundToInt()
@get:JsonIgnore
override val mediaItemId get() = if (libraryItemId != null) {
if (episodeId.isNullOrEmpty()) libraryItemId ?: "" else "$libraryItemId-$episodeId"
} else {
if (localEpisodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
}
@JsonIgnore
fun isMatch(mediaProgress:MediaProgress):Boolean {
if (episodeId != null) return libraryItemId == mediaProgress.libraryItemId && episodeId == mediaProgress.episodeId
return libraryItemId == mediaProgress.libraryItemId
}
@JsonIgnore
fun updateIsFinished(finished:Boolean) {
if (isFinished != finished) { // If finished changed then set progress
progress = if (finished) 1.0 else 0.0
}
if (finished) {
currentTime = 0.0
}
isFinished = finished
lastUpdate = System.currentTimeMillis()
finishedAt = if (isFinished) lastUpdate else null
@@ -37,9 +59,28 @@ data class LocalMediaProgress(
fun updateFromPlaybackSession(playbackSession:PlaybackSession) {
currentTime = playbackSession.currentTime
progress = playbackSession.progress
lastUpdate = System.currentTimeMillis()
lastUpdate = playbackSession.updatedAt
isFinished = playbackSession.progress >= 0.99
finishedAt = if (isFinished) lastUpdate else null
}
@JsonIgnore
fun updateEbookProgress(ebookLocation:String, ebookProgress:Double) {
lastUpdate = System.currentTimeMillis()
this.ebookProgress = ebookProgress
this.ebookLocation = ebookLocation
}
@JsonIgnore
fun updateFromServerMediaProgress(serverMediaProgress:MediaProgress) {
isFinished = serverMediaProgress.isFinished
progress = serverMediaProgress.progress
currentTime = serverMediaProgress.currentTime
ebookProgress = serverMediaProgress.ebookProgress
ebookLocation = serverMediaProgress.ebookLocation
duration = serverMediaProgress.duration
lastUpdate = serverMediaProgress.lastUpdate
finishedAt = serverMediaProgress.finishedAt
startedAt = serverMediaProgress.startedAt
}
}
@@ -0,0 +1,15 @@
package com.audiobookshelf.app.data
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
class MediaItemEvent(
var name:String, // e.g. Play/Pause/Stop/Seek/Save
var type:String, // Playback/Info
var description:String?,
var currentTime:Number?, // Seconds
var serverSyncAttempted:Boolean?,
var serverSyncSuccess:Boolean?,
var serverSyncMessage:String?,
var timestamp: Long
)
@@ -0,0 +1,17 @@
package com.audiobookshelf.app.data
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
class MediaItemHistory(
var id: String, // media id
var mediaDisplayTitle: String,
var libraryItemId: String,
var episodeId: String?,
var isLocal: Boolean,
var serverConnectionConfigId: String?,
var serverAddress: String?,
var serverUserId: String?,
var createdAt: Long,
var events: MutableList<MediaItemEvent>,
)
@@ -0,0 +1,24 @@
package com.audiobookshelf.app.data
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
class MediaProgress(
var id:String,
var libraryItemId:String,
var episodeId:String?,
var duration:Double, // seconds
progress:Double, // 0 to 1
currentTime:Double,
isFinished:Boolean,
var ebookLocation:String?, // cfi tag
var ebookProgress:Double?, // 0 to 1
var lastUpdate:Long,
var startedAt:Long,
var finishedAt:Long?
) : MediaProgressWrapper(isFinished, currentTime, progress) {
@get:JsonIgnore
override val mediaItemId get() = if (episodeId.isNullOrEmpty()) libraryItemId else "$libraryItemId-$episodeId"
}
@@ -1,75 +1,96 @@
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
import android.provider.MediaStore
import android.support.v4.media.MediaMetadataCompat
import androidx.core.app.NotificationCompat
import androidx.core.content.FileProvider
import androidx.core.net.toFile
import com.audiobookshelf.app.BuildConfig
import com.audiobookshelf.app.R
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.player.MediaProgressSyncData
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.C
import com.google.android.exoplayer2.MediaItem
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
// TODO: enum or something in kotlin?
val PLAYMETHOD_DIRECTPLAY = 0
val PLAYMETHOD_DIRECTSTREAM = 1
val PLAYMETHOD_TRANSCODE = 2
val PLAYMETHOD_LOCAL = 3
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 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 currentTimeMs get() = (currentTime * 1000L).toLong()
val isPodcastEpisode
get() = mediaType == "podcast"
@get:JsonIgnore
val totalDurationMs get() = (getTotalDuration() * 1000L).toLong()
val currentTimeMs
get() = (currentTime * 1000L).toLong()
@get:JsonIgnore
val localLibraryItemId get() = localLibraryItem?.id ?: ""
val totalDurationMs
get() = (getTotalDuration() * 1000L).toLong()
@get:JsonIgnore
val localMediaProgressId get() = if (episodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
val localLibraryItemId
get() = localLibraryItem?.id ?: ""
@get:JsonIgnore
val progress get() = currentTime / getTotalDuration()
val localMediaProgressId
get() =
if (localEpisodeId.isNullOrEmpty()) localLibraryItemId
else "$localLibraryItemId-$localEpisodeId"
@get:JsonIgnore
val progress
get() = currentTime / getTotalDuration()
@get:JsonIgnore
val mediaItemId
get() = if (episodeId.isNullOrEmpty()) libraryItemId ?: "" else "$libraryItemId-$episodeId"
@JsonIgnore
fun getCurrentTrackIndex():Int {
for (i in 0..(audioTracks.size - 1)) {
var track = audioTracks[i]
if (currentTimeMs >= track.startOffsetMs && (track.endOffsetMs) > currentTimeMs) {
fun getCurrentTrackIndex(): Int {
for (i in 0 until audioTracks.size) {
val track = audioTracks[i]
if (currentTimeMs >= track.startOffsetMs && (track.endOffsetMs > currentTimeMs)) {
return i
}
}
@@ -77,130 +98,352 @@ class PlaybackSession(
}
@JsonIgnore
fun getCurrentTrackTimeMs():Long {
var currentTrack = audioTracks[this.getCurrentTrackIndex()]
var time = currentTime - currentTrack.startOffset
fun getNextTrackIndex(): Int {
for (i in 0 until audioTracks.size) {
val track = audioTracks[i]
if (currentTimeMs < track.startOffsetMs) {
return i
}
}
return audioTracks.size - 1
}
@JsonIgnore
fun getChapterForTime(time: Long): BookChapter? {
if (chapters.isEmpty()) return null
return chapters.find { time >= it.startMs && it.endMs > time }
}
@JsonIgnore
fun getCurrentTrackEndTime(): Long {
val currentTrack = audioTracks[this.getCurrentTrackIndex()]
return currentTrack.startOffsetMs + currentTrack.durationMs
}
@JsonIgnore
fun getNextChapterForTime(time: Long): BookChapter? {
if (chapters.isEmpty()) return null
return chapters.find { time < it.startMs } // First chapter where start time is > then time
}
@JsonIgnore
fun getNextTrackEndTime(): Long {
val currentTrack = audioTracks[this.getNextTrackIndex()]
return currentTrack.startOffsetMs + currentTrack.durationMs
}
@JsonIgnore
fun getCurrentTrackTimeMs(): Long {
val currentTrack = audioTracks[this.getCurrentTrackIndex()]
val time = currentTime - currentTrack.startOffset
return (time * 1000L).toLong()
}
@JsonIgnore
fun getTrackStartOffsetMs(index:Int):Long {
var currentTrack = audioTracks[index]
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(): Uri {
if (localLibraryItem?.coverContentUrl != null) return Uri.parse(localLibraryItem?.coverContentUrl) ?: Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
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
}
if (coverPath == null) return Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon)
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()
)
}
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)
// 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
fun getMediaMetadataCompat(): MediaMetadataCompat {
var 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, "series")
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, id)
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)
.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, totalDurationMs)
.putString(
MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI,
coverUri.toString()
)
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(audioTrack:AudioTrack): MediaMetadata {
var metadataBuilder = MediaMetadata.Builder()
.setTitle(displayTitle)
.setDisplayTitle(displayTitle)
.setArtist(displayAuthor)
.setAlbumArtist(displayAuthor)
.setSubtitle(displayAuthor)
fun resolveCoverBitmapAsync(
ctx: Context,
coroutineScope: CoroutineScope,
onArtResolved: () -> Unit
): Job? {
val coverUri = getCoverUri(ctx)
var contentUri = this.getContentUri(audioTrack)
metadataBuilder.setMediaUri(contentUri)
// 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():List<MediaItem> {
var mediaItems:MutableList<MediaItem> = mutableListOf()
fun getMediaItems(ctx: Context): List<MediaItem> {
val mediaItems: MutableList<MediaItem> = mutableListOf()
for (audioTrack in audioTracks) {
var mediaMetadata = this.getExoMediaMetadata(audioTrack)
var mediaUri = this.getContentUri(audioTrack)
var mimeType = audioTrack.mimeType
val mediaMetadata = this.getExoMediaMetadata(ctx)
val mediaUri = this.getContentUri(audioTrack)
val mimeType = audioTrack.mimeType
var queueItem = getQueueItem(audioTrack) // Queue item used in exo player CastManager
var mediaItem = MediaItem.Builder().setUri(mediaUri).setTag(queueItem).setMediaMetadata(mediaMetadata).setMimeType(mimeType).build()
val queueItem = getQueueItem(audioTrack) // Queue item used in exo player CastManager
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 {
var 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_CHAPTER_TITLE, audioTrack.title)
castMetadata.putInt(com.google.android.gms.cast.MediaMetadata.KEY_TRACK_NUMBER, audioTrack.index)
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
)
return castMetadata
}
@JsonIgnore
fun getQueueItem(audioTrack:AudioTrack):MediaQueueItem {
var castMetadata = getCastMediaMetadata(audioTrack)
fun getQueueItem(audioTrack: AudioTrack): MediaQueueItem {
val castMetadata = getCastMediaMetadata(audioTrack)
var mediaUri = getContentUri(audioTrack)
val mediaUri = getContentUri(audioTrack)
var 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,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
fun syncData(syncData:MediaProgressSyncData) {
fun syncData(syncData: MediaProgressSyncData) {
timeListening += syncData.timeListened
updatedAt = System.currentTimeMillis()
currentTime = syncData.currentTime
}
@JsonIgnore
fun getNewLocalMediaProgress():LocalMediaProgress {
return LocalMediaProgress(localMediaProgressId,localLibraryItemId,localEpisodeId,getTotalDuration(),progress,currentTime,false,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
)
}
}
@@ -1,29 +1,228 @@
package com.audiobookshelf.app.device
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.util.Log
import com.audiobookshelf.app.data.DbManager
import com.audiobookshelf.app.data.DeviceData
import com.audiobookshelf.app.data.ServerConnectionConfig
import com.audiobookshelf.app.MediaPlayerWidget
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.managers.DbManager
import com.audiobookshelf.app.player.PlayerNotificationService
import com.audiobookshelf.app.updateAppWidget
/** Interface for widget event handling. */
interface WidgetEventEmitter {
/**
* 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"
val dbManager:DbManager = DbManager()
var deviceData:DeviceData = dbManager.getDeviceData()
val dbManager: DbManager = DbManager()
var deviceData: DeviceData = dbManager.getDeviceData()
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 hasLastServerConnectionConfig get() = deviceData.getLastServerConnectionConfig() != 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
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
) {
deviceData.deviceSettings?.autoSleepTimerStartTime = "22:00"
deviceData.deviceSettings?.autoSleepTimerEndTime = "06:00"
deviceData.deviceSettings?.sleepTimerLength = 900000L
}
if (deviceData.deviceSettings?.shakeSensitivity == null) {
deviceData.deviceSettings?.shakeSensitivity = ShakeSensitivitySetting.MEDIUM
}
// Initialize auto sleep timer auto rewind added in v0.9.64
if (deviceData.deviceSettings?.autoSleepTimerAutoRewindTime == null) {
deviceData.deviceSettings?.autoSleepTimerAutoRewindTime = 300000L // 5 minutes
}
// 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.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
)
}
/**
* 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 } }
}
/**
* 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)) {
Log.i("Internet", "NetworkCapabilities.TRANSPORT_CELLULAR")
return true
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
Log.i("Internet", "NetworkCapabilities.TRANSPORT_WIFI")
return true
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET)) {
Log.i("Internet", "NetworkCapabilities.TRANSPORT_ETHERNET")
return true
}
}
return false
}
/**
* Sets the last playback session.
* @param playbackSession The playback session to set.
*/
fun setLastPlaybackSession(playbackSession: PlaybackSession) {
deviceData.lastPlaybackSession = playbackSession
dbManager.saveDeviceData(deviceData)
}
/**
* 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) {
val isPlaying = pns.currentPlayer.isPlaying
val appWidgetManager = AppWidgetManager.getInstance(context)
val componentName = ComponentName(context, MediaPlayerWidget::class.java)
val ids = appWidgetManager.getAppWidgetIds(componentName)
val playbackSession = pns.getCurrentPlaybackSessionCopy()
for (widgetId in ids) {
updateAppWidget(
context,
appWidgetManager,
widgetId,
playbackSession,
isPlaying,
PlayerNotificationService.isClosed
)
}
}
override fun onPlayerClosed() {
val appWidgetManager = AppWidgetManager.getInstance(context)
val componentName = ComponentName(context, MediaPlayerWidget::class.java)
val ids = appWidgetManager.getAppWidgetIds(componentName)
for (widgetId in ids) {
updateAppWidget(
context,
appWidgetManager,
widgetId,
deviceData.lastPlaybackSession,
false,
PlayerNotificationService.isClosed
)
}
}
})
}
}
@@ -5,465 +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.plugins.AbsDownloader
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 com.audiobookshelf.app.models.DownloadItem
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"
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)}"
enum class ItemScanResult {
ADDED, REMOVED, UPDATED, UPTODATE
}
// 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 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()
)
}
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(false, 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 result = scanLibraryItemFolder(itemFolder, localFolder, existingItem, forceAudioProbe)
if (result == ItemScanResult.REMOVED) mediaItemsRemoved++
else if (result == ItemScanResult.UPDATED) mediaItemsUpdated++
else if (result == 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) {
var 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())
}
}
fun scanLibraryItemFolder(itemFolder: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
val filesInFolder = itemFolder.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4"))
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
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
)
}
filesInFolder.forEach { file ->
val mimeType = file.mimeType ?: ""
val filename = file.name ?: ""
val isAudio = mimeType.startsWith("audio") || mimeType == "video/mp4"
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 (isAudio) {
var audioTrackToAdd:AudioTrack? = null
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
}
}
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
var track = AudioTrack(index, startOffset, audioProbeResult.duration, filename, localFile.contentUrl, mimeType, null, true, localFileId, audioProbeResult, null)
audioTrackToAdd = track
}
startOffset += audioProbeResult.duration
isNewOrUpdated = true
} else {
audioTrackToAdd = existingAudioTrack
}
startOffset += audioTrackToAdd.duration
index++
audioTracks.add(audioTrackToAdd)
} 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
}
}
}
if (existingItem != null && audioTracks.isEmpty()) {
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()) {
Log.d(tag, "Found local media item named $itemFolderName with ${audioTracks.size} tracks and ${localFiles.size} local files")
val localMediaItem = LocalMediaItem(itemId, itemFolderName, localFolder.mediaType, localFolder.id, itemFolder.uri.toString(), itemFolder.getSimplePath(ctx), itemFolder.getBasePath(ctx), itemFolder.getAbsolutePath(ctx),audioTracks,localFiles,coverContentUrl,coverAbsolutePath)
val localLibraryItem = localMediaItem.getLocalLibraryItem()
DeviceManager.dbManager.saveLocalLibraryItem(localLibraryItem)
return ItemScanResult.ADDED
} else {
return ItemScanResult.UPTODATE
}
}
// Scan item after download and create local library item
fun scanDownloadItem(downloadItem: AbsDownloader.DownloadItem):DownloadItemScanResult? {
val folderDf = DocumentFileCompat.fromUri(ctx, Uri.parse(downloadItem.localFolder.contentUrl))
val foldersFound = folderDf?.search(false, DocumentFileType.FOLDER) ?: mutableListOf()
var itemFolderId = ""
var itemFolderUrl = ""
var itemFolderBasePath = ""
var itemFolderAbsolutePath = ""
foldersFound.forEach {
if (it.name == downloadItem.itemTitle) {
itemFolderId = it.id
itemFolderUrl = it.uri.toString()
itemFolderBasePath = it.getBasePath(ctx)
itemFolderAbsolutePath = it.getAbsolutePath(ctx)
}
}
if (itemFolderUrl == "") {
Log.d(tag, "scanDownloadItem failed to find media folder")
// 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
}
val df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(itemFolderUrl))
if (df == null) {
Log.e(tag, "Folder Doc File Invalid ${downloadItem.itemFolderPath}")
return 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
val filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4"))
Log.d(tag, "scanDownloadItem ${filesFound.size} files found in ${downloadItem.itemFolderPath}")
var localEpisodeId:String? = null
var localLibraryItem:LocalLibraryItem? = null
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()
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)
// TODO: Make asynchronous
val audioProbeResult = probeAudioFile(localFile.absolutePath)
// Create new audio track
val track = AudioTrack(audioTrackFromServer?.index ?: -1, audioTrackFromServer?.startOffset ?: 0.0, audioProbeResult.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, audioTrackFromServer?.index ?: -1)
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
var newEpisode = podcast.addEpisode(track, podcastEpisode)
localEpisodeId = newEpisode.id
Log.d(tag, "scanDownloadItem: Added episode to podcast ${podcastEpisode.title} ${track.title} | Track index: ${podcastEpisode.audioTrack?.index}")
}
} else { // Cover image
val localFileId = DeviceManager.getBase64Id(docFile.id)
val localFile = LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx),docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
localLibraryItem.coverAbsolutePath = localFile.absolutePath
localLibraryItem.coverContentUrl = localFile.contentUrl
localLibraryItem.localFiles.add(localFile)
}
}
if (audioTracks.isEmpty()) {
Log.d(tag, "scanDownloadItem did not find any audio tracks in folder for ${downloadItem.itemFolderPath}")
return 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,
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)
return downloadItemScanResult
return LocalFile(
DeviceManager.getBase64Id(document.id),
document.name,
document.uri.toString(),
document.getBasePath(ctx),
document.getAbsolutePath(ctx),
document.mimeType,
document.length()
)
}
fun scanLocalLibraryItem(localLibraryItem:LocalLibraryItem, forceAudioProbe:Boolean):LocalLibraryItemScanResult? {
var df: DocumentFile? = DocumentFileCompat.fromUri(ctx, Uri.parse(localLibraryItem.contentUrl))
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
)
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}")
private fun scanParts(
item: DownloadItem,
localItem: LocalLibraryItem,
externalFolder: DocumentFile? = null,
callback: (DownloadItemScanResult?) -> Unit
) {
val tracks = mutableListOf<AudioTrack>()
var foundEbook = false
var localEpisodeId: String? = null
var wasUpdated = false
// Search for files in media item folder
var filesFound = df.search(false, DocumentFileType.FILE, arrayOf("audio/*", "image/*", "video/mp4"))
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)
}
}
var existingAudioTracks = localLibraryItem.media.getAudioTracks()
// Remove any files no longer found in library item folder
var 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)
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
}
}
wasUpdated = true
}
}
filesFound.forEach { docFile ->
var localFileId = DeviceManager.getBase64Id(docFile.id)
var existingLocalFile = localLibraryItem.localFiles.find { it.id == localFileId }
if (existingLocalFile == null || (existingLocalFile.isAudioFile() && forceAudioProbe)) {
var localFile = existingLocalFile ?: LocalFile(localFileId,docFile.name,docFile.uri.toString(),docFile.getBasePath(ctx), docFile.getAbsolutePath(ctx),docFile.getSimplePath(ctx),docFile.mimeType,docFile.length())
if (existingLocalFile == null) {
localLibraryItem.localFiles.add(localFile)
Log.d(tag, "scanLocalLibraryItem new file found ${localFile.filename}")
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
)
}
if (localFile.isAudioFile()) {
// TODO: Make asynchronous
var audioProbeResult = probeAudioFile(localFile.absolutePath)
var existingTrack = existingAudioTracks.find { audioTrack ->
audioTrack.localFileId == localFile.id
}
if (existingTrack == null) {
// Create new audio track
var lastTrack = existingAudioTracks.lastOrNull()
var startOffset = (lastTrack?.startOffset ?: 0.0) + (lastTrack?.duration ?: 0.0)
var track = AudioTrack(existingAudioTracks.size, startOffset, audioProbeResult.duration, localFile.filename ?: "", localFile.contentUrl, localFile.mimeType ?: "", null, true, localFileId, audioProbeResult, null)
localLibraryItem.media.addAudioTrack(track)
wasUpdated = true
} else {
existingTrack.audioProbeResult = audioProbeResult
// TODO: Update data found from probe
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
}
else -> {
localItem.coverAbsolutePath = localFile.absolutePath
localItem.coverContentUrl = localFile.contentUrl
localItem.localFiles.removeAll { it.id == localFile.id }
localItem.localFiles.add(localFile)
}
}
}
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")
if (tracks.isEmpty() && !foundEbook) {
callback(null)
return
}
return LocalLibraryItemScanResult(wasUpdated, localLibraryItem)
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)
}
fun probeAudioFile(absolutePath:String):AudioProbeResult {
var session = FFprobeKit.execute("-i \"${absolutePath}\" -print_format json -show_format -show_streams -select_streams a -show_chapters -loglevel quiet")
Log.d(tag, "FFprobe output ${JSObject(session.output)}")
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
}
val audioProbeResult = jacksonMapper.readValue<AudioProbeResult>(session.output)
Log.d(tag, "Probe Result DATA ${audioProbeResult.duration} | ${audioProbeResult.size} | ${audioProbeResult.title} | ${audioProbeResult.artist}")
return audioProbeResult
/**
* 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 mimeTypeFor(part: DownloadItemPart): String? {
return part.audioTrack?.mimeType
?: when (part.ebookFile?.ebookFormat?.lowercase()) {
"epub" -> "application/epub+zip"
"pdf" -> "application/pdf"
else -> "image/jpeg"
}
}
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 root = DocumentFileCompat.fromUri(ctx, Uri.parse(item.localFolder.contentUrl))
if (root == null) {
Log.e(tag, "Invalid SAF root: ${item.localFolder.contentUrl}")
callback(null)
return
}
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)
}
}
@@ -0,0 +1,325 @@
package com.audiobookshelf.app.managers
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
class DbManager {
val tag = "DbManager"
companion object {
private var isDbInitialized = false
fun initialize(ctx: Context) {
if (isDbInitialized) return
Paper.init(ctx)
isDbInitialized = true
Log.i("DbManager", "Initialized Paper db")
}
}
fun getDeviceData(): DeviceData {
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()
Paper.book("localLibraryItems").allKeys.forEach {
val localLibraryItem: LocalLibraryItem? = Paper.book("localLibraryItems").read(it)
if (localLibraryItem != null &&
(mediaType.isNullOrEmpty() || mediaType == localLibraryItem.mediaType)
) {
localLibraryItems.add(localLibraryItem)
}
}
return localLibraryItems
}
fun getLocalLibraryItemsInFolder(folderId: String): List<LocalLibraryItem> {
val localLibraryItems = getLocalLibraryItems()
return localLibraryItems.filter { it.folderId == folderId }
}
fun getLocalLibraryItemByLId(libraryItemId: String): LocalLibraryItem? {
return getLocalLibraryItems().find { it.libraryItemId == libraryItemId }
}
fun getLocalLibraryItem(localLibraryItemId: String): LocalLibraryItem? {
return Paper.book("localLibraryItems").read(localLibraryItemId)
}
fun getLocalLibraryItemWithEpisode(podcastEpisodeId: String): LibraryItemWithEpisode? {
var podcastEpisode: PodcastEpisode? = null
val localLibraryItem =
getLocalLibraryItems("podcast").find { localLibraryItem ->
val podcast = localLibraryItem.media as Podcast
podcastEpisode = podcast.episodes?.find { it.id == podcastEpisodeId }
podcastEpisode != null
}
return if (localLibraryItem != null) {
LibraryItemWithEpisode(localLibraryItem, podcastEpisode!!)
} else {
null
}
}
fun removeLocalLibraryItem(localLibraryItemId: String) {
Paper.book("localLibraryItems").delete(localLibraryItemId)
}
fun saveLocalLibraryItems(localLibraryItems: List<LocalLibraryItem>) {
localLibraryItems.map { Paper.book("localLibraryItems").write(it.id, it) }
}
fun saveLocalLibraryItem(localLibraryItem: LocalLibraryItem) {
Paper.book("localLibraryItems").write(localLibraryItem.id, localLibraryItem)
}
fun saveLocalFolder(localFolder: LocalFolder) {
Paper.book("localFolders").write(localFolder.id, localFolder)
}
fun getLocalFolder(folderId: String): LocalFolder? {
return Paper.book("localFolders").read(folderId)
}
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) }
}
return localFolders
}
fun removeLocalFolder(folderId: String) {
val localLibraryItems = getLocalLibraryItemsInFolder(folderId)
localLibraryItems.forEach { Paper.book("localLibraryItems").delete(it.id) }
Paper.book("localFolders").delete(folderId)
}
fun saveDownloadItem(downloadItem: DownloadItem) {
Paper.book("downloadItems").write(downloadItem.id, downloadItem)
}
fun removeDownloadItem(downloadItemId: String) {
Paper.book("downloadItems").delete(downloadItemId)
}
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) }
}
return downloadItems
}
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()
Paper.book("localMediaProgress").allKeys.forEach { localMediaProgressId ->
Paper.book("localMediaProgress").read<LocalMediaProgress>(localMediaProgressId)?.let {
mediaProgress.add(it)
}
}
return mediaProgress
}
fun removeLocalMediaProgress(localMediaProgressId: String) {
Paper.book("localMediaProgress").delete(localMediaProgressId)
}
fun removeAllLocalMediaProgress() {
Paper.book("localMediaProgress").destroy()
}
// Make sure all local file ids still exist
fun cleanLocalLibraryItems(context: Context) {
val localLibraryItems = getLocalLibraryItems()
localLibraryItems.forEach { lli ->
var hasUpdates = false
// Check local files
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>
} 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>
}
// Check cover still there
lli.coverAbsolutePath?.let {
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 (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)
}
}
}
// Remove any local media progress where the local media item is not found
fun cleanLocalMediaProgress() {
val localMediaProgress = getAllLocalMediaProgress()
val localLibraryItems = getLocalLibraryItems()
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)"
)
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"
)
Paper.book("localMediaProgress").delete(it.id)
} else if (matchingLLI.isPodcast) {
if (it.localEpisodeId.isNullOrEmpty()) {
Log.d(tag, "cleanLocalMediaProgress: Podcast media progress has no episode id - removing")
Paper.book("localMediaProgress").delete(it.id)
} else {
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"
)
Paper.book("localMediaProgress").delete(it.id)
}
}
}
}
}
fun saveMediaItemHistory(mediaItemHistory: MediaItemHistory) {
Paper.book("mediaItemHistory").write(mediaItemHistory.id, mediaItemHistory)
}
fun getMediaItemHistory(id: String): MediaItemHistory? {
return Paper.book("mediaItemHistory").read(id)
}
fun savePlaybackSession(playbackSession: PlaybackSession) {
Paper.book("playbackSession").write(playbackSession.id, playbackSession)
}
fun removePlaybackSession(playbackSessionId: String) {
Paper.book("playbackSession").delete(playbackSessionId)
}
fun getPlaybackSessions(): List<PlaybackSession> {
val sessions: MutableList<PlaybackSession> = mutableListOf()
Paper.book("playbackSession").allKeys.forEach { playbackSessionId ->
Paper.book("playbackSession").read<PlaybackSession>(playbackSessionId)?.let {
sessions.add(it)
}
}
return sessions
}
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"
)
}
}
}
@@ -0,0 +1,794 @@
package com.audiobookshelf.app.managers
import android.content.Context
import android.net.Uri
import android.os.StatFs
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.file.fullName
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.plugins.AbsLogger
import com.audiobookshelf.app.server.ApiHandler
import com.fasterxml.jackson.core.json.JsonReadFeature
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.getcapacitor.JSObject
import java.io.File
import java.io.FileInputStream
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.max
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/** Manages the process-owned queue for app-managed downloads. */
class DownloadItemManager(
private val folderScanner: FolderScanner,
private val context: Context,
private var clientEventEmitter: DownloadEventEmitter
) {
private val tag = "DownloadItemManager"
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val activeCalls = ConcurrentHashMap<String, InternalDownloadManager.DownloadHandle>()
private val safFolderLocks = ConcurrentHashMap<String, Any>()
private val scanLocks = ConcurrentHashMap<String, Any>()
private val reservations = mutableMapOf<String, Long>()
private val lastPersistTime = mutableMapOf<String, Long>()
private val finalizingItems = mutableSetOf<String>()
private val refreshingServerIds = mutableSetOf<String>()
private val apiHandler = ApiHandler(context)
private var watcherRunning = false
private val jacksonMapper =
jacksonObjectMapper()
.enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
var downloadItemQueue: MutableList<DownloadItem> = mutableListOf()
private set
var currentDownloadItemParts: MutableList<DownloadItemPart> = mutableListOf()
private set
interface DownloadEventEmitter {
fun onDownloadItem(downloadItem: DownloadItem)
fun onDownloadItemPartUpdate(downloadItemPart: DownloadItemPart)
fun onDownloadItemComplete(jsobj: JSObject)
fun onQueueChanged(hasWork: Boolean)
}
interface InternalProgressCallback {
fun onSizeResolved(totalBytes: Long)
fun onProgress(totalBytesWritten: Long, progress: Long)
fun onComplete(failed: Boolean)
fun onAuthError()
}
@Synchronized
fun setEventEmitter(eventEmitter: DownloadEventEmitter) {
clientEventEmitter = eventEmitter
downloadItemQueue.forEach(clientEventEmitter::onDownloadItem)
notifyQueueChanged()
}
@Synchronized
fun restoreQueue() {
if (downloadItemQueue.isNotEmpty()) return
DeviceManager.dbManager.getDownloadItems().forEach { item ->
item.downloadItemParts.filter { it.moved }.forEach { part ->
if (!finalizedFileExists(part)) {
AbsLogger.error(tag, "Finalized file is missing; resetting ${part.filename}")
part.moved = false
part.completed = false
part.completedDestinationUri = null
part.downloadId = null
part.reusedExistingFile = false
}
}
if (item.isDownloadFinished) {
downloadItemQueue.add(item)
checkDownloadItemFinished(item)
return@forEach
}
var resetFailed = false
item.downloadItemParts.forEach { part ->
if (part.moved) return@forEach
if (!resetPartForFreshDownload(part)) resetFailed = true
}
if (resetFailed) {
item.terminalFailureAt = item.terminalFailureAt ?: System.currentTimeMillis()
item.stagingCleanupAt = null
}
if (item.terminalFailureAt != null) {
item.downloadItemParts.filter { !it.moved }.forEach { it.failed = true }
}
downloadItemQueue.add(item)
if (item.terminalFailureAt != null) IncompleteDownloadCleanup.schedule(context, item)
clientEventEmitter.onDownloadItem(item)
}
notifyQueueChanged()
}
@Synchronized
fun addDownloadItem(downloadItem: DownloadItem) {
val existingItem = downloadItemQueue.find { it.id == downloadItem.id }
if (existingItem != null) {
return
}
persist(downloadItem, force = true)
downloadItemQueue.add(downloadItem)
clientEventEmitter.onDownloadItem(downloadItem)
notifyQueueChanged()
}
@Synchronized
fun retryDownloadItem(downloadItemId: String): Boolean {
val item = downloadItemQueue.find { it.id == downloadItemId } ?: return false
if (item.downloadItemParts.any { it in currentDownloadItemParts }) return false
if (item.isDownloadFinished) return false
synchronized(IncompleteDownloadCleanup) {
var resetFailed = false
item.downloadItemParts.filter { !it.moved }.forEach { part ->
if (!resetPartForFreshDownload(part)) resetFailed = true
}
if (resetFailed) {
item.downloadItemParts.filter { !it.moved }.forEach { it.failed = true }
persist(item, force = true)
return false
}
item.terminalFailureAt = null
item.stagingCleanupAt = null
IncompleteDownloadCleanup.cancel(context, item.id)
persist(item, force = true)
}
clientEventEmitter.onDownloadItem(item)
notifyQueueChanged()
return true
}
@Synchronized
fun resumeWork() {
checkUpdateDownloadQueue()
notifyQueueChanged()
}
@Synchronized
fun cancelAll() {
activeCalls.values.forEach(InternalDownloadManager.DownloadHandle::cancel)
activeCalls.clear()
downloadItemQueue.forEach { item ->
item.downloadItemParts.forEach { part ->
File(part.destinationPath).delete()
if (part.moved && !part.reusedExistingFile && part.isInternalStorage) {
File(part.finalDestinationPath).delete()
} else if (part.moved && !part.reusedExistingFile) {
part.completedDestinationUri?.let { uri ->
try {
DocumentFile.fromSingleUri(context, Uri.parse(uri))?.delete()
} catch (e: Exception) {
AbsLogger.error(
tag,
"Could not delete cancelled SAF file ${part.filename}: ${e.message}"
)
}
}
}
}
IncompleteDownloadCleanup.cancel(context, item.id)
DeviceManager.dbManager.removeDownloadItem(item.id)
}
currentDownloadItemParts.clear()
reservations.clear()
downloadItemQueue.clear()
notifyQueueChanged()
}
@Synchronized
fun hasWork(): Boolean =
finalizingItems.isNotEmpty() ||
downloadItemQueue.any { item ->
item.downloadItemParts.any { part ->
(!part.moved && !part.failed) || part.isMoving
}
}
@Synchronized
private fun checkUpdateDownloadQueue() {
downloadItemQueue.toList().forEach { item ->
var slots = MAX_SIMULTANEOUS_DOWNLOADS - currentDownloadItemParts.size
if (slots <= 0) return@forEach
item.downloadItemParts
.filter { part ->
part.completed &&
!part.moved &&
!part.failed &&
!part.isMoving &&
part !in currentDownloadItemParts &&
File(part.destinationPath).exists() &&
!hasActiveDestinationConflict(part)
}
.take(slots)
.forEach { part ->
currentDownloadItemParts.add(part)
part.downloadId = APP_MANAGED_DOWNLOAD_ID
}
slots = MAX_SIMULTANEOUS_DOWNLOADS - currentDownloadItemParts.size
if (slots <= 0) return@forEach
item.getNextDownloadItemParts(slots).forEach { part ->
val existingFile = findSharedStorageFile(part)
if (existingFile != null) {
part.bytesDownloaded = existingFile.length()
part.progress = 100L
part.completedDestinationUri = existingFile.uri.toString()
part.reusedExistingFile = true
File(part.destinationPath).delete()
completePart(item, part)
clientEventEmitter.onDownloadItemPartUpdate(part)
return@forEach
}
if (completeFromExistingInternalCover(item, part)) return@forEach
if (hasActiveDestinationConflict(part)) {
leaveQueued(item, part)
} else if (part.fileSize <= 0L && currentDownloadItemParts.any { it.fileSize <= 0L }) {
leaveQueued(item, part)
} else if (tryReserve(part)) startDownload(item, part)
else {
part.waitingForSpace = true
part.lastUpdateTime = System.currentTimeMillis()
persist(item)
clientEventEmitter.onDownloadItemPartUpdate(part)
}
}
}
if (hasWork()) startWatchingDownloads() else notifyQueueChanged()
}
private fun startDownload(item: DownloadItem, part: DownloadItemPart) {
val stagingFile = File(part.destinationPath)
stagingFile.parentFile?.mkdirs()
part.downloadId = APP_MANAGED_DOWNLOAD_ID
part.waitingForSpace = false
part.lastUpdateTime = System.currentTimeMillis()
currentDownloadItemParts.add(part)
persist(item, force = true)
AbsLogger.info(tag, "Starting download for ${part.filename}")
val activeConfig = DeviceManager.serverConnectionConfig
val token =
if (activeConfig?.id == item.serverConnectionConfigId) activeConfig.token
else
DeviceManager.getServerConnectionConfig(item.serverConnectionConfigId)?.token
?: DeviceManager.token
val handle =
InternalDownloadManager(
stagingFile,
part.fileSize,
object : InternalProgressCallback {
override fun onSizeResolved(totalBytes: Long) {
synchronized(this@DownloadItemManager) {
if (part !in currentDownloadItemParts || totalBytes < 0L) return
if (part.fileSize == totalBytes) return
AbsLogger.info(
tag,
"Using server size $totalBytes instead of metadata size ${part.fileSize} for ${part.filename}"
)
part.fileSize = totalBytes
part.lastUpdateTime = System.currentTimeMillis()
persist(item, force = true)
clientEventEmitter.onDownloadItemPartUpdate(part)
}
}
override fun onProgress(totalBytesWritten: Long, progress: Long) {
synchronized(this@DownloadItemManager) {
if (part !in currentDownloadItemParts) return
part.bytesDownloaded = totalBytesWritten
part.progress = progress
part.lastUpdateTime = System.currentTimeMillis()
persist(item)
}
}
override fun onComplete(failed: Boolean) {
synchronized(this@DownloadItemManager) {
if (part !in currentDownloadItemParts) return
part.failed = failed
part.completed = !failed
part.lastUpdateTime = System.currentTimeMillis()
activeCalls.remove(part.id)
persist(item, force = true)
}
}
override fun onAuthError() {
synchronized(this@DownloadItemManager) {
if (part !in currentDownloadItemParts) return
handleAuthError(item, part)
}
}
},
{ hasAvailableSpace(part) }
)
.download(serverUrl(item, part), token)
if (part in currentDownloadItemParts && !part.completed && !part.failed) {
activeCalls[part.id] = handle
}
}
@Synchronized
private fun startWatchingDownloads() {
if (watcherRunning) return
watcherRunning = true
scope.launch {
while (true) {
val activeParts =
synchronized(this@DownloadItemManager) { currentDownloadItemParts.toList() }
activeParts.forEach(::handlePartUpdate)
synchronized(this@DownloadItemManager) {
checkUpdateDownloadQueue()
if (!hasWork()) {
watcherRunning = false
notifyQueueChanged()
return@launch
}
}
delay(WATCH_INTERVAL_MS)
}
}
}
private fun handlePartUpdate(part: DownloadItemPart) {
clientEventEmitter.onDownloadItemPartUpdate(part)
val item =
synchronized(this) { downloadItemQueue.find { it.id == part.downloadItemId } }
?: run {
removeActivePart(part)
return
}
if (!part.completed && !part.failed) {
val lastUpdate = part.lastUpdateTime ?: return
if (System.currentTimeMillis() - lastUpdate > STALL_TIMEOUT_MS) {
AbsLogger.error(tag, "Download stalled: ${part.filename}")
activeCalls.remove(part.id)?.cancel()
failOrRetry(item, part, "Download stalled")
}
return
}
if (part.failed) {
failOrRetry(item, part, "Transfer failed")
return
}
if (part.isInternalStorage) finalizeInternalFile(item, part) else moveDownloadedFile(item, part)
}
@Synchronized
private fun failOrRetry(item: DownloadItem, part: DownloadItemPart, reason: String) {
removeActivePart(part)
part.retryCount += 1
reservations.remove(part.destinationPath)
if (part.retryCount > MAX_RETRIES) {
markTerminalFailure(item, part, "$reason after $MAX_RETRIES retries")
return
}
part.failed = false
part.completed = false
part.downloadId = null
part.isMoving = false
persist(item, force = true)
clientEventEmitter.onDownloadItemPartUpdate(part)
}
/** A 401 refreshes the token for this queued item's server without consuming transfer retries. */
@Synchronized
private fun handleAuthError(item: DownloadItem, part: DownloadItemPart) {
removeActivePart(part)
reservations.remove(part.destinationPath)
part.downloadId = null
part.isMoving = false
part.failed = false
part.completed = false
part.authRetryCount += 1
part.lastUpdateTime = System.currentTimeMillis()
if (part.authRetryCount > MAX_AUTH_RETRIES) {
markTerminalFailure(item, part, "Unauthorized after $MAX_AUTH_RETRIES token refresh attempts")
return
}
AbsLogger.info(
tag,
"Refreshing token after 401 for ${part.filename} (attempt ${part.authRetryCount})"
)
persist(item, force = true)
clientEventEmitter.onDownloadItemPartUpdate(part)
refreshTokenThenResume(item.serverConnectionConfigId)
}
private fun refreshTokenThenResume(serverConnectionConfigId: String) {
if (!refreshingServerIds.add(serverConnectionConfigId)) return
apiHandler.refreshAuthTokens(serverConnectionConfigId) { result ->
synchronized(this@DownloadItemManager) {
refreshingServerIds.remove(serverConnectionConfigId)
when (result) {
is ApiHandler.RefreshResult.Success -> {
AbsLogger.info(
tag,
"Token refresh succeeded; resuming downloads for $serverConnectionConfigId"
)
checkUpdateDownloadQueue()
}
ApiHandler.RefreshResult.Rejected -> failParkedAuthParts(serverConnectionConfigId)
// Parked parts are still queued, so MAX_AUTH_RETRIES bounds the reattempts.
ApiHandler.RefreshResult.Transient -> {
AbsLogger.info(
tag,
"Token refresh could not be completed; retrying downloads for $serverConnectionConfigId"
)
checkUpdateDownloadQueue()
}
}
}
}
}
@Synchronized
private fun failParkedAuthParts(serverConnectionConfigId: String) {
downloadItemQueue.toList().forEach { item ->
if (item.serverConnectionConfigId != serverConnectionConfigId) return@forEach
item.downloadItemParts
.filter {
it.authRetryCount > 0 &&
!it.completed &&
!it.failed &&
it.downloadId == null &&
it !in currentDownloadItemParts
}
.forEach { part ->
markTerminalFailure(item, part, "Unable to refresh download authorization")
}
}
}
@Synchronized
private fun markTerminalFailure(item: DownloadItem, part: DownloadItemPart, reason: String) {
AbsLogger.error(tag, "$reason: ${part.filename}")
removeActivePart(part)
reservations.remove(part.destinationPath)
part.failed = true
part.completed = false
part.downloadId = null
part.isMoving = false
item.terminalFailureAt = item.terminalFailureAt ?: System.currentTimeMillis()
item.stagingCleanupAt = null
persist(item, force = true)
IncompleteDownloadCleanup.schedule(context, item)
clientEventEmitter.onDownloadItemPartUpdate(part)
notifyQueueChanged()
}
private fun finalizeInternalFile(item: DownloadItem, part: DownloadItemPart) {
if (part.moved || part.isMoving) return
part.isMoving = true
val stagingFile = File(part.destinationPath)
val finalFile = File(part.finalDestinationPath)
finalFile.parentFile?.mkdirs()
val backup = File(finalFile.parentFile, ".${finalFile.name}.abs-backup")
try {
if (backup.exists() && !backup.delete()) throw IllegalStateException("Could not clear backup")
if (finalFile.exists() && !finalFile.renameTo(backup))
throw IllegalStateException("Could not protect existing file")
if (!stagingFile.renameTo(finalFile)) {
if (backup.exists()) backup.renameTo(finalFile)
throw IllegalStateException("Could not finalize internal staging file")
}
backup.delete()
AbsLogger.info(tag, "Move completed for ${part.filename} to ${finalFile.absolutePath}")
completePart(item, part)
} catch (e: Exception) {
part.isMoving = false
part.failed = true
failOrRetry(item, part, e.message ?: "Internal finalization failed")
}
}
private fun moveDownloadedFile(item: DownloadItem, part: DownloadItemPart) {
if (part.moved || part.isMoving) return
val root =
DocumentFile.fromTreeUri(context, Uri.parse(part.localFolderUrl))
?: return failFinalization(item, part, "Could not resolve SAF destination")
part.isMoving = true
persist(item, force = true)
scope.launch {
try {
if (!hasAvailableSpace(part))
throw IllegalStateException("Insufficient storage for SAF copy")
val folderKey = "${root.uri}/${part.finalDestinationSubfolder}"
val folderLock = safFolderLocks.computeIfAbsent(folderKey) { Any() }
val folder =
synchronized(folderLock) { getOrCreateFolder(root, part.finalDestinationSubfolder) }
?: throw IllegalStateException("Could not create SAF destination folder")
val temporaryName = ".${part.filename}.${part.id.hashCode()}.part"
folder.findFile(temporaryName)?.delete()
val temporary =
folder.createFile(mimeTypeFor(part), temporaryName)
?: throw IllegalStateException("Could not create SAF temporary file")
val staging = File(part.destinationPath)
FileInputStream(staging).use { input ->
context.contentResolver.openOutputStream(temporary.uri, "w")?.use { input.copyTo(it) }
?: throw IllegalStateException("Could not open SAF output stream")
}
if (temporary.length() != staging.length())
throw IllegalStateException("SAF copy size mismatch")
val existing = findDocumentByFilename(folder, part)
if (existing != null && !existing.delete())
throw IllegalStateException("Could not replace existing file")
if (!temporary.renameTo(part.filename))
throw IllegalStateException("Could not finalize SAF temporary file")
val destination =
findDocumentByFilename(folder, part)
?: throw IllegalStateException("Could not reopen finalized SAF file")
if (destination.length() != staging.length())
throw IllegalStateException("SAF final size mismatch")
if (!staging.delete()) AbsLogger.error(tag, "Could not remove staging file ${staging.name}")
part.completedDestinationUri = destination.uri.toString()
AbsLogger.info(tag, "Move completed for ${part.filename} to ${destination.uri}")
completePart(item, part)
} catch (e: Exception) {
failFinalization(item, part, "SAF copy failed: ${e.message}")
}
}
}
@Synchronized
private fun failFinalization(item: DownloadItem, part: DownloadItemPart, message: String) {
AbsLogger.error(tag, message)
part.isMoving = false
part.failed = true
failOrRetry(item, part, message)
}
@Synchronized
private fun completePart(item: DownloadItem, part: DownloadItemPart) {
part.moved = true
part.completed = true
part.failed = false
part.isMoving = false
reservations.remove(part.destinationPath)
removeActivePart(part)
persist(item, force = true)
checkDownloadItemFinished(item)
}
@Synchronized
private fun checkDownloadItemFinished(item: DownloadItem) {
if (!item.isDownloadFinished || !finalizingItems.add(item.id)) return
IncompleteDownloadCleanup.cancel(context, item.id)
scope.launch {
try {
val scanLock = scanLocks.computeIfAbsent(scanDestinationKey(item)) { Any() }
synchronized(scanLock) {
folderScanner.scanDownloadItem(item) { scanResult ->
val event =
JSObject().apply {
put("libraryItemId", item.id)
put("localFolderId", item.localFolder.id)
scanResult?.localLibraryItem?.let {
put("localLibraryItem", JSObject(jacksonMapper.writeValueAsString(it)))
}
scanResult?.localMediaProgress?.let {
put("localMediaProgress", JSObject(jacksonMapper.writeValueAsString(it)))
}
}
clientEventEmitter.onDownloadItemComplete(event)
synchronized(this@DownloadItemManager) {
downloadItemQueue.remove(item)
DeviceManager.dbManager.removeDownloadItem(item.id)
}
}
}
} catch (e: Exception) {
// The files are already in place, so leave the item queued for restoreQueue to rescan.
AbsLogger.error(tag, "Could not finalize download item ${item.id}: ${e.message}")
} finally {
synchronized(this@DownloadItemManager) {
finalizingItems.remove(item.id)
notifyQueueChanged()
}
}
}
}
private fun tryReserve(part: DownloadItemPart): Boolean {
val staging = File(part.destinationPath)
staging.parentFile?.mkdirs()
val expectedSize = if (part.fileSize > 0L) part.fileSize else UNKNOWN_PART_RESERVATION_BYTES
val remaining =
(expectedSize - (staging.takeIf(File::exists)?.length() ?: 0L)).coerceAtLeast(0L)
val required = if (part.isInternalStorage) remaining else remaining + expectedSize
val key = storageKey(staging)
val fs = statFsFor(staging)
val headroom = max(MIN_FREE_SPACE_BYTES, fs.totalBytes / 20L)
val alreadyReserved = reservations.filterKeys { storageKey(File(it)) == key }.values.sum()
if (fs.availableBytes - alreadyReserved < required + headroom) return false
reservations[part.destinationPath] = required
return true
}
private fun hasAvailableSpace(part: DownloadItemPart): Boolean {
val staging = File(part.destinationPath)
val fs = statFsFor(staging)
return fs.availableBytes >= max(MIN_FREE_SPACE_BYTES, fs.totalBytes / 20L)
}
private fun statFsFor(staging: File): StatFs {
var directory = staging.parentFile ?: context.filesDir
directory.mkdirs()
while (!directory.exists()) directory = directory.parentFile ?: context.filesDir
return StatFs(directory.absolutePath)
}
private fun storageKey(file: File): String =
if (file.absolutePath.startsWith(context.filesDir.absolutePath)) "internal"
else "external"
@Synchronized
private fun removeActivePart(part: DownloadItemPart) {
activeCalls.remove(part.id)
currentDownloadItemParts.remove(part)
}
private fun persist(item: DownloadItem, force: Boolean = false) {
val now = System.currentTimeMillis()
if (!force && now - (lastPersistTime[item.id] ?: 0L) < PERSIST_INTERVAL_MS) return
lastPersistTime[item.id] = now
DeviceManager.dbManager.saveDownloadItem(item)
}
private fun notifyQueueChanged() {
clientEventEmitter.onQueueChanged(hasWork())
}
fun destroy() {
activeCalls.values.forEach(InternalDownloadManager.DownloadHandle::cancel)
activeCalls.clear()
scope.cancel()
}
private fun getOrCreateFolder(root: DocumentFile, relativePath: String): DocumentFile? {
var current = root
relativePath.split('/').filter { it.isNotBlank() }.forEach { segment ->
if (segment == "." || segment == "..") return null
current = current.findFile(segment) ?: current.createDirectory(segment) ?: return null
}
return current
}
private fun findSharedStorageFile(part: DownloadItemPart): DocumentFile? {
if (part.isInternalStorage) return null
val root = DocumentFile.fromTreeUri(context, Uri.parse(part.localFolderUrl)) ?: return null
var folder = root
part.finalDestinationSubfolder.split('/').filter { it.isNotBlank() }.forEach { segment ->
if (segment == "." || segment == "..") return null
folder = folder.findFile(segment) ?: return null
}
val file = findDocumentByFilename(folder, part) ?: return null
if (!file.isFile) return null
if (part.fileSize > 0L && file.length() != part.fileSize) return null
if (part.fileSize <= 0L && file.length() <= 0L) return null
return file
}
private fun completeFromExistingInternalCover(
item: DownloadItem,
part: DownloadItemPart
): Boolean {
if (!part.isInternalStorage || !part.serverPath.endsWith("/cover")) return false
val file = File(part.finalDestinationPath)
if (!file.isFile || file.length() <= 0L) return false
if (part.fileSize > 0L && file.length() != part.fileSize) return false
part.bytesDownloaded = file.length()
part.progress = 100L
part.reusedExistingFile = true
AbsLogger.info(tag, "Reusing existing cover ${part.filename}")
File(part.destinationPath).delete()
completePart(item, part)
clientEventEmitter.onDownloadItemPartUpdate(part)
return true
}
private fun hasActiveDestinationConflict(part: DownloadItemPart): Boolean =
currentDownloadItemParts.any { activePart ->
activePart !== part &&
activePart.localFolderId == part.localFolderId &&
activePart.finalDestinationPath == part.finalDestinationPath
}
private fun leaveQueued(item: DownloadItem, part: DownloadItemPart) {
if (!part.waitingForSpace) return
part.waitingForSpace = false
part.downloadId = null
persist(item)
clientEventEmitter.onDownloadItemPartUpdate(part)
}
private fun scanDestinationKey(item: DownloadItem): String =
"${item.localFolder.id}:${item.itemFolderPath}"
private fun finalizedFileExists(part: DownloadItemPart): Boolean {
if (part.isInternalStorage) {
val file = File(part.finalDestinationPath)
return file.isFile &&
if (part.fileSize > 0L) file.length() == part.fileSize else file.length() > 0L
}
part.completedDestinationUri?.let { uri ->
try {
val file = DocumentFile.fromSingleUri(context, Uri.parse(uri))
if (file?.isFile == true && (part.fileSize <= 0L || file.length() == part.fileSize))
return true
} catch (e: Exception) {
AbsLogger.error(tag, "Could not validate SAF file ${part.filename}: ${e.message}")
}
}
return findSharedStorageFile(part) != null
}
/** Resets an unmoved part when recovery crosses a service-session boundary. */
private fun resetPartForFreshDownload(part: DownloadItemPart): Boolean {
val stagingFile = File(part.destinationPath)
if (stagingFile.exists() && !stagingFile.delete()) {
AbsLogger.error(tag, "Could not delete staging file ${part.filename}")
part.failed = true
return false
}
part.completed = false
part.bytesDownloaded = 0L
part.progress = 0L
part.failed = false
part.isMoving = false
part.downloadId = null
part.retryCount = 0
part.authRetryCount = 0
part.waitingForSpace = false
part.reusedExistingFile = false
return true
}
private fun findDocumentByFilename(folder: DocumentFile, part: DownloadItemPart): DocumentFile? {
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 ||
document.fullName.substringBeforeLast('.') == expectedBaseName))
}
}
private fun mimeTypeFor(part: DownloadItemPart): String =
part.audioTrack?.mimeType
?: when (part.ebookFile?.ebookFormat?.lowercase()) {
"epub" -> "application/epub+zip"
"pdf" -> "application/pdf"
else -> "image/jpeg"
}
private fun serverUrl(item: DownloadItem, part: DownloadItemPart): String {
val rawCover = if (part.serverPath.endsWith("/cover")) "?raw=1" else ""
return "${item.serverAddress}${part.serverPath}$rawCover"
}
private companion object {
const val APP_MANAGED_DOWNLOAD_ID = -1L
const val MAX_SIMULTANEOUS_DOWNLOADS = 3
const val WATCH_INTERVAL_MS = 1_000L
const val STALL_TIMEOUT_MS = 60_000L
const val MAX_RETRIES = 5
const val MAX_AUTH_RETRIES = 2
const val PERSIST_INTERVAL_MS = 2_000L
const val MIN_FREE_SPACE_BYTES = 100L * 1024L * 1024L
const val UNKNOWN_PART_RESERVATION_BYTES = 100L * 1024L * 1024L
}
}
@@ -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()
}
}
@@ -0,0 +1,203 @@
package com.audiobookshelf.app.managers
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
/** 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"
interface DownloadHandle {
fun cancel()
}
private class ActiveDownloadHandle : DownloadHandle {
private val cancelled = AtomicBoolean(false)
private val activeCall = AtomicReference<Call?>()
fun setCall(call: Call) {
activeCall.set(call)
if (cancelled.get()) call.cancel()
}
override fun cancel() {
cancelled.set(true)
activeCall.get()?.cancel()
}
}
/**
* 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
}
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()
}
}
@@ -0,0 +1,544 @@
package com.audiobookshelf.app.managers
import android.content.Context
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 com.audiobookshelf.app.plugins.AbsLogger
import java.util.*
import kotlin.concurrent.schedule
import kotlin.math.roundToInt
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 autoTimerDisabled: Boolean = false // Disable until out of auto timer period
private var sleepTimerSessionId: String = ""
/**
* Gets the current time from the player notification service.
* @return Long - the current time in milliseconds.
*/
private fun getCurrentTime(): Long {
return playerNotificationService.getCurrentTime()
}
/**
* Gets the duration of the current playback.
* @return Long - the duration in milliseconds.
*/
private fun getDuration(): Long {
return playerNotificationService.getDuration()
}
/**
* 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
}
/**
* 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()
}
/**
* 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() / speed).roundToInt()
}
/**
* 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)
if (time == 0L) {
// Get the current chapter time and set the sleep timer to the end of the chapter
val chapterEndTime = this.getChapterEndTime()
if (chapterEndTime == null) {
Log.e(tag, "Setting sleep timer to end of chapter/track but there is no current session")
return false
}
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 {
sleepTimerEndTime = 0L
}
// Set sleep timer length. Will be 0L if using chapter end time
sleepTimerLength = time
// Register shake sensor
playerNotificationService.registerSensor()
playerNotificationService.clientEventEmitter?.onSleepTimerSet(
getSleepTimerTimeRemainingSeconds(getPlaybackSpeed()),
isAutoSleepTimer
)
sleepTimerTask =
Timer("SleepTimer", false).schedule(0L, 1000L) {
Handler(Looper.getMainLooper()).post {
if (getIsPlaying()) {
sleepTimerElapsed += 1000L
val sleepTimeSecondsRemaining =
getSleepTimerTimeRemainingSeconds(getPlaybackSpeed())
Log.d(
tag,
"Timer Elapsed $sleepTimerElapsed | Sleep TIMER time remaining $sleepTimeSecondsRemaining s"
)
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
}
/**
* 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
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
sleepTimerEndTime = 0
sleepTimerRunning = false
playerNotificationService.unregisterSensor()
setVolume(1f)
}
/**
* 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 for this time period")
autoTimerDisabled = true
}
clearSleepTimer()
playerNotificationService.clientEventEmitter?.onSleepTimerSet(0, false)
}
/** 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
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager =
context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
vibrator = vibratorManager.defaultVibrator
} else {
@Suppress("DEPRECATION")
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
}
vibrator.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val vibrationEffect = VibrationEffect.createWaveform(longArrayOf(0, 150, 150, 150), -1)
it.vibrate(vibrationEffect)
} else {
@Suppress("DEPRECATION") it.vibrate(10)
}
}
}
/** 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")
return null
}
val timeLeftInChapter = currentChapterEndTimeMs - getCurrentTime()
// 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"
)
null
} else {
nextChapterEndTimeMs
}
} else {
currentChapterEndTimeMs
}
}
/**
* 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) {
// 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
if (finishedAtDistance > SLEEP_TIMER_WAKE_UP_EXPIRATION) // 2 minutes
{
Log.d(tag, "Sleep timer finished over 2 mins ago, clearing it")
sleepTimerFinishedAt = 0L
return
}
// 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
Log.d(tag, "Resetting stopped sleep timer")
vibrateFeedback()
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 && getIsPlaying()) || sleepTimerFinishedAt > 0L) {
if (DeviceManager.deviceData.deviceSettings?.disableShakeToResetSleepTimer == true) {
Log.d(tag, "Shake to reset sleep timer is disabled")
return
}
checkShouldResetSleepTimer()
}
}
/**
* 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
sleepTimerLength = minOf(sleepTimerLength, getDuration() - getCurrentTime())
} else {
// Chapter end time
sleepTimerEndTime =
minOf(sleepTimerEndTime + (time * getPlaybackSpeed()).roundToInt(), getDuration())
}
setVolume(1F)
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) {
// Fixed length
sleepTimerLength = maxOf(sleepTimerLength - time, 1000L)
} else {
// Chapter end time
sleepTimerEndTime =
maxOf(
sleepTimerEndTime - (time * getPlaybackSpeed()).roundToInt(),
getCurrentTime() + 1000
)
}
setVolume(1F)
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
}
DeviceManager.deviceData.deviceSettings?.let { deviceSettings ->
if (!deviceSettings.autoSleepTimer) return // Check auto sleep timer is enabled
val startCalendar = Calendar.getInstance()
startCalendar.set(Calendar.HOUR_OF_DAY, deviceSettings.autoSleepTimerStartHour)
startCalendar.set(Calendar.MINUTE, deviceSettings.autoSleepTimerStartMinute)
val endCalendar = Calendar.getInstance()
endCalendar.set(Calendar.HOUR_OF_DAY, deviceSettings.autoSleepTimerEndHour)
endCalendar.set(Calendar.MINUTE, deviceSettings.autoSleepTimerEndMinute)
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.
// e.g. start time 22:00 and end time 06:00.
// If current time is less than start time (e.g. 00:30) then start time will be the
// previous day.
// If current time is greater than start time (e.g. 23:00) then end time will be the
// next day.
if (endCalendar.before(startCalendar)) {
if (currentCalendar.before(startCalendar)) { // Shift start back a day
startCalendar.add(Calendar.DAY_OF_MONTH, -1)
} else { // Shift end forward a day
endCalendar.add(Calendar.DAY_OF_MONTH, 1)
}
}
val isDuringAutoTime =
currentCalendar.after(startCalendar) && currentCalendar.before(endCalendar)
// 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 {
// Auto time is disabled, do not set sleep timer
Log.i(tag, "Auto sleep timer is disabled for this time period")
}
} else {
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")
}
}
}
}
/**
* 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
if (sleepTimerSessionId == playbackSessionId || sleepTimerRunning) {
checkShouldResetSleepTimer()
} 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)
}
}
}
@@ -0,0 +1,132 @@
package com.audiobookshelf.app.media
import android.util.Log
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.player.PlayerNotificationService
object MediaEventManager {
const val tag = "MediaEventManager"
var clientEventEmitter: PlayerNotificationService.ClientEventEmitter? = null
fun playEvent(playbackSession: PlaybackSession) {
Log.i(tag, "Play Event for media \"${playbackSession.displayTitle}\"")
addPlaybackEvent("Play", playbackSession, null)
}
fun pauseEvent(playbackSession: PlaybackSession, syncResult: SyncResult?) {
Log.i(tag, "Pause Event for media \"${playbackSession.displayTitle}\"")
addPlaybackEvent("Pause", playbackSession, syncResult)
}
fun stopEvent(playbackSession: PlaybackSession, syncResult: SyncResult?) {
Log.i(tag, "Stop Event for media \"${playbackSession.displayTitle}\"")
addPlaybackEvent("Stop", playbackSession, syncResult)
}
fun saveEvent(playbackSession: PlaybackSession, syncResult: SyncResult?) {
Log.i(tag, "Save Event for media \"${playbackSession.displayTitle}\"")
addPlaybackEvent("Save", playbackSession, syncResult)
}
fun finishedEvent(playbackSession: PlaybackSession, syncResult: SyncResult?) {
Log.i(tag, "Finished Event for media \"${playbackSession.displayTitle}\"")
addPlaybackEvent("Finished", playbackSession, syncResult)
}
fun seekEvent(playbackSession: PlaybackSession, syncResult: SyncResult?) {
Log.i(
tag,
"Seek Event for media \"${playbackSession.displayTitle}\", currentTime=${playbackSession.currentTime}"
)
addPlaybackEvent("Seek", playbackSession, syncResult)
}
fun syncEvent(mediaProgress: MediaProgressWrapper, description: String) {
Log.i(
tag,
"Sync Event for media item id \"${mediaProgress.mediaItemId}\", currentTime=${mediaProgress.currentTime}"
)
addSyncEvent("Sync", mediaProgress, description)
}
private fun addSyncEvent(
eventName: String,
mediaProgress: MediaProgressWrapper,
description: String
) {
val mediaItemHistory = getMediaItemHistoryMediaItem(mediaProgress.mediaItemId)
if (mediaItemHistory == null) {
Log.w(
tag,
"addSyncEvent: Media Item History not created yet for media item id ${mediaProgress.mediaItemId}"
)
return
}
val mediaItemEvent =
MediaItemEvent(
name = eventName,
type = "Sync",
description = description,
currentTime = mediaProgress.currentTime,
serverSyncAttempted = false,
serverSyncSuccess = null,
serverSyncMessage = null,
timestamp = System.currentTimeMillis()
)
mediaItemHistory.events.add(mediaItemEvent)
DeviceManager.dbManager.saveMediaItemHistory(mediaItemHistory)
clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory)
}
private fun addPlaybackEvent(
eventName: String,
playbackSession: PlaybackSession,
syncResult: SyncResult?
) {
val mediaItemHistory =
getMediaItemHistoryMediaItem(playbackSession.mediaItemId)
?: createMediaItemHistoryForSession(playbackSession)
val mediaItemEvent =
MediaItemEvent(
name = eventName,
type = "Playback",
description = "",
currentTime = playbackSession.currentTime,
serverSyncAttempted = syncResult?.serverSyncAttempted ?: false,
serverSyncSuccess = syncResult?.serverSyncSuccess,
serverSyncMessage = syncResult?.serverSyncMessage,
timestamp = System.currentTimeMillis()
)
mediaItemHistory.events.add(mediaItemEvent)
DeviceManager.dbManager.saveMediaItemHistory(mediaItemHistory)
clientEventEmitter?.onMediaItemHistoryUpdated(mediaItemHistory)
}
private fun getMediaItemHistoryMediaItem(mediaItemId: String): MediaItemHistory? {
return DeviceManager.dbManager.getMediaItemHistory(mediaItemId)
}
private fun createMediaItemHistoryForSession(playbackSession: PlaybackSession): MediaItemHistory {
Log.i(tag, "Creating new media item history for media \"${playbackSession.displayTitle}\"")
val libraryItemId = playbackSession.libraryItemId ?: ""
val episodeId: String? = playbackSession.episodeId
return MediaItemHistory(
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()
)
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,360 @@
package com.audiobookshelf.app.media
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.audiobookshelf.app.data.LocalMediaProgress
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
)
data class SyncResult(
var serverSyncAttempted: Boolean,
var serverSyncSuccess: Boolean?,
var serverSyncMessage: String?
)
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
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
fun start(playbackSession: PlaybackSession) {
if (listeningTimerRunning) {
Log.d(tag, "start: Timer already running for $currentDisplayTitle")
if (playbackSession.id != currentSessionId) {
Log.d(tag, "Playback session changed, reset timer")
currentLocalMediaProgress = null
listeningTimerTask?.cancel()
lastSyncTime = 0L
Log.d(tag, "start: Set last sync time 0 $lastSyncTime")
failedSyncs = 0
} else {
return
}
} else if (playbackSession.id != currentSessionId) {
currentLocalMediaProgress = null
}
listeningTimerRunning = true
lastSyncTime = System.currentTimeMillis()
currentPlaybackSession = playbackSession.clone()
Log.d(
tag,
"start: init last sync time $lastSyncTime with playback session id=${currentPlaybackSession?.id}"
)
listeningTimerTask =
Timer("ListeningTimer", false).schedule(15000L, 15000L) {
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
val currentTime = playerNotificationService.getCurrentTimeSeconds()
if (currentTime > 0) {
sync(shouldSyncServer, currentTime) { syncResult ->
Log.d(tag, "Sync complete")
currentPlaybackSession?.let { playbackSession ->
MediaEventManager.saveEvent(playbackSession, syncResult)
}
}
}
}
}
}
}
fun play(playbackSession: PlaybackSession) {
Log.d(tag, "play ${playbackSession.displayTitle}")
MediaEventManager.playEvent(playbackSession)
start(playbackSession)
}
fun stop(shouldSync: Boolean? = true, cb: () -> Unit) {
if (!listeningTimerRunning) {
reset()
return cb()
}
listeningTimerTask?.cancel()
listeningTimerTask = null
listeningTimerRunning = false
Log.d(tag, "stop: Stopping listening for $currentDisplayTitle")
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 ->
MediaEventManager.stopEvent(playbackSession, syncResult)
}
reset()
cb()
}
} else {
currentPlaybackSession?.let { playbackSession ->
MediaEventManager.stopEvent(playbackSession, null)
}
reset()
cb()
}
}
fun pause(cb: () -> Unit) {
if (!listeningTimerRunning) return
listeningTimerTask?.cancel()
listeningTimerTask = null
listeningTimerRunning = false
Log.d(tag, "pause: Pausing progress syncer for $currentDisplayTitle")
Log.d(tag, "pause: Last sync time $lastSyncTime")
val currentTime = playerNotificationService.getCurrentTimeSeconds()
if (currentTime > 0) { // Current time should always be > 0 on pause
sync(true, currentTime) { syncResult ->
lastSyncTime = 0L
Log.d(tag, "pause: Set last sync time 0 $lastSyncTime")
failedSyncs = 0
currentPlaybackSession?.let { playbackSession ->
MediaEventManager.pauseEvent(playbackSession, syncResult)
}
cb()
}
} else {
lastSyncTime = 0L
Log.d(tag, "pause: Set last sync time 0 $lastSyncTime (current time < 0)")
failedSyncs = 0
currentPlaybackSession?.let { playbackSession ->
MediaEventManager.pauseEvent(playbackSession, null)
}
cb()
}
}
fun finished(cb: () -> Unit) {
if (!listeningTimerRunning) return
listeningTimerTask?.cancel()
listeningTimerTask = null
listeningTimerRunning = false
Log.d(tag, "finished: Stopping listening for $currentDisplayTitle")
sync(true, currentPlaybackSession?.duration ?: 0.0) { syncResult ->
reset()
currentPlaybackSession?.let { playbackSession ->
MediaEventManager.finishedEvent(playbackSession, syncResult)
}
cb()
}
}
fun seek() {
currentPlaybackSession?.currentTime = playerNotificationService.getCurrentTimeSeconds()
Log.d(tag, "seek: $currentDisplayTitle, currentTime=${currentPlaybackSession?.currentTime}")
if (currentPlaybackSession == null) {
Log.e(tag, "seek: Playback session not set")
return
}
MediaEventManager.seekEvent(currentPlaybackSession!!, null)
}
// 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"
)
saveLocalProgress(it)
}
}
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)
}
val diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
if (diffSinceLastSync < 1000L) {
return cb(null)
}
val listeningTimeToAdd = diffSinceLastSync / 1000L
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()}"
)
return cb(null)
}
val hasNetworkConnection = DeviceManager.checkConnectivity(playerNotificationService)
// Save playback session to db (server linked sessions only)
// Sessions are removed once successfully synced with the server
currentPlaybackSession?.let { DeviceManager.dbManager.savePlaybackSession(it) }
if (currentIsLocal) {
// Save local progress sync
currentPlaybackSession?.let {
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
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
}
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) {
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) {
AbsLogger.info("MediaProgressSyncer", "sync: Successfully synced progress (title: \"$currentDisplayTitle\") (currentTime: $currentTime) (session id: ${currentSessionId}) (${DeviceManager.serverConnectionConfigName})")
failedSyncs = 0
playerNotificationService.alertSyncSuccess()
lastSyncTime = System.currentTimeMillis()
DeviceManager.dbManager.removePlaybackSession(currentSessionId) // Remove session from db
} else {
failedSyncs++
if (failedSyncs == 2) {
playerNotificationService.alertSyncFailing() // Show alert in client
failedSyncs = 0
}
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) {
if (currentLocalMediaProgress == null) {
val mediaProgress =
DeviceManager.dbManager.getLocalMediaProgress(playbackSession.localMediaProgressId)
if (mediaProgress == null) {
currentLocalMediaProgress = playbackSession.getNewLocalMediaProgress()
} else {
currentLocalMediaProgress = mediaProgress
currentLocalMediaProgress?.updateFromPlaybackSession(playbackSession)
}
} else {
currentLocalMediaProgress?.updateFromPlaybackSession(playbackSession)
}
currentLocalMediaProgress?.let {
if (it.progress.isNaN()) {
Log.e(tag, "Invalid progress on local media progress")
} 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}%"
)
}
}
}
fun reset() {
currentPlaybackSession = null
currentLocalMediaProgress = null
lastSyncTime = 0L
Log.d(tag, "reset: Set last sync time 0 $lastSyncTime")
failedSyncs = 0
}
}
@@ -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))
}
@@ -0,0 +1,50 @@
package com.audiobookshelf.app.models
import com.audiobookshelf.app.data.LocalFolder
import com.audiobookshelf.app.data.MediaProgress
import com.audiobookshelf.app.data.MediaType
import com.fasterxml.jackson.annotation.JsonIgnore
data class DownloadItem(
val id: String,
val libraryItemId: String,
val episodeId: String?,
val userMediaProgress: MediaProgress?,
val serverConnectionConfigId: String,
val serverAddress: String,
val serverUserId: String,
val mediaType: String,
val itemFolderPath: String,
val localFolder: LocalFolder,
val itemTitle: String,
val itemSubfolder: String,
val media: MediaType,
val downloadItemParts: MutableList<DownloadItemPart>,
@JsonIgnore var terminalFailureAt: Long? = null,
@JsonIgnore var stagingCleanupAt: Long? = null
) {
@get:JsonIgnore
val isInternalStorage
get() = localFolder.id.startsWith("internal-")
@get:JsonIgnore
val isDownloadFinished
get() = downloadItemParts.isNotEmpty() && downloadItemParts.all {
it.completed && it.moved && !it.isMoving && !it.failed
}
@JsonIgnore
fun getNextDownloadItemParts(limit: Int): MutableList<DownloadItemPart> {
val itemParts = mutableListOf<DownloadItemPart>()
if (limit == 0) return itemParts
for (it in downloadItemParts) {
if (!it.completed && !it.failed && it.downloadId == null) {
itemParts.add(it)
if (itemParts.size >= limit) break
}
}
return itemParts
}
}
@@ -0,0 +1,88 @@
package com.audiobookshelf.app.models
import android.net.Uri
import android.util.Log
import com.audiobookshelf.app.data.AudioTrack
import com.audiobookshelf.app.data.EBookFile
import com.audiobookshelf.app.data.LocalFolder
import com.audiobookshelf.app.data.PodcastEpisode
import com.audiobookshelf.app.device.DeviceManager
import com.fasterxml.jackson.annotation.JsonIgnore
import java.io.File
data class DownloadItemPart(
val id: String,
val downloadItemId: String,
val filename: String,
var fileSize: Long,
@JsonIgnore val destinationPath: String,
val finalDestinationPath:String,
val serverPath: String,
val localFolderName: String,
val localFolderUrl: String,
val localFolderId: String,
val ebookFile: EBookFile?,
val audioTrack: AudioTrack?,
val episode: PodcastEpisode?,
var completed:Boolean,
var moved:Boolean,
var isMoving:Boolean,
var failed:Boolean,
@JsonIgnore val uri: Uri,
@JsonIgnore val destinationUri: Uri,
@JsonIgnore val finalDestinationUri: Uri,
@JsonIgnore var completedDestinationUri: String?,
val finalDestinationSubfolder: String,
var downloadId: Long?,
@JsonIgnore var lastUpdateTime: Long?,
var progress: 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")
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,
localFolderUrl = localFolder.contentUrl,
localFolderId = localFolder.id,
ebookFile = ebookFile,
audioTrack = audioTrack,
episode = episode,
completed = false,
moved = false,
isMoving = false,
failed = false,
uri = downloadUri,
destinationUri = destinationUri,
finalDestinationUri = finalDestinationUri,
completedDestinationUri = null,
finalDestinationSubfolder = subfolder,
downloadId = null,
lastUpdateTime = null,
progress = 0,
bytesDownloaded = 0,
reusedExistingFile = false
)
}
}
@get:JsonIgnore
val isInternalStorage get() = localFolderId.startsWith("internal-")
}
@@ -0,0 +1,11 @@
package com.audiobookshelf.app.models
import com.audiobookshelf.app.data.MediaProgress
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
@JsonIgnoreProperties(ignoreUnknown = true)
data class User(
val id:String,
val username: String,
val mediaProgress:List<MediaProgress>
)
@@ -2,31 +2,22 @@ package com.audiobookshelf.app.player
import android.app.PendingIntent
import android.graphics.Bitmap
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 androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.file.getAbsolutePath
import com.anggrayudi.storage.file.toRawFile
import com.audiobookshelf.app.R
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.RequestOptions
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import kotlinx.coroutines.*
const val NOTIFICATION_LARGE_ICON_SIZE = 144 // px
class AbMediaDescriptionAdapter constructor(private val controller: MediaControllerCompat, val playerNotificationService: PlayerNotificationService) : PlayerNotificationManager.MediaDescriptionAdapter {
class AbMediaDescriptionAdapter (private val controller: MediaControllerCompat, private val playerNotificationService: PlayerNotificationService) : PlayerNotificationManager.MediaDescriptionAdapter {
private val tag = "MediaDescriptionAdapter"
var currentIconUri: Uri? = null
var currentBitmap: Bitmap? = null
private var currentIconUri: Uri? = null
private var currentBitmap: Bitmap? = null
private val glideOptions = RequestOptions()
.fallback(R.drawable.icon)
.diskCacheStrategy(DiskCacheStrategy.DATA)
private val serviceJob = SupervisorJob()
private val serviceScope = CoroutineScope(Dispatchers.Main + serviceJob)
@@ -42,59 +33,40 @@ 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")
serviceScope.launch {
currentBitmap = albumArtUri?.let {
resolveUriAsBitmap(it)
if (currentIconUri.toString().startsWith("content://")) {
currentBitmap = if (Build.VERSION.SDK_INT < 28) {
@Suppress("DEPRECATION")
MediaStore.Images.Media.getBitmap(playerNotificationService.contentResolver, currentIconUri)
} else {
val source: ImageDecoder.Source = ImageDecoder.createSource(playerNotificationService.contentResolver, currentIconUri!!)
ImageDecoder.decodeBitmap(source)
}
currentBitmap?.let { callback.onBitmap(it) }
currentBitmap
} else {
serviceScope.launch {
currentBitmap = albumArtUri?.let {
resolveUriAsBitmap(playerNotificationService, it)
}
currentBitmap?.let { callback.onBitmap(it) }
}
null
}
null
} else {
currentBitmap
}
}
private suspend fun resolveUriAsBitmap(uri: Uri): Bitmap? {
return withContext(Dispatchers.IO) {
// Block on downloading artwork.
val context = playerNotificationService.getContext()
// Fix attempt for #35 local cover crashing
// Convert content uri to a file and pass to Glide
var urival:Any = uri
if (uri.toString().startsWith("content:")) {
val imageDocFile = DocumentFile.fromSingleUri(context, uri)
Log.d(tag, "Converting local content url $uri to file with path ${imageDocFile?.getAbsolutePath(context)}")
val file = imageDocFile?.toRawFile(context)
file?.let {
Log.d(tag, "Using local file image instead of content uri ${it.absolutePath}")
urival = it
}
}
try {
Glide.with(context).applyDefaultRequestOptions(glideOptions)
.asBitmap()
.load(urival)
.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(context).applyDefaultRequestOptions(glideOptions)
.asBitmap()
.load(Uri.parse("android.resource://com.audiobookshelf.app/" + R.drawable.icon))
.submit(NOTIFICATION_LARGE_ICON_SIZE, NOTIFICATION_LARGE_ICON_SIZE)
.get()
}
}
}
}
@@ -1,50 +1,33 @@
package com.audiobookshelf.app.player
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import android.support.v4.media.MediaMetadataCompat
import android.util.Log
import androidx.annotation.AnyRes
import com.audiobookshelf.app.R
import com.audiobookshelf.app.data.LibraryCategory
import com.audiobookshelf.app.data.LibraryItem
import com.audiobookshelf.app.data.LocalLibraryItem
import com.audiobookshelf.app.data.*
import com.audiobookshelf.app.media.getUriToDrawable
class BrowseTree(
val context: Context,
libraryCategories: List<LibraryCategory>
itemsInProgress: List<ItemInProgress>,
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 context - context
* @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))
}
init {
val rootList = mediaIdToChildren[AUTO_BROWSE_ROOT] ?: mutableListOf()
val continueReadingMetadata = MediaMetadataCompat.Builder().apply {
val continueListeningMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, CONTINUE_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Listening")
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Continue")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_localaudio).toString())
}.build()
val allMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, ALL_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Library Items")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_books).toString())
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 {
@@ -53,50 +36,44 @@ class BrowseTree(
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.exo_icon_downloaddone).toString())
}.build()
// Server continue Listening cat
libraryCategories.find { it.id == "continue-listening" }?.let { continueListeningCategory ->
var continueListeningMediaMetadata = continueListeningCategory.entities.map { liw ->
var libraryItem = liw as LibraryItem
libraryItem.getMediaMetadata()
val librariesMetadata = MediaMetadataCompat.Builder().apply {
putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, LIBRARIES_ROOT)
putString(MediaMetadataCompat.METADATA_KEY_TITLE, "Libraries")
putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ART_URI, getUriToDrawable(context, R.drawable.icon_library_folder).toString())
}.build()
if (itemsInProgress.isNotEmpty()) {
rootList += continueListeningMetadata
}
if (libraries.isNotEmpty()) {
if (recentsLoaded) {
rootList += recentMetadata
}
if (continueListeningMediaMetadata.isNotEmpty()) {
rootList += continueReadingMetadata
}
continueListeningMediaMetadata.forEach {
val children = mediaIdToChildren[CONTINUE_ROOT] ?: mutableListOf()
children += it
mediaIdToChildren[CONTINUE_ROOT] = children
rootList += librariesMetadata
libraries.forEach { library ->
// 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
}
}
}
rootList += allMetadata
rootList += downloadsMetadata
// Server library cat
libraryCategories.find { it.id == "library" }?.let { libraryCategory ->
var libraryMediaMetadata = libraryCategory.entities.map { libc ->
var libraryItem = libc as LibraryItem
libraryItem.getMediaMetadata()
}
libraryMediaMetadata.forEach {
val children = mediaIdToChildren[ALL_ROOT] ?: mutableListOf()
children += it
mediaIdToChildren[ALL_ROOT] = children
}
}
libraryCategories.find { it.id == "local-books" }?.let { localBooksCat ->
var localMediaMetadata = localBooksCat.entities.map { libc ->
var libraryItem = libc as LocalLibraryItem
libraryItem.getMediaMetadata()
}
localMediaMetadata.forEach {
val children = mediaIdToChildren[DOWNLOADS_ROOT] ?: mutableListOf()
children += it
mediaIdToChildren[DOWNLOADS_ROOT] = children
}
}
mediaIdToChildren[AUTO_BROWSE_ROOT] = rootList
}
@@ -104,6 +81,7 @@ class BrowseTree(
}
const val AUTO_BROWSE_ROOT = "/"
const val ALL_ROOT = "__ALL__"
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,7 +148,14 @@ class CastManager constructor(val mainActivity:Activity) {
}
fun startRouteScan(connListener:ChromecastListener) {
var callback = object : ScanCallback() {
// 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}")
// if the routes have changed, we may have an available device
@@ -136,7 +168,7 @@ class CastManager constructor(val mainActivity:Activity) {
// Let the client know a receiver is available
connListener.onReceiverAvailableUpdate(true)
// Since we have a receiver we may also have an active session
var session = getSessionManager()?.currentCastSession
val session = getSessionManager()?.currentCastSession
// If we do have a session
if (session != null) {
// Let the client know
@@ -246,15 +278,15 @@ class CastManager constructor(val mainActivity:Activity) {
onRouteUpdate(outRoutes)
}
override fun onRouteAdded(router: MediaRouter?, route: MediaRouter.RouteInfo?) {
override fun onRouteAdded(router: MediaRouter, route: MediaRouter.RouteInfo) {
onFilteredRouteUpdate()
}
override fun onRouteChanged(router: MediaRouter?, route: MediaRouter.RouteInfo?) {
override fun onRouteChanged(router: MediaRouter, route: MediaRouter.RouteInfo) {
onFilteredRouteUpdate()
}
override fun onRouteRemoved(router: MediaRouter?, route: MediaRouter.RouteInfo?) {
override fun onRouteRemoved(router: MediaRouter, route: MediaRouter.RouteInfo) {
onFilteredRouteUpdate()
}
}
@@ -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
@@ -11,12 +12,12 @@ import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.C.TrackType
import com.google.android.exoplayer2.MediaMetadata
import com.google.android.exoplayer2.Player.*
import com.google.android.exoplayer2.TracksInfo.TrackGroupInfo
import com.google.android.exoplayer2.Tracks.Group
import com.google.android.exoplayer2.audio.AudioAttributes
import com.google.android.exoplayer2.ext.cast.SessionAvailabilityListener
import com.google.android.exoplayer2.source.TrackGroup
import com.google.android.exoplayer2.source.TrackGroupArray
import com.google.android.exoplayer2.text.Cue
import com.google.android.exoplayer2.text.CueGroup
import com.google.android.exoplayer2.trackselection.TrackSelection
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
import com.google.android.exoplayer2.trackselection.TrackSelectionParameters
@@ -49,10 +50,14 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
private var myPlayWhenReady:Boolean = false
private var playbackParameters:PlaybackParameters = PlaybackParameters.DEFAULT
private var myAvailableCommands: Commands
private var myCurrentTracksInfo: TracksInfo
private var myCurrentTracksInfo: Tracks
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_TRACK_INFOS)
COMMAND_GET_TRACKS,
COMMAND_GET_DEVICE_VOLUME,
COMMAND_SET_DEVICE_VOLUME,
COMMAND_ADJUST_DEVICE_VOLUME)
.build()
var currentPlaybackState = Player.STATE_IDLE
@@ -88,14 +96,14 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
private var currentMediaItemIndex = 0
private var pendingMediaItemRemovalPosition:PositionInfo? = null
private var pendingSeekCount = 0
private var pendingSeekWindowIndex = 0
private var pendingSeekWindowIndex = C.INDEX_UNSET
private var pendingSeekPositionMs = 0L
init {
Log.d(tag, "Init CastPlayer")
myCurrentTrackGroups = TrackGroupArray.EMPTY
myCurrentTrackSelections = EMPTY_TRACK_SELECTION_ARRAY
myCurrentTracksInfo = TracksInfo.EMPTY
myCurrentTracksInfo = Tracks.EMPTY
statusListener = StatusListener()
timelineTracker = CastTimelineTracker()
myCurrentTimeline = CastTimeline.EMPTY_CAST_TIMELINE
@@ -385,7 +393,7 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
}
if (updateTracksAndSelectionsAndNotifyIfChanged()) {
listeners.queueEvent(
EVENT_TRACKS_CHANGED) { listener: Listener -> listener.onTracksInfoChanged(currentTracksInfo) }
EVENT_TRACKS_CHANGED) { listener: Listener -> listener.onTracksChanged(currentTracks) }
}
updateAvailableCommandsAndNotifyIfChanged()
listeners.flushEvents()
@@ -405,7 +413,7 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
val hasChanged = !myCurrentTrackGroups.isEmpty
myCurrentTrackGroups = TrackGroupArray.EMPTY
myCurrentTrackSelections = EMPTY_TRACK_SELECTION_ARRAY
myCurrentTracksInfo = TracksInfo.EMPTY
myCurrentTracksInfo = Tracks.EMPTY
return hasChanged
}
var activeTrackIds = mediaStatus.activeTrackIds
@@ -414,7 +422,7 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
}
val trackGroups = arrayOfNulls<TrackGroup>(castMediaTracks.size)
val trackSelections = arrayOfNulls<TrackSelection>(RENDERER_COUNT)
val trackGroupInfos = arrayOfNulls<TrackGroupInfo>(castMediaTracks.size)
val trackGroupInfos = arrayOfNulls<Group>(castMediaTracks.size)
for (i in castMediaTracks.indices) {
val mediaTrack = castMediaTracks[i]
trackGroups[i] = TrackGroup( /* id= */i.toString(), mediaTrackToFormat(mediaTrack))
@@ -428,18 +436,16 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
}
val trackSupport = intArrayOf(if (supported) C.FORMAT_HANDLED else C.FORMAT_UNSUPPORTED_TYPE)
val trackSelected = booleanArrayOf(selected)
trackGroupInfos[i] = TrackGroupInfo(trackGroups[i]!!, trackSupport, trackType, trackSelected)
trackGroupInfos[i] = Group(trackGroups[i]!!, false, trackSupport, trackSelected)
}
val tg = trackGroups.filterNotNull().toTypedArray()
var ts = trackSelections.filterNotNull().toTypedArray()
var tgi = trackGroupInfos.filterNotNull().toMutableList()
val ts = trackSelections.filterNotNull().toTypedArray()
val tgi = trackGroupInfos.filterNotNull().toMutableList()
val newTrackGroups = TrackGroupArray(*tg)
val newTrackSelections = TrackSelectionArray(*ts)
val newTracksInfo = TracksInfo(tgi)
if (newTrackGroups != currentTrackGroups
|| newTrackSelections != currentTrackSelections
|| newTracksInfo != currentTracksInfo) {
val newTracksInfo = Tracks(tgi)
if (newTracksInfo != currentTracks) {
myCurrentTrackSelections = newTrackSelections
myCurrentTrackGroups = newTrackGroups
myCurrentTracksInfo = newTracksInfo
@@ -593,7 +599,7 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
listeners.flushEvents()
val pendingResult: PendingResult<RemoteMediaClient.MediaChannelResult> = if (playWhenReady) remoteMediaClient!!.play() else remoteMediaClient!!.pause()
var resultCb = ResultCallback<RemoteMediaClient.MediaChannelResult?> {
val resultCb = ResultCallback<RemoteMediaClient.MediaChannelResult?> {
updatePlayerStateAndNotifyIfChanged()
}
@@ -622,10 +628,10 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
return false
}
override fun seekTo(mediaItemIndex: Int, positionMs: Long) {
override fun seekTo(mediaItemIndex: Int, positionMs: Long, seekCommand: Int, isRepeatingCurrentItem: Boolean) {
Log.d(tag, "seekTo $mediaItemIndex position $positionMs")
var resultCb = ResultCallback<RemoteMediaClient.MediaChannelResult?> {
val resultCb = ResultCallback<RemoteMediaClient.MediaChannelResult?> {
val statusCode: Int = it.status.statusCode
if (statusCode != CastStatusCodes.SUCCESS && statusCode != CastStatusCodes.REPLACED) {
Log.e(tag, "Seek failed. Error code $statusCode")
@@ -645,14 +651,14 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
// We assume the default position is 0. There is no support for seeking to the default position
// in RemoteMediaClient.
var positionMsFinal = if (positionMs != C.TIME_UNSET) positionMs else 0
val positionMsFinal = if (positionMs != C.TIME_UNSET) positionMs else 0
if (mediaStatus != null) {
if (currentMediaItemIndex != mediaItemIndex) {
Log.d(tag, "seekTo: Changing media item index from $currentMediaItemIndex to $mediaItemIndex")
remoteMediaClient?.queueJumpToItem(myCurrentTimeline.getPeriod(mediaItemIndex, period).uid as Int, positionMsFinal, JSONObject())?.setResultCallback(resultCb)
} else {
Log.d(tag, "seekTo: Same media index seek to position $positionMsFinal")
var mediaSeekOptions = MediaSeekOptions.Builder().setPosition(positionMsFinal).build()
val mediaSeekOptions = MediaSeekOptions.Builder().setPosition(positionMsFinal).build()
remoteMediaClient?.seek(mediaSeekOptions)?.setResultCallback(resultCb)
}
val oldPosition = getCurrentPositionInfo()
@@ -709,7 +715,7 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
val pendingResult: PendingResult<RemoteMediaClient.MediaChannelResult>? = remoteMediaClient?.setPlaybackRate(actualPlaybackParameters.speed.toDouble(), /* customData= */null)
var resultCb = ResultCallback<RemoteMediaClient.MediaChannelResult?> {
val resultCb = ResultCallback<RemoteMediaClient.MediaChannelResult?> {
updatePlaybackRateAndNotifyIfChanged()
listeners.flushEvents()
}
@@ -736,15 +742,7 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
sessionManager.endCurrentSession(false)
}
override fun getCurrentTrackGroups(): TrackGroupArray {
return this.myCurrentTrackGroups
}
override fun getCurrentTrackSelections(): TrackSelectionArray {
return this.myCurrentTrackSelections
}
override fun getCurrentTracksInfo(): TracksInfo {
override fun getCurrentTracks(): Tracks {
return this.myCurrentTracksInfo
}
@@ -817,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() {
@@ -858,8 +865,12 @@ class CastPlayer(var castContext: CastContext) : BasePlayer() {
return VideoSize.UNKNOWN
}
override fun getCurrentCues(): MutableList<Cue> {
return mutableListOf()
override fun getSurfaceSize(): Size {
return Size.UNKNOWN
}
override fun getCurrentCues(): CueGroup {
return CueGroup.EMPTY_TIME_ZERO
}
override fun getDeviceInfo(): DeviceInfo {
@@ -867,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()
}
}
}
@@ -1,130 +0,0 @@
package com.audiobookshelf.app.player
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.audiobookshelf.app.data.LocalMediaProgress
import com.audiobookshelf.app.data.PlaybackSession
import com.audiobookshelf.app.device.DeviceManager
import com.audiobookshelf.app.server.ApiHandler
import java.util.*
import kotlin.concurrent.schedule
import kotlin.math.roundToInt
data class MediaProgressSyncData(
var timeListened:Long, // seconds
var duration:Double, // seconds
var currentTime:Double // seconds
)
class MediaProgressSyncer(playerNotificationService:PlayerNotificationService, apiHandler: ApiHandler) {
private val tag = "MediaProgressSync"
private val playerNotificationService:PlayerNotificationService = playerNotificationService
private val apiHandler = apiHandler
private var listeningTimerTask: TimerTask? = null
var listeningTimerRunning:Boolean = false
private var lastSyncTime:Long = 0
var currentPlaybackSession: PlaybackSession? = null // copy of pb session currently syncing
var currentLocalMediaProgress: LocalMediaProgress? = null
val currentDisplayTitle get() = currentPlaybackSession?.displayTitle ?: "Unset"
val currentIsLocal get() = currentPlaybackSession?.isLocal == true
val currentSessionId get() = currentPlaybackSession?.id ?: ""
val currentPlaybackDuration get() = currentPlaybackSession?.duration ?: 0.0
fun start() {
if (listeningTimerRunning) {
Log.d(tag, "start: Timer already running for $currentDisplayTitle")
if (playerNotificationService.getCurrentPlaybackSessionId() != currentSessionId) {
Log.d(tag, "Playback session changed, reset timer")
currentLocalMediaProgress = null
listeningTimerTask?.cancel()
lastSyncTime = 0L
} else {
return
}
}
listeningTimerRunning = true
lastSyncTime = System.currentTimeMillis()
currentPlaybackSession = playerNotificationService.getCurrentPlaybackSessionCopy()
listeningTimerTask = Timer("ListeningTimer", false).schedule(0L, 5000L) {
Handler(Looper.getMainLooper()).post() {
if (playerNotificationService.currentPlayer.isPlaying) {
var currentTime = playerNotificationService.getCurrentTimeSeconds()
sync(currentTime)
}
}
}
}
fun stop() {
if (!listeningTimerRunning) return
Log.d(tag, "stop: Stopping listening for $currentDisplayTitle")
var currentTime = playerNotificationService.getCurrentTimeSeconds()
sync(currentTime)
reset()
}
fun sync(currentTime:Double) {
var diffSinceLastSync = System.currentTimeMillis() - lastSyncTime
if (diffSinceLastSync < 1000L) {
return
}
var listeningTimeToAdd = diffSinceLastSync / 1000L
lastSyncTime = System.currentTimeMillis()
var syncData = MediaProgressSyncData(listeningTimeToAdd,currentPlaybackDuration,currentTime)
currentPlaybackSession?.syncData(syncData)
if (currentIsLocal) {
// Save local progress sync
currentPlaybackSession?.let {
DeviceManager.dbManager.saveLocalPlaybackSession(it)
saveLocalProgress(it)
// Send sync to server also if connected to this server and local item belongs to this server
if (it.serverConnectionConfigId != null && DeviceManager.serverConnectionConfig?.id == it.serverConnectionConfigId) {
apiHandler.sendLocalProgressSync(it) {
Log.d(tag, "Local progress sync data sent to server $currentDisplayTitle for time $currentTime")
}
}
}
} else {
apiHandler.sendProgressSync(currentSessionId, syncData) {
Log.d(tag, "Progress sync data sent to server $currentDisplayTitle for time $currentTime")
}
}
}
private fun saveLocalProgress(playbackSession:PlaybackSession) {
if (currentLocalMediaProgress == null) {
var mediaProgress = DeviceManager.dbManager.getLocalMediaProgress(playbackSession.localMediaProgressId)
if (mediaProgress == null) {
currentLocalMediaProgress = playbackSession.getNewLocalMediaProgress()
} else {
currentLocalMediaProgress = mediaProgress
}
} else {
currentLocalMediaProgress?.updateFromPlaybackSession(playbackSession)
}
currentLocalMediaProgress?.let {
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.progress * 100).roundToInt()}%")
}
}
fun reset() {
listeningTimerTask?.cancel()
listeningTimerTask = null
listeningTimerRunning = false
currentPlaybackSession = null
currentLocalMediaProgress = null
lastSyncTime = 0L
}
}
@@ -2,18 +2,12 @@ package com.audiobookshelf.app.player
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.os.*
import android.support.v4.media.session.MediaSessionCompat
import android.util.Log
import android.view.KeyEvent
import com.audiobookshelf.app.data.LibraryItem
import com.audiobookshelf.app.data.LibraryItemWrapper
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import com.audiobookshelf.app.data.PodcastEpisode
import java.util.*
import kotlin.concurrent.schedule
@@ -21,16 +15,19 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
var tag = "MediaSessionCallback"
private var mediaButtonClickCount: Int = 0
var mediaButtonClickTimeout: Long = 1000 //ms
var seekAmount: Long = 20000 //ms
private var mediaButtonClickTimeout: Long = 1000 //ms
override fun onPrepare() {
Log.d(tag, "ON PREPARE MEDIA SESSION COMPAT")
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
Log.d(tag, "About to prepare player with ${it.displayTitle}")
Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,true,null)
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getPlayItemRequestPayload(false)) {
if (it == null) {
Log.e(tag, "Failed to play library item")
} else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
Handler(Looper.getMainLooper()).post {
playerNotificationService.preparePlayer(it,true, playbackRate)
}
}
}
}
@@ -49,10 +46,14 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
override fun onPlayFromSearch(query: String?, extras: Bundle?) {
Log.d(tag, "ON PLAY FROM SEARCH $query")
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
Log.d(tag, "About to prepare player with ${it.displayTitle}")
Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,true,null)
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getPlayItemRequestPayload(false)) {
if (it == null) {
Log.e(tag, "Failed to play library item")
} else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
Handler(Looper.getMainLooper()).post {
playerNotificationService.preparePlayer(it, true, playbackRate)
}
}
}
}
@@ -68,39 +69,74 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
}
override fun onSkipToPrevious() {
playerNotificationService.seekBackward(seekAmount)
playerNotificationService.skipToPrevious()
}
override fun onSkipToNext() {
playerNotificationService.seekForward(seekAmount)
playerNotificationService.skipToNext()
}
override fun onFastForward() {
playerNotificationService.seekForward(seekAmount)
playerNotificationService.jumpForward()
}
override fun onRewind() {
playerNotificationService.seekForward(seekAmount)
playerNotificationService.jumpBackward()
}
override fun onSeekTo(pos: Long) {
playerNotificationService.seekPlayer(pos)
val currentTrackStartOffset = playerNotificationService.getCurrentTrackStartOffsetMs()
playerNotificationService.seekPlayer(currentTrackStartOffset + pos)
}
private fun onChangeSpeed() {
// cycle to next speed, only contains preset android app options, as each increment needs it's own icon
// Rounding values in the event a non preset value (.5, 1, 1.2, 1.5, 2, 3) is selected in the phone app
val mediaManager = playerNotificationService.mediaManager
val newSpeed = when (mediaManager.getSavedPlaybackRate()) {
in 0.5f..0.7f -> 1.0f
in 0.8f..1.0f -> 1.2f
in 1.1f..1.2f -> 1.5f
in 1.3f..1.5f -> 2.0f
in 1.6f..2.0f -> 3.0f
in 2.1f..3.0f -> 0.5f
// anything set above 3 (can happen in the android app) will be reset to 1
else -> 1.0f
}
mediaManager.setSavedPlaybackRate(newSpeed)
playerNotificationService.setPlaybackSpeed(newSpeed)
playerNotificationService.clientEventEmitter?.onPlaybackSpeedChanged(newSpeed)
}
override fun onPlayFromMediaId(mediaId: String?, extras: Bundle?) {
Log.d(tag, "ON PLAY FROM MEDIA ID $mediaId")
var libraryItemWrapper: LibraryItemWrapper? = null
val libraryItemWrapper: LibraryItemWrapper?
var podcastEpisode: PodcastEpisode? = null
if (mediaId.isNullOrEmpty()) {
libraryItemWrapper = playerNotificationService.mediaManager.getFirstItem()
} else {
libraryItemWrapper = playerNotificationService.mediaManager.getById(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")
}
}
}
libraryItemWrapper?.let { li ->
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
Log.d(tag, "About to prepare player with ${it.displayTitle}")
Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,true,null)
playerNotificationService.mediaManager.play(li, podcastEpisode, playerNotificationService.getPlayItemRequestPayload(false)) {
if (it == null) {
Log.e(tag, "Failed to play library item")
} else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
Handler(Looper.getMainLooper()).post {
playerNotificationService.preparePlayer(it, true, playbackRate)
}
}
}
}
@@ -110,12 +146,67 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
return handleCallMediaButton(mediaButtonEvent)
}
fun handleCallMediaButton(intent: Intent): Boolean {
private fun handleCallMediaButton(intent: Intent): Boolean {
Log.w(tag, "handleCallMediaButton $intent | ${intent.action}")
if(Intent.ACTION_MEDIA_BUTTON == intent.action) {
var keyEvent = intent.getParcelableExtra<KeyEvent>(Intent.EXTRA_KEY_EVENT)
if (keyEvent?.getAction() == KeyEvent.ACTION_UP) {
when (keyEvent?.getKeyCode()) {
val keyEvent = if (Build.VERSION.SDK_INT >= 33) {
intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT, KeyEvent::class.java)
} else {
@Suppress("DEPRECATION")
intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT)
}
Log.d(tag, "handleCallMediaButton keyEvent = $keyEvent | action ${keyEvent?.action}")
// Widget button intent is only sending the action down event
if (keyEvent?.action == KeyEvent.ACTION_DOWN) {
Log.d(tag, "handleCallMediaButton: key action_down for ${keyEvent.keyCode}")
when (keyEvent.keyCode) {
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
Log.d(tag, "handleCallMediaButton: Media Play/Pause")
// TODO: Play/pause event sent from widget when app is closed. Currently the service gets destroyed before anything can happen
// if (playerNotificationService.currentPlaybackSession == null && DeviceManager.deviceData.lastPlaybackSession != null) {
// Log.i(tag, "No playback session but had one in the db")
//
// val connectionConfig = DeviceManager.deviceData.serverConnectionConfigs.find { it.id == DeviceManager.deviceData.lastPlaybackSession?.serverConnectionConfigId }
// connectionConfig?.let {
// Log.i(tag, "Setting playback session from db $it")
// DeviceManager.serverConnectionConfig = it
//
// playerNotificationService.currentPlaybackSession = DeviceManager.deviceData.lastPlaybackSession
// playerNotificationService.startNewPlaybackSession()
// return true
// }
// }
if (playerNotificationService.mPlayer.isPlaying) {
if (0 == mediaButtonClickCount) playerNotificationService.pause()
handleMediaButtonClickCount()
} else {
if (0 == mediaButtonClickCount) {
playerNotificationService.play()
}
handleMediaButtonClickCount()
}
}
KeyEvent.KEYCODE_MEDIA_FAST_FORWARD -> {
Log.d(tag, "handleCallMediaButton: Media Fast Forward")
playerNotificationService.jumpForward()
}
KeyEvent.KEYCODE_MEDIA_REWIND -> {
Log.d(tag, "handleCallMediaButton: Media Rewind")
playerNotificationService.jumpBackward()
}
}
}
if (keyEvent?.action == KeyEvent.ACTION_UP) {
Log.d(tag, "handleCallMediaButton: key action_up for ${keyEvent.keyCode}")
when (keyEvent.keyCode) {
KeyEvent.KEYCODE_HEADSETHOOK -> {
Log.d(tag, "handleCallMediaButton: Headset Hook")
if (0 == mediaButtonClickCount) {
if (playerNotificationService.mPlayer.isPlaying)
playerNotificationService.pause()
@@ -125,39 +216,28 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
handleMediaButtonClickCount()
}
KeyEvent.KEYCODE_MEDIA_PLAY -> {
Log.d(tag, "handleCallMediaButton: Media Play")
if (0 == mediaButtonClickCount) {
playerNotificationService.play()
playerNotificationService.sleepTimerManager.checkShouldExtendSleepTimer()
}
handleMediaButtonClickCount()
}
KeyEvent.KEYCODE_MEDIA_PAUSE -> {
Log.d(tag, "handleCallMediaButton: Media Pause")
if (0 == mediaButtonClickCount) playerNotificationService.pause()
handleMediaButtonClickCount()
}
KeyEvent.KEYCODE_MEDIA_NEXT -> {
playerNotificationService.seekForward(seekAmount)
playerNotificationService.jumpForward()
}
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> {
playerNotificationService.seekBackward(seekAmount)
playerNotificationService.jumpBackward()
}
KeyEvent.KEYCODE_MEDIA_STOP -> {
playerNotificationService.closePlayback()
}
KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
if (playerNotificationService.mPlayer.isPlaying) {
if (0 == mediaButtonClickCount) playerNotificationService.pause()
handleMediaButtonClickCount()
} else {
if (0 == mediaButtonClickCount) {
playerNotificationService.play()
playerNotificationService.sleepTimerManager.checkShouldExtendSleepTimer()
}
handleMediaButtonClickCount()
}
playerNotificationService.pause()
}
else -> {
Log.d(tag, "KeyCode:${keyEvent.getKeyCode()}")
Log.d(tag, "KeyCode:${keyEvent.keyCode}")
return false
}
}
@@ -166,7 +246,7 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
return true
}
fun handleMediaButtonClickCount() {
private fun handleMediaButtonClickCount() {
mediaButtonClickCount++
if (1 == mediaButtonClickCount) {
Timer().schedule(mediaButtonClickTimeout) {
@@ -182,14 +262,25 @@ class MediaSessionCallback(var playerNotificationService:PlayerNotificationServi
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
if (2 == msg.what) {
playerNotificationService.seekBackward(seekAmount)
playerNotificationService.jumpBackward()
playerNotificationService.play()
}
else if (msg.what >= 3) {
playerNotificationService.seekForward(seekAmount)
playerNotificationService.jumpForward()
playerNotificationService.play()
}
}
}
override fun onCustomAction(action: String?, extras: Bundle?) {
super.onCustomAction(action, extras)
when (action) {
CUSTOM_ACTION_JUMP_FORWARD -> onFastForward()
CUSTOM_ACTION_JUMP_BACKWARD -> onRewind()
CUSTOM_ACTION_SKIP_FORWARD -> onSkipToNext()
CUSTOM_ACTION_SKIP_BACKWARD -> onSkipToPrevious()
CUSTOM_ACTION_CHANGE_SPEED -> onChangeSpeed()
}
}
}
@@ -7,8 +7,8 @@ import android.os.Looper
import android.os.ResultReceiver
import android.support.v4.media.session.PlaybackStateCompat
import android.util.Log
import com.audiobookshelf.app.data.LibraryItem
import com.audiobookshelf.app.data.LibraryItemWrapper
import com.audiobookshelf.app.data.PodcastEpisode
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
@@ -30,9 +30,14 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
override fun onPrepare(playWhenReady: Boolean) {
Log.d(tag, "ON PREPARE $playWhenReady")
playerNotificationService.mediaManager.getFirstItem()?.let { li ->
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,playWhenReady,null)
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getPlayItemRequestPayload(false)) {
if (it == null) {
Log.e(tag, "Failed to play library item")
} else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
Handler(Looper.getMainLooper()).post {
playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
}
}
}
}
@@ -41,12 +46,26 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
override fun onPrepareFromMediaId(mediaId: String, playWhenReady: Boolean, extras: Bundle?) {
Log.d(tag, "ON PREPARE FROM MEDIA ID $mediaId $playWhenReady")
var libraryItemWrapper: LibraryItemWrapper? = playerNotificationService.mediaManager.getById(mediaId)
val libraryItemWrapper: LibraryItemWrapper?
var podcastEpisode: PodcastEpisode? = null
val libraryItemWithEpisode = playerNotificationService.mediaManager.getPodcastWithEpisodeByEpisodeId(mediaId)
if (libraryItemWithEpisode != null) {
libraryItemWrapper = libraryItemWithEpisode.libraryItemWrapper
podcastEpisode = libraryItemWithEpisode.episode
} else {
libraryItemWrapper = playerNotificationService.mediaManager.getById(mediaId)
}
libraryItemWrapper?.let { li ->
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
Log.d(tag, "About to prepare player with ${it.displayTitle}")
Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,playWhenReady,null)
playerNotificationService.mediaManager.play(li, podcastEpisode, playerNotificationService.getPlayItemRequestPayload(false)) {
if (it == null) {
Log.e(tag, "Failed to play library item")
} else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
Handler(Looper.getMainLooper()).post {
playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
}
}
}
}
@@ -55,10 +74,14 @@ class MediaSessionPlaybackPreparer(var playerNotificationService:PlayerNotificat
override fun onPrepareFromSearch(query: String, playWhenReady: Boolean, extras: Bundle?) {
Log.d(tag, "ON PREPARE FROM SEARCH $query")
playerNotificationService.mediaManager.getFromSearch(query)?.let { li ->
playerNotificationService.mediaManager.play(li, playerNotificationService.getMediaPlayer()) {
Log.d(tag, "About to prepare player with ${it.displayTitle}")
Handler(Looper.getMainLooper()).post() {
playerNotificationService.preparePlayer(it,playWhenReady,null)
playerNotificationService.mediaManager.play(li, null, playerNotificationService.getPlayItemRequestPayload(false)) {
if (it == null) {
Log.e(tag, "Failed to play library item")
} else {
val playbackRate = playerNotificationService.mediaManager.getSavedPlaybackRate()
Handler(Looper.getMainLooper()).post {
playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
}
}
}
}
@@ -0,0 +1,21 @@
package com.audiobookshelf.app.player
const val CUSTOM_ACTION_JUMP_FORWARD = "com.audiobookshelf.customAction.jump_forward";
const val CUSTOM_ACTION_JUMP_BACKWARD = "com.audiobookshelf.customAction.jump_backward";
const val CUSTOM_ACTION_SKIP_FORWARD = "com.audiobookshelf.customAction.skip_forward";
const val CUSTOM_ACTION_SKIP_BACKWARD = "com.audiobookshelf.customAction.skip_backward";
const val CUSTOM_ACTION_CHANGE_SPEED = "com.audiobookshelf.customAction.change_speed";
const val PLAYMETHOD_DIRECTPLAY = 0
const val PLAYMETHOD_DIRECTSTREAM = 1
const val PLAYMETHOD_TRANSCODE = 2
const val PLAYMETHOD_LOCAL = 3
//
// MEDIA BROWSER STUFF (ANDROID AUTO)
//
const val ANDROID_AUTO_PKG_NAME = "com.google.android.projection.gearhead"
const val ANDROID_AUTO_SIMULATOR_PKG_NAME = "com.google.android.autosimulator"
const val ANDROID_WEARABLE_PKG_NAME = "com.google.android.wearable.app"
const val ANDROID_GSEARCH_PKG_NAME = "com.google.android.googlequicksearchbox"
const val ANDROID_AUTOMOTIVE_PKG_NAME = "com.google.android.carassistant"
@@ -1,27 +1,121 @@
package com.audiobookshelf.app.player
import android.util.Log
import com.audiobookshelf.app.data.PlaybackSession
import com.audiobookshelf.app.data.PlayerState
import com.audiobookshelf.app.device.DeviceManager
import com.google.android.exoplayer2.PlaybackException
import com.google.android.exoplayer2.Player
//const val PAUSE_LEN_BEFORE_RECHECK = 30000 // 30 seconds
class PlayerListener(var playerNotificationService:PlayerNotificationService) : Player.Listener {
var tag = "PlayerListener"
companion object {
var lastPauseTime: Long = 0 //ms
var lazyIsPlaying: Boolean = false
}
private var onSeekBack: Boolean = false
override fun onPlayerError(error: PlaybackException) {
var errorMessage = error.message ?: "Unknown Error"
val errorMessage = error.message ?: "Unknown Error"
Log.e(tag, "onPlayerError $errorMessage")
playerNotificationService.handlePlayerPlaybackError(errorMessage) // If was direct playing session, fallback to transcode
}
override fun onPositionDiscontinuity(
oldPosition: Player.PositionInfo,
newPosition: Player.PositionInfo,
reason: Int
) {
if (reason == Player.DISCONTINUITY_REASON_SEEK) {
// If playing set seeking flag
Log.d(tag, "onPositionDiscontinuity: oldPosition=${oldPosition.positionMs}/${oldPosition.mediaItemIndex}, newPosition=${newPosition.positionMs}/${newPosition.mediaItemIndex}, isPlaying=${playerNotificationService.currentPlayer.isPlaying} reason=SEEK")
playerNotificationService.mediaProgressSyncer.seek()
lastPauseTime = 0 // When seeking while paused reset the auto-rewind
} else {
Log.d(tag, "onPositionDiscontinuity: oldPosition=${oldPosition.positionMs}/${oldPosition.mediaItemIndex}, newPosition=${newPosition.positionMs}/${newPosition.mediaItemIndex}, isPlaying=${playerNotificationService.currentPlayer.isPlaying}, reason=$reason")
}
}
override fun onIsPlayingChanged(isPlaying: Boolean) {
Log.d(tag, "onIsPlayingChanged to $isPlaying | ${playerNotificationService.getMediaPlayer()} | playbackState=${playerNotificationService.currentPlayer.playbackState}")
val player = playerNotificationService.currentPlayer
// Goal of these 2 if statements and the lazyIsPlaying is to ignore this event when it is triggered by a seek
// When a seek occurs the player is paused and buffering, then plays again right afterwards.
if (!isPlaying && player.playbackState == Player.STATE_BUFFERING) {
Log.d(tag, "onIsPlayingChanged: Pause event when buffering is ignored")
return
}
if (lazyIsPlaying == isPlaying) {
Log.d(tag, "onIsPlayingChanged: Lazy is playing $lazyIsPlaying is already set to this so ignoring")
return
}
lazyIsPlaying = isPlaying
// Update widget
DeviceManager.widgetUpdater?.onPlayerChanged(playerNotificationService)
if (isPlaying) {
Log.d(tag, "SeekBackTime: Player is playing")
if (lastPauseTime > 0 && DeviceManager.deviceData.deviceSettings?.disableAutoRewind != true) {
Log.d(tag, "SeekBackTime: playing started now set seek back time $lastPauseTime")
var seekBackTime = calcPauseSeekBackTime()
if (seekBackTime > 0) {
// Current chapter is used so that seek back does not go back to the previous chapter
val currentChapter = playerNotificationService.getCurrentBookChapter()
val minSeekBackTime = currentChapter?.startMs ?: 0
val currentTime = playerNotificationService.getCurrentTime()
val newTime = currentTime - seekBackTime
if (newTime < minSeekBackTime) {
seekBackTime = currentTime - minSeekBackTime
}
Log.d(tag, "SeekBackTime $seekBackTime")
}
// TODO: this needs to be reworked so that the audio doesn't start playing before it checks for updated progress
// Check if playback session still exists or sync media progress if updated
// val pauseLength: Long = System.currentTimeMillis() - lastPauseTime
// if (pauseLength > PAUSE_LEN_BEFORE_RECHECK) {
// val shouldCarryOn = playerNotificationService.checkCurrentSessionProgress(seekBackTime)
// if (!shouldCarryOn) return
// }
if (seekBackTime > 0L) {
playerNotificationService.seekBackward(seekBackTime)
}
}
} else {
Log.d(tag, "SeekBackTime: Player not playing set last pause time | playbackState=${player.playbackState}")
lastPauseTime = System.currentTimeMillis()
}
// Start/stop progress sync interval
if (isPlaying) {
val playbackSession: PlaybackSession? = playerNotificationService.mediaProgressSyncer.currentPlaybackSession ?: playerNotificationService.currentPlaybackSession
playbackSession?.let {
// Handles auto-starting sleep timer and resetting sleep timer
playerNotificationService.sleepTimerManager.handleMediaPlayEvent(it.id)
player.volume = 1F // Volume on sleep timer might have decreased this
playerNotificationService.mediaProgressSyncer.play(it)
}
} else {
playerNotificationService.mediaProgressSyncer.pause {
Log.d(tag, "Media Progress Syncer paused and synced")
}
}
playerNotificationService.clientEventEmitter?.onPlayingUpdate(isPlaying)
}
override fun onEvents(player: Player, events: Player.Events) {
Log.d(tag, "onEvents ${player.deviceInfo} | ${playerNotificationService.getMediaPlayer()} | ${events.size()}")
Log.d(tag, "onEvents ${playerNotificationService.getMediaPlayer()} | ${events.size()}")
if (events.contains(Player.EVENT_POSITION_DISCONTINUITY)) {
Log.d(tag, "EVENT_POSITION_DISCONTINUITY")
@@ -38,7 +132,7 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
Log.d(tag, "STATE_READY : " + playerNotificationService.currentPlayer.duration)
if (lastPauseTime == 0L) {
lastPauseTime = -1;
lastPauseTime = -1
}
playerNotificationService.sendClientMetadata(PlayerState.READY)
}
@@ -49,6 +143,8 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_ENDED) {
Log.d(tag, "STATE_ENDED")
playerNotificationService.sendClientMetadata(PlayerState.ENDED)
playerNotificationService.handlePlaybackEnded()
}
if (playerNotificationService.currentPlayer.playbackState == Player.STATE_IDLE) {
Log.d(tag, "STATE_IDLE")
@@ -62,49 +158,15 @@ class PlayerListener(var playerNotificationService:PlayerNotificationService) :
if (events.contains(Player.EVENT_PLAYLIST_METADATA_CHANGED)) {
Log.d(tag, "EVENT_PLAYLIST_METADATA_CHANGED ${playerNotificationService.getMediaPlayer()}")
}
if (events.contains(Player.EVENT_IS_PLAYING_CHANGED)) {
Log.d(tag, "EVENT IS PLAYING CHANGED ${playerNotificationService.getMediaPlayer()}")
if (player.isPlaying) {
Log.d(tag, "SeekBackTime: Player is playing")
if (lastPauseTime > 0) {
if (onSeekBack) onSeekBack = false
else {
Log.d(tag, "SeekBackTime: playing started now set seek back time $lastPauseTime")
var backTime = calcPauseSeekBackTime()
if (backTime > 0) {
if (backTime >= playerNotificationService.getCurrentTime()) backTime = playerNotificationService.getCurrentTime() - 500
Log.d(tag, "SeekBackTime $backTime")
onSeekBack = true
playerNotificationService.seekBackward(backTime)
} else {
Log.d(tag, "SeekBackTime: back time is 0")
}
}
}
} else {
Log.d(tag, "SeekBackTime: Player not playing set last pause time")
lastPauseTime = System.currentTimeMillis()
}
// Start/stop progress sync interval
Log.d(tag, "Playing ${playerNotificationService.getCurrentBookTitle()}")
if (player.isPlaying) {
playerNotificationService.mediaProgressSyncer.start()
} else {
playerNotificationService.mediaProgressSyncer.stop()
}
playerNotificationService.clientEventEmitter?.onPlayingUpdate(player.isPlaying)
}
}
private fun calcPauseSeekBackTime() : Long {
if (lastPauseTime <= 0) return 0
var time: Long = System.currentTimeMillis() - lastPauseTime
var seekback: Long
if (time < 3000) seekback = 0
else if (time < 300000) seekback = 10000 // 3s to 5m = jump back 10s
val time: Long = System.currentTimeMillis() - lastPauseTime
val seekback: Long
if (time < 10000) seekback = 0 // 10s or less = no seekback
else if (time < 60000) seekback = 3000 // 10s to 1m = jump back 3s
else if (time < 300000) seekback = 10000 // 1m to 5m = jump back 10s
else if (time < 1800000) seekback = 20000 // 5m to 30m = jump back 20s
else seekback = 29500 // 30m and up = jump back 30s
return seekback
@@ -1,20 +1,37 @@
package com.audiobookshelf.app.player
import android.app.Notification
import android.content.pm.ServiceInfo
import android.os.Build
import android.util.Log
import com.google.android.exoplayer2.ui.PlayerNotificationManager
class PlayerNotificationListener(var playerNotificationService:PlayerNotificationService) : PlayerNotificationManager.NotificationListener {
var tag = "PlayerNotificationListener"
companion object {
var isForegroundService = false
}
override fun onNotificationPosted(
notificationId: Int,
notification: Notification,
onGoing: Boolean) {
// Start foreground service
Log.d(tag, "Notification Posted $notificationId - Start Foreground | $notification")
playerNotificationService.startForeground(notificationId, notification)
if (onGoing && !isForegroundService) {
// Start foreground service
Log.d(tag, "Notification Posted $notificationId - Start Foreground | $notification")
PlayerNotificationService.isClosed = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
playerNotificationService.startForeground(notificationId, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)
} else {
playerNotificationService.startForeground(notificationId, notification)
}
isForegroundService = true
} else {
Log.d(tag, "Notification posted $notificationId, not starting foreground - onGoing=$onGoing | isForegroundService=$isForegroundService")
}
}
override fun onNotificationCancelled(
@@ -26,6 +43,14 @@ class PlayerNotificationListener(var playerNotificationService:PlayerNotificatio
playerNotificationService.stopSelf()
} else {
Log.d(tag, "onNotificationCancelled not dismissed by user")
if (PlayerNotificationService.isSwitchingPlayer) {
// When switching from cast player to exo player and vice versa the notification is cancelled and posted again
// so we don't want to cancel the playback during this switch
Log.d(tag, "PNS is switching player")
PlayerNotificationService.isSwitchingPlayer = false
}
}
isForegroundService = false
}
}
File diff suppressed because it is too large. Load diff
@@ -4,6 +4,8 @@ import android.hardware.Sensor
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 {
@@ -34,7 +36,8 @@ class ShakeDetector : SensorEventListener {
val gZ = z / SensorManager.GRAVITY_EARTH
// gForce will be close to 1 when there is no movement.
val gForce: Float = sqrt(gX * gX + gY * gY + gZ * gZ)
if (gForce > SHAKE_THRESHOLD_GRAVITY) {
val shakeThreshold = DeviceManager.deviceData.deviceSettings?.getShakeThresholdGravity() ?: 1.6f
if (gForce > shakeThreshold) {
val now = System.currentTimeMillis()
// ignore shake events too close to each other (500ms)
if (mShakeTimestamp + SHAKE_SLOP_TIME_MS > now) {
@@ -44,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)
@@ -59,7 +63,7 @@ class ShakeDetector : SensorEventListener {
* from the Google Play Store and run it to see how
* many G's it takes to register a shake
*/
private const val SHAKE_THRESHOLD_GRAVITY = 1.5f // orig 2.7f
// var SHAKE_THRESHOLD_GRAVITY = 1.5f // orig 2.7f
private const val SHAKE_SLOP_TIME_MS = 500
private const val SHAKE_COUNT_RESET_TIME_MS = 3000
}
@@ -1,251 +0,0 @@
package com.audiobookshelf.app.player
import android.content.Context
import android.os.*
import android.util.Log
import java.util.*
import kotlin.concurrent.schedule
import kotlin.math.roundToInt
const val SLEEP_EXTENSION_TIME = 900000L // 15m
class SleepTimerManager constructor(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 sleepTimerExtensionTime:Long = 0L
private var sleepTimerFinishedAt:Long = 0L
private fun getCurrentTime():Long {
return playerNotificationService.getCurrentTime()
}
private fun getDuration():Long {
return playerNotificationService.getDuration()
}
private fun getIsPlaying():Boolean {
return playerNotificationService.currentPlayer.isPlaying
}
private fun setVolume(volume:Float) {
playerNotificationService.currentPlayer.volume = volume
}
private fun pause() {
playerNotificationService.currentPlayer.pause()
}
private fun play() {
playerNotificationService.currentPlayer.play()
}
private fun getSleepTimerTimeRemainingSeconds():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()
}
fun setSleepTimer(time: Long, isChapterTime: Boolean) : Boolean {
Log.d(tag, "Setting Sleep Timer for $time is chapter time $isChapterTime")
sleepTimerTask?.cancel()
sleepTimerRunning = false
sleepTimerFinishedAt = 0L
sleepTimerElapsed = 0L
// Register shake sensor
playerNotificationService.registerSensor()
val currentTime = getCurrentTime()
if (isChapterTime) {
if (currentTime > time) {
Log.d(tag, "Invalid sleep timer - current time is already passed chapter time $time")
return false
}
sleepTimerEndTime = time
sleepTimerLength = 0
sleepTimerExtensionTime = SLEEP_EXTENSION_TIME
if (sleepTimerEndTime > getDuration()) {
sleepTimerEndTime = getDuration()
}
} else {
sleepTimerLength = time
sleepTimerEndTime = 0L
sleepTimerExtensionTime = time
if (sleepTimerLength + getCurrentTime() > getDuration()) {
sleepTimerLength = getDuration() - getCurrentTime()
}
}
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
sleepTimerRunning = true
sleepTimerTask = Timer("SleepTimer", false).schedule(0L, 1000L) {
Handler(Looper.getMainLooper()).post() {
if (getIsPlaying()) {
sleepTimerElapsed += 1000L
val sleepTimeSecondsRemaining = getSleepTimerTimeRemainingSeconds()
Log.d(tag, "Timer Elapsed $sleepTimerElapsed | Sleep TIMER time remaining $sleepTimeSecondsRemaining s")
if (sleepTimeSecondsRemaining > 0) {
playerNotificationService.clientEventEmitter?.onSleepTimerSet(sleepTimeSecondsRemaining)
}
if (sleepTimeSecondsRemaining <= 0) {
Log.d(tag, "Sleep Timer Pausing Player on Chapter")
pause()
playerNotificationService.clientEventEmitter?.onSleepTimerEnded(getCurrentTime())
clearSleepTimer()
sleepTimerFinishedAt = System.currentTimeMillis()
} else if (sleepTimeSecondsRemaining <= 30) {
// Start fading out audio
val volume = sleepTimeSecondsRemaining / 30F
Log.d(tag, "SLEEP VOLUME FADE $volume | ${sleepTimeSecondsRemaining}s remaining")
setVolume(volume)
}
}
}
}
return true
}
fun clearSleepTimer() {
sleepTimerTask?.cancel()
sleepTimerTask = null
sleepTimerEndTime = 0
sleepTimerRunning = false
playerNotificationService.unregisterSensor()
}
fun getSleepTimerTime():Long {
return sleepTimerEndTime
}
fun cancelSleepTimer() {
Log.d(tag, "Canceling Sleep Timer")
clearSleepTimer()
playerNotificationService.clientEventEmitter?.onSleepTimerSet(0)
}
// Vibrate when extending sleep timer by shaking
private fun vibrate() {
val context = playerNotificationService.getContext()
val vibrator:Vibrator
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager =
context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
vibrator = vibratorManager.defaultVibrator
} else {
@Suppress("DEPRECATION")
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
}
vibrator.let {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val vibrationEffect = VibrationEffect.createWaveform(longArrayOf(0, 150, 150, 150),-1)
it.vibrate(vibrationEffect)
} else {
@Suppress("DEPRECATION")
it.vibrate(10)
}
}
}
private fun extendSleepTime() {
if (!sleepTimerRunning) return
setVolume(1F)
if (sleepTimerEndTime == 0L) {
sleepTimerLength += sleepTimerExtensionTime
if (sleepTimerLength + getCurrentTime() > getDuration()) sleepTimerLength = getDuration() - getCurrentTime()
} else {
sleepTimerEndTime += sleepTimerExtensionTime
if (sleepTimerEndTime > getDuration()) sleepTimerEndTime = getDuration()
}
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
}
fun checkShouldExtendSleepTimer() {
if (!sleepTimerRunning) {
if (sleepTimerFinishedAt <= 0L) return
val finishedAtDistance = System.currentTimeMillis() - sleepTimerFinishedAt
if (finishedAtDistance > SLEEP_TIMER_WAKE_UP_EXPIRATION) // 2 minutes
{
Log.d(tag, "Sleep timer finished over 2 mins ago, clearing it")
sleepTimerFinishedAt = 0L
return
}
val newSleepTime = if (sleepTimerExtensionTime >= 0) sleepTimerExtensionTime else SLEEP_EXTENSION_TIME
vibrate()
setSleepTimer(newSleepTime, false)
play()
return
}
// Only extend if within 30 seconds of finishing
val sleepTimeRemaining = getSleepTimerTimeRemainingSeconds()
if (sleepTimeRemaining <= 30) {
vibrate()
extendSleepTime()
}
}
fun handleShake() {
Log.d(tag, "HANDLE SHAKE HERE")
if (sleepTimerRunning || sleepTimerFinishedAt > 0L) checkShouldExtendSleepTimer()
}
fun increaseSleepTime(time: Long) {
Log.d(tag, "Increase Sleep time $time")
if (!sleepTimerRunning) return
if (sleepTimerEndTime == 0L) {
sleepTimerLength += time
if (sleepTimerLength + getCurrentTime() > getDuration()) sleepTimerLength = getDuration() - getCurrentTime()
} else {
val newSleepEndTime = sleepTimerEndTime + time
sleepTimerEndTime = if (newSleepEndTime >= getDuration()) {
getDuration()
} else {
newSleepEndTime
}
}
setVolume(1F)
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
}
fun decreaseSleepTime(time: Long) {
Log.d(tag, "Decrease Sleep time $time")
if (!sleepTimerRunning) return
if (sleepTimerEndTime == 0L) {
sleepTimerLength -= time
if (sleepTimerLength <= 0) sleepTimerLength = 1000L
} else {
val newSleepEndTime = sleepTimerEndTime - time
sleepTimerEndTime = if (newSleepEndTime <= 1000) {
// End sleep timer in 1 second
getCurrentTime() + 1000
} else {
newSleepEndTime
}
}
setVolume(1F)
playerNotificationService.clientEventEmitter?.onSleepTimerSet(getSleepTimerTimeRemainingSeconds())
}
}
@@ -1,14 +1,14 @@
package com.audiobookshelf.app.plugins
import android.content.Intent
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.content.ContextCompat
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.player.CastManager
import com.audiobookshelf.app.player.PlayerListener
import com.audiobookshelf.app.player.PlayerNotificationService
import com.audiobookshelf.app.server.ApiHandler
import com.fasterxml.jackson.core.json.JsonReadFeature
@@ -23,7 +23,7 @@ import org.json.JSONObject
@CapacitorPlugin(name = "AbsAudioPlayer")
class AbsAudioPlayer : Plugin() {
private val tag = "AbsAudioPlayer"
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
private lateinit var mainActivity: MainActivity
private lateinit var apiHandler:ApiHandler
@@ -33,11 +33,18 @@ 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)
initCastManager()
try {
initCastManager()
} catch(e:Exception) {
Log.e(tag, "initCastManager exception ${e.printStackTrace()}")
}
val foregroundServiceReady : () -> Unit = {
playerNotificationService = mainActivity.foregroundService
@@ -56,25 +63,27 @@ 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)))
}
override fun onPrepare(audiobookId: String, playWhenReady: Boolean) {
val jsobj = JSObject()
jsobj.put("audiobookId", audiobookId)
jsobj.put("playWhenReady", playWhenReady)
notifyListeners("onPrepareMedia", jsobj)
}
override fun onSleepTimerEnded(currentPosition: Long) {
emit("onSleepTimerEnded", currentPosition)
}
override fun onSleepTimerSet(sleepTimeRemaining: Int) {
emit("onSleepTimerSet", sleepTimeRemaining)
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)
notifyListeners("onSleepTimerSet", ret)
}
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)))
}
@@ -85,7 +94,29 @@ class AbsAudioPlayer : Plugin() {
override fun onMediaPlayerChanged(mediaPlayer:String) {
emit("onMediaPlayerChanged", mediaPlayer)
}
override fun onProgressSyncFailing() {
emit("onProgressSyncFailing", "")
}
override fun onProgressSyncSuccess() {
emit("onProgressSyncSuccess", "")
}
override fun onNetworkMeteredChanged(isUnmetered:Boolean) {
emit("onNetworkMeteredChanged", isUnmetered)
}
override fun onMediaItemHistoryUpdated(mediaItemHistory:MediaItemHistory) {
notifyListeners("onMediaItemHistoryUpdated", JSObject(jacksonMapper.writeValueAsString(mediaItemHistory)))
}
override fun onPlaybackSpeedChanged(playbackSpeed:Float) {
emit("onPlaybackSpeedChanged", playbackSpeed)
}
})
MediaEventManager.clientEventEmitter = playerNotificationService.clientEventEmitter
}
mainActivity.pluginCallback = foregroundServiceReady
}
@@ -96,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)
@@ -153,22 +210,17 @@ class AbsAudioPlayer : Plugin() {
@PluginMethod
fun prepareLibraryItem(call: PluginCall) {
// Need to make sure the player service has been started
if (!PlayerNotificationService.isStarted) {
Log.w(tag, "prepareLibraryItem: PlayerService not started - Starting foreground service --")
Intent(mainActivity, PlayerNotificationService::class.java).also { intent ->
ContextCompat.startForegroundService(mainActivity, intent)
}
}
val libraryItemId = call.getString("libraryItemId", "").toString()
val episodeId = call.getString("episodeId", "").toString()
val playWhenReady = call.getBoolean("playWhenReady") == true
val playbackRate = call.getFloat("playbackRate",1f) ?: 1f
val startTimeOverride = call.getDouble("startTime")
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")
return call.resolve()
return call.resolve(JSObject("{\"error\":\"Invalid request\"}"))
}
if (libraryItemId.startsWith("local")) { // Play local media item
@@ -179,28 +231,63 @@ class AbsAudioPlayer : Plugin() {
episode = podcastMedia.episodes?.find { ep -> ep.id == episodeId }
if (episode == null) {
Log.e(tag, "prepareLibraryItem: Podcast episode not found $episodeId")
return call.resolve(JSObject())
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)}")
val playbackSession = it.getPlaybackSession(episode)
playerNotificationService.preparePlayer(playbackSession, playWhenReady, playbackRate)
val playbackSession = it.getPlaybackSession(episode, playerNotificationService.getDeviceInfo())
if (startTimeOverride != null) {
Log.d(tag, "prepareLibraryItem: Using start time override $startTimeOverride")
playbackSession.currentTime = startTimeOverride
}
if (playerNotificationService.mediaProgressSyncer.listeningTimerRunning) { // If progress syncing then first stop before preparing next
playerNotificationService.mediaProgressSyncer.stop {
Log.d(tag, "Media progress syncer was already syncing - stopped")
PlayerListener.lazyIsPlaying = false
Handler(Looper.getMainLooper()).post { // TODO: This was needed again which is probably a design a flaw
playerNotificationService.preparePlayer(
playbackSession,
playWhenReady,
playbackRate
)
}
}
} else {
playerNotificationService.mediaProgressSyncer.reset()
playerNotificationService.preparePlayer(playbackSession, playWhenReady, playbackRate)
}
}
return call.resolve(JSObject())
}
} else { // Play library item from server
val mediaPlayer = playerNotificationService.getMediaPlayer()
val playItemRequestPayload = playerNotificationService.getPlayItemRequestPayload(false)
Handler(Looper.getMainLooper()).post {
playerNotificationService.mediaProgressSyncer.stop {
apiHandler.playLibraryItem(libraryItemId, episodeId, playItemRequestPayload) {
if (it == null) {
call.resolve(JSObject("{\"error\":\"Server play request failed\"}"))
} else {
if (startTimeOverride != null) {
Log.d(tag, "prepareLibraryItem: Using start time override $startTimeOverride")
it.currentTime = startTimeOverride
}
apiHandler.playLibraryItem(libraryItemId, episodeId, false, mediaPlayer) {
Handler(Looper.getMainLooper()).post {
Log.d(tag, "Preparing Player TEST ${jacksonMapper.writeValueAsString(it)}")
playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
Handler(Looper.getMainLooper()).post {
Log.d(tag, "Preparing Player playback session ${jacksonMapper.writeValueAsString(it)}")
PlayerListener.lazyIsPlaying = false
playerNotificationService.preparePlayer(it, playWhenReady, playbackRate)
}
call.resolve(JSObject(jacksonMapper.writeValueAsString(it)))
}
}
}
call.resolve(JSObject(jacksonMapper.writeValueAsString(it)))
}
}
}
@@ -243,9 +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()
}
}
@@ -292,7 +380,8 @@ class AbsAudioPlayer : Plugin() {
val isChapterTime:Boolean = call.getBoolean("isChapterTime", false) == true
Handler(Looper.getMainLooper()).post {
val success:Boolean = playerNotificationService.sleepTimerManager.setSleepTimer(time, isChapterTime)
val playbackSession: PlaybackSession? = playerNotificationService.mediaProgressSyncer.currentPlaybackSession ?: playerNotificationService.currentPlaybackSession
val success:Boolean = playerNotificationService.sleepTimerManager.setManualSleepTimer(playbackSession?.id ?: "", time, isChapterTime)
val ret = JSObject()
ret.put("success", success)
call.resolve(ret)
@@ -333,7 +422,9 @@ class AbsAudioPlayer : Plugin() {
@PluginMethod
fun cancelSleepTimer(call: PluginCall) {
playerNotificationService.sleepTimerManager.cancelSleepTimer()
Handler(Looper.getMainLooper()).post {
playerNotificationService.sleepTimerManager.cancelSleepTimer()
}
call.resolve()
}
@@ -1,42 +1,53 @@
package com.audiobookshelf.app.data
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
import com.getcapacitor.*
import com.getcapacitor.annotation.CapacitorPlugin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import org.json.JSONObject
@CapacitorPlugin(name = "AbsDatabase")
class AbsDatabase : Plugin() {
val tag = "AbsDatabase"
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
lateinit var mainActivity: MainActivity
lateinit var 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, 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
fun getDeviceData(call:PluginCall) {
GlobalScope.launch(Dispatchers.IO) {
var deviceData = DeviceManager.dbManager.getDeviceData()
val deviceData = DeviceManager.dbManager.getDeviceData()
call.resolve(JSObject(jacksonMapper.writeValueAsString(deviceData)))
}
}
@@ -44,17 +55,17 @@ class AbsDatabase : Plugin() {
@PluginMethod
fun getLocalFolders(call:PluginCall) {
GlobalScope.launch(Dispatchers.IO) {
var folders = DeviceManager.dbManager.getAllLocalFolders()
val folders = DeviceManager.dbManager.getAllLocalFolders()
call.resolve(JSObject(jacksonMapper.writeValueAsString(LocalFoldersPayload(folders))))
}
}
@PluginMethod
fun getLocalFolder(call:PluginCall) {
var folderId = call.getString("folderId", "").toString()
val folderId = call.getString("folderId", "").toString()
GlobalScope.launch(Dispatchers.IO) {
DeviceManager.dbManager.getLocalFolder(folderId)?.let {
var folderObj = jacksonMapper.writeValueAsString(it)
val folderObj = jacksonMapper.writeValueAsString(it)
call.resolve(JSObject(folderObj))
} ?: call.resolve()
}
@@ -62,10 +73,10 @@ class AbsDatabase : Plugin() {
@PluginMethod
fun getLocalLibraryItem(call:PluginCall) {
var id = call.getString("id", "").toString()
val id = call.getString("id", "").toString()
GlobalScope.launch(Dispatchers.IO) {
var localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(id)
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(id)
if (localLibraryItem == null) {
call.resolve()
} else {
@@ -75,10 +86,10 @@ class AbsDatabase : Plugin() {
}
@PluginMethod
fun getLocalLibraryItemByLLId(call:PluginCall) {
var libraryItemId = call.getString("libraryItemId", "").toString()
fun getLocalLibraryItemByLId(call:PluginCall) {
val libraryItemId = call.getString("libraryItemId", "").toString()
GlobalScope.launch(Dispatchers.IO) {
var localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLLId(libraryItemId)
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItemByLId(libraryItemId)
if (localLibraryItem == null) {
call.resolve()
} else {
@@ -89,40 +100,52 @@ class AbsDatabase : Plugin() {
@PluginMethod
fun getLocalLibraryItems(call:PluginCall) {
var mediaType = call.getString("mediaType", "").toString()
val mediaType = call.getString("mediaType", "").toString()
GlobalScope.launch(Dispatchers.IO) {
var localLibraryItems = DeviceManager.dbManager.getLocalLibraryItems(mediaType)
val localLibraryItems = DeviceManager.dbManager.getLocalLibraryItems(mediaType)
call.resolve(JSObject(jacksonMapper.writeValueAsString(LocalLibraryItemsPayload(localLibraryItems))))
}
}
@PluginMethod
fun getLocalLibraryItemsInFolder(call:PluginCall) {
var folderId = call.getString("folderId", "").toString()
val folderId = call.getString("folderId", "").toString()
GlobalScope.launch(Dispatchers.IO) {
var localLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(folderId)
val localLibraryItems = DeviceManager.dbManager.getLocalLibraryItemsInFolder(folderId)
call.resolve(JSObject(jacksonMapper.writeValueAsString(LocalLibraryItemsPayload(localLibraryItems))))
}
}
@PluginMethod
fun setCurrentServerConnectionConfig(call:PluginCall) {
var serverConnectionConfigId = call.getString("id", "").toString()
var serverConnectionConfig = DeviceManager.deviceData.serverConnectionConfigs.find { it.id == serverConnectionConfigId }
Log.d(tag, "setCurrentServerConnectionConfig called")
val serverConfigPayload = jacksonMapper.readValue<ServerConnConfigPayload>(call.data.toString())
var serverConnectionConfig = DeviceManager.deviceData.serverConnectionConfigs.find { it.id == serverConfigPayload.id }
var userId = call.getString("userId", "").toString()
var username = call.getString("username", "").toString()
var token = call.getString("token", "").toString()
val userId = serverConfigPayload.userId
val username = serverConfigPayload.username
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
var serverAddress = call.getString("address", "").toString()
val serverAddress = call.getString("address", "").toString()
// Create new server connection config
var sscId = DeviceManager.getBase64Id("$serverAddress@$username")
var sscIndex = DeviceManager.deviceData.serverConnectionConfigs.size
serverConnectionConfig = ServerConnectionConfig(sscId, sscIndex, "$serverAddress ($username)", serverAddress, userId, username, token)
val sscId = DeviceManager.getBase64Id("$serverAddress@$username")
val sscIndex = DeviceManager.deviceData.serverConnectionConfigs.size
// 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!!)
@@ -130,17 +153,24 @@ 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 != serverConnectionConfigId) {
DeviceManager.deviceData.lastServerConnectionConfigId = serverConnectionConfigId
if (DeviceManager.deviceData.lastServerConnectionConfigId != serverConfigPayload.id) {
DeviceManager.deviceData.lastServerConnectionConfigId = serverConfigPayload.id
shouldSave = true
}
@@ -155,7 +185,11 @@ class AbsDatabase : Plugin() {
@PluginMethod
fun removeServerConnectionConfig(call:PluginCall) {
GlobalScope.launch(Dispatchers.IO) {
var serverConnectionConfigId = call.getString("serverConnectionConfigId", "").toString()
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
@@ -168,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) {
@@ -181,26 +251,124 @@ class AbsDatabase : Plugin() {
@PluginMethod
fun getAllLocalMediaProgress(call:PluginCall) {
GlobalScope.launch(Dispatchers.IO) {
var localMediaProgress = DeviceManager.dbManager.getAllLocalMediaProgress()
val localMediaProgress = DeviceManager.dbManager.getAllLocalMediaProgress()
call.resolve(JSObject(jacksonMapper.writeValueAsString(LocalMediaProgressPayload(localMediaProgress))))
}
}
@PluginMethod
fun getLocalMediaProgressForServerItem(call:PluginCall) {
val libraryItemId = call.getString("libraryItemId", "").toString()
var episodeId:String? = call.getString("episodeId", "").toString()
if (episodeId == "") episodeId = null
GlobalScope.launch(Dispatchers.IO) {
val allLocalMediaProgress = DeviceManager.dbManager.getAllLocalMediaProgress()
val localMediaProgress = allLocalMediaProgress.find { libraryItemId == it.libraryItemId && (episodeId == null || it.episodeId == episodeId) }
if (localMediaProgress == null) {
call.resolve()
} else {
call.resolve(JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
}
}
}
@PluginMethod
fun removeLocalMediaProgress(call:PluginCall) {
var localMediaProgressId = call.getString("localMediaProgressId", "").toString()
val localMediaProgressId = call.getString("localMediaProgressId", "").toString()
DeviceManager.dbManager.removeLocalMediaProgress(localMediaProgressId)
call.resolve()
}
@PluginMethod
fun syncLocalMediaProgressWithServer(call:PluginCall) {
fun syncLocalSessionsWithServer(call:PluginCall) {
if (DeviceManager.serverConnectionConfig == null) {
Log.e(tag, "syncLocalMediaProgressWithServer not connected to server")
AbsLogger.error("AbsDatabase", "syncLocalSessionsWithServer: not connected to server")
return call.resolve()
}
apiHandler.syncMediaProgress {
call.resolve(JSObject(jacksonMapper.writeValueAsString(it)))
apiHandler.syncLocalMediaProgressForUser {
val savedSessions = DeviceManager.dbManager.getPlaybackSessions().filter { it.serverConnectionConfigId == DeviceManager.serverConnectionConfigId }
if (savedSessions.isNotEmpty()) {
apiHandler.sendSyncLocalSessions(savedSessions) { success, errorMsg ->
if (!success) {
call.resolve(JSObject("{\"error\":\"$errorMsg\"}"))
} else {
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()
}
}
}
// Updates received via web socket
// This function doesn't need to sync with the server also because this data is coming from the server
// If sending the localMediaProgressId then update existing media progress
// If sending localLibraryItemId then save new local media progress
@PluginMethod
fun syncServerMediaProgressWithLocalMediaProgress(call:PluginCall) {
val serverMediaProgress = call.getObject("mediaProgress").toString()
val localLibraryItemId = call.getString("localLibraryItemId", "").toString()
var localEpisodeId:String? = call.getString("localEpisodeId", "").toString()
if (localEpisodeId.isNullOrEmpty()) localEpisodeId = null
var localMediaProgressId = call.getString("localMediaProgressId") ?: ""
val mediaProgress = jacksonMapper.readValue<MediaProgress>(serverMediaProgress)
if (localMediaProgressId == "") {
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
if (localLibraryItem != null) {
localMediaProgressId = if (localEpisodeId.isNullOrEmpty()) localLibraryItemId else "$localLibraryItemId-$localEpisodeId"
val localMediaProgress = LocalMediaProgress(
id = localMediaProgressId,
localLibraryItemId = localLibraryItemId,
localEpisodeId = localEpisodeId,
duration = mediaProgress.duration,
progress = mediaProgress.progress,
currentTime = mediaProgress.currentTime,
isFinished = mediaProgress.isFinished,
ebookLocation = mediaProgress.ebookLocation,
ebookProgress = mediaProgress.ebookProgress,
lastUpdate = mediaProgress.lastUpdate,
startedAt = mediaProgress.startedAt,
finishedAt = mediaProgress.finishedAt,
serverConnectionConfigId = localLibraryItem.serverConnectionConfigId,
serverAddress = localLibraryItem.serverAddress,
serverUserId = localLibraryItem.serverUserId,
libraryItemId = localLibraryItem.libraryItemId,
episodeId = mediaProgress.episodeId)
Log.d(tag, "syncServerMediaProgressWithLocalMediaProgress: Saving new local media progress $localMediaProgress")
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
call.resolve(JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
} else {
Log.e(tag, "syncServerMediaProgressWithLocalMediaProgress: Local library item not found")
}
} else {
Log.d(tag, "syncServerMediaProgressWithLocalMediaProgress $localMediaProgressId")
val localMediaProgress = DeviceManager.dbManager.getLocalMediaProgress(localMediaProgressId)
if (localMediaProgress == null) {
Log.w(tag, "syncServerMediaProgressWithLocalMediaProgress Local media progress not found $localMediaProgressId")
call.resolve()
} else {
MediaEventManager.syncEvent(mediaProgress, "Received from webhook event")
localMediaProgress.updateFromServerMediaProgress(mediaProgress)
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
call.resolve(JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
}
}
}
@@ -227,8 +395,8 @@ class AbsDatabase : Plugin() {
return call.resolve(JSObject("{\"error\":\"Invalid library item not a podcast\"}"))
}
var duration = 0.0
var podcastEpisode:PodcastEpisode? = null
val duration: Double
var podcastEpisode: PodcastEpisode? = null
if (!localEpisodeId.isNullOrEmpty()) {
val podcast = localLibraryItem.media as Podcast
podcastEpisode = podcast.episodes?.find { episode ->
@@ -252,6 +420,8 @@ class AbsDatabase : Plugin() {
progress = if (isFinished) 1.0 else 0.0,
currentTime = 0.0,
isFinished = isFinished,
ebookLocation = null,
ebookProgress = null,
lastUpdate = currentTime,
startedAt = if (isFinished) currentTime else 0L,
finishedAt = if (isFinished) currentTime else null,
@@ -273,13 +443,13 @@ class AbsDatabase : Plugin() {
// Send update to server media progress is linked to a server and user is logged into that server
localMediaProgress.serverConnectionConfigId?.let { configId ->
if (DeviceManager.serverConnectionConfigId == configId) {
var libraryItemId = localMediaProgress.libraryItemId ?: ""
var episodeId = localMediaProgress.episodeId ?: ""
var updatePayload = JSObject()
val libraryItemId = localMediaProgress.libraryItemId ?: ""
val episodeId = localMediaProgress.episodeId ?: ""
val updatePayload = JSObject()
updatePayload.put("isFinished", isFinished)
apiHandler.updateMediaProgress(libraryItemId,episodeId,updatePayload) {
Log.d(tag, "updateLocalMediaProgressFinished: Updated media progress isFinished on server")
var jsobj = JSObject()
val jsobj = JSObject()
jsobj.put("local", true)
jsobj.put("server", true)
jsobj.put("localMediaProgress", JSObject(lmpstring))
@@ -288,7 +458,7 @@ class AbsDatabase : Plugin() {
}
}
if (localMediaProgress.serverConnectionConfigId == null || DeviceManager.serverConnectionConfigId != localMediaProgress.serverConnectionConfigId) {
var jsobj = JSObject()
val jsobj = JSObject()
jsobj.put("local", true)
jsobj.put("server", false)
jsobj.put("localMediaProgress", JSObject(lmpstring))
@@ -296,27 +466,76 @@ class AbsDatabase : Plugin() {
}
}
@PluginMethod
fun updateLocalEbookProgress(call:PluginCall) {
val localLibraryItemId = call.getString("localLibraryItemId", "").toString()
val ebookLocation = call.getString("ebookLocation", "").toString()
val ebookProgress = call.getDouble("ebookProgress") ?: 0.0
val localMediaProgressId = localLibraryItemId
var localMediaProgress = DeviceManager.dbManager.getLocalMediaProgress(localMediaProgressId)
if (localMediaProgress == null) {
Log.d(tag, "updateLocalEbookProgress Local Media Progress not found $localMediaProgressId - Creating new")
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
?: return call.resolve(JSObject("{\"error\":\"Library Item not found\"}"))
val book = localLibraryItem.media as Book
localMediaProgress = LocalMediaProgress(
id = localMediaProgressId,
localLibraryItemId = localLibraryItemId,
localEpisodeId = null,
duration = book.duration ?: 0.0,
progress = 0.0,
currentTime = 0.0,
isFinished = false,
ebookLocation = ebookLocation,
ebookProgress = ebookProgress,
lastUpdate = System.currentTimeMillis(),
startedAt = 0L,
finishedAt = null,
serverConnectionConfigId = localLibraryItem.serverConnectionConfigId,
serverAddress = localLibraryItem.serverAddress,
serverUserId = localLibraryItem.serverUserId,
libraryItemId = localLibraryItem.libraryItemId,
episodeId = null)
} else {
localMediaProgress.updateEbookProgress(ebookLocation, ebookProgress)
}
// Save local media progress locally
DeviceManager.dbManager.saveLocalMediaProgress(localMediaProgress)
val lmpstring = jacksonMapper.writeValueAsString(localMediaProgress)
Log.d(tag, "updateLocalEbookProgress: Local Media Progress String $lmpstring")
val jsobj = JSObject()
jsobj.put("localMediaProgress", JSObject(lmpstring))
call.resolve(jsobj)
}
@PluginMethod
fun updateLocalTrackOrder(call:PluginCall) {
var localLibraryItemId = call.getString("localLibraryItemId", "") ?: ""
var localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
val localLibraryItemId = call.getString("localLibraryItemId", "") ?: ""
val localLibraryItem = DeviceManager.dbManager.getLocalLibraryItem(localLibraryItemId)
if (localLibraryItem == null) {
call.resolve()
return
}
var audioTracks = localLibraryItem.media.getAudioTracks() as MutableList
val audioTracks = localLibraryItem.media.getAudioTracks() as MutableList
var tracks:JSArray = call.getArray("tracks") ?: JSArray()
val tracks:JSArray = call.getArray("tracks") ?: JSArray()
Log.d(tag, "updateLocalTrackOrder $tracks")
var index = 1
var hasUpdates = false
for (i in 0 until tracks.length()) {
var track = tracks.getJSONObject(i)
var localFileId = track.getString("localFileId")
val track = tracks.getJSONObject(i)
val localFileId = track.getString("localFileId")
var existingTrack = audioTracks.find{ it.localFileId == localFileId }
val existingTrack = audioTracks.find{ it.localFileId == localFileId }
if (existingTrack != null) {
Log.d(tag, "Found existing track ${existingTrack.localFileId} that has index ${existingTrack.index} should be index $index")
if (existingTrack.index != index) hasUpdates = true
@@ -336,4 +555,37 @@ class AbsDatabase : Plugin() {
call.resolve()
}
}
@PluginMethod
fun updateDeviceSettings(call:PluginCall) { // Returns device data
Log.d(tag, "updateDeviceSettings called")
val newDeviceSettings = jacksonMapper.readValue<DeviceSettings>(call.data.toString())
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 called")
val mediaId = call.getString("mediaId") ?: ""
GlobalScope.launch(Dispatchers.IO) {
val mediaItemHistory = DeviceManager.dbManager.getMediaItemHistory(mediaId)
if (mediaItemHistory == null) {
call.resolve()
} else {
call.resolve(JSObject(jacksonMapper.writeValueAsString(mediaItemHistory)))
}
}
}
}
@@ -1,21 +1,14 @@
package com.audiobookshelf.app.plugins
import android.app.DownloadManager
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.util.Log
import androidx.documentfile.provider.DocumentFile
import com.anggrayudi.storage.callback.FileCallback
import com.anggrayudi.storage.file.*
import com.anggrayudi.storage.media.FileDescription
import com.audiobookshelf.app.MainActivity
import com.audiobookshelf.app.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.fasterxml.jackson.annotation.JsonIgnore
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
@@ -23,109 +16,65 @@ import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.File
@CapacitorPlugin(name = "AbsDownloader")
class AbsDownloader : Plugin() {
private val tag = "AbsDownloader"
var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
private var jacksonMapper = jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature())
lateinit var mainActivity: MainActivity
lateinit var downloadManager: DownloadManager
lateinit var apiHandler: ApiHandler
lateinit var folderScanner: FolderScanner
lateinit var downloadItemManager: DownloadItemManager
data class DownloadItemPart(
val id: String,
val filename: String,
val finalDestinationPath:String,
val itemTitle: String,
val serverPath: String,
val localFolderName: String,
val localFolderUrl: String,
val localFolderId: String,
val audioTrack: AudioTrack?,
val episode:PodcastEpisode?,
var completed:Boolean,
var moved:Boolean,
var failed:Boolean,
@JsonIgnore val uri: Uri,
@JsonIgnore val destinationUri: Uri,
@JsonIgnore val finalDestinationUri: Uri,
var downloadId: Long?,
var progress: Long
) {
companion object {
fun make(filename:String, destinationFile:File, finalDestinationFile:File, itemTitle:String, serverPath:String, localFolder:LocalFolder, audioTrack:AudioTrack?, episode:PodcastEpisode?) :DownloadItemPart {
val destinationUri = Uri.fromFile(destinationFile)
val finalDestinationUri = Uri.fromFile(finalDestinationFile)
var downloadUrl = "${DeviceManager.serverAddress}${serverPath}?token=${DeviceManager.token}"
if (serverPath.endsWith("/cover")) downloadUrl += "&format=jpeg" // For cover images force to jpeg
val downloadUri = Uri.parse(downloadUrl)
Log.d("DownloadItemPart", "Audio File Destination Uri: $destinationUri | Final Destination Uri: $finalDestinationUri | Download URI $downloadUri")
return DownloadItemPart(
id = DeviceManager.getBase64Id(finalDestinationFile.absolutePath),
filename = filename, finalDestinationFile.absolutePath,
itemTitle = itemTitle,
serverPath = serverPath,
localFolderName = localFolder.name,
localFolderUrl = localFolder.contentUrl,
localFolderId = localFolder.id,
audioTrack = audioTrack,
episode = episode,
completed = false,
moved = false,
failed = false,
uri = downloadUri,
destinationUri = destinationUri,
finalDestinationUri = finalDestinationUri,
downloadId = null,
progress = 0
)
}
private val clientEventEmitter = (object : DownloadItemManager.DownloadEventEmitter {
override fun onDownloadItem(downloadItem:DownloadItem) {
notifyListeners("onDownloadItem", JSObject(jacksonMapper.writeValueAsString(downloadItem)))
}
@JsonIgnore
fun getDownloadRequest(): DownloadManager.Request {
val dlRequest = DownloadManager.Request(uri)
dlRequest.setTitle(filename)
dlRequest.setDescription("Downloading to $localFolderName for book $itemTitle")
dlRequest.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
dlRequest.setDestinationUri(destinationUri)
return dlRequest
override fun onDownloadItemPartUpdate(downloadItemPart:DownloadItemPart) {
notifyListeners("onDownloadItemPartUpdate", JSObject(jacksonMapper.writeValueAsString(downloadItemPart)))
}
}
data class DownloadItem(
val id: String,
val libraryItemId:String,
val episodeId:String?,
val userMediaProgress:MediaProgress?,
val serverConnectionConfigId:String,
val serverAddress:String,
val serverUserId:String,
val mediaType: String,
val itemFolderPath:String,
val localFolder: LocalFolder,
val itemTitle: String,
val media:MediaType,
val downloadItemParts: MutableList<DownloadItemPart>
)
var downloadQueue: MutableList<DownloadItem> = mutableListOf()
override fun 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 = DownloadServiceHost.ensure(mainActivity)
DownloadServiceHost.attachBridge(mainActivity, clientEventEmitter)
}
Log.d(tag, "Build SDK ${Build.VERSION.SDK_INT}")
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
@@ -133,49 +82,59 @@ class AbsDownloader : Plugin() {
val libraryItemId = call.data.getString("libraryItemId").toString()
var episodeId = call.data.getString("episodeId").toString()
if (episodeId == "null") episodeId = ""
val localFolderId = call.data.getString("localFolderId").toString()
Log.d(tag, "Download library item $libraryItemId to folder $localFolderId / episode: $episodeId")
var localFolderId = call.data.getString("localFolderId", "").toString()
AbsLogger.info(tag, "Requested download for item $libraryItemId${if (episodeId.isEmpty()) "" else " / episode $episodeId"}")
val downloadId = if (episodeId.isEmpty()) libraryItemId else "$libraryItemId-$episodeId"
if (downloadQueue.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 ->
Log.d(tag, "Got library item from server ${libraryItem.id}")
val localFolder = DeviceManager.dbManager.getLocalFolder(localFolderId)
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\"}"))
} else {
startLibraryItemDownload(libraryItem, localFolder, episode)
call.resolve()
}
} else {
startLibraryItemDownload(libraryItem, localFolder, null)
call.resolve()
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\"}"))
}
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 {
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 {
call.resolve(JSObject("{\"error\":\"Local Folder Not Found\"}"))
}
}
}
// Clean folder path so it can be used in URL
private fun cleanRelPath(relPath: String): String {
val cleanedRelPath = relPath.replace("\\", "/").replace("%", "%25").replace("#", "%23")
return if (cleanedRelPath.startsWith("/")) cleanedRelPath.substring(1) else cleanedRelPath
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
@@ -198,226 +157,116 @@ class AbsDownloader : Plugin() {
return newTitle
}
private fun startLibraryItemDownload(libraryItem: LibraryItem, localFolder: LocalFolder, episode:PodcastEpisode?) {
val tempFolderPath = mainActivity.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
private fun startLibraryItemDownload(
libraryItem: LibraryItem,
localFolder: LocalFolder,
episode: PodcastEpisode?,
callback: (String?) -> Unit
) {
val isInternal = localFolder.id.startsWith("internal-")
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}"
}
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")
val itemFolderPath = localFolder.absolutePath + "/" + bookTitle
val downloadItem = DownloadItem(libraryItem.id, libraryItem.id, null, libraryItem.userMediaProgress,DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, bookTitle, libraryItem.media, mutableListOf())
AbsLogger.info(tag, "Queueing library item download with ${tracks.size} files")
val itemSubfolder = "$bookAuthor/$bookTitle"
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
book.ebookFile?.let { ebookFile ->
val fileSize = ebookFile.metadata?.size ?: 0
val serverPath = "/api/items/${libraryItem.id}/file/${ebookFile.ino}/download"
val destinationFilename = getFilenameFromRelPath(ebookFile.metadata?.relPath ?: "")
val finalDestinationFile = File("$itemFolderPath/$destinationFilename")
val destinationFile = File("$tempFolderPath/$destinationFilename.part")
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, fileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,ebookFile,null,null)
downloadItem.downloadItemParts.add(downloadItemPart)
}
// Create download item part for each audio track
val audioFiles = (libraryItem.media as Book).audioFiles ?: mutableListOf()
tracks.forEach { audioTrack ->
val serverPath = "/s/item/${libraryItem.id}/${cleanRelPath(audioTrack.relPath)}"
val fileSize = audioTrack.metadata?.size ?: 0
// TODO: Currently file ino is only stored on AudioFile. This should be updated server side to be in FileMetadata or on the AudioTrack
val audioFileIno = audioFiles.find { it.metadata.path == audioTrack.metadata?.path }?.ino
val serverPath = "/api/items/${libraryItem.id}/file/${audioFileIno}/download"
val destinationFilename = getFilenameFromRelPath(audioTrack.relPath)
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")
val destinationFile = File("$tempFolderPath/$destinationFilename.part")
if (finalDestinationFile.exists()) {
Log.d(tag, "Audio file already exists, removing it from ${finalDestinationFile.absolutePath}")
finalDestinationFile.delete()
}
val downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,bookTitle,serverPath,localFolder,audioTrack,null)
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, fileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,null,audioTrack,null)
downloadItem.downloadItemParts.add(downloadItemPart)
val dlRequest = downloadItemPart.getDownloadRequest()
val downloadId = downloadManager.enqueue(dlRequest)
downloadItemPart.downloadId = downloadId
}
if (downloadItem.downloadItemParts.isNotEmpty()) {
// Add cover download item
if (libraryItem.media.coverPath != null && libraryItem.media.coverPath?.isNotEmpty() == true) {
val coverLibraryFile = libraryItem.libraryFiles?.find { it.metadata.path == libraryItem.media.coverPath }
val coverFileSize = coverLibraryFile?.metadata?.size ?: 0
val serverPath = "/api/items/${libraryItem.id}/cover"
val destinationFilename = "cover.jpg"
val destinationFile = File("$tempFolderPath/$destinationFilename")
val destinationFilename = "cover-${libraryItem.id}.jpg"
val destinationFile = File("$tempFolderPath/$destinationFilename.part")
val finalDestinationFile = File("$itemFolderPath/$destinationFilename")
if (finalDestinationFile.exists()) {
Log.d(tag, "Cover already exists, removing it from ${finalDestinationFile.absolutePath}")
finalDestinationFile.delete()
}
val downloadItemPart = DownloadItemPart.make(destinationFilename,destinationFile,finalDestinationFile,bookTitle,serverPath,localFolder,null,null)
val downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename, coverFileSize, destinationFile,finalDestinationFile,itemSubfolder,serverPath,localFolder,null,null,null)
downloadItem.downloadItemParts.add(downloadItemPart)
val dlRequest = downloadItemPart.getDownloadRequest()
val downloadId = downloadManager.enqueue(dlRequest)
downloadItemPart.downloadId = downloadId
}
downloadQueue.add(downloadItem)
startWatchingDownloads(downloadItem)
DeviceManager.dbManager.saveDownloadItem(downloadItem)
}
DownloadServiceHost.enqueue(mainActivity, downloadItem, callback)
} else callback("No downloadable files found")
} else {
// Podcast episode download
val podcastTitle = cleanStringForFileSystem(libraryItem.media.metadata.title)
val audioTrack = episode?.audioTrack
Log.d(tag, "Starting podcast episode download")
val itemFolderPath = localFolder.absolutePath + "/" + podcastTitle
val audioFileIno = episode?.audioFile?.ino
val fileSize = audioTrack?.metadata?.size ?: 0
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, libraryItem.media, mutableListOf())
val downloadItem = DownloadItem(downloadItemId, libraryItem.id, episode?.id, libraryItem.userMediaProgress, DeviceManager.serverConnectionConfig?.id ?: "", DeviceManager.serverAddress, DeviceManager.serverUserId, libraryItem.mediaType, itemFolderPath, localFolder, podcastTitle, podcastTitle, libraryItem.media, mutableListOf())
var serverPath = "/s/item/${libraryItem.id}/${cleanRelPath(audioTrack?.relPath ?: "")}"
var serverPath = "/api/items/${libraryItem.id}/file/${audioFileIno}/download"
var destinationFilename = getFilenameFromRelPath(audioTrack?.relPath ?: "")
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(destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,null)
var downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,fileSize, destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,audioTrack,episode)
downloadItem.downloadItemParts.add(downloadItemPart)
var dlRequest = downloadItemPart.getDownloadRequest()
var downloadId = downloadManager.enqueue(dlRequest)
downloadItemPart.downloadId = downloadId
if (libraryItem.media.coverPath != null && libraryItem.media.coverPath?.isNotEmpty() == true) {
val coverLibraryFile = libraryItem.libraryFiles?.find { it.metadata.path == libraryItem.media.coverPath }
val coverFileSize = coverLibraryFile?.metadata?.size ?: 0
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(destinationFilename,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,audioTrack,null)
downloadItem.downloadItemParts.add(downloadItemPart)
dlRequest = downloadItemPart.getDownloadRequest()
downloadId = downloadManager.enqueue(dlRequest)
downloadItemPart.downloadId = downloadId
}
downloadItemPart = DownloadItemPart.make(downloadItem.id, destinationFilename,coverFileSize,destinationFile,finalDestinationFile,podcastTitle,serverPath,localFolder,null,null,null)
downloadItem.downloadItemParts.add(downloadItemPart)
}
downloadQueue.add(downloadItem)
startWatchingDownloads(downloadItem)
DeviceManager.dbManager.saveDownloadItem(downloadItem)
}
}
private fun startWatchingDownloads(downloadItem: DownloadItem) {
GlobalScope.launch(Dispatchers.IO) {
while (downloadItem.downloadItemParts.find { !it.moved && !it.failed } != null) { // While some item is not completed
val numPartsBefore = downloadItem.downloadItemParts.size
checkDownloads(downloadItem)
// Keep database updated as item parts finish downloading
if (downloadItem.downloadItemParts.size > 0 && downloadItem.downloadItemParts.size != numPartsBefore) {
Log.d(tag, "Save download item on num parts changed from $numPartsBefore to ${downloadItem.downloadItemParts.size}")
DeviceManager.dbManager.saveDownloadItem(downloadItem)
}
notifyListeners("onItemDownloadUpdate", JSObject(jacksonMapper.writeValueAsString(downloadItem)))
delay(500)
}
// Remove download notifications
downloadItem.downloadItemParts.forEach { downloadItemPart ->
downloadItemPart.downloadId?.let {
downloadManager.remove(it)
}
}
val downloadItemScanResult = folderScanner.scanDownloadItem(downloadItem)
DeviceManager.dbManager.removeDownloadItem(downloadItem.id)
downloadQueue.remove(downloadItem)
Log.d(tag, "Item download complete ${downloadItem.itemTitle} | local library item id: ${downloadItemScanResult?.localLibraryItem?.id} | Items remaining in Queue ${downloadQueue.size}")
val jsobj = JSObject()
jsobj.put("libraryItemId", downloadItem.id)
jsobj.put("localFolderId", downloadItem.localFolder.id)
downloadItemScanResult?.localLibraryItem?.let { localLibraryItem ->
jsobj.put("localLibraryItem", JSObject(jacksonMapper.writeValueAsString(localLibraryItem)))
}
downloadItemScanResult?.localMediaProgress?.let { localMediaProgress ->
jsobj.put("localMediaProgress", JSObject(jacksonMapper.writeValueAsString(localMediaProgress)))
}
notifyListeners("onItemDownloadComplete", jsobj)
}
}
private fun checkDownloads(downloadItem: DownloadItem) {
val itemParts = downloadItem.downloadItemParts.map { it }
for (downloadItemPart in itemParts) {
if (downloadItemPart.downloadId != null) {
val dlid = downloadItemPart.downloadId!!
val query = DownloadManager.Query().setFilterById(dlid)
downloadManager.query(query).use {
if (it.moveToFirst()) {
val bytesColumnIndex = it.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES)
val statusColumnIndex = it.getColumnIndex(DownloadManager.COLUMN_STATUS)
val bytesDownloadedColumnIndex = it.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR)
val totalBytes = if (bytesColumnIndex >= 0) it.getInt(bytesColumnIndex) else 0
val downloadStatus = if (statusColumnIndex >= 0) it.getInt(statusColumnIndex) else 0
val bytesDownloadedSoFar = if (bytesDownloadedColumnIndex >= 0) it.getInt(bytesDownloadedColumnIndex) else 0
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} bytes $totalBytes | bytes dled $bytesDownloadedSoFar | downloadStatus $downloadStatus")
if (downloadStatus == DownloadManager.STATUS_SUCCESSFUL) {
// Once file download is complete move the file to the final destination
if (!downloadItemPart.completed) {
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Done")
downloadItemPart.completed = true
val file = DocumentFileCompat.fromUri(mainActivity, downloadItemPart.destinationUri)
Log.d(tag, "DOWNLOAD: Attempt move for file at destination ${downloadItemPart.destinationUri} | ${file?.getBasePath(mainActivity)}")
val fcb = object : FileCallback() {
override fun onPrepare() {
Log.d(tag, "DOWNLOAD: PREPARING MOVE FILE")
}
override fun onFailed(errorCode:ErrorCode) {
Log.e(tag, "DOWNLOAD: FAILED TO MOVE FILE $errorCode")
downloadItemPart.failed = true
file?.delete()
}
override fun onCompleted(result:Any) {
Log.d(tag, "DOWNLOAD: FILE MOVE COMPLETED")
val resultDocFile = result as DocumentFile
Log.d(tag, "DOWNLOAD: COMPLETED FILE INFO ${resultDocFile.getAbsolutePath(mainActivity)}")
downloadItemPart.moved = true
}
}
Log.d(tag, "DOWNLOAD: Move file to final destination path: ${downloadItemPart.finalDestinationPath}")
val localFolderFile = DocumentFileCompat.fromUri(mainActivity,Uri.parse(downloadItemPart.localFolderUrl))
val mimetype = if (downloadItemPart.audioTrack != null) MimeType.AUDIO else MimeType.IMAGE
val fileDescription = FileDescription(downloadItemPart.filename, downloadItemPart.itemTitle, mimetype)
file?.moveFileTo(mainActivity,localFolderFile!!,fileDescription,fcb)
} else {
// Why is kotlin requiring an else here..
}
} else if (downloadStatus == DownloadManager.STATUS_FAILED) {
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Failed")
downloadItem.downloadItemParts.remove(downloadItemPart)
} else {
//update progress
val percentProgress = if (totalBytes > 0) ((bytesDownloadedSoFar * 100L) / totalBytes) else 0
Log.d(tag, "checkDownloads Download ${downloadItemPart.filename} Progress = $percentProgress%")
downloadItemPart.progress = percentProgress
}
} else {
Log.d(tag, "Download ${downloadItemPart.filename} not found in dlmanager")
downloadItem.downloadItemParts.remove(downloadItemPart)
}
}
}
DownloadServiceHost.enqueue(mainActivity, downloadItem, callback)
}
}
}
Loaded 100 of 558 files, more files were not shown because too many files have changed in this diff. Show more