Compare commits

...
Author SHA1 Message Date
advplyr 96d4021a3c Version bump v2.36.0 2026-07-27 17:31:12 -05:00
advplyrandGitHub 49826f5163 Merge pull request #5406 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2026-07-27 18:26:57 -04:00
FiendFEARingandHosted Weblate 76e33a2a4e Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1167 of 1167 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2026-07-27 11:54:06 +02:00
FiendFEARingandHosted Weblate 1a440811c5 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1166 of 1166 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2026-07-27 11:54:06 +02:00
advplyrandGitHub 529a16b718 Merge pull request #5407 from nichwall/readme-update
Readme update
2026-07-27 05:53:49 -04:00
Nicholas Wallace 032f0b6b51 Fix installation link 2026-07-26 21:41:51 -07:00
Nicholas Wallace f5bfa1edd6 Add note about frontend rewrite 2026-07-26 21:40:59 -07:00
advplyrandGitHub 4dfad46722 Merge pull request #5405 from advplyr/auth_sessions_enhancements
Delete auth session endpoint & paginate sessions
2026-07-26 18:49:25 -04:00
advplyr c86f336536 Update delete session to return status 200 instead of json 2026-07-26 17:40:14 -05:00
advplyr 1876bfbfe7 Add delete auth session endpoint to logout individual session 2026-07-26 17:36:36 -05:00
advplyr a7b380cd7a Add pagination to /me/sessions endpoint & sessions account page table 2026-07-26 17:11:03 -05:00
advplyrandGitHub 71c5d27ea5 Merge pull request #5269 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2026-07-26 08:45:00 -04:00
FiendFEARingandHosted Weblate 83707e08bf Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1164 of 1164 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2026-07-25 22:44:45 +00:00
Igor DobračaandHosted Weblate e8d1b58bd2 Translated using Weblate (Croatian)
Currently translated at 100.0% (1164 of 1164 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2026-07-25 22:44:44 +00:00
FiendFEARingandHosted Weblate aff79b5579 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1164 of 1164 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2026-07-25 22:44:43 +00:00
Mona LisaandHosted Weblate 321d8bfda1 Translated using Weblate (Swedish)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2026-07-25 22:44:42 +00:00
Axel BjörnssonandHosted Weblate a3ff41f90e Translated using Weblate (Icelandic)
Currently translated at 2.5% (30 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/is/
2026-07-25 22:44:40 +00:00
Mona LisaandHosted Weblate 60f9ac93f7 Translated using Weblate (Swedish)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2026-07-25 22:44:38 +00:00
Mona LisaandHosted Weblate bd61a43458 Translated using Weblate (Swedish)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2026-07-25 22:44:36 +00:00
FiendFEARingandHosted Weblate df6ec07f18 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2026-07-25 22:44:35 +00:00
FiendFEARingandHosted Weblate 6d02aa3aa0 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2026-07-25 22:44:33 +00:00
FiendFEARingandHosted Weblate 144fe743c6 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2026-07-25 22:44:31 +00:00
FiendFEARingandHosted Weblate 0851b346d0 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2026-07-25 22:44:29 +00:00
FiendFEARingandHosted Weblate 53e904314e Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2026-07-25 22:44:25 +00:00
FiendFEARingandHosted Weblate 84f80f1903 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2026-07-25 22:44:20 +00:00
FiendFEARingandHosted Weblate 98b1f4abad Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2026-07-25 22:44:18 +00:00
FiendFEARingandHosted Weblate ec0d147d6d Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2026-07-25 22:44:16 +00:00
FiendFEARingandHosted Weblate a7825bb461 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2026-07-25 22:44:15 +00:00
தமிழ்நேரம்andHosted Weblate 09be7799f6 Translated using Weblate (Tamil)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ta/
2026-07-25 22:44:14 +00:00
தமிழ்நேரம்andHosted Weblate dc6a8a7292 Translated using Weblate (Tamil)
Currently translated at 16.7% (195 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ta/
2026-07-25 22:44:12 +00:00
GabrielandHosted Weblate 0c075674a0 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pt_BR/
2026-07-25 22:44:11 +00:00
advplyrandHosted Weblate 5b13820cb4 Added translation using Weblate (Tamil) 2026-07-25 22:44:10 +00:00
Hans Henrik JuhlandHosted Weblate d5b4d57fe3 Translated using Weblate (Danish)
Currently translated at 99.3% (1156 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2026-07-25 22:44:07 +00:00
Mikkel Dupont OlesenandHosted Weblate f12b64495a Translated using Weblate (Danish)
Currently translated at 99.7% (1160 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2026-07-25 22:44:05 +00:00
Hans Henrik JuhlandHosted Weblate 43f3150a52 Translated using Weblate (Danish)
Currently translated at 99.7% (1160 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2026-07-25 22:44:01 +00:00
Mikkel Dupont OlesenandHosted Weblate 65d3a668e3 Translated using Weblate (Danish)
Currently translated at 99.7% (1160 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2026-07-25 22:43:58 +00:00
Mikkel Dupont OlesenandHosted Weblate 2b5297bee8 Translated using Weblate (Danish)
Currently translated at 99.7% (1160 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2026-07-25 22:43:57 +00:00
no1taandHosted Weblate 08caeb5975 Translated using Weblate (Greek)
Currently translated at 29.2% (340 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/el/
2026-07-25 22:43:55 +00:00
Igor DobračaandHosted Weblate 6c170a5064 Translated using Weblate (Croatian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2026-07-25 22:43:52 +00:00
advplyrandHosted Weblate 4b62f90964 Added translation using Weblate (Uzbek) 2026-07-25 22:43:50 +00:00
advplyrandGitHub e9ea511c90 Merge pull request #5400 from advplyr/account_sessions_table
Account sessions table
2026-07-25 18:43:26 -04:00
advplyr c6afeb7dc9 Fix logout button logging out all devices 2026-07-25 17:20:32 -05:00
advplyr 35a2c5e87c Add /me/sessions endpoint and show sessions table on account page 2026-07-25 17:18:38 -05:00
advplyrandGitHub 4c71a076e7 Merge pull request #5395 from advplyr/logout_all_devices
Add logout all devices button and extend auth logout endpoint
2026-07-24 18:51:35 -04:00
advplyr e56c1099e6 Add logout all devices button and extend auth logout endpoint 2026-07-24 17:44:03 -05:00
advplyrandGitHub dc0b24a03f Merge pull request #5393 from advplyr/pw_change_invalidates_sessions
User password changes invalidate sessions
2026-07-23 18:40:30 -04:00
advplyr c900676be7 User password changes invalidate sessions 2026-07-23 17:21:25 -05:00
advplyrandGitHub a6cde926ee Merge pull request #5336 from francisrath/fix-broken-help-links
Update outdated in-app help links to new docs site structure
2026-07-22 18:07:58 -04:00
advplyr e8db14fe33 Update collection help docs link 2026-07-22 17:07:32 -05:00
advplyrandGitHub e9b533637f Merge pull request #5376 from DanielAshley/contrib/refresh-grace-period
Make refresh-token grace period configurable (default 10 min)
2026-07-22 18:01:32 -04:00
advplyr 8b97da500d Remove TokenManager test 2026-07-22 16:58:03 -05:00
advplyrandGitHub 37b42601ef Merge pull request #5363 from Vito0912/feat/newEndpoints
add progress and bookmarks specific endpoints
2026-07-22 17:10:51 -04:00
advplyr 5da4ec51ce Remove MeController test 2026-07-22 16:05:59 -05:00
advplyr 4c65a87d5c Update bookmarks endpoints to use json object as root 2026-07-22 16:05:37 -05:00
0e5618dcd8 Apply suggestions from code review
Co-authored-by: advplyr <dev@advplyr.com>
2026-07-22 10:20:41 +02:00
advplyrandGitHub d063d851e1 Merge pull request #5388 from mikiher/fix/bulk-download-permissions
Enforce per-item access checks on bulk library download
2026-07-21 18:34:13 -04:00
advplyrandGitHub 6458e6d3d2 Merge pull request #5387 from mikiher/fix/refresh-token-auth
Reject refresh tokens on API and WebSocket authentication
2026-07-21 18:20:57 -04:00
advplyr 70953d8c82 Slim down TokenManager test 2026-07-21 17:18:58 -05:00
advplyrandGitHub 2d9f63963e Merge pull request #5370 from mikiher/fix/root-user-delete-protection
Prevent admin users from deleting the root account
2026-07-20 18:35:32 -04:00
advplyr f12abca3fc Simplify user delete checks 2026-07-20 17:33:15 -05:00
advplyr bced308462 Trim down UserController test 2026-07-20 17:31:03 -05:00
advplyrandGitHub 528d03fef3 Merge pull request #5386 from mikiher/fix/oidc-url-validation
Fix oidc url validation
2026-07-20 18:08:51 -04:00
advplyr b93c3133a9 Update x-forwarded-proto to check comma separated value 2026-07-20 17:06:21 -05:00
mikiher 8d9db8d103 fix OidcAuthStrategy test module load order 2026-07-20 18:43:15 +03:00
advplyr 08bbf9cd9d Update server settings to include host timezone for use in cron scheduler next run dates 2026-07-19 17:31:00 -05:00
Daniel 95af4812cd Make the refresh-token grace period configurable (default 10 min)
When /auth/refresh rotates the refresh token, the previous token is kept
valid for a short grace period so a client that never received the
rotation response (e.g. a dropped or suspended mobile request) can retry
with the old token and get back the already-rotated current token
instead of a 401.

That window was hardcoded to 60 seconds, which is too short for real
mobile conditions: iOS backgrounding, VPN/proxy timeouts, or network
handoff can delay the retry past a minute, permanently locking the
session out (#5281).

Make it configurable via REFRESH_TOKEN_GRACE_PERIOD (seconds), following
the existing REFRESH_TOKEN_EXPIRY / ACCESS_TOKEN_EXPIRY pattern, and
raise the default to 10 minutes. Adds unit tests for the new config.

Fixes #5281
2026-07-18 21:42:06 -04:00
mikiher 8ec547e302 fix(auth): refresh tokens authentication in API and WebSocket requests 2026-07-16 18:58:44 +03:00
mikiher 32e4da8c92 fix(permissions): enforce per-item access on bulk library download 2026-07-16 18:11:19 +03:00
mikiher 0ce37004af fix(auth): harden OIDC web callback URL validation 2026-07-16 17:11:11 +03:00
mikiher 527745b54e refactor(server): centralize request HTTPS and origin detection 2026-07-16 17:10:42 +03:00
mikiher 3c016314e6 fix(auth): prevent admin users from deleting the root account 2026-07-15 20:34:02 +03:00
advplyrandGitHub 59c7d0275c Merge pull request #5354 from mikiher/scan-author-events
Emit author socket events when book scans create or link authors
2026-07-13 19:27:31 -04:00
Finn Dittmar 4883d0f60c add progress and bookmarks specific endpoints 2026-07-13 11:41:49 +02:00
mikiher 9d9ab49664 Batch author numBooks updates at scan end via authors_num_books_updated 2026-07-10 17:37:31 +03:00
advplyrandGitHub 82aec5f60c Merge pull request #5341 from mikiher/expanded-minified-superset
Add all minified fields to expanded library item JSON
2026-07-09 16:56:45 -05:00
mikiher 7ee5e1bdf6 Emit author socket events when book scans create or link authors 2026-07-09 10:35:18 +03:00
mikiher 3bb53d939d Add all minified fields to expanded library item JSON so socket updates remain a strict superset of shelf payloads 2026-07-01 22:30:21 +03:00
advplyrandGitHub 6f03467f35 Update links in the README 2026-06-29 16:46:33 -05:00
advplyrandGitHub b2f593f1c4 Update readme organizing docs link & remove api docs link 2026-06-29 16:43:24 -05:00
advplyrandGitHub 58dcda36a5 Cleanup readme and link to contributing docs 2026-06-29 16:37:54 -05:00
advplyrandGitHub 354e0bf793 Update readme remove reverse proxy set up and link to docs page 2026-06-29 16:32:41 -05:00
Francis Rath 0784373b61 Update outdated in-app help links to new docs site structure
The help (?) links throughout the client pointed to the old
www.audiobookshelf.org/guides/* pages, which no longer exist and
resolve to a 404. Update all of them to the current
audiobookshelf.org/docs/documentation/* locations.
2026-06-29 03:02:19 +02:00
advplyrandGitHub 2f219ea3cc Merge pull request #5325 from mikiher/file-upload-internal-api
Let next.js handle file uploads through internal-api routes
2026-06-25 17:03:34 -05:00
mikiher 56e60b8420 Server.js: Let next.js handle file uploads through internal-api routes 2026-06-23 14:08:16 +03:00
advplyrandGitHub 9b92b5de34 Merge pull request #5318 from mikiher/match-update-episode-enclosure
Enhance PodcastController to handle enclosure updates
2026-06-20 17:06:32 -05:00
mikiher 3417c0c721 Enhance PodcastController to handle enclosure updates, allowing for null values and object structure validation for enclosure properties. 2026-06-18 12:39:50 +03:00
advplyrandGitHub cbda0360aa Merge pull request #5291 from mikiher/allowed-dev-origins
Read AllowedDevOrigins from dev.js into ALLOWED_DEV_ORIGINS env var
2026-06-04 15:13:34 -05:00
mikiher 036bc081f0 index.js: Read AllowedDevOrigins from dev,js into ALLOWED_DEV_ORIGINS env var 2026-06-04 14:03:59 +03:00
advplyr e70e4b9d40 Fix typo on onTest notification body 2026-05-30 15:43:50 -05:00
advplyr aacdcc47ec Version bump v2.35.1 2026-05-28 15:22:55 -05:00
advplyr 499b52b4dd Update Sequelize where query for User username/email case insensitive 2026-05-28 14:49:49 -05:00
advplyr 1bad2d9072 Cleanup abmetadata file parsing & fix server crash #5268 #4287 #5142 2026-05-27 17:33:14 -05:00
advplyrandGitHub c009db9f28 Merge pull request #5256 from nichwall/fix-bookauthor-collision-on-rename
Fix duplicate bookAuthor creation when renaming authors
2026-05-22 15:43:13 -05:00
advplyrandGitHub 325469c5a5 Merge pull request #5255 from nichwall/refresh-token-uniqueness
Add unique UUID to access and refresh tokens
2026-05-22 15:39:01 -05:00
Nicholas Wallace c97b36e11c Add ignoreDuplicates for bookAuthor when renaming to respect unique index 2026-05-21 21:06:17 -07:00
Nicholas Wallace e944b2a2f5 Add unique UUID to access and refresh tokens 2026-05-21 17:08:39 -07:00
advplyr 2d0a5462d2 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2026-05-17 14:31:45 -05:00
advplyr 72dc75482f Version bump v2.35.0 2026-05-17 14:31:41 -05:00
advplyrandGitHub cac74f3477 Merge pull request #5004 from nichwall/token_refresh_race_condition
Access token refresh grace period
2026-05-17 14:16:58 -05:00
advplyrandGitHub 1ad11b2b9e Merge pull request #5216 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2026-05-17 14:16:24 -05:00
Pavel MiniutkaandHosted Weblate 50eeca2e0f Translated using Weblate (Belarusian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2026-05-15 18:13:27 +00:00
EteranlKandHosted Weblate 4f21fc023c Translated using Weblate (Arabic)
Currently translated at 96.3% (1120 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ar/
2026-05-15 18:13:26 +00:00
advplyrandHosted Weblate 52a485d135 Added translation using Weblate (Latvian) 2026-05-15 18:13:25 +00:00
d0nizamandHosted Weblate 3b025076e8 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/bg/
2026-05-15 18:13:23 +00:00
Mateusz LesiakandHosted Weblate 6d5d89429d Translated using Weblate (Polish)
Currently translated at 99.8% (1161 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2026-05-15 18:13:22 +00:00
advplyr c010f0e1eb Fix android device sdkVersion not handling it using number type, causing android session device names to show as iOS 2026-05-15 13:13:14 -05:00
advplyr eee377e081 Cleanup TokenManager logs 2026-05-13 16:23:26 -05:00
advplyr b0aaa24660 Update socket events to check client is admin & validate log level 2026-05-12 16:57:28 -05:00
advplyr 47ea6b5092 Update book/podcast scanner to sanitize description pulled from metadata 2026-05-05 17:18:49 -05:00
advplyrandGitHub 4b060febc2 Merge pull request #5221 from brandonfhall/fix/rss-feed-m4b-content-type
Fix: RSS feed serves m4b files with correct Content-Type: audio/mp4
2026-05-03 14:40:43 -05:00
Brandon 40869bcf39 fix: set correct Content-Type for RSS feed audio files
Express's mime package does not recognize .m4b, causing it to fall back
to application/octet-stream. This reuses the existing
getAudioMimeTypeFromExtname utility (already applied to the download
endpoint) to set the correct audio/mp4 header before sendFile.

Fixes #5041
2026-05-02 22:13:35 -04:00
advplyr 3942805129 Cleanup rotateTokensForSession 2026-04-30 16:25:43 -05:00
advplyr dc446862c1 Rename migration to v2.35.0 & merge master 2026-04-30 16:08:24 -05:00
advplyr 379f6c716a Merge branch 'master' into token_refresh_race_condition 2026-04-30 15:59:22 -05:00
advplyr 47457ee1e7 Version bump v2.34.0 2026-04-27 16:51:34 -05:00
advplyrandGitHub cb6ff9eedf Merge pull request #5204 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2026-04-27 16:45:57 -05:00
LvanAlphenandHosted Weblate 5dc01261c1 Translated using Weblate (Dutch)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nl/
2026-04-26 21:51:49 +00:00
Naoto IshikawaandHosted Weblate cbc103cf05 Translated using Weblate (Japanese)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ja/
2026-04-26 21:51:49 +00:00
Pavel MiniutkaandHosted Weblate e79256d0fb Translated using Weblate (Belarusian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2026-04-26 21:51:48 +00:00
ugyesandHosted Weblate f8ef56c6bc Translated using Weblate (Hungarian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2026-04-26 21:51:48 +00:00
advplyr 62d7097e23 Add ApiCacheManager test for should remove recent-episodes cache entries 2026-04-26 16:51:39 -05:00
advplyr 92df92ec99 Fix recent episodes endpoint cache not being cleared when updating media progress #5159 2026-04-26 16:51:08 -05:00
advplyrandGitHub 1c229e0627 Merge pull request #5211 from na3shkw/add-i18n-japanese
Add Japanese (ja) language and Japan podcast search region
2026-04-26 16:19:37 -05:00
advplyrandGitHub f8a71cc514 Merge pull request #5089 from meek2100/pass_managers
feat: add autocomplete attributes for password manager support
2026-04-26 16:16:42 -05:00
Naoto IshikawaandGitHub 63de5bb2d5 Merge branch 'advplyr:master' into add-i18n-japanese 2026-04-26 15:23:22 +09:00
advplyrandGitHub 2c3108a1fa Merge pull request #5163 from pjkottke/master
The timestamp in the share URL should override the saved position for the user.
2026-04-25 17:15:23 -05:00
advplyr 928051744a ShareController check ?t param is less than duration, revert frontend mounted usage of param 2026-04-25 17:13:22 -05:00
advplyr 3ccdcaec1a Implement SSRF filter for podcast episode downloads 2026-04-25 16:46:54 -05:00
na3shkw f47bbc7886 Add Japanese language and Japan podcast search region 2026-04-25 15:56:16 +00:00
advplyr 7c0ca44727 Update podcast create/update endpoints to validate autoDownloadSchedule cron expression, validate cron expression before starting in CronManager 2026-04-24 16:55:42 -05:00
advplyr d6a2e5596b Fix undefined variable in error log for when podcast cron is invalid 2026-04-24 16:18:56 -05:00
advplyr a5362de9cc Update podcast createFromRequest to sanitize html description 2026-04-23 14:34:59 -05:00
advplyr 9ab35ef418 Update playlist endpoints to check user still has library access 2026-04-22 16:42:58 -05:00
advplyr 79cc9765cf Update collection endpoints to check user library access 2026-04-22 16:29:47 -05:00
advplyr 5b2a788cfc Update LibraryItemController test with 403 tests 2026-04-21 17:13:52 -05:00
advplyr 80b39abaa2 Update library item batch api endpoints check users per-item access & return 403 2026-04-21 17:13:06 -05:00
advplyr b41db23994 Version bump v2.33.2 2026-04-19 16:46:10 -05:00
advplyrandGitHub 125f265f55 Merge pull request #5141 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2026-04-19 16:21:40 -05:00
GernomalyandHosted Weblate aa4a191567 Translated using Weblate (German)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2026-04-19 21:20:46 +00:00
JanandHosted Weblate e431ea0472 Translated using Weblate (German)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2026-04-19 21:20:45 +00:00
Laurin SorgendandHosted Weblate e3388d4446 Translated using Weblate (German)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2026-04-19 21:20:45 +00:00
MarioandHosted Weblate 88879f1409 Translated using Weblate (German)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2026-04-19 21:20:44 +00:00
Pavel MiniutkaandHosted Weblate 3e0099e8d9 Translated using Weblate (Belarusian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2026-04-19 21:20:44 +00:00
tizio04andHosted Weblate f558182d94 Translated using Weblate (Italian)
Currently translated at 99.9% (1162 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2026-04-19 21:20:43 +00:00
Владимир МакеевandHosted Weblate a30fe15b10 Translated using Weblate (Russian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2026-04-19 21:20:43 +00:00
A LandHosted Weblate 0bbf8bde5c Translated using Weblate (Bulgarian)
Currently translated at 91.2% (1061 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/bg/
2026-04-19 21:20:42 +00:00
AlexandHosted Weblate 0e2cdde731 Translated using Weblate (Slovak)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sk/
2026-04-19 21:20:41 +00:00
tfr tintandHosted Weblate bc6bfbe804 Translated using Weblate (Italian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2026-04-19 21:20:41 +00:00
Vadzim KurdzesauandHosted Weblate 2755204168 Translated using Weblate (Russian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2026-04-19 21:20:40 +00:00
Francisco SerradorandHosted Weblate 2d4df273f0 Translated using Weblate (Spanish)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2026-04-19 21:20:40 +00:00
Pavel MiniutkaandHosted Weblate d73b64a19c Translated using Weblate (Belarusian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2026-04-19 21:20:39 +00:00
advplyr b7e8a0474a Update bulk download endpoint ensure items are from the same library requested 2026-04-19 16:20:31 -05:00
advplyr 39adefb632 Update backup load & upload to remove tempfile on failed backups, validate details filesize & close zip 2026-04-18 17:03:37 -05:00
advplyr 24cab79c66 Update filesystem/pathexists endpoint to use existing isSameOrSubPath func 2026-04-18 16:24:48 -05:00
advplyr b27f21fd95 Update podcastUtils to sanitize episode subtitle from rss feed 2026-04-17 16:59:22 -05:00
advplyr 09fa0b38f5 Update podcast create path validation & fix relPath 2026-04-17 16:51:22 -05:00
advplyr 455e605162 Update author & library item image endpoints to clamp width/height query params 2026-04-17 16:30:08 -05:00
advplyrandGitHub 88667d00a1 Merge pull request #5115 from rktjmp/fix-mka-opus-desktop-streaming
Force AAC transcode when streaming mka+opus to desktop client
2026-04-10 16:54:18 -05:00
advplyr 94c426bd97 Update comments on matroska 2026-04-10 16:42:39 -05:00
Oliver Marriott 522b9735e2 Add audio/(x-)matroska to client player MIME types to avoid transcode
Firefox, at least, supports playing `matroska/audio` containers natively but
the client was not checking for support. Clients  that do not support
playing `matroska/audio` containers will fallback to transcoding.
2026-04-09 21:07:14 +10:00
peter.kottke 5a6b3d8e61 updates to allow share t argument to over-ride server stored position 2026-04-01 21:05:48 -04:00
advplyrandGitHub 64cbf59609 Merge pull request #5160 from mikiher/fix-item-removed-payload
Fix item_removed payload to include libraryId
2026-03-31 16:43:51 -05:00
mikiher fda1a6ea9b Fix item_removed payload to include libraryId 2026-03-31 22:02:52 +03:00
advplyrandGitHub c4c8b8d0f2 Merge pull request #5158 from mikiher/book-update-author-events
Emit proper author_updated/added events when updating book media
2026-03-30 16:26:47 -05:00
advplyr ab3bd6f4a1 Update JS docs 2026-03-30 16:22:27 -05:00
mikiher 093124aac6 Emit proper author_updated/added events when updating book media 2026-03-30 22:02:56 +03:00
advplyr 5de92d08f9 Fix share playback session not including coverAspectRatio 2026-03-29 15:36:07 -05:00
advplyr 8b89b27654 Version bump v2.33.1 2026-03-19 17:09:14 -05:00
advplyr 3faa6f3e7d Update playlist create/update endpoint to strip all html tags 2026-03-19 16:57:22 -05:00
advplyr 9821c31f8e Update collection create/update endpoints to strip html tags from collection name 2026-03-19 16:53:21 -05:00
advplyrandGitHub efe2a22674 Merge pull request #5122 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2026-03-19 16:41:19 -05:00
Francisco SerradorandHosted Weblate 9634c46bc5 Translated using Weblate (Spanish)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2026-03-18 23:01:31 +01:00
Francisco SerradorandHosted Weblate 5f8db24b96 Translated using Weblate (Spanish)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2026-03-18 23:01:30 +01:00
Francisco SerradorandHosted Weblate e781ff5eae Translated using Weblate (Spanish)
Currently translated at 99.7% (1160 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2026-03-18 23:01:30 +01:00
Francisco SerradorandHosted Weblate 32a17c0044 Translated using Weblate (Spanish)
Currently translated at 99.7% (1160 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2026-03-18 23:01:29 +01:00
Fabian JülichandHosted Weblate f84831d6f1 Translated using Weblate (German)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2026-03-18 23:01:29 +01:00
Pavel MiniutkaandHosted Weblate dc54d42dcf Translated using Weblate (Belarusian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2026-03-18 23:01:28 +01:00
Pavel MiniutkaandHosted Weblate 15af7407ff Translated using Weblate (Belarusian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2026-03-18 23:01:27 +01:00
CharlieandHosted Weblate 5d9682410a Translated using Weblate (French)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2026-03-18 23:01:27 +01:00
advplyr 4bdd76d94c Update podcast episode update endpoint to sanitize subtitle 2026-03-18 17:01:19 -05:00
advplyr 7c0d9efe91 Update Confirm component to support allowHtml prompt option 2026-03-18 16:51:51 -05:00
advplyr 874e9e1856 Update API Key jwtAuthCheck to check user active status 2026-03-18 16:17:45 -05:00
advplyr 6d3773a0b8 Version bump v2.33.0 2026-03-12 17:01:54 -05:00
advplyr a47c869d0b Update migration file to v2.33.0 2026-03-12 16:45:08 -05:00
advplyrandGitHub eb0383d37a Merge pull request #5073 from kevingatera/perf/minimal-upstream-patchset
Improve personalized/discover query performance and cache invalidation behavior
2026-03-12 16:38:26 -05:00
advplyr e66ffb9c23 Add indexes to MediaProgress and BookSeries models 2026-03-12 16:37:59 -05:00
advplyr 972193b193 Update server settings authLoginCustomMessage to sanitize on save and load 2026-03-11 17:18:05 -05:00
advplyr 690a7e0da9 Update session DeviceInfo with sanitize on clientDeviceInfo 2026-03-11 17:03:07 -05:00
Oliver Marriott d9355ac3aa Force AAC transcode when streaming mka+opus to desktop client
Matroska audio containers (aka mka files) with Opus codec streams inside
were unplayable on the desktop client because hls.js was unable to
decode the stream, resulting in an infinitely "spinning" play button.

When configuring a stream, we now check for the opus codec and force AAC
transcoding.

Matroska containers support other codecs besides Opus, eg: mp3, which do
not require transcoding and work fine before this patch, which is why we
check for opus in codecsToForceAAC instead of AudioMimeType.MKA in
mimeTypesToForceAAC.

The AudioMimeType.OPUS mimetype is already marked as requiring
transcoding but since its inside a container this check does not
evaluate to true, we must check the codec explicitly.
2026-03-11 00:35:12 +11:00
advplyrandGitHub fbe1d1eed6 Merge pull request #5030 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2026-03-08 17:30:54 -05:00
Grzegorz OrlowskiandHosted Weblate e83aca572e Translated using Weblate (Polish)
Currently translated at 95.7% (1113 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2026-03-08 23:26:10 +01:00
ArturandHosted Weblate 367826ce64 Translated using Weblate (Polish)
Currently translated at 95.7% (1113 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2026-03-08 23:26:10 +01:00
Torstein EideandHosted Weblate 6e6c43c53c Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nb_NO/
2026-03-08 23:26:10 +01:00
weblate.user.1274andHosted Weblate 6479cdb66d Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nb_NO/
2026-03-08 23:26:10 +01:00
Øystein S. HegnanderandHosted Weblate 635e132325 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nb_NO/
2026-03-08 23:26:10 +01:00
thehijackerandHosted Weblate 45dd843ce1 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2026-03-08 23:26:10 +01:00
Torstein EideandHosted Weblate 7c956b1582 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nb_NO/
2026-03-08 23:26:10 +01:00
Pavel MiniutkaandHosted Weblate 9afa39e29e Translated using Weblate (Belarusian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2026-03-08 23:26:10 +01:00
Pavel MiniutkaandHosted Weblate 5f8450602e Translated using Weblate (Belarusian)
Currently translated at 92.1% (1072 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2026-03-08 23:26:10 +01:00
Pavel MiniutkaandHosted Weblate fa6dae1a53 Translated using Weblate (Belarusian)
Currently translated at 79.8% (929 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2026-03-08 23:26:10 +01:00
lambolightingandHosted Weblate a1d439b8d5 Translated using Weblate (Greek)
Currently translated at 27.6% (322 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/el/
2026-03-08 23:26:10 +01:00
peter cernyandHosted Weblate 2f32673991 Translated using Weblate (Slovak)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sk/
2026-03-08 23:26:10 +01:00
herny ucetandHosted Weblate c1a6b51d78 Translated using Weblate (Slovak)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sk/
2026-03-08 23:26:09 +01:00
GnebandHosted Weblate 6c2e13fb4e Translated using Weblate (Slovak)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sk/
2026-03-08 23:26:09 +01:00
PlazecandHosted Weblate 210fa55b6a Translated using Weblate (Czech)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2026-03-08 23:26:09 +01:00
Silviu BajenaruandHosted Weblate fb8ca043ad Translated using Weblate (Romanian)
Currently translated at 55.1% (641 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ro/
2026-03-08 23:26:09 +01:00
Karl BernstålandHosted Weblate 69d7c399b8 Translated using Weblate (Swedish)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2026-03-08 23:26:09 +01:00
Igor DobračaandHosted Weblate a35ba05600 Translated using Weblate (Croatian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2026-03-08 23:26:09 +01:00
B0raxandHosted Weblate 40f42b2ab6 Translated using Weblate (German)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2026-03-08 23:26:09 +01:00
MaxklosandHosted Weblate cf4b9e938d Translated using Weblate (German)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2026-03-08 23:26:09 +01:00
ugyesandHosted Weblate 31120ad111 Translated using Weblate (Hungarian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2026-03-08 23:26:09 +01:00
Yusuke SakaiandHosted Weblate cd8640f00e Translated using Weblate (Japanese)
Currently translated at 23.1% (269 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ja/
2026-03-08 23:26:09 +01:00
Luis LandeiroandHosted Weblate d0ba455ed6 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pt_BR/
2026-03-08 23:26:09 +01:00
advplyrandGitHub e5b7aea46c Merge pull request #5063 from mandreko/idor-fixes
IDOR fixes
2026-03-08 17:26:03 -05:00
advplyr 2f2d026b06 Auto format 2026-03-08 17:25:52 -05:00
advplyrandGitHub c156b063f5 Merge pull request #5036 from kctdfh/Slightly-better-subtitle-parsing-logic
Improved subtitle parsing to account for bare colon in title
2026-03-08 17:12:55 -05:00
advplyr e6d49a2d53 Fix home page check current user for hide from continue listening 2026-03-04 16:50:36 -06:00
advplyr 6d3404272c Fix Match click to use current label string 2026-02-28 11:32:28 -06:00
meek2100 a9e12657f5 Add autocomplete attributes to login and setup fields for password manager support 2026-02-26 14:29:28 -08:00
Kevin Gatera c0319ebbac Adjust discover/search changes for API compatibility 2026-02-23 19:22:45 -05:00
advplyrandGitHub 1d0b7e383a Merge pull request #5071 from pavel-miniutka/master
Add Belarusian language option & Belarus podcast region
2026-02-22 16:19:19 -06:00
advplyrandGitHub 9f5d8386f3 Merge pull request #5077 from belpe/add-slovak-language
Add Slovak (sk) language to language selector
2026-02-22 16:15:55 -06:00
advplyr 6e0da3bf7a Fix updating author name merging with same name authors in a different library #4628 2026-02-21 16:00:38 -06:00
Peter BelákandClaude Opus 4.6 ee6016f70e Add Slovak (sk) language to the language selector
The Slovak translation file (client/strings/sk.json) already exists
with a complete translation but was missing from the languageCodeMap
in i18n.js, making it inaccessible from the language dropdown.

Also adds Slovakia to the podcast search region map.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 09:11:50 +01:00
Kevin Gatera f1a2e56054 Add database indexes for discover query performance 2026-02-19 20:11:49 -05:00
Kevin Gatera d2915e689f Speed up personalized shelves and reduce search payload size 2026-02-19 20:11:38 -05:00
Kevin Gatera 05d9ab81f9 Improve API cache invalidation for high-churn models 2026-02-19 20:11:24 -05:00
pavel-miniutka 75eed9d09a Add Belarusian language option & Belarus podcast region 2026-02-19 10:36:28 +03:00
Matt Andreko e5af2f336b Move file to correct folder... 2026-02-15 22:06:20 -05:00
Matt Andreko ade1752e97 Fix IDOR bugs 2026-02-15 22:01:36 -05:00
advplyr fa5fa7b788 Fix server crash on /me/progress/:libraryItemId/:episodeId? when episodeId is not passed in for a podcast library item #5058 2026-02-14 17:17:12 -06:00
advplyrandGitHub b01facc034 Merge pull request #5042 from openam/open-api-spec-fixes
Fix OpenAPI spec description
2026-02-08 17:23:18 -06:00
Michael Tuttle dd4fc09909 Fix OpenAPI spec description 2026-02-08 00:19:23 -07:00
Khashayar Toodehfallah c15cb48def Improved subtitle parsing to account for bare colon in title 2026-02-06 14:31:09 -05:00
advplyrandGitHub fe13456a2b Merge pull request #4936 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2026-02-03 17:26:42 -06:00
Lluís FornsandHosted Weblate 2ee893062f Translated using Weblate (Catalan)
Currently translated at 92.2% (1073 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ca/
2026-02-03 20:10:06 +01:00
dapitch666andHosted Weblate 31630f50a5 Translated using Weblate (French)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2026-02-03 20:10:06 +01:00
Delta UmhöferandHosted Weblate edfce46058 Translated using Weblate (German)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2026-02-02 08:02:28 +00:00
Jan-Eric MyhrgrenandHosted Weblate cc5244c596 Translated using Weblate (Swedish)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2026-02-02 08:02:27 +00:00
enoshandHosted Weblate b8942c5931 Translated using Weblate (Hebrew)
Currently translated at 81.4% (947 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/he/
2026-02-02 08:02:26 +00:00
FiendFEARingandHosted Weblate 6e5feee78a Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2026-02-02 08:02:25 +00:00
Dawid KuźnickiandHosted Weblate e7cb0466e6 Translated using Weblate (Polish)
Currently translated at 94.5% (1100 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2026-02-02 08:02:24 +00:00
dv4yGY2UandHosted Weblate 6c7221d37d Translated using Weblate (Turkish)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/tr/
2026-02-02 08:02:24 +00:00
Samuel GuerreroandHosted Weblate 1f3fa80ddd Translated using Weblate (Spanish)
Currently translated at 97.5% (1134 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2026-02-02 08:02:23 +00:00
MantasandHosted Weblate 87f3766299 Translated using Weblate (Lithuanian)
Currently translated at 59.9% (697 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/lt/
2026-02-02 08:02:22 +00:00
BartłomiejandHosted Weblate d08cef11ed Translated using Weblate (Polish)
Currently translated at 94.5% (1100 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2026-02-02 08:02:21 +00:00
Kabika82andHosted Weblate 7201cced42 Translated using Weblate (Hungarian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2026-02-02 08:02:20 +00:00
N VisiandHosted Weblate 4f8fbbc979 Translated using Weblate (Japanese)
Currently translated at 22.8% (266 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ja/
2026-02-02 08:02:19 +00:00
Henrik LyngeandHosted Weblate e55fed4a33 Translated using Weblate (Danish)
Currently translated at 99.9% (1162 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2026-02-02 08:02:19 +00:00
herny ucetandHosted Weblate dcbeecff7a Translated using Weblate (Slovak)
Currently translated at 99.7% (1160 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sk/
2026-02-02 08:02:18 +00:00
xxzp3andHosted Weblate 32276aacd9 Translated using Weblate (Danish)
Currently translated at 99.8% (1161 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2026-02-02 08:02:17 +00:00
J. LavoieandHosted Weblate b921a08809 Translated using Weblate (French)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2026-02-02 08:02:16 +00:00
J. LavoieandHosted Weblate c089336e41 Translated using Weblate (Italian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2026-02-02 08:02:15 +00:00
CharlieandHosted Weblate 5107b0307c Translated using Weblate (French)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2026-02-02 08:02:14 +00:00
advplyrandGitHub 8498378bca Merge pull request #4952 from sir-wilhelm/use-localized-text-for-selected-filter
Display localized/styled text for selected filter.
2026-01-31 17:56:07 -06:00
advplyrandGitHub b61e2c30f2 Merge pull request #4973 from KiwiHour/jump-backwards-label-fix
Fix screen reader compatability issue for the "jump backwards" button for media controls
2026-01-31 17:50:41 -06:00
Nicholas Wallace cfeb6bd502 Fix: grace period enable statement 2026-01-24 18:57:40 -07:00
Nicholas Wallace 077b523bd6 Fix JS Doc deletion 2026-01-24 18:42:50 -07:00
Nicholas Wallace b8a2d113f0 Allow rotation without grace period for invalidating all user sessions 2026-01-24 18:26:11 -07:00
Nicholas Wallace e1ae4f2d31 Fix: race condition in rotation 2026-01-24 18:10:38 -07:00
Nicholas Wallace 7aa2f84daa Revert default token expiry 2026-01-24 17:00:07 -07:00
Nicholas Wallace da0a64daed Add: 10 second grace period to access token cycle 2026-01-24 16:57:25 -07:00
KiwiHourandGitHub 3e4225bced Fix aria-label for jumpBackward button 2026-01-09 14:26:56 +00:00
sir-wilhelm e6d99d07f0 Display localized/styled text for selected filter.
The selected filter was using the id before.
2025-12-28 11:28:36 -06:00
advplyr 122fc34a75 Fix server crash filtering by decade with collapsed series 2025-12-24 17:07:05 -06:00
advplyr e5c0a9d22c Version bump v2.32.1 2025-12-23 16:51:54 -06:00
advplyrandGitHub 3bf136a20b Merge pull request #4933 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-12-23 16:49:04 -06:00
MarcinandHosted Weblate b387d9484a Translated using Weblate (Polish)
Currently translated at 89.2% (1038 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2025-12-23 23:44:39 +01:00
bittin1ddc447d824349b2andHosted Weblate e8668d9f22 Translated using Weblate (Swedish)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-12-23 23:44:38 +01:00
Petri HämäläinenandHosted Weblate f3e90bd420 Translated using Weblate (Finnish)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-12-23 23:44:37 +01:00
Ivan SmoliakovandHosted Weblate 4bf15bbffd Translated using Weblate (Russian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-12-23 23:44:37 +01:00
advplyr 04eb3bc437 Fix server crash on audible match #4931 2025-12-23 16:44:29 -06:00
advplyr 81e96df9c5 Version bump v2.32.0 2025-12-21 15:54:07 -06:00
advplyrandGitHub 44aff23e1b Merge pull request #4921 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-12-21 15:44:28 -06:00
lambolightingandHosted Weblate cc48d9f26d Translated using Weblate (Greek)
Currently translated at 26.9% (313 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/el/
2025-12-21 21:38:43 +00:00
AhetekandHosted Weblate ac08e897ee Translated using Weblate (Polish)
Currently translated at 89.2% (1038 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2025-12-21 21:38:43 +00:00
FiendFEARingandHosted Weblate 3c2eec8279 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-12-21 21:38:42 +00:00
advplyrandGitHub 7b37c98e88 Book tags genres dedupe (#4927)
* Update Audible provider dedupe genres/tags and return tags as array

* Update custom metadata provider to dedupe tags/genres and return tags as array
2025-12-21 15:38:34 -06:00
advplyrandGitHub 088353ae26 Merge pull request #4649 from votex001/multi-select-item-fix
[fix] prevent duplicates in multi-selects
2025-12-21 14:58:04 -06:00
advplyrandGitHub e003544edd Merge pull request #4766 from TN-SKYC/Authors-bug
Bug in matching author of a book when this author already exists in the db.
2025-12-21 14:49:37 -06:00
advplyr 076ece6fe7 Auto-formatting 2025-12-21 14:45:04 -06:00
advplyrandGitHub 14f72ab7d4 Merge pull request #4740 from Yetangitu/fix_debian_user_exists
fix #1617 (useradd: user 'audiobookshelf' already exists)
2025-12-21 14:08:10 -06:00
advplyrandGitHub ebcb122eb8 Merge pull request #4906 from sir-wilhelm/playlist-sorted
When adding items to playlist, sort the playlist sections alphabetically.
2025-12-21 14:01:08 -06:00
advplyrandGitHub 626596b192 Merge pull request #4890 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-12-14 16:35:16 -06:00
lambolightingandHosted Weblate 10a4777ddf Translated using Weblate (Greek)
Currently translated at 9.0% (105 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/el/
2025-12-14 23:00:22 +01:00
MODI NAVONandHosted Weblate 0ecbb1c3f4 Translated using Weblate (Hebrew)
Currently translated at 73.0% (850 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/he/
2025-12-12 23:24:22 +00:00
Lucas JaksysandHosted Weblate dc2398a072 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pt_BR/
2025-12-12 23:24:21 +00:00
lambolightingandHosted Weblate c1e21d31ee Translated using Weblate (Greek)
Currently translated at 1.1% (13 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/el/
2025-12-12 23:24:21 +00:00
MarioandHosted Weblate 70e6efc3d0 Translated using Weblate (German)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-12-12 23:24:20 +00:00
Alberto CoronadoandHosted Weblate 092c504eb1 Translated using Weblate (Spanish)
Currently translated at 97.5% (1134 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2025-12-12 23:24:19 +00:00
A LandHosted Weblate f7d7c9a4f5 Translated using Weblate (Bulgarian)
Currently translated at 88.2% (1026 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/bg/
2025-12-12 23:24:19 +00:00
kfctatertotandHosted Weblate 8bdcabf973 Translated using Weblate (Spanish)
Currently translated at 96.8% (1126 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2025-12-12 23:24:18 +00:00
kfctatertotandHosted Weblate 646c861bcc Translated using Weblate (Arabic)
Currently translated at 95.9% (1116 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ar/
2025-12-12 23:24:17 +00:00
zard KimandHosted Weblate ee60169995 Translated using Weblate (Korean)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ko/
2025-12-12 23:24:17 +00:00
advplyrandHosted Weblate afb4108c30 Added translation using Weblate (Greek) 2025-12-12 23:24:16 +00:00
Igor DobračaandHosted Weblate 2e2d857ce0 Translated using Weblate (Croatian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2025-12-12 23:24:15 +00:00
Alessandro BurzioandHosted Weblate ed5766b4ab Translated using Weblate (Italian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2025-12-12 23:24:14 +00:00
Petri HämäläinenandHosted Weblate a33e87db99 Translated using Weblate (Finnish)
Currently translated at 97.6% (1136 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-12-12 23:24:14 +00:00
NapitaukiandHosted Weblate 5de942aefb Translated using Weblate (Finnish)
Currently translated at 97.6% (1136 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-12-12 23:24:13 +00:00
thehijackerandHosted Weblate bcfe1e9647 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2025-12-12 23:24:12 +00:00
advplyr 503f4611b2 Update tooltip with plaintext prop 2025-12-12 17:24:01 -06:00
sir-wilhelm 648983708e Sort the playlist sections alphabetically. 2025-12-12 04:18:29 +00:00
advplyr 991d25f628 Version bump v2.31.0 2025-12-01 17:13:15 -06:00
advplyrandGitHub d2a7c3c381 Merge pull request #4879 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-12-01 17:09:02 -06:00
NetleakandHosted Weblate 219a9fc6d5 Translated using Weblate (Czech)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2025-12-01 16:59:30 +00:00
advplyrandGitHub ba2259d174 Merge pull request #4881 from mikiher/wrap-ensure-dir
Wrap ensureDir in try-catch blocks in manager classes
2025-12-01 10:59:21 -06:00
mikiher d7bfccdc4a BackupManager: Remove backup fallback logic 2025-12-01 18:54:43 +02:00
mikiher 5f1edcb609 Wrap ensureDir in try-catch blocks 2025-12-01 18:00:34 +02:00
advplyr 329e3c7179 Add Korean language option 2025-11-29 16:32:57 -06:00
advplyrandGitHub 919ea32416 Merge pull request #4848 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-11-29 16:27:45 -06:00
J. LavoieandHosted Weblate 3b6419bc1b Translated using Weblate (French)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2025-11-29 23:26:56 +01:00
J. LavoieandHosted Weblate d4fdb47c7f Translated using Weblate (German)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-11-29 23:26:55 +01:00
Petri HämäläinenandHosted Weblate cee9b9d8e3 Translated using Weblate (Finnish)
Currently translated at 97.5% (1134 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-11-29 23:26:54 +01:00
advplyrandHosted Weblate 9441346b0a Added translation using Weblate (Icelandic) 2025-11-29 23:26:53 +01:00
Ivan SmoliakovandHosted Weblate 6b8464c270 Translated using Weblate (Russian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-11-29 23:26:52 +01:00
ArturandHosted Weblate d12f727603 Translated using Weblate (Polish)
Currently translated at 89.2% (1038 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2025-11-29 23:26:51 +01:00
Renaldas RepečkaandHosted Weblate 1552c250df Translated using Weblate (Lithuanian)
Currently translated at 59.8% (696 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/lt/
2025-11-29 23:26:50 +01:00
zard KimandHosted Weblate 623c2fba12 Translated using Weblate (Korean)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ko/
2025-11-29 23:26:50 +01:00
PlazecandHosted Weblate be27908d44 Translated using Weblate (Czech)
Currently translated at 99.5% (1158 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2025-11-29 23:26:48 +01:00
Grzegorz OrlowskiandHosted Weblate 7a39d581a1 Translated using Weblate (Polish)
Currently translated at 88.9% (1035 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2025-11-29 23:26:47 +01:00
ArturandHosted Weblate 53a416fd28 Translated using Weblate (Polish)
Currently translated at 88.9% (1035 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2025-11-29 23:26:47 +01:00
B0raxandHosted Weblate 7393c03218 Translated using Weblate (German)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-11-29 23:26:46 +01:00
ugyesandHosted Weblate 594589da3d Translated using Weblate (Hungarian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2025-11-29 23:26:45 +01:00
FiendFEARingandHosted Weblate 44d7deae99 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-11-29 23:26:44 +01:00
Jan-Eric MyhrgrenandHosted Weblate ff9e87c4d5 Translated using Weblate (Swedish)
Currently translated at 99.9% (1162 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-11-29 23:26:43 +01:00
Cèlia GarrigaandHosted Weblate c2fd87d55c Translated using Weblate (Catalan)
Currently translated at 91.8% (1068 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ca/
2025-11-29 23:26:42 +01:00
Максим ГорпинічandHosted Weblate 27843c3f9b Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-11-29 23:26:41 +01:00
Vito0912andHosted Weblate 0ec2ced011 Translated using Weblate (German)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-11-29 23:26:40 +01:00
ugyesandHosted Weblate 552ed43243 Translated using Weblate (Hungarian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2025-11-29 23:26:39 +01:00
advplyrandGitHub 0606738b38 Merge pull request #4873 from Vito0912/feat/fixUploadLookup
Fix Upload Lookup
2025-11-29 16:26:26 -06:00
Finn Dittmar a5d2c1bd64 Fix Upload Lookup 2025-11-29 11:02:42 +01:00
advplyrandGitHub d8e272e091 Merge pull request #4870 from Vito0912/patch-1
Fix region parameter extraction in findChapters
2025-11-27 16:20:34 -06:00
Finn DittmarandGitHub 3e9ca51088 Fix region parameter extraction in findChapters 2025-11-27 21:40:29 +01:00
advplyrandGitHub 8758c62ae2 Merge pull request #4702 from Vito0912/feat/uploadProgress
feat: Added progress indicator to upload
2025-11-24 17:08:03 -06:00
advplyrandGitHub db9019a94f Merge pull request #4770 from jamerst/days-in-a-row-today-optional
Don't require listening today for 'Days in a row' stat
2025-11-20 16:59:17 -06:00
advplyr 39b8b9df4f Auto formatting 2025-11-20 16:58:43 -06:00
advplyrandGitHub a36f097095 Merge pull request #4768 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-11-19 08:06:42 -06:00
advplyrandHosted Weblate ae0ccb1b47 Added translation using Weblate (Korean) 2025-11-19 15:03:59 +01:00
Jan-Eric MyhrgrenandHosted Weblate f178841e57 Translated using Weblate (Swedish)
Currently translated at 99.8% (1161 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-11-19 05:41:52 +01:00
Mikael EngströmandHosted Weblate 568b154e8a Translated using Weblate (Swedish)
Currently translated at 99.8% (1161 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-11-17 21:51:27 +01:00
dapitch666andHosted Weblate 8c5678b573 Translated using Weblate (French)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2025-11-17 21:51:19 +01:00
Jan SchoenfeldandHosted Weblate e51c7b2be1 Translated using Weblate (German)
Currently translated at 99.9% (1162 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-11-17 21:51:17 +01:00
Milo IvirandHosted Weblate d460757df4 Translated using Weblate (Croatian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2025-11-14 23:51:49 +00:00
thehijackerandHosted Weblate cd295c03ca Translated using Weblate (Slovenian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2025-11-12 10:53:50 +00:00
FiendFEARingandHosted Weblate 38dd1beff7 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-11-12 10:53:37 +00:00
Максим ГорпинічandHosted Weblate 61b72aff9d Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-11-12 10:53:35 +00:00
Lucas JaksysandHosted Weblate 9eda4e36fa Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pt_BR/
2025-11-12 10:53:14 +00:00
biuklijaandHosted Weblate a05cb170a2 Translated using Weblate (Croatian)
Currently translated at 99.9% (1162 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2025-11-12 10:52:18 +00:00
Dinu BălanandHosted Weblate c75d976320 Translated using Weblate (Romanian)
Currently translated at 17.3% (202 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ro/
2025-11-10 23:37:16 +01:00
Petri HämäläinenandHosted Weblate ab9b798bfa Translated using Weblate (Finnish)
Currently translated at 97.2% (1131 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-11-10 23:37:16 +01:00
CharlieandHosted Weblate 40783c8644 Translated using Weblate (French)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2025-11-10 23:37:16 +01:00
phewiandHosted Weblate a0137fcb42 Translated using Weblate (Finnish)
Currently translated at 95.5% (1111 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-11-10 23:37:16 +01:00
Lucas Gonçalves JaksysandHosted Weblate 9fdda6be62 Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pt_BR/
2025-11-10 23:37:16 +01:00
Lucas Gonçalves JaksysandHosted Weblate b0c073dd7e Translated using Weblate (Hungarian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2025-11-10 23:37:16 +01:00
Sami TauseefandHosted Weblate b83e2836f6 Translated using Weblate (Bengali)
Currently translated at 92.6% (1078 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/bn/
2025-11-10 23:37:16 +01:00
Lucas Gonçalves JaksysandHosted Weblate f91be18527 Translated using Weblate (Portuguese (Brazil))
Currently translated at 93.1% (1083 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pt_BR/
2025-11-10 23:37:16 +01:00
dapitch666andHosted Weblate c1f4e4120e Translated using Weblate (French)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2025-11-10 23:37:16 +01:00
Petri HämäläinenandHosted Weblate b42c7421b0 Translated using Weblate (Finnish)
Currently translated at 95.5% (1111 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-11-10 23:37:16 +01:00
pryszczoskorandHosted Weblate 82f512d405 Translated using Weblate (Polish)
Currently translated at 88.3% (1027 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2025-11-10 23:37:16 +01:00
Aleksandr ZakirovandHosted Weblate 01a833ea59 Translated using Weblate (Estonian)
Currently translated at 64.2% (747 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/et/
2025-11-10 23:37:16 +01:00
advplyrandGitHub f1c39e8587 Merge pull request #4815 from mikiher/fix-4095
Update wording for library watcher settings and scan note
2025-11-10 16:37:10 -06:00
mikiher 5e68936c20 Update English strings for library watcher settings and scan notes #4095 2025-11-10 09:14:46 +02:00
advplyrandGitHub a4c9b062c1 Merge pull request #4808 from Vito0912/feat/eac3
Support eac3 and ac3 in transcodes
2025-11-08 17:13:06 -06:00
advplyr 763d8810e3 Update Stream ac3/eac3 check 2025-11-08 17:08:43 -06:00
Finn Dittmar 3316505d1c Really makes sure nothing can break 2025-11-07 19:12:38 +01:00
Finn Dittmar 2cf6e8a5fe Support eac3 2025-11-07 19:02:32 +01:00
Tomasz N. 961d066bdd Wrong branch. 2025-10-31 15:39:12 +01:00
Tomasz N. 372c9a5322 Increasing the timeout for bookfinder - some metadata providers heavily throttle the requests, original 10s is not enough. 2025-10-31 15:36:47 +01:00
James Tattersall f77de1743e Don't require listening today for 'Days in a row' stat 2025-10-23 21:31:37 +01:00
Tomasz N. a5750deaaf The key change: Move the Database.bookAuthorModel.create() block outside the if (!author) check,
so it runs whether the author was just created OR already existed in the database.

This bug was visible when using "Match Books" for a library and the outcome was books had no author(s) assigned
despite the custom providers correctly providing those values.
2025-10-22 23:02:49 +02:00
advplyrandGitHub 0c7b738b7c Merge pull request #4730 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-10-21 17:26:49 -05:00
burghy86andHosted Weblate c3c9e7731d Translated using Weblate (Italian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2025-10-22 00:24:25 +02:00
Petri HämäläinenandHosted Weblate d3b5612fc0 Translated using Weblate (Finnish)
Currently translated at 95.3% (1109 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-10-22 00:24:24 +02:00
BlubberlandandHosted Weblate 96ef0129ed Translated using Weblate (German)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-10-22 00:24:23 +02:00
nlqogandHosted Weblate 85546b7dd7 Translated using Weblate (Portuguese (Brazil))
Currently translated at 76.6% (892 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pt_BR/
2025-10-22 00:24:22 +02:00
pmangroandHosted Weblate d59714d804 Translated using Weblate (Portuguese (Brazil))
Currently translated at 74.2% (863 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pt_BR/
2025-10-22 00:24:22 +02:00
CoxeandHosted Weblate 96693659bf Translated using Weblate (Danish)
Currently translated at 96.9% (1127 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2025-10-22 00:24:21 +02:00
pryszczoskorandHosted Weblate ee2d8d1f71 Translated using Weblate (Polish)
Currently translated at 88.2% (1026 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2025-10-22 00:24:20 +02:00
HezhaandHosted Weblate f03b0915eb Translated using Weblate (Arabic)
Currently translated at 95.9% (1116 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ar/
2025-10-22 00:24:20 +02:00
advplyrandGitHub a92ba564bd Merge pull request #4750 from mikiher/providers-api
Add metadata providers API and use them on web client
2025-10-21 17:24:11 -05:00
advplyr e684a8dc43 Update JSDocs & auto-formatting of PodcastFinder 2025-10-21 17:22:10 -05:00
mikiher 6db6b862e6 Upgrade codeql-actions to v3 2025-10-21 17:33:53 +03:00
mikiher 57c7b123f0 Fix codeQL error: Return json error object 2025-10-21 11:00:29 +03:00
mikiher fd593caafc SearchController: simplify query param validation logic 2025-10-21 09:38:35 +03:00
advplyrandGitHub d0a3f74710 Merge pull request #4756 from Vito0912/tokenExpiry
Change token expiry
2025-10-20 17:19:54 -05:00
advplyr b1921e7034 Fix podcast failed to parse rss feed when feed is not using CData in content:encoded #4757 2025-10-19 17:25:18 -05:00
mikiher 538a5065a4 Update providers users to fetch providers on demand 2025-10-19 18:57:27 +03:00
mikiher 166e0442a0 Remove providers prefetch, refresh on custom provider add/remove 2025-10-19 11:47:17 +03:00
mikiher 816a47a4ba Remove custom providers from library fetch action 2025-10-19 11:40:40 +03:00
mikiher 141211590f Merge provider actions and mutations, add loaded state 2025-10-19 11:39:10 +03:00
mikiher b01e7570d3 Remove custom providers from library filterdata request 2025-10-19 10:54:26 +03:00
mikiher 0a8662d198 Merge providers API into a single endpoint 2025-10-19 10:53:27 +03:00
Finn Dittmar 0a4de61eff Chnage Auth Expiry 2025-10-19 09:22:12 +02:00
mikiher 0a82d6a41b CoverSearchManager: Fix broken podcast cover search 2025-10-17 08:11:03 +03:00
mikiher 3f6162f53c CodeQL fix: limit parameter sizes 2025-10-15 18:54:29 +03:00
mikiher 888190a6be Fix codeQL failures 2025-10-15 18:28:15 +03:00
mikiher ce4ff4f894 Client: Use new server providers API 2025-10-15 09:52:15 +03:00
mikiher 1da3ab7fdc ApiRouter: New provider API routes 2025-10-14 18:10:12 +03:00
mikiher 4f30cbf2f6 SearchController: New providers API, query param validation 2025-10-14 18:09:32 +03:00
advplyr a87ea32715 Fix admin user unable to close sessions of other users #4746 2025-10-13 09:50:01 -05:00
advplyr feed827223 Update settings update endpoint to validate allowedOrigins is array 2025-10-10 18:00:37 -05:00
Frank de Lange 797dba2448 fix #1617 (useradd: user 'audiobookshelf' already exists)
This change fixes the problem of failing upgrades on dpkg-based systems
by reworking the check for whether the `audiobookshelf` user/group already
exists.
2025-10-10 22:30:38 +02:00
advplyrandGitHub f0acbb2e81 Merge pull request #4737 from Yetangitu/explicit_oidc_autolaunch
Explicitly launch OpenID Connect authentication with ?autoLaunch=1
2025-10-09 17:08:51 -05:00
Frank de Lange fc06aa2c78 Explicitly launch OpenID Connect authentication with ?autoLaunch=1
This change extends OIDC authentication by enabling explicit redirection
to the OAuth provider when navigating to the login page with the manual
override parameter (/login?autoLaunch=1).

Use case: directly launch audiobookshelf from within e.g. Nextcloud using the
external sites app (use something like https://abs.example.org/login?autoLaunch=1
as URL) while keeping the possibility to launch audiobookshelf using its built-in
authentication mechanism. Assuming the username or mail address used in Nextcloud
and audiobookshelf are identical the user will be logged in to his or her account
no matter which method is used.
2025-10-09 16:02:02 +02:00
Finn Dittmar 4224f44259 Remove duplicate (and also wrong byte conversion) 2025-10-09 08:49:13 +02:00
advplyr 2592467d09 Fix: Always re-load libraries when changing users #4694 2025-10-08 15:32:37 -05:00
advplyr 37beb7b37c Disable chapter editor chapter play button when time is invalid #4691 2025-10-08 15:03:33 -05:00
advplyr cafd92e206 Fix item edit modal show next/prev arrows when opening from Files or Match context menu item #4718 2025-10-08 14:52:14 -05:00
advplyr 3e876e3383 Add Turkish to the language option 2025-10-08 14:24:50 -05:00
advplyr 29752798f3 Version bump v2.30.0 2025-10-08 10:34:34 -05:00
advplyrandGitHub 8c86ca4ea5 Merge pull request #4729 from mikiher/build-win-no-compress
Add a script to build an uncompressed windows executable
2025-10-08 10:08:21 -05:00
mikiher 00c62fa494 Add a script to build an uncompressed windows executable 2025-10-08 17:42:00 +03:00
advplyrandGitHub 6c7f3c7e77 Merge pull request #4695 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-10-07 16:25:14 -05:00
Jan-Eric MyhrgrenandHosted Weblate aec8acbdd7 Translated using Weblate (Swedish)
Currently translated at 97.2% (1131 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-10-07 12:02:09 +02:00
Юра КлимовичandHosted Weblate 6e19ad7777 Translated using Weblate (Russian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-10-07 12:02:08 +02:00
Grzegorz OrlowskiandHosted Weblate 3aa95fec11 Translated using Weblate (Polish)
Currently translated at 88.2% (1026 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2025-10-07 12:02:05 +02:00
AhetekandHosted Weblate 37dd46d31f Translated using Weblate (Polish)
Currently translated at 88.2% (1026 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2025-10-07 12:02:03 +02:00
Petter Schaug-PettersenandHosted Weblate 54a996634e Translated using Weblate (Norwegian Bokmål)
Currently translated at 90.4% (1052 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nb_NO/
2025-10-07 12:02:02 +02:00
Oğuz ErsenandHosted Weblate 54a5e368c2 Translated using Weblate (Turkish)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/tr/
2025-10-05 17:02:05 +02:00
FiendFEARingandHosted Weblate 2d313851d2 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-10-05 17:02:04 +02:00
Максим ГорпинічandHosted Weblate eb00b19457 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-10-05 17:02:03 +02:00
Kabika82andHosted Weblate bbae9acc2d Translated using Weblate (Hungarian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2025-10-05 17:02:02 +02:00
Milo IvirandHosted Weblate a4e8f01f0e Translated using Weblate (Croatian)
Currently translated at 100.0% (1163 of 1163 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2025-10-05 17:02:01 +02:00
SmileFateandHosted Weblate 6bdf402da8 Translated using Weblate (Turkish)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/tr/
2025-10-03 21:43:22 +00:00
DRandHosted Weblate 80b0e3546e Translated using Weblate (Hebrew)
Currently translated at 72.9% (847 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/he/
2025-10-03 21:43:21 +00:00
B0raxandHosted Weblate 161f3cb177 Translated using Weblate (German)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-10-03 21:43:21 +00:00
JanandHosted Weblate 4a4d4a8f17 Translated using Weblate (German)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-10-03 21:43:20 +00:00
Petri HämäläinenandHosted Weblate b21046027c Translated using Weblate (Finnish)
Currently translated at 95.5% (1109 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fi/
2025-10-03 21:43:19 +00:00
max grakovandHosted Weblate 3a163e1746 Translated using Weblate (Russian)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-10-03 21:43:18 +00:00
Amirhossein GhorbanmehrandHosted Weblate 3c4e80f1c1 Translated using Weblate (Persian)
Currently translated at 2.2% (26 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fa/
2025-10-03 21:43:18 +00:00
SmileFateandHosted Weblate 2f3036faba Translated using Weblate (Turkish)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/tr/
2025-10-03 21:43:17 +00:00
advplyrandHosted Weblate 3934461c46 Added translation using Weblate (Persian) 2025-10-03 21:43:16 +00:00
advplyrandGitHub 123351e08a Merge pull request #4716 from mikiher/async-cover-search
Async Cover Search
2025-10-03 16:43:05 -05:00
advplyr 1280ddfe74 ui/ux disable inputs while cover search in progress, add padding on empty state texts 2025-10-03 16:39:36 -05:00
mikiher 7e89b97a6d Tidy up cover search console logging and error toasts 2025-10-03 09:08:17 +03:00
mikiher 20de2ea388 Add "Best" option to book cover search 2025-10-03 08:23:53 +03:00
mikiher dbb5ee79ac Revert removal of audiobookcovers provider 2025-10-03 08:20:56 +03:00
mikiher c6dabd2620 Shorten timeout and error message for remaining providers 2025-10-02 22:23:12 +03:00
mikiher 26f949b9ba Remove audiobookcovers from provider list 2025-10-02 22:14:48 +03:00
mikiher 7630dbdcb7 Replace cover search with streaming version 2025-10-02 13:30:03 +03:00
mikiher a164c17d38 Reduce provider timout to 10 secs, Shorten error message 2025-10-02 13:26:05 +03:00
advplyr 03da194953 Update for nextjs client, pass all remaining requests through to nextjs 2025-09-28 09:41:15 -05:00
Vito0912 9ce6de3100 Added progress to upload 2025-09-27 17:00:57 +02:00
advplyrandGitHub e040396b20 Merge pull request #4656 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-09-22 09:41:08 -05:00
Phil JopeandHosted Weblate bcbec67fec Translated using Weblate (German)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-09-20 23:02:02 +02:00
Vito0912andHosted Weblate 1543021685 Translated using Weblate (German)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-09-20 23:02:01 +02:00
SalmanegrandHosted Weblate 577e6aaec9 Translated using Weblate (Arabic)
Currently translated at 95.4% (1108 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ar/
2025-09-19 08:02:02 +00:00
Milo IvirandHosted Weblate 77579acfd4 Translated using Weblate (Croatian)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2025-09-10 22:24:06 +00:00
Yuta ImadaandHosted Weblate 9ca98ca750 Translated using Weblate (Japanese)
Currently translated at 15.5% (180 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ja/
2025-09-10 22:24:05 +00:00
SatanowskiandHosted Weblate feb225d3a6 Translated using Weblate (Polish)
Currently translated at 82.3% (956 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2025-09-10 22:24:05 +00:00
Yuta ImadaandHosted Weblate e501aa4f1e Translated using Weblate (Japanese)
Currently translated at 15.4% (179 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ja/
2025-09-10 22:24:04 +00:00
peter cernyandHosted Weblate 104f6e6c58 Translated using Weblate (Slovak)
Currently translated at 99.9% (1160 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sk/
2025-09-10 22:24:03 +00:00
jhonthanandHosted Weblate 552d8ae3b8 Translated using Weblate (Portuguese (Brazil))
Currently translated at 67.7% (787 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pt_BR/
2025-09-10 22:24:03 +00:00
advplyrandGitHub a41e9bae5d Merge pull request #4664 from advplyr/episode_download_fallback
Fix issue with episode downloads without streams
2025-09-10 17:23:53 -05:00
advplyr a456865ec0 Fix issue with episode downloads without streams, fallback to regular dl on ffprobe fail 2025-09-10 17:10:00 -05:00
advplyr 85d5531bc1 Update chapter editor remove redirect on save or delete all #4650 2025-09-07 17:50:59 -05:00
advplyrandGitHub 4b840f9c97 Merge pull request #4627 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-09-05 17:24:15 -05:00
peter cernyandHosted Weblate b9510a69fe Translated using Weblate (Slovak)
Currently translated at 97.7% (1135 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sk/
2025-09-06 00:22:25 +02:00
LosicekandHosted Weblate d737a66af2 Translated using Weblate (Czech)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2025-09-06 00:22:24 +02:00
PlazecandHosted Weblate 576d18d8d6 Translated using Weblate (Czech)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2025-09-06 00:22:23 +02:00
petr-prikrylandHosted Weblate d238b02bd2 Translated using Weblate (Czech)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2025-09-06 00:22:23 +02:00
kuci-JKandHosted Weblate c6cb13ed39 Translated using Weblate (Czech)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2025-09-06 00:22:22 +02:00
Jon Erling HustadnesandHosted Weblate 44c5dce8aa Translated using Weblate (Norwegian Bokmål)
Currently translated at 90.5% (1051 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nb_NO/
2025-09-06 00:22:21 +02:00
Darius MandHosted Weblate b726bee4e5 Translated using Weblate (Romanian)
Currently translated at 17.0% (198 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ro/
2025-09-06 00:22:20 +02:00
HnatiucbandHosted Weblate b07e449043 Translated using Weblate (Romanian)
Currently translated at 17.0% (198 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ro/
2025-09-06 00:22:19 +02:00
icutehunterandHosted Weblate 9273e61f1e Translated using Weblate (Turkish)
Currently translated at 28.5% (332 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/tr/
2025-09-06 00:22:18 +02:00
ugyesandHosted Weblate 1b4a7acf13 Translated using Weblate (Hungarian)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2025-09-06 00:22:18 +02:00
kuci-JKandHosted Weblate 68c1395bdf Translated using Weblate (Czech)
Currently translated at 98.1% (1140 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2025-09-06 00:22:17 +02:00
HnatiucbandHosted Weblate a007a9ec98 Translated using Weblate (Romanian)
Currently translated at 3.7% (44 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ro/
2025-09-06 00:22:16 +02:00
tngchandHosted Weblate 8b33b5e383 Translated using Weblate (Japanese)
Currently translated at 14.2% (166 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ja/
2025-09-06 00:22:15 +02:00
Jan-Eric MyhrgrenandHosted Weblate c81b762d52 Translated using Weblate (Swedish)
Currently translated at 97.2% (1129 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-09-06 00:22:15 +02:00
HnatiucbandHosted Weblate c53a5c5a0b Translated using Weblate (Romanian)
Currently translated at 3.3% (39 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ro/
2025-09-06 00:22:14 +02:00
thehijackerandHosted Weblate 83af75a582 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2025-09-06 00:22:13 +02:00
Jan-Eric MyhrgrenandHosted Weblate 60389a3bf3 Translated using Weblate (Swedish)
Currently translated at 97.2% (1129 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-09-06 00:22:13 +02:00
biuklijaandHosted Weblate 20cceb3a8f Translated using Weblate (Croatian)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2025-09-06 00:22:11 +02:00
lolly76andHosted Weblate 7562fb2c21 Translated using Weblate (French)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2025-09-06 00:22:11 +02:00
Matej KrajčovičandHosted Weblate c7647aafd7 Translated using Weblate (Slovak)
Currently translated at 96.2% (1117 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sk/
2025-09-06 00:22:10 +02:00
FiendFEARingandHosted Weblate 4a73247e5c Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-09-06 00:22:09 +02:00
Максим ГорпинічandHosted Weblate 326086c197 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-09-06 00:22:08 +02:00
Jan-Eric MyhrgrenandHosted Weblate 5ff5245476 Translated using Weblate (Swedish)
Currently translated at 97.2% (1129 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-09-06 00:22:08 +02:00
advplyr 856cf180a5 Fix chapter editor overflow, set custom wrap breakpoint #4652 2025-09-05 17:21:55 -05:00
votex001 fbe9971a8b [fix] prevent duplicates in multi-selects 2025-09-03 18:19:52 +03:00
advplyrandGitHub 6ea70608a1 Merge pull request #4636 from nichwall/multiselect_duplicate_entries
Fix: MultiSelect causes web client to become unresponsive if duplicate keys exist
2025-09-02 18:37:07 -04:00
advplyr ba7160c305 Add index to removeItem on multiselect keydown 2025-09-02 17:31:48 -05:00
advplyrandGitHub 7d048b7a50 Merge pull request #4635 from Vito0912/feat/OIDCfix
Fix Invalid callback URL - must be same-origin for NPM users
2025-09-02 18:18:52 -04:00
Nicholas Wallace afab429c75 Fix: ensure all keys are unique in MultiSelect 2025-08-30 10:01:25 -07:00
Vito0912 50e2fe7fd2 Fix http/https error 2025-08-30 17:47:21 +02:00
advplyr c7c21cc137 Version bump v2.29.0 2025-08-25 17:10:29 -05:00
advplyrandGitHub 7e4c7a7e3b Merge pull request #4618 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-08-25 17:50:57 -04:00
FiendFEARingandHosted Weblate 40babc9650 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-08-25 21:50:34 +00:00
Максим ГорпинічandHosted Weblate 7a94f014ea Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-08-25 21:50:33 +00:00
Jan-Eric MyhrgrenandHosted Weblate 32adb1bafd Translated using Weblate (Swedish)
Currently translated at 97.5% (1132 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-08-25 21:50:32 +00:00
Yurt PageandHosted Weblate f9a6239049 Translated using Weblate (Russian)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-08-25 21:50:32 +00:00
Mathias FrancoandHosted Weblate 8dee1ec942 Translated using Weblate (Dutch)
Currently translated at 100.0% (1161 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nl/
2025-08-25 21:50:31 +00:00
laxandreaandHosted Weblate 58e43cc6a7 Translated using Weblate (Italian)
Currently translated at 99.6% (1157 of 1161 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2025-08-25 21:50:30 +00:00
Jan-Eric MyhrgrenandHosted Weblate b8999fbc37 Translated using Weblate (Swedish)
Currently translated at 97.3% (1126 of 1157 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-08-25 21:50:29 +00:00
advplyrandHosted Weblate 0dda4b6b27 Added translation using Weblate (Basque) 2025-08-25 21:50:29 +00:00
Kabika82andHosted Weblate 817f2f6915 Translated using Weblate (Hungarian)
Currently translated at 100.0% (1157 of 1157 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2025-08-25 21:50:28 +00:00
idojius86andHosted Weblate 77fc6bba1a Translated using Weblate (Spanish)
Currently translated at 97.1% (1124 of 1157 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/es/
2025-08-25 21:50:28 +00:00
Jan-Eric MyhrgrenandHosted Weblate c66d652a53 Translated using Weblate (Swedish)
Currently translated at 97.3% (1126 of 1157 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-08-25 21:50:27 +00:00
thehijackerandHosted Weblate 86bddba5c3 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1157 of 1157 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2025-08-25 21:50:26 +00:00
Jan-Eric MyhrgrenandHosted Weblate 7779fd2972 Translated using Weblate (Swedish)
Currently translated at 96.8% (1121 of 1157 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-08-25 21:50:26 +00:00
Yurt PageandHosted Weblate 05a4577792 Translated using Weblate (Russian)
Currently translated at 100.0% (1157 of 1157 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-08-25 21:50:25 +00:00
laxandreaandHosted Weblate 56dc042282 Translated using Weblate (Italian)
Currently translated at 99.3% (1149 of 1157 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2025-08-25 21:50:24 +00:00
advplyr 95973243a6 Update book library progress sort labels as per discord vote 2025-08-25 16:50:10 -05:00
18ad23d016 Issue 4540 New SortBy Options: Started Date & Finished Date (#4575)
---------

Co-authored-by: advplyr <advplyr@protonmail.com>
2025-08-24 16:54:38 -05:00
advplyr e258f122f1 Update weblate readme widget to use vertical bar chart 2025-08-24 08:41:31 -05:00
advplyrandGitHub 18200a8f01 Merge pull request #4533 from sir-wilhelm/wmic_replacement
Use PowerShell to get windows drive paths.
2025-08-23 17:55:50 -04:00
advplyr 9c47f404c9 Fix current author not showing in podcast match #4617 2025-08-23 16:55:32 -05:00
advplyr 2f6de71a3a Fix match tab local storage book provider being updated by podcast matching #4615 2025-08-22 08:35:11 -05:00
advplyr deb121c523 Fix podcast itunesId not set on create or update from match #4614 2025-08-22 08:20:49 -05:00
advplyrandGitHub 320e4dfb47 Merge pull request #4586 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-08-20 18:47:09 -04:00
FiendFEARingandHosted Weblate 6194c48549 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1157 of 1157 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-08-20 22:45:34 +00:00
Максим ГорпинічandHosted Weblate 6aa9ecaaba Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1157 of 1157 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-08-20 22:45:33 +00:00
Jan-Eric MyhrgrenandHosted Weblate b3d020b89f Translated using Weblate (Swedish)
Currently translated at 96.8% (1121 of 1157 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-08-20 22:45:33 +00:00
owlcollectorandHosted Weblate e196a6e5ca Translated using Weblate (Japanese)
Currently translated at 9.7% (113 of 1156 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ja/
2025-08-20 22:45:32 +00:00
Jan-Eric MyhrgrenandHosted Weblate 73cf22b499 Translated using Weblate (Swedish)
Currently translated at 96.7% (1118 of 1156 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-08-20 22:45:32 +00:00
ugyesandHosted Weblate ac7464ce7e Translated using Weblate (Hungarian)
Currently translated at 99.8% (1154 of 1156 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2025-08-20 22:45:31 +00:00
B0raxandHosted Weblate 84e742f2a5 Translated using Weblate (German)
Currently translated at 100.0% (1156 of 1156 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-08-20 22:45:30 +00:00
Daniel SchosserandHosted Weblate a1e882cbf1 Translated using Weblate (German)
Currently translated at 100.0% (1156 of 1156 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-08-20 22:45:29 +00:00
Zhelyan RadoevandHosted Weblate 09121acbd5 Translated using Weblate (Bulgarian)
Currently translated at 87.3% (1010 of 1156 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/bg/
2025-08-20 22:45:28 +00:00
owlcollectorandHosted Weblate 5b9df84ba3 Translated using Weblate (Japanese)
Currently translated at 8.5% (99 of 1156 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ja/
2025-08-20 22:45:28 +00:00
Zhelyan RadoevandHosted Weblate 266db491aa Translated using Weblate (Bulgarian)
Currently translated at 83.9% (971 of 1156 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/bg/
2025-08-20 22:45:27 +00:00
FiendFEARingandHosted Weblate c7a317a87b Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1156 of 1156 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-08-20 22:45:26 +00:00
Максим ГорпинічandHosted Weblate b027f3bda1 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1156 of 1156 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-08-20 22:45:26 +00:00
vanapro1andHosted Weblate cea991b82f Translated using Weblate (Russian)
Currently translated at 100.0% (1156 of 1156 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-08-20 22:45:25 +00:00
biuklijaandHosted Weblate 7e2b51e6d2 Translated using Weblate (Croatian)
Currently translated at 100.0% (1156 of 1156 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2025-08-20 22:45:24 +00:00
Hang PhamandHosted Weblate 8f310b6bf0 Translated using Weblate (Vietnamese)
Currently translated at 62.2% (708 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/vi/
2025-08-20 22:45:24 +00:00
SneakyandHosted Weblate b2a5fb46f1 Translated using Weblate (Swedish)
Currently translated at 96.6% (1100 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-08-20 22:45:23 +00:00
Jan-Eric MyhrgrenandHosted Weblate 6d7639853b Translated using Weblate (Swedish)
Currently translated at 96.6% (1100 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-08-20 22:45:22 +00:00
ugyesandHosted Weblate 3a16acbba4 Translated using Weblate (Hungarian)
Currently translated at 99.8% (1136 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2025-08-20 22:45:22 +00:00
Troj@andHosted Weblate 027e1efaca Translated using Weblate (Belarusian)
Currently translated at 66.5% (757 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-08-20 22:45:21 +00:00
Ivan SmoliakovandHosted Weblate d1fabba86b Translated using Weblate (Russian)
Currently translated at 100.0% (1138 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-08-20 22:45:20 +00:00
Mathias FrancoandHosted Weblate b290a4ada3 Translated using Weblate (Dutch)
Currently translated at 100.0% (1138 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nl/
2025-08-20 22:45:19 +00:00
Paolo RicciandHosted Weblate bb477c617e Translated using Weblate (Italian)
Currently translated at 99.7% (1135 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2025-08-20 22:45:19 +00:00
Michael FörsterandHosted Weblate 9238c38842 Translated using Weblate (German)
Currently translated at 100.0% (1138 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-08-20 22:45:18 +00:00
Daniel SchosserandHosted Weblate d268516fcb Translated using Weblate (German)
Currently translated at 100.0% (1138 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-08-20 22:45:17 +00:00
Vito0912andHosted Weblate d353cff1ae Translated using Weblate (German)
Currently translated at 100.0% (1138 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-08-20 22:45:17 +00:00
kuci-JKandHosted Weblate 604f17f60b Translated using Weblate (Czech)
Currently translated at 98.6% (1123 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2025-08-20 22:45:16 +00:00
advplyrandGitHub 3911a7273b Merge pull request #4595 from renesat/fix/listening-ipv6-url
Fix listening url log with ipv6 host
2025-08-20 18:45:04 -04:00
advplyr 138bb563b8 Update ipv6 server listening log 2025-08-20 17:42:22 -05:00
advplyrandGitHub 3801ef062a Merge pull request #4596 from renesat/fix/redirect-url
Fix freeze on some audio sources
2025-08-19 18:11:13 -04:00
advplyr e4b9ac5446 Add episode updated translation for podcast match #4606 2025-08-19 16:52:50 -05:00
advplyr 9987d219f8 Remove success toast on podcast episodes removed #4606 2025-08-19 16:50:43 -05:00
advplyrandGitHub dc7045c562 Merge pull request #4608 from laxandrea/remove-token-from-hls-url
Remove token from hls url
2025-08-19 17:29:10 -04:00
laxandrea 2cc6e56bd1 remove token from hls url
- following PR #4263
2025-08-19 15:29:49 +02:00
advplyrandGitHub a89a24e48e Merge pull request #4598 from advplyr/episode_meta_tagging
Update podcast episode downloads to always attempt embedding meta tags
2025-08-17 10:20:54 -04:00
advplyr a968aca304 Update podcast episode downloads to always attempt embedding meta tags regardless of format 2025-08-17 09:05:29 -05:00
renesat 8d1f460640 Fix freeze on some audio sources 2025-08-16 18:56:23 +02:00
renesat 553ffd1934 Fix listening url log with ipv6 host 2025-08-16 18:24:31 +02:00
advplyr fd4932cdbb Add additional debug logs for OIDC login 2025-08-15 17:23:20 -05:00
advplyrandGitHub dcaca43817 Merge pull request #4384 from josh-vin/feat/ChaptersEnhancments
Enhancement: Improves chapter editing and adds bulk import
2025-08-14 17:38:56 -04:00
advplyr 0eed4e82f9 Fix bulk add chapter icon button tooltip 2025-08-14 16:35:28 -05:00
advplyr 2ed2328401 Remove negative chapter end check & tooltip 2025-08-14 16:18:33 -05:00
advplyr 8b260c8bc6 Update bulk chapter modal styles, decreased text and button sizes 2025-08-14 16:16:34 -05:00
advplyr 7dcb9b98a0 Chapter lookup modal add back button to clear lookup results 2025-08-14 16:03:32 -05:00
advplyrandGitHub 311ac7104e Merge pull request #4590 from advplyr/fix_authorize_race_condition
Fix authorize race condition by not updating the user on token refresh
2025-08-13 09:36:17 -04:00
advplyr 2c45b28d48 Fix authorize race condition by not updating the user on token refresh #4567 2025-08-13 08:31:01 -05:00
advplyrandGitHub b53613f82c Merge pull request #4552 from Toby222/master
Replace some SVG icons with material-symbols
2025-08-12 18:55:50 -04:00
advplyr 751371abb8 Update ReadIcon svg with material-symbols 2025-08-12 17:46:01 -05:00
advplyr 6365c02875 Update explicit material symbols icon to fill 2025-08-12 17:40:48 -05:00
advplyr fb3834156b Version bump v2.28.0 2025-08-10 17:42:32 -05:00
advplyrandGitHub c03f3f722d Merge pull request #4559 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-08-10 18:31:40 -04:00
FiendFEARingandHosted Weblate a06f48ca29 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1138 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-08-10 22:26:37 +00:00
NickSkierandHosted Weblate 9d79552dda Translated using Weblate (Russian)
Currently translated at 100.0% (1138 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-08-10 22:26:36 +00:00
Laurin SorgendandHosted Weblate ed98614b6f Translated using Weblate (German)
Currently translated at 99.9% (1137 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-08-10 22:26:36 +00:00
owlcollectorandHosted Weblate 09dd2cc79c Translated using Weblate (Japanese)
Currently translated at 6.0% (69 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ja/
2025-08-10 22:26:35 +00:00
weblate.user.1274andHosted Weblate e87237048a Translated using Weblate (Norwegian Bokmål)
Currently translated at 92.1% (1049 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nb_NO/
2025-08-10 22:26:35 +00:00
Kent HenriksenandHosted Weblate d71968fd80 Translated using Weblate (Norwegian Bokmål)
Currently translated at 92.1% (1049 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nb_NO/
2025-08-10 22:26:34 +00:00
ThomasandHosted Weblate f83c605ae1 Translated using Weblate (French)
Currently translated at 99.1% (1128 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2025-08-10 22:26:34 +00:00
J. LavoieandHosted Weblate 4325f470dd Translated using Weblate (German)
Currently translated at 99.8% (1136 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-08-10 22:26:33 +00:00
numerfoltandHosted Weblate 800ecf8e82 Translated using Weblate (German)
Currently translated at 99.8% (1136 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-08-10 22:26:32 +00:00
Vito0912andHosted Weblate 5cb143d50b Translated using Weblate (German)
Currently translated at 99.8% (1136 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-08-10 22:26:32 +00:00
Troj@andHosted Weblate 798c73c66c Translated using Weblate (Belarusian)
Currently translated at 64.6% (736 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-08-10 22:26:31 +00:00
Максим ГорпинічandHosted Weblate 0fa7c46274 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1138 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-08-10 22:26:31 +00:00
Kent HenriksenandHosted Weblate c2d420ec70 Translated using Weblate (Norwegian Bokmål)
Currently translated at 91.0% (1036 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nb_NO/
2025-08-10 22:26:30 +00:00
biuklijaandHosted Weblate 152daf7bf3 Translated using Weblate (Croatian)
Currently translated at 100.0% (1138 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2025-08-10 22:26:29 +00:00
Ashish WadekarandHosted Weblate 8d99249e50 Translated using Weblate (Hindi)
Currently translated at 8.7% (100 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hi/
2025-08-10 22:26:29 +00:00
Camille de LuneandHosted Weblate c6724ba353 Translated using Weblate (French)
Currently translated at 99.1% (1128 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2025-08-10 22:26:28 +00:00
Aleksandr ZakirovandHosted Weblate a519d44666 Translated using Weblate (Estonian)
Currently translated at 65.4% (745 of 1138 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/et/
2025-08-10 22:26:27 +00:00
Grzegorz OrlowskiandHosted Weblate 7e8bf977cc Translated using Weblate (Polish)
Currently translated at 82.9% (942 of 1135 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2025-08-10 22:26:27 +00:00
advplyr 4018be6330 Fix oidc auto-register not cleaning up new user on errors #4563 2025-08-10 17:26:15 -05:00
advplyr 99a3867ce9 Update callback url check
Co-authored-by: Denis Arnst <git@sapd.eu>
2025-08-10 17:08:25 -05:00
advplyrandGitHub 2116f60133 Merge pull request #4565 from advplyr/redirect_transcode_requests
Fix server crash when transcode requests are made to the direct play endpoint
2025-08-07 18:31:45 -04:00
advplyr 794f0ef42a Fix server crash when transcode requests are made to the direct play endpoint #4555 2025-08-07 17:21:05 -05:00
Josh Vincent 3e423839a1 Fixes UI for Bulk Chapter adder, and changes logic around locking 2025-08-04 18:33:06 -06:00
Josh Vincent 2773c8c4a9 Merge remote-tracking branch 'josh-vin/master' into feat/ChaptersEnhancments 2025-08-04 18:32:28 -06:00
advplyrandGitHub e510174f12 Merge pull request #4557 from Vito0912/cors
Allow a whitelist of CORS origins
2025-08-04 19:02:30 -04:00
advplyr 08c9e8d47d Fix i18n string order 2025-08-04 17:56:56 -05:00
advplyr 1908ec3df5 Remove commented out experimental features setting 2025-08-04 17:54:59 -05:00
advplyr df3878d4ca Add Security section to settings with allowed cors origin setting, increase width of setting inputs 2025-08-04 17:54:29 -05:00
Vito0912 1097de6f1f now updates the input field 2025-08-04 19:17:46 +02:00
Vito0912 e408070b19 better heading 2025-08-03 14:02:33 +02:00
Vito0912 af67c2e86f locale 2025-08-03 13:57:44 +02:00
Vito0912 6a52d2a968 CORS 2025-08-03 13:52:58 +02:00
advplyr 3337b3af18 Version bump v2.27.0 2025-08-02 17:53:27 -05:00
advplyrandGitHub 835d2c7f36 Merge pull request #4535 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-08-02 18:52:04 -04:00
FiendFEARingandHosted Weblate 03f91099e0 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1135 of 1135 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-08-02 15:02:00 +02:00
Максим ГорпинічandHosted Weblate c04afd0787 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1135 of 1135 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-08-02 15:01:59 +02:00
Grzegorz OrlowskiandHosted Weblate b03bd79f5d Translated using Weblate (Polish)
Currently translated at 77.6% (881 of 1135 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2025-08-02 15:01:57 +02:00
Tobias Berger 5ef632a7eb Replace some SVG icons with material-symbols 2025-08-01 09:20:34 +02:00
Troj@andHosted Weblate 79b4042e8e Translated using Weblate (Belarusian)
Currently translated at 63.6% (721 of 1133 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-08-01 02:51:15 +02:00
FiendFEARingandHosted Weblate 8f718ef91c Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1133 of 1133 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-08-01 02:51:15 +02:00
PepijnandHosted Weblate 4053b20623 Translated using Weblate (Dutch)
Currently translated at 100.0% (1133 of 1133 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nl/
2025-08-01 02:51:15 +02:00
Remco SchrijverandHosted Weblate c4d654635f Translated using Weblate (Dutch)
Currently translated at 100.0% (1133 of 1133 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nl/
2025-08-01 02:51:15 +02:00
enoshandHosted Weblate ef5d0ffa48 Translated using Weblate (Hebrew)
Currently translated at 74.7% (847 of 1133 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/he/
2025-08-01 02:51:15 +02:00
Troj@andHosted Weblate 6a826cdb36 Translated using Weblate (Belarusian)
Currently translated at 58.1% (659 of 1133 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/be/
2025-08-01 02:51:15 +02:00
thehijackerandHosted Weblate 1d837f5f21 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1133 of 1133 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2025-08-01 02:51:15 +02:00
Максим ГорпинічandHosted Weblate 80873b379c Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1133 of 1133 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-08-01 02:51:15 +02:00
Remco SchrijverandHosted Weblate 82a8f8f126 Translated using Weblate (Dutch)
Currently translated at 98.6% (1118 of 1133 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nl/
2025-08-01 02:51:15 +02:00
Kabika82andHosted Weblate 4725a466da Translated using Weblate (Hungarian)
Currently translated at 100.0% (1133 of 1133 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2025-08-01 02:51:15 +02:00
JannikandHosted Weblate 031edc870c Translated using Weblate (German)
Currently translated at 99.8% (1131 of 1133 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-08-01 02:51:15 +02:00
Jan-Eric MyhrgrenandHosted Weblate 625e2445b5 Translated using Weblate (Swedish)
Currently translated at 96.2% (1089 of 1131 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-08-01 02:51:15 +02:00
max grakovandHosted Weblate 1640af2f1c Translated using Weblate (Russian)
Currently translated at 100.0% (1131 of 1131 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-08-01 02:51:15 +02:00
Remco SchrijverandHosted Weblate c76f76cc27 Translated using Weblate (Dutch)
Currently translated at 98.4% (1113 of 1131 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nl/
2025-08-01 02:51:15 +02:00
ugyesandHosted Weblate 74af212293 Translated using Weblate (Hungarian)
Currently translated at 100.0% (1131 of 1131 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2025-08-01 02:51:15 +02:00
Vito0912andHosted Weblate e04efb9c6a Translated using Weblate (German)
Currently translated at 99.9% (1130 of 1131 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-08-01 02:51:14 +02:00
B0raxandHosted Weblate ee17e7a555 Translated using Weblate (German)
Currently translated at 99.9% (1130 of 1131 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-08-01 02:51:14 +02:00
FiendFEARingandHosted Weblate 694a852c07 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1131 of 1131 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-08-01 02:51:14 +02:00
Максим ГорпинічandHosted Weblate 18068bb261 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1131 of 1131 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-08-01 02:51:14 +02:00
Mikkel Dupont OlesenandHosted Weblate 71257f6c6c Translated using Weblate (Danish)
Currently translated at 99.4% (1124 of 1130 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/da/
2025-08-01 02:51:14 +02:00
advplyr 4d70929d2e Add locale strings for user stats heatmap #4550 2025-07-31 19:51:05 -05:00
advplyrandGitHub 578e9559e4 Merge pull request #4551 from chriscam85/master
Including total durations into the de-branding from #4226 as warning message is always present currently
2025-07-31 20:23:47 -04:00
advplyr 894ea0b80a Update chapter data log 2025-07-31 19:19:11 -05:00
Chris Campanile e54571f011 Including total durations into the de-branding from #4226 as warning message is always present currently 2025-07-31 16:48:05 -07:00
Josh Vincent 77d7a50b99 Merge remote-tracking branch 'josh-vin/master' into feat/ChaptersEnhancments 2025-07-30 16:51:12 -06:00
advplyrandGitHub 32da0f1224 Merge pull request #4542 from advplyr/progress_updated_sort
Add book library sort by progress updated #1215
2025-07-28 15:13:40 -04:00
advplyr 2054accdc9 Update library sort dropdown to use max height available 2025-07-28 15:07:57 -04:00
advplyr 7d8b857c77 Add book library sort by progress updated #1215 2025-07-28 14:58:28 -04:00
advplyr 0107cb4782 UI/UX fix x overflow for sessions tables on mobile 2025-07-26 09:45:44 -05:00
advplyrandGitHub f273eee807 Merge pull request #4534 from michaeldvinci/sepia-theme
Add 'sepia' theme to EpubReader
2025-07-25 17:34:34 -05:00
advplyr 4af21b079a Fix epub toc search input colors for themes 2025-07-25 17:31:59 -05:00
Michael Vinci c9eaf2db2d Add 'sepia' theme to EpubReader 2025-07-25 17:01:16 -05:00
sir-wilhelm cae1560344 Use PowerShell to get windows drive paths.
wmic has been deprecated on newer versions of Windows 11 and is not installed.

resolves #4531
2025-07-25 12:34:29 -05:00
advplyrandGitHub a5fb0d9cdb Merge pull request #4530 from advplyr/fix_ereader_socket_event
Fix ereader update socket event sending all devices #4529
2025-07-24 17:33:42 -05:00
advplyrandGitHub 53c80d9798 Merge pull request #4528 from FelixSche/master
Update SideRail.vue
2025-07-24 17:32:44 -05:00
advplyr 832165716b Fix ereader update socket event sending all devices #4529 2025-07-24 17:29:08 -05:00
FelixandGitHub d9f2d8bf1d Update SideRail.vue
Changed cursor at version to pointer
2025-07-24 13:57:26 +02:00
advplyr a7a3a56509 Version bump v2.26.3 2025-07-23 17:18:51 -05:00
advplyrandGitHub 4082fadf90 Merge pull request #4525 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-07-23 17:17:51 -05:00
FiendFEARingandHosted Weblate 93160b83bf Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1130 of 1130 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-07-23 13:06:32 +02:00
Максим ГорпинічandHosted Weblate 472240f994 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1130 of 1130 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-07-23 13:06:31 +02:00
DmitryandHosted Weblate c3f0fb8e5e Translated using Weblate (Russian)
Currently translated at 100.0% (1130 of 1130 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-07-23 13:06:30 +02:00
Daniel SchosserandHosted Weblate b156ebeb9f Translated using Weblate (German)
Currently translated at 99.9% (1129 of 1130 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-07-23 13:06:29 +02:00
advplyrandGitHub e4c775c847 Merge pull request #4523 from advplyr/fix_change_empty_root_password
Update change password to support null or empty string passwords #4522
2025-07-22 17:01:07 -05:00
advplyr 45e8e72759 Update change password to support null or empty string passwords #4522 2025-07-22 15:17:00 -05:00
advplyrandGitHub 0ae7340889 Merge pull request #4520 from advplyr/fix_podcast_session_track_index
Fix podcast episode track index null in playback session
2025-07-22 15:04:22 -05:00
advplyr 8c38987d92 Fix podcast episode track index null in playback session 2025-07-22 14:44:36 -05:00
advplyr 878f0787ba Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2025-07-21 17:07:12 -05:00
advplyr 880d85eaef Version bump v2.26.2 2025-07-21 17:07:06 -05:00
advplyrandGitHub f7aaebc1de Merge pull request #4508 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-07-21 17:01:15 -05:00
CharlieandHosted Weblate d96ebbe82d Translated using Weblate (French)
Currently translated at 100.0% (1129 of 1129 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/fr/
2025-07-22 00:00:51 +02:00
kuci-JKandHosted Weblate 70d67156f0 Translated using Weblate (Czech)
Currently translated at 99.2% (1121 of 1129 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2025-07-22 00:00:51 +02:00
Serhat GülaştıandHosted Weblate f293b317be Translated using Weblate (Turkish)
Currently translated at 28.3% (320 of 1129 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/tr/
2025-07-22 00:00:51 +02:00
Simple16andHosted Weblate 1f23794f88 Translated using Weblate (Russian)
Currently translated at 99.9% (1128 of 1129 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-07-22 00:00:51 +02:00
FiendFEARingandHosted Weblate e6bfd118f6 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1129 of 1129 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-07-22 00:00:51 +02:00
SunSpringandHosted Weblate 1166400ab1 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1129 of 1129 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-07-22 00:00:51 +02:00
Максим ГорпинічandHosted Weblate 55f0ac871b Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1129 of 1129 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-07-22 00:00:51 +02:00
Angelo PrandelliandHosted Weblate 3584f6e24f Translated using Weblate (Italian)
Currently translated at 98.2% (1109 of 1129 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/it/
2025-07-22 00:00:51 +02:00
biuklijaandHosted Weblate 23bf2594c8 Translated using Weblate (Croatian)
Currently translated at 100.0% (1129 of 1129 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2025-07-22 00:00:51 +02:00
advplyrandGitHub 8fb460ce05 Merge pull request #4319 from mikiher/audible-confidence-score
Audible confidence score
2025-07-21 17:00:44 -05:00
advplyr 8c4bbfd6a2 Add match confidence as a badge on match book card 2025-07-21 16:52:21 -05:00
advplyrandGitHub 742961e0b8 Merge pull request #4510 from advplyr/fix_set_token
Fix set token on page load #4509
2025-07-18 17:11:09 -05:00
advplyr 5b6807892f Fix set token on page load #4509 2025-07-18 16:59:27 -05:00
advplyr b911a25c57 Version bump v2.26.1 2025-07-16 17:16:43 -05:00
advplyrandGitHub 53110674e4 Merge pull request #4492 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-07-16 17:15:48 -05:00
Grzegorz OrlowskiandHosted Weblate f963cd4753 Translated using Weblate (Polish)
Currently translated at 76.1% (859 of 1128 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2025-07-16 22:11:56 +00:00
Grzegorz OrlowskiandHosted Weblate 0dccc3bcae Translated using Weblate (Polish)
Currently translated at 76.0% (858 of 1128 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/pl/
2025-07-16 22:11:55 +00:00
FiendFEARingandHosted Weblate 5b4fd5b254 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1128 of 1128 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-07-16 22:11:55 +00:00
Fredrik LindqvistandHosted Weblate bdb9d3caeb Translated using Weblate (Swedish)
Currently translated at 95.2% (1074 of 1128 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-07-16 22:11:54 +00:00
JannikandHosted Weblate 9aca824b59 Translated using Weblate (German)
Currently translated at 99.9% (1127 of 1128 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-07-16 22:11:53 +00:00
JannikandHosted Weblate 8e891805eb Translated using Weblate (German)
Currently translated at 99.9% (1127 of 1128 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-07-16 22:11:53 +00:00
JannikandHosted Weblate 2760517445 Translated using Weblate (German)
Currently translated at 99.9% (1127 of 1128 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-07-16 22:11:52 +00:00
JannikandHosted Weblate 889ee33320 Translated using Weblate (German)
Currently translated at 99.9% (1127 of 1128 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-07-16 22:11:51 +00:00
JannikandHosted Weblate 4f65801713 Translated using Weblate (German)
Currently translated at 99.9% (1127 of 1128 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-07-16 22:11:50 +00:00
JannikandHosted Weblate 3e75acd4ef Translated using Weblate (German)
Currently translated at 99.9% (1127 of 1128 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-07-16 22:11:50 +00:00
JannikandHosted Weblate 3e8fe2ef60 Translated using Weblate (German)
Currently translated at 99.9% (1127 of 1128 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-07-16 22:11:49 +00:00
JannikandHosted Weblate 0bc441de20 Translated using Weblate (German)
Currently translated at 99.9% (1127 of 1128 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-07-16 22:11:48 +00:00
JannikandHosted Weblate a8c2f0d4c8 Translated using Weblate (German)
Currently translated at 99.9% (1127 of 1128 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-07-16 22:11:48 +00:00
FiendFEARingandHosted Weblate b59da8bd0c Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1128 of 1128 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-07-16 22:11:47 +00:00
FiendFEARingandHosted Weblate 77cb4f75c6 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1128 of 1128 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-07-16 22:11:46 +00:00
Максим ГорпинічandHosted Weblate 9cf1711fae Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1128 of 1128 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-07-16 22:11:45 +00:00
Jan-Eric MyhrgrenandHosted Weblate f472116dc3 Translated using Weblate (Swedish)
Currently translated at 94.5% (1066 of 1128 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-07-16 22:11:45 +00:00
Jan-Eric MyhrgrenandHosted Weblate c7eb9d7799 Translated using Weblate (Swedish)
Currently translated at 94.5% (1066 of 1128 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-07-16 22:11:44 +00:00
Jan-Eric MyhrgrenandHosted Weblate c66380eaeb Translated using Weblate (Swedish)
Currently translated at 94.5% (1066 of 1128 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sv/
2025-07-16 22:11:43 +00:00
Simple16andHosted Weblate 1bebb22705 Translated using Weblate (Russian)
Currently translated at 100.0% (1128 of 1128 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-07-16 22:11:43 +00:00
Simple16andHosted Weblate 4e96649fe3 Translated using Weblate (Russian)
Currently translated at 100.0% (1128 of 1128 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-07-16 22:11:42 +00:00
Simple16andHosted Weblate a21cec806e Translated using Weblate (Russian)
Currently translated at 100.0% (1128 of 1128 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/ru/
2025-07-16 22:11:41 +00:00
biuklijaandHosted Weblate 8a3b8d2249 Translated using Weblate (Croatian)
Currently translated at 100.0% (1128 of 1128 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hr/
2025-07-16 22:11:40 +00:00
advplyrandGitHub 581277914c Merge pull request #4503 from advplyr/session_modal_user
Update sessions modal to show username & update legacy token input with warning
2025-07-16 17:11:28 -05:00
advplyr e678fe6e2f Update sessions modal to show username & update sessions endpoints to always return username 2025-07-16 16:56:07 -05:00
advplyr 3845940245 Add warning under legacy token input on users page to use api keys instead 2025-07-16 16:43:53 -05:00
advplyr 6c63e2131c Update AllowCors to apply to every request #4497 2025-07-15 16:28:41 -05:00
advplyrandGitHub e25e2b238f Merge pull request #4493 from advplyr/localize_durations
Localize elapsed duration on sessions tables
2025-07-14 17:28:58 -05:00
advplyr 99110f587a Localize elapsed duration on sessions tables 2025-07-14 17:17:39 -05:00
advplyrandGitHub b553e959e2 Merge pull request #4486 from advplyr/fix_oidc_create_user
Fix OIDC auto register user #4485
2025-07-13 17:09:40 -05:00
advplyr f7b94a4b6d Fix OIDC auto register user #4485 2025-07-13 17:04:02 -05:00
mikiherandGitHub e9a705587a Merge branch 'advplyr:master' into audible-confidence-score 2025-07-13 10:13:00 +03:00
advplyr 264ae928a9 Version bump v2.26.0 2025-07-12 11:43:14 -05:00
advplyrandGitHub f5248a9f00 Merge pull request #4476 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-07-12 11:41:54 -05:00
FiendFEARingandHosted Weblate 3473ff594a Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1108 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-07-12 18:32:35 +02:00
FiendFEARingandHosted Weblate 20bb6e13b5 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1108 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-07-12 18:32:35 +02:00
FiendFEARingandHosted Weblate a05d32b1d7 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1108 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-07-12 18:32:34 +02:00
Kabika82andHosted Weblate c6b3521cb6 Translated using Weblate (Hungarian)
Currently translated at 100.0% (1108 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2025-07-12 18:32:34 +02:00
Kabika82andHosted Weblate 2444504c6a Translated using Weblate (Hungarian)
Currently translated at 100.0% (1108 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/hu/
2025-07-12 18:32:33 +02:00
advplyrandGitHub d38532c07a Merge pull request #4444 from advplyr/jwt_auth_refactor
Implement new JWT auth
2025-07-12 11:32:22 -05:00
advplyr 4f7831611f Update auth re-login i18n string 2025-07-12 11:23:08 -05:00
advplyr d09db19cd5 Update re-login message to show for users without github discussion link, add message to i18n strings 2025-07-12 11:21:52 -05:00
advplyr 030e43f382 Support disabled rate limiter by setting max to 0, add logs when rate limit is changed from default 2025-07-12 10:51:07 -05:00
advplyr f081a7fdc1 Update rate limiter to use requestIp as key, pass in configurable error message 2025-07-12 10:32:35 -05:00
advplyr f0d5f46199 Merge branch 'master' into jwt_auth_refactor 2025-07-11 16:59:19 -05:00
advplyrandGitHub 0b8f6db45e Merge pull request #4445 from weblate/weblate-audiobookshelf-abs-web-client
Translations update from Hosted Weblate
2025-07-11 16:58:05 -05:00
advplyr 806c0a2991 Remove return_tokens query param for login 2025-07-11 16:01:45 -05:00
advplyr 7d6d3e6687 Move invalidate refresh token to TokenManager 2025-07-11 14:43:07 -05:00
FiendFEARingandHosted Weblate ad07ed7e25 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1108 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-07-11 03:04:29 +02:00
advplyr d3402e30c2 Update ereaders to handle refreshing, epubjs to use custom request method, separate accessToken in store 2025-07-10 16:54:28 -05:00
advplyr 25fe4dee3a Update epub reader to use axios for handling refresh tokens 2025-07-09 17:03:10 -05:00
advplyr 3c21c82ce1 Merge branch 'master' into jwt_auth_refactor 2025-07-09 14:55:05 -05:00
thehijackerandHosted Weblate 3c8876a37d Translated using Weblate (Slovenian)
Currently translated at 100.0% (1108 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2025-07-09 19:54:31 +00:00
thehijackerandHosted Weblate fba70c9831 Translated using Weblate (Slovenian)
Currently translated at 100.0% (1108 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sl/
2025-07-09 19:54:30 +00:00
SunSpringandHosted Weblate 27e40d16fd Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1108 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-07-09 19:54:30 +00:00
FiendFEARingandHosted Weblate 448cbf8530 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1108 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-07-09 19:54:29 +00:00
SunSpringandHosted Weblate f1153f9da5 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1108 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-07-09 19:54:28 +00:00
RajandHosted Weblate d09a21d922 Translated using Weblate (Gujarati)
Currently translated at 16.6% (184 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/gu/
2025-07-09 19:54:28 +00:00
Richard PožgayandHosted Weblate 62afa3c3ee Translated using Weblate (Czech)
Currently translated at 100.0% (1108 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2025-07-09 19:54:27 +00:00
Richard PožgayandHosted Weblate 85446be0e5 Translated using Weblate (Czech)
Currently translated at 100.0% (1108 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/cs/
2025-07-09 19:54:27 +00:00
MichalandHosted Weblate 018ca8e7ee Translated using Weblate (Slovak)
Currently translated at 99.9% (1107 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/sk/
2025-07-09 19:54:26 +00:00
Максим ГорпинічandHosted Weblate f02453ac92 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1108 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-07-09 19:54:25 +00:00
DavevanIerselandHosted Weblate 84b77f4c7f Translated using Weblate (Dutch)
Currently translated at 100.0% (1108 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nl/
2025-07-09 19:54:25 +00:00
FiendFEARingandHosted Weblate d41276ba8c Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.9% (1107 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-07-09 19:54:24 +00:00
FiendFEARingandHosted Weblate 576d7dc024 Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 99.9% (1107 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/zh_Hans/
2025-07-09 19:54:24 +00:00
Максим ГорпинічandHosted Weblate 6d2b1df560 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (1108 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/uk/
2025-07-09 19:54:23 +00:00
DavevanIerselandHosted Weblate 8255e4308c Translated using Weblate (Dutch)
Currently translated at 100.0% (1108 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nl/
2025-07-09 19:54:22 +00:00
DavevanIerselandHosted Weblate 794adf0292 Translated using Weblate (Dutch)
Currently translated at 100.0% (1108 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/nl/
2025-07-09 19:54:22 +00:00
Daniel SchosserandHosted Weblate f2e0b9762c Translated using Weblate (German)
Currently translated at 100.0% (1108 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-07-09 19:54:21 +00:00
Daniel SchosserandHosted Weblate 7d0def0edb Translated using Weblate (German)
Currently translated at 100.0% (1108 of 1108 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-07-09 19:54:21 +00:00
Vito0912andHosted Weblate 0653572396 Translated using Weblate (German)
Currently translated at 99.9% (1106 of 1107 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-07-09 19:54:20 +00:00
Vito0912andHosted Weblate d9a3750667 Translated using Weblate (German)
Currently translated at 99.9% (1106 of 1107 strings)

Translation: Audiobookshelf/Abs Web Client
Translate-URL: https://hosted.weblate.org/projects/audiobookshelf/abs-web-client/de/
2025-07-09 19:54:19 +00:00
advplyrandGitHub 9c0c7b6b08 Merge pull request #4469 from advplyr/fix_scanner_deleting_single_file_books
Fix scanner after deleting single file books #4459
2025-07-09 14:54:05 -05:00
mikiherandGitHub bf6d81b333 Merge branch 'advplyr:master' into audible-confidence-score 2025-07-09 09:04:52 +03:00
advplyr 8775e55762 Update jwt secret handling 2025-07-08 16:39:50 -05:00
advplyr d0d152c20d Seperate setUserToken from setUser in store 2025-07-08 09:45:24 -05:00
advplyr 4ff7355262 Fix hashPassword 2025-07-08 09:14:07 -05:00
advplyr 6cc7a44a22 Update oidc redirect to pass both new and old token in url 2025-07-07 17:21:25 -05:00
advplyr ad092ef8f8 Merge branch 'master' into jwt_auth_refactor 2025-07-07 16:50:58 -05:00
advplyr 691f291843 Update LibraryItemController unit test 2025-07-07 16:26:17 -05:00
advplyr ac381854e5 Add rate limiter for auth endpoints 2025-07-07 16:23:15 -05:00
advplyr 9c8900560c Seperate out auth strategies, update change password to return error status codes 2025-07-07 15:04:40 -05:00
advplyr d9cfcc86e7 Update oidc to return refresh token in response body for mobile 2025-07-07 09:16:07 -05:00
advplyr 97afd22f81 Refactor Auth to breakout functions in TokenManager, handle token generation for OIDC 2025-07-06 16:43:03 -05:00
advplyr e24eaab3f1 Log when token expiry is set via env var, api-keys create/update returns with user association 2025-07-06 13:10:14 -05:00
advplyr e201247d69 Handle socket re-authentication, fix socket toast to be re-usable, socket cleanup 2025-07-06 11:07:01 -05:00
advplyr a24dae5262 Merge branch 'master' into jwt_auth_refactor 2025-07-06 09:06:39 -05:00
advplyr e59babdf24 Force re-login if using old token, show alert if admin user, add isOldToken flag to user 2025-07-05 17:46:18 -05:00
advplyr 8dbe1e4e5d Fix express.json position 2025-07-04 16:49:45 -05:00
advplyr cdc37ddb0f Use x-refresh-token for alt method of passing refresh token, check x-refresh-token for logout 2025-07-04 13:54:37 -05:00
advplyr f127a7beb5 Update router for internal-api routes 2025-07-03 17:31:38 -05:00
advplyr 44ff90a6f2 Update refresh endpoint to support override cookie token 2025-07-01 16:31:26 -05:00
advplyr 8b995a179d Add support for returning refresh token for mobile clients 2025-06-30 17:31:31 -05:00
advplyr 4d32a22de9 Update API Keys to be tied to a user, add apikey lru-cache, handle deactivating expired keys 2025-06-30 14:53:11 -05:00
advplyr af1ff12dbb Add get all, update and delete endpoints. Add api keys config page 2025-06-30 11:32:02 -05:00
advplyr d96ed01ce4 Set up ApiKey model and create Api Key endpoint 2025-06-30 10:12:39 -05:00
advplyr 4f5123e842 Implement new JWT auth 2025-06-29 17:22:58 -05:00
mikiherandGitHub 9c44fc0d01 Merge branch 'advplyr:master' into audible-confidence-score 2025-06-26 18:09:13 +03:00
mikiherandGitHub 5017e7ce9e Merge branch 'advplyr:master' into audible-confidence-score 2025-06-16 10:26:58 +03:00
Josh Vincent 9da0be6d36 Allow clicking on elapsedTime to adjust chapter start 2025-06-08 13:18:41 -06:00
Josh Vincent c41bdb951c Moves the lock button and fixes padding on bulk add feature.
Moves the lock button the right of the Title text box.

Enhances the bulk chapter add feature by preserving zero-padding in chapter titles and prevents editing of locked chapters. Also allows Enter to be pressed in the Add Multiple Chapters modal.

Adds a warning toast when attempting to modify locked chapters.

Fixes sizing of boxes on smaller windows
2025-06-07 14:48:05 -06:00
Josh Vincent 54815ea9c7 Add a second to bulk chapters so its valid
This will enable users to go in and fix the chapter timing later but still save easily with the bulk import.
2025-06-06 13:25:20 -06:00
Josh Vincent 679ffed0ea Alphabetizes strings 2025-06-06 11:50:44 -06:00
Josh Vincent 09397cf3de Improves chapter editing and adds bulk import
Adds chapter locking functionality, allowing users to lock individual chapters or all chapters at once to prevent accidental edits.

Implements time increment buttons to precisely adjust chapter start times.

Introduces bulk chapter import functionality, allowing users to quickly add multiple chapters using a detected numbering pattern.

Adds elapsed time display during chapter playback for better user feedback.

Updates UI tooltips and icons for improved clarity and user experience.
2025-06-06 11:22:38 -06:00
mikiher de25763a74 Add match confidence display to BookMatchCard 2025-05-21 11:16:46 +03:00
mikiher a894ceb9cf Match confidence calculation for audible results 2025-05-21 10:25:42 +03:00
mikiher 387e58a714 Add levenshteinSimilarity function to utils 2025-05-21 09:57:44 +03:00
231 changed files with 18305 additions and 4178 deletions
+3 -3
View File
@@ -47,7 +47,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -60,7 +60,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v3
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -73,6 +73,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v3
with:
category: '/language:${{matrix.language}}'
+2 -2
View File
@@ -22,7 +22,7 @@ add_user() {
declare -r descr="${4:-No description}"
declare -r shell="${5:-/bin/false}"
if ! getent passwd | grep -q "^$user:"; then
if ! getent passwd "$user" 2>&1 >/dev/null; then
echo "Creating system user: $user in $group with $descr and shell $shell"
useradd $uid_flags --gid $group --no-create-home --system --shell $shell -c "$descr" $user
fi
@@ -39,7 +39,7 @@ add_group() {
declare -r gid_flags="--gid $gid"
fi
if ! getent group | grep -q "^$group:" ; then
if ! getent group "$group" 2>&1 >/dev/null; then
echo "Creating system group: $group"
groupadd $gid_flags --system $group
fi
+1
View File
@@ -196,6 +196,7 @@ export default {
requestBatchQuickEmbed() {
const payload = {
message: this.$strings.MessageConfirmQuickEmbed,
allowHtml: true,
callback: (confirmed) => {
if (confirmed) {
this.$axios
@@ -300,6 +300,8 @@ export default {
})
},
userUpdated(user) {
if (user.id !== this.$store.state.user.user.id) return
if (user.seriesHideFromContinueListening && user.seriesHideFromContinueListening.length) {
this.removeAllSeriesFromContinueSeries(user.seriesHideFromContinueListening)
}
+2 -2
View File
@@ -93,10 +93,10 @@ export default {
editAuthor(author) {
this.$store.commit('globals/showEditAuthorModal', author)
},
editItem(libraryItem) {
editItem(libraryItem, tab = 'details') {
var itemIds = this.shelf.entities.map((e) => e.id)
this.$store.commit('setBookshelfBookIds', itemIds)
this.$store.commit('showEditModal', libraryItem)
this.$store.commit('showEditModalOnTab', { libraryItem, tab: tab || 'details' })
},
editEpisode({ libraryItem, episode }) {
this.$store.commit('setEpisodeTableEpisodeIds', [episode.id])
+4 -15
View File
@@ -3,24 +3,18 @@
<div class="flex md:hidden h-10 items-center">
<nuxt-link :to="`/library/${currentLibraryId}`" class="grow h-full flex justify-center items-center" :class="isHomePage ? 'bg-primary/80' : 'bg-primary/40'">
<p v-if="isHomePage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonHome }}</p>
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
<span v-else class="material-symbols text-lg">home</span>
</nuxt-link>
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="grow h-full flex justify-center items-center" :class="isLibraryPage ? 'bg-primary/80' : 'bg-primary/40'">
<p v-if="isLibraryPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonLibrary }}</p>
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
<span v-else class="material-symbols text-lg">import_contacts</span>
</nuxt-link>
<nuxt-link v-if="isPodcastLibrary" :to="`/library/${currentLibraryId}/podcast/latest`" class="grow h-full flex justify-center items-center" :class="isPodcastLatestPage ? 'bg-primary/80' : 'bg-primary/40'">
<p class="text-sm">{{ $strings.ButtonLatest }}</p>
</nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="grow h-full flex justify-center items-center" :class="isSeriesPage ? 'bg-primary/80' : 'bg-primary/40'">
<p v-if="isSeriesPage" class="text-sm">{{ $strings.ButtonSeries }}</p>
<svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
</svg>
<span v-else class="material-symbols text-lg">view_column</span>
</nuxt-link>
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="grow h-full flex justify-center items-center" :class="isPlaylistsPage ? 'bg-primary/80' : 'bg-primary/40'">
<p v-if="isPlaylistsPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonPlaylists }}</p>
@@ -32,12 +26,7 @@
</nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="grow h-full flex justify-center items-center" :class="isAuthorsPage ? 'bg-primary/80' : 'bg-primary/40'">
<p v-if="isAuthorsPage" class="text-sm">{{ $strings.ButtonAuthors }}</p>
<svg v-else class="w-5 h-5" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z"
/>
</svg>
<span v-else class="material-symbols text-lg">groups</span>
</nuxt-link>
<nuxt-link v-if="isPodcastLibrary && userIsAdminOrUp" :to="`/library/${currentLibraryId}/podcast/search`" class="grow h-full flex justify-center items-center" :class="isPodcastSearchPage ? 'bg-primary/80' : 'bg-primary/40'">
<p class="text-sm">{{ $strings.ButtonAdd }}</p>
+5
View File
@@ -70,6 +70,11 @@ export default {
title: this.$strings.HeaderUsers,
path: '/config/users'
},
{
id: 'config-api-keys',
title: this.$strings.HeaderApiKeys,
path: '/config/api-keys'
},
{
id: 'config-sessions',
title: this.$strings.HeaderListeningSessions,
+3 -9
View File
@@ -22,7 +22,7 @@
<div v-if="entityName === 'collections' || entityName === 'playlists'" class="flex justify-center mt-4">
{{ emptyMessageHelp }}
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/collections" target="_blank" class="inline-flex">
<a href="https://audiobookshelf.org/docs/documentation/libraries/common-content/playlists" target="_blank" class="inline-flex">
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
</a>
</ui-tooltip>
@@ -232,11 +232,11 @@ export default {
clearFilter() {
this.$store.dispatch('user/updateUserSettings', { filterBy: 'all' })
},
editEntity(entity) {
editEntity(entity, tab = 'details') {
if (this.entityName === 'items' || this.entityName === 'series-books') {
const bookIds = this.entities.map((e) => e.id)
this.$store.commit('setBookshelfBookIds', bookIds)
this.$store.commit('showEditModal', entity)
this.$store.commit('showEditModalOnTab', { libraryItem: entity, tab: tab || 'details' })
} else if (this.entityName === 'collections') {
this.$store.commit('globals/setEditCollection', entity)
} else if (this.entityName === 'playlists') {
@@ -778,10 +778,6 @@ export default {
windowResize() {
this.executeRebuild()
},
socketInit() {
// Server settings are set on socket init
this.executeRebuild()
},
initListeners() {
window.addEventListener('resize', this.windowResize)
@@ -794,7 +790,6 @@ export default {
})
this.$eventBus.$on('bookshelf_clear_selection', this.clearSelectedEntities)
this.$eventBus.$on('socket_init', this.socketInit)
this.$eventBus.$on('user-settings', this.settingsUpdated)
if (this.$root.socket) {
@@ -826,7 +821,6 @@ export default {
}
this.$eventBus.$off('bookshelf_clear_selection', this.clearSelectedEntities)
this.$eventBus.$off('socket_init', this.socketInit)
this.$eventBus.$off('user-settings', this.settingsUpdated)
if (this.$root.socket) {
+5 -16
View File
@@ -5,9 +5,7 @@
<div id="siderail-buttons-container" role="navigation" aria-label="Library Navigation" :class="{ 'player-open': streamLibraryItem }" class="w-full overflow-y-auto overflow-x-hidden">
<nuxt-link :to="`/library/${currentLibraryId}`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="homePage ? 'bg-primary/80' : 'bg-bg/60'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
</svg>
<span class="material-symbols text-2xl">home</span>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonHome }}</p>
@@ -23,9 +21,7 @@
</nuxt-link>
<nuxt-link :to="`/library/${currentLibraryId}/bookshelf`" class="w-full h-20 flex flex-col items-center justify-center text-white border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="showLibrary ? 'bg-primary/80' : 'bg-bg/60'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
<span class="material-symbols text-2xl">import_contacts</span>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonLibrary }}</p>
@@ -33,9 +29,7 @@
</nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/series`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isSeriesPage ? 'bg-primary/80' : 'bg-bg/60'">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" />
</svg>
<span class="material-symbols text-2xl">view_column</span>
<p class="pt-1.5 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonSeries }}</p>
@@ -59,12 +53,7 @@
</nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/authors`" class="w-full h-20 flex flex-col items-center justify-center text-white/80 border-b border-primary/70 hover:bg-primary cursor-pointer relative" :class="isAuthorsPage ? 'bg-primary/80' : 'bg-bg/60'">
<svg class="w-6 h-6" viewBox="0 0 24 24">
<path
fill="currentColor"
d="M12,5.5A3.5,3.5 0 0,1 15.5,9A3.5,3.5 0 0,1 12,12.5A3.5,3.5 0 0,1 8.5,9A3.5,3.5 0 0,1 12,5.5M5,8C5.56,8 6.08,8.15 6.53,8.42C6.38,9.85 6.8,11.27 7.66,12.38C7.16,13.34 6.16,14 5,14A3,3 0 0,1 2,11A3,3 0 0,1 5,8M19,8A3,3 0 0,1 22,11A3,3 0 0,1 19,14C17.84,14 16.84,13.34 16.34,12.38C17.2,11.27 17.62,9.85 17.47,8.42C17.92,8.15 18.44,8 19,8M5.5,18.25C5.5,16.18 8.41,14.5 12,14.5C15.59,14.5 18.5,16.18 18.5,18.25V20H5.5V18.25M0,20V18.5C0,17.11 1.89,15.94 4.45,15.6C3.86,16.28 3.5,17.22 3.5,18.25V20H0M24,20H20.5V18.25C20.5,17.22 20.14,16.28 19.55,15.6C22.11,15.94 24,17.11 24,18.5V20Z"
/>
</svg>
<span class="material-symbols text-2xl">groups</span>
<p class="pt-1 text-center leading-4" style="font-size: 0.9rem">{{ $strings.ButtonAuthors }}</p>
@@ -116,7 +105,7 @@
</div>
<div class="w-full h-12 px-1 py-2 border-t border-black/20 bg-bg absolute left-0" :style="{ bottom: streamLibraryItem ? '224px' : '65px' }">
<p class="underline font-mono text-xs text-center text-gray-300 leading-3 mb-1" @click="clickChangelog">v{{ $config.version }}</p>
<p class="underline font-mono text-xs text-center text-gray-300 leading-3 mb-1 cursor-pointer" @click="clickChangelog">v{{ $config.version }}</p>
<a v-if="hasUpdate" :href="githubTagUrl" target="_blank" class="text-warning text-xxs text-center block leading-3">Update</a>
<p v-else class="text-xxs text-gray-400 leading-3 text-center italic">{{ Source }}</p>
</div>
-3
View File
@@ -71,9 +71,6 @@ export default {
coverHeight() {
return this.cardHeight
},
userToken() {
return this.store.getters['user/getToken']
},
_author() {
return this.author || {}
},
+11 -3
View File
@@ -13,9 +13,17 @@
<div class="grow" />
<p class="text-sm md:text-base">{{ book.publishedYear }}</p>
</div>
<p v-if="book.author" class="text-gray-300 text-xs md:text-sm">{{ $getString('LabelByAuthor', [book.author]) }}</p>
<p v-if="book.narrator" class="text-gray-400 text-xs">{{ $strings.LabelNarrators }}: {{ book.narrator }}</p>
<p v-if="book.duration" class="text-gray-400 text-xs">{{ $strings.LabelDuration }}: {{ $elapsedPrettyExtended(bookDuration, false) }} {{ bookDurationComparison }}</p>
<div class="flex items-center">
<div>
<p v-if="book.author" class="text-gray-300 text-xs md:text-sm">{{ $getString('LabelByAuthor', [book.author]) }}</p>
<p v-if="book.narrator" class="text-gray-400 text-xs">{{ $strings.LabelNarrators }}: {{ book.narrator }}</p>
<p v-if="book.duration" class="text-gray-400 text-xs">{{ $strings.LabelDuration }}: {{ $elapsedPrettyExtended(bookDuration, false) }} {{ bookDurationComparison }}</p>
</div>
<div class="grow" />
<div v-if="book.matchConfidence" class="rounded-full px-2 py-1 text-xs whitespace-nowrap text-white" :class="book.matchConfidence > 0.95 ? 'bg-success/80' : 'bg-info/80'">{{ $strings.LabelMatchConfidence }}: {{ (book.matchConfidence * 100).toFixed(0) }}%</div>
</div>
<div v-if="book.series?.length" class="flex py-1 -mx-1">
<div v-for="(series, index) in book.series" :key="index" class="bg-white/10 rounded-full px-1 py-0.5 mx-1">
<p class="leading-3 text-xs text-gray-400">
+47 -2
View File
@@ -62,7 +62,24 @@
</widgets-alert>
<div v-if="isNonInteractable" class="absolute top-0 left-0 w-full h-full bg-black/50 flex items-center justify-center z-20">
<ui-loading-indicator :text="nonInteractionLabel" />
<ui-loading-indicator>
<div class="mb-4">
<span class="text-lg font-medium text-white">
{{ nonInteractionLabel }}
</span>
</div>
<div v-if="isUploading" class="w-64 mx-auto mb-2">
<div class="flex items-center justify-center mb-2">
<span class="text-sm font-medium text-white/60 text-center w-full">
{{ uploadProgressText }}
</span>
</div>
<div class="w-full bg-primary/20 rounded-full h-2.5">
<div class="bg-green-500 h-2.5 rounded-full transition-all duration-300 ease-out" :style="{ width: uploadProgressPercent + '%' }"></div>
</div>
</div>
</ui-loading-indicator>
</div>
</div>
</template>
@@ -91,7 +108,11 @@ export default {
isUploading: false,
uploadFailed: false,
uploadSuccess: false,
isFetchingMetadata: false
isFetchingMetadata: false,
uploadProgress: {
loaded: 0,
total: 0
}
}
},
computed: {
@@ -116,6 +137,15 @@ export default {
} else if (this.isFetchingMetadata) {
return this.$strings.LabelFetchingMetadata
}
},
uploadProgressPercent() {
if (this.uploadProgress.total === 0) return 0
return Math.min(100, Math.round((this.uploadProgress.loaded / this.uploadProgress.total) * 100))
},
uploadProgressText() {
const loaded = this.$bytesPretty(this.uploadProgress.loaded)
const total = this.$bytesPretty(this.uploadProgress.total)
return `${this.uploadProgressPercent}% (${loaded} / ${total})`
}
},
methods: {
@@ -123,6 +153,21 @@ export default {
this.isUploading = status === 'uploading'
this.uploadFailed = status === 'failed'
this.uploadSuccess = status === 'success'
if (status !== 'uploading') {
this.uploadProgress = {
loaded: 0,
total: 0
}
}
},
setUploadProgress(progress) {
if (this.isUploading && progress) {
this.uploadProgress = {
loaded: progress.loaded || 0,
total: progress.total || 0
}
}
},
titleUpdated() {
this.error = ''
+34 -6
View File
@@ -78,7 +78,7 @@
</div>
<!-- Error widget -->
<ui-tooltip cy-id="ErrorTooltip" v-if="showError" :text="errorText" class="absolute bottom-4e left-0 z-10">
<ui-tooltip cy-id="ErrorTooltip" v-if="showError" :text="errorText" plaintext class="absolute bottom-4e left-0 z-10">
<div :style="{ height: 1.5 + 'em', width: 2.5 + 'em' }" class="bg-error rounded-r-full shadow-md flex items-center justify-end border-r border-b border-red-300">
<span class="material-symbols text-red-100 pr-1e" :style="{ fontSize: 0.875 + 'em' }">priority_high</span>
</div>
@@ -101,7 +101,8 @@
<!-- Podcast Episode # -->
<div cy-id="podcastEpisodeNumber" v-if="recentEpisodeNumber !== null && !isHovering && !isSelectionMode && !processing" class="absolute rounded-lg bg-black/90 box-shadow-md z-10" :style="{ top: 0.375 + 'em', right: 0.375 + 'em', padding: `${0.1}em ${0.25}em` }">
<p :style="{ fontSize: 0.8 + 'em' }">
Episode<span v-if="recentEpisodeNumber"> #{{ recentEpisodeNumber }}</span>
Episode
<span v-if="recentEpisodeNumber">#{{ recentEpisodeNumber }}</span>
</p>
</div>
@@ -120,12 +121,12 @@
<!-- Alternative bookshelf title/author/sort -->
<div cy-id="detailBottom" :id="`description-area-${index}`" v-if="isAlternativeBookshelfView || isAuthorBookshelfView" dir="auto" class="relative mt-2e mb-2e left-0 z-50 w-full">
<div :style="{ fontSize: 0.9 + 'em' }">
<ui-tooltip v-if="displayTitle" :text="displayTitle" :disabled="!displayTitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center">
<ui-tooltip v-if="displayTitle" :text="displayTitle" plaintext :disabled="!displayTitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center">
<p cy-id="title" ref="displayTitle" class="truncate">{{ displayTitle }}</p>
<widgets-explicit-indicator cy-id="explicitIndicator" v-if="isExplicit" />
</ui-tooltip>
</div>
<ui-tooltip v-if="showSubtitles" :text="displaySubtitle" :disabled="!displaySubtitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center">
<ui-tooltip v-if="showSubtitles" :text="displaySubtitle" plaintext :disabled="!displaySubtitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center">
<p cy-id="subtitle" class="truncate" ref="displaySubtitle" :style="{ fontSize: 0.6 + 'em' }">{{ displaySubtitle }}</p>
</ui-tooltip>
<p cy-id="line2" class="truncate text-gray-400" :style="{ fontSize: 0.8 + 'em' }">{{ displayLineTwo || '&nbsp;' }}</p>
@@ -200,6 +201,9 @@ export default {
dateFormat() {
return this.store.getters['getServerSetting']('dateFormat')
},
timeFormat() {
return this.store.getters['getServerSetting']('timeFormat')
},
_libraryItem() {
return this.libraryItem || {}
},
@@ -345,6 +349,18 @@ export default {
if (this.mediaMetadata.publishedYear) return this.$getString('LabelPublishedDate', [this.mediaMetadata.publishedYear])
return '\u00A0'
}
if (this.orderBy === 'progress') {
if (!this.userProgressLastUpdated) return '\u00A0'
return this.$getString('LabelLastProgressDate', [this.$formatDatetime(this.userProgressLastUpdated, this.dateFormat, this.timeFormat)])
}
if (this.orderBy === 'progress.createdAt') {
if (!this.userProgressStartedDate) return '\u00A0'
return this.$getString('LabelStartedDate', [this.$formatDatetime(this.userProgressStartedDate, this.dateFormat, this.timeFormat)])
}
if (this.orderBy === 'progress.finishedAt') {
if (!this.userProgressFinishedDate) return '\u00A0'
return this.$getString('LabelFinishedDate', [this.$formatDatetime(this.userProgressFinishedDate, this.dateFormat, this.timeFormat)])
}
return null
},
episodeProgress() {
@@ -377,6 +393,18 @@ export default {
let progressPercent = this.itemIsFinished ? 1 : this.booksInSeries ? this.seriesProgressPercent : this.useEBookProgress ? this.userProgress?.ebookProgress || 0 : this.userProgress?.progress || 0
return Math.max(Math.min(1, progressPercent), 0)
},
userProgressLastUpdated() {
if (!this.userProgress) return null
return this.userProgress.lastUpdate
},
userProgressStartedDate() {
if (!this.userProgress) return null
return this.userProgress.startedAt
},
userProgressFinishedDate() {
if (!this.userProgress) return null
return this.userProgress.finishedAt
},
itemIsFinished() {
if (this.booksInSeries) return this.seriesIsFinished
return this.userProgress ? !!this.userProgress.isFinished : false
@@ -760,11 +788,11 @@ export default {
},
showEditModalFiles() {
// More menu func
this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'files' })
this.$emit('edit', this.libraryItem, 'files')
},
showEditModalMatch() {
// More menu func
this.store.commit('showEditModalOnTab', { libraryItem: this.libraryItem, tab: 'match' })
this.$emit('edit', this.libraryItem, 'match')
},
sendToDevice(deviceName) {
// More menu func
@@ -338,6 +338,18 @@ export default {
const series = this.series.find((se) => se.id == decoded)
if (series) filterValue = series.name
}
} else if (parts[0] === 'progress') {
const item = this.progress.find((p) => p.id == decoded)
if (item) filterValue = item.name
} else if (parts[0] === 'tracks') {
const item = this.tracks.find((t) => t.id == decoded)
if (item) filterValue = item.name
} else if (parts[0] === 'ebooks') {
const item = this.ebooks.find((e) => e.id == decoded)
if (item) filterValue = item.name
} else if (parts[0] === 'missing') {
const item = this.missing.find((m) => m.id == decoded)
if (item) filterValue = item.name
} else {
filterValue = decoded
}
@@ -7,7 +7,7 @@
</span>
</button>
<ul v-show="showMenu" class="absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 ring-1 ring-black/5 overflow-auto focus:outline-hidden text-sm" role="menu">
<ul v-show="showMenu" class="librarySortMenu absolute z-10 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-96 rounded-md py-1 ring-1 ring-black/5 overflow-auto focus:outline-hidden text-sm" role="menu">
<template v-for="item in selectItems">
<li :key="item.value" class="select-none relative py-2 pr-9 cursor-pointer hover:bg-white/5" :class="item.value === selected ? 'bg-white/5 text-yellow-400' : 'text-gray-200 hover:text-white'" role="menuitem" @click="clickedOption(item.value)">
<div class="flex items-center">
@@ -130,6 +130,18 @@ export default {
text: this.$strings.LabelFileModified,
value: 'mtimeMs'
},
{
text: this.$strings.LabelLibrarySortByProgress,
value: 'progress'
},
{
text: this.$strings.LabelLibrarySortByProgressStarted,
value: 'progress.createdAt'
},
{
text: this.$strings.LabelLibrarySortByProgressFinished,
value: 'progress.finishedAt'
},
{
text: this.$strings.LabelRandomly,
value: 'random'
@@ -191,3 +203,9 @@ export default {
}
}
</script>
<style scoped>
.librarySortMenu {
max-height: calc(100vh - 125px);
}
</style>
-3
View File
@@ -39,9 +39,6 @@ export default {
}
},
computed: {
userToken() {
return this.$store.getters['user/getToken']
},
_author() {
return this.author || {}
},
+3 -6
View File
@@ -309,9 +309,9 @@ export default {
} else {
console.log('Account updated', data.user)
if (data.user.id === this.user.id && data.user.token !== this.user.token) {
console.log('Current user token was updated')
this.$store.commit('user/setUserToken', data.user.token)
if (data.user.id === this.user.id && data.user.accessToken !== this.user.accessToken) {
console.log('Current user access token was updated')
this.$store.commit('user/setAccessToken', data.user.accessToken)
}
this.$toast.success(this.$strings.ToastAccountUpdateSuccess)
@@ -351,9 +351,6 @@ export default {
this.$toast.error(errMsg || 'Failed to create account')
})
},
toggleActive() {
this.newUser.isActive = !this.newUser.isActive
},
userTypeUpdated(type) {
this.newUser.permissions = {
download: type !== 'guest',
@@ -0,0 +1,60 @@
<template>
<modals-modal ref="modal" v-model="show" name="api-key-created" :width="800" :height="'unset'" persistent>
<template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
<p class="text-3xl text-white truncate">{{ title }}</p>
</div>
</template>
<form @submit.prevent="submitForm">
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 overflow-y-auto overflow-x-hidden" style="min-height: 200px; max-height: 80vh">
<div class="w-full p-8">
<p class="text-lg text-white mb-4">{{ $getString('LabelApiKeyCreated', [apiKeyName]) }}</p>
<p class="text-lg text-white mb-4">{{ $strings.LabelApiKeyCreatedDescription }}</p>
<ui-text-input label="API Key" :value="apiKeyKey" readonly show-copy />
<div class="flex justify-end mt-4">
<ui-btn color="bg-primary" @click="show = false">{{ $strings.ButtonClose }}</ui-btn>
</div>
</div>
</div>
</form>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean,
apiKey: {
type: Object,
default: () => null
}
},
data() {
return {}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
title() {
return this.$strings.HeaderNewApiKey
},
apiKeyName() {
return this.apiKey?.name || ''
},
apiKeyKey() {
return this.apiKey?.apiKey || ''
}
},
methods: {},
mounted() {}
}
</script>
+198
View File
@@ -0,0 +1,198 @@
<template>
<modals-modal ref="modal" v-model="show" name="api-key" :width="800" :height="'unset'" :processing="processing">
<template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden">
<p class="text-3xl text-white truncate">{{ title }}</p>
</div>
</template>
<form @submit.prevent="submitForm">
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300 overflow-y-auto overflow-x-hidden" style="min-height: 400px; max-height: 80vh">
<div class="w-full p-8">
<div class="flex py-2">
<div class="w-1/2 px-2">
<ui-text-input-with-label v-model.trim="newApiKey.name" :readonly="!isNew" :label="$strings.LabelName" />
</div>
<div v-if="isNew" class="w-1/2 px-2">
<ui-text-input-with-label v-model.trim="newApiKey.expiresIn" :label="$strings.LabelExpiresInSeconds" type="number" :min="0" />
</div>
</div>
<div class="flex items-center pt-4 pb-2 gap-2">
<div class="flex items-center px-2">
<p class="px-3 font-semibold" id="user-enabled-toggle">{{ $strings.LabelEnable }}</p>
<ui-toggle-switch :disabled="isExpired && !apiKey.isActive" labeledBy="user-enabled-toggle" v-model="newApiKey.isActive" />
</div>
<div v-if="isExpired" class="px-2">
<p class="text-sm text-error">{{ $strings.LabelExpired }}</p>
</div>
</div>
<div class="w-full border-t border-b border-black-200 py-4 px-3 mt-4">
<p class="text-lg mb-2 font-semibold">{{ $strings.LabelApiKeyUser }}</p>
<p class="text-sm mb-2 text-gray-400">{{ $strings.LabelApiKeyUserDescription }}</p>
<ui-select-input v-model="newApiKey.userId" :disabled="isExpired && !apiKey.isActive" :items="userItems" :placeholder="$strings.LabelSelectUser" :label="$strings.LabelApiKeyUser" label-hidden />
</div>
<div class="flex pt-4 px-2">
<div class="grow" />
<ui-btn color="bg-success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
</div>
</div>
</div>
</form>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean,
apiKey: {
type: Object,
default: () => null
},
users: {
type: Array,
default: () => []
}
},
data() {
return {
processing: false,
newApiKey: {},
isNew: true
}
},
watch: {
show: {
handler(newVal) {
if (newVal) {
this.init()
}
}
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
},
title() {
return this.isNew ? this.$strings.HeaderNewApiKey : this.$strings.HeaderUpdateApiKey
},
userItems() {
return this.users
.filter((u) => {
// Only show root user if the current user is root
return u.type !== 'root' || this.$store.getters['user/getIsRoot']
})
.map((u) => ({ text: u.username, value: u.id, subtext: u.type }))
},
isExpired() {
if (!this.apiKey || !this.apiKey.expiresAt) return false
return new Date(this.apiKey.expiresAt).getTime() < Date.now()
}
},
methods: {
submitForm() {
if (!this.newApiKey.name) {
this.$toast.error(this.$strings.ToastNameRequired)
return
}
if (!this.newApiKey.userId) {
this.$toast.error(this.$strings.ToastNewApiKeyUserError)
return
}
if (this.isNew) {
this.submitCreateApiKey()
} else {
this.submitUpdateApiKey()
}
},
submitUpdateApiKey() {
if (this.newApiKey.isActive === this.apiKey.isActive && this.newApiKey.userId === this.apiKey.userId) {
this.$toast.info(this.$strings.ToastNoUpdatesNecessary)
this.show = false
return
}
const apiKey = {
isActive: this.newApiKey.isActive,
userId: this.newApiKey.userId
}
this.processing = true
this.$axios
.$patch(`/api/api-keys/${this.apiKey.id}`, apiKey)
.then((data) => {
this.processing = false
if (data.error) {
this.$toast.error(`${this.$strings.ToastFailedToUpdate}: ${data.error}`)
} else {
this.show = false
this.$emit('updated', data.apiKey)
}
})
.catch((error) => {
this.processing = false
console.error('Failed to update apiKey', error)
var errMsg = error.response ? error.response.data || '' : ''
this.$toast.error(errMsg || this.$strings.ToastFailedToUpdate)
})
},
submitCreateApiKey() {
const apiKey = { ...this.newApiKey }
if (this.newApiKey.expiresIn) {
apiKey.expiresIn = parseInt(this.newApiKey.expiresIn)
} else {
delete apiKey.expiresIn
}
this.processing = true
this.$axios
.$post('/api/api-keys', apiKey)
.then((data) => {
this.processing = false
if (data.error) {
this.$toast.error(this.$strings.ToastFailedToCreate + ': ' + data.error)
} else {
this.show = false
this.$emit('created', data.apiKey)
}
})
.catch((error) => {
this.processing = false
console.error('Failed to create apiKey', error)
var errMsg = error.response ? error.response.data || '' : ''
this.$toast.error(errMsg || this.$strings.ToastFailedToCreate)
})
},
init() {
this.isNew = !this.apiKey
if (this.apiKey) {
this.newApiKey = {
name: this.apiKey.name,
isActive: this.apiKey.isActive,
userId: this.apiKey.userId
}
} else {
this.newApiKey = {
name: null,
expiresIn: null,
isActive: true,
userId: null
}
}
}
},
mounted() {}
}
</script>
@@ -88,7 +88,7 @@ export default {
},
providers() {
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
return this.$store.state.scanners.providers
return this.$store.state.scanners.bookProviders
},
libraryProvider() {
return this.$store.getters['libraries/getLibraryProvider'](this.currentLibraryId) || 'google'
@@ -96,6 +96,9 @@ export default {
},
methods: {
init() {
// Fetch providers when modal is shown
this.$store.dispatch('scanners/fetchProviders')
// If we don't have a set provider (first open of dialog) or we've switched library, set
// the selected provider to the current library default provider
if (!this.options.provider || this.lastUsedLibrary != this.currentLibraryId) {
@@ -127,8 +130,7 @@ export default {
this.show = false
})
}
},
mounted() {}
}
}
</script>
@@ -81,7 +81,7 @@
</div>
<div class="w-full md:w-1/3">
<p v-if="!isMediaItemShareSession" class="font-semibold uppercase text-xs text-gray-400 tracking-wide mb-2 mt-6 md:mt-0">{{ $strings.LabelUser }}</p>
<p v-if="!isMediaItemShareSession" class="mb-1 text-xs">{{ _session.userId }}</p>
<p v-if="!isMediaItemShareSession" class="mb-1 text-xs">{{ username }}</p>
<p class="font-semibold uppercase text-xs text-gray-400 tracking-wide mt-6 mb-2">{{ $strings.LabelMediaPlayer }}</p>
<p class="mb-1">{{ playMethodName }}</p>
@@ -132,6 +132,9 @@ export default {
_session() {
return this.session || {}
},
username() {
return this._session.user?.username || this._session.userId || ''
},
deviceInfo() {
return this._session.deviceInfo || {}
},
+2 -2
View File
@@ -23,7 +23,7 @@ export default {
processing: Boolean,
persistent: {
type: Boolean,
default: true
default: false
},
width: {
type: [String, Number],
@@ -99,7 +99,7 @@ export default {
this.preventClickoutside = false
return
}
if (this.processing && this.persistent) return
if (this.processing || this.persistent) return
if (ev.srcElement && ev.srcElement.classList.contains('modal-bg')) {
this.show = false
}
+1 -1
View File
@@ -8,7 +8,7 @@
<div class="px-6 py-8 w-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 overflow-y-auto overflow-x-hidden" style="max-height: 80vh">
<div class="absolute top-0 right-0 p-4">
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/media-item-shares" target="_blank" class="inline-flex">
<a href="https://audiobookshelf.org/docs/documentation/libraries/common-content/public-shares#media-item-shares" target="_blank" class="inline-flex">
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
</a>
</ui-tooltip>
@@ -25,7 +25,7 @@
<div class="text-sm flex items-center justify-center text-gray-200">
<p>{{ $strings.MessageBookshelfNoCollectionsHelp }}</p>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/collections" target="_blank" class="inline-flex">
<a href="https://audiobookshelf.org/docs/documentation/libraries/common-content/playlists" target="_blank" class="inline-flex">
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
</a>
</ui-tooltip>
@@ -227,7 +227,7 @@ export default {
.catch((error) => {
console.error('Failed to create collection', error)
var errMsg = error.response ? error.response.data || '' : ''
this.$toast.error(this.$strings.ToastCollectionCreateFailed + ': ' + errMsg)
this.$toast.error(errMsg)
this.processing = false
})
}
+148 -20
View File
@@ -51,19 +51,21 @@
<form @submit.prevent="submitSearchForm">
<div class="flex flex-wrap sm:flex-nowrap items-center justify-start -mx-1">
<div class="w-48 grow p-1">
<ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelProvider" small />
<ui-dropdown v-model="provider" :items="providers" :disabled="searchInProgress" :label="$strings.LabelProvider" small />
</div>
<div class="w-72 grow p-1">
<ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" :placeholder="$strings.PlaceholderSearch" />
<ui-text-input-with-label v-model="searchTitle" :disabled="searchInProgress" :label="searchTitleLabel" :placeholder="$strings.PlaceholderSearch" />
</div>
<div v-show="provider != 'itunes' && provider != 'audiobookcovers'" class="w-72 grow p-1">
<ui-text-input-with-label v-model="searchAuthor" :label="$strings.LabelAuthor" />
<ui-text-input-with-label v-model="searchAuthor" :disabled="searchInProgress" :label="$strings.LabelAuthor" />
</div>
<ui-btn class="mt-5 ml-1 md:min-w-24" :padding-x="4" type="submit">{{ $strings.ButtonSearch }}</ui-btn>
<ui-btn v-if="!searchInProgress" class="mt-5 ml-1 md:min-w-24" :padding-x="4" type="submit">{{ $strings.ButtonSearch }}</ui-btn>
<ui-btn v-else class="mt-5 ml-1 md:min-w-24" :padding-x="4" type="button" color="bg-error" @click.prevent="cancelCurrentSearch">{{ $strings.ButtonCancel }}</ui-btn>
</div>
</form>
<div v-if="hasSearched" class="flex items-center flex-wrap justify-center sm:max-h-80 sm:overflow-y-scroll mt-2 max-w-full">
<p v-if="!coversFound.length">{{ $strings.MessageNoCoversFound }}</p>
<p v-if="searchInProgress && !coversFound.length" class="text-gray-300 py-4">{{ $strings.MessageLoading }}</p>
<p v-else-if="!searchInProgress && !coversFound.length" class="text-gray-300 py-4">{{ $strings.MessageNoCoversFound }}</p>
<template v-for="cover in coversFound">
<div :key="cover" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === coverPath ? 'border-yellow-300' : ''" @click="updateCover(cover)">
<covers-preview-cover :src="cover" :width="80" show-open-new-tab :book-cover-aspect-ratio="bookCoverAspectRatio" />
@@ -105,7 +107,10 @@ export default {
showLocalCovers: false,
previewUpload: null,
selectedFile: null,
provider: 'google'
provider: 'google',
currentSearchRequestId: null,
searchInProgress: false,
socketListenersActive: false
}
},
watch: {
@@ -128,8 +133,8 @@ export default {
}
},
providers() {
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
return [{ text: 'All', value: 'all' }, ...this.$store.state.scanners.providers, ...this.$store.state.scanners.coverOnlyProviders]
if (this.isPodcast) return this.$store.state.scanners.podcastCoverProviders
return this.$store.state.scanners.bookCoverProviders
},
searchTitleLabel() {
if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN
@@ -186,6 +191,9 @@ export default {
_file.localPath = `${process.env.serverUrl}/api/items/${this.libraryItemId}/file/${file.ino}?token=${this.userToken}`
return _file
})
},
socket() {
return this.$root.socket
}
},
methods: {
@@ -235,7 +243,19 @@ export default {
this.searchTitle = this.mediaMetadata.title || ''
this.searchAuthor = this.mediaMetadata.authorName || ''
if (this.isPodcast) this.provider = 'itunes'
else this.provider = localStorage.getItem('book-cover-provider') || localStorage.getItem('book-provider') || 'google'
else {
// Migrate from 'all' to 'best' (only once)
const migrationKey = 'book-cover-provider-migrated'
const currentProvider = localStorage.getItem('book-cover-provider') || localStorage.getItem('book-provider') || 'google'
if (!localStorage.getItem(migrationKey) && currentProvider === 'all') {
localStorage.setItem('book-cover-provider', 'best')
localStorage.setItem(migrationKey, 'true')
this.provider = 'best'
} else {
this.provider = currentProvider
}
}
},
removeCover() {
if (!this.coverPath) {
@@ -291,22 +311,116 @@ export default {
console.error('PersistProvider', error)
}
},
generateRequestId() {
return `cover-search-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
},
addSocketListeners() {
if (!this.socket || this.socketListenersActive) return
this.socket.on('cover_search_result', this.handleSearchResult)
this.socket.on('cover_search_complete', this.handleSearchComplete)
this.socket.on('cover_search_error', this.handleSearchError)
this.socket.on('cover_search_provider_error', this.handleProviderError)
this.socket.on('cover_search_cancelled', this.handleSearchCancelled)
this.socket.on('disconnect', this.handleSocketDisconnect)
this.socketListenersActive = true
},
removeSocketListeners() {
if (!this.socket || !this.socketListenersActive) return
this.socket.off('cover_search_result', this.handleSearchResult)
this.socket.off('cover_search_complete', this.handleSearchComplete)
this.socket.off('cover_search_error', this.handleSearchError)
this.socket.off('cover_search_provider_error', this.handleProviderError)
this.socket.off('cover_search_cancelled', this.handleSearchCancelled)
this.socket.off('disconnect', this.handleSocketDisconnect)
this.socketListenersActive = false
},
handleSearchResult(data) {
if (data.requestId !== this.currentSearchRequestId) return
// Add new covers to the list (avoiding duplicates)
const newCovers = data.covers.filter((cover) => !this.coversFound.includes(cover))
this.coversFound.push(...newCovers)
},
handleSearchComplete(data) {
if (data.requestId !== this.currentSearchRequestId) return
this.searchInProgress = false
this.currentSearchRequestId = null
},
handleSearchError(data) {
if (data.requestId !== this.currentSearchRequestId) return
console.error('[Cover Search] Search error:', data.error)
this.$toast.error(this.$strings.ToastCoverSearchFailed)
this.searchInProgress = false
this.currentSearchRequestId = null
},
handleProviderError(data) {
if (data.requestId !== this.currentSearchRequestId) return
console.warn(`[Cover Search] Provider ${data.provider} failed:`, data.error)
},
handleSearchCancelled(data) {
if (data.requestId !== this.currentSearchRequestId) return
this.searchInProgress = false
this.currentSearchRequestId = null
},
handleSocketDisconnect() {
// If we were in the middle of a search, cancel it (server can't send results anymore)
if (this.searchInProgress && this.currentSearchRequestId) {
this.searchInProgress = false
this.currentSearchRequestId = null
}
},
cancelCurrentSearch() {
if (!this.currentSearchRequestId || !this.socket?.connected) {
console.error('[Cover Search] Socket not connected')
this.$toast.error(this.$strings.ToastConnectionNotAvailable)
return
}
this.socket.emit('cancel_cover_search', this.currentSearchRequestId)
this.currentSearchRequestId = null
this.searchInProgress = false
},
async submitSearchForm() {
if (!this.socket?.connected) {
console.error('[Cover Search] Socket not connected')
this.$toast.error(this.$strings.ToastConnectionNotAvailable)
return
}
// Cancel any existing search
if (this.searchInProgress) {
this.cancelCurrentSearch()
}
// Store provider in local storage
this.persistProvider()
this.isProcessing = true
const searchQuery = this.getSearchQuery()
const results = await this.$axios
.$get(`/api/search/covers?${searchQuery}`)
.then((res) => res.results)
.catch((error) => {
console.error('Failed', error)
return []
})
this.coversFound = results
this.isProcessing = false
// Setup socket listeners if not already done
this.addSocketListeners()
// Clear previous results
this.coversFound = []
this.hasSearched = true
this.searchInProgress = true
// Generate unique request ID
const requestId = this.generateRequestId()
this.currentSearchRequestId = requestId
// Emit search request via WebSocket
this.socket.emit('search_covers', {
requestId,
title: this.searchTitle,
author: this.searchAuthor || '',
provider: this.provider,
podcast: this.isPodcast
})
},
setCover(coverFile) {
this.isProcessing = true
@@ -320,6 +434,20 @@ export default {
this.isProcessing = false
})
}
},
mounted() {
// Setup socket listeners when component is mounted
this.addSocketListeners()
// Fetch providers if not already loaded
this.$store.dispatch('scanners/fetchProviders')
},
beforeDestroy() {
// Cancel any ongoing search when component is destroyed
if (this.searchInProgress) {
this.cancelCurrentSearch()
}
// Remove socket listeners
this.removeSocketListeners()
}
}
</script>
@@ -29,9 +29,6 @@ export default {
media() {
return this.libraryItem.media || {}
},
userToken() {
return this.$store.getters['user/getToken']
},
userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate']
},
+50 -21
View File
@@ -2,7 +2,7 @@
<div id="match-wrapper" class="w-full h-full overflow-hidden px-2 md:px-4 py-4 md:py-6 relative">
<form @submit.prevent="submitSearch">
<div class="flex flex-wrap md:flex-nowrap items-center justify-start -mx-1">
<div class="w-36 px-1">
<div v-if="providersLoaded" class="w-36 px-1">
<ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelProvider" small />
</div>
<div class="grow md:w-72 px-1">
@@ -77,8 +77,8 @@
<ui-checkbox v-model="selectedMatchUsage.author" checkbox-bg="bg" @input="checkboxToggled" />
<div class="grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.author" :disabled="!selectedMatchUsage.author" :label="$strings.LabelAuthor" />
<p v-if="mediaMetadata.authorName" class="text-xs ml-1 text-white/60">
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('author', mediaMetadata.authorName)">{{ mediaMetadata.authorName }}</a>
<p v-if="mediaMetadata.authorName || (isPodcast && mediaMetadata.author)" class="text-xs ml-1 text-white/60">
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('author', isPodcast ? mediaMetadata.author : mediaMetadata.authorName)">{{ isPodcast ? mediaMetadata.author : mediaMetadata.authorName }}</a>
</p>
</div>
</div>
@@ -87,7 +87,7 @@
<div class="grow ml-4">
<ui-multi-select v-model="selectedMatch.narrator" :items="narrators" :disabled="!selectedMatchUsage.narrator" :label="$strings.LabelNarrators" />
<p v-if="mediaMetadata.narratorName" class="text-xs ml-1 text-white/60">
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('narrator', mediaMetadata.narrators)">{{ mediaMetadata.narratorName }}</a>
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('narrator', mediaMetadata.narrators)">{{ mediaMetadata.narratorName }}</a>
</p>
</div>
</div>
@@ -96,7 +96,7 @@
<div class="grow ml-4">
<ui-rich-text-editor v-model="selectedMatch.description" :disabled="!selectedMatchUsage.description" :label="$strings.LabelDescription" />
<p v-if="mediaMetadata.description" class="text-xs ml-1 text-white/60">
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('description', mediaMetadata.description)">{{ mediaMetadata.descriptionPlain.substr(0, 100) + (mediaMetadata.descriptionPlain.length > 100 ? '...' : '') }}</a>
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('description', mediaMetadata.description)">{{ mediaMetadata.descriptionPlain.substr(0, 100) + (mediaMetadata.descriptionPlain.length > 100 ? '...' : '') }}</a>
</p>
</div>
</div>
@@ -105,7 +105,7 @@
<div class="grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.publisher" :disabled="!selectedMatchUsage.publisher" :label="$strings.LabelPublisher" />
<p v-if="mediaMetadata.publisher" class="text-xs ml-1 text-white/60">
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publisher', mediaMetadata.publisher)">{{ mediaMetadata.publisher }}</a>
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publisher', mediaMetadata.publisher)">{{ mediaMetadata.publisher }}</a>
</p>
</div>
</div>
@@ -114,7 +114,7 @@
<div class="grow ml-4">
<ui-text-input-with-label v-model="selectedMatch.publishedYear" :disabled="!selectedMatchUsage.publishedYear" :label="$strings.LabelPublishYear" />
<p v-if="mediaMetadata.publishedYear" class="text-xs ml-1 text-white/60">
{{ $strings.LabelCurrently }} <a title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publishedYear', mediaMetadata.publishedYear)">{{ mediaMetadata.publishedYear }}</a>
{{ $strings.LabelCurrently }} <a :title="$strings.LabelClickToUseCurrentValue" class="cursor-pointer hover:underline" @click.stop="setMatchFieldValue('publishedYear', mediaMetadata.publishedYear)">{{ mediaMetadata.publishedYear }}</a>
</p>
</div>
</div>
@@ -253,6 +253,7 @@ export default {
hasSearched: false,
selectedMatch: null,
selectedMatchOrig: null,
waitingForProviders: false,
selectedMatchUsage: {
title: true,
subtitle: true,
@@ -285,9 +286,19 @@ export default {
handler(newVal) {
if (newVal) this.init()
}
},
providersLoaded(isLoaded) {
// Complete initialization once providers are loaded
if (isLoaded && this.waitingForProviders) {
this.waitingForProviders = false
this.initProviderAndSearch()
}
}
},
computed: {
providersLoaded() {
return this.$store.getters['scanners/areProvidersLoaded']
},
isProcessing: {
get() {
return this.processing
@@ -319,7 +330,7 @@ export default {
},
providers() {
if (this.isPodcast) return this.$store.state.scanners.podcastProviders
return this.$store.state.scanners.providers
return this.$store.state.scanners.bookProviders
},
searchTitleLabel() {
if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN
@@ -400,7 +411,9 @@ export default {
this.$toast.warning(this.$strings.ToastTitleRequired)
return
}
this.persistProvider()
if (!this.isPodcast) {
this.persistProvider()
}
this.runSearch()
},
async runSearch() {
@@ -476,6 +489,24 @@ export default {
this.checkboxToggled()
},
initProviderAndSearch() {
// Set provider based on media type
if (this.isPodcast) {
this.provider = 'itunes'
} else {
this.provider = this.getDefaultBookProvider()
}
// Prefer using ASIN if set and using audible provider
if (this.provider.startsWith('audible') && this.libraryItem.media.metadata.asin) {
this.searchTitle = this.libraryItem.media.metadata.asin
this.searchAuthor = ''
}
if (this.searchTitle) {
this.submitSearch()
}
},
init() {
this.clearSelectedMatch()
this.initSelectedMatchUsage()
@@ -493,19 +524,13 @@ export default {
}
this.searchTitle = this.libraryItem.media.metadata.title
this.searchAuthor = this.libraryItem.media.metadata.authorName || ''
if (this.isPodcast) this.provider = 'itunes'
else {
this.provider = this.getDefaultBookProvider()
}
// Prefer using ASIN if set and using audible provider
if (this.provider.startsWith('audible') && this.libraryItem.media.metadata.asin) {
this.searchTitle = this.libraryItem.media.metadata.asin
this.searchAuthor = ''
}
if (this.searchTitle) {
this.submitSearch()
// Wait for providers to be loaded before setting provider and searching
if (this.providersLoaded || this.isPodcast) {
this.waitingForProviders = false
this.initProviderAndSearch()
} else {
this.waitingForProviders = true
}
},
selectMatch(match) {
@@ -635,6 +660,10 @@ export default {
this.selectedMatch = null
this.selectedMatchOrig = null
}
},
mounted() {
// Fetch providers if not already loaded
this.$store.dispatch('scanners/fetchProviders')
}
}
</script>
@@ -158,6 +158,8 @@ export default {
this.isProcessing = true
var updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, updatePayload).catch((error) => {
console.error('Failed to update', error)
const errorMessage = typeof error?.response?.data === 'string' ? error?.response?.data : null
this.$toast.error(errorMessage || this.$strings.ToastFailedToUpdate)
return false
})
this.isProcessing = false
@@ -107,6 +107,7 @@ export default {
quickEmbed() {
const payload = {
message: this.$strings.MessageConfirmQuickEmbed,
allowHtml: true,
callback: (confirmed) => {
if (confirmed) {
this.$axios
@@ -74,7 +74,7 @@ export default {
},
providers() {
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
return this.$store.state.scanners.providers
return this.$store.state.scanners.bookProviders
}
},
methods: {
@@ -156,6 +156,8 @@ export default {
},
mounted() {
this.init()
// Fetch providers if not already loaded
this.$store.dispatch('scanners/fetchProviders')
}
}
</script>
@@ -8,7 +8,7 @@
<div class="flex items-center justify-between md:justify-start mb-4">
<p class="text-sm text-gray-300 pr-2">{{ $strings.LabelMetadataOrderOfPrecedenceDescription }}</p>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex">
<a href="https://www.audiobookshelf.org/guides/book-scanner" target="_blank" class="inline-flex">
<a href="https://audiobookshelf.org/docs/documentation/libraries/book-library/book-metadata" target="_blank" class="inline-flex">
<span class="material-symbols text-xl w-5">help_outline</span>
</a>
</ui-tooltip>
@@ -104,7 +104,6 @@ export default {
},
data() {
return {
provider: null,
useSquareBookCovers: false,
enableWatcher: false,
skipMatchingMediaWithAsin: false,
@@ -134,10 +133,6 @@ export default {
isPodcastLibrary() {
return this.mediaType === 'podcast'
},
providers() {
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
return this.$store.state.scanners.providers
},
maskAsFinishedWhenItems() {
return [
{
@@ -25,7 +25,7 @@
<div class="text-sm flex items-center justify-center text-gray-200">
<p>{{ $strings.MessageNoUserPlaylistsHelp }}</p>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/collections" target="_blank" class="inline-flex">
<a href="https://audiobookshelf.org/docs/documentation/libraries/common-content/playlists" target="_blank" class="inline-flex">
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
</a>
</ui-tooltip>
@@ -97,7 +97,10 @@ export default {
...playlist
}
})
.sort((a, b) => (a.isItemIncluded ? -1 : 1))
.sort((a, b) => {
if (a.isItemIncluded !== b.isItemIncluded) return a.isItemIncluded ? -1 : 1
return a.name.localeCompare(b.name)
})
},
isBatch() {
return this.selectedPlaylistItems.length > 1
@@ -94,7 +94,6 @@ export default {
}
this.processing = false
this.$toast.success(`${this.episodes.length} episode${this.episodes.length > 1 ? 's' : ''} removed`)
this.show = false
this.$emit('clearSelected')
}
@@ -114,7 +114,7 @@ export default {
.$patch(`/api/podcasts/${this.libraryItem.id}/episode/${this.episodeId}`, updatePayload)
.then(() => {
this.isProcessing = false
this.$toast.success('Podcast episode updated')
this.$toast.success(this.$strings.ToastPodcastEpisodeUpdated)
this.$emit('selectTab', 'details')
})
.catch((error) => {
@@ -8,7 +8,7 @@
</button>
</ui-tooltip>
<ui-tooltip direction="top" :text="jumpBackwardText">
<button :aria-label="jumpForwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
<button :aria-label="jumpBackwardText" class="text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
<span class="material-symbols text-2xl sm:text-3xl">replay</span>
</button>
</ui-tooltip>
-3
View File
@@ -129,9 +129,6 @@ export default {
return `${hoursRounded}h`
}
},
token() {
return this.$store.getters['user/getToken']
},
timeRemaining() {
if (this.useChapterTrack && this.currentChapter) {
var currChapTime = this.currentTime - this.currentChapter.start
+21 -1
View File
@@ -3,7 +3,8 @@
<div class="absolute top-0 left-0 right-0 w-full h-36 bg-linear-to-t from-transparent via-black-500 to-black-700 opacity-90 pointer-events-none" />
<div ref="content" class="relative text-white" :style="{ height: modalHeight, width: modalWidth }" v-click-outside="clickedOutside">
<div class="px-4 w-full text-sm py-6 rounded-lg bg-bg shadow-lg border border-black-300">
<p id="confirm-prompt-message" class="text-lg mb-6 mt-2 px-1" v-html="message" />
<p v-if="allowHtmlMessage" id="confirm-prompt-message" class="text-lg mb-6 mt-2 px-1" v-html="sanitizedMessage" />
<p v-else id="confirm-prompt-message" class="text-lg mb-6 mt-2 px-1">{{ message }}</p>
<ui-checkbox v-if="checkboxLabel" v-model="checkboxValue" checkbox-bg="bg" :label="checkboxLabel" label-class="pl-2 text-base" class="mb-6 px-1" />
@@ -52,6 +53,17 @@ export default {
message() {
return this.confirmPromptOptions.message || ''
},
allowHtmlMessage() {
return !!this.confirmPromptOptions.allowHtml
},
sanitizedMessage() {
if (!this.allowHtmlMessage) return this.message
return this.escapeHtml(this.message)
.replace(/&lt;br\s*\/?&gt;/gi, '<br>')
.replace(/&lt;code&gt;/gi, '<code>')
.replace(/&lt;\/code&gt;/gi, '</code>')
},
callback() {
return this.confirmPromptOptions.callback
},
@@ -103,6 +115,14 @@ export default {
if (this.callback) this.callback(true, this.checkboxValue)
this.show = false
},
escapeHtml(value) {
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
},
setShow() {
this.checkboxValue = this.checkboxDefaultValue
this.$eventBus.$emit('showing-prompt', true)
+1 -7
View File
@@ -104,9 +104,6 @@ export default {
}
},
computed: {
userToken() {
return this.$store.getters['user/getToken']
},
libraryItemId() {
return this.libraryItem?.id
},
@@ -234,10 +231,7 @@ export default {
async extract() {
this.loading = true
var buff = await this.$axios.$get(this.ebookUrl, {
responseType: 'blob',
headers: {
Authorization: `Bearer ${this.userToken}`
}
responseType: 'blob'
})
const archive = await Archive.open(buff)
const originalFilesObject = await archive.getFilesObject()
+59 -38
View File
@@ -57,9 +57,6 @@ export default {
}
},
computed: {
userToken() {
return this.$store.getters['user/getToken']
},
/** @returns {string} */
libraryItemId() {
return this.libraryItem?.id
@@ -97,27 +94,37 @@ export default {
},
ebookUrl() {
if (this.fileId) {
return `${this.$config.routerBasePath}/api/items/${this.libraryItemId}/ebook/${this.fileId}`
return `/api/items/${this.libraryItemId}/ebook/${this.fileId}`
}
return `${this.$config.routerBasePath}/api/items/${this.libraryItemId}/ebook`
return `/api/items/${this.libraryItemId}/ebook`
},
themeRules() {
const isDark = this.ereaderSettings.theme === 'dark'
const fontColor = isDark ? '#fff' : '#000'
const backgroundColor = isDark ? 'rgb(35 35 35)' : 'rgb(255, 255, 255)'
const theme = this.ereaderSettings.theme
const isDark = theme === 'dark'
const isSepia = theme === 'sepia'
const fontColor = isDark
? '#fff'
: isSepia
? '#5b4636'
: '#000'
const backgroundColor = isDark
? 'rgb(35 35 35)'
: isSepia
? 'rgb(244, 236, 216)'
: 'rgb(255, 255, 255)'
const lineSpacing = this.ereaderSettings.lineSpacing / 100
const fontScale = this.ereaderSettings.fontScale / 100
const textStroke = this.ereaderSettings.textStroke / 100
const fontScale = this.ereaderSettings.fontScale / 100
const textStroke = this.ereaderSettings.textStroke / 100
return {
'*': {
color: `${fontColor}!important`,
'background-color': `${backgroundColor}!important`,
'line-height': lineSpacing * fontScale + 'rem!important',
'-webkit-text-stroke': textStroke + 'px ' + fontColor + '!important'
'line-height': `${lineSpacing * fontScale}rem!important`,
'-webkit-text-stroke': `${textStroke}px ${fontColor}!important`
},
a: {
color: `${fontColor}!important`
@@ -309,14 +316,24 @@ export default {
/** @type {EpubReader} */
const reader = this
// Use axios to make request because we have token refresh logic in interceptor
const customRequest = async (url) => {
try {
return this.$axios.$get(url, {
responseType: 'arraybuffer'
})
} catch (error) {
console.error('EpubReader.initEpub customRequest failed:', error)
throw error
}
}
/** @type {ePub.Book} */
reader.book = new ePub(reader.ebookUrl, {
width: this.readerWidth,
height: this.readerHeight - 50,
openAs: 'epub',
requestHeaders: {
Authorization: `Bearer ${this.userToken}`
}
requestMethod: customRequest
})
/** @type {ePub.Rendition} */
@@ -337,29 +354,33 @@ export default {
this.applyTheme()
})
reader.book.ready.then(() => {
// set up event listeners
reader.rendition.on('relocated', reader.relocated)
reader.rendition.on('keydown', reader.keyUp)
reader.book.ready
.then(() => {
// set up event listeners
reader.rendition.on('relocated', reader.relocated)
reader.rendition.on('keydown', reader.keyUp)
reader.rendition.on('touchstart', (event) => {
this.$emit('touchstart', event)
})
reader.rendition.on('touchend', (event) => {
this.$emit('touchend', event)
})
// load ebook cfi locations
const savedLocations = this.loadLocations()
if (savedLocations) {
reader.book.locations.load(savedLocations)
} else {
reader.book.locations.generate().then(() => {
this.checkSaveLocations(reader.book.locations.save())
reader.rendition.on('touchstart', (event) => {
this.$emit('touchstart', event)
})
}
this.getChapters()
})
reader.rendition.on('touchend', (event) => {
this.$emit('touchend', event)
})
// load ebook cfi locations
const savedLocations = this.loadLocations()
if (savedLocations) {
reader.book.locations.load(savedLocations)
} else {
reader.book.locations.generate().then(() => {
this.checkSaveLocations(reader.book.locations.save())
})
}
this.getChapters()
})
.catch((error) => {
console.error('EpubReader.initEpub failed:', error)
})
},
getChapters() {
// Load the list of chapters in the book. See https://github.com/futurepress/epub.js/issues/759
+2 -8
View File
@@ -26,9 +26,6 @@ export default {
return {}
},
computed: {
userToken() {
return this.$store.getters['user/getToken']
},
libraryItemId() {
return this.libraryItem?.id
},
@@ -96,11 +93,8 @@ export default {
},
async initMobi() {
// Fetch mobi file as blob
var buff = await this.$axios.$get(this.ebookUrl, {
responseType: 'blob',
headers: {
Authorization: `Bearer ${this.userToken}`
}
const buff = await this.$axios.$get(this.ebookUrl, {
responseType: 'blob'
})
var reader = new FileReader()
reader.onload = async (event) => {
+30 -2
View File
@@ -55,7 +55,8 @@ export default {
loadedRatio: 0,
page: 1,
numPages: 0,
pdfDocInitParams: null
pdfDocInitParams: null,
isRefreshing: false
}
},
computed: {
@@ -152,7 +153,34 @@ export default {
this.page++
this.updateProgress()
},
error(err) {
async refreshToken() {
if (this.isRefreshing) return
this.isRefreshing = true
const newAccessToken = await this.$store.dispatch('user/refreshToken').catch((error) => {
console.error('Failed to refresh token', error)
return null
})
if (!newAccessToken) {
// Redirect to login on failed refresh
this.$router.push('/login')
return
}
// Force Vue to re-render the PDF component by creating a new object
this.pdfDocInitParams = {
url: this.ebookUrl,
httpHeaders: {
Authorization: `Bearer ${newAccessToken}`
}
}
this.isRefreshing = false
},
async error(err) {
if (err && err.status === 401) {
console.log('Received 401 error, refreshing token')
await this.refreshToken()
return
}
console.error(err)
},
resize() {
+12 -6
View File
@@ -1,5 +1,5 @@
<template>
<div v-if="show" id="reader" :data-theme="ereaderTheme" class="group absolute top-0 left-0 w-full z-60 data-[theme=dark]:bg-primary data-[theme=dark]:text-white data-[theme=light]:bg-white data-[theme=light]:text-black" :class="{ 'reader-player-open': !!streamLibraryItem }">
<div v-if="show" id="reader" :data-theme="ereaderTheme" class="group absolute top-0 left-0 w-full z-60 data-[theme=dark]:bg-primary data-[theme=dark]:text-white data-[theme=light]:bg-white data-[theme=light]:text-black data-[theme=sepia]:bg-[rgb(244,236,216)] data-[theme=sepia]:text-[#5b4636]" :class="{ 'reader-player-open': !!streamLibraryItem }">
<div class="absolute top-4 left-4 z-20 flex items-center">
<button v-if="isEpub" @click="toggleToC" type="button" aria-label="Table of contents menu" class="inline-flex opacity-80 hover:opacity-100">
<span class="material-symbols text-2xl">menu</span>
@@ -27,7 +27,12 @@
<!-- TOC side nav -->
<div v-if="tocOpen" class="w-full h-full overflow-y-scroll absolute inset-0 bg-black/20 z-20" @click.stop.prevent="toggleToC"></div>
<div v-if="isEpub" class="w-96 h-full max-h-full absolute top-0 left-0 shadow-xl transition-transform z-30 group-data-[theme=dark]:bg-primary group-data-[theme=dark]:text-white group-data-[theme=light]:bg-white group-data-[theme=light]:text-black" :class="tocOpen ? 'translate-x-0' : '-translate-x-96'" @click.stop.prevent>
<div
v-if="isEpub"
class="w-96 h-full max-h-full absolute top-0 left-0 shadow-xl transition-transform z-30 group-data-[theme=dark]:bg-primary group-data-[theme=dark]:text-white group-data-[theme=light]:bg-white group-data-[theme=light]:text-black group-data-[theme=sepia]:bg-[rgb(244,236,216)] group-data-[theme=sepia]:text-[#5b4636]"
:class="tocOpen ? 'translate-x-0' : '-translate-x-96'"
@click.stop.prevent
>
<div class="flex flex-col p-4 h-full">
<div class="flex items-center mb-2">
<button @click.stop.prevent="toggleToC" type="button" aria-label="Close table of contents" class="inline-flex opacity-80 hover:opacity-100">
@@ -37,7 +42,7 @@
<p class="text-lg font-semibold ml-2">{{ $strings.HeaderTableOfContents }}</p>
</div>
<form @submit.prevent="searchBook" @click.stop.prevent>
<ui-text-input clearable ref="input" @clear="searchBook" v-model="searchQuery" :placeholder="$strings.PlaceholderSearch" class="h-8 w-full text-sm flex mb-2" />
<ui-text-input clearable ref="input" @clear="searchBook" v-model="searchQuery" :placeholder="$strings.PlaceholderSearch" custom-input-class="text-inherit !bg-inherit" class="h-8 w-full text-sm flex mb-2" />
</form>
<div class="overflow-y-auto">
@@ -181,6 +186,10 @@ export default {
text: this.$strings.LabelThemeDark,
value: 'dark'
},
{
text: this.$strings.LabelThemeSepia,
value: 'sepia'
},
{
text: this.$strings.LabelThemeLight,
value: 'light'
@@ -266,9 +275,6 @@ export default {
isComic() {
return this.ebookFormat == 'cbz' || this.ebookFormat == 'cbr'
},
userToken() {
return this.$store.getters['user/getToken']
},
keepProgress() {
return this.$store.state.ereaderKeepProgress
},
@@ -14,7 +14,7 @@
<div :key="n" class="absolute pointer-events-none left-0 h-px bg-white/10" :style="{ top: n * lineSpacing - lineSpacing / 2 + 'px', width: '360px', marginLeft: '24px' }" />
<div :key="`dot-${n}`" class="absolute z-10" :style="{ left: points[n - 1].x + 'px', bottom: points[n - 1].y + 'px' }">
<ui-tooltip :text="last7DaysOfListening[n - 1].minutesListening" direction="top">
<ui-tooltip :text="last7DaysOfListening[n - 1].minutesListening" plaintext direction="top">
<div class="h-2 w-2 bg-yellow-400 hover:bg-yellow-300 rounded-full transform duration-150 transition-transform hover:scale-125" />
</ui-tooltip>
</div>
@@ -186,10 +186,16 @@ export default {
daysInARow() {
var count = 0
while (true) {
var _date = this.$addDaysToToday(count * -1)
var datestr = this.$formatJsDate(_date, 'yyyy-MM-dd')
const _date = this.$addDaysToToday(count * -1 - 1)
const datestr = this.$formatJsDate(_date, 'yyyy-MM-dd')
if (!this.listeningStatsDays[datestr] || this.listeningStatsDays[datestr] === 0) {
// don't require listening today to count towards days in a row, but do count it if already listened today
const today = this.$formatJsDate(new Date(), 'yyyy-MM-dd')
if (this.listeningStatsDays[today]) {
count++
}
return count
}
count++
+1 -1
View File
@@ -152,7 +152,7 @@ export default {
this.showingTooltipIndex = index
this.tooltipEl.style.display = 'block'
this.tooltipTextEl.innerHTML = block.value ? `<strong>${this.$elapsedPretty(block.value, true)} listening</strong> on ${block.datePretty}` : `No listening sessions on ${block.datePretty}`
this.tooltipTextEl.innerHTML = block.value ? this.$getString('MessageHeatmapListeningTimeTooltip', [this.$elapsedPrettyLocalized(block.value, true), block.datePretty]) : this.$getString('MessageHeatmapNoListeningSessions', [block.datePretty])
const calculateRect = this.tooltipEl.getBoundingClientRect()
+2 -6
View File
@@ -1,9 +1,7 @@
<template>
<div class="flex flex-wrap justify-center mt-6">
<div class="flex p-2">
<svg class="h-14 w-14" viewBox="0 0 24 24">
<path fill="currentColor" d="M9 3V18H12V3H9M12 5L16 18L19 17L15 4L12 5M5 5V18H8V5H5M3 19V21H21V19H3Z" />
</svg>
<span class="material-symbols text-5xl py-1">newsstand</span>
<div class="px-1">
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalItems) }}</p>
<p class="text-xs md:text-sm text-white/80">{{ $strings.LabelStatsItemsInLibrary }}</p>
@@ -19,9 +17,7 @@
</div>
<div v-if="isBookLibrary" class="flex p-2">
<svg class="h-14 w-14" viewBox="0 0 24 24">
<path fill="currentColor" d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,6A2,2 0 0,0 10,8A2,2 0 0,0 12,10A2,2 0 0,0 14,8A2,2 0 0,0 12,6M12,13C14.67,13 20,14.33 20,17V20H4V17C4,14.33 9.33,13 12,13M12,14.9C9.03,14.9 5.9,16.36 5.9,17V18.1H18.1V17C18.1,16.36 14.97,14.9 12,14.9Z" />
</svg>
<span class="material-symbols text-5xl py-1">person</span>
<div class="px-1">
<p class="text-4.5xl leading-none font-bold">{{ $formatNumber(totalAuthors) }}</p>
<p class="text-xs md:text-sm text-white/80">{{ $strings.LabelStatsAuthors }}</p>
+177
View File
@@ -0,0 +1,177 @@
<template>
<div>
<div class="text-center">
<table v-if="apiKeys.length > 0" id="api-keys">
<tr>
<th>{{ $strings.LabelName }}</th>
<th class="w-44">{{ $strings.LabelApiKeyUser }}</th>
<th class="w-32">{{ $strings.LabelExpiresAt }}</th>
<th class="w-32">{{ $strings.LabelCreatedAt }}</th>
<th class="w-32"></th>
</tr>
<tr v-for="apiKey in apiKeys" :key="apiKey.id" :class="apiKey.isActive ? '' : 'bg-error/10!'">
<td>
<div class="flex items-center">
<p class="pl-2 truncate">{{ apiKey.name }}</p>
</div>
</td>
<td class="text-xs">
<nuxt-link v-if="apiKey.user" :to="`/config/users/${apiKey.user.id}`" class="text-xs hover:underline">
{{ apiKey.user.username }}
</nuxt-link>
<p v-else class="text-xs">Error</p>
</td>
<td class="text-xs">
<p v-if="apiKey.expiresAt" class="text-xs" :title="apiKey.expiresAt">{{ getExpiresAtText(apiKey) }}</p>
<p v-else class="text-xs">{{ $strings.LabelExpiresNever }}</p>
</td>
<td class="text-xs font-mono">
<ui-tooltip direction="top" :text="$formatJsDatetime(new Date(apiKey.createdAt), dateFormat, timeFormat)">
{{ $formatJsDate(new Date(apiKey.createdAt), dateFormat) }}
</ui-tooltip>
</td>
<td class="py-0">
<div class="w-full flex justify-left">
<div class="h-8 w-8 flex items-center justify-center text-white/50 hover:text-white/100 cursor-pointer" @click.stop="editApiKey(apiKey)">
<button type="button" :aria-label="$strings.ButtonEdit" class="material-symbols text-base">edit</button>
</div>
<div class="h-8 w-8 flex items-center justify-center text-white/50 hover:text-error cursor-pointer" @click.stop="deleteApiKeyClick(apiKey)">
<button type="button" :aria-label="$strings.ButtonDelete" class="material-symbols text-base">delete</button>
</div>
</div>
</td>
</tr>
</table>
<p v-else class="text-base text-gray-300 py-4">{{ $strings.LabelNoApiKeys }}</p>
</div>
</div>
</template>
<script>
export default {
data() {
return {
apiKeys: [],
isDeletingApiKey: false
}
},
computed: {
dateFormat() {
return this.$store.state.serverSettings.dateFormat
},
timeFormat() {
return this.$store.state.serverSettings.timeFormat
}
},
methods: {
getExpiresAtText(apiKey) {
if (new Date(apiKey.expiresAt).getTime() < Date.now()) {
return this.$strings.LabelExpired
}
return this.$formatJsDatetime(new Date(apiKey.expiresAt), this.dateFormat, this.timeFormat)
},
deleteApiKeyClick(apiKey) {
if (this.isDeletingApiKey) return
const payload = {
message: this.$getString('MessageConfirmDeleteApiKey', [apiKey.name]),
callback: (confirmed) => {
if (confirmed) {
this.deleteApiKey(apiKey)
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
deleteApiKey(apiKey) {
this.isDeletingApiKey = true
this.$axios
.$delete(`/api/api-keys/${apiKey.id}`)
.then((data) => {
if (data.error) {
this.$toast.error(data.error)
} else {
this.removeApiKey(apiKey.id)
this.$emit('numApiKeys', this.apiKeys.length)
}
})
.catch((error) => {
console.error('Failed to delete apiKey', error)
this.$toast.error(this.$strings.ToastFailedToDelete)
})
.finally(() => {
this.isDeletingApiKey = false
})
},
editApiKey(apiKey) {
this.$emit('edit', apiKey)
},
addApiKey(apiKey) {
this.apiKeys.push(apiKey)
},
removeApiKey(apiKeyId) {
this.apiKeys = this.apiKeys.filter((a) => a.id !== apiKeyId)
},
updateApiKey(apiKey) {
this.apiKeys = this.apiKeys.map((a) => (a.id === apiKey.id ? apiKey : a))
},
loadApiKeys() {
this.$axios
.$get('/api/api-keys')
.then((res) => {
this.apiKeys = res.apiKeys.sort((a, b) => {
return a.createdAt - b.createdAt
})
this.$emit('numApiKeys', this.apiKeys.length)
})
.catch((error) => {
console.error('Failed to load apiKeys', error)
})
}
},
mounted() {
this.loadApiKeys()
}
}
</script>
<style>
#api-keys {
table-layout: fixed;
border-collapse: collapse;
border: 1px solid #474747;
width: 100%;
}
#api-keys td,
#api-keys th {
/* border: 1px solid #2e2e2e; */
padding: 8px 8px;
text-align: left;
}
#api-keys td.py-0 {
padding: 0px 8px;
}
#api-keys tr:nth-child(even) {
background-color: #373838;
}
#api-keys tr:nth-child(odd) {
background-color: #2f2f2f;
}
#api-keys tr:hover {
background-color: #444;
}
#api-keys th {
font-size: 0.8rem;
font-weight: 600;
padding-top: 5px;
padding-bottom: 5px;
background-color: #272727;
}
</style>
@@ -49,9 +49,6 @@ export default {
libraryItemId() {
return this.libraryItem.id
},
userToken() {
return this.$store.getters['user/getToken']
},
userCanDownload() {
return this.$store.getters['user/getUserCanDownload']
},
@@ -53,9 +53,6 @@ export default {
libraryItemId() {
return this.libraryItem.id
},
userToken() {
return this.$store.getters['user/getToken']
},
userCanDownload() {
return this.$store.getters['user/getUserCanDownload']
},
+7 -2
View File
@@ -1,5 +1,5 @@
<template>
<div class="w-40">
<div :class="hasSlotContent ? 'w-auto' : 'w-40'">
<div class="bg-bg border border-gray-500 py-2 px-5 rounded-lg flex items-center flex-col box-shadow-md">
<div class="loader-dots block relative w-20 h-5 mt-2">
<div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
@@ -7,7 +7,9 @@
<div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
<div class="absolute top-0 mt-1 w-3 h-3 rounded-full bg-green-500"></div>
</div>
<div class="text-gray-200 text-xs font-light mt-2 text-center">{{ message }}</div>
<slot>
<div class="text-gray-200 text-xs font-light mt-2 text-center">{{ message }}</div>
</slot>
</div>
</div>
</template>
@@ -23,6 +25,9 @@ export default {
computed: {
message() {
return this.text || this.$strings.MessagePleaseWait
},
hasSlotContent() {
return this.$slots.default && this.$slots.default.length > 0
}
}
}
+7 -5
View File
@@ -4,10 +4,11 @@
<div ref="wrapper" class="relative">
<form @submit.prevent="submitForm">
<div ref="inputWrapper" role="list" style="min-height: 36px" class="flex-wrap relative w-full shadow-xs flex items-center border border-gray-600 rounded-sm px-2 py-1" :class="wrapperClass" @click.stop.prevent="clickWrapper" @mouseup.stop.prevent @mousedown.prevent>
<div v-for="item in selected" :key="item" role="listitem" class="rounded-full px-2 py-1 mx-0.5 my-0.5 text-xs bg-bg flex flex-nowrap break-all items-center relative">
<!-- Use index in v-for and key in case the same key exists multiple times -->
<div v-for="(item, idx) in selected" :key="item + '-' + idx" role="listitem" class="rounded-full px-2 py-1 mx-0.5 my-0.5 text-xs bg-bg flex flex-nowrap break-all items-center relative">
<div v-if="!disabled" class="w-full h-full rounded-full absolute top-0 left-0 px-1 bg-bg/75 flex items-center justify-end opacity-0 hover:opacity-100" :class="{ 'opacity-100': inputFocused }">
<button v-if="showEdit" type="button" :aria-label="$strings.ButtonEdit" class="material-symbols text-white hover:text-warning cursor-pointer" style="font-size: 1.1rem" @click.stop="editItem(item)">edit</button>
<button type="button" :aria-label="$strings.ButtonRemove" class="material-symbols text-white hover:text-error focus:text-error cursor-pointer" style="font-size: 1.1rem" @click.stop="removeItem(item)" @keydown.enter.stop.prevent="removeItem(item)" @focus="setInputFocused(true)" @blur="setInputFocused(false)" tabindex="0">close</button>
<button type="button" :aria-label="$strings.ButtonRemove" class="material-symbols text-white hover:text-error focus:text-error cursor-pointer" style="font-size: 1.1rem" @click.stop="removeItem(item, idx)" @keydown.enter.stop.prevent="removeItem(item, idx)" @focus="setInputFocused(true)" @blur="setInputFocused(false)" tabindex="0">close</button>
</div>
{{ item }}
</div>
@@ -259,8 +260,9 @@ export default {
}
this.focus()
},
removeItem(item) {
var remaining = this.selected.filter((i) => i !== item)
removeItem(item, idx) {
var remaining = this.selected.slice()
remaining.splice(idx, 1)
this.$emit('input', remaining)
this.$emit('removedItem', item)
this.$nextTick(() => {
@@ -276,7 +278,7 @@ export default {
})
},
insertNewItem(item) {
this.selected.push(item)
if (!this.selected.includes(item)) this.selected.push(item)
this.$emit('input', this.selected)
this.$emit('newItem', item)
this.textInput = null
@@ -85,9 +85,6 @@ export default {
this.$emit('input', val)
}
},
userToken() {
return this.$store.getters['user/getToken']
},
wrapperClass() {
var classes = []
if (this.disabled) classes.push('bg-black-300')
@@ -290,7 +287,7 @@ export default {
})
},
insertNewItem(item) {
this.selected.push(item)
if (!this.selected.find((i) => i.name === item.name)) this.selected.push(item)
this.$emit('input', this.selected)
this.$emit('newItem', item)
this.textInput = null
+3 -7
View File
@@ -1,12 +1,8 @@
<template>
<button :aria-label="isRead ? $strings.MessageMarkAsNotFinished : $strings.MessageMarkAsFinished" class="icon-btn rounded-md flex items-center justify-center h-9 w-9 relative" :class="borderless ? '' : 'bg-primary border border-gray-600'" @click="clickBtn">
<div class="w-5 h-5 text-white relative">
<svg v-if="isRead" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="rgb(63, 181, 68)">
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-9 15l-5-5 1.41-1.41L10 13.17l7.59-7.59L19 7l-9 9z" />
</svg>
<svg v-else xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M19 1H5c-1.1 0-1.99.9-1.99 2L3 15.93c0 .69.35 1.3.88 1.66L12 23l8.11-5.41c.53-.36.88-.97.88-1.66L21 3c0-1.1-.9-2-2-2zm-7 19.6l-7-4.66V3h14v12.93l-7 4.67zm-2.01-7.42l-2.58-2.59L6 12l4 4 8-8-1.42-1.42z" />
</svg>
<div class="w-5 h-5 relative">
<span v-if="isRead" class="material-symbols fill text-xl text-success">beenhere</span>
<span v-else class="material-symbols text-xl text-white">beenhere</span>
</div>
</button>
</template>
+7 -2
View File
@@ -1,9 +1,9 @@
<template>
<div class="relative w-full">
<p v-if="label" class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
<p v-if="label && !labelHidden" class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
<button ref="buttonWrapper" type="button" :aria-label="longLabel" :disabled="disabled" class="relative w-full border rounded-sm shadow-xs pl-3 pr-8 py-2 text-left sm:text-sm" :class="buttonClass" aria-haspopup="listbox" aria-expanded="true" @click.stop.prevent="clickShowMenu">
<span class="flex items-center">
<span class="block truncate font-sans" :class="{ 'font-semibold': selectedSubtext, 'text-sm': small }">{{ selectedText }}</span>
<span class="block truncate font-sans" :class="{ 'font-semibold': selectedSubtext, 'text-sm': small, 'text-gray-400': !selectedText }">{{ selectedText || placeholder }}</span>
<span v-if="selectedSubtext">:&nbsp;</span>
<span v-if="selectedSubtext" class="font-normal block truncate font-sans text-sm text-gray-400">{{ selectedSubtext }}</span>
</span>
@@ -36,10 +36,15 @@ export default {
type: String,
default: ''
},
labelHidden: Boolean,
items: {
type: Array,
default: () => []
},
placeholder: {
type: String,
default: ''
},
disabled: Boolean,
small: Boolean,
menuMaxHeight: {
+3 -2
View File
@@ -1,6 +1,6 @@
<template>
<div ref="wrapper" class="relative">
<input :id="inputId" :name="inputName" ref="input" v-model="inputValue" :type="actualType" :step="step" :min="min" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" dir="auto" class="rounded-sm bg-primary text-gray-200 focus:bg-bg focus:outline-hidden border h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
<input :id="inputId" :name="inputName" ref="input" v-model="inputValue" :type="actualType" :step="step" :min="min" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" :autocomplete="autocomplete" dir="auto" class="rounded-sm bg-primary text-gray-200 focus:bg-bg focus:outline-hidden border h-full w-full" :class="classList" @keyup="keyup" @change="change" @focus="focused" @blur="blurred" />
<div v-if="clearable && inputValue" class="absolute top-0 right-0 h-full px-2 flex items-center justify-center">
<span class="material-symbols text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span>
</div>
@@ -41,7 +41,8 @@ export default {
step: [String, Number],
min: [String, Number],
customInputClass: String,
trimWhitespace: Boolean
trimWhitespace: Boolean,
autocomplete: String
},
data() {
return {
+4 -2
View File
@@ -6,7 +6,7 @@
<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em>
</label>
</slot>
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" :show-copy="showCopy" class="w-full" :class="inputClass" :trim-whitespace="trimWhitespace" @blur="inputBlurred" />
<ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" :min="min" :show-copy="showCopy" :autocomplete="autocomplete" class="w-full" :class="inputClass" :trim-whitespace="trimWhitespace" @blur="inputBlurred" />
</div>
</template>
@@ -21,11 +21,13 @@ export default {
type: String,
default: 'text'
},
min: [String, Number],
readonly: Boolean,
disabled: Boolean,
inputClass: String,
showCopy: Boolean,
trimWhitespace: Boolean
trimWhitespace: Boolean,
autocomplete: String
},
data() {
return {}
+12 -3
View File
@@ -22,7 +22,8 @@ export default {
type: Number,
default: 0
},
disabled: Boolean
disabled: Boolean,
plaintext: Boolean
},
data() {
return {
@@ -46,7 +47,11 @@ export default {
methods: {
updateText() {
if (this.tooltip) {
this.tooltip.innerHTML = this.text
if (this.plaintext) {
this.tooltip.textContent = this.text
} else {
this.tooltip.innerHTML = this.text
}
this.setTooltipPosition(this.tooltip)
}
},
@@ -58,7 +63,11 @@ export default {
tooltip.className = 'tooltip-wrapper absolute px-2 py-1 text-white text-xs rounded-sm shadow-lg max-w-xs text-center hidden sm:block'
tooltip.style.zIndex = 100
tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)'
tooltip.innerHTML = this.text
if (this.plaintext) {
tooltip.textContent = this.text
} else {
tooltip.innerHTML = this.text
}
tooltip.addEventListener('mouseover', this.cancelHide)
tooltip.addEventListener('mouseleave', this.hideTooltip)
@@ -1,40 +1,6 @@
<template>
<ui-tooltip :text="$strings.LabelExplicit" direction="top">
<svg xmlns="http://www.w3.org/2000/svg" width="12px" height="12px" viewBox="0 0 512 512" class="ml-1">
<path
fill="white"
d="M 89.00,40.12
C 89.00,40.12 127.00,40.12 127.00,40.12
127.00,40.12 198.00,40.12 198.00,40.12
198.00,40.12 416.00,40.12 416.00,40.12
446.58,40.05 472.95,66.42 473.00,97.00
473.00,97.00 473.00,303.00 473.00,303.00
473.00,303.00 473.00,418.00 473.00,418.00
472.65,447.55 445.06,472.95 416.00,473.00
416.00,473.00 210.00,473.00 210.00,473.00
210.00,473.00 95.00,473.00 95.00,473.00
65.45,472.65 40.05,445.06 40.00,416.00
40.00,416.00 40.00,136.00 40.00,136.00
40.00,136.00 40.00,109.00 40.00,109.00
40.00,109.00 40.00,96.00 40.00,96.00
40.07,81.58 46.89,67.14 57.01,57.01
61.17,52.86 64.86,50.13 70.00,47.31
77.25,43.33 81.02,42.18 89.00,40.12 Z
M 337.00,121.00
C 337.00,121.00 175.00,121.00 175.00,121.00
175.00,121.00 175.00,392.00 175.00,392.00
175.00,392.00 337.00,392.00 337.00,392.00
337.00,392.00 337.00,349.00 337.00,349.00
337.00,349.00 226.00,349.00 226.00,349.00
226.00,349.00 226.00,274.00 226.00,274.00
226.00,274.00 332.00,274.00 332.00,274.00
332.00,274.00 332.00,232.00 332.00,232.00
332.00,232.00 226.00,232.00 226.00,232.00
226.00,232.00 226.00,164.00 226.00,164.00
226.00,164.00 337.00,164.00 337.00,164.00
337.00,164.00 337.00,121.00 337.00,121.00 Z"
/>
</svg>
<span class="material-symbols fill text-sm ml-1 !block">explicit</span>
</ui-tooltip>
</template>
+2 -2
View File
@@ -132,10 +132,10 @@ export default {
editAuthor(author) {
this.$store.commit('globals/showEditAuthorModal', author)
},
editItem(libraryItem) {
editItem(libraryItem, tab = 'details') {
var itemIds = this.items.map((e) => e.id)
this.$store.commit('setBookshelfBookIds', itemIds)
this.$store.commit('showEditModal', libraryItem)
this.$store.commit('showEditModalOnTab', { libraryItem, tab: tab || 'details' })
},
selectItem(payload) {
this.$emit('selectEntity', payload)
+45 -6
View File
@@ -33,6 +33,7 @@ export default {
return {
socket: null,
isSocketConnected: false,
isSocketAuthenticated: false,
isFirstSocketConnection: true,
socketConnectionToastId: null,
currentLang: null,
@@ -81,9 +82,28 @@ export default {
document.body.classList.add('app-bar')
}
},
tokenRefreshed(newAccessToken) {
if (this.isSocketConnected && !this.isSocketAuthenticated) {
console.log('[SOCKET] Re-authenticating socket after token refresh')
this.socket.emit('auth', newAccessToken)
}
},
updateSocketConnectionToast(content, type, timeout) {
if (this.socketConnectionToastId !== null && this.socketConnectionToastId !== undefined) {
this.$toast.update(this.socketConnectionToastId, { content: content, options: { timeout: timeout, type: type, closeButton: false, position: 'bottom-center', onClose: () => null, closeOnClick: timeout !== null } }, false)
const toastUpdateOptions = {
content: content,
options: {
timeout: timeout,
type: type,
closeButton: false,
position: 'bottom-center',
onClose: () => {
this.socketConnectionToastId = null
},
closeOnClick: timeout !== null
}
}
this.$toast.update(this.socketConnectionToastId, toastUpdateOptions, false)
} else {
this.socketConnectionToastId = this.$toast[type](content, { position: 'bottom-center', timeout: timeout, closeButton: false, closeOnClick: timeout !== null })
}
@@ -109,7 +129,7 @@ export default {
this.updateSocketConnectionToast(this.$strings.ToastSocketDisconnected, 'error', null)
},
reconnect() {
console.error('[SOCKET] reconnected')
console.log('[SOCKET] reconnected')
},
reconnectAttempt(val) {
console.log(`[SOCKET] reconnect attempt ${val}`)
@@ -120,6 +140,10 @@ export default {
reconnectFailed() {
console.error('[SOCKET] reconnect failed')
},
authFailed(payload) {
console.error('[SOCKET] auth failed', payload.message)
this.isSocketAuthenticated = false
},
init(payload) {
console.log('Init Payload', payload)
@@ -127,7 +151,7 @@ export default {
this.$store.commit('users/setUsersOnline', payload.usersOnline)
}
this.$eventBus.$emit('socket_init')
this.isSocketAuthenticated = true
},
streamOpen(stream) {
if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamOpen(stream)
@@ -175,7 +199,7 @@ export default {
}
} else {
console.error('User has no more accessible libraries')
this.$store.commit('libraries/setCurrentLibrary', null)
this.$store.commit('libraries/setCurrentLibrary', { id: null })
}
}
},
@@ -347,13 +371,24 @@ export default {
},
customMetadataProviderAdded(provider) {
if (!provider?.id) return
this.$store.commit('scanners/addCustomMetadataProvider', provider)
// Refresh providers cache
this.$store.dispatch('scanners/refreshProviders')
},
customMetadataProviderRemoved(provider) {
if (!provider?.id) return
this.$store.commit('scanners/removeCustomMetadataProvider', provider)
// Refresh providers cache
this.$store.dispatch('scanners/refreshProviders')
},
initializeSocket() {
if (this.$root.socket) {
// Can happen in dev due to hot reload
console.warn('Socket already initialized')
this.socket = this.$root.socket
this.isSocketConnected = this.$root.socket?.connected
this.isFirstSocketConnection = false
this.socketConnectionToastId = null
return
}
this.socket = this.$nuxtSocket({
name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
persist: 'main',
@@ -364,6 +399,7 @@ export default {
path: `${this.$config.routerBasePath}/socket.io`
})
this.$root.socket = this.socket
this.isSocketAuthenticated = false
console.log('Socket initialized')
// Pre-defined socket events
@@ -377,6 +413,7 @@ export default {
// Event received after authorizing socket
this.socket.on('init', this.init)
this.socket.on('auth_failed', this.authFailed)
// Stream Listeners
this.socket.on('stream_open', this.streamOpen)
@@ -571,6 +608,7 @@ export default {
this.updateBodyClass()
this.resize()
this.$eventBus.$on('change-lang', this.changeLanguage)
this.$eventBus.$on('token_refreshed', this.tokenRefreshed)
window.addEventListener('resize', this.resize)
window.addEventListener('keydown', this.keyDown)
@@ -594,6 +632,7 @@ export default {
},
beforeDestroy() {
this.$eventBus.$off('change-lang', this.changeLanguage)
this.$eventBus.$off('token_refreshed', this.tokenRefreshed)
window.removeEventListener('resize', this.resize)
window.removeEventListener('keydown', this.keyDown)
}
+2 -2
View File
@@ -118,8 +118,8 @@ export default {
propsData: props,
parent: this,
created() {
this.$on('edit', (entity) => {
if (_this.editEntity) _this.editEntity(entity)
this.$on('edit', (entity, tab) => {
if (_this.editEntity) _this.editEntity(entity, tab)
})
this.$on('select', ({ entity, shiftKey }) => {
if (_this.selectEntity) _this.selectEntity(entity, shiftKey)
+2 -1
View File
@@ -73,7 +73,8 @@ module.exports = {
// Axios module configuration: https://go.nuxtjs.dev/config-axios
axios: {
baseURL: routerBasePath
baseURL: routerBasePath,
progress: false
},
// nuxt/pwa https://pwa.nuxtjs.org
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "audiobookshelf-client",
"version": "2.25.1",
"version": "2.36.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "audiobookshelf-client",
"version": "2.25.1",
"version": "2.36.0",
"license": "ISC",
"dependencies": {
"@nuxtjs/axios": "^5.13.6",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "2.25.1",
"version": "2.36.0",
"buildNumber": 1,
"description": "Self-hosted audiobook and podcast client",
"main": "index.js",
+153 -15
View File
@@ -1,6 +1,6 @@
<template>
<div id="page-wrapper" class="page p-6 overflow-y-auto relative" :class="streamLibraryItem ? 'streaming' : ''">
<div class="w-full max-w-xl mx-auto">
<div class="w-full max-w-2xl mx-auto">
<h1 class="text-2xl">{{ $strings.HeaderAccount }}</h1>
<div class="my-4">
@@ -69,8 +69,52 @@
</app-settings-content>
</div>
<div class="py-4 mt-8 flex">
<ui-btn color="bg-primary flex items-center text-lg" @click="logout"><span class="material-symbols mr-4 icon-text">logout</span>{{ $strings.ButtonLogout }}</ui-btn>
<div v-if="!isGuest">
<div class="w-full h-px bg-white/10 my-4" />
<app-settings-content :header-text="$strings.HeaderSessions">
<table v-if="authSessions.length" class="tracksTable mt-4 table-fixed w-full">
<tr>
<th class="text-left">{{ $strings.LabelDeviceInfo }}</th>
<th class="text-left hidden sm:table-cell w-36">{{ $strings.LabelIpAddress }}</th>
<th class="text-left w-30 sm:w-40">{{ $strings.LabelLastUpdate }}</th>
<th class="w-12"></th>
</tr>
<tr v-for="session in authSessions" :key="session.id">
<td class="max-w-0">
<div class="flex items-center gap-0.5 min-w-0">
<span class="text-sm text-gray-100 truncate min-w-0" :title="session.userAgent">{{ getSessionDeviceLabel(session) }}</span>
<ui-tooltip v-if="session.current" direction="top" :text="$strings.LabelCurrent" class="inline-flex shrink-0">
<span class="material-symbols text-success text-sm leading-none">check</span>
</ui-tooltip>
</div>
</td>
<td class="hidden sm:table-cell w-36">
<p class="text-sm text-gray-100 truncate" :title="session.ipAddress">{{ session.ipAddress || '-' }}</p>
</td>
<td class="w-30 sm:w-40">
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDatetime(session.updatedAt, dateFormat, timeFormat)">
<p class="text-xs sm:text-sm text-gray-100">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
</ui-tooltip>
</td>
<td class="w-12">
<div class="flex justify-end items-center h-10">
<ui-icon-btn icon="delete" borderless :size="8" icon-font-size="1.1rem" :disabled="deletingSessionId === session.id || loadingAuthSessions" @click="deleteAuthSessionClick(session)" />
</div>
</td>
</tr>
</table>
<div v-if="authSessionsNumPages > 1" class="flex items-center justify-end py-1">
<ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="loadingAuthSessions || authSessionsPage === 0" @click="prevAuthSessionsPage" />
<p class="text-sm mx-1">{{ $getString('LabelPaginationPageXOfY', [authSessionsPage + 1, authSessionsNumPages]) }}</p>
<ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="loadingAuthSessions || authSessionsPage >= authSessionsNumPages - 1" @click="nextAuthSessionsPage" />
</div>
</app-settings-content>
</div>
<div class="py-4 mt-8 flex flex-wrap gap-2">
<ui-btn v-if="!isGuest" color="bg-primary flex items-center text-lg" :disabled="loggingOut" @click="logout(true)"> <span class="material-symbols mr-4 icon-text">devices</span>{{ $strings.ButtonLogoutAllDevices }} </ui-btn>
<ui-btn color="bg-primary flex items-center text-lg" :disabled="loggingOut" @click="logout(false)"><span class="material-symbols mr-4 icon-text">logout</span>{{ $strings.ButtonLogout }}</ui-btn>
</div>
<modals-emails-user-e-reader-device-modal v-model="showEReaderDeviceModal" :existing-devices="revisedEreaderDevices" :ereader-device="selectedEReaderDevice" @update="ereaderDevicesUpdated" />
@@ -87,6 +131,13 @@ export default {
newPassword: null,
confirmPassword: null,
changingPassword: false,
loggingOut: false,
authSessions: [],
authSessionsPage: 0,
authSessionsNumPages: 0,
authSessionsItemsPerPage: 10,
loadingAuthSessions: false,
deletingSessionId: null,
selectedLanguage: '',
newEReaderDevice: {
name: '',
@@ -129,13 +180,19 @@ export default {
},
revisedEreaderDevices() {
return this.ereaderDevices.filter((device) => device.users?.length === 1)
},
dateFormat() {
return this.$store.getters['getServerSetting']('dateFormat')
},
timeFormat() {
return this.$store.getters['getServerSetting']('timeFormat')
}
},
methods: {
updateLocalLanguage(lang) {
this.$setLanguageCode(lang)
},
logout() {
logout(allDevices = false) {
// Disconnect from socket
if (this.$root.socket) {
console.log('Disconnecting from socket', this.$root.socket.id)
@@ -149,8 +206,10 @@ export default {
this.$store.commit('libraries/setUserPlaylists', [])
this.$store.commit('libraries/setCollections', [])
this.loggingOut = true
const url = allDevices ? '/logout?allDevices=1' : '/logout'
this.$axios
.$post('/logout')
.$post(url)
.then((logoutPayload) => {
const redirect_url = logoutPayload.redirect_url
@@ -163,6 +222,9 @@ export default {
.catch((error) => {
console.error(error)
})
.finally(() => {
this.loggingOut = false
})
},
resetForm() {
this.password = null
@@ -182,18 +244,19 @@ export default {
password: this.password,
newPassword: this.newPassword
})
.then((res) => {
if (res.success) {
this.$toast.success(this.$strings.ToastUserPasswordChangeSuccess)
this.resetForm()
} else {
this.$toast.error(res.error || this.$strings.ToastUnknownError)
}
this.changingPassword = false
.then(() => {
this.$toast.success(this.$strings.ToastUserPasswordChangeSuccess)
this.resetForm()
})
.catch((error) => {
console.error(error)
this.$toast.error(this.$strings.ToastUnknownError)
console.error('Failed to change password', error)
let errorMessage = this.$strings.ToastUnknownError
if (error.response?.data && typeof error.response.data === 'string') {
errorMessage = error.response.data
}
this.$toast.error(errorMessage)
})
.finally(() => {
this.changingPassword = false
})
},
@@ -237,11 +300,86 @@ export default {
},
ereaderDevicesUpdated(ereaderDevices) {
this.ereaderDevices = ereaderDevices
},
loadAuthSessions(page = 0) {
if (this.loadingAuthSessions) return
if (page < 0) return
if (this.authSessionsNumPages > 0 && page > this.authSessionsNumPages - 1) return
this.loadingAuthSessions = true
this.$axios
.$get(`/api/me/sessions?page=${page}&itemsPerPage=${this.authSessionsItemsPerPage}`)
.then((data) => {
this.authSessions = data.sessions || []
this.authSessionsPage = data.page ?? page
this.authSessionsNumPages = data.numPages ?? 0
})
.catch((error) => {
console.error('Failed to load sessions', error)
})
.finally(() => {
this.loadingAuthSessions = false
})
},
prevAuthSessionsPage() {
if (this.authSessionsPage <= 0) return
this.loadAuthSessions(this.authSessionsPage - 1)
},
nextAuthSessionsPage() {
if (this.authSessionsPage >= this.authSessionsNumPages - 1) return
this.loadAuthSessions(this.authSessionsPage + 1)
},
deleteAuthSessionClick(session) {
this.$store.commit('globals/setConfirmPrompt', {
message: this.$getString('MessageConfirmLogoutDevice', [this.getSessionDeviceLabel(session)]),
callback: (confirmed) => {
if (confirmed) this.deleteAuthSession(session)
},
type: 'yesNo'
})
},
deleteAuthSession(session) {
// Call logout instead for current session
if (session.current) {
this.logout(false)
return
}
this.deletingSessionId = session.id
this.$axios
.$delete(`/api/me/sessions/${session.id}`)
.then(() => {
if (this.authSessions.length === 1 && this.authSessionsPage > 0) {
this.loadAuthSessions(this.authSessionsPage - 1)
} else {
this.loadAuthSessions(this.authSessionsPage)
}
})
.catch((error) => {
console.error('Failed to delete session', error)
this.$toast.error(this.$strings.ToastFailedToDelete)
})
.finally(() => {
this.deletingSessionId = null
})
},
getSessionDeviceLabel(session) {
const deviceInfo = session.deviceInfo
if (!deviceInfo) return session.userAgent || '-'
const parts = []
if (deviceInfo.model) parts.push(deviceInfo.model)
if (deviceInfo.osName) parts.push(`${deviceInfo.osName}${deviceInfo.osVersion ? ` ${deviceInfo.osVersion}` : ''}`)
if (deviceInfo.browserName) parts.push(deviceInfo.browserName)
return parts.join(' / ') || session.userAgent || '-'
}
},
mounted() {
this.selectedLanguage = this.$languageCodes.current
this.ereaderDevices = this.$store.state.libraries.ereaderDevices || []
if (!this.isGuest) {
this.loadAuthSessions()
}
}
}
</script>
+404 -92
View File
@@ -12,24 +12,24 @@
<p class="text-base font-mono ml-4 hidden md:block">{{ $secondsToTimestamp(mediaDurationRounded) }}</p>
</div>
<div class="flex flex-wrap-reverse lg:flex-nowrap justify-center py-4 px-4">
<div class="flex flex-wrap-reverse min-[1120px]:flex-nowrap justify-center py-4 px-4">
<div class="w-full max-w-3xl py-4">
<div class="flex items-center">
<div class="w-12 hidden lg:block" />
<div class="w-12 hidden min-w-[1120px]:block" />
<p class="text-lg mb-4 font-semibold">{{ $strings.HeaderChapters }}</p>
<div class="grow" />
<ui-checkbox v-model="showSecondInputs" checkbox-bg="primary" small label-class="text-sm text-gray-200 pl-1" :label="$strings.LabelShowSeconds" class="mx-2" />
<div class="w-32 hidden lg:block" />
<div class="w-32 hidden min-[1120px]:block" />
</div>
<div class="flex items-center mb-3 py-1 -mx-1">
<div class="w-12 hidden lg:block" />
<div class="w-12 hidden min-[1120px]:block" />
<ui-btn v-if="chapters.length" color="bg-primary" small class="mx-1 whitespace-nowrap" @click.stop="removeAllChaptersClick">{{ $strings.ButtonRemoveAll }}</ui-btn>
<ui-btn v-if="newChapters.length > 1" :color="showShiftTimes ? 'bg-bg' : 'bg-primary'" class="mx-1 whitespace-nowrap" small @click="showShiftTimes = !showShiftTimes">{{ $strings.ButtonShiftTimes }}</ui-btn>
<ui-btn color="bg-primary" small :class="{ 'mx-1': newChapters.length > 1 }" @click="showFindChaptersModal = true">{{ $strings.ButtonLookup }}</ui-btn>
<div class="grow" />
<ui-btn v-if="hasChanges" small class="mx-1" @click.stop="resetChapters">{{ $strings.ButtonReset }}</ui-btn>
<ui-btn v-if="hasChanges" color="bg-success" class="mx-1" :disabled="!hasChanges" small @click="saveChapters">{{ $strings.ButtonSave }}</ui-btn>
<div class="w-32 hidden lg:block" />
<div class="w-32 hidden min-[1120px]:block" />
</div>
<div class="overflow-hidden">
@@ -53,54 +53,104 @@
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
<div class="w-8 min-w-8 md:w-12 md:min-w-12"></div>
<div class="w-24 min-w-24 md:w-32 md:min-w-32 px-2">{{ $strings.LabelStart }}</div>
<div class="grow px-2">{{ $strings.LabelTitle }}</div>
<div class="w-38 min-w-38 md:w-40 md:min-w-40 px-1 pl-8">{{ $strings.LabelStart }}</div>
<div class="grow px-1 min-w-54">{{ $strings.LabelTitle }}</div>
<div class="w-7 min-w-7 px-1 flex items-center justify-center">
<ui-tooltip :text="allChaptersLocked ? $strings.TooltipUnlockAllChapters : $strings.TooltipLockAllChapters" direction="bottom">
<button class="w-7 h-7 rounded-full flex items-center justify-center cursor-pointer transition-colors duration-150" :class="allChaptersLocked ? 'text-orange-400 hover:text-orange-300' : 'text-gray-300 hover:text-white'" @click="toggleAllChaptersLock">
<span class="material-symbols text-xl">{{ allChaptersLocked ? 'lock' : 'lock_open' }}</span>
</button>
</ui-tooltip>
</div>
<div class="w-32"></div>
</div>
<template v-for="chapter in newChapters">
<div :key="chapter.id" class="flex py-1">
<div class="w-8 min-w-8 md:w-12 md:min-w-12">#{{ chapter.id + 1 }}</div>
<div class="w-24 min-w-24 md:w-32 md:min-w-32 px-1">
<ui-text-input v-if="showSecondInputs" v-model="chapter.start" type="number" class="text-xs" @change="checkChapters" />
<ui-time-picker v-else class="text-xs" v-model="chapter.start" :show-three-digit-hour="mediaDuration >= 360000" @change="checkChapters" />
</div>
<div class="grow px-1">
<ui-text-input v-model="chapter.title" @change="checkChapters" class="text-xs min-w-52" />
</div>
<div class="w-32 min-w-32 px-2 py-1">
<div class="flex items-center">
<ui-tooltip :text="$strings.MessageRemoveChapter" direction="bottom">
<button v-if="newChapters.length > 1" class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-error transform hover:scale-110 duration-150" @click="removeChapter(chapter)">
<span class="material-symbols text-base">remove</span>
</button>
</ui-tooltip>
<div v-for="chapter in newChapters" :key="chapter.id" class="flex py-1">
<div class="w-8 min-w-8 md:w-12 md:min-w-12">#{{ chapter.id + 1 }}</div>
<div class="w-38 min-w-38 md:w-40 md:min-w-40 px-1">
<div class="flex items-center gap-1">
<ui-tooltip :text="$strings.TooltipSubtractOneSecond" direction="bottom">
<button
class="w-6 h-6 rounded-full flex items-center justify-center text-gray-300 hover:text-white transform hover:scale-110 duration-150 flex-shrink-0"
:class="{ 'opacity-50 cursor-not-allowed': chapter.id === 0 && chapter.start - timeIncrementAmount < 0 }"
@click="incrementChapterTime(chapter, -timeIncrementAmount)"
:disabled="chapter.id === 0 && chapter.start - timeIncrementAmount < 0"
>
<span class="material-symbols text-sm">remove</span>
</button>
</ui-tooltip>
<ui-tooltip :text="$strings.MessageInsertChapterBelow" direction="bottom">
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-success transform hover:scale-110 duration-150" @click="addChapter(chapter)">
<span class="material-symbols text-lg">add</span>
</button>
</ui-tooltip>
<ui-tooltip :text="selectedChapterId === chapter.id && isPlayingChapter ? $strings.MessagePauseChapter : $strings.MessagePlayChapter" direction="bottom">
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-white transform hover:scale-110 duration-150" @click="playChapter(chapter)">
<widgets-loading-spinner v-if="selectedChapterId === chapter.id && isLoadingChapter" />
<span v-else-if="selectedChapterId === chapter.id && isPlayingChapter" class="material-symbols text-base">pause</span>
<span v-else class="material-symbols text-base">play_arrow</span>
</button>
</ui-tooltip>
<ui-tooltip v-if="chapter.error" :text="chapter.error" direction="left">
<button class="w-7 h-7 rounded-full flex items-center justify-center text-error">
<span class="material-symbols text-lg">error_outline</span>
</button>
</ui-tooltip>
<div class="flex-1 min-w-0">
<ui-text-input v-if="showSecondInputs" v-model="chapter.start" type="number" class="text-xs" @change="checkChapters" />
<ui-time-picker v-else class="text-xs" v-model="chapter.start" :show-three-digit-hour="mediaDuration >= 360000" @change="checkChapters" />
</div>
<ui-tooltip :text="$strings.TooltipAddOneSecond" direction="bottom">
<button class="w-6 h-6 rounded-full flex items-center justify-center text-gray-300 hover:text-white transform hover:scale-110 duration-150 flex-shrink-0" :class="{ 'opacity-50 cursor-not-allowed': chapter.start + timeIncrementAmount >= mediaDuration }" @click="incrementChapterTime(chapter, timeIncrementAmount)" :disabled="chapter.start + timeIncrementAmount >= mediaDuration">
<span class="material-symbols text-sm">add</span>
</button>
</ui-tooltip>
</div>
</div>
</template>
<div class="grow px-1">
<ui-text-input v-model="chapter.title" @change="checkChapters" class="text-xs min-w-52" />
</div>
<div class="w-7 min-w-7 px-1 py-1">
<div class="flex items-center justify-center">
<ui-tooltip :text="lockedChapters.has(chapter.id) ? $strings.TooltipUnlockChapter : $strings.TooltipLockChapter" direction="bottom">
<button class="w-7 h-7 rounded-full flex items-center justify-center transform hover:scale-110 duration-150 flex-shrink-0" :class="lockedChapters.has(chapter.id) ? 'text-orange-400 hover:text-orange-300' : 'text-gray-300 hover:text-white'" @click="toggleChapterLock(chapter, $event)">
<span class="material-symbols text-base">{{ lockedChapters.has(chapter.id) ? 'lock' : 'lock_open' }}</span>
</button>
</ui-tooltip>
</div>
</div>
<div class="w-32 min-w-32 px-2 py-1">
<div class="flex items-center">
<ui-tooltip :text="$strings.MessageRemoveChapter" direction="bottom">
<button v-if="newChapters.length > 1" class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-error transform hover:scale-110 duration-150" @click="removeChapter(chapter)">
<span class="material-symbols text-base">delete</span>
</button>
</ui-tooltip>
<ui-tooltip :text="$strings.MessageInsertChapterBelow" direction="bottom">
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-success transform hover:scale-110 duration-150" @click="addChapter(chapter)">
<span class="material-symbols text-lg">add_row_below</span>
</button>
</ui-tooltip>
<ui-tooltip :text="selectedChapterId === chapter.id && isPlayingChapter ? $strings.MessagePauseChapter : $strings.MessagePlayChapter" direction="bottom">
<button :disabled="!getAudioTrackForTime(chapter.start)" class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-white transform hover:scale-110 duration-150 disabled:opacity-50 disabled:cursor-not-allowed" @click="playChapter(chapter)">
<widgets-loading-spinner v-if="selectedChapterId === chapter.id && isLoadingChapter" />
<span v-else-if="selectedChapterId === chapter.id && isPlayingChapter" class="material-symbols text-base">pause</span>
<span v-else class="material-symbols text-xl">play_arrow</span>
</button>
</ui-tooltip>
<ui-tooltip v-if="selectedChapterId === chapter.id && (isPlayingChapter || isLoadingChapter)" :text="$strings.TooltipAdjustChapterStart" direction="bottom">
<div class="ml-2 text-xs text-gray-300 font-mono min-w-10 cursor-pointer hover:text-white transition-colors duration-150" @click="adjustChapterStartTime(chapter)">{{ elapsedTime }}s</div>
</ui-tooltip>
<ui-tooltip v-if="chapter.error" :text="chapter.error" plaintext direction="left">
<button class="w-7 h-7 rounded-full flex items-center justify-center text-error">
<span class="material-symbols text-lg">error_outline</span>
</button>
</ui-tooltip>
</div>
</div>
</div>
<div class="flex items-center mt-4 mb-2">
<div class="w-8 min-w-8 md:w-12 md:min-w-12"></div>
<div class="w-38 min-w-38 md:w-40 md:min-w-40 px-1"></div>
<div class="flex items-center gap-2 grow px-1">
<ui-text-input v-model="bulkChapterInput" :placeholder="$strings.PlaceholderBulkChapterInput" class="text-xs grow min-w-52" @keyup.enter="handleBulkChapterAdd" />
</div>
<div class="w-39 min-w-39 px-1 py-1">
<ui-tooltip :text="$strings.TooltipAddChapters" direction="bottom" class="inline-block align-middle">
<button class="w-5 h-5 rounded-full flex items-center justify-center text-gray-300 hover:text-success transform hover:scale-110 duration-150 flex-shrink-0" :aria-label="$strings.TooltipAddChapters" :class="{ 'opacity-50 cursor-not-allowed': !bulkChapterInput.trim() }" :disabled="!bulkChapterInput.trim()" @click="handleBulkChapterAdd">
<span class="material-symbols text-lg">add</span>
</button>
</ui-tooltip>
</div>
</div>
</div>
<div class="w-full max-w-xl py-4 px-2">
<div class="w-full max-w-3xl min-[1120px]:max-w-xl py-4 px-2">
<div class="flex items-center mb-4 py-1">
<p class="text-lg font-semibold">{{ $strings.HeaderAudioTracks }}</p>
<div class="grow" />
@@ -110,23 +160,19 @@
</ui-tooltip>
</div>
<div class="flex text-xs uppercase text-gray-300 font-semibold mb-2">
<div class="grow">{{ $strings.LabelFilename }}</div>
<div class="grow min-[1120px]:max-w-64 xl:max-w-sm">{{ $strings.LabelFilename }}</div>
<div class="w-20">{{ $strings.LabelDuration }}</div>
<div class="w-20 hidden md:block text-center">{{ $strings.HeaderChapters }}</div>
</div>
<template v-for="track in audioTracks">
<div :key="track.ino" class="flex items-center py-2" :class="currentTrackIndex === track.index && isPlayingChapter ? 'bg-success/10' : ''">
<div class="grow max-w-[calc(100%-80px)] pr-2">
<p class="text-xs truncate max-w-sm">{{ track.metadata.filename }}</p>
</div>
<div class="w-20" style="min-width: 80px">
<p class="text-xs font-mono text-gray-200">{{ $secondsToTimestamp(Math.round(track.duration), false, true) }}</p>
</div>
<div class="w-20 hidden md:flex justify-center" style="min-width: 80px">
<span v-if="(track.chapters || []).length" class="material-symbols text-success text-sm">check</span>
</div>
<div v-for="track in audioTracks" :key="track.ino" class="flex items-center py-2" :class="currentTrackIndex === track.index && isPlayingChapter ? 'bg-success/10' : ''">
<div class="pr-2 grow min-[1120px]:max-w-64 xl:max-w-sm">
<p class="text-xs truncate">{{ track.metadata.filename }}</p>
</div>
</template>
<div class="w-20" style="min-width: 80px">
<p class="text-xs font-mono text-gray-200">{{ $secondsToTimestamp(Math.round(track.duration), false, true) }}</p>
</div>
<div class="w-20 hidden md:flex justify-center" style="min-width: 80px"><span v-if="(track.chapters || []).length" class="material-symbols text-success text-sm">check</span></div>
</div>
</div>
</div>
@@ -134,6 +180,7 @@
<ui-loading-indicator />
</div>
<!-- audible chapter lookup modal -->
<modals-modal v-model="showFindChaptersModal" name="edit-book" :width="500" :processing="findingChapters">
<template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
@@ -159,12 +206,16 @@
</div>
</div>
<div v-else class="w-full p-4">
<div class="flex justify-between mb-4">
<div class="flex mb-4">
<button class="w-7 h-7 rounded-full flex items-center justify-center text-gray-300 hover:text-white flex-shrink-0" :aria-label="$strings.ButtonBack" @click="resetChapterLookupData">
<span class="material-symbols text-lg">arrow_back</span>
</button>
<p>
{{ $strings.LabelDurationFound }} <span class="font-semibold">{{ $secondsToTimestamp(chapterData.runtimeLengthSec) }}</span
><br />
{{ $strings.LabelDurationFound }} <span class="font-semibold">{{ $secondsToTimestamp(chapterData.runtimeLengthSec) }}</span>
<br />
<span class="font-semibold" :class="{ 'text-warning': chapters.length !== chapterData.chapters.length }">{{ chapterData.chapters.length }}</span> {{ $strings.LabelChaptersFound }}
</p>
<div class="grow" />
<p>
{{ $strings.LabelYourAudiobookDuration }}: <span class="font-semibold">{{ $secondsToTimestamp(mediaDurationRounded) }}</span
><br />
@@ -198,17 +249,49 @@
<p class="pl-2">{{ $strings.MessageChapterStartIsAfter }}</p>
</div>
</div>
<div class="flex items-center pt-2">
<ui-btn small color="bg-primary" class="mr-1" @click="applyChapterNamesOnly">{{ $strings.ButtonMapChapterTitles }}</ui-btn>
<ui-tooltip :text="$strings.MessageMapChapterTitles" direction="top" class="flex items-center">
<span class="material-symbols text-xl text-gray-200">info</span>
</ui-tooltip>
<div class="grow" />
<div class="flex items-center pt-2 justify-between">
<div class="flex items-center gap-2">
<ui-btn small color="bg-primary" @click="applyChapterNamesOnly">{{ $strings.ButtonMapChapterTitles }}</ui-btn>
<ui-tooltip :text="$strings.MessageMapChapterTitles" direction="top" class="flex items-center">
<span class="material-symbols text-xl text-gray-200">info</span>
</ui-tooltip>
</div>
<ui-btn small color="bg-success" @click="applyChapterData">{{ $strings.ButtonApplyChapters }}</ui-btn>
</div>
</div>
</div>
</modals-modal>
<!-- create bulk chapters modal -->
<modals-modal v-model="showBulkChapterModal" name="bulk-chapters" :width="400">
<template #outer>
<div class="absolute top-0 left-0 p-5 w-2/3 overflow-hidden pointer-events-none">
<p class="text-3xl text-white truncate pointer-events-none">{{ $strings.HeaderBulkChapterModal }}</p>
</div>
</template>
<div class="w-full h-full max-h-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative p-6">
<div class="flex flex-col space-y-8">
<p class="text-base">{{ $strings.MessageBulkChapterPattern }}</p>
<div v-if="detectedPattern" class="text-sm text-gray-400 bg-gray-800 p-2 rounded">
<strong>{{ $strings.LabelDetectedPattern }}</strong> "{{ detectedPattern.before }}{{ formatNumberWithPadding(detectedPattern.startingNumber, detectedPattern) }}{{ detectedPattern.after }}"
<br />
<strong>{{ $strings.LabelNextChapters }}</strong>
"{{ detectedPattern.before }}{{ formatNumberWithPadding(detectedPattern.startingNumber + 1, detectedPattern) }}{{ detectedPattern.after }}", "{{ detectedPattern.before }}{{ formatNumberWithPadding(detectedPattern.startingNumber + 2, detectedPattern) }}{{ detectedPattern.after }}", etc.
</div>
<div class="flex px-1 items-center">
<label class="text-base font-medium">{{ $strings.LabelNumberOfChapters }}</label>
<div class="grow" />
<ui-text-input v-model="bulkChapterCount" type="number" min="1" max="50" class="w-14" :style="{ height: `2em` }" @keyup.enter="addBulkChapters" />
</div>
<div class="flex px-1 items-center">
<ui-btn small @click="showBulkChapterModal = false">{{ $strings.ButtonCancel }}</ui-btn>
<div class="grow" />
<ui-btn small color="bg-success" @click="addBulkChapters">{{ $strings.ButtonAddChapters }}</ui-btn>
</div>
</div>
</div>
</modals-modal>
</div>
</template>
@@ -265,7 +348,17 @@ export default {
removeBranding: false,
showSecondInputs: false,
audibleRegions: ['US', 'CA', 'UK', 'AU', 'FR', 'DE', 'JP', 'IT', 'IN', 'ES'],
hasChanges: false
hasChanges: false,
timeIncrementAmount: 1,
elapsedTime: 0,
playStartTime: null,
elapsedTimeInterval: null,
lockedChapters: new Set(),
lastSelectedLockIndex: null,
bulkChapterInput: '',
showBulkChapterModal: false,
bulkChapterCount: 1,
detectedPattern: null
}
},
computed: {
@@ -304,9 +397,18 @@ export default {
},
selectedChapterId() {
return this.selectedChapter ? this.selectedChapter.id : null
},
allChaptersLocked() {
return this.newChapters.length > 0 && this.newChapters.every((chapter) => this.lockedChapters.has(chapter.id))
}
},
methods: {
formatNumberWithPadding(number, pattern) {
if (!pattern || !pattern.hasLeadingZeros || !pattern.originalPadding) {
return number.toString()
}
return number.toString().padStart(pattern.originalPadding, '0')
},
setChaptersFromTracks() {
let currentStartTime = 0
let index = 0
@@ -321,7 +423,7 @@ export default {
currentStartTime += track.duration
}
this.newChapters = chapters
this.lockedChapters = new Set()
this.checkChapters()
},
toggleRemoveBranding() {
@@ -334,19 +436,22 @@ export default {
const amount = Number(this.shiftAmount)
const lastChapter = this.newChapters[this.newChapters.length - 1]
if (lastChapter.start + amount > this.mediaDurationRounded) {
this.$toast.error(this.$strings.ToastChaptersInvalidShiftAmountLast)
return
}
// Check if any unlocked chapters would be affected negatively
const unlockedChapters = this.newChapters.filter((chap) => !this.lockedChapters.has(chap.id))
if (this.newChapters[1].start + amount <= 0) {
this.$toast.error(this.$strings.ToastChaptersInvalidShiftAmountStart)
if (unlockedChapters.length === 0) {
this.$toast.warning(this.$strings.ToastChaptersAllLocked)
return
}
for (let i = 0; i < this.newChapters.length; i++) {
const chap = this.newChapters[i]
// Skip locked chapters
if (this.lockedChapters.has(chap.id)) {
continue
}
chap.end = Math.min(chap.end + amount, this.mediaDuration)
if (i > 0) {
chap.start = Math.max(0, chap.start + amount)
@@ -354,6 +459,83 @@ export default {
}
this.checkChapters()
},
incrementChapterTime(chapter, amount) {
if (chapter.id === 0 && chapter.start + amount < 0) {
return
}
if (chapter.start + amount >= this.mediaDuration) {
return
}
chapter.start = Math.max(0, chapter.start + amount)
this.checkChapters()
},
adjustChapterStartTime(chapter) {
const newStartTime = chapter.start + this.elapsedTime
chapter.start = newStartTime
this.checkChapters()
this.$toast.success(this.$strings.ToastChapterStartTimeAdjusted.replace('{0}', this.elapsedTime))
this.destroyAudioEl()
},
startElapsedTimeTracking() {
this.elapsedTime = 0
this.playStartTime = Date.now()
this.elapsedTimeInterval = setInterval(() => {
this.elapsedTime = Math.floor((Date.now() - this.playStartTime) / 1000)
}, 100)
},
stopElapsedTimeTracking() {
if (this.elapsedTimeInterval) {
clearInterval(this.elapsedTimeInterval)
this.elapsedTimeInterval = null
}
this.elapsedTime = 0
this.playStartTime = null
},
toggleChapterLock(chapter, event) {
const chapterId = chapter.id
if (event.shiftKey && this.lastSelectedLockIndex !== null) {
const startIndex = Math.min(this.lastSelectedLockIndex, chapterId)
const endIndex = Math.max(this.lastSelectedLockIndex, chapterId)
const shouldLock = !this.lockedChapters.has(chapterId)
for (let i = startIndex; i <= endIndex; i++) {
if (shouldLock) {
this.lockedChapters.add(i)
} else {
this.lockedChapters.delete(i)
}
}
} else {
if (this.lockedChapters.has(chapterId)) {
this.lockedChapters.delete(chapterId)
} else {
this.lockedChapters.add(chapterId)
}
}
this.lastSelectedLockIndex = chapterId
this.lockedChapters = new Set(this.lockedChapters)
},
lockAllChapters() {
this.newChapters.forEach((chapter) => {
this.lockedChapters.add(chapter.id)
})
this.lockedChapters = new Set(this.lockedChapters)
},
unlockAllChapters() {
this.lockedChapters.clear()
this.lockedChapters = new Set(this.lockedChapters)
},
toggleAllChaptersLock() {
if (this.allChaptersLocked) {
this.unlockAllChapters()
} else {
this.lockAllChapters()
}
},
editItem() {
this.$store.commit('showEditModal', this.libraryItem)
},
@@ -368,6 +550,10 @@ export default {
this.checkChapters()
},
removeChapter(chapter) {
if (this.lockedChapters.has(chapter.id)) {
this.$toast.warning(this.$strings.ToastChapterLocked)
return
}
this.newChapters = this.newChapters.filter((ch) => ch.id !== chapter.id)
this.checkChapters()
},
@@ -408,6 +594,14 @@ export default {
this.hasChanges = hasChanges
},
getAudioTrackForTime(time) {
if (typeof time !== 'number') {
return null
}
return this.tracks.find((at) => {
return time >= at.startOffset && time < at.startOffset + at.duration
})
},
playChapter(chapter) {
console.log('Play Chapter', chapter.id)
if (this.selectedChapterId === chapter.id) {
@@ -422,9 +616,12 @@ export default {
this.destroyAudioEl()
}
const audioTrack = this.tracks.find((at) => {
return chapter.start >= at.startOffset && chapter.start < at.startOffset + at.duration
})
const audioTrack = this.getAudioTrackForTime(chapter.start)
if (!audioTrack) {
console.error('No audio track found for chapter', chapter)
return
}
this.selectedChapter = chapter
this.isLoadingChapter = true
@@ -451,6 +648,7 @@ export default {
console.log('Audio playing')
this.isLoadingChapter = false
this.isPlayingChapter = true
this.startElapsedTimeTracking()
})
audioEl.addEventListener('ended', () => {
console.log('Audio ended')
@@ -473,6 +671,10 @@ export default {
this.selectedChapter = null
this.isPlayingChapter = false
this.isLoadingChapter = false
this.stopElapsedTimeTracking()
},
resetChapterLookupData() {
this.chapterData = null
},
saveChapters() {
this.checkChapters()
@@ -506,11 +708,7 @@ export default {
this.saving = false
if (data.updated) {
this.$toast.success(this.$strings.ToastChaptersUpdated)
if (this.previousRoute) {
this.$router.push(this.previousRoute)
} else {
this.$router.push(`/item/${this.libraryItem.id}`)
}
this.reloadLibraryItem()
} else {
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
}
@@ -523,7 +721,7 @@ export default {
},
applyChapterNamesOnly() {
this.newChapters.forEach((chapter, index) => {
if (this.chapterData.chapters[index]) {
if (this.chapterData.chapters[index] && !this.lockedChapters.has(chapter.id)) {
chapter.title = this.chapterData.chapters[index].title
}
})
@@ -535,7 +733,7 @@ export default {
},
applyChapterData() {
let index = 0
this.newChapters = this.chapterData.chapters
const audibleChapters = this.chapterData.chapters
.filter((chap) => chap.startOffsetSec < this.mediaDuration)
.map((chap) => {
return {
@@ -545,6 +743,21 @@ export default {
title: chap.title
}
})
const merged = []
let audibleIdx = 0
for (let i = 0; i < Math.max(this.newChapters.length, audibleChapters.length); i++) {
const isLocked = this.lockedChapters.has(i)
if (isLocked && this.newChapters[i]) {
merged.push({ ...this.newChapters[i], id: i })
} else if (audibleChapters[audibleIdx]) {
merged.push({ ...audibleChapters[audibleIdx], id: i })
audibleIdx++
} else if (this.newChapters[i]) {
merged.push({ ...this.newChapters[i], id: i })
}
}
this.newChapters = merged
this.showFindChaptersModal = false
this.chapterData = null
@@ -572,7 +785,7 @@ export default {
if (data.error) {
this.asinError = this.$getString(data.stringKey)
} else {
console.log('Chapter data', data)
console.log('Chapter data', { ...data })
this.chapterData = this.removeBranding ? this.removeBrandingFromData(data) : data
}
})
@@ -609,6 +822,11 @@ export default {
data.chapters.pop()
}
// Remove Branding durations from Runtime totals
data.runtimeLengthMs -= introDuration + outroDuration
data.runtimeLengthSec = Math.floor(data.runtimeLengthMs / 1000)
console.log('Brandless Chapter data', data)
return data
} catch {
return data
@@ -638,6 +856,7 @@ export default {
}
]
}
this.lockedChapters = new Set()
this.checkChapters()
},
removeAllChaptersClick() {
@@ -662,11 +881,7 @@ export default {
.then((data) => {
if (data.updated) {
this.$toast.success(this.$strings.ToastChaptersRemoved)
if (this.previousRoute) {
this.$router.push(this.previousRoute)
} else {
this.$router.push(`/item/${this.libraryItem.id}`)
}
this.reloadLibraryItem()
} else {
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
}
@@ -679,6 +894,91 @@ export default {
this.saving = false
})
},
handleBulkChapterAdd() {
const input = this.bulkChapterInput.trim()
if (!input) return
const numberMatch = input.match(/(\d+)/)
if (numberMatch) {
// Extract the base pattern and number, preserving zero-padding
const originalNumberString = numberMatch[1]
const foundNumber = parseInt(originalNumberString)
const numberIndex = numberMatch.index
const beforeNumber = input.substring(0, numberIndex)
const afterNumber = input.substring(numberIndex + originalNumberString.length)
this.detectedPattern = {
before: beforeNumber,
after: afterNumber,
startingNumber: foundNumber,
originalPadding: originalNumberString.length,
hasLeadingZeros: originalNumberString.length > 1 && originalNumberString.startsWith('0')
}
this.bulkChapterCount = 1
this.showBulkChapterModal = true
} else {
this.addSingleChapterFromInput(input)
}
},
addSingleChapterFromInput(title) {
// Find the last chapter to determine where to add the new one
const lastChapter = this.newChapters[this.newChapters.length - 1]
const newStart = lastChapter ? lastChapter.end : 0
const newEnd = Math.min(newStart + 300, this.mediaDuration)
const newChapter = {
id: this.newChapters.length,
start: newStart,
end: newEnd,
title: title
}
this.newChapters.push(newChapter)
this.bulkChapterInput = ''
this.checkChapters()
},
addBulkChapters() {
const count = parseInt(this.bulkChapterCount)
if (!count || count < 1 || count > 150) {
this.$toast.error(this.$strings.ToastBulkChapterInvalidCount)
return
}
const { before, after, startingNumber, originalPadding, hasLeadingZeros } = this.detectedPattern
const lastChapter = this.newChapters[this.newChapters.length - 1]
const baseStart = lastChapter ? lastChapter.start + 1 : 0
// Add multiple chapters with the detected pattern
for (let i = 0; i < count; i++) {
const chapterNumber = startingNumber + i
let formattedNumber = chapterNumber.toString()
// Apply zero-padding if the original had leading zeros
if (hasLeadingZeros && originalPadding > 1) {
formattedNumber = chapterNumber.toString().padStart(originalPadding, '0')
}
const newStart = baseStart + i
const newEnd = Math.min(newStart + i + i, this.mediaDuration)
const newChapter = {
id: this.newChapters.length,
start: newStart,
end: newEnd,
title: `${before}${formattedNumber}${after}`
}
this.newChapters.push(newChapter)
}
this.bulkChapterInput = ''
this.showBulkChapterModal = false
this.detectedPattern = null
this.checkChapters()
},
libraryItemUpdated(libraryItem) {
if (libraryItem.id === this.libraryItem.id) {
if (!!libraryItem.media.metadata.asin && this.mediaMetadata.asin !== libraryItem.media.metadata.asin) {
@@ -686,6 +986,18 @@ export default {
}
this.libraryItem = libraryItem
}
},
reloadLibraryItem() {
this.$axios
.$get(`/api/items/${this.libraryItem.id}?expanded=1`)
.then((data) => {
this.libraryItem = data
this.initChapters()
})
.catch((error) => {
console.error('Failed to reload library item', error)
this.$toast.error(this.$strings.ToastFailedToLoadData)
})
}
},
mounted() {
+1
View File
@@ -53,6 +53,7 @@ export default {
else if (pageName === 'sessions') return this.$strings.HeaderListeningSessions
else if (pageName === 'stats') return this.$strings.HeaderYourStats
else if (pageName === 'users') return this.$strings.HeaderUsers
else if (pageName === 'api-keys') return this.$strings.HeaderApiKeys
else if (pageName === 'item-metadata-utils') return this.$strings.HeaderItemMetadataUtils
else if (pageName === 'rss-feeds') return this.$strings.HeaderRSSFeeds
else if (pageName === 'email') return this.$strings.HeaderEmail
+84
View File
@@ -0,0 +1,84 @@
<template>
<div>
<app-settings-content :header-text="$strings.HeaderApiKeys">
<template #header-items>
<div v-if="numApiKeys" class="mx-2 px-1.5 rounded-lg bg-primary/50 text-gray-300/90 text-sm inline-flex items-center justify-center">
<span>{{ numApiKeys }}</span>
</div>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://audiobookshelf.org/docs/documentation/server-management/api-keys" target="_blank" class="inline-flex">
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
</a>
</ui-tooltip>
<div class="grow" />
<ui-btn color="bg-primary" :disabled="loadingUsers || users.length === 0" small @click="setShowApiKeyModal()">{{ $strings.ButtonAddApiKey }}</ui-btn>
</template>
<tables-api-keys-table ref="apiKeysTable" class="pt-2" @edit="setShowApiKeyModal" @numApiKeys="(count) => (numApiKeys = count)" />
</app-settings-content>
<modals-api-key-modal ref="apiKeyModal" v-model="showApiKeyModal" :api-key="selectedApiKey" :users="users" @created="apiKeyCreated" @updated="apiKeyUpdated" />
<modals-api-key-created-modal ref="apiKeyCreatedModal" v-model="showApiKeyCreatedModal" :api-key="selectedApiKey" />
</div>
</template>
<script>
export default {
asyncData({ store, redirect }) {
if (!store.getters['user/getIsAdminOrUp']) {
redirect('/')
}
},
data() {
return {
loadingUsers: false,
selectedApiKey: null,
showApiKeyModal: false,
showApiKeyCreatedModal: false,
numApiKeys: 0,
users: []
}
},
methods: {
apiKeyCreated(apiKey) {
this.numApiKeys++
this.selectedApiKey = apiKey
this.showApiKeyCreatedModal = true
if (this.$refs.apiKeysTable) {
this.$refs.apiKeysTable.addApiKey(apiKey)
}
},
apiKeyUpdated(apiKey) {
if (this.$refs.apiKeysTable) {
this.$refs.apiKeysTable.updateApiKey(apiKey)
}
},
setShowApiKeyModal(selectedApiKey) {
this.selectedApiKey = selectedApiKey
this.showApiKeyModal = true
},
loadUsers() {
this.loadingUsers = true
this.$axios
.$get('/api/users')
.then((res) => {
this.users = res.users.sort((a, b) => {
return a.createdAt - b.createdAt
})
})
.catch((error) => {
console.error('Failed', error)
})
.finally(() => {
this.loadingUsers = false
})
}
},
mounted() {
this.loadUsers()
},
beforeDestroy() {}
}
</script>
+1 -1
View File
@@ -24,7 +24,7 @@
<ui-checkbox v-model="enableOpenIDAuth" checkbox-bg="bg" />
<p class="text-lg pl-4">{{ $strings.HeaderOpenIDConnectAuthentication }}</p>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/oidc_authentication" target="_blank" class="inline-flex">
<a href="https://audiobookshelf.org/docs/documentation/server-management/oidc-authentication" target="_blank" class="inline-flex">
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
</a>
</ui-tooltip>
+1 -1
View File
@@ -3,7 +3,7 @@
<app-settings-content :header-text="$strings.HeaderEmailSettings" :description="''">
<template #header-items>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/send_to_ereader" target="_blank" class="inline-flex">
<a href="https://audiobookshelf.org/docs/documentation/libraries/book-library/ebooks" target="_blank" class="inline-flex">
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
</a>
</ui-tooltip>
+35 -19
View File
@@ -131,35 +131,26 @@
</div>
<div class="grow py-2">
<ui-dropdown :label="$strings.LabelSettingsDateFormat" v-model="newServerSettings.dateFormat" :items="dateFormats" small class="max-w-52" @input="(val) => updateSettingsKey('dateFormat', val)" />
<ui-dropdown :label="$strings.LabelSettingsDateFormat" v-model="newServerSettings.dateFormat" :items="dateFormats" small class="max-w-72" @input="(val) => updateSettingsKey('dateFormat', val)" />
<p class="text-xs ml-1 text-white/60">{{ $strings.LabelExample }}: {{ dateExample }}</p>
</div>
<div class="grow py-2">
<ui-dropdown :label="$strings.LabelSettingsTimeFormat" v-model="newServerSettings.timeFormat" :items="timeFormats" small class="max-w-52" @input="(val) => updateSettingsKey('timeFormat', val)" />
<ui-dropdown :label="$strings.LabelSettingsTimeFormat" v-model="newServerSettings.timeFormat" :items="timeFormats" small class="max-w-72" @input="(val) => updateSettingsKey('timeFormat', val)" />
<p class="text-xs ml-1 text-white/60">{{ $strings.LabelExample }}: {{ timeExample }}</p>
</div>
<div class="py-2">
<ui-dropdown :label="$strings.LabelLanguageDefaultServer" ref="langDropdown" v-model="newServerSettings.language" :items="$languageCodeOptions" small class="max-w-52" @input="updateServerLanguage" />
<ui-dropdown :label="$strings.LabelLanguageDefaultServer" ref="langDropdown" v-model="newServerSettings.language" :items="$languageCodeOptions" small class="max-w-72" @input="updateServerLanguage" />
</div>
<!-- old experimental features -->
<!-- <div class="pt-4">
<h2 class="font-semibold">{{ $strings.HeaderSettingsExperimental }}</h2>
<div class="pt-4">
<h2 class="font-semibold">{{ $strings.HeaderSettingsSecurity }}</h2>
</div>
<div class="flex items-center py-2">
<ui-toggle-switch labeledBy="settings-experimental-features" v-model="showExperimentalFeatures" />
<ui-tooltip :text="$strings.LabelSettingsExperimentalFeaturesHelp">
<p class="pl-4">
<span id="settings-experimental-features">{{ $strings.LabelSettingsExperimentalFeatures }}</span>
<a :aria-label="$strings.LabelSettingsExperimentalFeaturesHelp" href="https://github.com/advplyr/audiobookshelf/discussions/75" target="_blank">
<span class="material-symbols icon-text">info</span>
</a>
</p>
</ui-tooltip>
</div> -->
<div class="py-2">
<ui-multi-select v-model="newServerSettings.allowedOrigins" :items="newServerSettings.allowedOrigins" :label="$strings.LabelCorsAllowed" class="max-w-72" @input="updateCorsOrigins" />
</div>
</div>
</div>
</app-settings-content>
@@ -256,7 +247,8 @@ export default {
return this.$store.state.serverSettings
},
providers() {
return this.$store.state.scanners.providers
// Use book cover providers for the cover provider dropdown
return this.$store.state.scanners.bookCoverProviders || []
},
dateFormats() {
return this.$store.state.globals.dateFormats
@@ -323,6 +315,27 @@ export default {
updateServerLanguage(val) {
this.updateSettingsKey('language', val)
},
updateCorsOrigins(val) {
const validOrigins = []
const invalidOrigins = []
val.forEach((origin) => {
const trimmedOrigin = origin.trim().toLowerCase()
try {
new URL(trimmedOrigin)
validOrigins.push(trimmedOrigin)
} catch {
invalidOrigins.push(trimmedOrigin)
}
})
if (invalidOrigins.length > 0) {
this.$toast.error(this.$strings.ToastInvalidUrls)
}
this.newServerSettings.allowedOrigins = validOrigins
this.updateSettingsKey('allowedOrigins', validOrigins)
},
updateSettingsKey(key, val) {
if (key === 'scannerDisableWatcher') {
this.newServerSettings.scannerDisableWatcher = val
@@ -352,6 +365,7 @@ export default {
initServerSettings() {
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
this.newServerSettings.sortingPrefixes = [...(this.newServerSettings.sortingPrefixes || [])]
this.newServerSettings.allowedOrigins = [...(this.newServerSettings.allowedOrigins || [])]
this.scannerEnableWatcher = !this.newServerSettings.scannerDisableWatcher
this.homepageUseBookshelfView = this.newServerSettings.homeBookshelfView != this.$constants.BookshelfView.DETAIL
@@ -376,8 +390,8 @@ export default {
},
purgeItemsCache() {
const payload = {
// message: `This will delete the entire folder at <code>/metadata/cache/items</code>.<br />Are you sure you want to purge items cache?`,
message: this.$strings.MessageConfirmPurgeItemsCache,
allowHtml: true,
callback: (confirmed) => {
if (confirmed) {
this.sendPurgeItemsCache()
@@ -403,6 +417,8 @@ export default {
},
mounted() {
this.initServerSettings()
// Fetch providers if not already loaded (for cover provider dropdown)
this.$store.dispatch('scanners/fetchProviders')
}
}
</script>
@@ -8,7 +8,7 @@
</template>
<template #header-items>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/custom-metadata-providers" target="_blank" class="inline-flex">
<a href="https://audiobookshelf.org/docs/documentation/community/community-providers" target="_blank" class="inline-flex">
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
</a>
</ui-tooltip>
@@ -90,9 +90,9 @@ export default {
let message = this.$getString('MessageConfirmRenameGenre', [this.editingGenre, this.newGenreName])
if (genreNameExists) {
message += `<br><span class="text-sm">${this.$strings.MessageConfirmRenameGenreMergeNote}</span>`
message += ` ${this.$strings.MessageConfirmRenameGenreMergeNote}`
} else if (genreNameExistsOfDifferentCase) {
message += `<br><span class="text-warning text-sm">${this.$getString('MessageConfirmRenameGenreWarning', [genreNameExistsOfDifferentCase])}</span>`
message += ` ${this.$getString('MessageConfirmRenameGenreWarning', [genreNameExistsOfDifferentCase])}`
}
const payload = {
@@ -86,9 +86,9 @@ export default {
let message = this.$getString('MessageConfirmRenameTag', [this.editingTag, this.newTagName])
if (tagNameExists) {
message += `<br><span class="text-sm">${this.$strings.MessageConfirmRenameTagMergeNote}</span>`
message += ` ${this.$strings.MessageConfirmRenameTagMergeNote}`
} else if (tagNameExistsOfDifferentCase) {
message += `<br><span class="text-warning text-sm">${this.$getString('MessageConfirmRenameTagWarning', [tagNameExistsOfDifferentCase])}</span>`
message += ` ${this.$getString('MessageConfirmRenameTagWarning', [tagNameExistsOfDifferentCase])}`
}
const payload = {
+1 -1
View File
@@ -3,7 +3,7 @@
<app-settings-content :header-text="$strings.HeaderLibraries">
<template #header-items>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/library_creation" target="_blank" class="inline-flex">
<a href="https://audiobookshelf.org/docs/documentation/libraries/common-content/overview" target="_blank" class="inline-flex">
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
</a>
</ui-tooltip>
+1 -1
View File
@@ -3,7 +3,7 @@
<app-settings-content :header-text="$strings.HeaderLogs" :description="$strings.MessageLogsDescription">
<template #header-items>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/server_logs" target="_blank" class="inline-flex">
<a href="https://audiobookshelf.org/docs/documentation/server-management/server-logs" target="_blank" class="inline-flex">
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
</a>
</ui-tooltip>
+1 -1
View File
@@ -3,7 +3,7 @@
<app-settings-content :header-text="$strings.HeaderRSSFeeds">
<template #header-items>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/rss_feeds" target="_blank" class="inline-flex">
<a href="https://audiobookshelf.org/docs/documentation/libraries/common-content/public-shares#rss-feed" target="_blank" class="inline-flex">
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
</a>
</ui-tooltip>
+93 -79
View File
@@ -6,80 +6,86 @@
</div>
<div v-if="listeningSessions.length" class="block max-w-full relative">
<table class="userSessionsTable">
<tr class="bg-primary/40">
<th class="w-6 min-w-6 text-left hidden md:table-cell h-11">
<ui-checkbox v-model="isAllSelected" :partial="numSelected > 0 && !isAllSelected" small checkbox-bg="bg" />
</th>
<th v-if="numSelected" class="grow text-left" :colspan="7">
<div class="flex items-center">
<p>{{ $getString('MessageSelected', [numSelected]) }}</p>
<div class="grow" />
<ui-btn small color="bg-error" :loading="deletingSessions" @click.stop="removeSessionsClick">{{ $strings.ButtonRemove }}</ui-btn>
</div>
</th>
<th v-if="!numSelected" class="grow sm:grow-0 sm:w-48 sm:max-w-48 text-left group cursor-pointer" @click.stop="sortColumn('displayTitle')">
<div class="inline-flex items-center">
{{ $strings.LabelItem }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('displayTitle') }" class="material-symbols text-base pl-px">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
</div>
</th>
<th v-if="!numSelected" class="w-20 min-w-20 text-left hidden md:table-cell">{{ $strings.LabelUser }}</th>
<th v-if="!numSelected" class="w-26 min-w-26 text-left hidden md:table-cell group cursor-pointer" @click.stop="sortColumn('playMethod')">
<div class="inline-flex items-center">
{{ $strings.LabelPlayMethod }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('playMethod') }" class="material-symbols text-base pl-px">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
</div>
</th>
<th v-if="!numSelected" class="w-32 min-w-32 text-left hidden sm:table-cell">{{ $strings.LabelDeviceInfo }}</th>
<th v-if="!numSelected" class="w-24 min-w-24 sm:w-32 sm:min-w-32 group cursor-pointer" @click.stop="sortColumn('timeListening')">
<div class="inline-flex items-center">
{{ $strings.LabelTimeListened }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('timeListening') }" class="material-symbols text-base pl-px hidden sm:inline-block">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
</div>
</th>
<th v-if="!numSelected" class="w-24 min-w-24 group cursor-pointer" @click.stop="sortColumn('currentTime')">
<div class="inline-flex items-center">
{{ $strings.LabelLastTime }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('currentTime') }" class="material-symbols text-base pl-px hidden sm:inline-block">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
</div>
</th>
<th v-if="!numSelected" class="grow hidden sm:table-cell cursor-pointer group" @click.stop="sortColumn('updatedAt')">
<div class="inline-flex items-center">
{{ $strings.LabelLastUpdate }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('updatedAt') }" class="material-symbols text-base pl-px">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
</div>
</th>
</tr>
<div class="overflow-x-auto">
<table class="userSessionsTable">
<tr class="bg-primary/40">
<th class="w-6 min-w-6 text-left hidden md:table-cell h-11">
<ui-checkbox v-model="isAllSelected" :partial="numSelected > 0 && !isAllSelected" small checkbox-bg="bg" />
</th>
<th v-if="numSelected" class="grow text-left" :colspan="7">
<div class="flex items-center">
<p>{{ $getString('MessageSelected', [numSelected]) }}</p>
<div class="grow" />
<ui-btn small color="bg-error" :loading="deletingSessions" @click.stop="removeSessionsClick">{{ $strings.ButtonRemove }}</ui-btn>
</div>
</th>
<th v-if="!numSelected" class="grow sm:grow-0 sm:w-48 sm:max-w-48 text-left group cursor-pointer" @click.stop="sortColumn('displayTitle')">
<div class="inline-flex items-center">
{{ $strings.LabelItem }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('displayTitle') }" class="material-symbols text-base pl-px">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
</div>
</th>
<th v-if="!numSelected" class="w-20 min-w-20 text-left hidden md:table-cell">{{ $strings.LabelUser }}</th>
<th v-if="!numSelected" class="w-26 min-w-26 text-left hidden md:table-cell group cursor-pointer" @click.stop="sortColumn('playMethod')">
<div class="inline-flex items-center">
{{ $strings.LabelPlayMethod }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('playMethod') }" class="material-symbols text-base pl-px">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
</div>
</th>
<th v-if="!numSelected" class="w-32 min-w-32 text-left hidden sm:table-cell">{{ $strings.LabelDeviceInfo }}</th>
<th v-if="!numSelected" class="w-24 min-w-24 sm:w-32 sm:min-w-32 group cursor-pointer" @click.stop="sortColumn('timeListening')">
<div class="inline-flex items-center">
{{ $strings.LabelTimeListened }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('timeListening') }" class="material-symbols text-base pl-px hidden sm:inline-block">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
</div>
</th>
<th v-if="!numSelected" class="w-24 min-w-24 group cursor-pointer" @click.stop="sortColumn('currentTime')">
<div class="inline-flex items-center">
{{ $strings.LabelLastTime }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('currentTime') }" class="material-symbols text-base pl-px hidden sm:inline-block">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
</div>
</th>
<th v-if="!numSelected" class="grow hidden sm:table-cell cursor-pointer group" @click.stop="sortColumn('updatedAt')">
<div class="inline-flex items-center">
{{ $strings.LabelLastUpdate }} <span :class="{ 'opacity-0 group-hover:opacity-30': !isSortSelected('updatedAt') }" class="material-symbols text-base pl-px">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
</div>
</th>
</tr>
<tr v-for="session in listeningSessions" :key="session.id" :class="{ selected: session.selected }" class="cursor-pointer" @click="clickSessionRow(session)">
<td class="hidden md:table-cell py-1 max-w-6 relative">
<ui-checkbox v-model="session.selected" small checkbox-bg="bg" />
<!-- overlay of the checkbox so that the entire box is clickable -->
<div class="absolute inset-0 w-full h-full" @click.stop="session.selected = !session.selected" />
</td>
<td class="py-1 grow sm:grow-0 sm:w-48 sm:max-w-48">
<p class="text-xs text-gray-200 truncate">{{ session.displayTitle }}</p>
<p class="text-xs text-gray-400 truncate">{{ session.displayAuthor }}</p>
</td>
<td class="hidden md:table-cell w-20 min-w-20">
<p v-if="filteredUserUsername" class="text-xs">{{ filteredUserUsername }}</p>
<p v-else class="text-xs">{{ session.user ? session.user.username : 'N/A' }}</p>
</td>
<td class="hidden md:table-cell w-26 min-w-26">
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
</td>
<td class="hidden sm:table-cell max-w-32 min-w-32">
<p class="text-xs truncate" v-html="getDeviceInfoString(session.deviceInfo)" />
</td>
<td class="text-center w-24 min-w-24 sm:w-32 sm:min-w-32">
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
</td>
<td class="text-center hover:underline w-24 min-w-24" @click.stop="clickCurrentTime(session)">
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
</td>
<td class="text-center hidden sm:table-cell">
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDatetime(session.updatedAt, dateFormat, timeFormat)">
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
</ui-tooltip>
</td>
</tr>
</table>
<tr v-for="session in listeningSessions" :key="session.id" :class="{ selected: session.selected }" class="cursor-pointer" @click="clickSessionRow(session)">
<td class="hidden md:table-cell py-1 max-w-6 relative">
<ui-checkbox v-model="session.selected" small checkbox-bg="bg" />
<!-- overlay of the checkbox so that the entire box is clickable -->
<div class="absolute inset-0 w-full h-full" @click.stop="session.selected = !session.selected" />
</td>
<td class="py-1 grow sm:grow-0 sm:w-48 sm:max-w-48">
<p class="text-xs text-gray-200 truncate">{{ session.displayTitle }}</p>
<p class="text-xs text-gray-400 truncate">{{ session.displayAuthor }}</p>
</td>
<td class="hidden md:table-cell w-20 min-w-20">
<p v-if="filteredUserUsername" class="text-xs">{{ filteredUserUsername }}</p>
<p v-else class="text-xs">{{ session.user ? session.user.username : 'N/A' }}</p>
</td>
<td class="hidden md:table-cell w-26 min-w-26">
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
</td>
<td class="hidden sm:table-cell max-w-32 min-w-32">
<p class="text-xs truncate">
<template v-for="(line, index) in getDeviceInfoLines(session.deviceInfo)">
<br v-if="index > 0" :key="'br-' + index" />{{ line }}
</template>
</p>
</td>
<td class="text-center w-24 min-w-24 sm:w-32 sm:min-w-32">
<p class="text-xs font-mono">{{ $elapsedPrettyLocalized(session.timeListening) }}</p>
</td>
<td class="text-center hover:underline w-24 min-w-24" @click.stop="clickCurrentTime(session)">
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
</td>
<td class="text-center hidden sm:table-cell">
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDatetime(session.updatedAt, dateFormat, timeFormat)">
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
</ui-tooltip>
</td>
</tr>
</table>
</div>
<!-- table bottom options -->
<div class="flex items-center my-2">
<div class="grow" />
@@ -128,7 +134,11 @@
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
</td>
<td class="hidden sm:table-cell max-w-32 min-w-32">
<p class="text-xs truncate" v-html="getDeviceInfoString(session.deviceInfo)" />
<p class="text-xs truncate">
<template v-for="(line, index) in getDeviceInfoLines(session.deviceInfo)">
<br v-if="index > 0" :key="'br-' + index" />{{ line }}
</template>
</p>
</td>
<td class="text-center">
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
@@ -170,7 +180,11 @@
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
</td>
<td class="hidden sm:table-cell max-w-32 min-w-32">
<p class="text-xs truncate" v-html="getDeviceInfoString(session.deviceInfo)" />
<p class="text-xs truncate">
<template v-for="(line, index) in getDeviceInfoLines(session.deviceInfo)">
<br v-if="index > 0" :key="'br-' + index" />{{ line }}
</template>
</p>
</td>
<td class="text-center hover:underline" @click.stop="clickCurrentTime(session)">
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
@@ -431,16 +445,16 @@ export default {
this.selectedSession = session
this.showSessionModal = true
},
getDeviceInfoString(deviceInfo) {
if (!deviceInfo) return ''
var lines = []
getDeviceInfoLines(deviceInfo) {
if (!deviceInfo) return []
const lines = []
if (deviceInfo.clientName) lines.push(`${deviceInfo.clientName} ${deviceInfo.clientVersion || ''}`)
if (deviceInfo.osName) lines.push(`${deviceInfo.osName} ${deviceInfo.osVersion}`)
if (deviceInfo.browserName) lines.push(deviceInfo.browserName)
if (deviceInfo.manufacturer && deviceInfo.model) lines.push(`${deviceInfo.manufacturer} ${deviceInfo.model}`)
if (deviceInfo.sdkVersion) lines.push(`SDK Version: ${deviceInfo.sdkVersion}`)
return lines.join('<br>')
return lines
},
getPlayMethodName(playMethod) {
if (playMethod === this.$constants.PlayMethod.DIRECTPLAY) return 'Direct Play'
+8 -3
View File
@@ -13,8 +13,10 @@
<widgets-online-indicator :value="!!userOnline" />
<h1 class="text-xl pl-2">{{ username }}</h1>
</div>
<div v-if="userToken" class="flex text-xs mt-4">
<ui-text-input-with-label :label="$strings.LabelApiToken" :value="userToken" readonly show-copy />
<div v-if="legacyToken" class="text-xs space-y-2 mt-4">
<ui-text-input-with-label label="Legacy API Token" :value="legacyToken" readonly show-copy />
<p class="text-warning" v-html="$strings.MessageAuthenticationLegacyTokenWarning" />
</div>
<div class="w-full h-px bg-white/10 my-2" />
<div class="py-2">
@@ -100,9 +102,12 @@ export default {
}
},
computed: {
userToken() {
legacyToken() {
return this.user.token
},
userToken() {
return this.user.accessToken
},
bookCoverAspectRatio() {
return this.$store.getters['libraries/getBookCoverAspectRatio']
},
+42 -36
View File
@@ -19,39 +19,45 @@
<div class="py-2">
<h1 class="text-lg mb-2 text-white/90 px-2 sm:px-0">{{ $strings.HeaderListeningSessions }}</h1>
<div v-if="listeningSessions.length">
<table class="userSessionsTable">
<tr class="bg-primary/40">
<th class="w-48 min-w-48 text-left">{{ $strings.LabelItem }}</th>
<th class="w-32 min-w-32 text-left hidden md:table-cell">{{ $strings.LabelPlayMethod }}</th>
<th class="w-32 min-w-32 text-left hidden sm:table-cell">{{ $strings.LabelDeviceInfo }}</th>
<th class="w-32 min-w-32">{{ $strings.LabelTimeListened }}</th>
<th class="w-16 min-w-16">{{ $strings.LabelLastTime }}</th>
<th class="grow hidden sm:table-cell">{{ $strings.LabelLastUpdate }}</th>
</tr>
<tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)">
<td class="py-1 max-w-48">
<p class="text-xs text-gray-200 truncate">{{ session.displayTitle }}</p>
<p class="text-xs text-gray-400 truncate">{{ session.displayAuthor }}</p>
<div class="overflow-x-auto">
<table class="userSessionsTable">
<tr class="bg-primary/40">
<th class="w-48 min-w-48 text-left">{{ $strings.LabelItem }}</th>
<th class="w-32 min-w-32 text-left hidden md:table-cell">{{ $strings.LabelPlayMethod }}</th>
<th class="w-32 min-w-32 text-left hidden sm:table-cell">{{ $strings.LabelDeviceInfo }}</th>
<th class="w-32 min-w-32">{{ $strings.LabelTimeListened }}</th>
<th class="w-16 min-w-16">{{ $strings.LabelLastTime }}</th>
<th class="grow hidden sm:table-cell">{{ $strings.LabelLastUpdate }}</th>
</tr>
<tr v-for="session in listeningSessions" :key="session.id" class="cursor-pointer" @click="showSession(session)">
<td class="py-1 max-w-48">
<p class="text-xs text-gray-200 truncate">{{ session.displayTitle }}</p>
<p class="text-xs text-gray-400 truncate">{{ session.displayAuthor }}</p>
</td>
<td class="hidden md:table-cell">
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
</td>
<td class="hidden sm:table-cell min-w-32 max-w-32">
<p class="text-xs truncate">
<template v-for="(line, index) in getDeviceInfoLines(session.deviceInfo)">
<br v-if="index > 0" :key="'br-' + index" />{{ line }}
</template>
</p>
</td>
<td class="hidden md:table-cell">
<p class="text-xs">{{ getPlayMethodName(session.playMethod) }}</p>
</td>
<td class="hidden sm:table-cell min-w-32 max-w-32">
<p class="text-xs truncate" v-html="getDeviceInfoString(session.deviceInfo)" />
</td>
<td class="text-center">
<p class="text-xs font-mono">{{ $elapsedPretty(session.timeListening) }}</p>
</td>
<td class="text-center hover:underline" @click.stop="clickCurrentTime(session)">
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
</td>
<td class="text-center hidden sm:table-cell">
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDatetime(session.updatedAt, dateFormat, timeFormat)">
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
</ui-tooltip>
</td>
</tr>
</table>
<td class="text-center">
<p class="text-xs font-mono">{{ $elapsedPrettyLocalized(session.timeListening) }}</p>
</td>
<td class="text-center hover:underline" @click.stop="clickCurrentTime(session)">
<p class="text-xs font-mono">{{ $secondsToTimestamp(session.currentTime) }}</p>
</td>
<td class="text-center hidden sm:table-cell">
<ui-tooltip v-if="session.updatedAt" direction="top" :text="$formatDatetime(session.updatedAt, dateFormat, timeFormat)">
<p class="text-xs text-gray-200">{{ $dateDistanceFromNow(session.updatedAt) }}</p>
</ui-tooltip>
</td>
</tr>
</table>
</div>
<div class="flex items-center justify-end py-1">
<ui-icon-btn icon="arrow_back_ios_new" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" />
<p class="text-sm mx-1">{{ $getString('LabelPaginationPageXOfY', [currentPage + 1, numPages]) }}</p>
@@ -191,16 +197,16 @@ export default {
this.selectedSession = session
this.showSessionModal = true
},
getDeviceInfoString(deviceInfo) {
if (!deviceInfo) return ''
var lines = []
getDeviceInfoLines(deviceInfo) {
if (!deviceInfo) return []
const lines = []
if (deviceInfo.clientName) lines.push(`${deviceInfo.clientName} ${deviceInfo.clientVersion || ''}`)
if (deviceInfo.osName) lines.push(`${deviceInfo.osName} ${deviceInfo.osVersion}`)
if (deviceInfo.browserName) lines.push(deviceInfo.browserName)
if (deviceInfo.manufacturer && deviceInfo.model) lines.push(`${deviceInfo.manufacturer} ${deviceInfo.model}`)
if (deviceInfo.sdkVersion) lines.push(`SDK Version: ${deviceInfo.sdkVersion}`)
return lines.join('<br>')
return lines
},
getPlayMethodName(playMethod) {
if (playMethod === this.$constants.PlayMethod.DIRECTPLAY) return 'Direct Play'
+1 -1
View File
@@ -7,7 +7,7 @@
</div>
<ui-tooltip :text="$strings.LabelClickForMoreInfo" class="inline-flex ml-2">
<a href="https://www.audiobookshelf.org/guides/users" target="_blank" class="inline-flex">
<a href="https://audiobookshelf.org/docs/documentation/server-management/user-management" target="_blank" class="inline-flex">
<span class="material-symbols text-xl w-5 text-gray-200">help_outline</span>
</a>
</ui-tooltip>
+45 -13
View File
@@ -17,9 +17,9 @@
<form @submit.prevent="submitServerSetup">
<p class="text-lg font-semibold mb-2 pl-1 text-center">Create Root User</p>
<ui-text-input-with-label v-model.trim="newRoot.username" label="Username" :disabled="processing" class="w-full mb-3 text-sm" />
<ui-text-input-with-label v-model="newRoot.password" label="Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" />
<ui-text-input-with-label v-model="confirmPassword" label="Confirm Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" />
<ui-text-input-with-label v-model.trim="newRoot.username" label="Username" autocomplete="username" :disabled="processing" class="w-full mb-3 text-sm" />
<ui-text-input-with-label v-model="newRoot.password" label="Password" type="password" autocomplete="new-password" :disabled="processing" class="w-full mb-3 text-sm" />
<ui-text-input-with-label v-model="confirmPassword" label="Confirm Password" type="password" autocomplete="new-password" :disabled="processing" class="w-full mb-3 text-sm" />
<p class="text-lg font-semibold mt-6 mb-2 pl-1 text-center">Directory Paths</p>
<ui-text-input-with-label v-model="ConfigPath" label="Config Path" disabled class="w-full mb-3 text-sm" />
@@ -40,12 +40,21 @@
<p v-if="error" class="text-error text-center py-2">{{ error }}</p>
<div v-if="showNewAuthSystemMessage" class="mb-4">
<widgets-alert type="warning">
<div>
<p>{{ $strings.MessageAuthenticationSecurityMessage }}</p>
<a v-if="showNewAuthSystemAdminMessage" href="https://github.com/advplyr/audiobookshelf/discussions/4460" target="_blank" class="underline">{{ $strings.LabelMoreInfo }}</a>
</div>
</widgets-alert>
</div>
<form v-show="login_local" @submit.prevent="submitForm">
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label>
<ui-text-input v-model.trim="username" :disabled="processing" class="mb-3 w-full" inputName="username" />
<ui-text-input v-model.trim="username" autocomplete="username" :disabled="processing" class="mb-3 w-full" inputName="username" />
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelPassword }}</label>
<ui-text-input v-model.trim="password" type="password" :disabled="processing" class="w-full mb-3" inputName="password" />
<ui-text-input v-model.trim="password" type="password" autocomplete="current-password" :disabled="processing" class="w-full mb-3" inputName="password" />
<div class="w-full flex justify-end py-3">
<ui-btn type="submit" :disabled="processing" color="bg-primary" class="leading-none">{{ processing ? 'Checking...' : $strings.ButtonSubmit }}</ui-btn>
</div>
@@ -85,7 +94,10 @@ export default {
MetadataPath: '',
login_local: true,
login_openid: false,
authFormData: null
authFormData: null,
// New JWT auth system re-login flags
showNewAuthSystemMessage: false,
showNewAuthSystemAdminMessage: false
}
},
watch: {
@@ -177,13 +189,20 @@ export default {
require('@/plugins/chromecast.js').default(this)
}
this.$store.commit('libraries/setCurrentLibrary', userDefaultLibraryId)
this.$store.commit('libraries/setLastLoad', 0) // Ensure libraries get loaded again when switching users
this.$store.commit('libraries/setCurrentLibrary', { id: userDefaultLibraryId })
this.$store.commit('user/setUser', user)
// Access token only returned from login, not authorize
if (user.accessToken) {
this.$store.commit('user/setAccessToken', user.accessToken)
}
this.$store.dispatch('user/loadUserSettings')
},
async submitForm() {
this.error = null
this.showNewAuthSystemMessage = false
this.showNewAuthSystemAdminMessage = false
this.processing = true
const payload = {
@@ -210,6 +229,8 @@ export default {
this.processing = true
this.$store.commit('user/setAccessToken', token)
return this.$axios
.$post('/api/authorize', null, {
headers: {
@@ -217,15 +238,25 @@ export default {
}
})
.then((res) => {
// Force re-login if user is using an old token with no expiration
if (res.user.isOldToken) {
this.username = res.user.username
this.showNewAuthSystemMessage = true
// Admin user sees link to github discussion
this.showNewAuthSystemAdminMessage = res.user.type === 'admin' || res.user.type === 'root'
return false
}
this.setUser(res)
this.processing = false
return true
})
.catch((error) => {
console.error('Authorize error', error)
this.processing = false
return false
})
.finally(() => {
this.processing = false
})
},
checkStatus() {
this.processing = true
@@ -268,8 +299,8 @@ export default {
}
if (authMethods.includes('openid')) {
// Auto redirect unless query string ?autoLaunch=0
if (this.authFormData?.authOpenIDAutoLaunch && this.$route.query?.autoLaunch !== '0') {
// Auto redirect unless query string ?autoLaunch=0 OR when explicity requested through ?autoLaunch=1
if ((this.authFormData?.authOpenIDAutoLaunch && this.$route.query?.autoLaunch !== '0') || this.$route.query?.autoLaunch == '1') {
window.location.href = this.openidAuthUri
}
@@ -280,8 +311,9 @@ export default {
}
},
async mounted() {
if (this.$route.query?.setToken) {
localStorage.setItem('token', this.$route.query.setToken)
// Token passed as query parameter after successful oidc login
if (this.$route.query?.accessToken) {
localStorage.setItem('token', this.$route.query.accessToken)
}
if (localStorage.getItem('token')) {
if (await this.checkAuth()) return // if valid user no need to check status
+1
View File
@@ -364,6 +364,7 @@ export default {
}
const startTime = this.playbackSession.currentTime || 0
this.localAudioPlayer.set(null, this.audioTracks, false, startTime, false)
this.localAudioPlayer.on('stateChange', this.playerStateChange.bind(this))
this.localAudioPlayer.on('timeupdate', this.playerTimeUpdate.bind(this))
+25 -2
View File
@@ -155,7 +155,7 @@ export default {
},
providers() {
if (this.selectedLibraryIsPodcast) return this.$store.state.scanners.podcastProviders
return this.$store.state.scanners.providers
return this.$store.state.scanners.bookProviders
},
canFetchMetadata() {
return !this.selectedLibraryIsPodcast && this.fetchMetadata.enabled
@@ -297,6 +297,15 @@ export default {
ref.setUploadStatus(status)
}
},
updateItemCardProgress(index, progress) {
var ref = this.$refs[`itemCard-${index}`]
if (ref && ref.length) ref = ref[0]
if (!ref) {
console.error('Book card ref not found', index, this.$refs)
} else {
ref.setUploadProgress(progress)
}
},
async uploadItem(item) {
var form = new FormData()
form.set('title', item.title)
@@ -312,8 +321,20 @@ export default {
form.set(`${index++}`, file)
})
const config = {
onUploadProgress: (progressEvent) => {
if (progressEvent.lengthComputable) {
const progress = {
loaded: progressEvent.loaded,
total: progressEvent.total
}
this.updateItemCardProgress(item.index, progress)
}
}
}
return this.$axios
.$post('/api/upload', form)
.$post('/api/upload', form, config)
.then(() => true)
.catch((error) => {
console.error('Failed to upload item', error)
@@ -394,6 +415,8 @@ export default {
this.setMetadataProvider()
this.setDefaultFolder()
// Fetch providers if not already loaded
this.$store.dispatch('scanners/fetchProviders')
window.addEventListener('dragenter', this.dragenter)
window.addEventListener('dragleave', this.dragleave)
window.addEventListener('dragover', this.dragover)
+14 -1
View File
@@ -46,7 +46,20 @@ export default class LocalAudioPlayer extends EventEmitter {
this.player.addEventListener('loadedmetadata', this.evtLoadedMetadata.bind(this))
this.player.addEventListener('timeupdate', this.evtTimeupdate.bind(this))
var mimeTypes = ['audio/flac', 'audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/aac', 'audio/x-ms-wma', 'audio/x-aiff', 'audio/webm']
var mimeTypes = [
'audio/flac',
'audio/mpeg',
'audio/mp4',
'audio/ogg',
'audio/aac',
'audio/x-ms-wma',
'audio/x-aiff',
'audio/webm',
// `audio/matroska` is the correct mimetype, but the server still uses `audio/x-matroska`
// ref: https://www.iana.org/assignments/media-types/media-types.xhtml
'audio/matroska',
'audio/x-matroska'
]
var mimeTypeCanPlayMap = {}
mimeTypes.forEach((mt) => {
var canPlay = this.player.canPlayType(mt)
+88 -3
View File
@@ -1,4 +1,19 @@
export default function ({ $axios, store, $config }) {
export default function ({ $axios, store, $root, app }) {
// Track if we're currently refreshing to prevent multiple refresh attempts
let isRefreshing = false
let failedQueue = []
const processQueue = (error, token = null) => {
failedQueue.forEach(({ resolve, reject }) => {
if (error) {
reject(error)
} else {
resolve(token)
}
})
failedQueue = []
}
$axios.onRequest((config) => {
if (!config.url) {
console.error('Axios request invalid config', config)
@@ -7,7 +22,7 @@ export default function ({ $axios, store, $config }) {
if (config.url.startsWith('http:') || config.url.startsWith('https:')) {
return
}
const bearerToken = store.state.user.user?.token || null
const bearerToken = store.getters['user/getToken']
if (bearerToken) {
config.headers.common['Authorization'] = `Bearer ${bearerToken}`
}
@@ -17,9 +32,79 @@ export default function ({ $axios, store, $config }) {
}
})
$axios.onError((error) => {
$axios.onError(async (error) => {
const originalRequest = error.config
const code = parseInt(error.response && error.response.status)
const message = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error'
console.error('Axios error', code, message)
// Handle 401 Unauthorized (token expired)
if (code === 401 && !originalRequest._retry) {
// Skip refresh for auth endpoints to prevent infinite loops
if (originalRequest.url === '/auth/refresh' || originalRequest.url === '/login') {
// Refresh failed or login failed, redirect to login
store.commit('user/setUser', null)
store.commit('user/setAccessToken', null)
app.router.push('/login')
return Promise.reject(error)
}
if (isRefreshing) {
// If already refreshing, queue this request
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject })
})
.then((token) => {
if (!originalRequest.headers) {
originalRequest.headers = {}
}
originalRequest.headers['Authorization'] = `Bearer ${token}`
return $axios(originalRequest)
})
.catch((err) => {
return Promise.reject(err)
})
}
originalRequest._retry = true
isRefreshing = true
try {
// Attempt to refresh the token
// Updates store if successful, otherwise clears store and throw error
const newAccessToken = await store.dispatch('user/refreshToken')
if (!newAccessToken) {
console.error('No new access token received')
return Promise.reject(error)
}
// Update the original request with new token
if (!originalRequest.headers) {
originalRequest.headers = {}
}
originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`
// Process any queued requests
processQueue(null, newAccessToken)
// Retry the original request
return $axios(originalRequest)
} catch (refreshError) {
console.error('Token refresh failed:', refreshError)
// Process queued requests with error
processQueue(refreshError, null)
// Redirect to login
app.router.push('/login')
return Promise.reject(refreshError)
} finally {
isRefreshing = false
}
}
return Promise.reject(error)
})
}
+8
View File
@@ -6,6 +6,7 @@ const defaultCode = 'en-us'
const languageCodeMap = {
ar: { label: 'عربي', dateFnsLocale: 'ar' },
be: { label: 'Беларуская', dateFnsLocale: 'be' },
bg: { label: 'Български', dateFnsLocale: 'bg' },
bn: { label: 'বাংলা', dateFnsLocale: 'bn' },
ca: { label: 'Català', dateFnsLocale: 'ca' },
@@ -20,15 +21,19 @@ const languageCodeMap = {
he: { label: 'עברית', dateFnsLocale: 'he' },
hr: { label: 'Hrvatski', dateFnsLocale: 'hr' },
it: { label: 'Italiano', dateFnsLocale: 'it' },
ja: { label: '日本語', dateFnsLocale: 'ja' },
lt: { label: 'Lietuvių', dateFnsLocale: 'lt' },
hu: { label: 'Magyar', dateFnsLocale: 'hu' },
ko: { label: '한국어', dateFnsLocale: 'ko' },
nl: { label: 'Nederlands', dateFnsLocale: 'nl' },
no: { label: 'Norsk', dateFnsLocale: 'no' },
pl: { label: 'Polski', dateFnsLocale: 'pl' },
'pt-br': { label: 'Português (Brasil)', dateFnsLocale: 'ptBR' },
ru: { label: 'Русский', dateFnsLocale: 'ru' },
sk: { label: 'Slovenčina', dateFnsLocale: 'sk' },
sl: { label: 'Slovenščina', dateFnsLocale: 'sl' },
sv: { label: 'Svenska', dateFnsLocale: 'sv' },
tr: { label: 'Türkçe', dateFnsLocale: 'tr' },
uk: { label: 'Українська', dateFnsLocale: 'uk' },
'vi-vn': { label: 'Tiếng Việt', dateFnsLocale: 'vi' },
'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' },
@@ -46,6 +51,7 @@ const podcastSearchRegionMap = {
au: { label: 'Australia' },
br: { label: 'Brasil' },
be: { label: 'België / Belgique / Belgien' },
by: { label: 'Беларусь' },
cz: { label: 'Česko' },
dk: { label: 'Danmark' },
de: { label: 'Deutschland' },
@@ -55,6 +61,7 @@ const podcastSearchRegionMap = {
hr: { label: 'Hrvatska' },
il: { label: 'ישראל / إسرائيل' },
it: { label: 'Italia' },
jp: { label: '日本' },
lu: { label: 'Luxembourg / Luxemburg / Lëtezebuerg' },
hu: { label: 'Magyarország' },
nl: { label: 'Nederland' },
@@ -65,6 +72,7 @@ const podcastSearchRegionMap = {
pt: { label: 'Portugal' },
ru: { label: 'Россия' },
ch: { label: 'Schweiz / Suisse / Svizzera' },
sk: { label: 'Slovensko' },
se: { label: 'Sverige' },
vn: { label: 'Việt Nam' },
ua: { label: 'Україна' },
+42
View File
@@ -37,6 +37,48 @@ Vue.prototype.$elapsedPretty = (seconds, useFullNames = false, useMilliseconds =
return `${hours} ${useFullNames ? `hour${hours === 1 ? '' : 's'}` : 'hr'} ${minutes} ${useFullNames ? `minute${minutes === 1 ? '' : 's'}` : 'min'}`
}
Vue.prototype.$elapsedPrettyLocalized = (seconds, useFullNames = false, useMilliseconds = false) => {
if (isNaN(seconds) || seconds === null) return ''
try {
const df = new Intl.DurationFormat(Vue.prototype.$languageCodes.current, {
style: useFullNames ? 'long' : 'short'
})
const duration = {}
if (seconds < 60) {
if (useMilliseconds && seconds < 1) {
duration.milliseconds = Math.floor(seconds * 1000)
} else {
duration.seconds = Math.floor(seconds)
}
} else if (seconds < 3600) {
// 1 hour
duration.minutes = Math.floor(seconds / 60)
} else if (seconds < 86400) {
// 1 day
duration.hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
if (minutes > 0) {
duration.minutes = minutes
}
} else {
duration.days = Math.floor(seconds / 86400)
const hours = Math.floor((seconds % 86400) / 3600)
if (hours > 0) {
duration.hours = hours
}
}
return df.format(duration)
} catch (error) {
// Handle not supported
console.warn('Intl.DurationFormat not supported, not localizing duration')
return Vue.prototype.$elapsedPretty(seconds, useFullNames, useMilliseconds)
}
}
Vue.prototype.$secondsToTimestamp = (seconds, includeMs = false, alwaysIncludeHours = false) => {
if (!seconds) {
return alwaysIncludeHours ? '00:00:00' : '0:00'
+6 -9
View File
@@ -117,7 +117,6 @@ export const actions = {
const library = data.library
const filterData = data.filterdata
const issues = data.issues || 0
const customMetadataProviders = data.customMetadataProviders || []
const numUserPlaylists = data.numUserPlaylists
dispatch('user/checkUpdateLibrarySortFilter', library.mediaType, { root: true })
@@ -131,9 +130,7 @@ export const actions = {
commit('setLibraryIssues', issues)
commit('setLibraryFilterData', filterData)
commit('setNumUserPlaylists', numUserPlaylists)
commit('scanners/setCustomMetadataProviders', customMetadataProviders, { root: true })
commit('setCurrentLibrary', libraryId)
commit('setCurrentLibrary', { id: libraryId })
return data
})
.catch((error) => {
@@ -159,7 +156,7 @@ export const actions = {
.$get(`/api/libraries`)
.then((data) => {
commit('set', data.libraries)
commit('setLastLoad')
commit('setLastLoad', new Date())
})
.catch((error) => {
console.error('Failed', error)
@@ -176,14 +173,14 @@ export const mutations = {
setFoldersLastUpdate(state) {
state.folderLastUpdate = Date.now()
},
setLastLoad(state) {
state.lastLoad = Date.now()
setLastLoad(state, date) {
state.lastLoad = date
},
setLibraryIssues(state, val) {
state.issues = val
},
setCurrentLibrary(state, val) {
state.currentLibraryId = val
setCurrentLibrary(state, { id }) {
state.currentLibraryId = id
},
set(state, libraries) {
state.libraries = libraries
+50 -116
View File
@@ -1,126 +1,60 @@
export const state = () => ({
providers: [
{
text: 'Google Books',
value: 'google'
},
{
text: 'Open Library',
value: 'openlibrary'
},
{
text: 'iTunes',
value: 'itunes'
},
{
text: 'Audible.com',
value: 'audible'
},
{
text: 'Audible.ca',
value: 'audible.ca'
},
{
text: 'Audible.co.uk',
value: 'audible.uk'
},
{
text: 'Audible.com.au',
value: 'audible.au'
},
{
text: 'Audible.fr',
value: 'audible.fr'
},
{
text: 'Audible.de',
value: 'audible.de'
},
{
text: 'Audible.co.jp',
value: 'audible.jp'
},
{
text: 'Audible.it',
value: 'audible.it'
},
{
text: 'Audible.co.in',
value: 'audible.in'
},
{
text: 'Audible.es',
value: 'audible.es'
},
{
text: 'FantLab.ru',
value: 'fantlab'
}
],
podcastProviders: [
{
text: 'iTunes',
value: 'itunes'
}
],
coverOnlyProviders: [
{
text: 'AudiobookCovers.com',
value: 'audiobookcovers'
}
]
bookProviders: [],
podcastProviders: [],
bookCoverProviders: [],
podcastCoverProviders: [],
providersLoaded: false
})
export const getters = {
checkBookProviderExists: state => (providerValue) => {
return state.providers.some(p => p.value === providerValue)
checkBookProviderExists: (state) => (providerValue) => {
return state.bookProviders.some((p) => p.value === providerValue)
},
checkPodcastProviderExists: state => (providerValue) => {
return state.podcastProviders.some(p => p.value === providerValue)
checkPodcastProviderExists: (state) => (providerValue) => {
return state.podcastProviders.some((p) => p.value === providerValue)
},
areProvidersLoaded: (state) => state.providersLoaded
}
export const actions = {
async fetchProviders({ commit, state }) {
// Only fetch if not already loaded
if (state.providersLoaded) {
return
}
try {
const response = await this.$axios.$get('/api/search/providers')
if (response?.providers) {
commit('setAllProviders', response.providers)
}
} catch (error) {
console.error('Failed to fetch providers', error)
}
},
async refreshProviders({ commit, state }) {
// if providers are not loaded, do nothing - they will be fetched when required (
if (!state.providersLoaded) {
return
}
try {
const response = await this.$axios.$get('/api/search/providers')
if (response?.providers) {
commit('setAllProviders', response.providers)
}
} catch (error) {
console.error('Failed to refresh providers', error)
}
}
}
export const actions = {}
export const mutations = {
addCustomMetadataProvider(state, provider) {
if (provider.mediaType === 'book') {
if (state.providers.some(p => p.value === provider.slug)) return
state.providers.push({
text: provider.name,
value: provider.slug
})
} else {
if (state.podcastProviders.some(p => p.value === provider.slug)) return
state.podcastProviders.push({
text: provider.name,
value: provider.slug
})
}
},
removeCustomMetadataProvider(state, provider) {
if (provider.mediaType === 'book') {
state.providers = state.providers.filter(p => p.value !== provider.slug)
} else {
state.podcastProviders = state.podcastProviders.filter(p => p.value !== provider.slug)
}
},
setCustomMetadataProviders(state, providers) {
if (!providers?.length) return
const mediaType = providers[0].mediaType
if (mediaType === 'book') {
// clear previous values, and add new values to the end
state.providers = state.providers.filter((p) => !p.value.startsWith('custom-'))
state.providers = [
...state.providers,
...providers.map((p) => ({
text: p.name,
value: p.slug
}))
]
} else {
// Podcast providers not supported yet
}
setAllProviders(state, providers) {
state.bookProviders = providers.books || []
state.podcastProviders = providers.podcasts || []
state.bookCoverProviders = providers.booksCovers || []
state.podcastCoverProviders = providers.podcasts || [] // Use same as bookCovers since podcasts use iTunes only
state.providersLoaded = true
}
}
}
+33 -12
View File
@@ -1,5 +1,6 @@
export const state = () => ({
user: null,
accessToken: null,
settings: {
orderBy: 'media.metadata.title',
orderDesc: false,
@@ -25,19 +26,19 @@ export const getters = {
getIsRoot: (state) => state.user && state.user.type === 'root',
getIsAdminOrUp: (state) => state.user && (state.user.type === 'admin' || state.user.type === 'root'),
getToken: (state) => {
return state.user?.token || null
return state.accessToken || null
},
getUserMediaProgress:
(state) =>
(libraryItemId, episodeId = null) => {
if (!state.user.mediaProgress) return null
if (!state.user?.mediaProgress) return null
return state.user.mediaProgress.find((li) => {
if (episodeId && li.episodeId !== episodeId) return false
return li.libraryItemId == libraryItemId
})
},
getUserBookmarksForItem: (state) => (libraryItemId) => {
if (!state.user.bookmarks) return []
if (!state.user?.bookmarks) return []
return state.user.bookmarks.filter((bm) => bm.libraryItemId === libraryItemId)
},
getUserSetting: (state) => (key) => {
@@ -91,7 +92,7 @@ export const actions = {
if (state.settings.orderBy == 'media.duration') {
settingsUpdate.orderBy = 'media.numTracks'
}
if (state.settings.orderBy == 'media.metadata.publishedYear') {
if (state.settings.orderBy == 'media.metadata.publishedYear' || state.settings.orderBy == 'progress') {
settingsUpdate.orderBy = 'media.metadata.title'
}
const invalidFilters = ['series', 'authors', 'narrators', 'publishers', 'publishedDecades', 'languages', 'progress', 'issues', 'ebooks', 'abridged']
@@ -145,21 +146,41 @@ export const actions = {
} catch (error) {
console.error('Failed to load userSettings from local storage', error)
}
},
refreshToken({ state, commit }) {
return this.$axios
.$post('/auth/refresh')
.then(async (response) => {
const newAccessToken = response.user.accessToken
commit('setAccessToken', newAccessToken)
// Emit event used to re-authenticate socket in default.vue since $root is not available here
if (this.$eventBus) {
this.$eventBus.$emit('token_refreshed', newAccessToken)
}
return newAccessToken
})
.catch((error) => {
console.error('Failed to refresh token', error)
commit('setUser', null)
commit('setAccessToken', null)
// Calling function handles redirect to login
throw error
})
}
}
export const mutations = {
setUser(state, user) {
state.user = user
if (user) {
if (user.token) localStorage.setItem('token', user.token)
} else {
localStorage.removeItem('token')
}
},
setUserToken(state, token) {
state.user.token = token
localStorage.setItem('token', token)
setAccessToken(state, token) {
if (!token) {
localStorage.removeItem('token')
state.accessToken = null
} else {
state.accessToken = token
localStorage.setItem('token', token)
}
},
updateMediaProgress(state, { id, data }) {
if (!state.user) return
+20 -5
View File
@@ -1,5 +1,6 @@
{
"ButtonAdd": "إضافة",
"ButtonAddApiKey": "إضافة مفتاح واجهة برمجة التطبيقات",
"ButtonAddChapters": "إضافة الفصول",
"ButtonAddDevice": "إضافة جهاز",
"ButtonAddLibrary": "إضافة مكتبة",
@@ -20,7 +21,8 @@
"ButtonChooseAFolder": "اختر المجلد",
"ButtonChooseFiles": "اختر الملفات",
"ButtonClearFilter": "تصفية الفرز",
"ButtonCloseFeed": "إغلاق",
"ButtonClose": "إغلاق",
"ButtonCloseFeed": "إغلاق الموجز",
"ButtonCloseSession": "إغلاق الجلسة المفتوحة",
"ButtonCollections": "المجموعات",
"ButtonConfigureScanner": "إعدادات الماسح الضوئي",
@@ -119,11 +121,13 @@
"HeaderAccount": "الحساب",
"HeaderAddCustomMetadataProvider": "إضافة موفر بيانات تعريفية مخصص",
"HeaderAdvanced": "متقدم",
"HeaderApiKeys": "مفاتيح API",
"HeaderAppriseNotificationSettings": "إعدادات الإشعارات",
"HeaderAudioTracks": "المقاطع الصوتية",
"HeaderAudiobookTools": "أدوات إدارة ملفات الكتب الصوتية",
"HeaderAuthentication": "المصادقة",
"HeaderBackups": "النسخ الاحتياطية",
"HeaderBulkChapterModal": "أضف فصولاً متعددة",
"HeaderChangePassword": "تغيير كلمة المرور",
"HeaderChapters": "الفصول",
"HeaderChooseAFolder": "اختيار المجلد",
@@ -162,6 +166,7 @@
"HeaderMetadataOrderOfPrecedence": "ترتيب أولوية البيانات الوصفية",
"HeaderMetadataToEmbed": "البيانات الوصفية المراد تضمينها",
"HeaderNewAccount": "حساب جديد",
"HeaderNewApiKey": "مفتاح API جديد",
"HeaderNewLibrary": "مكتبة جديدة",
"HeaderNotificationCreate": "إنشاء إشعار",
"HeaderNotificationUpdate": "تحديث إشعار",
@@ -195,6 +200,7 @@
"HeaderSettingsExperimental": "ميزات تجريبية",
"HeaderSettingsGeneral": "عام",
"HeaderSettingsScanner": "إعدادات المسح",
"HeaderSettingsSecurity": "الأمان",
"HeaderSettingsWebClient": "عميل الويب",
"HeaderSleepTimer": "مؤقت النوم",
"HeaderStatsLargestItems": "أكبر العناصر حجماً",
@@ -206,6 +212,7 @@
"HeaderTableOfContents": "جدول المحتويات",
"HeaderTools": "أدوات",
"HeaderUpdateAccount": "تحديث الحساب",
"HeaderUpdateApiKey": "تحديث مفتاح API",
"HeaderUpdateAuthor": "تحديث المؤلف",
"HeaderUpdateDetails": "تحديث التفاصيل",
"HeaderUpdateLibrary": "تحديث المكتبة",
@@ -235,6 +242,10 @@
"LabelAllUsersExcludingGuests": "جميع المستخدمين باستثناء الضيوف",
"LabelAllUsersIncludingGuests": "جميع المستخدمين بما في ذلك الضيوف",
"LabelAlreadyInYourLibrary": "موجود بالفعل في مكتبتك",
"LabelApiKeyCreated": "تم إنشاء مفتاح API \"{0}\" بنجاح.",
"LabelApiKeyCreatedDescription": "تأكد من نسخ مفتاح API الآن، لن تتمكن من رؤيته مرة أخرى.",
"LabelApiKeyUser": "التصرف بالنيابة عن مستخدم",
"LabelApiKeyUserDescription": "مفتاح API سيمتلك نفس صلاحيات المستخدم الذي ينوب عنه ، سيظهر بالسجلات وكأن المستخدم قام بالطلب.",
"LabelApiToken": "رمز API",
"LabelAppend": "إلحاق",
"LabelAudioBitrate": "معدل بت الصوت (على سبيل المثال 128 كيلو بايت)",
@@ -284,6 +295,7 @@
"LabelContinueListening": "استمرار الاستماع",
"LabelContinueReading": "استمرار القراءة",
"LabelContinueSeries": "استمرار المسلسلات",
"LabelCorsAllowed": "CORS Origins مسموح",
"LabelCover": "الغلاف",
"LabelCoverImageURL": "رابط صورة الغلاف",
"LabelCoverProvider": "مزود الغلاف",
@@ -346,7 +358,7 @@
"LabelExample": "مثال",
"LabelExpandSeries": "توسيع السلاسل",
"LabelExpandSubSeries": "توسيع السلاسل الفرعية",
"LabelExplicit": "صريح",
"LabelExplicit": "محتوى صريح",
"LabelExplicitChecked": "صريح (محدد)",
"LabelExplicitUnchecked": "غير صريح (غير محدد)",
"LabelExportOPML": "تصدير OPML",
@@ -365,7 +377,7 @@
"LabelFolders": "مجلدات",
"LabelFontBold": "عريض",
"LabelFontBoldness": "تعريض الخط",
"LabelFontFamily": "عائلة الخط",
"LabelFontFamily": "عائلة الخطوط",
"LabelFontItalic": "مائل",
"LabelFontScale": "نطاق الخط",
"LabelFontStrikethrough": "يتوسطه خط",
@@ -417,6 +429,9 @@
"LabelLibraryFilterSublistEmpty": "لا يوجد {0}",
"LabelLibraryItem": "عنصر المكتبة",
"LabelLibraryName": "اسم المكتبة",
"LabelLibrarySortByProgress": "المرحلة: الأحدث",
"LabelLibrarySortByProgressFinished": "المرحلة: تم الانتهاء",
"LabelLibrarySortByProgressStarted": "المرحلة: تم البدء",
"LabelLimit": "حد",
"LabelLineSpacing": "تباعد الأسطر",
"LabelListenAgain": "الاستماع مجدداً",
@@ -561,7 +576,7 @@
"LabelSettingsBookshelfViewHelp": "تصميم يحاكي الواقع مع رفوف خشبية",
"LabelSettingsChromecastSupport": "دعم Chromecast",
"LabelSettingsDateFormat": "تنسيق التاريخ",
"LabelSettingsEnableWatcher": "فحص المكتبات تلقائيًا بحثًا عن تغييرات",
"LabelSettingsEnableWatcher": "مراقبة المكتبات تلقائياً بحثاً عن تغييرات",
"LabelSettingsEnableWatcherForLibrary": "فحص المكتبة تلقائيًا بحثًا عن تغييرات",
"LabelSettingsEnableWatcherHelp": "يمكّن الإضافة/التحديث التلقائي للعناصر عند اكتشاف تغييرات في الملفات. *يتطلب إعادة تشغيل الخادم",
"LabelSettingsEpubsAllowScriptedContent": "السماح بالمحتوى النصي في ملفات epub",
@@ -852,7 +867,7 @@
"MessageResetChaptersConfirm": "هل أنت متأكد أنك تريد إعادة تعيين الفصول والتراجع عن التغييرات التي أجريتها؟",
"MessageRestoreBackupConfirm": "هل أنت متأكد أنك تريد استعادة النسخ الاحتياطي الذي تم إنشاؤه في",
"MessageRestoreBackupWarning": "ستؤدي استعادة النسخ الاحتياطي إلى الكتابة فوق قاعدة البيانات بأكملها الموجودة في /config وصور الأغلفة في /metadata/items و /metadata/authors.<br /><br /> لا تعدل النسخ الاحتياطية أي ملفات في مجلدات مكتبتك. إذا قمت بتمكين إعدادات الخادم لتخزين صور الأغلفة والبيانات الوصفية في مجلدات مكتبتك، فلن يتم نسخها احتياطيًا أو الكتابة فوقها.<br /><br /> سيتم تحديث جميع العملاء الذين يستخدمون الخادم الخاص بك تلقائيًا.",
"MessageScheduleLibraryScanNote": "بالنسبة لمعظم المستخدمين، يوصى بترك هذه الميزة معطلة وإبقاء إعداد مراقب المجلدات ممكّنًا. سيكتشف مراقب المجلدات تلقائيًا التغييرات في مجلدات مكتبتك. لا يعمل مراقب المجلدات مع كل نظام ملفات (مثل NFS)، لذا يمكن استخدام عمليات فحص المكتبة المجدولة بدلاً من ذلك.",
"MessageScheduleLibraryScanNote": "لمعظم المستخدمين، موصى بترك هذه الميزة معطلة وإبقاء ممكّنة الأعداد، ”قم بمراقبة المكتبة تلقائاً للتغييرات“. سوف يقم بالكشف التلقائي عن تغييرات في مجلدات مكتبتك. لو لم يعمل الإعداد، \"قم بمراقبة المكتبة تلقائاً للتغييرات،“مع نظمة ملفاتك المستخدمة (مثل NFS على سبيل المثال)، فأمكِن هذه الميزة.",
"MessageScheduleRunEveryWeekdayAtTime": "تشغيل كل {0} في الساعة {1}",
"MessageSearchResultsFor": "نتائج البحث عن",
"MessageSelected": "تم تحديد {0}",
+722 -172
View File
File diff suppressed because it is too large Load Diff
+267 -10
View File
@@ -1,5 +1,6 @@
{
"ButtonAdd": "Създай",
"ButtonAddApiKey": "Добави API ключ",
"ButtonAddChapters": "Добави Глави",
"ButtonAddDevice": "Добави Устройство",
"ButtonAddLibrary": "Добави Библиотека",
@@ -20,6 +21,7 @@
"ButtonChooseAFolder": "Избери Папка",
"ButtonChooseFiles": "Избери Файлове",
"ButtonClearFilter": "Изчисти филтър",
"ButtonClose": "Затвори",
"ButtonCloseFeed": "Затвори стената",
"ButtonCloseSession": "Затвори отворената сесия",
"ButtonCollections": "Колекции",
@@ -119,11 +121,13 @@
"HeaderAccount": "Профил",
"HeaderAddCustomMetadataProvider": "Добави персонализиран доставчик на метаданни",
"HeaderAdvanced": "Разширени настройки",
"HeaderApiKeys": "API ключове",
"HeaderAppriseNotificationSettings": "Apprise Notification Опции",
"HeaderAudioTracks": "Песни",
"HeaderAudiobookTools": "Инструмент за Менижиране на Аудиокниги",
"HeaderAuthentication": "Аутентикация",
"HeaderBackups": "Архив",
"HeaderBulkChapterModal": "Добави няколко глави",
"HeaderChangePassword": "Промяна на Парола",
"HeaderChapters": "Глави",
"HeaderChooseAFolder": "Избети Папка",
@@ -162,6 +166,7 @@
"HeaderMetadataOrderOfPrecedence": "Предимство на Метаданни",
"HeaderMetadataToEmbed": "Метаданни за Вграждане",
"HeaderNewAccount": "Нов Профил",
"HeaderNewApiKey": "Нов API ключ",
"HeaderNewLibrary": "Нова Библиотека",
"HeaderNotificationCreate": "Създай нотификация",
"HeaderNotificationUpdate": "Обнови нотификация",
@@ -195,6 +200,7 @@
"HeaderSettingsExperimental": "Експериментални Функции",
"HeaderSettingsGeneral": "Общи",
"HeaderSettingsScanner": "Скенер",
"HeaderSettingsSecurity": "Сигурност",
"HeaderSettingsWebClient": "Уеб клиент",
"HeaderSleepTimer": "Таймер за заспиване",
"HeaderStatsLargestItems": "Най-Големите Елементи",
@@ -206,6 +212,7 @@
"HeaderTableOfContents": "Съдържание",
"HeaderTools": "Инструменти",
"HeaderUpdateAccount": "Обнови Профил",
"HeaderUpdateApiKey": "Обнови API ключ",
"HeaderUpdateAuthor": "Обнови Автор",
"HeaderUpdateDetails": "Обнови Детайли",
"HeaderUpdateLibrary": "Обнови Библиотека",
@@ -230,10 +237,15 @@
"LabelAddedDate": "Добавено",
"LabelAdminUsersOnly": "Само за Администратори",
"LabelAll": "Всичко",
"LabelAllEpisodesDownloaded": "Всички епизоди са изтеглени",
"LabelAllUsers": "Всички Потребители",
"LabelAllUsersExcludingGuests": "Всички потребители без гости",
"LabelAllUsersIncludingGuests": "Всички потребители включително гости",
"LabelAlreadyInYourLibrary": "Вече е в твоята библиотека",
"LabelApiKeyCreated": "API ключ \"{0}\" успешно създатен.",
"LabelApiKeyCreatedDescription": "Погрижете се да копирате API ключът сега, защото повече няма да можете да го виждате онново.",
"LabelApiKeyUser": "Действай от името на потребителя",
"LabelApiKeyUserDescription": "Този API ключ ще има същите права като на потребителя за чието име действа. В логовете ще изглежда все едно потребителя прави заявката.",
"LabelApiToken": "АПИ Токен",
"LabelAppend": "Добави",
"LabelAudioBitrate": "Аудио битрейт (напр. 128k)",
@@ -253,7 +265,7 @@
"LabelBackToUser": "Обратно към Потребител",
"LabelBackupAudioFiles": "Създай резервно копие на аудио файлове",
"LabelBackupLocation": "Местоположение на Архив",
"LabelBackupsEnableAutomaticBackups": "Включи автоматично архивиране",
"LabelBackupsEnableAutomaticBackups": "Автоматично архивиране",
"LabelBackupsEnableAutomaticBackupsHelp": "Архиви запазени в /metadata/backups",
"LabelBackupsMaxBackupSize": "Максимален размер на архива (в GB) (0 за неограничен)",
"LabelBackupsMaxBackupSizeHelp": "За защита срещу грешки в конфигурацията, архивите ще се провалят ако надхвърлят конфигурирания размер.",
@@ -272,7 +284,7 @@
"LabelChaptersFound": "намерени глави",
"LabelClickForMoreInfo": "Кликни за повече информация",
"LabelClickToUseCurrentValue": "Натисни да ползваш сегашната стойност",
"LabelClosePlayer": "Затвори",
"LabelClosePlayer": "Затвори плейъра",
"LabelCodec": "Кодек",
"LabelCollapseSeries": "Скрий сериите",
"LabelCollapseSubSeries": "Свий подсерии",
@@ -283,6 +295,7 @@
"LabelContinueListening": "Продължи слушане",
"LabelContinueReading": "Продължи четене",
"LabelContinueSeries": "Продължи серии",
"LabelCorsAllowed": "Разрешени CORS Origins",
"LabelCover": "Корица",
"LabelCoverImageURL": "URL на Корица",
"LabelCoverProvider": "Източник за обложки",
@@ -296,6 +309,7 @@
"LabelDeleteFromFileSystemCheckbox": "Изтрий от файловата система (отмени за да бъдат премахни само от базата данни)",
"LabelDescription": "Описание",
"LabelDeselectAll": "Премахни всички",
"LabelDetectedPattern": "Намерен образец:",
"LabelDevice": "Устройство",
"LabelDeviceInfo": "Информация за Устройство",
"LabelDeviceIsAvailableTo": "Устройството е достъпно за ...",
@@ -345,7 +359,11 @@
"LabelExample": "Пример",
"LabelExpandSeries": "Покажи сериите",
"LabelExpandSubSeries": "Покажи съб сериите",
"LabelExplicit": "С нецензурно съдържание",
"LabelExpired": "Изтекъл",
"LabelExpiresAt": "Изтича на",
"LabelExpiresInSeconds": "Изтича след (секунди)",
"LabelExpiresNever": "Никога",
"LabelExplicit": "Експлицитно",
"LabelExplicitChecked": "С нецензурно съдържание (проверено)",
"LabelExplicitUnchecked": "Без нецензурно съдържание (непроверено)",
"LabelExportOPML": "Експортирай OPML",
@@ -360,6 +378,7 @@
"LabelFilterByUser": "Филтриране по Потребител",
"LabelFindEpisodes": "Намери Епизоди",
"LabelFinished": "Дата на приключване",
"LabelFinishedDate": "Приключено на {0}",
"LabelFolder": "Папка",
"LabelFolders": "Папки",
"LabelFontBold": "Получерно",
@@ -404,6 +423,7 @@
"LabelLanguages": "Езици",
"LabelLastBookAdded": "Последно Добавена Книга",
"LabelLastBookUpdated": "Последно Обновена Книга",
"LabelLastProgressDate": "Последен прогрес: {0}",
"LabelLastSeen": "Последно Видян",
"LabelLastTime": "Последно Време",
"LabelLastUpdate": "Последно Обновяване",
@@ -416,6 +436,9 @@
"LabelLibraryFilterSublistEmpty": "Не {0}",
"LabelLibraryItem": "Елемент на Библиотека",
"LabelLibraryName": "Име на Библиотека",
"LabelLibrarySortByProgress": "Прогрес: Последно обновление",
"LabelLibrarySortByProgressFinished": "Прогрес: Приключено",
"LabelLibrarySortByProgressStarted": "Прогрес: Започнато",
"LabelLimit": "Лимит",
"LabelLineSpacing": "Междуредие",
"LabelListenAgain": "Слушай отново",
@@ -424,6 +447,7 @@
"LabelLogLevelWarn": "Предупреждение",
"LabelLookForNewEpisodesAfterDate": "Търси нови епизоди след дата",
"LabelLowestPriority": "Най-нисък Приоритет",
"LabelMatchConfidence": "Увереност",
"LabelMatchExistingUsersBy": "Съпостави съществуващи потребители по",
"LabelMatchExistingUsersByDescription": "Използва се за свързване на съществуващи потребители. След свързване потребителите ще бъдат съпоставени по уникален идентификатор от вашия доставчик на SSO",
"LabelMaxEpisodesToDownload": "Максимален брой епизоди за сваляне. Използвай 0 за неограничен.",
@@ -453,7 +477,9 @@
"LabelNewestAuthors": "Най-новите автори",
"LabelNewestEpisodes": "Най-новите епизоди",
"LabelNextBackupDate": "Следваща Дата на Архивиране",
"LabelNextChapters": "Следващите глави ще бъдат:",
"LabelNextScheduledRun": "Следващо Планирано Изпълнение",
"LabelNoApiKeys": "Няма API ключове",
"LabelNoCustomMetadataProviders": "Няма потребителски доставчици на метаданни",
"LabelNoEpisodesSelected": "Няма избрани епизоди",
"LabelNotFinished": "Не е приключено",
@@ -469,6 +495,7 @@
"LabelNotificationsMaxQueueSize": "Максимален размер на опашката за известия",
"LabelNotificationsMaxQueueSizeHelp": "Събитията са ограничени до изстрелване на 1 на секунда. Събитията ще бъдат игнорирани ако опашката е на максимален размер. Това предотвратява спамирането на известия.",
"LabelNumberOfBooks": "Брой на Книги",
"LabelNumberOfChapters": "Брой глави:",
"LabelNumberOfEpisodes": "Брой епизоди",
"LabelOpenIDAdvancedPermsClaimDescription": "Име на OpenID твърдението, което съдържа разширени права за достъп до потребителски действия в приложението, които ще се прилагат за роли, различни от администраторските (<b>ако е конфигурирано</b>). Ако твърдението липсва в отговора, достъпът до ABS ще бъде отказан. Ако липсва една опция, тя ще се третира като <code>false</code>. Уверете се, че твърдението на доставчика на идентичност съответства на очакваната структура:",
"LabelOpenIDClaims": "Оставете следните опции празни, за да деактивирате разширеното присвояване на групи, като автоматично ще бъде присвоена групата 'Потребител'.",
@@ -513,7 +540,7 @@
"LabelPublishers": "Издателство",
"LabelRSSFeedCustomOwnerEmail": "Персонализиран имейл на собственика",
"LabelRSSFeedCustomOwnerName": "Персонализирано име на собственика",
"LabelRSSFeedOpen": "RSS Feed Оптворен",
"LabelRSSFeedOpen": "RSS Feed е отворен",
"LabelRSSFeedPreventIndexing": "Предотвратете индексиране",
"LabelRSSFeedSlug": "идентификатор на RSS емисия",
"LabelRSSFeedURL": "URL на RSS емисия",
@@ -543,6 +570,7 @@
"LabelSelectAll": "Избери всичко",
"LabelSelectAllEpisodes": "Избери всички епизоди",
"LabelSelectEpisodesShowing": "Избери {0} епизоди показани",
"LabelSelectUser": "Избери потребител",
"LabelSelectUsers": "Избери Потребители",
"LabelSendEbookToDevice": "Изпрати електронна книга до ...",
"LabelSequence": "Последователност",
@@ -560,8 +588,8 @@
"LabelSettingsBookshelfViewHelp": "Скеуморфен дизайн с дървени рафтове",
"LabelSettingsChromecastSupport": "Chromecast поддръжка",
"LabelSettingsDateFormat": "Формат на Дата",
"LabelSettingsEnableWatcher": "Автоматично сканиране на библиотеките за промени",
"LabelSettingsEnableWatcherForLibrary": "Автоматично сканиране на библиотеката за промени",
"LabelSettingsEnableWatcher": "Автоматично преглеждане на библиотеките за промени",
"LabelSettingsEnableWatcherForLibrary": "Автоматично преглеждане на библиотеката за промени",
"LabelSettingsEnableWatcherHelp": "Включва автоматичното добавяне/обновяване на елементи, когато се открият промени във файловете. *Изисква рестарт на сървъра",
"LabelSettingsEpubsAllowScriptedContent": "Позволи скриптово съдържание в epub-и",
"LabelSettingsEpubsAllowScriptedContentHelp": "Позволи epub файловете да изпълняват скриптове. Препоръчително е да бъде изключено освен ако не се доверявате на източника на epub файловете.",
@@ -610,6 +638,7 @@
"LabelStartTime": "Начално Време",
"LabelStarted": "Стартирано",
"LabelStartedAt": "Стартирано на",
"LabelStartedDate": "Започнато {0}",
"LabelStatsAudioTracks": "Аудио Канали",
"LabelStatsAuthors": "Автори",
"LabelStatsBestDay": "Най-добър ден",
@@ -639,6 +668,7 @@
"LabelTheme": "Тема",
"LabelThemeDark": "Тъмна",
"LabelThemeLight": "Светла",
"LabelThemeSepia": "Сепия",
"LabelTimeBase": "Времева Основа",
"LabelTimeDurationXHours": "{0} часа",
"LabelTimeDurationXMinutes": "{0} минути",
@@ -693,7 +723,11 @@
"LabelViewPlayerSettings": "Виж настройки на плеъра",
"LabelViewQueue": "Виж Опашка",
"LabelVolume": "Сила на Звука",
"LabelWebRedirectURLsDescription": "Разрешете тези URL-и във вашият OAuth доставчик, за да позволите пренасочването обратно към уеб приложението след вход:",
"LabelWebRedirectURLsSubfolder": "Подпапка за URL адреси за пренасочване",
"LabelWeekdaysToRun": "Делници за изпълнение",
"LabelXBooks": "{0} книги",
"LabelXItems": "{0} елемента",
"LabelYearReviewHide": "Скрий ревю на годината ти",
"LabelYearReviewShow": "Виж ревю на годината ти",
"LabelYourAudiobookDuration": "Продължителност на вашата аудиокнига",
@@ -702,41 +736,64 @@
"LabelYourProgress": "Твоят прогрес",
"MessageAddToPlayerQueue": "Добави към опашката на плейъра",
"MessageAppriseDescription": "За да ползвате тази функция трябва да имате активна инстанция на <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> или на друго АПИ което да обработва тези заявки. <br />The Apprise API Url-а трябва дае пълния URL път за изпращане на известията, например, ако вашето АПИ ве подава от <code>http://192.168.1.1:8337</code> трябва да сложитев <code>http://192.168.1.1:8337/notify</code>.",
"MessageAsinCheck": "Уверете се, че използвате ASIN от правилния Audible регион, а не от Amazon.",
"MessageAuthenticationLegacyTokenWarning": "Остарелите API токени ще бъдат премахнати в бъдеще. Вместо това използвайте <a href=\"/config/api-keys\">API ключове</a>.",
"MessageAuthenticationOIDCChangesRestart": "Рестартирайте сървърът след записването на настройките, за да активирате OIDC промените.",
"MessageAuthenticationSecurityMessage": "За осигуряването на по-добра сигурност, автентикацията беше подобрена. Всеки потребител ще трябва да се автентикира наново.",
"MessageBackupsDescription": "Резервните копия включват потребители, напредък на потребителите, подробности за елементите в библиотеката, настройки на сървъра и изображения, съхранени в <code>/metadata/items</code> и <code>/metadata/authors</code>. Резервните копия <strong>не</strong> включват никакви файлове, съхранени в папките на вашата библиотека.",
"MessageBackupsLocationEditNote": "Забележка: Актуализирането на местоположението за архивиране няма да премести или промени съществуващите архиви",
"MessageBackupsLocationNoEditNote": "Забележка: Местоположението за архивиране се задава с помощта на променлива на средата и не може бъде променена от тук.",
"MessageBackupsLocationPathEmpty": "Пътят към местоположението за архивиране не може да бъде празен",
"MessageBatchEditPopulateMapDetailsAllHelp": "Популирайте активираните полета с данни от всички елементи. Полетата със няколко стоайности ще бъдат обединени",
"MessageBatchEditPopulateMapDetailsItemHelp": "Попълнете активираните полета с информация за картата с данни от този елемент",
"MessageBatchQuickMatchDescription": "Бързото Съпоставяне ще опита да добави липсващи корици и метаданни за избраните елементи. Активирайте опциите по-долу, за да позволите на Бързото съпоставяне да презапише съществуващите корици и/или метаданни.",
"MessageBookshelfNoCollections": "Все още нямате създадени колекции",
"MessageBookshelfNoCollectionsHelp": "Колекциите са публични. Всички потребители с достъп до библиотеката ще могат да ги виждат.",
"MessageBookshelfNoRSSFeeds": "Няма отворени RSS feed-ове",
"MessageBookshelfNoResultsForFilter": "Няма резултат за филтер \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "Няма резултати от заявката",
"MessageBookshelfNoSeries": "Нямаш сеЗЙ",
"MessageBookshelfNoSeries": "Нямате поредица",
"MessageBulkChapterPattern": "Колко глави искате да добавите, използвайки тази схема за номериране?",
"MessageChapterEndIsAfter": "Краят на главата е след края на вашата аудиокнига",
"MessageChapterErrorFirstNotZero": "Първата глава трябва да започва от 0",
"MessageChapterErrorStartGteDuration": "Началото на главата трябва да бъде по-малко от продължителността на аудиокнигата",
"MessageChapterErrorStartLtPrev": "Началото на главата трябва да бъде по-голямо или равно на края на предишната глава",
"MessageChapterStartIsAfter": "Началото на главата е след края на вашата аудиокнига",
"MessageChaptersNotFound": "Главите не са намерени",
"MessageCheckingCron": "Проверяване на cron...",
"MessageConfirmCloseFeed": "Сигурни ли сте, че искате да затворите този feed?",
"MessageConfirmDeleteApiKey": "Сигурни ли сте, че искате да изтриете API ключ \"{0}\"?",
"MessageConfirmDeleteBackup": "Сигурни ли сте, че искате да изтриете този архив {0}?",
"MessageConfirmDeleteDevice": "Сигурни ли сте, че искате да изтриете е-четец \"{0}\"?",
"MessageConfirmDeleteFile": "Това ще изтрие файла от файловата Ви система. Сигурни ли сте?",
"MessageConfirmDeleteLibrary": "Сигурни ли сте, че искате да изтриете за винаги библиотека \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "Това ще изтрие елемента от базата данни и файловата Ви система. Сигурни ли сте?",
"MessageConfirmDeleteLibraryItems": "Това ще изтрие {0} елемента от базата данни и файловата Ви система. Сигурни ли сте?",
"MessageConfirmDeleteMetadataProvider": "Сигурни ли сте, че искате да изтриете доставчика нa метаданни \"{0}\"?",
"MessageConfirmDeleteNotification": "Сигурни ли сте, че искате да изтриете това уведомление?",
"MessageConfirmDeleteSession": "Сигурни ли сте, че искате да изтриете тази сесия?",
"MessageConfirmEmbedMetadataInAudioFiles": "Сигурнли ли сте, че искате да вградите метаданните в {0} аудио файла?",
"MessageConfirmForceReScan": "Сигурни ли сте, че искате да принудите повторно сканиране?",
"MessageConfirmMarkAllEpisodesFinished": "Сигурни ли сте, че искате да маркирате всички епизоди като завършени?",
"MessageConfirmMarkAllEpisodesNotFinished": "Сигурни ли сте, че искате да маркирате всички епизоди като незавършени?",
"MessageConfirmMarkItemFinished": "Сигурни ли сте, че искате да маркирате \"{0}\" като приключено?",
"MessageConfirmMarkItemNotFinished": "Сигурни ли сте, че искате да маркирате \"{0}\" като неприключено?",
"MessageConfirmMarkSeriesFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като завършени?",
"MessageConfirmMarkSeriesNotFinished": "Сигурни ли сте, че искате да маркирате всички книги в тази серия като незавършени?",
"MessageConfirmNotificationTestTrigger": "Пуснете това уведомление с тестови данни?",
"MessageConfirmPurgeCache": "Изчистването на кеша ще изтрие цялата директория в <code>/metadata/cache</code>. <br /><br />Сигурни ли сте, че искате да премахнете директорията на кеша?",
"MessageConfirmPurgeItemsCache": "Изчистването на кеша на елементите ще изтрие цялата директория в <code>/metadata/cache/items</code>. <br />Сигурни ли сте?",
"MessageConfirmQuickEmbed": "Внимание! Бързото вграждане няма да архивира вашите аудио файлове. Уверете се, че имате резервно копие на вашите аудио файлове. <br><br>Искате ли да продължите?",
"MessageConfirmQuickMatchEpisodes": "Бързото сравняване на епизоди ще презапише детайлите, ако се намери съвпадение. Само не съвпаднали епизоди ще бъдат обновени. Сигурни ли сте?",
"MessageConfirmReScanLibraryItems": "Сигурни ли сте, че искате да сканирате отново {0} елемента?",
"MessageConfirmRemoveAllChapters": "Сигурни ли сте, че искате да премахнете всички глави?",
"MessageConfirmRemoveAuthor": "Сигурни ли сте, че искате да премахнете автор \"{0}\"?",
"MessageConfirmRemoveCollection": "Сигурни ли сте, че искате да премахнете колекция \"{0}\"?",
"MessageConfirmRemoveEpisode": "Сигурни ли сте, че искате да премахнете епизод \"{0}\"?",
"MessageConfirmRemoveEpisodeNote": "Забележка: Това няма да доведе до изтриване на аудио файла, освен ако не активирате опцията \"Твърдо изтриване на файла\"",
"MessageConfirmRemoveEpisodes": "Сигурни ли сте, че искате да премахнете {0} епизода?",
"MessageConfirmRemoveListeningSessions": "Сигурни ли сте, че искате да премахнете {0} слушателски сесии?",
"MessageConfirmRemoveMetadataFiles": "Сигурни ли сте, че искате да премахнете всичките метаданни. {0} файлове във папките на Вашата библиотека?",
"MessageConfirmRemoveNarrator": "Сигурни ли сте, че искате да премахнете разказвач \"{0}\"?",
"MessageConfirmRemovePlaylist": "Сигурни ли сте, че искате да премахнете плейлиста \"{0}\"?",
"MessageConfirmRenameGenre": "Сигурни ли сте, че искате да преименувате жанра \"{0}\" на \"{1}\" за всички елементи?",
@@ -745,19 +802,27 @@
"MessageConfirmRenameTag": "Сигурни ли сте, че искате да преименувате таг \"{0}\" на \"{1}\" за всички елементи?",
"MessageConfirmRenameTagMergeNote": "Забележка: Този таг вече съществува и ще бъде слято.",
"MessageConfirmRenameTagWarning": "Внимание! Вече съществува подобен таг с различно писане \"{0}\".",
"MessageConfirmResetProgress": "Сигурни ли сте, че искате да нулирате прогреса си?",
"MessageConfirmSendEbookToDevice": "Сигурни ли сте, че искате да изпратите {0} електронна книга \"{1}\" до устройство \"{2}\"?",
"MessageConfirmUnlinkOpenId": "Сигурни ли сте, че искате да отвържете този потребител от OpenID?",
"MessageDaysListenedInTheLastYear": "{0} дни слушане през последната година",
"MessageDownloadingEpisode": "Сваля епизод",
"MessageDragFilesIntoTrackOrder": "Плъзнете файлове в правилния ред на каналите",
"MessageEmbedFailed": "Вграждането беше неуспешно!",
"MessageEmbedFinished": "Вграждането завърши!",
"MessageEmbedQueue": "Поставено в опашката за вграждане на метаданни ({0} в опашката)",
"MessageEpisodesQueuedForDownload": "{0} Епизод(и) са сложени за сваляне",
"MessageEreaderDevices": "За да осигурите доставката на е-книги, може да се наложи да добавите горепосочения имейл адрес като валиден подател за всяко устройство, изброено по-долу.",
"MessageFeedURLWillBe": "Адресът на емисията ще бъде {0}",
"MessageFetching": "Извличане...",
"MessageForceReScanDescription": "ще сканира всички файлове отново като прясно сканиране. Аудио файлове ID3 тагове, OPF файлове и текстови файлове ще бъдат сканирани като нови.",
"MessageHeatmapListeningTimeTooltip": "<strong>{0} слушане</strong> на {1}",
"MessageHeatmapNoListeningSessions": "Няма сесии за слушане на {0}",
"MessageImportantNotice": "Важно Съобщение!",
"MessageInsertChapterBelow": "Вмъкни глава под",
"MessageItemsSelected": "{0} избрани",
"MessageItemsUpdated": "{0} елемента обновени",
"MessageInvalidAsin": "Невалиден ASIN",
"MessageItemsSelected": "{0} избрани елемента",
"MessageItemsUpdated": "{0} обновени елемента",
"MessageJoinUsOn": "Присъединете се към нас",
"MessageLoading": "Зарежда...",
"MessageLoadingFolders": "Зареждане на Папки...",
@@ -778,6 +843,7 @@
"MessageNoCollections": "Няма колекции",
"MessageNoCoversFound": "Не са намерени корици",
"MessageNoDescription": "Няма описание",
"MessageNoDevices": "Няма устройства",
"MessageNoDownloadsInProgress": "Няма изтегляния в прогрес",
"MessageNoDownloadsQueued": "Няма изтегляния в опашка",
"MessageNoEpisodeMatchesFound": "Няма намерени съвпадения за епизоди",
@@ -791,6 +857,7 @@
"MessageNoLogs": "Няма логове",
"MessageNoMediaProgress": "Няма прогрес на медията",
"MessageNoNotifications": "Няма известия",
"MessageNoPodcastFeed": "Невалиден подкаст: Няма канал",
"MessageNoPodcastsFound": "Няма намерени подкасти",
"MessageNoResults": "Няма резултати",
"MessageNoSearchResultsFor": "Няма резултати за \"{0}\"",
@@ -799,13 +866,19 @@
"MessageNoTasksRunning": "Няма вършещи се задачи",
"MessageNoUpdatesWereNecessary": "Няма нужда от обновяване",
"MessageNoUserPlaylists": "Нямате създадени плейлисти",
"MessageNoUserPlaylistsHelp": "Плейлистите за частни. Само създалият ги потребител ще може да ги вижда.",
"MessageNotYetImplemented": "Още не е изпълнено",
"MessageOpmlPreviewNote": "Забележка: Това е преглед на анализирания OPML файл. Действителното заглавие на подкаста ще бъде взето от RSS фийда.",
"MessageOr": "или",
"MessagePauseChapter": "Пауза на глава",
"MessagePlayChapter": "Пусни налчалото на глава",
"MessagePlaylistCreateFromCollection": "Създай плейлист от колекция",
"MessagePleaseWait": "Моля изчакайте...",
"MessagePodcastHasNoRSSFeedForMatching": "Подкастът няма URL адрес на RSS feed за използване за съпоставяне",
"MessagePodcastSearchField": "Въведи какво да търся или RSS емисия адрес",
"MessageQuickEmbedInProgress": "Бързото вграждане е в процес на изпълнение",
"MessageQuickEmbedQueue": "Поставено в опашката за бързо вграждане ({0} в опашката)",
"MessageQuickMatchAllEpisodes": "Бързо Сравняване на Всички Епизоди",
"MessageQuickMatchDescription": "Попълни празните детайли и корици с първия резултат от '{0}'. Не презаписва детайлите, освен ако не е активирана настройката 'Предпочети съвпадащи метаданни' на сървъра.",
"MessageRemoveChapter": "Премахни глава",
"MessageRemoveEpisodes": "Премахни {0} епизод(и)",
@@ -815,11 +888,52 @@
"MessageResetChaptersConfirm": "Сигурни ли сте, че искате да нулирате главите и да отмените промените, които сте направили?",
"MessageRestoreBackupConfirm": "Сигурни ли сте, че искате да възстановите архива създаден на",
"MessageRestoreBackupWarning": "Възстановяването на архив ще презапише цялата база данни, намираща се в /config и кориците в /metadata/items & /metadata/authors.<br /><br />Архивите не променят файловете в папките на вашата библиотека. Ако сте активирали настройките на сървъра за съхранение на корици и метаданни в папките на вашата библиотека, те няма да бъдат архивирани или презаписани.<br /><br />Всички клиенти, използващи вашия сървър, ще бъдат автоматично обновени.",
"MessageScheduleLibraryScanNote": "За повече потребители се препоръчва да оставят този фийчър изключен и да оставят настройката \"Автоматично преглеждане за промени в библиотеката\" включена - тя автоматично ще засече промени в папките на вашата библиотека. Включете тази настройка ако \"Автоматично преглеждане за промени в библиотеката\" не рабови на вашата файлова система (например NFS).",
"MessageScheduleRunEveryWeekdayAtTime": "Изпълни всеки {0} в {1}",
"MessageSearchResultsFor": "Резултати от търсенето за",
"MessageSelected": "{0} избрани",
"MessageSeriesSequenceCannotContainSpaces": "Подредбата в серия не може да съдържа шпации",
"MessageServerCouldNotBeReached": "Сървърът не може да бъде достигнат",
"MessageSetChaptersFromTracksDescription": "Задайте глави, като използвате всеки аудио файл като глава и заглавие на главата като име на аудио файла",
"MessageShareExpirationWillBe": "Изтичането ще бъде на <strong>{0}</strong>",
"MessageShareExpiresIn": "Изтича след {0}",
"MessageShareURLWillBe": "URL за споделяне ще бъде <strong>{0}</strong>",
"MessageStartPlaybackAtTime": "Започни възпроизвеждане на \"{0}\" в {1}?",
"MessageTaskAudioFileNotWritable": "На Аудио файл \"{0}\" не може да се записва",
"MessageTaskCanceledByUser": "Задачата е отказана от потребител",
"MessageTaskDownloadingEpisodeDescription": "Изтегляне на епизод \"{0}\"",
"MessageTaskEmbeddingMetadata": "Вграждане на метаданни",
"MessageTaskEmbeddingMetadataDescription": "Вграждане на метаданни в аудиокнига \"{0}\"",
"MessageTaskEncodingM4b": "Кодиране M4B",
"MessageTaskEncodingM4bDescription": "Кодиране на аудиокнига \"{0}\" в единичен m4b файл",
"MessageTaskFailed": "Неуспешно",
"MessageTaskFailedToBackupAudioFile": "Неуспешно създаване на разервно копие на аудио файл \"{0}\"",
"MessageTaskFailedToCreateCacheDirectory": "Неуспешно създаване на директория за кеширане",
"MessageTaskFailedToEmbedMetadataInFile": "Неуспешно вграждане на метаданни във файл \"{0}\"",
"MessageTaskFailedToMergeAudioFiles": "Неуспешно сливане на аудио файловете",
"MessageTaskFailedToMoveM4bFile": "Неуспешно преместване на m4b файл",
"MessageTaskFailedToWriteMetadataFile": "Неуспешно записване на файла за метаданни",
"MessageTaskMatchingBooksInLibrary": "Съответстващи книги в библиотека \"{0}\"",
"MessageTaskNoFilesToScan": "Няма файлове за сканиране",
"MessageTaskOpmlImport": "OPML импортиране",
"MessageTaskOpmlImportDescription": "Създаване на подкасти от {0} RSS хранилки",
"MessageTaskOpmlImportFeed": "OPML импортиран фийд",
"MessageTaskOpmlImportFeedDescription": "Импортиране на RSS хранилка \"{0}\"",
"MessageTaskOpmlImportFeedFailed": "Неуспешно взимане на подкаст фийд",
"MessageTaskOpmlImportFeedPodcastDescription": "Създаване на подкаст \"{0}\"",
"MessageTaskOpmlImportFeedPodcastExists": "На този път вече съществува подкаст",
"MessageTaskOpmlImportFeedPodcastFailed": "Неуспешно създаване на подкаст",
"MessageTaskOpmlImportFinished": "Добавени {0} подкаста",
"MessageTaskOpmlParseFailed": "Неуспешно анализиране на OPML файла",
"MessageTaskOpmlParseFastFail": "Невалиден OPML файл, не беше намерен нито <opml> таг нито <outline> таг",
"MessageTaskOpmlParseNoneFound": "Няма намерени канали във OPML файла",
"MessageTaskScanItemsAdded": "{0} добавени",
"MessageTaskScanItemsMissing": "{0} липсващи",
"MessageTaskScanItemsUpdated": "{0} обновени",
"MessageTaskScanNoChangesNeeded": "Не са нужни промени",
"MessageTaskScanningFileChanges": "Проверка за промени във файловете в \"{0}\"",
"MessageTaskScanningLibrary": "Сканиране на \"{0}\" библиотека",
"MessageTaskTargetDirectoryNotWritable": "Целевата директория не е достъпна за запис",
"MessageThinking": "Мисля...",
"MessageUploaderItemFailed": "Неуспешно качване",
"MessageUploaderItemSuccess": "Успешно качване!",
@@ -837,43 +951,117 @@
"NoteUploaderFoldersWithMediaFiles": "Папките с медийни файлове ще бъдат обработени като отделни елементи на библиотеката.",
"NoteUploaderOnlyAudioFiles": "Ако качвате само аудио файлове, то всеки аудио файл ще бъде обработен като отделна аудиокнига.",
"NoteUploaderUnsupportedFiles": "Неподдържаните файлове се игнорират. При избор или пускане на папка, други файлове, които не са в папка на елемент, се игнорират.",
"NotificationOnBackupCompletedDescription": "Изпълнява се при завършване на създаване на резервно копие",
"NotificationOnBackupFailedDescription": "Изпълнява се при неуспешено създаване на резервно копие",
"NotificationOnEpisodeDownloadedDescription": "Изпълнява се при автоматично изтегляне на подкаст епизод",
"NotificationOnRSSFeedDisabledDescription": "Изпълнява се, когато автоматичното изтегляне на епизодите е деактивирано, поради твърде много неуспешни опити",
"NotificationOnRSSFeedFailedDescription": "Пуска се когато заявката за RSS фийд е неуспешна за автоматично сваляне на епизод",
"NotificationOnTestDescription": "Event за тестване на системата за нотификации",
"PlaceholderBulkChapterInput": "Въведете име на глава или използвайте номериране (прим. 'Епизод 1', 'Глава 10', '1.')",
"PlaceholderNewCollection": "Ново име на колекцията",
"PlaceholderNewFolderPath": "Нов път на папката",
"PlaceholderNewPlaylist": "Ново име на плейлиста",
"PlaceholderSearch": "Търсене...",
"PlaceholderSearchEpisode": "Търсене на Епизоди...",
"StatsAuthorsAdded": "добаврени автори",
"StatsBooksAdded": "добавени книги",
"StatsBooksAdditional": "Някой от вкючените добавки…",
"StatsBooksFinished": "завършени книги",
"StatsBooksFinishedThisYear": "Някой от книгите приключени тази година…",
"StatsBooksListenedTo": "слушани книги",
"StatsCollectionGrewTo": "Твоята книжна колекция израсна до…",
"StatsSessions": "сесии",
"StatsSpentListening": "прекарано в слушане",
"StatsTopAuthor": "ТОП АВТОР",
"StatsTopAuthors": "ТОП АВТОРИ",
"StatsTopGenre": "ТОП ЖАНР",
"StatsTopGenres": "ТОП ЖАНРА",
"StatsTopMonth": "ТОП МЕСЕЦ",
"StatsTopNarrator": "ТОП РАЗКАЗВАЧ",
"StatsTopNarrators": "ТОП РАЗКАЗВАЧИ",
"StatsTotalDuration": "С пълно времетраене…",
"StatsYearInReview": "ГОДИНАТА В ПРЕГЛЕД",
"ToastAccountUpdateSuccess": "Успешно обновяване на акаунта",
"ToastAppriseUrlRequired": "Трябва да въведете Apprise URL",
"ToastAsinRequired": "ASIN-а е задължителен",
"ToastAuthorImageRemoveSuccess": "Авторската снимка е премахната",
"ToastAuthorNotFound": "Автор \"{0}\" не е намерен",
"ToastAuthorRemoveSuccess": "Арторът е премахнат",
"ToastAuthorSearchNotFound": "Авторът не е намерен",
"ToastAuthorUpdateMerged": "Обновяване на автора сливано",
"ToastAuthorUpdateSuccess": "Автора обновен",
"ToastAuthorUpdateSuccessNoImageFound": "Автор обновен (не е намерена снимка)",
"ToastBackupAppliedSuccess": "Архивът е приложен",
"ToastBackupCreateFailed": "Неуспешно създаване на архив",
"ToastBackupCreateSuccess": "Архивът е създаден",
"ToastBackupDeleteFailed": "Неуспешно изтриване на архив",
"ToastBackupDeleteSuccess": "Архивът е изтрит",
"ToastBackupInvalidMaxKeep": "Невалиден брой за архиви за запазване",
"ToastBackupInvalidMaxSize": "Невалиден максимален рамер на архив",
"ToastBackupRestoreFailed": "Неуспешно възстановяване на архив",
"ToastBackupUploadFailed": "Неуспешно качване на архив",
"ToastBackupUploadSuccess": "Архивът е качен",
"ToastBatchApplyDetailsToItemsSuccess": "Детайли приложени на предмети",
"ToastBatchDeleteFailed": "Груповото изтриване се провали",
"ToastBatchDeleteSuccess": "Успешно групово изтриване",
"ToastBatchQuickMatchFailed": "Груповото Бързо Съвпадение се провали!",
"ToastBatchQuickMatchStarted": "Груповото Бързо Съвпадение на {0} книги започна!",
"ToastBatchUpdateFailed": "Неуспешно групово актуализиране",
"ToastBatchUpdateSuccess": "Успешно групово актуализиране",
"ToastBookmarkCreateFailed": "Неуспешно създаване на отметка",
"ToastBookmarkCreateSuccess": "Отметката е създадена",
"ToastBookmarkRemoveSuccess": "Отметката е премахната",
"ToastBulkChapterInvalidCount": "Въведете число между 1 и 150",
"ToastCachePurgeFailed": "Неуспешно изчистване на кеша",
"ToastCachePurgeSuccess": "Успешно изчистване на кеша",
"ToastChapterLocked": "Главата е заключена.",
"ToastChapterStartTimeAdjusted": "Начално време на главате е настоено с {0} секунди",
"ToastChaptersAllLocked": "Всички глави са заключени. Оключете някой глави за да преместите техните времена.",
"ToastChaptersHaveErrors": "Главите имат грешки",
"ToastChaptersInvalidShiftAmountLast": "Невалидно време за преместване. Началният час на последната глава ще превиши общата продължителност на аудиокнигата.",
"ToastChaptersInvalidShiftAmountStart": "Невалидно време за преместване. Първата глава ще има нулева или отрицателна дължина и ще бъде презаписана от втората глава. Увеличете началното време на втората глава.",
"ToastChaptersMustHaveTitles": "Главите трябва да имат заглавия",
"ToastChaptersRemoved": "Главите са премахнати",
"ToastChaptersUpdated": "Главите са актуализирани",
"ToastCollectionItemsAddFailed": "Неуспешно добавяне на елемент(и) към колекцията",
"ToastCollectionRemoveSuccess": "Колекцията е премахната",
"ToastCollectionUpdateSuccess": "Колекцията е обновена",
"ToastConnectionNotAvailable": "Няма връзка. Моля, опитайте отново по-късно",
"ToastCoverSearchFailed": "Търсенето на корица е неуспешно",
"ToastCoverUpdateFailed": "Обновяването на корицата е неуспешно",
"ToastDateTimeInvalidOrIncomplete": "Датата и часът са невалидни или непълни",
"ToastDeleteFileFailed": "Неуспешно изтриване на файла",
"ToastDeleteFileSuccess": "Успешно изтриване на файла",
"ToastDeviceAddFailed": "Неуспешно добавяне на устройство",
"ToastDeviceNameAlreadyExists": "Вече съществува четец с това име",
"ToastDeviceTestEmailFailed": "Неуспешно изпращане на тестов имейл",
"ToastDeviceTestEmailSuccess": "Тестовият имейл е изпратен",
"ToastEmailSettingsUpdateSuccess": "Имейл настройките са актуализирани",
"ToastEncodeCancelFailed": "Неуспешно отменяне на кодирането",
"ToastEncodeCancelSucces": "Кодирането е отменено",
"ToastEpisodeDownloadQueueClearFailed": "Неуспешно изчистване на опашката",
"ToastEpisodeDownloadQueueClearSuccess": "Опашката за изтегляне на епизоди е изчистена",
"ToastEpisodeUpdateSuccess": "{0} епизода са актуализирани",
"ToastErrorCannotShare": "Не може да се споделя директно от това устройство",
"ToastFailedToCreate": "Неуспешно създаване",
"ToastFailedToDelete": "Неуспешно изтриване",
"ToastFailedToLoadData": "Неуспешно зареждане на данни",
"ToastFailedToMatch": "Неуспешно съвпадение",
"ToastFailedToShare": "Неуспешно споделяне",
"ToastFailedToUpdate": "Неуспешно актуализиране",
"ToastInvalidImageUrl": "Невалиден URL адрес на изображение",
"ToastInvalidMaxEpisodesToDownload": "Невалиден максимален брой епизоди за изтегляне",
"ToastInvalidUrl": "Невалиден URL адрес",
"ToastInvalidUrls": "Един или повече URL адреси са невалидни",
"ToastItemCoverUpdateSuccess": "Корицата на елемента е обновена",
"ToastItemDeletedFailed": "Неуспешно изтриване на елемента",
"ToastItemDeletedSuccess": "Елементът е изтрит",
"ToastItemDetailsUpdateSuccess": "Детайлите на елемента са обновени",
"ToastItemMarkedAsFinishedFailed": "Неуспешно маркиране като Завършено",
"ToastItemMarkedAsFinishedSuccess": "Елементът е маркиран като завършен",
"ToastItemMarkedAsNotFinishedFailed": "Неуспешно маркиране като Незавършено",
"ToastItemMarkedAsNotFinishedSuccess": "Елементът е маркиран като незавършен",
"ToastItemUpdateSuccess": "Елементът е актуализиран",
"ToastLibraryCreateFailed": "Неуспешно създаване на библиотека",
"ToastLibraryCreateSuccess": "Библиотеката \"{0}\" е създадена",
"ToastLibraryDeleteFailed": "Неуспешно изтриване на библиотека",
@@ -881,28 +1069,97 @@
"ToastLibraryScanFailedToStart": "Неуспешно стартиране на сканиране",
"ToastLibraryScanStarted": "Сканирането на библиотеката е стартирано",
"ToastLibraryUpdateSuccess": "Библиотеката \"{0}\" е обновена",
"ToastMatchAllAuthorsFailed": "Неуспешно съвпадение на всички автори",
"ToastMetadataFilesRemovedError": "Грешка при премахване на metadata.{0} файлове",
"ToastMetadataFilesRemovedNoneFound": "Не са намерени metadata.{0} файлове в библиотеката",
"ToastMetadataFilesRemovedNoneRemoved": "Не са премахнати metadata.{0} файлове",
"ToastMetadataFilesRemovedSuccess": "Премахнати са {0} файла metadata.{1}",
"ToastMustHaveAtLeastOnePath": "Трябва да има поне един път",
"ToastNameEmailRequired": "Изискват се име и имейл",
"ToastNameRequired": "Изисква се име",
"ToastNewApiKeyUserError": "Трябва да изберете потребител",
"ToastNewEpisodesFound": "Намерени са {0} нови епизода",
"ToastNewUserCreatedFailed": "Неуспешно създаване на акаунт: „{0}“",
"ToastNewUserCreatedSuccess": "Създаден е нов акаунт",
"ToastNewUserLibraryError": "Трябва да изберете поне една библиотека",
"ToastNewUserPasswordError": "Трябва да има парола; само root потребителят може да бъде с празна парола",
"ToastNewUserTagError": "Трябва да изберете поне един етикет",
"ToastNewUserUsernameError": "Въведете потребителско име",
"ToastNoNewEpisodesFound": "Не са намерени нови епизоди",
"ToastNoRSSFeed": "Подкастът няма RSS емисия",
"ToastNoUpdatesNecessary": "Не са необходими актуализации",
"ToastNotificationCreateFailed": "Неуспешно създаване на известие",
"ToastNotificationDeleteFailed": "Неуспешно изтриване на известието",
"ToastNotificationFailedMaximum": "Максималният брой неуспешни опити трябва да бъде >= 0",
"ToastNotificationQueueMaximum": "Максималната опашка за известия трябва да бъде >= 0",
"ToastNotificationSettingsUpdateSuccess": "Настройките за известия са актуализирани",
"ToastNotificationTestTriggerFailed": "Неуспешно задействане на тестово известие",
"ToastNotificationTestTriggerSuccess": "Тестовото известие е задействано",
"ToastNotificationUpdateSuccess": "Известието е актуализирано",
"ToastPlaylistCreateFailed": "Неуспешно създаване на плейлист",
"ToastPlaylistCreateSuccess": "Плейлистът е създаден",
"ToastPlaylistRemoveSuccess": "Плейлистът е премахнат",
"ToastPlaylistUpdateSuccess": "Плейлистът е обновен",
"ToastPodcastCreateFailed": "Неуспешно създаване на подкаст",
"ToastPodcastCreateSuccess": "Подкаст успешно създаден",
"ToastPodcastEpisodeUpdated": "Епизодът е актуализиран",
"ToastPodcastGetFeedFailed": "Неуспешно извличане на емисията на подкаста",
"ToastPodcastNoEpisodesInFeed": "Не са намерени епизоди в RSS емисията",
"ToastPodcastNoRssFeed": "Подкастът няма RSS емисия",
"ToastProgressIsNotBeingSynced": "Напредъкът не се синхронизира, рестартирайте възпроизвеждането",
"ToastProviderCreatedFailed": "Неуспешно добавяне на доставчик",
"ToastProviderCreatedSuccess": "Добавен е нов доставчик",
"ToastProviderNameAndUrlRequired": "Изискват се име и URL адрес",
"ToastProviderRemoveSuccess": "Доставчикът е премахнат",
"ToastRSSFeedCloseFailed": "Неуспешно затваряне на RSS емисията",
"ToastRSSFeedCloseSuccess": "RSS емисията е затворена",
"ToastRemoveFailed": "Неуспешно премахване",
"ToastRemoveItemFromCollectionFailed": "Неуспешно премахване на елемент от колекция",
"ToastRemoveItemFromCollectionSuccess": "Елементът е премахнат от колекция",
"ToastRemoveItemsWithIssuesFailed": "Неуспешно премахване на елементите от библиотеката с проблеми",
"ToastRemoveItemsWithIssuesSuccess": "Елементите от библиотеката с проблеми са премахнати",
"ToastRenameFailed": "Неуспешно преименуване",
"ToastRescanFailed": "Повторното сканиране е неуспешно за {0}",
"ToastRescanRemoved": "Повторното сканиране завърши: елементът е премахнат",
"ToastRescanUpToDate": "Повторното сканиране завърши: елементът вече е актуален",
"ToastRescanUpdated": "Повторното сканиране завърши: елементът е актуализиран",
"ToastScanFailed": "Неуспешно сканиране на елемент от библиотеката",
"ToastSelectAtLeastOneUser": "Изберете поне един потребител",
"ToastSendEbookToDeviceFailed": "Неуспешно изпращане на електронна книга до устройство",
"ToastSendEbookToDeviceSuccess": "Електронната книга е изпратена до устройство \"{0}\"",
"ToastSeriesSubmitFailedSameName": "Не могат да бъдат добавени два сериала с едно и също име",
"ToastSeriesUpdateFailed": "Неуспешно обновяване на серия",
"ToastSeriesUpdateSuccess": "Серията е обновена",
"ToastServerSettingsUpdateSuccess": "Настройките на сървъра са актуализирани",
"ToastSessionCloseFailed": "Неуспешно затваряне на сесията",
"ToastSessionDeleteFailed": "Неуспешно изтриване на сесия",
"ToastSessionDeleteSuccess": "Сесията е изтрита",
"ToastSleepTimerDone": "Таймерът за заспиване приключи... zZzzZz",
"ToastSlugMustChange": "Краткият URL (slug) съдържа невалидни символи",
"ToastSlugRequired": "Изисква се кратък URL (slug)",
"ToastSocketConnected": "Свързан сокет",
"ToastSocketDisconnected": "Сокетът е прекъснат",
"ToastSocketFailedToConnect": "Неуспешно свързване на сокет",
"ToastSortingPrefixesEmptyError": "Трябва да има поне 1 префикс за сортиране",
"ToastSortingPrefixesUpdateSuccess": "Префиксите за сортиране са актуализирани ({0} елемента)",
"ToastTitleRequired": "Изисква се заглавие",
"ToastUnknownError": "Неизвестна грешка",
"ToastUnlinkOpenIdFailed": "Неуспешно прекъсване на връзката на потребителя с OpenID",
"ToastUnlinkOpenIdSuccess": "Връзката на потребителя с OpenID е прекъсната",
"ToastUploaderFilepathExistsError": "Файловият път „{0}“ вече съществува на сървъра",
"ToastUploaderItemExistsInSubdirectoryError": "Елементът „{0}“ използва поддиректория на пътя за качване.",
"ToastUserDeleteFailed": "Неуспешно изтриване на потребител",
"ToastUserDeleteSuccess": "Потребителят е изтрит"
"ToastUserDeleteSuccess": "Потребителят е изтрит",
"ToastUserPasswordChangeSuccess": "Паролата е променена успешно",
"ToastUserPasswordMismatch": "Паролите не съвпадат",
"ToastUserPasswordMustChange": "Новата парола не може да бъде същата като старата",
"ToastUserRootRequireName": "Трябва да въведете root потребителско име",
"TooltipAddChapters": "Добавяне на глава(и)",
"TooltipAddOneSecond": "Добавяне на 1 секунда",
"TooltipAdjustChapterStart": "Кликнете за коригиране на началния час",
"TooltipLockAllChapters": "Заключване на всички глави",
"TooltipLockChapter": "Заключване на глава (Shift+клик за диапазон)",
"TooltipSubtractOneSecond": "Изваждане на 1 секунда",
"TooltipUnlockAllChapters": "Отключване на всички глави",
"TooltipUnlockChapter": "Отключване на глава (Shift+клик за диапазон)"
}
+9
View File
@@ -1,5 +1,6 @@
{
"ButtonAdd": "যোগ করুন",
"ButtonAddApiKey": "এপিআই কী যোগ করুন",
"ButtonAddChapters": "অধ্যায় যোগ করুন",
"ButtonAddDevice": "ডিভাইস যোগ করুন",
"ButtonAddLibrary": "লাইব্রেরি যোগ করুন",
@@ -10,6 +11,8 @@
"ButtonApplyChapters": "অধ্যায় প্রয়োগ করুন",
"ButtonAuthors": "লেখকগণ",
"ButtonBack": "পেছনে যান",
"ButtonBatchEditPopulateFromExisting": "বিদ্যমান থেকে পূরণ করুন",
"ButtonBatchEditPopulateMapDetails": "ম্যাপ থেকে পূরণ করুন",
"ButtonBrowseForFolder": "ফোল্ডারের জন্য ব্রাউজ করুন",
"ButtonCancel": "বাতিল করুন",
"ButtonCancelEncode": "এনকোড বাতিল করুন",
@@ -18,6 +21,7 @@
"ButtonChooseAFolder": "একটি ফোল্ডার চয়ন করুন",
"ButtonChooseFiles": "ফাইল চয়ন করুন",
"ButtonClearFilter": "ফিল্টার পরিষ্কার করুন",
"ButtonClose": "বন্ধ করুন",
"ButtonCloseFeed": "ফিড বন্ধ করুন",
"ButtonCloseSession": "খোলা সেশন বন্ধ করুন",
"ButtonCollections": "সংগ্রহ",
@@ -117,11 +121,13 @@
"HeaderAccount": "অ্যাকাউন্ট",
"HeaderAddCustomMetadataProvider": "কাস্টম মেটাডেটা সরবরাহকারী যোগ করুন",
"HeaderAdvanced": "অ্যাডভান্সড",
"HeaderApiKeys": "এপিআই কী সমূহ",
"HeaderAppriseNotificationSettings": "বিজ্ঞপ্তি সেটিংস অবহিত করুন",
"HeaderAudioTracks": "অডিও ট্র্যাকসগুলো",
"HeaderAudiobookTools": "অডিওবই ফাইল ম্যানেজমেন্ট টুলস",
"HeaderAuthentication": "প্রমাণীকরণ",
"HeaderBackups": "ব্যাকআপ",
"HeaderBulkChapterModal": "একাধিক অধ্যায় যোগ করুন",
"HeaderChangePassword": "পাসওয়ার্ড পরিবর্তন করুন",
"HeaderChapters": "অধ্যায়",
"HeaderChooseAFolder": "একটি ফোল্ডার চয়ন করুন",
@@ -160,6 +166,7 @@
"HeaderMetadataOrderOfPrecedence": "মেটাডেটা অগ্রাধিকারের ক্রম",
"HeaderMetadataToEmbed": "এম্বেড করার জন্য মেটাডেটা",
"HeaderNewAccount": "নতুন অ্যাকাউন্ট",
"HeaderNewApiKey": "নতুন API কী",
"HeaderNewLibrary": "নতুন লাইব্রেরি",
"HeaderNotificationCreate": "বিজ্ঞপ্তি তৈরি করুন",
"HeaderNotificationUpdate": "বিজ্ঞপ্তি আপডেট করুন",
@@ -175,6 +182,7 @@
"HeaderPlaylist": "প্লেলিস্ট",
"HeaderPlaylistItems": "প্লেলিস্ট আইটেম",
"HeaderPodcastsToAdd": "যোগ করার জন্য পডকাস্ট",
"HeaderPresets": "প্রিসেট",
"HeaderPreviewCover": "কভার ্দেখুন",
"HeaderRSSFeedGeneral": "আরএসএস বিবরণ",
"HeaderRSSFeedIsOpen": "আরএসএস ফিড খোলা আছে",
@@ -192,6 +200,7 @@
"HeaderSettingsExperimental": "পরীক্ষামূলক ফিচার",
"HeaderSettingsGeneral": "সাধারণ",
"HeaderSettingsScanner": "স্ক্যানার",
"HeaderSettingsSecurity": "নিরাপত্তা",
"HeaderSettingsWebClient": "ওয়েব ক্লায়েন্ট",
"HeaderSleepTimer": "স্লিপ টাইমার",
"HeaderStatsLargestItems": "সবচেয়ে বড় আইটেম",
+10 -1
View File
@@ -1,5 +1,6 @@
{
"ButtonAdd": "Afegeix",
"ButtonAddApiKey": "Afegeix clau API",
"ButtonAddChapters": "Afegeix capítols",
"ButtonAddDevice": "Afegeix un aparell",
"ButtonAddLibrary": "Afegeix una biblioteca",
@@ -20,6 +21,7 @@
"ButtonChooseAFolder": "Trieu una carpeta",
"ButtonChooseFiles": "Trieu fitxers",
"ButtonClearFilter": "Neteja el filtre",
"ButtonClose": "Tanca",
"ButtonCloseFeed": "Tanca el canal",
"ButtonCloseSession": "Tanca la sessió oberta",
"ButtonCollections": "Col·leccions",
@@ -119,11 +121,13 @@
"HeaderAccount": "Compte",
"HeaderAddCustomMetadataProvider": "Afegeix un proveïdor de metadades personalitzat",
"HeaderAdvanced": "Avançat",
"HeaderApiKeys": "Claus API",
"HeaderAppriseNotificationSettings": "Paràmetres de notificacions Apprise",
"HeaderAudioTracks": "Pistes d'àudio",
"HeaderAudiobookTools": "Eines de gestió de fitxers de l'audiollibre",
"HeaderAuthentication": "Autenticació",
"HeaderBackups": "Còpies de Seguretat",
"HeaderBulkChapterModal": "Afegeix capítols múltiples",
"HeaderChangePassword": "Canvia Contrasenya",
"HeaderChapters": "Capítols",
"HeaderChooseAFolder": "Tria una Carpeta",
@@ -162,6 +166,7 @@
"HeaderMetadataOrderOfPrecedence": "Ordre de Precedència de Metadades",
"HeaderMetadataToEmbed": "Metadades a Inserir",
"HeaderNewAccount": "Nou Compte",
"HeaderNewApiKey": "Nova clau API",
"HeaderNewLibrary": "Nova Biblioteca",
"HeaderNotificationCreate": "Crea Notificació",
"HeaderNotificationUpdate": "Actualització de Notificació",
@@ -195,6 +200,7 @@
"HeaderSettingsExperimental": "Funcionalitats experimentals",
"HeaderSettingsGeneral": "Generals",
"HeaderSettingsScanner": "Escàner",
"HeaderSettingsSecurity": "Seguretat",
"HeaderSettingsWebClient": "Client web",
"HeaderSleepTimer": "Temporitzador de son",
"HeaderStatsLargestItems": "Elements més grans",
@@ -417,6 +423,9 @@
"LabelLibraryFilterSublistEmpty": "Sense {0}",
"LabelLibraryItem": "Element de Biblioteca",
"LabelLibraryName": "Nom de Biblioteca",
"LabelLibrarySortByProgress": "Progrés: Última actualització",
"LabelLibrarySortByProgressFinished": "Progrés: Finalitzat",
"LabelLibrarySortByProgressStarted": "Progrés: Començat",
"LabelLimit": "Límits",
"LabelLineSpacing": "Interlineat",
"LabelListenAgain": "Escoltar de nou",
@@ -439,7 +448,7 @@
"LabelMetadataProvider": "Proveïdor de metadades",
"LabelMinute": "Minut",
"LabelMinutes": "Minuts",
"LabelMissing": "Absent",
"LabelMissing": "Falta",
"LabelMissingEbook": "No té llibre electrònic",
"LabelMissingSupplementaryEbook": "No té ebook complementari",
"LabelMobileRedirectURIs": "URI de redirecció mòbil permeses",
+72 -16
View File
@@ -1,5 +1,6 @@
{
"ButtonAdd": "Přidat",
"ButtonAddApiKey": "Přidat API klíč",
"ButtonAddChapters": "Přidat kapitoly",
"ButtonAddDevice": "Přidat zařízení",
"ButtonAddLibrary": "Přidat knihovnu",
@@ -10,7 +11,7 @@
"ButtonApplyChapters": "Aplikovat kapitoly",
"ButtonAuthors": "Autoři",
"ButtonBack": "Zpět",
"ButtonBatchEditPopulateFromExisting": "Vytvořit z existujících",
"ButtonBatchEditPopulateFromExisting": "Předvyplnit z existujících",
"ButtonBatchEditPopulateMapDetails": "Předvyplnit podrobnosti mapování",
"ButtonBrowseForFolder": "Vyhledat složku",
"ButtonCancel": "Zrušit",
@@ -20,6 +21,7 @@
"ButtonChooseAFolder": "Vybrat složku",
"ButtonChooseFiles": "Vybrat soubory",
"ButtonClearFilter": "Vymazat filtr",
"ButtonClose": "Zavřít",
"ButtonCloseFeed": "Zavřít kanál",
"ButtonCloseSession": "Zavřít otevřenou relaci",
"ButtonCollections": "Kolekce",
@@ -59,7 +61,7 @@
"ButtonPause": "Pozastavit",
"ButtonPlay": "Přehrát",
"ButtonPlayAll": "Přehrát vše",
"ButtonPlaying": "Hraje",
"ButtonPlaying": "Přehrává",
"ButtonPlaylists": "Seznamy skladeb",
"ButtonPrevious": "Předchozí",
"ButtonPreviousChapter": "Předchozí Kapitola",
@@ -69,7 +71,7 @@
"ButtonQueueAddItem": "Přidat do fronty",
"ButtonQueueRemoveItem": "Odstranit z fronty",
"ButtonQuickEmbed": "Rychle Zapsat",
"ButtonQuickEmbedMetadata": "Rychle zapsat Metadata",
"ButtonQuickEmbedMetadata": "Rychle Vložit Metadata",
"ButtonQuickMatch": "Rychlé přiřazení",
"ButtonReScan": "Znovu prohledat",
"ButtonRead": "Číst",
@@ -119,11 +121,13 @@
"HeaderAccount": "Účet",
"HeaderAddCustomMetadataProvider": "Přidat vlastního poskytovatele metadat",
"HeaderAdvanced": "Pokročilé",
"HeaderApiKeys": "API klíče",
"HeaderAppriseNotificationSettings": "Nastavení oznámení Apprise",
"HeaderAudioTracks": "Zvukové stopy",
"HeaderAudiobookTools": "Nástroje pro správu souborů audioknih",
"HeaderAuthentication": "Autentizace",
"HeaderBackups": "Zálohy",
"HeaderBulkChapterModal": "Přidat více kapitol",
"HeaderChangePassword": "Změnit heslo",
"HeaderChapters": "Kapitoly",
"HeaderChooseAFolder": "Zvolte složku",
@@ -162,6 +166,7 @@
"HeaderMetadataOrderOfPrecedence": "Pořadí priorit metadat",
"HeaderMetadataToEmbed": "Metadata k vložení",
"HeaderNewAccount": "Nový účet",
"HeaderNewApiKey": "Nový API klíč",
"HeaderNewLibrary": "Nová knihovna",
"HeaderNotificationCreate": "Vytvořit notifikaci",
"HeaderNotificationUpdate": "Aktualizovat notifikaci",
@@ -195,6 +200,7 @@
"HeaderSettingsExperimental": "Experimentální funkce",
"HeaderSettingsGeneral": "Obecné",
"HeaderSettingsScanner": "Skener",
"HeaderSettingsSecurity": "Zabezpečení",
"HeaderSettingsWebClient": "Webový klient",
"HeaderSleepTimer": "Časovač vypnutí",
"HeaderStatsLargestItems": "Největší položky",
@@ -206,6 +212,7 @@
"HeaderTableOfContents": "Obsah",
"HeaderTools": "Nástroje",
"HeaderUpdateAccount": "Aktualizovat účet",
"HeaderUpdateApiKey": "Aktualizovat API klíč",
"HeaderUpdateAuthor": "Aktualizovat autora",
"HeaderUpdateDetails": "Aktualizovat podrobnosti",
"HeaderUpdateLibrary": "Aktualizovat knihovnu",
@@ -235,6 +242,10 @@
"LabelAllUsersExcludingGuests": "Všichni uživatelé kromě hostů",
"LabelAllUsersIncludingGuests": "Všichni uživatelé včetně hostů",
"LabelAlreadyInYourLibrary": "Již ve vaší knihovně",
"LabelApiKeyCreated": "API klíč \"{0}\" byl úspěšně vytvořen.",
"LabelApiKeyCreatedDescription": "Zkopírujte si API klíč nyní, později již nebude možné jej zobrazit.",
"LabelApiKeyUser": "Vydávat se za uživatele",
"LabelApiKeyUserDescription": "Tento API klíč bude mít stejná oprávnění jako uživatel za něhož vystupuje. V protokolech to bude vypadat jako kdyby požadavky vytvářel přímo daný uživatel.",
"LabelApiToken": "API Token",
"LabelAppend": "Připojit",
"LabelAudioBitrate": "Bitový tok zvuku (např. 128k)",
@@ -284,6 +295,7 @@
"LabelContinueListening": "Pokračovat v poslechu",
"LabelContinueReading": "Pokračovat ve čtení",
"LabelContinueSeries": "Pokračovat v sérii",
"LabelCorsAllowed": "Povolené CORS Origins",
"LabelCover": "Obálka",
"LabelCoverImageURL": "URL obrázku obálky",
"LabelCoverProvider": "Poskytovatel obálky",
@@ -297,6 +309,7 @@
"LabelDeleteFromFileSystemCheckbox": "Smazat ze souborového systému (zrušte zaškrtnutí pro odstranění pouze z databáze)",
"LabelDescription": "Popis",
"LabelDeselectAll": "Odznačit vše",
"LabelDetectedPattern": "Detekovaný vzor:",
"LabelDevice": "Zařízení",
"LabelDeviceInfo": "Informace o zařízení",
"LabelDeviceIsAvailableTo": "Zařízení je dostupné pro...",
@@ -346,11 +359,15 @@
"LabelExample": "Příklad",
"LabelExpandSeries": "Rozbalit série",
"LabelExpandSubSeries": "Rozbalit podsérie",
"LabelExpired": "Expirovaný",
"LabelExpiresAt": "Expiruje v",
"LabelExpiresInSeconds": "Expiruje za (sekundy)",
"LabelExpiresNever": "Nikdy",
"LabelExplicit": "Explicitní",
"LabelExplicitChecked": "Explicitní (zaškrtnuto)",
"LabelExplicitUnchecked": "Není explicitní (nezaškrtnuto)",
"LabelExportOPML": "Export OPML",
"LabelFeedURL": "URL zdroje",
"LabelFeedURL": "URL kanálu",
"LabelFetchingMetadata": "Získávání metadat",
"LabelFile": "Soubor",
"LabelFileBirthtime": "Čas vzniku souboru",
@@ -361,21 +378,22 @@
"LabelFilterByUser": "Filtrovat podle uživatele",
"LabelFindEpisodes": "Najít epizody",
"LabelFinished": "Dokončeno",
"LabelFinishedDate": "Dokončeno {0}",
"LabelFolder": "Složka",
"LabelFolders": "Složky",
"LabelFontBold": "Tučně",
"LabelFontBoldness": "Výraznost písma",
"LabelFontFamily": "Rodina písem",
"LabelFontItalic": "Kurzíva",
"LabelFontScale": "Měřítko písma",
"LabelFontScale": "Velikost písma",
"LabelFontStrikethrough": "Přeškrtnutí",
"LabelFormat": "Formát",
"LabelFull": "Plné",
"LabelGenre": "Žánr",
"LabelGenres": "Žánry",
"LabelHardDeleteFile": "Trvale smazat soubor",
"LabelHasEbook": "Obsahuje elektronickou knihu",
"LabelHasSupplementaryEbook": "Obsahuje doplňkovou elektronickou knihu",
"LabelHasEbook": "Má e-knihu",
"LabelHasSupplementaryEbook": "Obsahuje doplňkovou e-knihu",
"LabelHideSubtitles": "Skrýt titulky",
"LabelHighestPriority": "Nejvyšší priorita",
"LabelHost": "Hostitel",
@@ -405,6 +423,7 @@
"LabelLanguages": "Jazyky",
"LabelLastBookAdded": "Poslední kniha přidána",
"LabelLastBookUpdated": "Poslední kniha aktualizována",
"LabelLastProgressDate": "Poslední pokrok: {0}",
"LabelLastSeen": "Naposledy viděno",
"LabelLastTime": "Naposledy",
"LabelLastUpdate": "Poslední aktualizace",
@@ -417,6 +436,9 @@
"LabelLibraryFilterSublistEmpty": "Žádné {0}",
"LabelLibraryItem": "Položka knihovny",
"LabelLibraryName": "Název knihovny",
"LabelLibrarySortByProgress": "Pokrok: naposledy aktualizováno",
"LabelLibrarySortByProgressFinished": "Pokrok: dokončeno",
"LabelLibrarySortByProgressStarted": "Pokrok: začato",
"LabelLimit": "Omezit",
"LabelLineSpacing": "Řádkování",
"LabelListenAgain": "Poslouchat znovu",
@@ -425,10 +447,11 @@
"LabelLogLevelWarn": "Varovat",
"LabelLookForNewEpisodesAfterDate": "Hledat nové epizody po tomto datu",
"LabelLowestPriority": "Nejnižší priorita",
"LabelMatchConfidence": "Jistota",
"LabelMatchExistingUsersBy": "Přiřadit stávající uživatele podle",
"LabelMatchExistingUsersByDescription": "Slouží k propojení stávajících uživatelů. Po propojení budou uživatelé přiřazeni k jedinečnému ID od poskytovatele SSO",
"LabelMaxEpisodesToDownload": "Maximální # epizod pro stažení. Použijte 0 pro bez omezení.",
"LabelMaxEpisodesToDownloadPerCheck": "Maximální počet nových epizod ke stažení při jedné kontrole",
"LabelMaxEpisodesToDownloadPerCheck": "Maximální # nových epizod ke stažení při jedné kontrole",
"LabelMaxEpisodesToKeep": "Maximální počet epizod k zachování",
"LabelMaxEpisodesToKeepHelp": "Hodnotou 0 není nastaven žádný maximální limit. Po automatickém stažení nové epizody se odstraní nejstarší epizoda, pokud máte více než X epizod. Při každém novém stažení se odstraní pouze 1 epizoda.",
"LabelMediaPlayer": "Přehrávač médií",
@@ -454,7 +477,9 @@
"LabelNewestAuthors": "Nejnovější autoři",
"LabelNewestEpisodes": "Nejnovější epizody",
"LabelNextBackupDate": "Datum příští zálohy",
"LabelNextChapters": "Další kapitola bude:",
"LabelNextScheduledRun": "Další naplánované spuštění",
"LabelNoApiKeys": "Žádné API klíče",
"LabelNoCustomMetadataProviders": "Žádní vlastní poskytovatelé metadat",
"LabelNoEpisodesSelected": "Nebyly vybrány žádné epizody",
"LabelNotFinished": "Nedokončeno",
@@ -470,6 +495,7 @@
"LabelNotificationsMaxQueueSize": "Maximální velikost fronty pro oznamovací události",
"LabelNotificationsMaxQueueSizeHelp": "Události jsou omezeny na 1 za sekundu. Události budou ignorovány, pokud je fronta v maximální velikosti. Tím se zabrání spamování oznámení.",
"LabelNumberOfBooks": "Počet knih",
"LabelNumberOfChapters": "Počet kapitol:",
"LabelNumberOfEpisodes": "Počet epizod",
"LabelOpenIDAdvancedPermsClaimDescription": "Název požadavku OpenID, který obsahuje rozšířená oprávnění pro akce uživatele v rámci aplikace, která se budou vztahovat na role, které nejsou administrátory (<b>pokud jsou nakonfigurovány</b>). Pokud požadavek v odpovědi chybí, přístup do systému ABS bude zamítnut. Pokud chybí jediná možnost, bude považována za <code>false</code>. Ujistěte se, že deklarace poskytovatele identity odpovídá očekávané struktuře:",
"LabelOpenIDClaims": "Následující možnosti ponechte prázdné, abyste zakázali pokročilé přiřazování skupin a oprávnění a automatické přiřazení skupiny \"User\".",
@@ -544,6 +570,7 @@
"LabelSelectAll": "Vybrat vše",
"LabelSelectAllEpisodes": "Vybrat všechny epizody",
"LabelSelectEpisodesShowing": "Vyberte {0} epizody, které se zobrazují",
"LabelSelectUser": "Vybrat uživatele",
"LabelSelectUsers": "Vybrat uživatele",
"LabelSendEbookToDevice": "Odeslat e-knihu do...",
"LabelSequence": "Sekvence",
@@ -562,7 +589,7 @@
"LabelSettingsChromecastSupport": "Podpora Chromecastu",
"LabelSettingsDateFormat": "Formát data",
"LabelSettingsEnableWatcher": "Automaticky skenovat změny v knihovnách",
"LabelSettingsEnableWatcherForLibrary": "Automaticky skenovat změny v knihovně",
"LabelSettingsEnableWatcherForLibrary": "Automaticky sledovat změny v knihovně",
"LabelSettingsEnableWatcherHelp": "Povoluje automatické přidávání/aktualizaci položek, když jsou zjištěny změny souborů. *Vyžaduje restart serveru",
"LabelSettingsEpubsAllowScriptedContent": "Povolení skriptovaného obsahu v epubu",
"LabelSettingsEpubsAllowScriptedContentHelp": "Povolení spouštění skriptů v souborech epub. Doporučujeme toto nastavení vypnout, pokud nedůvěřujete zdroji souborů epub.",
@@ -611,6 +638,7 @@
"LabelStartTime": "Čas Spuštění",
"LabelStarted": "Spuštěno",
"LabelStartedAt": "Spuštěno v",
"LabelStartedDate": "Spuštěno {0}",
"LabelStatsAudioTracks": "Zvukové stopy",
"LabelStatsAuthors": "Autoři",
"LabelStatsBestDay": "Nejlepší den",
@@ -640,6 +668,7 @@
"LabelTheme": "Téma",
"LabelThemeDark": "Tmavé",
"LabelThemeLight": "Světlé",
"LabelThemeSepia": "Hnědé",
"LabelTimeBase": "Časová základna",
"LabelTimeDurationXHours": "{0} hodin",
"LabelTimeDurationXMinutes": "{0} minut",
@@ -708,7 +737,9 @@
"MessageAddToPlayerQueue": "Přidat do fronty přehrávače",
"MessageAppriseDescription": "Abyste mohli používat tuto funkci, musíte mít spuštěnou instanci <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> nebo API, které bude zpracovávat stejné požadavky. <br />Adresa URL API Apprise by měla být úplná URL cesta pro odeslání oznámení, např. pokud je vaše instance API obsluhována na adrese <code>http://192.168.1.1:8337</code> pak byste měli zadat <code>http://192.168.1.1:8337/notify</code>.",
"MessageAsinCheck": "Ujistěte se, že používáte ASIN ze správného regionu Audible a ne z Amazonu.",
"MessageAuthenticationLegacyTokenWarning": "Zastaralé API tokeny budou v budoucnu odstraněny. Použijte místo nich <a href=\"/config/api-keys\">API klíče</a>.",
"MessageAuthenticationOIDCChangesRestart": "Po uložení restartujte server, aby se změny OIDC použily.",
"MessageAuthenticationSecurityMessage": "Bezpečnost autentizace byla vylepšena. Všichni uživatelé se musí znovu přihlásit.",
"MessageBackupsDescription": "Zálohy zahrnují uživatele, průběh uživatele, podrobnosti o položkách knihovny, nastavení serveru a obrázky uložené v <code>/metadata/items</code> a <code>/metadata/authors</code>. Zálohy <strong>ne</strong> zahrnují všechny soubory uložené ve složkách knihovny.",
"MessageBackupsLocationEditNote": "Poznámka: Změna umístění záloh nepřesune ani nezmění existující zálohy",
"MessageBackupsLocationNoEditNote": "Poznámka: Umístění záloh je nastavené z proměnných prostředí a nelze zde změnit.",
@@ -722,6 +753,7 @@
"MessageBookshelfNoResultsForFilter": "Filtr \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "Žádné výsledky pro dotaz",
"MessageBookshelfNoSeries": "Nemáte žádnou sérii",
"MessageBulkChapterPattern": "Kolik kapitol chcete přidat s tímto vzorem číslování?",
"MessageChapterEndIsAfter": "Konec kapitoly přesahuje konec audioknihy",
"MessageChapterErrorFirstNotZero": "První kapitola musí začínat na 0",
"MessageChapterErrorStartGteDuration": "Neplatný čas začátku, musí být kratší než doba trvání audioknihy",
@@ -730,6 +762,7 @@
"MessageChaptersNotFound": "Kapitoly nenalezeny",
"MessageCheckingCron": "Kontrola cronu...",
"MessageConfirmCloseFeed": "Opravdu chcete zavřít tento kanál?",
"MessageConfirmDeleteApiKey": "Opravdu chcete vymazat API klíč \"{0}\"?",
"MessageConfirmDeleteBackup": "Opravdu chcete smazat zálohu pro {0}?",
"MessageConfirmDeleteDevice": "Opravdu chcete vymazat zařízení e-reader \"{0}\"?",
"MessageConfirmDeleteFile": "Tento krok smaže soubor ze souborového systému. Jsi si jisti?",
@@ -747,7 +780,7 @@
"MessageConfirmMarkItemNotFinished": "Opravdu chcete označit \"{0}\" jako nedokončené?",
"MessageConfirmMarkSeriesFinished": "Opravdu chcete označit všechny knihy z této série jako dokončené?",
"MessageConfirmMarkSeriesNotFinished": "Opravdu chcete označit všechny knihy z této série jako nedokončené?",
"MessageConfirmNotificationTestTrigger": "Spustit toto oznámení s testovacími daty?",
"MessageConfirmNotificationTestTrigger": "Vyvolat tuto notifikaci s testovacími daty?",
"MessageConfirmPurgeCache": "Vyčistit mezipaměť odstraní celý adresář na adrese <code>/metadata/cache</code>. <br /><br />Určitě chcete odstranit adresář mezipaměti?",
"MessageConfirmPurgeItemsCache": "Vyčištění mezipaměti položek odstraní celý adresář <code>/metadata/cache/items</code>.<br />Jste si jistí?",
"MessageConfirmQuickEmbed": "Varování! Rychlé vložení nezálohuje vaše zvukové soubory. Ujistěte se, že máte zálohu zvukových souborů. <br><br>Chcete pokračovat?",
@@ -757,6 +790,7 @@
"MessageConfirmRemoveAuthor": "Opravdu chcete odstranit autora \"{0}\"?",
"MessageConfirmRemoveCollection": "Opravdu chcete odstranit kolekci \"{0}\"?",
"MessageConfirmRemoveEpisode": "Opravdu chcete odstranit epizodu \"{0}\"?",
"MessageConfirmRemoveEpisodeNote": "Poznámka: Tím se zvukový soubor neodstraní, pokud nepřepnete volbu “Tvrdé odstranění souboru“",
"MessageConfirmRemoveEpisodes": "Opravdu chcete odstranit {0} epizody?",
"MessageConfirmRemoveListeningSessions": "Opravdu chcete odebrat {0} poslechových relací?",
"MessageConfirmRemoveMetadataFiles": "Jste si jisti, že chcete odstranit všechny metadata.{0} soubory ve složkách s položkami ve vaší knihovně?",
@@ -782,6 +816,8 @@
"MessageFeedURLWillBe": "URL zdroje bude {0}",
"MessageFetching": "Načítání...",
"MessageForceReScanDescription": "znovu prohledá všechny soubory jako při novém skenování. ID3 tagy zvukových souborů OPF soubory a textové soubory budou skenovány jako nové.",
"MessageHeatmapListeningTimeTooltip": "<strong>{0} poslechnuto</strong> na {1}",
"MessageHeatmapNoListeningSessions": "Žádné relace poslouchání na {0}",
"MessageImportantNotice": "Důležité upozornění!",
"MessageInsertChapterBelow": "Vložit kapitolu níže",
"MessageInvalidAsin": "Neplatný ASIN",
@@ -818,7 +854,7 @@
"MessageNoItems": "Žádné položky",
"MessageNoItemsFound": "Nebyly nalezeny žádné položky",
"MessageNoListeningSessions": "Žádné poslechové relace",
"MessageNoLogs": "Žádné logy",
"MessageNoLogs": "Žádné záznamy událostí",
"MessageNoMediaProgress": "Žádný průběh médií",
"MessageNoNotifications": "Žádná oznámení",
"MessageNoPodcastFeed": "Neplatný podcast: Žádný kanál",
@@ -848,7 +884,7 @@
"MessageRemoveEpisodes": "Odstranit {0} epizodu",
"MessageRemoveFromPlayerQueue": "Odstranit z fronty přehrávače",
"MessageRemoveUserWarning": "Opravdu chcete trvale smazat uživatele \"{0}\"?",
"MessageReportBugsAndContribute": "Hlásit chyby, žádat o funkce a přispívat",
"MessageReportBugsAndContribute": "Nahlašte chyby, vyžádejte si funkce a přispěte na",
"MessageResetChaptersConfirm": "Opravdu chcete resetovat kapitoly a vrátit zpět provedené změny?",
"MessageRestoreBackupConfirm": "Opravdu chcete obnovit zálohu vytvořenou dne",
"MessageRestoreBackupWarning": "Obnovení zálohy přepíše celou databázi umístěnou v /config a obálku obrázků v /metadata/items & /metadata/authors.<br /><br />Backups nezmění žádné soubory ve složkách knihovny. Pokud jste povolili nastavení serveru pro ukládání obrázků obalu a metadat do složek knihovny, nebudou zálohovány ani přepsány.<br /><br />Všichni klienti používající váš server budou automaticky obnoveni.",
@@ -881,7 +917,7 @@
"MessageTaskNoFilesToScan": "Žádné soubory k prohledání",
"MessageTaskOpmlImport": "Import OPML",
"MessageTaskOpmlImportDescription": "Vytváření podcastů z {0} RSS feedů",
"MessageTaskOpmlImportFeed": "Importní zdroj OPML",
"MessageTaskOpmlImportFeed": "Import OPML feedu",
"MessageTaskOpmlImportFeedDescription": "Importování RSS feedu \"{0}\"",
"MessageTaskOpmlImportFeedFailed": "Nepodařilo se získat kanál podcastu",
"MessageTaskOpmlImportFeedPodcastDescription": "Vytváření podcastu \"{0}\"",
@@ -921,6 +957,7 @@
"NotificationOnRSSFeedDisabledDescription": "Aktivováno když je automatické stahování pozastaveno z důvodu příliš mnoho neůspěšných pokusů",
"NotificationOnRSSFeedFailedDescription": "Aktivováno když selže RSS kanál pro stahování epizod",
"NotificationOnTestDescription": "Akce pro otestování upozorňovacího systému",
"PlaceholderBulkChapterInput": "Zadejte název kapitoly nebo použije číslování (např. 'Epizoda 1', 'Kapitola 10', '1.')",
"PlaceholderNewCollection": "Nový název kolekce",
"PlaceholderNewFolderPath": "Nová cesta ke složce",
"PlaceholderNewPlaylist": "Nový název seznamu přehrávání",
@@ -974,8 +1011,12 @@
"ToastBookmarkCreateFailed": "Vytvoření záložky se nezdařilo",
"ToastBookmarkCreateSuccess": "Přidána záložka",
"ToastBookmarkRemoveSuccess": "Záložka odstraněna",
"ToastBulkChapterInvalidCount": "Zadejte číslo mezi 1 a 150",
"ToastCachePurgeFailed": "Nepodařilo se vyčistit mezipaměť",
"ToastCachePurgeSuccess": "Vyrovnávací paměť úspěšně vyčištěna",
"ToastChapterLocked": "Kapitola je uzamčena.",
"ToastChapterStartTimeAdjusted": "Začátek kapitoly posunut o {0} sekund",
"ToastChaptersAllLocked": "Všechny kapitoly jsou uzamčeny. Pro posun kapitol některé odemkněte.",
"ToastChaptersHaveErrors": "Kapitoly obsahují chyby",
"ToastChaptersInvalidShiftAmountLast": "Nesprávná délka posunu. Čas začátku poslední kapitoly by přesáhl dobu trvání této audioknihy.",
"ToastChaptersInvalidShiftAmountStart": "Nesprávná délka posunu. První kapitola by měla nulovou nebo zápornou délku a byla by přepsána druhou kapitolou. Zvětšete čas začátku druhé kapitoly.",
@@ -985,6 +1026,8 @@
"ToastCollectionItemsAddFailed": "Přidávání položek do kolekce selhalo",
"ToastCollectionRemoveSuccess": "Kolekce odstraněna",
"ToastCollectionUpdateSuccess": "Kolekce aktualizována",
"ToastConnectionNotAvailable": "Připojení není k dispozici. Zkuste to prosím znovu později",
"ToastCoverSearchFailed": "Hledání obálky se nezdařilo",
"ToastCoverUpdateFailed": "Aktualizace obálky selhala",
"ToastDateTimeInvalidOrIncomplete": "Datum a čas jsou chybné nebo nekompletní",
"ToastDeleteFileFailed": "Nepodařilo se smazat soubor",
@@ -994,12 +1037,14 @@
"ToastDeviceTestEmailFailed": "Odeslání testovacího emailu selhalo",
"ToastDeviceTestEmailSuccess": "Testovací email byl odeslán",
"ToastEmailSettingsUpdateSuccess": "Nastavení emailu aktualizována",
"ToastEncodeCancelFailed": "Chyba zrušení dování",
"ToastEncodeCancelFailed": "Zrušení encodování selhalo",
"ToastEncodeCancelSucces": "Kódování zrušeno",
"ToastEpisodeDownloadQueueClearFailed": "Vyčištění fronty selhalo",
"ToastEpisodeDownloadQueueClearSuccess": "Fronta stahování epizod je prázdná",
"ToastEpisodeUpdateSuccess": "{0} epizod aktualizováno",
"ToastErrorCannotShare": "Na tomto zařízení nelze nativně sdílet",
"ToastFailedToCreate": "Nepodařilo se vytvořit",
"ToastFailedToDelete": "Nepodařilo se odstranit",
"ToastFailedToLoadData": "Nepodařilo se načíst data",
"ToastFailedToMatch": "Nepodařilo se spárovat",
"ToastFailedToShare": "Sdílení selhalo",
@@ -1007,6 +1052,7 @@
"ToastInvalidImageUrl": "Neplatná URL obrázku",
"ToastInvalidMaxEpisodesToDownload": "Neplatný maximální počet epizod ke stažení",
"ToastInvalidUrl": "Neplatná URL",
"ToastInvalidUrls": "Alespoň jedna URL je neplatná",
"ToastItemCoverUpdateSuccess": "Obálka předmětu byl aktualizována",
"ToastItemDeletedFailed": "Smazání položky selhalo",
"ToastItemDeletedSuccess": "Položka smazána",
@@ -1031,6 +1077,7 @@
"ToastMustHaveAtLeastOnePath": "Musí mít minimálně jednu cestu",
"ToastNameEmailRequired": "Jméno a email jsou vyžadovány",
"ToastNameRequired": "Jméno je vyžadováno",
"ToastNewApiKeyUserError": "Je nutné vybrat uživatele",
"ToastNewEpisodesFound": "{0} nových epizod bylo nalezeno",
"ToastNewUserCreatedFailed": "Chyba při vytváření účtu: \"{0}\"",
"ToastNewUserCreatedSuccess": "Vytvořen nový účet",
@@ -1055,6 +1102,7 @@
"ToastPlaylistUpdateSuccess": "Seznam přehrávání aktualizován",
"ToastPodcastCreateFailed": "Vytvoření podcastu se nezdařilo",
"ToastPodcastCreateSuccess": "Podcast byl úspěšně vytvořen",
"ToastPodcastEpisodeUpdated": "Epizoda aktualizována",
"ToastPodcastGetFeedFailed": "Chyba při získání podcastového feedu",
"ToastPodcastNoEpisodesInFeed": "Žádné epizody nenalezeny v RSS feedu",
"ToastPodcastNoRssFeed": "Podcast nemá RSS feed",
@@ -1087,7 +1135,7 @@
"ToastSessionDeleteFailed": "Nepodařilo se smazat relaci",
"ToastSessionDeleteSuccess": "Relace smazána",
"ToastSleepTimerDone": "Uspání knížky ... zZzzZz",
"ToastSlugMustChange": "Slug (URL) obsahuje chybné znaky",
"ToastSlugMustChange": "Slug obsahuje chybné znaky",
"ToastSlugRequired": "Slug (URL) je vyžadována",
"ToastSocketConnected": "Socket připojen",
"ToastSocketDisconnected": "Socket odpojen",
@@ -1105,5 +1153,13 @@
"ToastUserPasswordChangeSuccess": "Heslo bylo změněno úspěšně",
"ToastUserPasswordMismatch": "Hesla se neschodují",
"ToastUserPasswordMustChange": "Nové heslo se musí lišit od předchozího",
"ToastUserRootRequireName": "Musíte zadat uživatelské jméno root"
"ToastUserRootRequireName": "Musíte zadat uživatelské jméno root",
"TooltipAddChapters": "Přidat kapitolu/y",
"TooltipAddOneSecond": "Přidat 1 sekundu",
"TooltipAdjustChapterStart": "Kliknutím upravte začátek",
"TooltipLockAllChapters": "Uzamknout všechny kapitoly",
"TooltipLockChapter": "Uzamknout kapitolu (Shift+klik pro rozsah)",
"TooltipSubtractOneSecond": "Odečíst 1 sekundu",
"TooltipUnlockAllChapters": "Odemknout všechny kapitoly",
"TooltipUnlockChapter": "Odemknout kapitolu (Shift+klik pro rozsah)"
}
+325 -267
View File
File diff suppressed because it is too large Load Diff
+81 -25
View File
@@ -1,5 +1,6 @@
{
"ButtonAdd": "Hinzufügen",
"ButtonAddApiKey": "API-Schlüssel hinzufügen",
"ButtonAddChapters": "Kapitel hinzufügen",
"ButtonAddDevice": "Gerät hinzufügen",
"ButtonAddLibrary": "Bibliothek hinzufügen",
@@ -12,14 +13,15 @@
"ButtonBack": "Zurück",
"ButtonBatchEditPopulateFromExisting": "Auffüllen aus vorhandenem",
"ButtonBatchEditPopulateMapDetails": "Kartendetails auffüllen",
"ButtonBrowseForFolder": "Ordnersuche",
"ButtonBrowseForFolder": "Ordner auswählen",
"ButtonCancel": "Abbrechen",
"ButtonCancelEncode": "Codierung abbrechen",
"ButtonCancelEncode": "Konvertierung abbrechen",
"ButtonChangeRootPassword": "Hauptpasswort ändern",
"ButtonCheckAndDownloadNewEpisodes": "Überprüfe & lade neue Episoden herunter",
"ButtonChooseAFolder": "Wähle einen Ordner",
"ButtonChooseFiles": "Wähle eine Datei",
"ButtonClearFilter": "Filter löschen",
"ButtonClose": "Schließen",
"ButtonCloseFeed": "Feed schließen",
"ButtonCloseSession": "Offene Sitzung schließen",
"ButtonCollections": "Sammlungen",
@@ -32,7 +34,7 @@
"ButtonEditChapters": "Kapitel bearbeiten",
"ButtonEditPodcast": "Podcast bearbeiten",
"ButtonEnable": "Aktivieren",
"ButtonFireAndFail": "Abfeuern und versagen",
"ButtonFireAndFail": "Abschicken und fehlschlagen",
"ButtonFireOnTest": "Test-Event abfeuern",
"ButtonForceReScan": "Komplett-Scan (alle Medien)",
"ButtonFullPath": "Vollständiger Pfad",
@@ -53,7 +55,7 @@
"ButtonNext": "Vor",
"ButtonNextChapter": "Nächstes Kapitel",
"ButtonNextItemInQueue": "Das nächste Element in der Warteschlange",
"ButtonOk": "Ok",
"ButtonOk": "OK",
"ButtonOpenFeed": "Feed öffnen",
"ButtonOpenManager": "Manager öffnen",
"ButtonPause": "Pausieren",
@@ -73,7 +75,7 @@
"ButtonQuickMatch": "Schnellabgleich",
"ButtonReScan": "Neu scannen",
"ButtonRead": "Lesen",
"ButtonReadLess": "weniger Anzeigen",
"ButtonReadLess": "Weniger Anzeigen",
"ButtonReadMore": "Mehr Anzeigen",
"ButtonRefresh": "Neu Laden",
"ButtonRemove": "Entfernen",
@@ -102,7 +104,7 @@
"ButtonStartM4BEncode": "M4B-Kodierung starten",
"ButtonStartMetadataEmbed": "Metadateneinbettung starten",
"ButtonStats": "Statistiken",
"ButtonSubmit": "Ok",
"ButtonSubmit": "Absenden",
"ButtonTest": "Test",
"ButtonUnlinkOpenId": "OpenID trennen",
"ButtonUpload": "Hochladen",
@@ -114,16 +116,18 @@
"ButtonViewAll": "Alles anzeigen",
"ButtonYes": "Ja",
"ErrorUploadFetchMetadataAPI": "Fehler beim Abrufen der Metadaten",
"ErrorUploadFetchMetadataNoResults": "Metadaten konnten nicht abgerufen werden. Versuche den Titel und oder den Autor zu aktualisieren",
"ErrorUploadFetchMetadataNoResults": "Metadaten konnten nicht abgerufen werden - versuche den Titel und/oder den Autor zu aktualisieren",
"ErrorUploadLacksTitle": "Es muss ein Titel eingegeben werden",
"HeaderAccount": "Konto",
"HeaderAddCustomMetadataProvider": "Benutzerdefinierten Metadatenanbieter hinzufügen",
"HeaderAdvanced": "Erweitert",
"HeaderApiKeys": "API-Schlüssel",
"HeaderAppriseNotificationSettings": "Apprise Benachrichtigungseinstellungen",
"HeaderAudioTracks": "Audiodateien",
"HeaderAudiobookTools": "Hörbuch-Dateiverwaltungswerkzeuge",
"HeaderAuthentication": "Authentifizierung",
"HeaderBackups": "Sicherungen",
"HeaderBulkChapterModal": "Mehrere Kapitel hinzufügen",
"HeaderChangePassword": "Passwort ändern",
"HeaderChapters": "Kapitel",
"HeaderChooseAFolder": "Wähle einen Ordner",
@@ -134,13 +138,13 @@
"HeaderCustomMessageOnLogin": "Benutzerdefinierte Nachricht für die Anmeldung",
"HeaderCustomMetadataProviders": "Benutzerdefinierte Metadatenanbieter",
"HeaderDetails": "Details",
"HeaderDownloadQueue": "Download Warteschlange",
"HeaderEbookFiles": "E-Buch-Dateien",
"HeaderDownloadQueue": "Download-Warteschlange",
"HeaderEbookFiles": "E-Book-Dateien",
"HeaderEmail": "E-Mail",
"HeaderEmailSettings": "E-Mail-Einstellungen",
"HeaderEpisodes": "Episoden",
"HeaderEreaderDevices": "E-Reader Geräte",
"HeaderEreaderSettings": "Einstellungen zum Lesen",
"HeaderEreaderSettings": "E-Reader-Einstellungen",
"HeaderFiles": "Dateien",
"HeaderFindChapters": "Kapitel suchen",
"HeaderIgnoredFiles": "Ignorierte Dateien",
@@ -162,6 +166,7 @@
"HeaderMetadataOrderOfPrecedence": "Metadaten Rangfolge",
"HeaderMetadataToEmbed": "Einzubettende Metadaten",
"HeaderNewAccount": "Neues Konto",
"HeaderNewApiKey": "Neuen API-Schlüssel erstellen",
"HeaderNewLibrary": "Neue Bibliothek",
"HeaderNotificationCreate": "Benachrichtigung erstellen",
"HeaderNotificationUpdate": "Benachrichtigung bearbeiten",
@@ -195,6 +200,7 @@
"HeaderSettingsExperimental": "Experimentelle Funktionen",
"HeaderSettingsGeneral": "Allgemein",
"HeaderSettingsScanner": "Scanner",
"HeaderSettingsSecurity": "Sicherheit",
"HeaderSettingsWebClient": "Web-Client",
"HeaderSleepTimer": "Sleep-Timer",
"HeaderStatsLargestItems": "Größte Medien",
@@ -206,6 +212,7 @@
"HeaderTableOfContents": "Inhaltsverzeichnis",
"HeaderTools": "Werkzeuge",
"HeaderUpdateAccount": "Konto aktualisieren",
"HeaderUpdateApiKey": "API-Schlüssel aktualisieren",
"HeaderUpdateAuthor": "Autor aktualisieren",
"HeaderUpdateDetails": "Details aktualisieren",
"HeaderUpdateLibrary": "Bibliothek aktualisieren",
@@ -235,6 +242,10 @@
"LabelAllUsersExcludingGuests": "Alle Benutzer außer Gästen",
"LabelAllUsersIncludingGuests": "Alle Benutzer und Gäste",
"LabelAlreadyInYourLibrary": "Bereits in der Bibliothek",
"LabelApiKeyCreated": "API-Schlüssel \"{0}\" erfolgreich erstellt.",
"LabelApiKeyCreatedDescription": "Speichere den API-Schlüssel an einem sicheren Ort, du wirst ihn später nicht mehr abrufen können.",
"LabelApiKeyUser": "Im Kontext eines Nutzers agieren",
"LabelApiKeyUserDescription": "Dieser API-Schlüssel hat die gleichen Berechtigungen wie der Benutzer, in dessen Namen er erstellt wurde .In den Protokollen wird es aussehen, als ob der Benutzer die Anfrage durchführte.",
"LabelApiToken": "API Schlüssel",
"LabelAppend": "Anhängen",
"LabelAudioBitrate": "Audiobitrate (z. B. 128 kbit/s)",
@@ -284,6 +295,7 @@
"LabelContinueListening": "Weiterhören",
"LabelContinueReading": "Weiterlesen",
"LabelContinueSeries": "Serien fortsetzen",
"LabelCorsAllowed": "Erlaubte CORS Quellen",
"LabelCover": "Titelbild",
"LabelCoverImageURL": "URL des Titelbildes",
"LabelCoverProvider": "Titelbildanbieter",
@@ -297,6 +309,7 @@
"LabelDeleteFromFileSystemCheckbox": "Löschen von der Festplatte + Datenbank (deaktivieren um nur aus der Datenbank zu entfernen)",
"LabelDescription": "Beschreibung",
"LabelDeselectAll": "Alles abwählen",
"LabelDetectedPattern": "Erkanntes Muster:",
"LabelDevice": "Gerät",
"LabelDeviceInfo": "Geräteinformationen",
"LabelDeviceIsAvailableTo": "Dem Gerät ist es möglich zu ...",
@@ -318,7 +331,7 @@
"LabelEmail": "E-Mail",
"LabelEmailSettingsFromAddress": "Sender",
"LabelEmailSettingsRejectUnauthorized": "Nicht autorisierte Zertifikate ablehnen",
"LabelEmailSettingsRejectUnauthorizedHelp": "Durch das Deaktivieren der SSL-Zertifikatsüberprüfung kann deine Verbindung Sicherheitsrisiken wie Man-in-the-Middle-Angriffen ausgesetzt sein. Deaktiviere diese Option nur, wenn due die Auswirkungen verstehst und dem E-Mail-Server vertraust, mit dem eine Verbindung hergestellt wird.",
"LabelEmailSettingsRejectUnauthorizedHelp": "Durch das Deaktivieren der SSL-Zertifikatsüberprüfung kann deine Verbindung Sicherheitsrisiken wie Man-in-the-Middle-Angriffen ausgesetzt sein. Deaktiviere diese Option nur, wenn du die Auswirkungen verstehst und dem E-Mail-Server vertraust, mit dem eine Verbindung hergestellt wird.",
"LabelEmailSettingsSecure": "Sicher",
"LabelEmailSettingsSecureHelp": "Wenn an, verwendet die Verbindung TLS, wenn du eine Verbindung zum Server herstellst. Bei „aus“ wird TLS verwendet, wenn der Server die STARTTLS-Erweiterung unterstützt. In den meisten Fällen solltest du diesen Wert auf „an“ schalten, wenn du eine Verbindung zu Port 465 herstellst. Für Port 587 oder 25 behalte den Wert „aus“ bei. (von nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Test-Adresse",
@@ -346,7 +359,11 @@
"LabelExample": "Beispiel",
"LabelExpandSeries": "Serie ausklappen",
"LabelExpandSubSeries": "Unterserie ausklappen",
"LabelExplicit": "Explizit (Altersbeschränkung)",
"LabelExpired": "Abgelaufen",
"LabelExpiresAt": "Läuft ab am",
"LabelExpiresInSeconds": "Ablauf in (seconds) Sekunden",
"LabelExpiresNever": "Niemals",
"LabelExplicit": "Explizit",
"LabelExplicitChecked": "Explicit (Altersbeschränkung) (angehakt)",
"LabelExplicitUnchecked": "Not Explicit (Altersbeschränkung) (nicht angehakt)",
"LabelExportOPML": "OPML exportieren",
@@ -361,6 +378,7 @@
"LabelFilterByUser": "Nach Benutzern filtern",
"LabelFindEpisodes": "Episoden suchen",
"LabelFinished": "Beendet",
"LabelFinishedDate": "Beendet {0}",
"LabelFolder": "Ordner",
"LabelFolders": "Verzeichnisse",
"LabelFontBold": "Fett",
@@ -405,6 +423,7 @@
"LabelLanguages": "Sprachen",
"LabelLastBookAdded": "Zuletzt hinzugefügtes Buch",
"LabelLastBookUpdated": "Zuletzt aktualisiertes Buch",
"LabelLastProgressDate": "Letzter Fortschritt: {0}",
"LabelLastSeen": "Zuletzt gesehen",
"LabelLastTime": "Letztes Mal",
"LabelLastUpdate": "Letzte Aktualisierung",
@@ -417,6 +436,9 @@
"LabelLibraryFilterSublistEmpty": "Keine {0}",
"LabelLibraryItem": "Bibliothekseintrag",
"LabelLibraryName": "Bibliotheksname",
"LabelLibrarySortByProgress": "Fortschritt: Letzte Aktualisierung",
"LabelLibrarySortByProgressFinished": "Fortschritt: Beendet",
"LabelLibrarySortByProgressStarted": "Fortschritt: Gestartet",
"LabelLimit": "Begrenzung",
"LabelLineSpacing": "Zeilenabstand",
"LabelListenAgain": "Erneut anhören",
@@ -425,6 +447,7 @@
"LabelLogLevelWarn": "Warnungen",
"LabelLookForNewEpisodesAfterDate": "Suche nach neuen Episoden nach diesem Datum",
"LabelLowestPriority": "Niedrigste Priorität",
"LabelMatchConfidence": "Vertrauenswert",
"LabelMatchExistingUsersBy": "Zuordnen existierender Benutzer mit",
"LabelMatchExistingUsersByDescription": "Wird zum Verbinden vorhandener Benutzer verwendet. Sobald die Verbindung hergestellt ist, wird den Benutzern eine eindeutige ID vom SSO-Anbieter zugeordnet",
"LabelMaxEpisodesToDownload": "Max. Anzahl an Episoden zum Herunterladen, 0 für unbegrenzte Episoden.",
@@ -433,7 +456,7 @@
"LabelMaxEpisodesToKeepHelp": "0 setzt keine Begrenzung. Wenn eine neue Episode automatisch heruntergeladen wird, wird die älteste Episode gelöscht, wenn du mehr als X Episoden gespeichert hast. Es wird nur eine Episode pro neuem Download gelöscht.",
"LabelMediaPlayer": "Mediaplayer",
"LabelMediaType": "Medientyp",
"LabelMetaTag": "Meta Schlagwort",
"LabelMetaTag": "Meta Tag",
"LabelMetaTags": "Meta Tags",
"LabelMetadataOrderOfPrecedenceDescription": "Höher priorisierte Quellen für Metadaten überschreiben Metadaten aus Quellen mit niedrigerer Priorität",
"LabelMetadataProvider": "Metadatenanbieter",
@@ -454,7 +477,9 @@
"LabelNewestAuthors": "Neueste Autoren",
"LabelNewestEpisodes": "Neueste Episoden",
"LabelNextBackupDate": "Nächstes Sicherungsdatum",
"LabelNextChapters": "Das nächste Kapitel ist:",
"LabelNextScheduledRun": "Nächster planmäßiger Durchlauf",
"LabelNoApiKeys": "Keine API-Schlüssel vorhanden",
"LabelNoCustomMetadataProviders": "Keine benutzerdefinierten Metadata Anbieter",
"LabelNoEpisodesSelected": "Keine Episoden ausgewählt",
"LabelNotFinished": "Nicht beendet",
@@ -470,6 +495,7 @@
"LabelNotificationsMaxQueueSize": "Maximale Größe der Warteschlange für die Benachrichtigungsereignisse",
"LabelNotificationsMaxQueueSizeHelp": "Es wird nur 1 Ereignis pro Sekunde ausgelöst. Ereignisse werden ignoriert, wenn die Warteschlange die maximale Größe erreicht hat. Dies verhindert Benachrichtigungsspamming.",
"LabelNumberOfBooks": "Anzahl der Hörbücher",
"LabelNumberOfChapters": "Anzahl an Kapiteln:",
"LabelNumberOfEpisodes": "Anzahl der Episoden",
"LabelOpenIDAdvancedPermsClaimDescription": "Name des OpenID-Claims, der erweiterte Berechtigungen für Benutzeraktionen innerhalb der Anwendung enthält, die auf Nicht-Admin-Rollen angewendet werden (<b>wenn konfiguriert</b>). Wenn der Claim in der Antwort fehlt, wird der Zugang zu ABS verweigert. Fehlt eine einzelne Option, wird sie als <code>false</code> behandelt. Stelle sicher, dass der Claim des Identitätsanbieters der erwarteten Struktur entspricht:",
"LabelOpenIDClaims": "Lass die folgenden Optionen leer, um die erweiterte Zuweisung von Gruppen und Berechtigungen zu deaktivieren und automatisch die 'User'-Gruppe zuzuweisen.",
@@ -514,7 +540,7 @@
"LabelPublishers": "Herausgeber",
"LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail",
"LabelRSSFeedCustomOwnerName": "Benutzerdefinierter Name des Eigentümers",
"LabelRSSFeedOpen": "RSS Feed öffnen",
"LabelRSSFeedOpen": "RSS-Feed offen",
"LabelRSSFeedPreventIndexing": "Indizierung verhindern",
"LabelRSSFeedSlug": "RSS-Feed-Schlagwort",
"LabelRSSFeedURL": "RSS-Feed-URL",
@@ -544,6 +570,7 @@
"LabelSelectAll": "Alles auswählen",
"LabelSelectAllEpisodes": "Alle Episoden auswählen",
"LabelSelectEpisodesShowing": "{0} ausgewählte Episoden werden angezeigt",
"LabelSelectUser": "Ausgewählter Benutzer",
"LabelSelectUsers": "Benutzer auswählen",
"LabelSendEbookToDevice": "E-Buch senden an …",
"LabelSequence": "Reihenfolge",
@@ -561,8 +588,8 @@
"LabelSettingsBookshelfViewHelp": "Skeumorphes Design mit Holzeinlegeböden",
"LabelSettingsChromecastSupport": "Chromecastunterstützung",
"LabelSettingsDateFormat": "Datumsformat",
"LabelSettingsEnableWatcher": "Bibliotheken automatisch nach Änderungen durchsuchen",
"LabelSettingsEnableWatcherForLibrary": "Bibliothek automatisch nach Änderungen durchsuchen",
"LabelSettingsEnableWatcher": "Bibliotheken automatisch nach Änderungen überwachen",
"LabelSettingsEnableWatcherForLibrary": "Bibliothek automatisch auf Änderungen überwachen",
"LabelSettingsEnableWatcherHelp": "Aktiviert das automatische Hinzufügen/Aktualisieren von Elementen, wenn Dateiänderungen erkannt werden. *Erfordert einen Server-Neustart",
"LabelSettingsEpubsAllowScriptedContent": "Skriptinhalte in Epubs zulassen",
"LabelSettingsEpubsAllowScriptedContentHelp": "Erlaube Epub-Dateien, Skripte auszuführen. Es wird empfohlen, diese Einstellung deaktiviert zu lassen, es sei denn, du vertraust der Quelle der Epub-Dateien.",
@@ -595,7 +622,7 @@
"LabelSettingsStoreMetadataWithItemHelp": "Standardmäßig werden die Metadaten in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Metadaten als OPF-Datei (Textdatei) in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet",
"LabelSettingsTimeFormat": "Zeitformat",
"LabelShare": "Freigeben",
"LabelShareDownloadableHelp": "Erlaubt es einem Nutzer, mit dem Link, die Dateien des Mediums als ZIP herunterzuladen.",
"LabelShareDownloadableHelp": "Erlaubt es einem Nutzer, mit dem Link die Dateien des Mediums als ZIP herunterzuladen.",
"LabelShareOpen": "Freigeben",
"LabelShareURL": "Freigabe URL",
"LabelShowAll": "Alles anzeigen",
@@ -611,6 +638,7 @@
"LabelStartTime": "Startzeit",
"LabelStarted": "Gestartet",
"LabelStartedAt": "Gestartet am",
"LabelStartedDate": "Angefangen am {0}",
"LabelStatsAudioTracks": "Audiodateien",
"LabelStatsAuthors": "Autoren",
"LabelStatsBestDay": "Bester Tag",
@@ -640,6 +668,7 @@
"LabelTheme": "Farbschema",
"LabelThemeDark": "Dunkel",
"LabelThemeLight": "Hell",
"LabelThemeSepia": "Sepia",
"LabelTimeBase": "Basiszeit",
"LabelTimeDurationXHours": "{0} Stunden",
"LabelTimeDurationXMinutes": "{0} Minuten",
@@ -681,7 +710,7 @@
"LabelUploaderDragAndDropFilesOnly": "Dateien per Drag & Drop hierher ziehen",
"LabelUploaderDropFiles": "Dateien löschen",
"LabelUploaderItemFetchMetadataHelp": "Automatisches Aktualisieren von Titel, Autor und Serie",
"LabelUseAdvancedOptions": "Nutze Erweiterte Optionen",
"LabelUseAdvancedOptions": "Erweiterte Optionen verwenden",
"LabelUseChapterTrack": "Kapiteldatei verwenden",
"LabelUseFullTrack": "Gesamte Datei verwenden",
"LabelUseZeroForUnlimited": "0 für unbegrenzt",
@@ -707,8 +736,10 @@
"LabelYourProgress": "Fortschritt",
"MessageAddToPlayerQueue": "Zur Abspielwarteliste hinzufügen",
"MessageAppriseDescription": "Um diese Funktion nutzen zu können, musst du eine Instanz von <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> laufen haben oder eine API verwenden welche dieselbe Anfragen bearbeiten kann. <br />Die Apprise API Url muss der vollständige URL-Pfad sein, an den die Benachrichtigung gesendet werden soll, z.B. wenn Ihre API-Instanz unter <code>http://192.168.1.1:8337</code> läuft, würdest du <code>http://192.168.1.1:8337/notify</code> eingeben.",
"MessageAsinCheck": "Stellen Sie sicher, dass Sie die ASIN aus der richtigen Audible Region verwenden, nicht Amazon.",
"MessageAsinCheck": "Stelle sicher, dass die ASIN aus der richtigen Audible Region verwendet wird, nicht Amazon.",
"MessageAuthenticationLegacyTokenWarning": "Nicht mehr unterstützte API tokens werden in der Zukunft entfernt. Nutze stattdessen <a href=\"/config/api-keys\">API Schlüssel</a>.",
"MessageAuthenticationOIDCChangesRestart": "Nach dem Speichern muss der Server neugestartet werden um die OIDC Änderungen zu übernehmen.",
"MessageAuthenticationSecurityMessage": "Die Anmeldung wurde abgesichert. Benutzersitzungen werden getrennt, alle Benutzer müssen sich erneut anmelden.",
"MessageBackupsDescription": "In einer Sicherung werden Benutzer, Benutzerfortschritte, Details zu den Bibliotheksobjekten, Servereinstellungen und Bilder welche in <code>/metadata/items</code> & <code>/metadata/authors</code> gespeichert sind gespeichert. Sicherungen enthalten keine Dateien welche in den einzelnen Bibliotheksordnern (Medien-Ordnern) gespeichert sind.",
"MessageBackupsLocationEditNote": "Hinweis: Durch das Aktualisieren des Backup-Speicherorts werden vorhandene Sicherungen nicht verschoben oder geändert",
"MessageBackupsLocationNoEditNote": "Hinweis: Der Sicherungsspeicherort wird über eine Umgebungsvariable festgelegt und kann hier nicht geändert werden.",
@@ -722,6 +753,7 @@
"MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für Filter \"{0}: {1}\"",
"MessageBookshelfNoResultsForQuery": "Keine Ergebnisse für die Abfrage",
"MessageBookshelfNoSeries": "Keine Serien vorhanden",
"MessageBulkChapterPattern": "Wie viele Kapitel mit diesem Nummerierungs-Muster sollen hinzugefügt werden?",
"MessageChapterEndIsAfter": "Ungültige Kapitelendzeit: Kapitelende > Mediumende (Kapitelende liegt nach dem Ende des Mediums)",
"MessageChapterErrorFirstNotZero": "Ungültige Kapitelstartzeit: Das erste Kapitel muss bei 0 beginnen",
"MessageChapterErrorStartGteDuration": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumlänge (Kapitelanfang liegt zeitlich nach dem Ende des Mediums -> Lösung: Kapitelanfang < Mediumlänge)",
@@ -730,6 +762,7 @@
"MessageChaptersNotFound": "Kapitel gefunden nicht",
"MessageCheckingCron": "Überprüfe Cron...",
"MessageConfirmCloseFeed": "Feed wird geschlossen! Bist du dir sicher?",
"MessageConfirmDeleteApiKey": "Möchtest du den API-Schlüssel \"{0}\" wirklich entfernen ?",
"MessageConfirmDeleteBackup": "Sicherung für {0} wird gelöscht! Bist du dir sicher?",
"MessageConfirmDeleteDevice": "Möchtest du das Lesegerät „{0}“ wirklich löschen?",
"MessageConfirmDeleteFile": "Datei wird vom System gelöscht! Bist du dir sicher?",
@@ -757,6 +790,7 @@
"MessageConfirmRemoveAuthor": "Autor \"{0}\" wird enfernt! Bist du dir sicher?",
"MessageConfirmRemoveCollection": "Sammlung \"{0}\" wird entfernt! Bist du dir sicher?",
"MessageConfirmRemoveEpisode": "Episode \"{0}\" wird entfernt! Bist du dir sicher?",
"MessageConfirmRemoveEpisodeNote": "Hinweis: Die Audiodatei wird nicht gelöscht, es sei denn \"Datei dauerhaft löschen\" ist aktiviert",
"MessageConfirmRemoveEpisodes": "{0} Episoden werden entfernt! Bist du dir sicher?",
"MessageConfirmRemoveListeningSessions": "Bist du dir sicher, dass du {0} Hörsitzungen enfernen möchtest?",
"MessageConfirmRemoveMetadataFiles": "Bist du sicher, dass du alle metadata.{0} Dateien in deinen Bibliotheksordnern löschen willst?",
@@ -782,6 +816,8 @@
"MessageFeedURLWillBe": "Feed-URL wird {0} sein",
"MessageFetching": "Wird abgerufen …",
"MessageForceReScanDescription": "Durchsucht alle Dateien erneut, wie bei einem frischen Scan. ID3-Tags von Audiodateien, OPF-Dateien und Textdateien werden neu durchsucht.",
"MessageHeatmapListeningTimeTooltip": "<strong>{0} gehört</strong> auf {1}",
"MessageHeatmapNoListeningSessions": "Keine Hörsitzungen am {0}",
"MessageImportantNotice": "Wichtiger Hinweis!",
"MessageInsertChapterBelow": "Kapitel unten einfügen",
"MessageInvalidAsin": "Ungültige ASIN",
@@ -852,7 +888,7 @@
"MessageResetChaptersConfirm": "Kapitel und vorgenommenen Änderungen werden zurückgesetzt und rückgängig gemacht! Bist du dir sicher?",
"MessageRestoreBackupConfirm": "Bist du dir sicher, dass du die Sicherung wiederherstellen willst, welche am",
"MessageRestoreBackupWarning": "Bei der Wiederherstellung einer Sicherung wird die gesamte Datenbank unter /config und die Titelbilder in /metadata/items und /metadata/authors überschrieben.<br /><br />Bei der Sicherung werden keine Dateien in deinen Bibliotheksordnern verändert. Wenn du die Servereinstellungen aktiviert hast, um Cover und Metadaten in deinen Bibliotheksordnern zu speichern, werden diese nicht gesichert oder überschrieben.<br /><br />Alle Clients, die Ihren Server nutzen, werden automatisch aktualisiert.",
"MessageScheduleLibraryScanNote": "Für die meisten Anwender wird empfohlen, diese Funktion deaktiviert und die Ordnerüberwachung aktiviert zu lassen. Die Ordnerüberwachung wird Änderungen in den Bibliotheksordnern automatisch erkennen. Die Ordnerüberwachung funktioniert nicht mit allen Dateisystemen (wie NFS), hier kann stattdessen die automatischen Bibliothekssuchen verwendet werden.",
"MessageScheduleLibraryScanNote": "Für die meisten Benutzer wird empfohlen, diese Funktion deaktiviert und die Einstellung „Bibliothek automatisch auf Änderungen überwachen“ aktiviert zu lassen dadurch werden Änderungen in Ihren Bibliotheksordnern automatisch erkannt. Aktivieren Sie diese Funktion, wenn „Bibliothek automatisch auf Änderungen überwachen“ für Ihr Dateisystem (wie NFS) nicht funktioniert.",
"MessageScheduleRunEveryWeekdayAtTime": "Immer {0} um {1} ausführen",
"MessageSearchResultsFor": "Suchergebnisse für",
"MessageSelected": "{0} ausgewählt",
@@ -921,6 +957,7 @@
"NotificationOnRSSFeedDisabledDescription": "Wird ausgeführt wenn automatische Downloads von Episoden wegen zu vielen fehlgeschlagenen Versuchen deaktiviert sind",
"NotificationOnRSSFeedFailedDescription": "Wird ausgelöst, wenn die RSS-Feed-Anforderung für einen automatischen Episoden-Download fehlschlägt",
"NotificationOnTestDescription": "Wird ausgeführt wenn das Benachrichtigungssystem getestet wird",
"PlaceholderBulkChapterInput": "Kapitelbezeichnung eingeben oder Nummerierung verwenden (z.B. 'Episode 1', 'Kapitel 10', '1.')",
"PlaceholderNewCollection": "Neuer Sammlungsname",
"PlaceholderNewFolderPath": "Neuer Ordnerpfad",
"PlaceholderNewPlaylist": "Neuer Wiedergabelistenname",
@@ -974,17 +1011,23 @@
"ToastBookmarkCreateFailed": "Lesezeichen konnte nicht erstellt werden",
"ToastBookmarkCreateSuccess": "Lesezeichen hinzugefügt",
"ToastBookmarkRemoveSuccess": "Lesezeichen entfernt",
"ToastBulkChapterInvalidCount": "Gebe eine Zahl zwischen 1 und 150 ein",
"ToastCachePurgeFailed": "Cache leeren fehlgeschlagen",
"ToastCachePurgeSuccess": "Cache geleert",
"ToastChapterLocked": "Kapitel ist freigegeben.",
"ToastChapterStartTimeAdjusted": "Kapitelbeginn um {0} Sekunden angepasst",
"ToastChaptersAllLocked": "Alle Kapitel sind gesperrt. Gebe einige Kapitel frei um die Zeiten anzupassen.",
"ToastChaptersHaveErrors": "Kapitel sind fehlerhaft",
"ToastChaptersInvalidShiftAmountLast": "Die Verschiebung ist nicht möglich, da die Startzeit des letzten Kapitels über die Gesamtdauer dieses Hörbuchs hinausgehen würde.",
"ToastChaptersInvalidShiftAmountStart": "Ungültige Höhe der Verschiebung. Das erste Kapitel hätte eine Länge von Null oder eine negative Länge und würde vom zweiten Kapitel überschrieben werden. Erhöhen Sie die Startdauer des zweiten Kapitels.",
"ToastChaptersInvalidShiftAmountStart": "Ungültige Höhe der Verschiebung. Das erste Kapitel hätte eine Länge von Null oder eine negative Länge und würde vom zweiten Kapitel überschrieben werden. Erhöhe die Startdauer des zweiten Kapitels.",
"ToastChaptersMustHaveTitles": "Kapitel benötigen eindeutige Namen",
"ToastChaptersRemoved": "Kapitel entfernt",
"ToastChaptersUpdated": "Kapitel aktualisiert",
"ToastCollectionItemsAddFailed": "Das Hinzufügen von Element(en) zur Sammlung ist fehlgeschlagen",
"ToastCollectionRemoveSuccess": "Sammlung entfernt",
"ToastCollectionUpdateSuccess": "Sammlung aktualisiert",
"ToastConnectionNotAvailable": "Verbindung nicht möglich. Bitte später erneut versuchen",
"ToastCoverSearchFailed": "Cover-Suche fehlgeschlagen",
"ToastCoverUpdateFailed": "Cover-Update fehlgeschlagen",
"ToastDateTimeInvalidOrIncomplete": "Datum und Zeit sind ungültig oder unvollständig",
"ToastDeleteFileFailed": "Die Datei konnte nicht gelöscht werden",
@@ -1000,13 +1043,16 @@
"ToastEpisodeDownloadQueueClearSuccess": "Warteschlange für Episoden-Downloads gelöscht",
"ToastEpisodeUpdateSuccess": "{0} Episoden aktualisiert",
"ToastErrorCannotShare": "Das kann nicht nativ auf diesem Gerät freigegeben werden",
"ToastFailedToLoadData": "Daten laden fehlgeschlagen",
"ToastFailedToCreate": "Fehler beim Erzeugen",
"ToastFailedToDelete": "Fehler beim Löschen",
"ToastFailedToLoadData": "Fehler beim laden der Daten",
"ToastFailedToMatch": "Fehler beim Abgleich",
"ToastFailedToShare": "Fehler beim Teilen",
"ToastFailedToUpdate": "Aktualisierung ist fehlgeschlagen",
"ToastInvalidImageUrl": "Ungültiger Bild URL",
"ToastInvalidMaxEpisodesToDownload": "Ungültige Max. Anzahl an Episoden zum Herunterladen",
"ToastInvalidUrl": "Ungültiger URL",
"ToastInvalidUrls": "Eine oder mehrere URLs sind ungültig",
"ToastItemCoverUpdateSuccess": "Titelbild aktualisiert",
"ToastItemDeletedFailed": "Fehler beim löschen des Artikels",
"ToastItemDeletedSuccess": "Artikel gelöscht",
@@ -1031,6 +1077,7 @@
"ToastMustHaveAtLeastOnePath": "Es muss mindestens ein Pfad angegeben werden",
"ToastNameEmailRequired": "Name und E-Mail sind erforderlich",
"ToastNameRequired": "Name ist erforderlich",
"ToastNewApiKeyUserError": "Bitte wähle einen Benutzer aus (Pflichtfeld)",
"ToastNewEpisodesFound": "{0} neue Episoden gefunden",
"ToastNewUserCreatedFailed": "Fehler beim erstellen des Accounts: \"{ 0}\"",
"ToastNewUserCreatedSuccess": "Neuer Account erstellt",
@@ -1055,11 +1102,12 @@
"ToastPlaylistUpdateSuccess": "Wiedergabeliste aktualisiert",
"ToastPodcastCreateFailed": "Podcast konnte nicht erstellt werden",
"ToastPodcastCreateSuccess": "Podcast erstellt",
"ToastPodcastGetFeedFailed": "Fehler beim abrufen des Podcast Feeds",
"ToastPodcastEpisodeUpdated": "Podcast-Folge aktualisiert",
"ToastPodcastGetFeedFailed": "Fehler beim Abrufen des Podcast Feeds",
"ToastPodcastNoEpisodesInFeed": "Keine Episoden in RSS Feed gefunden",
"ToastPodcastNoRssFeed": "Podcast enthält keinen RSS Feed",
"ToastProgressIsNotBeingSynced": "Fortschritt wird nicht synchronisiert, Wiedergabe wird neu gestartet",
"ToastProviderCreatedFailed": "Fehler beim hinzufügen des Anbieters",
"ToastProviderCreatedFailed": "Fehler beim Hinzufügen des Anbieters",
"ToastProviderCreatedSuccess": "Neuer Anbieter hinzugefügt",
"ToastProviderNameAndUrlRequired": "Name und URL notwendig",
"ToastProviderRemoveSuccess": "Anbieter entfernt",
@@ -1105,5 +1153,13 @@
"ToastUserPasswordChangeSuccess": "Passwort erfolgreich verändert",
"ToastUserPasswordMismatch": "Passwörter stimmen nicht überein",
"ToastUserPasswordMustChange": "Neues Passwort muss sich von altem Passwort unterscheiden",
"ToastUserRootRequireName": "Root Benutzername muss angegeben werden"
"ToastUserRootRequireName": "Root Benutzername muss angegeben werden",
"TooltipAddChapters": "Kapitel hinzufügen",
"TooltipAddOneSecond": "1 Sekunde hinzufügen",
"TooltipAdjustChapterStart": "Klicke um die Startzeit anzupassen",
"TooltipLockAllChapters": "Alle Kapitel sperren",
"TooltipLockChapter": "Kapitel sperren (Shift+Klick für mehrere)",
"TooltipSubtractOneSecond": "1 Sekunde abziehen",
"TooltipUnlockAllChapters": "Alle Kapitel freigeben",
"TooltipUnlockChapter": "Kapitel freigeben (Shift+Klick für mehrere)"
}
+342
View File
@@ -0,0 +1,342 @@
{
"ButtonAdd": "Προσθήκη",
"ButtonAddApiKey": "Προσθήκη Κλειδιού API",
"ButtonAddChapters": "Προσθήκη Κεφαλαίων",
"ButtonAddDevice": "Προσθήκη Συσκευής",
"ButtonAddLibrary": "Προσθήκη Βιβλιοθήκης",
"ButtonAddPodcasts": "Προσθήκη Podcasts",
"ButtonAddUser": "Προσθήκη Χρήστη",
"ButtonAddYourFirstLibrary": "Πρόσθεσε την πρώτη σου βιβλιοθήκη",
"ButtonApply": "Εφαρμογή",
"ButtonApplyChapters": "Εφαρμογή Κεφαλαίων",
"ButtonAuthors": "Συγγραφείς",
"ButtonBack": "Πίσω",
"ButtonBatchEditPopulateFromExisting": "Συμπλήρωση από υπάρχοντα",
"ButtonBatchEditPopulateMapDetails": "Συμπλήρωση λεπτομερειών χάρτη",
"ButtonBrowseForFolder": "Περιήγηση για Φάκελο",
"ButtonCancel": "Ακύρωση",
"ButtonCancelEncode": "Ακύρωση Κωδικοποίησης",
"ButtonChangeRootPassword": "Αλλαγή Κωδικού Πρόσβασης Root",
"ButtonCheckAndDownloadNewEpisodes": "Έλεγχος και Κατέβασμα Νέων Επεισοδίων",
"ButtonChooseAFolder": "Επιλογή φακέλου",
"ButtonChooseFiles": "Επιλογή αρχείων",
"ButtonClearFilter": "Διαγραφή Φίλτρου",
"ButtonClose": "Κλείσιμο",
"ButtonCloseFeed": "Κλείσιμο Τροφοδοσίας",
"ButtonCloseSession": "Κλείσιμο Ανοιχτής Συνεδρίας",
"ButtonCollections": "Συλλογές",
"ButtonConfigureScanner": "Ρύθμιση Παραμέτρων Σαρωτή",
"ButtonCreate": "Δημιουργία",
"ButtonCreateBackup": "Δημιουργία Αντιγράφου Ασφαλείας",
"ButtonDelete": "Διαγραφή",
"ButtonDownloadQueue": "Ουρά",
"ButtonEdit": "Επεξεργασία",
"ButtonEditChapters": "Επεξεργασία Κεφαλαίων",
"ButtonEditPodcast": "Επεξεργασία Podcast",
"ButtonEnable": "Ενεργοποίηση",
"ButtonForceReScan": "Αναγκαστική Επανάληψη Σάρωσης",
"ButtonFullPath": "Πλήρης Διαδρομή",
"ButtonHide": "Απόκρυψη",
"ButtonHome": "Αρχική",
"ButtonIssues": "Θέματα",
"ButtonJumpBackward": "Μεταπήδηση Πίσω",
"ButtonJumpForward": "Μεταπήδηση Μπροστά",
"ButtonLatest": "Τελευταία",
"ButtonLibrary": "Βιβλιοθήκη",
"ButtonLogout": "Αποσύνδεση",
"ButtonLookup": "Εύρεση",
"ButtonManageTracks": "Διαχείριση Κομματιών",
"ButtonMapChapterTitles": "Χαρτογράφηση Τίτλων Κεφαλαίων",
"ButtonMatchAllAuthors": "Αντιστοίχιση Όλων των Συγγραφέων",
"ButtonMatchBooks": "Αντιστοίχιση Βιβλίων",
"ButtonNevermind": "Άστο",
"ButtonNext": "Επόμενο",
"ButtonNextChapter": "Επόμενο Κεφάλαιο",
"ButtonNextItemInQueue": "Επόμενο Αντικείμενο στην Ουρά",
"ButtonOk": "Εντάξει",
"ButtonOpenFeed": "Άνοιγμα Τροφοδοσίας",
"ButtonOpenManager": "Άνοιγμα Διαχειριστή",
"ButtonPause": "Παύση",
"ButtonPlay": "Αναπαραγωγή",
"ButtonPlayAll": "Αναπαραγωγή Όλων",
"ButtonPlaying": "Αναπαράγεται",
"ButtonPlaylists": "Λίστες Αναπαραγωγής",
"ButtonPrevious": "Προηγούμενο",
"ButtonPreviousChapter": "Προηγούμενο Κεφάλαιο",
"ButtonProbeAudioFile": "Ανάλυση Αρχείου Ήχου",
"ButtonPurgeAllCache": "Εκκαθάριση Όλης της Προσωρινής Μνήμης",
"ButtonPurgeItemsCache": "Εκκαθάριση της Μνήμης Αντικειμένων",
"ButtonQueueAddItem": "Προσθήκη στην ουρά",
"ButtonQueueRemoveItem": "Αφαίρεση απ'την ουρά",
"ButtonQuickEmbed": "Γρήγορη Ενσωμάτωση",
"ButtonQuickEmbedMetadata": "Γρήγορη Ενσωμάτωση Μεταδεδομένων",
"ButtonQuickMatch": "Γρήγορη Αντιστοίχηση",
"ButtonReScan": "Επανασάρωση",
"ButtonRead": "Ανάγνωση",
"ButtonReadLess": "Ανάγνωση λιγότερων",
"ButtonReadMore": "Διάβασε περισσότερα",
"ButtonRefresh": "Ανανέωση",
"ButtonRemove": "Αφαίρεση",
"ButtonRemoveAll": "Αφαίρεση Όλων",
"ButtonRemoveAllLibraryItems": "Αφαίρεση Όλων των Αντικειμέων Βιβλιοθήκης",
"ButtonRemoveFromContinueListening": "Αφαίρεση από τη Συνέχεια Ακρόασης",
"ButtonRemoveFromContinueReading": "Αφαίρεση από τη Συνέχεια Ανάγνωσης",
"ButtonRemoveSeriesFromContinueSeries": "Αφαίρεση Σειράς από τη Συνέχεια Σειράς",
"ButtonReset": "Επαναφορά",
"ButtonResetToDefault": "Επαναφορά στις προεπιλογές",
"ButtonRestore": "Επαναφορά",
"ButtonSave": "Αποθήκευση",
"ButtonSaveAndClose": "Αποθήκευση και Κλείσιμο",
"ButtonSaveTracklist": "Αποθήκευση Λίστας Κομματιών",
"ButtonScan": "Σάρψση",
"ButtonScanLibrary": "Σάρωση Βιβλιοθήκης",
"ButtonScrollLeft": "Κύλιση Αριστερά",
"ButtonScrollRight": "Κύλιση Δεξιά",
"ButtonSearch": "Αναζήτηση",
"ButtonSelectFolderPath": "Επιλογή Διαδρομής Φακέλου",
"ButtonSeries": "Σειρά",
"ButtonSetChaptersFromTracks": "Ορισμός κεφαλαίων από τα κομμάτια",
"ButtonShare": "Κοινοποίηση",
"ButtonShiftTimes": "Χρόνοι Μετακίνησης",
"ButtonShow": "Εμφάνιση",
"ButtonStartM4BEncode": "Έναρξη Κωδικοποίησης M4B",
"ButtonStartMetadataEmbed": "Έναρξη Ενσωμάτωσης Μεταδεδομένων",
"ButtonStats": "Στατιστικά",
"ButtonSubmit": "Υποβολή",
"ButtonTest": "Δοκιμή",
"ButtonUnlinkOpenId": "Αποσύνδεση OpenID",
"ButtonUpload": "Μεταφόρτωση",
"ButtonUploadBackup": "Μεταφόρτωση Αντιγράφου Ασφαλείας",
"ButtonUploadCover": "Μεταφόρτωση Εξωφύλλου",
"ButtonUploadOPMLFile": "Μεταφόρτωση Αρχείου OPML",
"ButtonUserDelete": "Διαγραφή Χρήστη {0}",
"ButtonUserEdit": "Επεξεργασίας χρήστη {0}",
"ButtonViewAll": "Εμφάνιση Όλων",
"ButtonYes": "Ναι",
"ErrorUploadFetchMetadataAPI": "Σφάλμα κατά την ανάκτηση μεταδεδομένων",
"ErrorUploadFetchMetadataNoResults": "Δεν ήταν δυνατή η ανάκτηση των μεταδεδομένων - δοκιμάστε να ενημερώσετε τον τίτλο και/ή τον συγγραφέα",
"ErrorUploadLacksTitle": "Πρέπει να έχει τίτλο",
"HeaderAccount": "Λογαριασμός",
"HeaderAddCustomMetadataProvider": "Προσθήκη Προσαρμοσμένου Παρόχου Μεταδεδομένων",
"HeaderAdvanced": "Για Προχωρημένους",
"HeaderApiKeys": "Κλειδιά API",
"HeaderAppriseNotificationSettings": "Ρυθμίσεις Ειδοποιήσεων Apprise",
"HeaderAudioTracks": "Κομμάτια Ήχου",
"HeaderAudiobookTools": "Εργαλεία Διαχείρισης Αρχείων Audiobooks",
"HeaderAuthentication": "Αυθεντικοποίηση",
"HeaderBackups": "Αντίγραφα Ασφαλείας",
"HeaderBulkChapterModal": "Προσθήκη Πολλαπλών Κεφαλαίων",
"HeaderChangePassword": "Αλλαγή Κωδικού Πρόσβασης",
"HeaderChapters": "Κεφάλαια",
"HeaderChooseAFolder": "Επιλογή Φακέλου",
"HeaderCollection": "Συλλογή",
"HeaderCollectionItems": "Αντικείμενα Συλλογής",
"HeaderCover": "Εξώφυλλο",
"HeaderCurrentDownloads": "Τρέχουσες Λήψεις",
"HeaderDetails": "Λεπτομέρειες",
"HeaderDownloadQueue": "Ουρά Λήψης",
"HeaderEbookFiles": "Αρχεία Ebook",
"HeaderEmail": "Ηλεκτρονικό Ταχυδρομίο",
"HeaderEmailSettings": "Ρυθμίσεις Ηλεκτρονικού Ταχυδρομίου",
"HeaderEpisodes": "Επεισόδια",
"HeaderEreaderSettings": "Ρυθμίσεις Ereader",
"HeaderFiles": "Αρχεία",
"HeaderFindChapters": "Εύρεση Κεφαλαίων",
"HeaderItemFiles": "Αρχεία Αντικειμένων",
"HeaderLastListeningSession": "Τελευταία Συνεδρία Ακρόασης",
"HeaderLatestEpisodes": "Τελευταία Επεισόδια",
"HeaderLibraries": "Βιβλιοθήκες",
"HeaderLibraryFiles": "Αρχεία Βιβλιοθήκης",
"HeaderLibraryStats": "Στατιστικά Βιβλιοθήκης",
"HeaderListeningSessions": "Συνεδρίες Ακρόασης",
"HeaderListeningStats": "Στατιστικά Ακρόασης",
"HeaderMatch": "Ταύτιση",
"HeaderNewAccount": "Νέος Λογαριασμός",
"HeaderNewApiKey": "Νέο Κλειδί API",
"HeaderNewLibrary": "Νέα Βιβλιοθήκη",
"HeaderNotificationCreate": "Δημιουργία Ειδοποίησης",
"HeaderNotificationUpdate": "Ενημέρωση Ειδοποίησης",
"HeaderNotifications": "Ειδοποιήσεις",
"HeaderOpenRSSFeed": "Άνοιγμα Τροφοδοσίας RSS",
"HeaderOtherFiles": "Άλλα Αρχεία",
"HeaderPermissions": "Δικαιώματα",
"HeaderPlayerSettings": "Ρυθμίσεις Αναπαραγωγής",
"HeaderPlaylist": "Λίστα Αναπαραγωγής",
"HeaderPlaylistItems": "Αντικείμενα Λίστας Αναπαραγωγής",
"HeaderPresets": "Προεπιλογές",
"HeaderRSSFeedGeneral": "Λεπτομέρειες RSS",
"HeaderRSSFeedIsOpen": "Η Τροφοδοσία RSS είναι Ανοιχτή",
"HeaderRemoveEpisode": "Αφαίρεση Επεισοδίου",
"HeaderSession": "Συνεδρία",
"HeaderSetBackupSchedule": "Ορισμός Προγράμματος Αντιγράφων Ασφαλείας",
"HeaderSettings": "Ρυθμίσεις",
"HeaderSettingsDisplay": "Προβολή",
"HeaderSettingsGeneral": "Γενικά",
"HeaderSettingsSecurity": "Ασφάλεια",
"HeaderSleepTimer": "Χρονοδιακόπτης Ύπνου",
"HeaderStatsLargestItems": "Μεγαλύτερα Αντικείμενα",
"HeaderStatsLongestItems": "Μεγαλύτερα Αντικείμενα (ώρες)",
"HeaderStatsMinutesListeningChart": "Λεπτά Ακρόασης (τελευταίες 7 ημέρες)",
"HeaderStatsRecentSessions": "Πρόσφατες Συνεδρίες",
"HeaderStatsTop10Authors": "10 Κορυφαίου Συγγραφείς",
"HeaderStatsTop5Genres": "5 Κορυφαία Είδη",
"HeaderTableOfContents": "Πίνακας Περιεχομένων",
"HeaderTools": "Εργαλεία",
"HeaderUpdateAccount": "Ενημέρωση Λογαριασμού",
"HeaderUpdateApiKey": "Ενημέρωση Κλειδιού API",
"HeaderUpdateAuthor": "Ενημέρωση Συγγραφέα",
"HeaderUpdateDetails": "Ενημέρωση Λεπτομερειεών",
"HeaderUpdateLibrary": "Ενημέρωση Βιβλιοθήκης",
"HeaderUsers": "Χρήστες",
"HeaderYourStats": "Τα Στατιστικά Σας",
"LabelAbridged": "Συνοπτικό",
"LabelAccessibleBy": "Προσβάσιμο από",
"LabelAccountType": "Τύπος Λογαριασμού",
"LabelAccountTypeAdmin": "Διαχειριστής",
"LabelAccountTypeGuest": "Επισκέπτης",
"LabelAccountTypeUser": "Χρήστης",
"LabelAddToCollection": "Προσθήκη σε Συλλογή",
"LabelAddToCollectionBatch": "Προσθήκη {0} Βιβλίων στην Συλλογή",
"LabelAddToPlaylist": "Προσθήκη στην Λίστα Αναπαραγωγής",
"LabelAddedAt": "Προστέθηκε Στις",
"LabelAddedDate": "Προστέθηκε {0}",
"LabelAll": "Όλα",
"LabelAllEpisodesDownloaded": "Όλα τα επεισόδια λήφθηκαν",
"LabelAllUsers": "Όλοι οι Χρήστες",
"LabelAlreadyInYourLibrary": "Υπάρχει ήδη στην βιβλιοθήκη",
"LabelAudioChannels": "Κανάλια Ήχου (1 ή 2)",
"LabelAuthor": "Συγγραφέας",
"LabelAuthorFirstLast": "Συγγραφέας (Όνομα Επώνυμο)",
"LabelAuthorLastFirst": "Συγγραφέας (Επώνυμο, Όνομα)",
"LabelAuthors": "Συγγραφείς",
"LabelAutoDownloadEpisodes": "Αυτόματο Κατέβασμα Επεισοδίων",
"LabelAutoLaunch": "Αυτόματη Εκκίνηση",
"LabelBackupLocation": "Τοποθεσία Αντιγράφου Ασφαλείας",
"LabelBackupsEnableAutomaticBackups": "Αυτόματα αντίγραφα ασφαλείας",
"LabelBackupsNumberToKeep": "Αριθμός αντιγράφων ασφαλείας προς διατήρηση",
"LabelBooks": "Βιβλία",
"LabelButtonText": "Κείμενο Κουμπιού",
"LabelByAuthor": "κατά {0}",
"LabelChangePassword": "Αλλαγή Κωδικού Πρόσβασης",
"LabelChannels": "Κανάλια",
"LabelChapterCount": "{0} Κεφάλαια",
"LabelChapterTitle": "Τίτλος Κεφαλαίου",
"LabelChapters": "Κεφάλαια",
"LabelChaptersFound": "κεφάλαια βρέθηκαν",
"LabelClosePlayer": "Κλείσιμο αναπαραγωγής",
"LabelCollapseSeries": "Σύμπτυξη Σειράς",
"LabelCollection": "Συλλογή",
"LabelCollections": "Συλλογές",
"LabelComplete": "Ολοκλήρωση",
"LabelConfirmPassword": "Επιβεβαίωση Κωδικού Πρόσβασης",
"LabelContinueListening": "Συνέχεια Ακρόασης",
"LabelContinueReading": "Συνέχεια Ανάγνωσης",
"LabelContinueSeries": "Συνέχεια Σειράς",
"LabelCover": "Εξώφυλλο",
"LabelCoverImageURL": "URL Εικόνας Εξωφύλλου",
"LabelCoverProvider": "Πάροχος Εξωφύλλου",
"LabelCreatedAt": "Δημιουρήθηκε Στις",
"LabelCurrent": "Τρέχων",
"LabelCurrently": "Τρέχων:",
"LabelDays": "Ημέρες",
"LabelDescription": "Περιγραφή",
"LabelDevice": "Συσκευή",
"LabelDeviceInfo": "Πληροφορίες Συσκευής",
"LabelDiscover": "Ανακάλυψη",
"LabelDownload": "Λήψη",
"LabelDownloadNEpisodes": "Λήψη {0} επεισοδίων",
"LabelDuration": "Διάρκεια",
"LabelDurationComparisonExactMatch": "(ακριβής ταύτιση)",
"LabelEbook": "Ηλεκτρονικό Βιβλίο/ Ebook",
"LabelEbooks": "Ηλεκτρονικά Βιβλία / Ebooks",
"LabelEdit": "Επεξεργασία",
"LabelEmail": "Ηλεκτρονικό Ταχυδρομίο",
"LabelEmailSettingsFromAddress": "Από Διεύθυνση",
"LabelEmailSettingsSecure": "Ασφαλές",
"LabelEmailSettingsTestAddress": "Δοκιμή Διεύθυνσης",
"LabelEmbeddedCover": "Ενσωματωμένο Εξώφυλλο",
"LabelEnable": "Ενεργοποίηση",
"LabelEnd": "Τέλος",
"LabelEndOfChapter": "Τέλος Κεφαλαίου",
"LabelEpisode": "Επεισόδιο",
"LabelExplicit": "Άσεμνο περιεχόμενο",
"LabelFeedURL": "URL ροής",
"LabelFile": "Αρχείο",
"LabelFileBirthtime": "Ώρα δημιουργίας αρχείου",
"LabelFileModified": "Τροποποιημένο αρχείο",
"LabelFilename": "Όνομα Αρχείου",
"LabelFinished": "Ολοκληρώθηκε",
"LabelFolder": "Φάκελος",
"LabelFontBoldness": "Ένταση γραμματοσειράς",
"LabelFontFamily": "Οικογένεια Γραμματοσειράς",
"LabelFontScale": "Μέγεθος γραμματοσειράς",
"LabelGenre": "Λογοτεχνικό Είδος",
"LabelGenres": "Λογοτεχνικά Είδη",
"LabelHasEbook": "Έχει ebook",
"LabelHasSupplementaryEbook": "Διαθέτει συμπληρωματικό ηλεκτρονικό βιβλίο",
"LabelHost": "Διακομιστής",
"LabelInProgress": "Σε Εξέλιξη",
"LabelIncomplete": "Ελειπής",
"LabelLanguage": "Γλώσσα",
"LabelLayout": "Διάταξη",
"LabelLayoutSinglePage": "Μονή Σελίδα",
"LabelLibrarySortByProgress": "Πρόοδος: Τελευταία ενημέρωση",
"LabelLibrarySortByProgressFinished": "Πρόοδος: Ολοκληρώθηκε",
"LabelLibrarySortByProgressStarted": "Πρόοδος: Έναρξη",
"LabelLineSpacing": "Διάστημα γραμμών",
"LabelListenAgain": "Επανάληψη Ακρόασης",
"LabelMediaType": "Τύπος Πολυμέσων",
"LabelMissing": "Λείπει η ετικέτα",
"LabelMore": "Περισσότερα",
"LabelMoreInfo": "Περισσότερες Πληροφορίες",
"LabelName": "Όνομα",
"LabelNarrator": "Αφηγητής",
"LabelNarrators": "Αφηγητές",
"LabelNewestAuthors": "Πρόσφατοι Συγγραφείς",
"LabelNewestEpisodes": "Πρόσφατα Επεισόδια",
"LabelNotFinished": "Δεν έχει ολοκληρωθεί",
"LabelNotStarted": "Δεν Έχει Ξεκινήσει",
"LabelNumberOfEpisodes": "# Επεισοδίων",
"LabelPassword": "Κωδικός Πρόσβασης",
"LabelPath": "Διαδρομή",
"LabelProgress": "Πρόοδος",
"LabelPubDate": "Ημερομηνία έκδοσης",
"LabelPublishYear": "Χρονολογία Έκδοσης",
"LabelPublishedDate": "Εκδόθηκε {0}",
"LabelRandomly": "Τυχαία",
"LabelRead": "Ανάγνωση",
"LabelReadAgain": "Ανάγνωση Ξανά",
"LabelRecentSeries": "Πρόσφατη Σειρά",
"LabelRecentlyAdded": "Προστέθηκαν Πρόσφατα",
"LabelSeries": "Σειρά",
"LabelSetEbookAsPrimary": "Ορισμός ως πρωτεύων",
"LabelShowAll": "Εμφάνιση Όλων",
"LabelSize": "Μέγεθος",
"LabelSleepTimer": "Χρονοδιακόπτης Ύπνου",
"LabelStart": "Έναρξη",
"LabelStatsBestDay": "Καλύτερη Ημέρα",
"LabelStatsDailyAverage": "Ημερήσιος Μέσος Όρος",
"LabelStatsDays": "Ημέρες",
"LabelStatsDaysListened": "Ημέρες Ακρόασης",
"LabelStatsInARow": "Σε σειρά",
"LabelStatsItemsFinished": "Ολοκληρωμένα Αντικείμενα",
"LabelStatsMinutes": "λεπτά",
"LabelStatsMinutesListening": "Λεπτά Ακρόασης",
"LabelStatsWeekListening": "Εβδομαδιαία Ακρόαση",
"LabelTheme": "Θέμα",
"LabelThemeDark": "Σκοτεινό",
"LabelThemeLight": "Φωτεινό",
"LabelTimeRemaining": "{0} απομένουν",
"LabelTitle": "Τίτλος",
"LabelTracks": "Κομμάτια",
"LabelType": "Τύπος",
"LabelUnknown": "Άγνωστο",
"LabelUser": "Χρήστης",
"LabelUsername": "Όνομα Χρήστη",
"LabelYourProgress": "Η Πρόοδος Σας",
"MessageDownloadingEpisode": "Λήψη επεισοδίου",
"MessageLoading": "Φόρτωση...",
"MessageMarkAsFinished": "Σήμανση ως Ολοκληρωμένο",
"MessageNoItemsFound": "Δεν βρέθηκαν αντικείμενα",
"MessageNoUserPlaylists": "Δεν έχετε λίστες αναπαραγωγής"
}

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