Compare commits

...

361 Commits

Author SHA1 Message Date
advplyr
166454ef43 Version bump v2.8.1 2024-03-16 17:15:33 -05:00
advplyr
d5c854d606 Update:Add robots.txt and noindex meta tag 2024-03-16 16:35:05 -05:00
advplyr
eace46bf55 Merge pull request #2688 from mfcar/mf/loginPage
Update Login Page with Logo and Input Form Styling
2024-03-16 15:57:45 -05:00
advplyr
b9ffce166e Login page add overflow scroll for mobile landscape, update z index for logo 2024-03-16 15:55:13 -05:00
advplyr
9713e94aed Reformat login page with logo in top left 2024-03-16 15:41:35 -05:00
advplyr
d71bc89c9d Merge branch 'master' into mf/loginPage 2024-03-16 15:24:22 -05:00
advplyr
a2b2a2d060 Fix:Applying backup not properly overwriting existing sqlite file
- Fixed resetting api cache on backup
- Added loading indicator in backups table
- Fixed apply backup api not responding with 200 http status code
- Added additional logging and failsafes
2024-03-16 15:12:33 -05:00
advplyr
88f9533b37 Fix:HLS.js retry fragments #2748 #2720 2024-03-15 17:10:43 -05:00
advplyr
630ece82ad Fix:Chapter modal scroll to current chapter 2024-03-15 14:35:09 -05:00
advplyr
5777184cae Merge pull request #2745 from mikiher/unit-tests-flow
Unit tests flow
2024-03-15 14:20:57 -05:00
mfcar
a76da14fb0 Merge branch 'refs/heads/master' into mf/loginPage 2024-03-15 08:14:45 +00:00
mikiher
0c612b4836 Update unit test workflow to include push event 2024-03-15 09:51:40 +02:00
mikiher
a1af672c7c Add unit test workflow 2024-03-15 08:50:51 +02:00
advplyr
5fcd23409a Update:dev.js in devcontainer to include the SkipBinariesCheck flag #2741 2024-03-14 16:32:23 -05:00
advplyr
99f0799a11 Update:Adding support for skipping check for ffmpeg/ffprobe binaries with environment variable SKIP_BINARIES_CHECK
- Set SKIP_BINARIES_CHECK=1 env variable to skip
- Or set SkipBinariesCheck: true in dev.js #2741
2024-03-14 16:29:01 -05:00
advplyr
316aeba1b0 Merge pull request #2740 from Schiriki123/master
Add name labels to login form
2024-03-14 15:40:45 -05:00
advplyr
bfd4a378f3 Merge pull request #2737 from justcallmelarry/feature/add-toggle-for-skipping-earlier-instalments-in-continue-series
Add library toggle setting for skipping earlier instalments in Continue Series
2024-03-14 14:40:20 -05:00
advplyr
521db90ae0 Update JSDocs, remove unused libraryId replacement 2024-03-14 14:37:24 -05:00
advplyr
d02fc2debe Update continue series skip earlier books query attribute to look for finished books, update wording on help text, map translations 2024-03-14 14:27:33 -05:00
advplyr
e6c21c5be1 Merge pull request #2742 from mikiher/broken-binary-manager-test
Fix broken BinaryManager.isBinaryGood test
2024-03-14 13:13:59 -05:00
advplyr
91248b496e Merge pull request #2734 from mikiher/fix-sequence-cleanup
Make series sequence cleanup slighlty less aggressive
2024-03-14 13:12:04 -05:00
mikiher
f7ae7783bd Fix broken BinaryManager.isBinaryGood test 2024-03-14 19:58:42 +02:00
mikiher
ae395497a5 Add tests for cleanSeriesSequence 2024-03-14 19:37:51 +02:00
mikiher
8826d3af62 fix cleanSeriesSequence method to extract first numeric value 2024-03-14 19:36:51 +02:00
Lauri Vuorela
65153fae9d var => let 2024-03-14 09:42:50 +01:00
Lauri Vuorela
d4c1bc5dfc use already fetched library settings, only fetch maxSequence if setting is turned on 2024-03-14 09:41:48 +01:00
Schiriki
d6f13513ae Add name labels to login form 2024-03-13 23:46:56 +01:00
advplyr
2584c3b432 Merge pull request #2733 from kaldigo/master
Added isbn to CustomProviderAdapter
2024-03-13 17:21:51 -05:00
advplyr
b54421412d Merge pull request #2738 from Sapd/auth-fix
Auth: Fix crash on missing logout URL
2024-03-13 17:18:35 -05:00
advplyr
e2451a3281 Merge pull request #2732 from den13501/i18n-add-zhTW
Add traditional Chinese(zh-TW) to i18n
2024-03-12 17:47:04 -05:00
advplyr
dbf4bd5c3d Merge pull request #2691 from lkiesow/hash-in-filename
Fix file names with URL control characters
2024-03-12 17:40:37 -05:00
Denis Arnst
2a722ab163 Auth: Fix crash on missing logout URL
When using OpenID
Also added debug information on openid errors
2024-03-12 18:07:13 +01:00
Lauri Vuorela
c83399c7b5 use the toggle to not show earlier works than the ones already read 2024-03-12 17:04:26 +01:00
Lauri Vuorela
a814e45150 add a toggle for the new continue series setting 2024-03-12 17:00:21 +01:00
mikiher
29e9216bb1 Make series sequence cleanup slighlty less aggressive 2024-03-12 13:17:52 +02:00
Kaldigo
94d1732b0d Added isbn to CustomProviderAdapter 2024-03-12 08:18:52 +00:00
-Shiken-
7610084627 Update zh-tw.json
fix
2024-03-12 15:39:01 +08:00
-Shiken-
d840905a97 Create zh-tw.json
add traditional Chinese translation text.
2024-03-12 15:36:42 +08:00
-Shiken-
7b1b448795 Add traditional Chinese translation
for traditional user.
2024-03-12 11:29:24 +08:00
advplyr
77559d29bb Merge pull request #2724 from mikiher/fix-library-filter-data-access
Fix library filter data direct access
2024-03-11 17:08:41 -05:00
advplyr
c14f9accaf Update functions for #2724 and add jsdocs 2024-03-11 17:07:03 -05:00
advplyr
76a1f48c62 Remove UID/GID from Server constructor 2024-03-11 11:11:13 -05:00
mikiher
ae0a9bcf86 Merge branch 'advplyr:master' into fix-library-filter-data-access 2024-03-11 08:33:47 +02:00
advplyr
9e44fe5524 Merge pull request #2721 from mikiher/keyboard-navigation-2
Add keyboard navigation to multi-select components
2024-03-10 09:45:16 -05:00
advplyr
727dad7e19 Update multi select highlight color to yellow, remove console logs 2024-03-10 09:43:24 -05:00
advplyr
0c2de91097 Merge pull request #2726 from nichwall/vietnamese_translations
Vietnamese translations
2024-03-09 20:09:09 -06:00
advplyr
450fa45360 Update Vietnamese datefns locale code 2024-03-09 20:09:08 -06:00
Nicholas Wallace
e0dddae2c2 Added missing keys 2024-03-09 23:13:20 +00:00
Nicholas Wallace
daa9fccc14 Add: Vietnamese translations 2024-03-09 23:00:01 +00:00
mikiher
ad45dadc15 Remove redundant space 2024-03-09 12:07:08 +02:00
mikiher
0e8148001e Fix direct access to Database.libraryFilterData 2024-03-09 11:59:50 +02:00
advplyr
fa71f9db2e Merge master 2024-03-08 12:22:29 -06:00
advplyr
0d9d2fa4be Merge pull request #2714 from mikiher/keyboard-navigation
Fix input width in MultiSelect UI components  - replacement solution
2024-03-08 12:20:46 -06:00
advplyr
c34e9cde05 Merge branch 'master' into keyboard-navigation 2024-03-08 12:15:07 -06:00
advplyr
b934a755b5 Merge branch 'master' into keyboard-navigation-2 2024-03-08 12:04:13 -06:00
mikiher
a5772f6b66 Add keyboard navigation to multi-select components 2024-03-08 08:51:05 +02:00
advplyr
153f149d58 Merge pull request #2580 from KeyboardHammer/authorSort
Add sorting to author page
2024-03-07 12:28:55 -06:00
advplyr
e50b06183e Merge branch 'master' into authorSort 2024-03-07 12:26:07 -06:00
advplyr
305689d513 Update authors sort 2024-03-07 12:26:04 -06:00
advplyr
4dd140585d Add:Abridged checkbox to batch edit overwrite map details #2695 2024-03-06 15:29:10 -06:00
mikiher
cd60d0219f Bring back setInputWidth 2024-03-06 14:02:15 +02:00
mikiher
8ec18e8d7b Merge branch 'keyboard-navigation' of https://github.com/mikiher/audiobookshelf into keyboard-navigation 2024-03-06 13:53:49 +02:00
mikiher
15545654ea Alternative input width fix in MultiSelect components 2024-03-06 13:41:54 +02:00
advplyr
8a0fab2b20 Fix:Resizing page update chapter ticks and track bar #2707 2024-03-05 14:30:39 -06:00
advplyr
6e8c6aa740 Merge pull request #2701 from mikiher/keyboard-navigation
Fix input width in MultiSelect UI components
2024-03-05 13:13:52 -06:00
mikiher
5005aabe5e Fix input width in MultiSelect components 2024-03-03 23:40:47 +02:00
advplyr
abc2d28617 Merge pull request #2699 from pmangro/master
[PT-BR] enhance-ebook-filter strings translation
2024-03-03 11:42:56 -06:00
pmangro
7569a14510 Merge pull request #1 from pmangro/enhance-ebook-filter-strings
[PT-BR] enhance-ebook-filter strings translation
2024-03-03 13:33:01 -03:00
pmangro
b52341dbcf [PT-BR] enhance-ebook-filter strings translation 2024-03-03 13:32:01 -03:00
advplyr
b4eed3bad2 Merge pull request #2694 from mikiher/client-image-caching
Client side cover image caching
2024-03-01 17:48:08 -06:00
mikiher
4fe672f09d Update cover image URLs with timestamp where available 2024-03-01 11:55:53 +02:00
advplyr
49af7eb7b0 Merge pull request #2692 from lkiesow/log-src
Fix log source in log file
2024-02-29 17:01:59 -06:00
advplyr
c93c863d82 Merge pull request #2677 from Teekeks/enhance-ebook-filter
feat: Expanded filter to include "has no ebook" and "has no supplementary ebooks" options
2024-02-29 14:00:50 -06:00
advplyr
763bb1b829 Map ebook filter translations 2024-02-29 13:59:00 -06:00
Lars Kiesow
79d32274aa Fix log source in log file
The logger should include a source containing the location where the
logger was called. This works well for logging to `stdout`. Unfortunately,
the file logs contain the locations where the file logging is called
inside of the logger. This is not helpful:

```
{"timestamp":"2023-11-19 16:35:43","source":"Logger.js:114","message":"[oldDbFiles] Processed db data file with 1 entities","levelName":"INFO","level":2}
{"timestamp":"2023-11-19 16:35:43","source":"Logger.js:114","message":"[oldDbFiles] Finished loading db data with 2 entities","levelName":"INFO","level":2}
{"timestamp":"2023-11-19 16:35:43","source":"Logger.js:114","message":"[oldDbFiles] 2 settings loaded","levelName":"INFO","level":2}
```

This patch fixes the issue, ensureing that the actual source location
will be logged:

```
{"timestamp":"2024-02-29 18:12:59.832","source":"DailyLog.js:132","message":"[DailyLog] 2024-02-29: Loaded 20 Logs","levelName":"DEBUG","level":1}
{"timestamp":"2024-02-29 18:12:59.638","source":"Server.js:172","message":"=== Starting Server ===","levelName":"INFO","level":2}
{"timestamp":"2024-02-29 18:12:59.638","source":"Server.js:103","message":"[Server] Init v2.8.0","levelName":"INFO","level":2}
```
2024-02-29 18:16:29 +01:00
Lars Kiesow
987842ed04 Fix file names with URL control characters
This patch ensures that files names like `series #3 xy.jpg` are actually
handled correctly instead of the part after `#` being interpreted as
fragment and being discarded.

I noticed that in a few rare cases the App wouldn't properly display
cover images. It turns out that due the file names containing a `#`, the
file path got corrupted, causing Audiobookshelf to return a 403.
2024-02-29 17:56:55 +01:00
advplyr
d2b006b909 Update:Windows binary manager to install ffmpeg/ffprobe 5.1 #1098 2024-02-28 16:16:44 -06:00
mfcar
f4a19e48ad Update login page 2024-02-28 19:21:11 +00:00
advplyr
38f12f4795 Fix:Podcast schedule max new episodes to download setting to 0 and fix input blurs #2680 2024-02-27 17:17:33 -06:00
advplyr
7a4f4b1586 Merge pull request #2676 from pmangro/2.8.0.a-PT-BT
[PT-BR] Updated strings
2024-02-27 15:59:09 -06:00
pmangro
20ec54e085 [PT-BR] Updated strings 2024-02-27 14:31:21 -03:00
Teekeks
655bebfec4 feat: Expanded filter to include "has no ebook" and "has no supplementary ebooks" options 2024-02-27 18:30:05 +01:00
advplyr
71e1abd263 Merge pull request #2673 from Machou/master
Update fr.json
2024-02-27 08:56:03 -06:00
Machou
72172dcb33 Update fr.json 2024-02-27 08:56:31 +01:00
advplyr
def2988e12 Update:Passport openid-client request timeout set to 10s (default was 3.5s) #2669 2024-02-26 17:20:11 -06:00
mikiher
b47793c365 Add cache control header for timestamped cover image requests 2024-02-26 14:00:25 +02:00
advplyr
3a99cc56b7 Update:Debian packager script to use xz compression instead of zstd 2024-02-25 12:56:04 -06:00
advplyr
24c35dede5 Merge pull request #2659 from mikiher/quick-match-dup-authors
Fix dup author addition logic
2024-02-25 08:12:05 -06:00
advplyr
8c4400dff1 Merge pull request #2657 from JBlond/master
Update de strings
2024-02-25 08:08:17 -06:00
advplyr
af8dffaa33 Merge pull request #2573 from mikiher/fix-match-update
Merge cover and media update in Match.vue into a single /media API call
2024-02-25 08:07:51 -06:00
advplyr
4a36a3c8e6 Merge branch 'master' into fix-match-update 2024-02-25 08:00:29 -06:00
mikiher
e6735e042e Fix dup author addition logic 2024-02-25 09:01:26 +02:00
JBlond
c799379a54 Update de strings 2024-02-24 20:23:51 +01:00
advplyr
d8b9f08e5a Merge pull request #2641 from Teekeks/year-review-translation
feat(i18n): made "Year in Review" UI elements translatable and added german translation for those
2024-02-23 17:01:43 -06:00
advplyr
608b25de45 Map en-us translations 2024-02-23 16:59:46 -06:00
advplyr
2db8869908 Merge branch 'master' into year-review-translation 2024-02-23 16:56:53 -06:00
advplyr
9500737bbe Merge pull request #2644 from RasmusKoit/estonian-translations
Adds estonian translation
2024-02-23 16:55:41 -06:00
advplyr
def2b6425b Update:Username and password inputs on login page trim whitespace #2628 2024-02-22 16:30:41 -06:00
mikiher
0f4b11494e Merge branch 'advplyr:master' into fix-match-update 2024-02-22 12:20:49 +02:00
rasmuskoit
46448ce1e9 Adds estonian translation 2024-02-22 09:46:09 +02:00
advplyr
fbe12b393f Merge pull request #2639 from pmangro/2.8.0.a-PT-BT
[PT-BR] Terminology adjustments and typo fixes
2024-02-21 18:29:29 -06:00
advplyr
ccf59b2c1a Merge pull request #2638 from DownloadableFox/master
Updated and fixed Spanish translation
2024-02-21 18:28:28 -06:00
Lena During
d7af3b7788 Merge branch 'master' into year-review-translation 2024-02-22 00:39:49 +01:00
Teekeks
682aca0b2a feat(i18n): made "Year in Review" UI elements translatable and added german translation for those 2024-02-22 00:36:43 +01:00
advplyr
3328ffe1b9 Merge pull request #2636 from megamegax/master
feat(i18n): add Hungarian translation
2024-02-21 16:07:36 -06:00
DownloadableFox
c07b7840e2 Updated and fixed Spanish translation 2024-02-20 21:14:55 -05:00
pmangro
9f848b2c64 Update pt-br.json - adjust terminology and typo 2024-02-20 17:28:37 -03:00
pmangro
3d66ec0761 PT-BR size adjustment 2024-02-20 11:42:48 -03:00
advplyr
f50920be69 Merge pull request #2635 from lonezel/lonezel-updated-german-translation
Update de.json
2024-02-20 08:37:48 -06:00
advplyr
d31add9d5a Merge pull request #2634 from pmangro/2.8.0-PT-BR
Updated pt-br string
2024-02-20 08:36:33 -06:00
Hunyady Mihály
a4dcb4f92e feat(i18n): add Hungarian translation 2024-02-20 12:25:43 +01:00
lonezel
2c589c1dbd Update de.json
Translated new strings to german - this is my first ever commit on Github, if I need to change something in my workflow let me know!
2024-02-20 10:19:48 +01:00
pmangro
60ea386c6d Updated pt-br string 2024-02-20 05:57:52 -03:00
advplyr
24be1a0ec5 Merge pull request #2629 from burghy86/patch-13
Update it.json
2024-02-19 08:56:39 -06:00
burghy86
e71a14756b Update it.json
new string update
2024-02-19 15:53:12 +01:00
advplyr
85fecbd1b9 Version bump v2.8.0 2024-02-18 16:43:16 -06:00
advplyr
335d39f317 Update guide link for custom metadata providers 2024-02-18 16:22:10 -06:00
advplyr
973a18d346 Update:Added button to user edit modal for unlinking user from openid #2587 2024-02-18 15:38:45 -06:00
advplyr
a43b93d796 Fix:Clear library filter data cache when library item is updated #2597 2024-02-18 14:58:46 -06:00
advplyr
acf75abdf1 Update:Match author use closest name match by levenshtein distance #2624 2024-02-18 13:06:51 -06:00
advplyr
58598bfcf2 Update:Clamp author description to 4 lines and add more button #2614 2024-02-18 11:32:24 -06:00
advplyr
7a570439db Update:Clamp item descriptions to 4 lines and show more button #2614 2024-02-18 11:24:36 -06:00
advplyr
6e769d1c20 Merge pull request #2554 from mikiher/ffmpeg-latest
Modify BinaryManager to download version 6.1
2024-02-17 17:42:24 -06:00
advplyr
d9e7f5d133 Update BinaryManager JSDocs, move validVersions to required binary objects 2024-02-17 17:40:33 -06:00
advplyr
a119b05d85 Merge branch 'master' into ffmpeg-latest 2024-02-17 17:05:51 -06:00
advplyr
7bf7b6bcf9 Merge pull request #2553 from Sapd/sso
OpenID: Implement Logout + Fix state + Fix URL Regex
2024-02-17 17:03:12 -06:00
advplyr
e47ea98cdd Fix:Disconnect from socket on logout, remove unnecessary logout function 2024-02-17 16:58:49 -06:00
advplyr
bf66e13377 Update jsdocs 2024-02-17 16:06:25 -06:00
advplyr
d7aba5629e Remove old login rate limiter 2024-02-17 15:29:06 -06:00
advplyr
a5c200ac79 Merge branch 'master' into sso 2024-02-17 14:15:41 -06:00
advplyr
fdc1fc1b2a Merge pull request #2491 from liaochuan/liaocl
Add Podcast Search Region
2024-02-17 13:32:23 -06:00
advplyr
42a4b762bd Fix translations sort order and pt-br translations 2024-02-17 13:30:30 -06:00
advplyr
180c328ed1 Update jsdocs for search podcasts 2024-02-17 13:24:49 -06:00
advplyr
2ec52a7a45 Merge branch 'master' into liaocl 2024-02-17 12:56:05 -06:00
advplyr
aacf37e32b Fix:Year in Review crashing when listening session has a null genre #2623 2024-02-16 16:16:55 -06:00
advplyr
52323b7eb5 Update:Podcast episode download show ffmpeg progress and print full debug log dump on error 2024-02-16 16:05:02 -06:00
advplyr
5b5613a762 Merge pull request #2617 from ipcintron/pwa
Update pwa icon to use iOS icon
2024-02-16 13:44:57 -06:00
ipcintron
de6df0c029 Forgot to modify nuxt.config.js 2024-02-16 12:29:52 -06:00
ipcintron
e180b3c171 Renamed the icon to make it clear it is being used for iOS 2024-02-16 12:27:10 -06:00
ipcintron
1364b79cbf Put the icon in the link array for iOS only 2024-02-16 10:10:09 -06:00
ipcintron
ef96f3102f Merge branch 'advplyr:master' into pwa 2024-02-16 09:51:10 -06:00
advplyr
06ce3b08f7 Merge pull request #2619 from ipcintron/theme
PWA (iOS) theme color fix
2024-02-16 09:31:11 -06:00
advplyr
a13217dddf Fix:Initial language code setting eventBus not yet defined 2024-02-16 09:12:47 -06:00
advplyr
ce528d4012 Merge pull request #2620 from pmangro/pmangro-patch-1
PT-BR Strings
2024-02-16 09:05:31 -06:00
pmangro
89207b6d2a Update i18n.js
Added PT-BR
2024-02-16 11:57:54 -03:00
pmangro
e9591caf81 Spelling 2024-02-16 11:56:31 -03:00
pmangro
24f1aae6b6 Update pt-br.json
Strings 541-766
2024-02-16 11:44:25 -03:00
ipcintron
04fbc9a22b change theme color 2024-02-16 02:15:27 -06:00
ipcintron
14e31d5690 update pwa icon 2024-02-16 01:32:04 -06:00
advplyr
a9e9808183 Fix:trim whitespace from asin for chapter lookup #2605 2024-02-15 17:05:48 -06:00
advplyr
af7cb2432b Update:Log uncaught exceptions to crash_logs.txt #706 & cleanup logger 2024-02-15 16:46:19 -06:00
pmangro
e0c1364916 Create pt-br.json
540 linhas iniciais
2024-02-15 19:08:05 -03:00
advplyr
04d16fc535 Fix:Audio player buttons to use button el and add aria-label translations #2599 2024-02-14 18:28:19 -06:00
advplyr
44135b3fed Rename StreamContainer to MediaPlayerContainer 2024-02-14 18:12:35 -06:00
advplyr
6111e8f0da Fix:Global search menu for mobile 2024-02-13 18:45:01 -06:00
advplyr
4e3e7b10ce Update:Custom metadata provider adapter sends mediaType as a query param 2024-02-12 17:12:49 -06:00
advplyr
ce7f81d676 Merge pull request #2486 from FlyinPancake/dewyer/add-custom-metadata-provider
[Feature] Add support for custom metadata providers through a REST API
2024-02-11 17:04:55 -06:00
advplyr
0cf2f8885e Add custom metadata provider controller, update model, move to item metadata utils 2024-02-11 16:48:16 -06:00
advplyr
ddf4b2646c Merge branch 'master' into dewyer/add-custom-metadata-provider 2024-02-11 09:10:29 -06:00
advplyr
fe1e0749a2 Update:Listening sessions table rows per page text wrapping 2024-02-08 19:12:59 -06:00
advplyr
2093468c92 Fix:Local playback sessions not persisting the last updatedAt value 2024-02-08 19:12:35 -06:00
mikiher
19af7454f2 Force Update LibraryItem model updatedAt refresh (fixes #2593) 2024-02-07 20:57:50 +02:00
KeyboardHammer
d24427aad8 fix property 2024-02-03 22:04:40 -06:00
KeyboardHammer
e2bb0cfb7c add sorting to author page 2024-02-03 21:48:35 -06:00
mikiher
2ebdb44826 Merge cover and media update in Match.vue into a single /media API call 2024-02-01 12:03:12 +02:00
advplyr
432e25565e Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-01-31 17:23:20 -06:00
advplyr
ebe511404a Remove updateMedia endpoint cover cache purge 2024-01-31 17:23:16 -06:00
advplyr
e0a79fb86c Merge pull request #2570 from Weldawadyathink/main
Return png from AudiobookCovers.com
2024-01-30 17:02:14 -06:00
Spenser Bushey
295ca3d9a2 Return png from AudiobookCovers.com
Changes AudiobookCovers.com provider to return the full size png file from the server. The original file url has the incorrect content-type header set, which caused issues downloading new cover images.
2024-01-30 09:15:50 -08:00
advplyr
dbad8bdb96 Merge pull request #2567 from ipcintron/lockscreen_cover
added raw cover on lockscreen for iOS
2024-01-29 15:21:44 -06:00
ipcintron
8c703859a0 added raw cover 2024-01-29 13:18:58 -06:00
advplyr
bedb260b00 Update:Epub ereader allow scripted content 2024-01-28 16:02:02 -06:00
advplyr
b49592301f Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2024-01-28 16:00:32 -06:00
advplyr
c6c67078b8 Update:PWA manifest icon to include PNG #2520 2024-01-28 16:00:26 -06:00
advplyr
9e45ad10f1 Merge pull request #2564 from Teekeks/patch-1
Update de.json
2024-01-28 10:10:13 -06:00
Lena During
24da859975 Update de.json 2024-01-28 16:06:07 +01:00
advplyr
0b6a8a9641 Update:Remove 64x64 app icon from PWA manifest so that only the SVG is available #2520 2024-01-27 10:50:44 -06:00
advplyr
e43c4f082e Fix:Rich text editor labels and add translations 2024-01-26 17:22:37 -06:00
advplyr
0b334cf957 Add:Authentication setting to show a custom message on login #2552 2024-01-26 17:08:23 -06:00
advplyr
ae387ab397 Merge pull request #2559 from bloodscript/master
German localization optimization
2024-01-26 16:28:02 -06:00
bloodscript
056e62dce8 added plural to metadata order hint 2024-01-26 23:15:27 +01:00
bloodscript
47999214bd corrected misspelling of adress 2024-01-26 23:13:11 +01:00
bloodscript
68473ee345 added missing metadata translation 2024-01-26 23:11:38 +01:00
bloodscript
455f27d443 mainly changed usage formular wording for you to the more commonly used, also corrected some misspellings 2024-01-26 23:09:54 +01:00
bloodscript
ba996c3b55 translation wasnt accurate verschluesselung means encryption 2024-01-26 20:12:37 +01:00
mikiher
d43a1109c8 Modify BinaryManager to download version 6.1 and remove old dowloaded versions 2024-01-25 17:51:06 +02:00
Denis Arnst
c3ba7daa16 Auth: Remove is_rest cookie 2024-01-25 16:05:41 +01:00
Denis Arnst
82048cd4f3 SSO: Also save openid_id_token longer 2024-01-25 15:13:56 +01:00
Denis Arnst
71b0a5cc81 SSO Settings: Fix Redirect URL Regex
Forgot to include subpaths
2024-01-25 11:49:10 +01:00
Denis Arnst
edb5ff1e33 SSO: Remove pick function 2024-01-25 11:44:20 +01:00
Denis Arnst
d4ed6348ee Auth: Store auth_method longer
Its not unrealistic that someone keeps being logged into the app for more than a year
if not stored longer logout process might not work anymore
2024-01-25 11:20:44 +01:00
Denis Arnst
f12ac685e8 /auth/openid: Restructure
- Distingush more explictly between mobile and web flow and simplify logic
- Allow state parameter to be passed in mobile flow
- Additional checks for correct parameters
- Remove unused id_token code
- Enforce S256 and don't allow plain PKCE
2024-01-25 11:13:34 +01:00
advplyr
b9ec4068ee Merge pull request #2510 from Torstein-Eide/patch-1
README, add HAproxy example
2024-01-24 16:55:32 -06:00
advplyr
02aabb8f97 Update readme.md 2024-01-24 16:55:21 -06:00
advplyr
dcec2154c0 Update readme.md 2024-01-24 16:55:17 -06:00
advplyr
bbc1d20396 Update readme.md 2024-01-24 16:55:12 -06:00
advplyr
e682213681 Update readme.md 2024-01-24 16:55:06 -06:00
advplyr
0153c0faae Update readme.md 2024-01-24 16:54:54 -06:00
Denis Arnst
87ebf4722b OpenID/SSO: Implement Logout functionality 2024-01-24 22:47:50 +01:00
advplyr
3906dca04e Update:RSS feeds only use chapter titles for episode titles if all audio tracks match chapter times #2543 2024-01-23 17:51:34 -06:00
advplyr
399ba314a3 Update github issue template to include Windows Tray App 2024-01-23 15:48:58 -06:00
advplyr
19e1803633 Remove unused import 2024-01-22 17:56:41 -06:00
advplyr
71048c7ff0 Remove support for Docker armv7 builds 2024-01-20 16:39:43 -06:00
advplyr
7f350279fa Update to node20
- updates many dependencies
- removes @nuxtjs/tailwindcss and postcss8
- pkg targets are using node18 until node20 targets are available
2024-01-19 17:54:41 -06:00
advplyr
90f4833c9e Version bump v2.7.2 2024-01-16 17:26:50 -06:00
advplyr
c0cb3a176f Update:Hide audiobook tools for windows install, remove debian folder picker alert 2024-01-16 17:19:45 -06:00
advplyr
7b0fa48e2e Update jsdocs for expanded library items 2024-01-16 16:31:16 -06:00
advplyr
b51853b3df Update:Use raw cover art for media session #2514 2024-01-15 08:34:12 -06:00
advplyr
f5545cd3f4 Add:Scanner extracts cover from comic files #1837 and ComicInfo.xml parser 2024-01-14 17:51:26 -06:00
advplyr
e76af3bfc2 Fix comic page menu dropdown highlight correct page 2024-01-13 16:41:13 -06:00
FlyinPancake
6ef4944d89 Merge branch 'advplyr:master' into dewyer/add-custom-metadata-provider 2024-01-13 01:08:23 +01:00
advplyr
850397e4c1 Add:Playlist button to podcast episodes on latest page #2455 2024-01-12 17:58:07 -06:00
FlyinPancake
3b531144cf implemented suggestions, extended CMPs with series 2024-01-12 21:45:03 +01:00
Torstein Eide
6ca684603c Fix typos 2024-01-12 14:35:30 +01:00
Torstein Eide
cf85d66b2f Add example for HAproxy 2024-01-12 14:26:32 +01:00
advplyr
e8fa029df7 Fix:Specific podcast rss feed cannot be fetched due to accept header #2446 2024-01-10 08:12:26 -06:00
advplyr
1a361c91f1 Merge pull request #2506 from FreedomBen/remove-dev-logs
Change `Logger.dev` calls to `Logger.debug`
2024-01-09 16:47:21 -06:00
Benjamin Porter
4a76059608 Change Logger.dev calls to Logger.debug
Logger.dev is kind of in a weird spot where it doesn't fit into the
standard log level.  It is called directly by some code and it only
checks whether a property is set (which comes from an env var) before
deciding to print out.

This standardizes on `debug` by changing the dev calls to debug. Also
removes the now unused code.
2024-01-09 15:24:23 -07:00
advplyr
da25eff5c1 Fix:Parse series sequence from OPF in cases where series_index is not directly underneath series meta #2505 2024-01-08 18:21:15 -06:00
advplyr
69e23ef9f2 Add:Epub metadata parser and cover extractor #1479 2024-01-07 17:51:07 -06:00
advplyr
48a08e9659 Merge pull request #2503 from Machou/master
Little Missed Update fr.json
2024-01-07 12:42:09 -06:00
Machou
4608f91ec6 Update fr.json 2024-01-07 02:41:16 +01:00
advplyr
e88c1fa329 Update:Show tooltip for library item card titles that are truncated #2451
- Refactored tooltip so that they dont overflow the window
2024-01-06 15:54:48 -06:00
advplyr
935e545caa Update readme for iOS beta full 2024-01-06 14:13:39 -06:00
advplyr
a426da534c Fix:Export OPML not escaping characters #2487 2024-01-05 14:45:25 -06:00
advplyr
eaf6bf29cc Fix:Improve performance for podcast rss feed episodes modal for large rss feeds 2024-01-05 14:39:25 -06:00
advplyr
a0eb6bd3dc Fix:Refresh podcast episode table when new episodes are downloaded 2024-01-05 14:38:29 -06:00
advplyr
fbe228a4f8 Merge pull request #2485 from Machou/patch-1
Update fr.json
2024-01-05 10:40:29 -06:00
advplyr
578a59063f Update discord invite link 2024-01-05 09:24:18 -06:00
mozhu
81020ff34d 播客搜索地区配置增加默认参数 2024-01-05 15:50:20 +08:00
mozhu
fea78898a5 移动播客搜索地区配置到媒体库配置 2024-01-05 14:45:35 +08:00
Machou
ffa7cc0d22 Update fr.json 2024-01-05 07:19:07 +01:00
advplyr
4f9969cd9b Merge pull request #2488 from FreedomBen/add-init-system-to-docker
Add tini as PID 1 handler in container image
2024-01-04 13:50:54 -06:00
mozhu
1be34564f2 数据绑定错误修改 2024-01-04 15:00:40 +08:00
mozhu
56eff7a236 增加播客搜索地区配置 2024-01-04 11:52:45 +08:00
advplyr
9f909b0d85 Update:Library folder browser to also work for debian and windows 2024-01-03 16:23:17 -06:00
Benjamin Porter
baa65b8155 Add tini as PID 1 handler in container image
This PR adds `tini` to the container image and uses it as PID 1 when
starting the container.  This ensures that proper PID 1 signal-handling
is implemented and passed to the underlying node.js process, thereby
ensuring that the ABS process has a chance to receive and handle signals
other than `SIGKILL`, such as the important `SIGINT`.

This is somewhat related to #2445 . Without this, the signal handled by
2445 won't be received when running in a container.

Some background:

In linux, PID 1 has special duties involving signal handling that are
different than other processes.  Node doesn't properly handle these
signals, which can lead to a number of problems ranging from annoying to
disruptive.  PID 1 also has reaping duties that can lead to resource
exhaustion if not properly handled.

For example, the container ignores `SIGINT` (Ctrl+C) as well as `docker stop`,
which can be annoying in development as you have to kill or wait for
the timeout to be reached.  In a production environment (such as Kubernetes)
this can lead to signal escalation and unnecessarily adds delays to
deployments and restarts as K8s has to wait for the timeout to be reached
before sending `SIGKILL`.

At best this is annoying and unnecessarily adds
delays.  At worst this can lead to file/data corruption as the process
doesn't get a chance to clean anything up when it is sent `SIGKILL`.
Without a proper PID 1 to forward signals, only SIGKILL can be used to
terminate the running process.
2024-01-03 13:55:43 -07:00
Barnabas Ratki
12c6a1baa0 Fix log messages 2024-01-03 20:42:35 +01:00
Barnabas Ratki
5ea423072b Small fixes 2024-01-03 20:40:36 +01:00
Barnabas Ratki
08a41e37b4 Add specification 2024-01-03 20:27:42 +01:00
Barnabas Ratki
8027c4a06f Added support for custom metadata providers
WiP but already open to feedback
2024-01-03 20:25:34 +01:00
Machou
a1e321b153 Update fr.json 2024-01-03 20:16:21 +01:00
advplyr
8c6a2ac5dd Merge pull request #2391 from mikiher/binary-manager
Add a binary manager that finds ffmpeg and ffprobe and installs them if not found
2024-01-02 14:25:56 -06:00
advplyr
b489bf9236 Restrict binary manager to Windows or development 2024-01-02 14:24:59 -06:00
advplyr
aa63aa6cf3 Merge branch 'master' into binary-manager 2024-01-02 14:16:27 -06:00
advplyr
9a2b93fb37 Version bump v2.7.1 2023-12-31 15:37:23 -06:00
advplyr
e8ea7efc98 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2023-12-31 15:36:37 -06:00
advplyr
81a76593da Fix:Merging chapters from multiple audio files with the same chapter titles #2461 2023-12-31 15:35:17 -06:00
advplyr
5336864f7d Merge pull request #2465 from thevoltagesource/getFileMtimeMs_Unhandled_Exception
Add try/catch to fileUtils.getFileMtimeMs
2023-12-31 15:34:43 -06:00
advplyr
d38058e1d2 Fix:Podcast episode time remaining shown on button showing 0 seconds after toggling mark as finished 2023-12-31 15:32:44 -06:00
advplyr
fececd4651 Fix:Playlists navigation button not showing on mobile screen #2469 2023-12-31 15:09:35 -06:00
advplyr
021adf3104 Update:Podcast episode table is lazy loaded #1549 2023-12-31 14:51:01 -06:00
advplyr
160c83df4a Update:podcastEpisodes table index added for createdAt column #2073 #2075 2023-12-30 16:14:14 -06:00
advplyr
456bb87a00 Update:Find one library item endpoint sequelize query split into two queries to improve performance #2073 #2075 2023-12-30 12:12:48 -06:00
advplyr
707451309c Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2023-12-29 17:05:40 -06:00
advplyr
269676e8a5 Update:CORS for /cover API endpoint for use in canvas in the mobile apps 2023-12-29 17:05:35 -06:00
Jacob Southard
e4effebc19 Add try/catch to fileutils.getFileMtimeMs 2023-12-29 10:04:59 -06:00
advplyr
fbbceddba8 Merge pull request #2454 from mikiher/socket-authority-close
Add SocketAuthority.close()
2023-12-28 16:32:40 -06:00
advplyr
9a634e0de5 Add JS docs for server stop 2023-12-28 16:32:21 -06:00
mikiher
21d0d43edc Add SocketAuthority.close() 2023-12-27 15:33:33 +02:00
mikiher
3051b963ef Merge branch 'advplyr:master' into binary-manager 2023-12-27 06:44:22 +02:00
advplyr
0d0bdce337 Fix:Fetch RSS feed request accept header #2446 2023-12-25 13:15:55 -06:00
advplyr
bdb5dc8c28 Merge pull request #2445 from mikiher/sigint-handler
Add a SIGINT handler for proper server shutdown
2023-12-25 12:51:22 -06:00
mikiher
209847d98a Add a SIGINT handler for proper server shutdown 2023-12-25 09:25:04 +02:00
advplyr
14f42e15d1 Fix:Book scanner update book series sequence if changed 2023-12-24 11:53:57 -06:00
advplyr
7402e4811d Merge pull request #2444 from jedrus2000/opf-multiple-series-support
Add: OPF file supports multiple series as sequence of : calibre:series and calibre:series_index; including tests
2023-12-24 11:42:06 -06:00
advplyr
6de0465b86 Update opf parser to ignore series with empty content and add tests 2023-12-24 11:41:27 -06:00
Andrzej Bargański
cd7c4baaaf Add: OPF file supports multiple series as sequence of : calibre:series and calibre:series_index; including tests 2023-12-24 00:43:42 +01:00
advplyr
a2db81bf7d Fix share button for year in review short card 2023-12-23 17:13:44 -06:00
advplyr
b376f89ce5 Version bump v2.7.0 2023-12-23 17:05:44 -06:00
advplyr
5633113f25 Update share buttons to not show an error on abort 2023-12-23 16:39:56 -06:00
advplyr
669415cfbf Merge pull request #2431 from pablojimenezmateo/comic-zoom
Add zoom controls to comic reader
2023-12-23 16:18:23 -06:00
advplyr
9f366863a9 Update comic reader buttons for mobile screens, add left scrollBy 2023-12-23 16:16:24 -06:00
advplyr
0d644fe0c9 Add:Year in review banner for user stats page #2373 2023-12-23 15:29:34 -06:00
advplyr
72fa6b8200 Fix:Show cover size widget when audio player is open #2443 2023-12-23 10:50:04 -06:00
advplyr
6d3f1d263a Merge pull request #2442 from JBlond/master
Follow up Translations for 76119445a3
2023-12-23 08:55:28 -06:00
JBlond
47bf9f7836 Follow up Translations for 76119445a3
* Update:Listening sessions table for multi-select, sorting and rows per page
- Updated get all sessions API endpoint to include sorting
- Added sessions API endpoint for batch deleting
2023-12-23 14:42:56 +01:00
advplyr
2738402aac Add:Year in review card for server stats #2373 2023-12-22 17:01:07 -06:00
advplyr
68d36522b1 Update:Listening sessions table UI for mobile 2023-12-21 14:36:51 -06:00
advplyr
24a587b944 Update:Remove playback sessions that are 3s or less on startup 2023-12-21 14:29:36 -06:00
advplyr
76119445a3 Update:Listening sessions table for multi-select, sorting and rows per page
- Updated get all sessions API endpoint to include sorting
- Added sessions API endpoint for batch deleting
2023-12-21 13:52:42 -06:00
advplyr
46ec59c74e Update:Year in review card prevent text overflow for narrator, author and genre #2373 2023-12-21 09:44:37 -06:00
advplyr
2b7122c744 Update:Year stats API endpoint & generate year in review image #2373 2023-12-20 17:18:21 -06:00
Pablo
52f0a5432b feat: enable zoom through the arrow buttons 2023-12-20 11:45:21 +01:00
advplyr
7391b4d0ec Add:User stats API for year stats 2023-12-19 17:19:33 -06:00
Pablo
aa7ee3e8ff fix: zoom buttons were showing when loading the image 2023-12-19 18:45:11 +01:00
Pablo
bef0f3709f feat: add basic zoom functionality to comic reader 2023-12-19 18:39:02 +01:00
advplyr
f33b011847 Merge pull request #2420 from treyg/synology-reverse-proxy-docs
docs: update synology reverse proxy to use the latest DSM settings
2023-12-17 16:42:43 -06:00
Trey Gordon
2d8d11d4da docs: update synology reverse proxy to use the latest DSM settings 2023-12-17 15:56:14 -05:00
advplyr
10b1784f6d Fix:Library search API endpoint /libraries/:id/search to check that query param q is a valid string 2023-12-17 12:23:55 -06:00
advplyr
f2f2ea161c Update:API endpoint /podcasts/feed validates rssFeed URL and uses SSRF req filter 2023-12-17 12:00:11 -06:00
advplyr
dc67a52000 Update:API endpoint /search/podcast throw 400 error if term query param is not supplied 2023-12-17 11:18:21 -06:00
advplyr
05820aa820 Update:API endpoints /podcasts/feed and /podcasts/opml restricted to admin users 2023-12-17 11:17:35 -06:00
advplyr
8966dbbcd1 Fix:Restrict podcast search page to admins 2023-12-17 11:06:03 -06:00
advplyr
cf32819c01 Merge branch 'master' of https://github.com/advplyr/audiobookshelf 2023-12-17 10:41:55 -06:00
advplyr
728496010c Update:/auth/openid/config API endpoint to require admin user and validate issuer URL 2023-12-17 10:41:39 -06:00
advplyr
0a08f47942 Merge pull request #2417 from springsunx/master
Update zh-cn.json
2023-12-15 12:14:15 -06:00
SunX
39ceb02500 Update zh-cn.json 2023-12-15 19:04:56 +08:00
advplyr
4336714248 Merge pull request #2415 from nichwall/docker_compose_update
Added comments to the Docker Compose file
2023-12-15 04:14:16 -06:00
nichwall
1d41904fc3 Added comments to the Docker Compose file 2023-12-14 21:04:37 -07:00
advplyr
fae383a045 Fix:RSS feeds for collections not updating #2414 2023-12-14 15:45:34 -06:00
mikiher
8f7a420cca Fix directory writable check (fs.access not working on Windows) 2023-12-14 09:47:18 +02:00
advplyr
9720ba3eed Merge pull request #2413 from JBlond/master
More german translations
2023-12-13 13:40:36 -06:00
JBlond
d3256d59d5 - Translate more strings
- Add missing least empty line
2023-12-13 20:12:25 +01:00
advplyr
fa5f7ab7a5 Merge pull request #2411 from Nab0y/master
Update Russian localization
2023-12-12 16:57:35 -06:00
Dmitry Naboychenko
6f26fd7238 Update Russian localization 2023-12-12 22:56:05 +03:00
advplyr
6abc0819d9 Merge pull request #2400 from mikiher/bookfinder-improvements
A few BookFinder improvements (including a fix for #2238)
2023-12-10 10:36:21 -06:00
advplyr
b580a23e7e BookFinder formatting update 2023-12-10 10:35:21 -06:00
advplyr
f659c3f11c Fix:Podcast RSS feed request header to include application/rss+xml #2401 2023-12-09 13:51:28 -06:00
mikiher
0282a0521b Sort audible match results by duration difference 2023-12-09 00:33:06 +02:00
advplyr
75637e4b94 Merge pull request #2397 from JBlond/master
Follow up for sso-redirecturi and #2305 and #2333
2023-12-08 15:42:28 -06:00
mikiher
b6c789dee6 Merge branch 'advplyr:master' into bookfinder-improvements 2023-12-08 14:07:25 +02:00
JBlond
8d3d636329 Follow up for sso-redirecturi and #2305 #2333
8f4c65ec8c / 7c9c278cc4 sso-redirecturi
2f6756eddf #2333
2e5822b7c8 #2305
2023-12-08 09:39:04 +01:00
advplyr
6f6395bad7 Only log update binary env path if it was updated 2023-12-07 17:32:06 -06:00
advplyr
b8c8d2a02e Merge pull request #2386 from Sapd/sso-redirecturi
SSO/OpenID: Use a mobile-redirect route (Fixes #2379 and #2381)
2023-12-07 17:12:36 -06:00
advplyr
98104a3c03 Map new translations to other files 2023-12-07 17:05:52 -06:00
advplyr
8f4c65ec8c Merge branch 'master' into sso-redirecturi 2023-12-07 17:04:59 -06:00
advplyr
341a0452da Update auth settings endpoint to return updated flag and show whether updates were made in client toast 2023-12-07 17:01:33 -06:00
mikiher
6afb8de3dd Remove ffbinaries local cache 2023-12-08 00:53:53 +02:00
mikiher
0e62ccc7aa Merge branch 'binary-manager' of https://github.com/mikiher/audiobookshelf into binary-manager 2023-12-07 23:51:33 +02:00
mikiher
09282a9a62 Remove all callbacks and refactor spaghetti code in downloadUrls 2023-12-07 23:49:46 +02:00
advplyr
18b3ab5610 Revert package-lock updates 2023-12-07 15:12:49 -06:00
mikiher
699a658df9 Remove debug printing from libs/ffbinaries 2023-12-07 08:50:45 +02:00
advplyr
b5e255a384 Update:Clean series sequence response from audible provider #2380
- Removes Book prefix
- Splits on spaces and takes first, removes trailing comma
2023-12-06 17:31:36 -06:00
mikiher
67ccd2c1fb Fix test after switching to libs/ffbinaries 2023-12-06 13:45:28 +02:00
mikiher
898b072e68 Merge branch 'advplyr:master' into binary-manager 2023-12-06 09:27:17 +02:00
advplyr
34156af403 Fix:Updating media progress not clearing cache #2392 2023-12-05 17:58:54 -06:00
advplyr
61a0126278 Remove ffbinaries dependency 2023-12-05 17:35:57 -06:00
advplyr
1ce1904c89 Add ffbinaries lib 2023-12-05 17:35:15 -06:00
advplyr
7c9c278cc4 Merge pull request #2293 from Dr-Blank/gujarati-translation
More Gujarati translations
2023-12-05 15:42:05 -06:00
advplyr
450507a812 Map english translations and merge with gu 2023-12-05 15:41:12 -06:00
mikiher
c074c835d4 Remove semicolons from test 2023-12-05 22:18:37 +02:00
mikiher
2e989fbe83 Add BinaryManager 2023-12-05 21:19:17 +02:00
mikiher
b1b325d00b Add ffbinaries dependency 2023-12-05 21:18:30 +02:00
Denis Arnst
cf00650c6d SSO/OpenID: Also fix possible race condition
- We need to define redirect_uri in the callback again, because the global params of passport can change between calls to the first route (ie. if multiple users log in at same time)
- Removed is_rest parameter as requirement for mobile flow (to maximise compatibility with possible oauth libraries)
- Also renamed some variables for clarity
2023-12-05 09:43:06 +01:00
Denis Arnst
e6ab28365f SSO/OpenID: Remove modifying redirect_uri in the callback
The redirect URI will be now correctly set to either /callback or /mobile-redirect in the /auth/openid route
2023-12-05 00:18:58 +01:00
Denis Arnst
80fd2a1a18 SSO/OpenID: Use a mobile-redirect route (Fixes #2379 and #2381)
- Implement /auth/openid/mobile-redirect this will redirect to an app-link like audiobookshelf://oauth
- An app must provide an `redirect_uri` parameter with the app-link in the authorization request to /auth/openid
- The user will have to whitelist possible URLs, or explicitly allow all
- Also modified MultiSelect to allow to hide the menu/popup
2023-12-04 22:36:34 +01:00
advplyr
84160b2f07 Fix:Server crash when user without a password attempts to login with a password #2378 2023-12-02 16:17:52 -06:00
advplyr
fbc2c2b481 Merge pull request #2333 from kieraneglin/ke/feature/upload-auto-fetch-data
Add ability to fetch book data on upload
2023-12-02 15:56:39 -06:00
Kieran Eglin
57a5005197 Addressed feedback changes 2023-12-01 21:42:54 -08:00
Kieran Eglin
9350c5513e Removed unneeded mixin 2023-12-01 15:19:50 -08:00
advplyr
f59516cc6e Fix:Hide change password form when password auth is disabled #2367 2023-12-01 17:10:33 -06:00
advplyr
88078ff813 Fix undefined series string when match has no series, minor ui updates 2023-12-01 16:44:04 -06:00
mikiher
281de48ed4 Fix "et al" cleanup 2023-11-30 21:49:24 +02:00
mikiher
3c6d6bf688 Merge branch 'advplyr:master' into bookfinder-improvements 2023-11-30 21:37:01 +02:00
mikiher
8ac0ce399f Remove "et al[.]" in author cleanup 2023-11-30 21:17:13 +02:00
mikiher
80458e24bd "[un]abridged" in title candidate generation 2023-11-30 21:15:25 +02:00
advplyr
6ab966ee2f Merge pull request #2365 from Sapd/sso-errorhandling
SSO/OpenID: Provide error messages to logs
2023-11-28 16:39:01 -06:00
advplyr
166477ae27 Fix:Narrators page 404 on reload #2359 2023-11-28 16:39:52 -06:00
advplyr
a719065b8d Auto formatting 2023-11-28 16:37:19 -06:00
Denis Arnst
36599a2984 SSO/OpenID: Rename probably misleading message 2023-11-28 21:16:39 +01:00
Kieran Eglin
d9c9289d65 Added error handling; Made querystring helper 2023-11-28 12:11:14 -08:00
Kieran Eglin
e5579b2c33 Improved UI; Added tooltips; Fixed unrelated layout issues 2023-11-28 11:45:44 -08:00
Denis Arnst
618028503b SSO/OpenID: Also Log token header 2023-11-28 20:07:49 +01:00
Kieran Eglin
2f6756eddf Merged parent 2023-11-28 10:21:41 -08:00
Denis Arnst
ad53894ea1 SSO/OpenID: Provide detailed error messages 2023-11-28 17:29:22 +01:00
Kieran Eglin
8c434703fb Added computed metadata check to UI dropdown 2023-11-20 09:18:50 -08:00
Kieran Eglin
3cc900ffbf Adds fetching book data on upload 2023-11-20 08:51:00 -08:00
Dr-Blank
6d968f9044 Update gu.json 2023-11-06 18:16:03 -05:00
Dr-Blank
23fa9e8d7f Update gu.json 2023-11-06 18:15:18 -05:00
Dr-Blank
59a428d549 more gu translations 2023-11-06 18:10:57 -05:00
201 changed files with 20303 additions and 27159 deletions

View File

@@ -1,5 +1,5 @@
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster
ARG VARIANT=16
ARG VARIANT=20
FROM mcr.microsoft.com/devcontainers/javascript-node:0-${VARIANT} as base
# Setup the node environment

View File

@@ -5,5 +5,6 @@ module.exports.config = {
ConfigPath: Path.resolve('config'),
MetadataPath: Path.resolve('metadata'),
FFmpegPath: '/usr/bin/ffmpeg',
FFProbePath: '/usr/bin/ffprobe'
FFProbePath: '/usr/bin/ffprobe',
SkipBinariesCheck: false
}

View File

@@ -8,7 +8,7 @@
// Append -bullseye or -buster to pin to an OS version.
// Use -bullseye variants on local arm64/Apple Silicon.
"args": {
"VARIANT": "16"
"VARIANT": "20"
}
},
"mounts": [

View File

@@ -11,7 +11,7 @@ body:
value: "### Mobile app issues report [here](https://github.com/advplyr/audiobookshelf-app/issues/new/choose)."
- type: markdown
attributes:
value: "### Join the [discord server](https://discord.gg/pJsjuNCKRq) for questions or if you are not sure about a bug."
value: "### Join the [discord server](https://discord.gg/HQgCbd6E75) for questions or if you are not sure about a bug."
- type: markdown
attributes:
value: "## Be as descriptive as you can. Include screenshots, error logs, browser, file types, everything you can think of that might be relevant."
@@ -44,6 +44,7 @@ body:
options:
- Docker
- Debian/PPA
- Windows Tray App
- Built from source
- Other
validations:

View File

@@ -1,7 +1,7 @@
blank_issues_enabled: false
contact_links:
- name: Discord
url: https://discord.gg/pJsjuNCKRq
url: https://discord.gg/HQgCbd6E75
about: Ask questions, get help troubleshooting, and join the Abs community here.
- name: Matrix
url: https://matrix.to/#/#audiobookshelf:matrix.org

View File

@@ -71,7 +71,7 @@ jobs:
with:
tags: ${{ github.event.inputs.tags || steps.meta.outputs.tags }}
context: .
platforms: linux/amd64,linux/arm64,linux/arm/v7
platforms: linux/amd64,linux/arm64
push: true
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max

View File

@@ -16,7 +16,7 @@ jobs:
- name: setup nade
uses: actions/setup-node@v3
with:
node-version: 16
node-version: 20
- name: install pkg
run: npm install -g pkg

31
.github/workflows/unit-tests.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Run Unit Tests
on:
workflow_dispatch:
inputs:
ref:
description: 'Branch/Tag/SHA to test'
required: true
pull_request:
push:
jobs:
run-unit-tests:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
ref: ${{ github.event_name != 'workflow_dispatch' && github.ref_name || inputs.ref}}
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test

2
.gitignore vendored
View File

@@ -13,6 +13,8 @@
/deploy/
/coverage/
/.nyc_output/
/ffmpeg*
/ffprobe*
sw.*
.DS_STORE

View File

@@ -1,5 +1,5 @@
### STAGE 0: Build client ###
FROM node:16-alpine AS build
FROM node:20-alpine AS build
WORKDIR /client
COPY /client /client
RUN npm ci && npm cache clean --force
@@ -7,7 +7,7 @@ RUN npm run generate
### STAGE 1: Build server ###
FROM sandreas/tone:v0.1.5 AS tone
FROM node:16-alpine
FROM node:20-alpine
ENV NODE_ENV=production
@@ -18,7 +18,8 @@ RUN apk update && \
ffmpeg \
make \
python3 \
g++
g++ \
tini
COPY --from=tone /usr/local/bin/tone /usr/local/bin/
COPY --from=build /client/dist /client/dist
@@ -31,4 +32,5 @@ RUN apk del make python3 g++
EXPOSE 80
ENTRYPOINT ["tini", "--"]
CMD ["node", "index.js"]

View File

@@ -48,11 +48,10 @@ Description: $DESCRIPTION"
echo "$controlfile" > dist/debian/DEBIAN/control;
# Package debian
pkg -t node16-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf .
pkg -t node18-linux-x64 -o dist/debian/usr/share/audiobookshelf/audiobookshelf .
fakeroot dpkg-deb --build dist/debian
fakeroot dpkg-deb -Zxz --build dist/debian
mv dist/debian.deb "dist/$OUTPUT_FILE"
chmod +x "dist/$OUTPUT_FILE"
echo "Finished! Filename: $OUTPUT_FILE"

View File

@@ -217,36 +217,6 @@ Bookshelf Label
filter: blur(20px);
}
.episode-subtitle {
word-break: break-word;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
line-height: 16px;
/* fallback */
max-height: 32px;
/* fallback */
-webkit-line-clamp: 2;
/* number of lines to show */
-webkit-box-orient: vertical;
}
.episode-subtitle-long {
word-break: break-word;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
line-height: 16px;
/* fallback */
max-height: 72px;
/* fallback */
-webkit-line-clamp: 6;
/* number of lines to show */
-webkit-box-orient: vertical;
}
/* Padding for toastification toasts in the top right to not cover appbar/toolbar */
.app-bar-and-toolbar .Vue-Toastification__container.top-right {
padding-top: 104px;

View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

View File

@@ -1,7 +1,7 @@
<template>
<div id="bookshelf" ref="wrapper" class="w-full max-w-full h-full overflow-y-scroll relative">
<!-- Cover size widget -->
<widgets-cover-size-widget class="fixed bottom-4 right-4 z-50" />
<widgets-cover-size-widget class="fixed right-4 z-50" :style="{ bottom: streamLibraryItem ? '181px' : '16px' }" />
<div v-if="loaded && !shelves.length && !search" class="w-full flex flex-col items-center justify-center py-12">
<p class="text-center text-2xl mb-4 py-4">{{ libraryName }} Library is empty!</p>
@@ -94,6 +94,9 @@ export default {
},
selectedMediaItems() {
return this.$store.state.globals.selectedMediaItems || []
},
streamLibraryItem() {
return this.$store.state.streamLibraryItem
}
},
methods: {

View File

@@ -22,6 +22,10 @@
<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>
</nuxt-link>
<nuxt-link v-if="showPlaylists" :to="`/library/${currentLibraryId}/bookshelf/playlists`" class="flex-grow h-full flex justify-center items-center" :class="isPlaylistsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p v-if="isPlaylistsPage || isPodcastLibrary" class="text-sm">{{ $strings.ButtonPlaylists }}</p>
<span v-else class="material-icons-outlined text-lg">queue_music</span>
</nuxt-link>
<nuxt-link v-if="isBookLibrary" :to="`/library/${currentLibraryId}/bookshelf/collections`" class="flex-grow h-full flex justify-center items-center" :class="isCollectionsPage ? 'bg-primary bg-opacity-80' : 'bg-primary bg-opacity-40'">
<p v-if="isCollectionsPage" class="text-sm">{{ $strings.ButtonCollections }}</p>
<span v-else class="material-icons-outlined text-lg">collections_bookmark</span>
@@ -94,6 +98,9 @@
<template v-else-if="page === 'authors'">
<div class="flex-grow" />
<ui-btn v-if="userCanUpdate && authors && authors.length && !isBatchSelecting" :loading="processingAuthors" color="primary" small @click="matchAllAuthors">{{ $strings.ButtonMatchAllAuthors }}</ui-btn>
<!-- author sort select -->
<controls-sort-select v-if="authors && authors.length" v-model="settings.authorSortBy" :descending.sync="settings.authorSortDesc" :items="authorSortItems" class="w-36 sm:w-44 md:w-48 h-7.5 ml-1 sm:ml-4" @change="updateAuthorSort" />
</template>
</div>
</div>
@@ -179,6 +186,30 @@ export default {
}
]
},
authorSortItems() {
return [
{
text: this.$strings.LabelAuthorFirstLast,
value: 'name'
},
{
text: this.$strings.LabelAuthorLastFirst,
value: 'lastFirst'
},
{
text: this.$strings.LabelNumberOfBooks,
value: 'numBooks'
},
{
text: this.$strings.LabelAddedAt,
value: 'addedAt'
},
{
text: this.$strings.LabelUpdatedAt,
value: 'updatedAt'
}
]
},
userIsAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
},
@@ -293,6 +324,9 @@ export default {
}
return items
},
showPlaylists() {
return this.$store.state.libraries.numUserPlaylists > 0
}
},
methods: {
@@ -448,6 +482,9 @@ export default {
updateCollapseBookSeries() {
this.saveSettings()
},
updateAuthorSort() {
this.saveSettings()
},
saveSettings() {
this.$store.dispatch('user/updateUserSettings', this.settings)
},

View File

@@ -21,7 +21,7 @@
</div>
</div>
<widgets-cover-size-widget class="fixed bottom-4 right-4 z-50" />
<widgets-cover-size-widget class="fixed right-4 z-50" :style="{ bottom: streamLibraryItem ? '181px' : '16px' }" />
</div>
</template>
@@ -205,6 +205,9 @@ export default {
sizeMultiplier() {
const baseSize = this.isCoverSquareAspectRatio ? 192 : 120
return this.entityWidth / baseSize
},
streamLibraryItem() {
return this.$store.state.streamLibraryItem
}
},
methods: {

View File

@@ -1,5 +1,5 @@
<template>
<div v-if="streamLibraryItem" id="streamContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 md:h-40 z-50 bg-primary px-2 md:px-4 pb-1 md:pb-4 pt-2">
<div v-if="streamLibraryItem" id="mediaPlayerContainer" class="w-full fixed bottom-0 left-0 right-0 h-48 md:h-40 z-50 bg-primary px-2 md:px-4 pb-1 md:pb-4 pt-2">
<div id="videoDock" />
<div class="absolute left-2 top-2 md:left-4 cursor-pointer">
<covers-book-cover expand-on-click :library-item="streamLibraryItem" :width="bookCoverWidth" :book-cover-aspect-ratio="coverAspectRatio" />
@@ -29,7 +29,7 @@
</div>
<div class="flex-grow" />
<ui-tooltip direction="top" :text="$strings.LabelClosePlayer">
<span class="material-icons sm:px-2 py-1 md:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</span>
<button :aria-label="$strings.LabelClosePlayer" class="material-icons sm:px-2 py-1 md:p-4 cursor-pointer text-xl sm:text-2xl" @click="closePlayer">close</button>
</ui-tooltip>
</div>
<player-ui
@@ -349,7 +349,7 @@ export default {
}
if ('mediaSession' in navigator) {
var coverImageSrc = this.$store.getters['globals/getLibraryItemCoverSrc'](this.streamLibraryItem, '/Logo.png')
var coverImageSrc = this.$store.getters['globals/getLibraryItemCoverSrc'](this.streamLibraryItem, '/Logo.png', true)
const artwork = [
{
src: coverImageSrc
@@ -380,7 +380,7 @@ export default {
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === data.stream) {
if (!data.numSegments) return
var chunks = data.chunks
console.log(`[StreamContainer] Stream Progress ${data.percent}`)
console.log(`[MediaPlayerContainer] Stream Progress ${data.percent}`)
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.setChunksReady(chunks, data.numSegments)
} else {
@@ -397,17 +397,17 @@ export default {
this.playerHandler.prepareOpenSession(session, this.currentPlaybackRate)
},
streamOpen(session) {
console.log(`[StreamContainer] Stream session open`, session)
console.log(`[MediaPlayerContainer] Stream session open`, session)
},
streamClosed(streamId) {
// Stream was closed from the server
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === streamId) {
console.warn('[StreamContainer] Closing stream due to request from server')
console.warn('[MediaPlayerContainer] Closing stream due to request from server')
this.playerHandler.closePlayer()
}
},
streamReady() {
console.log(`[StreamContainer] Stream Ready`)
console.log(`[MediaPlayerContainer] Stream Ready`)
if (this.$refs.audioPlayer) {
this.$refs.audioPlayer.setStreamReady()
} else {
@@ -417,7 +417,7 @@ export default {
streamError(streamId) {
// Stream had critical error from the server
if (this.playerHandler.isPlayingLocalItem && this.playerHandler.currentStreamId === streamId) {
console.warn('[StreamContainer] Closing stream due to stream error from server')
console.warn('[MediaPlayerContainer] Closing stream due to stream error from server')
this.playerHandler.closePlayer()
}
},
@@ -496,7 +496,7 @@ export default {
</script>
<style>
#streamContainer {
#mediaPlayerContainer {
box-shadow: 0px -6px 8px #1111113f;
}
</style>

View File

@@ -1,6 +1,7 @@
<template>
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8">
<div class="flex items-center mb-2">
<slot name="header-prefix"></slot>
<h1 class="text-xl">{{ headerText }}</h1>
<slot name="header-items"></slot>

View File

@@ -15,24 +15,33 @@
<div class="flex my-2 -mx-2">
<div class="w-1/2 px-2">
<ui-text-input-with-label v-model="itemData.title" :disabled="processing" :label="$strings.LabelTitle" @input="titleUpdated" />
<ui-text-input-with-label v-model.trim="itemData.title" :disabled="processing" :label="$strings.LabelTitle" @input="titleUpdated" />
</div>
<div class="w-1/2 px-2">
<ui-text-input-with-label v-if="!isPodcast" v-model="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" />
<div v-if="!isPodcast" class="flex items-end">
<ui-text-input-with-label v-model.trim="itemData.author" :disabled="processing" :label="$strings.LabelAuthor" />
<ui-tooltip :text="$strings.LabelUploaderItemFetchMetadataHelp">
<div
class="ml-2 mb-1 w-8 h-8 bg-bg border border-white border-opacity-10 flex items-center justify-center rounded-full hover:bg-primary cursor-pointer"
@click="fetchMetadata">
<span class="text-base text-white text-opacity-80 font-mono material-icons">sync</span>
</div>
</ui-tooltip>
</div>
<div v-else class="w-full">
<p class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></p>
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs" style="height: 38px" />
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs" />
</div>
</div>
</div>
<div v-if="!isPodcast" class="flex my-2 -mx-2">
<div class="w-1/2 px-2">
<ui-text-input-with-label v-model="itemData.series" :disabled="processing" :label="$strings.LabelSeries" note="(optional)" />
<ui-text-input-with-label v-model.trim="itemData.series" :disabled="processing" :label="$strings.LabelSeries" note="(optional)" inputClass="h-10" />
</div>
<div class="w-1/2 px-2">
<div class="w-full">
<p class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></p>
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs" style="height: 38px" />
<label class="px-1 text-sm font-semibold">{{ $strings.LabelDirectory }} <em class="font-normal text-xs pl-2">(auto)</em></label>
<ui-text-input :value="directory" disabled class="w-full font-mono text-xs h-10" />
</div>
</div>
</div>
@@ -48,8 +57,8 @@
<p class="text-base">{{ $strings.MessageUploaderItemFailed }}</p>
</widgets-alert>
<div v-if="isUploading" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20">
<ui-loading-indicator :text="$strings.MessageUploading" />
<div v-if="isNonInteractable" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-50 flex items-center justify-center z-20">
<ui-loading-indicator :text="nonInteractionLabel" />
</div>
</div>
</template>
@@ -61,10 +70,11 @@ export default {
props: {
item: {
type: Object,
default: () => {}
default: () => { }
},
mediaType: String,
processing: Boolean
processing: Boolean,
provider: String
},
data() {
return {
@@ -76,7 +86,8 @@ export default {
error: '',
isUploading: false,
uploadFailed: false,
uploadSuccess: false
uploadSuccess: false,
isFetchingMetadata: false
}
},
computed: {
@@ -87,12 +98,19 @@ export default {
if (!this.itemData.title) return ''
if (this.isPodcast) return this.itemData.title
if (this.itemData.series && this.itemData.author) {
return Path.join(this.itemData.author, this.itemData.series, this.itemData.title)
} else if (this.itemData.author) {
return Path.join(this.itemData.author, this.itemData.title)
} else {
return this.itemData.title
const outputPathParts = [this.itemData.author, this.itemData.series, this.itemData.title]
const cleanedOutputPathParts = outputPathParts.filter(Boolean).map(part => this.$sanitizeFilename(part))
return Path.join(...cleanedOutputPathParts)
},
isNonInteractable() {
return this.isUploading || this.isFetchingMetadata
},
nonInteractionLabel() {
if (this.isUploading) {
return this.$strings.MessageUploading
} else if (this.isFetchingMetadata) {
return this.$strings.LabelFetchingMetadata
}
}
},
@@ -105,9 +123,42 @@ export default {
titleUpdated() {
this.error = ''
},
async fetchMetadata() {
if (!this.itemData.title.trim().length) {
return
}
this.isFetchingMetadata = true
this.error = ''
try {
const searchQueryString = new URLSearchParams({
title: this.itemData.title,
author: this.itemData.author,
provider: this.provider
})
const [bestCandidate, ..._rest] = await this.$axios.$get(`/api/search/books?${searchQueryString}`)
if (bestCandidate) {
this.itemData = {
...this.itemData,
title: bestCandidate.title,
author: bestCandidate.author,
series: (bestCandidate.series || [])[0]?.series
}
} else {
this.error = this.$strings.ErrorUploadFetchMetadataNoResults
}
} catch (e) {
console.error('Failed', e)
this.error = this.$strings.ErrorUploadFetchMetadataAPI
} finally {
this.isFetchingMetadata = false
}
},
getData() {
if (!this.itemData.title) {
this.error = 'Must have a title'
this.error = this.$strings.ErrorUploadLacksTitle
return null
}
this.error = ''
@@ -128,4 +179,4 @@ export default {
}
}
}
</script>
</script>

View File

@@ -8,10 +8,10 @@
<!-- Alternative bookshelf title/author/sort -->
<div v-if="isAlternativeBookshelfView || isAuthorBookshelfView" class="absolute left-0 z-50 w-full" :style="{ bottom: `-${titleDisplayBottomOffset}rem` }">
<div :style="{ fontSize: 0.9 * sizeMultiplier + 'rem' }">
<div class="flex items-center">
<span class="truncate">{{ displayTitle }}</span>
<ui-tooltip v-if="displayTitle" :text="displayTitle" :disabled="!displayTitleTruncated" direction="bottom" :delayOnShow="500" class="flex items-center">
<p ref="displayTitle" class="truncate">{{ displayTitle }}</p>
<widgets-explicit-indicator :explicit="isExplicit" />
</div>
</ui-tooltip>
</div>
<p class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displayLineTwo || '&nbsp;' }}</p>
<p v-if="displaySortLine" class="truncate text-gray-400" :style="{ fontSize: 0.8 * sizeMultiplier + 'rem' }">{{ displaySortLine }}</p>
@@ -164,6 +164,7 @@ export default {
imageReady: false,
selected: false,
isSelectionMode: false,
displayTitleTruncated: false,
showCoverBg: false
}
},
@@ -642,6 +643,12 @@ export default {
}
this.libraryItem = libraryItem
this.$nextTick(() => {
if (this.$refs.displayTitle) {
this.displayTitleTruncated = this.$refs.displayTitle.scrollWidth > this.$refs.displayTitle.clientWidth
}
})
},
clickCard(e) {
if (this.processing) return

View File

@@ -1,13 +1,15 @@
<template>
<div class="sm:w-80 w-full relative">
<form @submit.prevent="submitSearch">
<ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
</form>
<div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear">
<span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span>
<span v-else class="material-icons" style="font-size: 1.2rem">close</span>
<div class="">
<div class="w-full relative sm:w-80">
<form @submit.prevent="submitSearch">
<ui-text-input ref="input" v-model="search" :placeholder="$strings.PlaceholderSearch" @input="inputUpdate" @focus="focussed" @blur="blurred" class="w-full h-8 text-sm" />
</form>
<div class="absolute top-0 right-0 bottom-0 h-full flex items-center px-2 text-gray-400 cursor-pointer" @click="clickClear">
<span v-if="!search" class="material-icons" style="font-size: 1.2rem">search</span>
<span v-else class="material-icons" style="font-size: 1.2rem">close</span>
</div>
</div>
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-40 sm:w-full bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu">
<div v-show="showMenu && (lastSearch || isTyping)" class="absolute z-40 -mt-px w-full max-w-64 sm:max-w-80 sm:w-80 bg-bg border border-black-200 shadow-lg rounded-md py-1 px-2 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm globalSearchMenu">
<ul class="h-full w-full" role="listbox" aria-labelledby="listbox-label">
<li v-if="isTyping" class="py-2 px-2">
<p>{{ $strings.MessageThinking }}</p>

View File

@@ -368,9 +368,17 @@ export default {
id: 'ebook',
name: this.$strings.LabelHasEbook
},
{
id: 'no-ebook',
name: this.$strings.LabelMissingEbook
},
{
id: 'supplementary',
name: this.$strings.LabelHasSupplementaryEbook
},
{
id: 'no-supplementary',
name: this.$strings.LabelMissingSupplementaryEbook
}
]
},

View File

@@ -1,8 +1,8 @@
<template>
<div class="relative" v-click-outside="clickOutside" @mouseover="mouseover" @mouseleave="mouseleave">
<div class="cursor-pointer text-gray-300 hover:text-white" @mousedown.prevent @mouseup.prevent @click="clickVolumeIcon">
<button :aria-label="$strings.LabelVolume" class="text-gray-300 hover:text-white" @mousedown.prevent @mouseup.prevent @click="clickVolumeIcon">
<span class="material-icons text-2xl sm:text-3xl">{{ volumeIcon }}</span>
</div>
</button>
<transition name="menux">
<div v-show="isOpen" class="volumeMenu h-6 absolute bottom-2 w-28 px-2 bg-bg shadow-sm rounded-lg" style="left: -116px">
<div ref="volumeTrack" class="h-1 w-full bg-gray-500 my-2.5 relative cursor-pointer rounded-full" @mousedown="mousedownTrack" @click="clickVolumeTrack">
@@ -38,8 +38,8 @@ export default {
},
set(val) {
try {
localStorage.setItem("volume", val);
} catch(error) {
localStorage.setItem('volume', val)
} catch (error) {
console.error('Failed to store volume', err)
}
this.$emit('input', val)
@@ -146,7 +146,7 @@ export default {
if (this.value === 0) {
this.isMute = true
}
const storageVolume = localStorage.getItem("volume")
const storageVolume = localStorage.getItem('volume')
if (storageVolume) {
this.volume = parseFloat(storageVolume)
}

View File

@@ -111,7 +111,8 @@
</div>
<div class="flex pt-4 px-2">
<ui-btn v-if="isEditingRoot" to="/account">{{ $strings.ButtonChangeRootPassword }}</ui-btn>
<ui-btn v-if="hasOpenIDLink" small :loading="unlinkingFromOpenID" color="primary" type="button" class="mr-2" @click.stop="unlinkOpenID">Unlink OpenID</ui-btn>
<ui-btn v-if="isEditingRoot" small class="flex items-center" to="/account">{{ $strings.ButtonChangeRootPassword }}</ui-btn>
<div class="flex-grow" />
<ui-btn color="success" type="submit">{{ $strings.ButtonSubmit }}</ui-btn>
</div>
@@ -136,7 +137,8 @@ export default {
newUser: {},
isNew: true,
tags: [],
loadingTags: false
loadingTags: false,
unlinkingFromOpenID: false
}
},
watch: {
@@ -180,7 +182,7 @@ export default {
return this.isNew ? this.$strings.HeaderNewAccount : this.$strings.HeaderUpdateAccount
},
isEditingRoot() {
return this.account && this.account.type === 'root'
return this.account?.type === 'root'
},
libraries() {
return this.$store.state.libraries.libraries
@@ -198,6 +200,9 @@ export default {
},
tagsSelectionText() {
return this.newUser.permissions.selectedTagsNotAccessible ? this.$strings.LabelTagsNotAccessibleToUser : this.$strings.LabelTagsAccessibleToUser
},
hasOpenIDLink() {
return !!this.account?.hasOpenIDLink
}
},
methods: {
@@ -205,6 +210,31 @@ export default {
// Force close when navigating - used in UsersTable
if (this.$refs.modal) this.$refs.modal.setHide()
},
unlinkOpenID() {
const payload = {
message: 'Are you sure you want to unlink this user from OpenID?',
callback: (confirmed) => {
if (confirmed) {
this.unlinkingFromOpenID = true
this.$axios
.$patch(`/api/users/${this.account.id}/openid-unlink`)
.then(() => {
this.$toast.success('User unlinked from OpenID')
this.show = false
})
.catch((error) => {
console.error('Failed to unlink user from OpenID', error)
this.$toast.error('Failed to unlink user from OpenID')
})
.finally(() => {
this.unlinkingFromOpenID = false
})
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
accessAllTagsToggled(val) {
if (val) {
if (this.newUser.itemTagsSelected?.length) {

View File

@@ -0,0 +1,105 @@
<template>
<modals-modal ref="modal" v-model="show" name="custom-metadata-provider" :width="600" :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">Add custom metadata provider</p>
</div>
</template>
<form @submit.prevent="submitForm">
<div class="px-4 w-full flex items-center 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 mb-2">
<div class="w-3/4 p-1">
<ui-text-input-with-label v-model="newName" :label="$strings.LabelName" />
</div>
<div class="w-1/4 p-1">
<ui-text-input-with-label value="Book" readonly :label="$strings.LabelMediaType" />
</div>
</div>
<div class="w-full mb-2 p-1">
<ui-text-input-with-label v-model="newUrl" label="URL" />
</div>
<div class="w-full mb-2 p-1">
<ui-text-input-with-label v-model="newAuthHeaderValue" :label="'Authorization Header Value'" type="password" />
</div>
<div class="flex px-1 pt-4">
<div class="flex-grow" />
<ui-btn color="success" type="submit">{{ $strings.ButtonAdd }}</ui-btn>
</div>
</div>
</div>
</form>
</modals-modal>
</template>
<script>
export default {
props: {
value: Boolean
},
data() {
return {
processing: false,
newName: '',
newUrl: '',
newAuthHeaderValue: ''
}
},
watch: {
show: {
handler(newVal) {
if (newVal) {
this.init()
}
}
}
},
computed: {
show: {
get() {
return this.value
},
set(val) {
this.$emit('input', val)
}
}
},
methods: {
submitForm() {
if (!this.newName || !this.newUrl) {
this.$toast.error('Must add name and url')
return
}
this.processing = true
this.$axios
.$post('/api/custom-metadata-providers', {
name: this.newName,
url: this.newUrl,
mediaType: 'book', // Currently only supporting book mediaType
authHeaderValue: this.newAuthHeaderValue
})
.then((data) => {
this.$emit('added', data.provider)
this.$toast.success('New provider added')
this.show = false
})
.catch((error) => {
const errorMsg = error.response?.data || 'Unknown error'
console.error('Failed to add provider', error)
this.$toast.error('Failed to add provider: ' + errorMsg)
})
.finally(() => {
this.processing = false
})
},
init() {
this.processing = false
this.newName = ''
this.newUrl = ''
this.newAuthHeaderValue = ''
}
},
mounted() {}
}
</script>

View File

@@ -34,11 +34,6 @@ export default {
data() {
return {}
},
watch: {
value(newVal) {
this.$nextTick(this.scrollToChapter)
}
},
computed: {
show: {
get() {
@@ -53,7 +48,7 @@ export default {
return this.playbackRate
},
currentChapterId() {
return this.currentChapter ? this.currentChapter.id : null
return this.currentChapter?.id || null
},
currentChapterStart() {
return (this.currentChapter?.start || 0) / this._playbackRate
@@ -74,6 +69,11 @@ export default {
}
}
}
},
updated() {
if (this.value) {
this.$nextTick(this.scrollToChapter)
}
}
}
</script>

View File

@@ -49,8 +49,8 @@
</div>
<div v-if="media.coverPath">
<p class="text-center text-gray-200">Current</p>
<a :href="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, null, true)" target="_blank" class="bg-primary">
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId, null, true)" :width="100" :book-cover-aspect-ratio="bookCoverAspectRatio" />
<a :href="$store.getters['globals/getLibraryItemCoverSrc'](libraryItem, null, true)" target="_blank" class="bg-primary">
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrc'](libraryItem, null, true)" :width="100" :book-cover-aspect-ratio="bookCoverAspectRatio" />
</a>
</div>
</div>
@@ -328,10 +328,22 @@ export default {
console.error('PersistProvider', error)
}
},
getDefaultBookProvider() {
let provider = localStorage.getItem('book-provider')
if (!provider) return 'google'
// Validate book provider
if (!this.$store.getters['scanners/checkBookProviderExists'](provider)) {
console.error('Stored book provider does not exist', provider)
localStorage.removeItem('book-provider')
return 'google'
}
return provider
},
getSearchQuery() {
if (this.isPodcast) return `term=${encodeURIComponent(this.searchTitle)}`
var searchQuery = `provider=${this.provider}&fallbackTitleOnly=1&title=${encodeURIComponent(this.searchTitle)}`
if (this.searchAuthor) searchQuery += `&author=${encodeURIComponent(this.searchAuthor)}`
if (this.libraryItemId) searchQuery += `&id=${this.libraryItemId}`
return searchQuery
},
submitSearch() {
@@ -433,7 +445,9 @@ 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 = localStorage.getItem('book-provider') || 'google'
else {
this.provider = this.getDefaultBookProvider()
}
// Prefer using ASIN if set and using audible provider
if (this.provider.startsWith('audible') && this.libraryItem.media.metadata.asin) {
@@ -532,24 +546,11 @@ export default {
// Persist in local storage
localStorage.setItem('selectedMatchUsage', JSON.stringify(this.selectedMatchUsage))
if (updatePayload.metadata.cover) {
const coverPayload = {
url: updatePayload.metadata.cover
}
const success = await this.$axios.$post(`/api/items/${this.libraryItemId}/cover`, coverPayload).catch((error) => {
console.error('Failed to update', error)
return false
})
if (success) {
this.$toast.success(this.$strings.ToastItemCoverUpdateSuccess)
} else {
this.$toast.error(this.$strings.ToastItemCoverUpdateFailed)
}
console.log('Updated cover')
delete updatePayload.metadata.cover
}
if (Object.keys(updatePayload).length) {
if (updatePayload.metadata.cover) {
updatePayload.url = updatePayload.metadata.cover
delete updatePayload.metadata.cover
}
const mediaUpdatePayload = updatePayload
const updateResult = await this.$axios.$patch(`/api/items/${this.libraryItemId}/media`, mediaUpdatePayload).catch((error) => {
console.error('Failed to update', error)

View File

@@ -20,7 +20,7 @@
</ui-tooltip>
</div>
<div v-if="enableAutoDownloadEpisodes" class="flex items-center py-2">
<ui-text-input ref="maxEpisodesInput" type="number" v-model="newMaxNewEpisodesToDownload" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updateMaxNewEpisodesToDownload" />
<ui-text-input ref="maxEpisodesToDownloadInput" type="number" v-model="newMaxNewEpisodesToDownload" no-spinner :padding-x="1" text-center class="w-10 text-base" @change="updateMaxNewEpisodesToDownload" />
<ui-tooltip text="Value of 0 sets no max limit. When checking for new episodes this is the max number of episodes that will be downloaded.">
<p class="pl-4 text-base">
Max new episodes to download per check
@@ -129,9 +129,12 @@ export default {
return
}
}
if (this.$refs.maxEpisodesInput && this.$refs.maxEpisodesInput.isFocused) {
if (this.$refs.maxEpisodesInput?.isFocused) {
this.$refs.maxEpisodesInput.blur()
return
}
if (this.$refs.maxEpisodesToDownloadInput?.isFocused) {
this.$refs.maxEpisodesToDownloadInput.blur()
}
const updatePayload = {
@@ -140,9 +143,11 @@ export default {
if (this.enableAutoDownloadEpisodes) {
updatePayload.autoDownloadSchedule = this.cronExpression
}
this.newMaxEpisodesToKeep = Number(this.newMaxEpisodesToKeep)
if (this.newMaxEpisodesToKeep !== this.maxEpisodesToKeep) {
updatePayload.maxEpisodesToKeep = this.newMaxEpisodesToKeep
}
this.newMaxNewEpisodesToDownload = Number(this.newMaxNewEpisodesToDownload)
if (this.newMaxNewEpisodesToDownload !== this.maxNewEpisodesToDownload) {
updatePayload.maxNewEpisodesToDownload = this.newMaxNewEpisodesToDownload
}

View File

@@ -2,8 +2,11 @@
<div class="w-full h-full overflow-hidden overflow-y-auto px-4 py-6">
<p class="text-xl font-semibold mb-2">{{ $strings.HeaderAudiobookTools }}</p>
<!-- alert for windows install -->
<widgets-alert v-if="isWindowsInstall" type="warning" class="my-8 text-base">Not supported for the Windows install yet</widgets-alert>
<!-- Merge to m4b -->
<div v-if="showM4bDownload" class="w-full border border-black-200 p-4 my-8">
<div v-if="showM4bDownload && !isWindowsInstall" class="w-full border border-black-200 p-4 my-8">
<div class="flex flex-wrap items-center">
<div>
<p class="text-lg">{{ $strings.LabelToolsMakeM4b }}</p>
@@ -19,22 +22,8 @@
</div>
</div>
<!-- Split to mp3 -->
<!-- <div v-if="showMp3Split" class="w-full border border-black-200 p-4 my-8">
<div class="flex items-center">
<div>
<p class="text-lg">{{ $strings.LabelToolsSplitM4b }}</p>
<p class="max-w-sm text-sm pt-2 text-gray-300">{{ $strings.LabelToolsSplitM4bDescription }}</p>
</div>
<div class="flex-grow" />
<div>
<ui-btn :disabled="true">{{ $strings.MessageNotYetImplemented }}</ui-btn>
</div>
</div>
</div> -->
<!-- Embed Metadata -->
<div v-if="mediaTracks.length" class="w-full border border-black-200 p-4 my-8">
<div v-if="mediaTracks.length && !isWindowsInstall" class="w-full border border-black-200 p-4 my-8">
<div class="flex items-center">
<div>
<p class="text-lg">{{ $strings.LabelToolsEmbedMetadata }}</p>
@@ -122,6 +111,12 @@ export default {
},
isEncodeTaskRunning() {
return this.encodeTask && !this.encodeTask?.isFinished
},
isWindowsInstall() {
return this.Source == 'windows'
},
Source() {
return this.$store.state.Source
}
},
methods: {

View File

@@ -31,7 +31,7 @@
<ui-btn class="w-full mt-2" color="primary" @click="browseForFolder">{{ $strings.ButtonBrowseForFolder }}</ui-btn>
</div>
</div>
<modals-libraries-folder-chooser v-else :paths="folderPaths" @back="showDirectoryPicker = false" @select="selectFolder" />
<modals-libraries-lazy-folder-chooser v-else :paths="folderPaths" @back="showDirectoryPicker = false" @select="selectFolder" />
</div>
</template>

View File

@@ -127,6 +127,7 @@ export default {
skipMatchingMediaWithIsbn: false,
autoScanCronExpression: null,
hideSingleBookSeries: false,
onlyShowLaterBooksInContinueSeries: false,
metadataPrecedence: ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
}
}

View File

@@ -4,35 +4,37 @@
<span class="material-icons text-3xl cursor-pointer hover:text-gray-300" @click="$emit('back')">arrow_back</span>
<p class="px-4 text-xl">{{ $strings.HeaderChooseAFolder }}</p>
</div>
<div v-if="allFolders.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2">
<p class="font-mono truncate">{{ selectedPath || '\\' }}</p>
<div v-if="rootDirs.length" class="w-full bg-primary bg-opacity-70 py-1 px-4 mb-2">
<p class="font-mono truncate">{{ selectedPath || '/' }}</p>
</div>
<div v-if="allFolders.length" class="flex bg-primary bg-opacity-50 p-4 folder-container">
<div v-if="rootDirs.length" class="relative flex bg-primary bg-opacity-50 p-4 folder-container">
<div class="w-1/2 border-r border-bg h-full overflow-y-auto">
<div v-if="level > 0" class="w-full p-1 cursor-pointer flex items-center" @click="goBack">
<div v-if="level > 0" class="w-full p-1 cursor-pointer flex items-center hover:bg-white/10" @click="goBack">
<span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
<p class="text-base font-mono px-2">..</p>
</div>
<div v-for="dir in _directories" :key="dir.path" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200" :class="dir.className" @click="selectDir(dir)">
<div v-for="dir in _directories" :key="dir.path" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" :class="dir.className" @click="selectDir(dir)">
<span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
<p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p>
<span v-if="dir.dirs && dir.dirs.length && dir.path === selectedPath" class="material-icons" style="font-size: 1.1rem">arrow_right</span>
<span v-if="dir.path === selectedPath" class="material-icons" style="font-size: 1.1rem">arrow_right</span>
</div>
</div>
<div class="w-1/2 h-full overflow-y-auto">
<div v-for="dir in _subdirs" :key="dir.path" :class="dir.className" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200" @click="selectSubDir(dir)">
<div v-for="dir in _subdirs" :key="dir.path" :class="dir.className" class="dir-item w-full p-1 cursor-pointer flex items-center hover:text-white text-gray-200 hover:bg-white/10" @click="selectSubDir(dir)">
<span class="material-icons bg-opacity-50 text-yellow-200" style="font-size: 1.2rem">folder</span>
<p class="text-base font-mono px-2 truncate">{{ dir.dirname }}</p>
</div>
</div>
<div v-if="loadingDirs" class="absolute inset-0 w-full h-full flex items-center justify-center bg-black/10">
<ui-loading-indicator />
</div>
</div>
<div v-else-if="loadingFolders" class="py-12 text-center">
<div v-else-if="initialLoad" class="py-12 text-center">
<p>{{ $strings.MessageLoadingFolders }}</p>
</div>
<div v-else class="py-12 text-center max-w-sm mx-auto">
<p class="text-lg mb-2">{{ $strings.MessageNoFoldersAvailable }}</p>
<p class="text-gray-300 mb-2">{{ $strings.NoteFolderPicker }}</p>
<p v-if="isDebian" class="text-red-400">{{ $strings.NoteFolderPickerDebian }}</p>
</div>
<div class="w-full py-2">
@@ -51,11 +53,12 @@ export default {
},
data() {
return {
loadingFolders: false,
allFolders: [],
initialLoad: false,
loadingDirs: false,
isPosix: true,
rootDirs: [],
directories: [],
selectedPath: '',
selectedFullPath: '',
subdirs: [],
level: 0,
currentDir: null,
@@ -89,68 +92,91 @@ export default {
...d
}
})
},
isDebian() {
return this.Source == 'debian'
},
Source() {
return this.$store.state.Source
}
},
methods: {
goBack() {
var splitPaths = this.selectedPath.split('\\').slice(1)
var prev = splitPaths.slice(0, -1).join('\\')
async goBack() {
let selPath = this.selectedPath.replace(/^\//, '')
var splitPaths = selPath.split('/')
var currDirs = this.allFolders
for (let i = 0; i < splitPaths.length; i++) {
var _dir = currDirs.find((dir) => dir.dirname === splitPaths[i])
if (_dir && _dir.path.slice(1) === prev) {
this.directories = currDirs
this.selectDir(_dir)
return
} else if (_dir) {
currDirs = _dir.dirs
}
let previousPath = ''
let lookupPath = ''
if (splitPaths.length > 2) {
lookupPath = splitPaths.slice(0, -2).join('/')
}
previousPath = splitPaths.slice(0, -1).join('/')
if (!this.isPosix) {
// For windows drives add a trailing slash. e.g. C:/
if (!this.isPosix && lookupPath.endsWith(':')) {
lookupPath += '/'
}
if (!this.isPosix && previousPath.endsWith(':')) {
previousPath += '/'
}
} else {
// Add leading slash
if (previousPath) previousPath = '/' + previousPath
if (lookupPath) lookupPath = '/' + lookupPath
}
this.level--
this.subdirs = this.directories
this.selectedPath = previousPath
this.directories = await this.fetchDirs(lookupPath, this.level)
},
selectDir(dir) {
async selectDir(dir) {
if (dir.isUsed) return
this.selectedPath = dir.path
this.selectedFullPath = dir.fullPath
this.level = dir.level
this.subdirs = dir.dirs
this.subdirs = await this.fetchDirs(dir.path, dir.level + 1)
},
selectSubDir(dir) {
async selectSubDir(dir) {
if (dir.isUsed) return
this.selectedPath = dir.path
this.selectedFullPath = dir.fullPath
this.level = dir.level
this.directories = this.subdirs
this.subdirs = dir.dirs
this.subdirs = await this.fetchDirs(dir.path, dir.level + 1)
},
selectFolder() {
if (!this.selectedPath) {
console.error('No Selected path')
return
}
if (this.paths.find((p) => p.startsWith(this.selectedFullPath))) {
if (this.paths.find((p) => p.startsWith(this.selectedPath))) {
this.$toast.error(`Oops, you cannot add a parent directory of a folder already added`)
return
}
this.$emit('select', this.selectedFullPath)
this.$emit('select', this.selectedPath)
this.selectedPath = ''
this.selectedFullPath = ''
},
fetchDirs(path, level) {
this.loadingDirs = true
return this.$axios
.$get(`/api/filesystem?path=${path}&level=${level}`)
.then((data) => {
console.log('Fetched directories', data.directories)
this.isPosix = !!data.posix
return data.directories
})
.catch((error) => {
console.error('Failed to get filesystem paths', error)
this.$toast.error('Failed to get filesystem paths')
return []
})
.finally(() => {
this.loadingDirs = false
})
},
async init() {
this.loadingFolders = true
this.allFolders = await this.$store.dispatch('libraries/loadFolders')
this.loadingFolders = false
this.initialLoad = true
this.rootDirs = await this.fetchDirs('', 0)
this.initialLoad = false
this.directories = this.allFolders
this.directories = this.rootDirs
this.subdirs = []
this.selectedPath = ''
this.selectedFullPath = ''
}
},
mounted() {

View File

@@ -63,7 +63,7 @@ export default {
},
audioMetatags: {
id: 'audioMetatags',
name: 'Audio file meta tags',
name: 'Audio file meta tags OR ebook metadata',
include: true
},
nfoFile: {

View File

@@ -49,6 +49,20 @@
</ui-tooltip>
</div>
</div>
<div v-if="isBookLibrary" class="py-3">
<div class="flex items-center">
<ui-toggle-switch v-model="onlyShowLaterBooksInContinueSeries" @input="formUpdated" />
<ui-tooltip :text="$strings.LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp">
<p class="pl-4 text-base">
{{ $strings.LabelSettingsOnlyShowLaterBooksInContinueSeries }}
<span class="material-icons icon-text text-sm">info_outlined</span>
</p>
</ui-tooltip>
</div>
</div>
<div v-if="isPodcastLibrary" class="py-3">
<ui-dropdown :label="$strings.LabelPodcastSearchRegion" v-model="podcastSearchRegion" :items="$podcastSearchRegionOptions" small class="max-w-52" @input="formUpdated" />
</div>
</div>
</template>
@@ -69,7 +83,9 @@ export default {
skipMatchingMediaWithAsin: false,
skipMatchingMediaWithIsbn: false,
audiobooksOnly: false,
hideSingleBookSeries: false
hideSingleBookSeries: false,
onlyShowLaterBooksInContinueSeries: false,
podcastSearchRegion: 'us'
}
},
computed: {
@@ -85,6 +101,9 @@ export default {
isBookLibrary() {
return this.mediaType === 'book'
},
isPodcastLibrary() {
return this.mediaType === 'podcast'
},
providers() {
if (this.mediaType === 'podcast') return this.$store.state.scanners.podcastProviders
return this.$store.state.scanners.providers
@@ -99,7 +118,9 @@ export default {
skipMatchingMediaWithAsin: !!this.skipMatchingMediaWithAsin,
skipMatchingMediaWithIsbn: !!this.skipMatchingMediaWithIsbn,
audiobooksOnly: !!this.audiobooksOnly,
hideSingleBookSeries: !!this.hideSingleBookSeries
hideSingleBookSeries: !!this.hideSingleBookSeries,
onlyShowLaterBooksInContinueSeries: !!this.onlyShowLaterBooksInContinueSeries,
podcastSearchRegion: this.podcastSearchRegion
}
}
},
@@ -113,6 +134,8 @@ export default {
this.skipMatchingMediaWithIsbn = !!this.librarySettings.skipMatchingMediaWithIsbn
this.audiobooksOnly = !!this.librarySettings.audiobooksOnly
this.hideSingleBookSeries = !!this.librarySettings.hideSingleBookSeries
this.onlyShowLaterBooksInContinueSeries = !!this.librarySettings.onlyShowLaterBooksInContinueSeries
this.podcastSearchRegion = this.librarySettings.podcastSearchRegion || 'us'
}
},
mounted() {

View File

@@ -33,7 +33,7 @@
<div class="break-words">{{ episode.title }}</div>
<widgets-podcast-type-indicator :type="episode.episodeType" />
</div>
<p v-if="episode.subtitle" class="break-words mb-1 text-sm text-gray-300 episode-subtitle">{{ episode.subtitle }}</p>
<p v-if="episode.subtitle" class="mb-1 text-sm text-gray-300 line-clamp-2">{{ episode.subtitle }}</p>
<p class="text-xs text-gray-300">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
</div>
</div>
@@ -68,7 +68,9 @@ export default {
selectAll: false,
search: null,
searchTimeout: null,
searchText: null
searchText: null,
downloadedEpisodeGuidMap: {},
downloadedEpisodeUrlMap: {}
}
},
watch: {
@@ -122,11 +124,13 @@ export default {
},
methods: {
getIsEpisodeDownloaded(episode) {
return this.itemEpisodes.some((downloadedEpisode) => {
if (episode.guid && downloadedEpisode.guid === episode.guid) return true
if (!downloadedEpisode.enclosure?.url) return false
return this.getCleanEpisodeUrl(downloadedEpisode.enclosure.url) === episode.cleanUrl
})
if (episode.guid && !!this.downloadedEpisodeGuidMap[episode.guid]) {
return true
}
if (this.downloadedEpisodeUrlMap[episode.cleanUrl]) {
return true
}
return false
},
/**
* UPDATE: As of v2.4.5 guid is used for matching existing downloaded episodes if it is found on the RSS feed.
@@ -219,6 +223,14 @@ export default {
})
},
init() {
this.downloadedEpisodeGuidMap = {}
this.downloadedEpisodeUrlMap = {}
this.itemEpisodes.forEach((episode) => {
if (episode.guid) this.downloadedEpisodeGuidMap[episode.guid] = episode.id
if (episode.enclosure?.url) this.downloadedEpisodeUrlMap[this.getCleanEpisodeUrl(episode.enclosure.url)] = episode.id
})
this.episodesCleaned = this.episodes
.filter((ep) => ep.enclosure?.url)
.map((_ep) => {

View File

@@ -19,7 +19,7 @@
<div class="w-full p-1">
<ui-textarea-with-label v-model="newEpisode.subtitle" :label="$strings.LabelSubtitle" :rows="3" />
</div>
<div class="w-full p-1 default-style">
<div class="w-full p-1">
<ui-rich-text-editor :label="$strings.LabelDescription" v-model="newEpisode.description" />
</div>
</div>

View File

@@ -18,7 +18,7 @@
<div v-for="(episode, index) in episodesFound" :key="index" class="w-full py-4 border-b border-white border-opacity-5 hover:bg-gray-300 hover:bg-opacity-10 cursor-pointer px-2" @click.stop="selectEpisode(episode)">
<p v-if="episode.episode" class="font-semibold text-gray-200">#{{ episode.episode }}</p>
<p class="break-words mb-1">{{ episode.title }}</p>
<p v-if="episode.subtitle" class="break-words mb-1 text-sm text-gray-300 episode-subtitle">{{ episode.subtitle }}</p>
<p v-if="episode.subtitle" class="mb-1 text-sm text-gray-300 line-clamp-2">{{ episode.subtitle }}</p>
<p class="text-xs text-gray-400">Published {{ episode.publishedAt ? $dateDistanceFromNow(episode.publishedAt) : 'Unknown' }}</p>
</div>
</template>

View File

@@ -2,21 +2,21 @@
<div class="flex pt-4 pb-2 md:pt-0 md:pb-2">
<div class="flex-grow" />
<template v-if="!loading">
<div class="cursor-pointer flex items-center justify-center text-gray-300 mr-4 md:mr-8" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter">
<button :aria-label="$strings.ButtonPreviousChapter" class="flex items-center justify-center text-gray-300 mr-4 md:mr-8" @mousedown.prevent @mouseup.prevent @click.stop="prevChapter">
<span class="material-icons text-2xl sm:text-3xl">first_page</span>
</div>
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
</button>
<button :aria-label="$strings.ButtonJumpBackward" class="flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpBackward">
<span class="material-icons text-2xl sm:text-3xl">replay_10</span>
</div>
<div class="cursor-pointer p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 md:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
</button>
<button :aria-label="paused ? $strings.ButtonPlay : $strings.ButtonPause" class="p-2 shadow-sm bg-accent flex items-center justify-center rounded-full text-primary mx-4 md:mx-8" :class="seekLoading ? 'animate-spin' : ''" @mousedown.prevent @mouseup.prevent @click.stop="playPause">
<span class="material-icons text-2xl">{{ seekLoading ? 'autorenew' : paused ? 'play_arrow' : 'pause' }}</span>
</div>
<div class="cursor-pointer flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
</button>
<button :aria-label="$strings.ButtonJumpForward" class="flex items-center justify-center text-gray-300" @mousedown.prevent @mouseup.prevent @click.stop="jumpForward">
<span class="material-icons text-2xl sm:text-3xl">forward_10</span>
</div>
<div class="flex items-center justify-center ml-4 md:ml-8" :class="hasNextChapter ? 'text-gray-300 cursor-pointer' : 'text-gray-500'" @mousedown.prevent @mouseup.prevent @click.stop="nextChapter">
</button>
<button :aria-label="$strings.ButtonNextChapter" class="flex items-center justify-center ml-4 md:ml-8" :disabled="!hasNextChapter" :class="hasNextChapter ? 'text-gray-300' : 'text-gray-500'" @mousedown.prevent @mouseup.prevent @click.stop="nextChapter">
<span class="material-icons text-2xl sm:text-3xl">last_page</span>
</div>
</button>
<controls-playback-speed-control v-model="playbackRateInput" @input="playbackRateUpdated" @change="playbackRateChanged" />
</template>
<template v-else>

View File

@@ -57,7 +57,6 @@ export default {
},
watch: {
duration: {
immediate: true,
handler() {
this.setChapterTicks()
}
@@ -205,10 +204,14 @@ export default {
},
windowResize() {
this.setTrackWidth()
this.setChapterTicks()
this.updatePlayedTrackWidth()
this.updateBufferTrack()
}
},
mounted() {
this.setTrackWidth()
this.setChapterTicks()
window.addEventListener('resize', this.windowResize)
},
beforeDestroy() {

View File

@@ -9,37 +9,37 @@
</ui-tooltip>
<ui-tooltip direction="top" :text="$strings.LabelSleepTimer">
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')">
<button :aria-label="$strings.LabelSleepTimer" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showSleepTimer')">
<span v-if="!sleepTimerSet" class="material-icons text-2xl">snooze</span>
<div v-else class="flex items-center">
<span class="material-icons text-lg text-warning">snooze</span>
<p class="text-xl text-warning font-mono font-semibold text-center px-0.5 pb-0.5" style="min-width: 30px">{{ sleepTimerRemainingString }}</p>
</div>
</div>
</button>
</ui-tooltip>
<ui-tooltip v-if="!isPodcast" direction="top" :text="$strings.LabelViewBookmarks">
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')">
<button :aria-label="$strings.LabelViewBookmarks" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showBookmarks')">
<span class="material-icons text-2xl">{{ bookmarks.length ? 'bookmarks' : 'bookmark_border' }}</span>
</div>
</button>
</ui-tooltip>
<ui-tooltip v-if="chapters.length" direction="top" :text="$strings.LabelViewChapters">
<div class="cursor-pointer text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
<button :aria-label="$strings.LabelViewChapters" class="text-gray-300 hover:text-white mx-1 lg:mx-2" @mousedown.prevent @mouseup.prevent @click.stop="showChapters">
<span class="material-icons text-2xl">format_list_bulleted</span>
</div>
</button>
</ui-tooltip>
<ui-tooltip v-if="playerQueueItems.length" direction="top" :text="$strings.LabelViewQueue">
<button class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')">
<button :aria-label="$strings.LabelViewQueue" class="outline-none text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="$emit('showPlayerQueueItems')">
<span class="material-icons text-2.5xl sm:text-3xl">playlist_play</span>
</button>
</ui-tooltip>
<ui-tooltip v-if="chapters.length" direction="top" :text="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack">
<div class="cursor-pointer text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="setUseChapterTrack">
<button :aria-label="useChapterTrack ? $strings.LabelUseFullTrack : $strings.LabelUseChapterTrack" class="text-gray-300 mx-1 lg:mx-2 hover:text-white" @mousedown.prevent @mouseup.prevent @click.stop="setUseChapterTrack">
<span class="material-icons text-2xl sm:text-3xl transform transition-transform" :class="useChapterTrack ? 'rotate-180' : ''">timelapse</span>
</div>
</button>
</ui-tooltip>
</div>

View File

@@ -1,7 +1,7 @@
<template>
<div class="w-full h-full">
<div v-show="showPageMenu" v-click-outside="clickOutside" class="pagemenu absolute top-9 left-8 rounded-md overflow-y-auto bg-bg shadow-lg z-20 border border-gray-400" :style="{ width: pageMenuWidth + 'px' }">
<div v-for="(file, index) in cleanedPageNames" :key="file" class="w-full cursor-pointer hover:bg-black-200 px-2 py-1" :class="page === index ? 'bg-black-200' : ''" @click="setPage(index + 1)">
<div v-for="(file, index) in cleanedPageNames" :key="file" class="w-full cursor-pointer hover:bg-black-200 px-2 py-1" :class="page === index + 1 ? 'bg-black-200' : ''" @click="setPage(index + 1)">
<p class="text-sm truncate">{{ file }}</p>
</div>
</div>
@@ -14,34 +14,40 @@
</div>
</div>
<a v-if="pages && numPages" :href="mainImg" :download="pages[page - 1]" class="absolute top-0 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" :class="comicMetadata ? 'left-32' : 'left-20'">
<span class="material-icons text-xl">download</span>
</a>
<div v-if="comicMetadata" class="absolute top-0 left-20 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowInfoMenu">
<span class="material-icons text-xl">more</span>
</div>
<div v-if="numPages" class="absolute top-0 left-8 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowPageMenu">
<div v-if="numPages" class="absolute top-0 left-4 sm:left-8 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowPageMenu">
<span class="material-icons text-xl">menu</span>
</div>
<div v-if="numPages" class="absolute top-0 right-16 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20">
<div v-if="comicMetadata" class="absolute top-0 left-16 sm:left-20 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" @mousedown.prevent @click.stop.prevent="clickShowInfoMenu">
<span class="material-icons text-xl">more</span>
</div>
<a v-if="pages && numPages" :href="mainImg" :download="pages[page - 1]" class="absolute top-0 bg-bg text-gray-100 border-b border-l border-r border-gray-400 hover:bg-black-200 cursor-pointer rounded-b-md w-10 h-9 flex items-center justify-center text-center z-20" :class="comicMetadata ? 'left-28 sm:left-32' : 'left-16 sm:left-20'">
<span class="material-icons text-xl">download</span>
</a>
<div v-if="numPages" class="absolute top-0 right-14 sm:right-16 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20">
<p class="font-mono">{{ page }} / {{ numPages }}</p>
</div>
<div v-if="mainImg" class="absolute top-0 right-36 sm:right-40 bg-bg text-gray-100 border-b border-l border-r border-gray-400 rounded-b-md px-2 h-9 flex items-center text-center z-20">
<ui-icon-btn icon="zoom_out" :size="8" :disabled="!canScaleDown" borderless class="mr-px" @click="zoomOut" />
<ui-icon-btn icon="zoom_in" :size="8" :disabled="!canScaleUp" borderless class="ml-px" @click="zoomIn" />
</div>
<div class="overflow-hidden w-full h-full relative">
<div v-show="canGoPrev" class="absolute top-0 left-0 h-full w-1/2 lg:w-1/3 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent>
<div class="w-full h-full relative">
<div v-show="canGoPrev" ref="prevButton" class="absolute top-0 left-0 h-full w-1/2 lg:w-1/3 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="prev" @mousedown.prevent>
<div class="flex items-center justify-center h-full w-1/2">
<span v-show="loadedFirstPage" class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_back_ios</span>
</div>
</div>
<div v-show="canGoNext" class="absolute top-0 right-0 h-full w-1/2 lg:w-1/3 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="next" @mousedown.prevent>
<div v-show="canGoNext" ref="nextButton" class="absolute top-0 right-0 h-full w-1/2 lg:w-1/3 hover:opacity-100 opacity-0 z-10 cursor-pointer" @click.stop.prevent="next" @mousedown.prevent>
<div class="flex items-center justify-center h-full w-1/2 ml-auto">
<span v-show="loadedFirstPage" class="material-icons text-5xl text-white cursor-pointer text-opacity-30 hover:text-opacity-90">arrow_forward_ios</span>
</div>
</div>
<div class="h-full flex justify-center">
<img v-if="mainImg" :src="mainImg" class="object-contain h-full m-auto" />
<div ref="imageContainer" class="w-full h-full relative overflow-auto">
<div class="h-full flex" :class="scale > 100 ? '' : 'justify-center'">
<img v-if="mainImg" :style="{ minWidth: scale + '%', width: scale + '%' }" :src="mainImg" class="object-contain m-auto" />
</div>
</div>
<div v-show="loading" class="w-full h-full absolute top-0 left-0 flex items-center justify-center z-10">
<ui-loading-indicator />
</div>
@@ -54,6 +60,10 @@ import Path from 'path'
import { Archive } from 'libarchive.js/main.js'
import { CompressedFile } from 'libarchive.js/src/compressed-file'
// This is % with respect to the screen width
const MAX_SCALE = 400
const MIN_SCALE = 10
Archive.init({
workerUrl: '/libarchive/worker-bundle.js'
})
@@ -81,7 +91,8 @@ export default {
showInfoMenu: false,
loadTimeout: null,
loadedFirstPage: false,
comicMetadata: null
comicMetadata: null,
scale: 80
}
},
watch: {
@@ -136,6 +147,12 @@ export default {
return p
}) || []
)
},
canScaleUp() {
return this.scale < MAX_SCALE
},
canScaleDown() {
return this.scale > MIN_SCALE
}
},
methods: {
@@ -331,10 +348,37 @@ export default {
orderedImages = orderedImages.concat(noNumImages.map((i) => i.filename))
this.pages = orderedImages
},
zoomIn() {
this.scale += 10
},
zoomOut() {
this.scale -= 10
},
scroll(event) {
const imageContainer = this.$refs.imageContainer
imageContainer.scrollBy({
top: event.deltaY,
left: event.deltaX,
behavior: 'auto'
})
}
},
mounted() {},
beforeDestroy() {}
mounted() {
const prevButton = this.$refs.prevButton
const nextButton = this.$refs.nextButton
prevButton.addEventListener('wheel', this.scroll, { passive: false })
nextButton.addEventListener('wheel', this.scroll, { passive: false })
},
beforeDestroy() {
const prevButton = this.$refs.prevButton
const nextButton = this.$refs.nextButton
prevButton.removeEventListener('wheel', this.scroll, { passive: false })
nextButton.removeEventListener('wheel', this.scroll, { passive: false })
}
}
</script>

View File

@@ -316,6 +316,7 @@ export default {
reader.rendition = reader.book.renderTo('viewer', {
width: this.readerWidth,
height: this.readerHeight * 0.8,
allowScriptedContent: true,
spread: 'auto',
snap: true,
manager: 'continuous',

View File

@@ -0,0 +1,285 @@
<template>
<div>
<div v-if="processing" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
<widgets-loading-spinner />
</div>
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" />
</div>
</template>
<script>
export default {
props: {
variant: {
type: Number,
default: 0
},
year: Number,
processing: Boolean
},
data() {
return {
canvas: null,
dataUrl: null,
yearStats: null
}
},
watch: {
variant() {
this.init()
}
},
methods: {
async initCanvas() {
if (!this.yearStats) return
const canvas = document.createElement('canvas')
canvas.width = 800
canvas.height = 800
const ctx = canvas.getContext('2d')
const createRoundedRect = (x, y, w, h) => {
const grd1 = ctx.createLinearGradient(x, y, x + w, y + h)
grd1.addColorStop(0, '#44444455')
grd1.addColorStop(1, '#ffffff11')
ctx.fillStyle = grd1
ctx.strokeStyle = '#C0C0C088'
ctx.beginPath()
ctx.roundRect(x, y, w, h, [20])
ctx.fill()
ctx.stroke()
}
const addText = (text, fontSize, fontWeight, color, letterSpacing, x, y, maxWidth = 0) => {
ctx.fillStyle = color
ctx.font = `${fontWeight} ${fontSize} Source Sans Pro`
ctx.letterSpacing = letterSpacing
// If maxWidth is specified then continue to remove chars until under maxWidth and add ellipsis
if (maxWidth) {
let txtWidth = ctx.measureText(text).width
while (txtWidth > maxWidth) {
console.warn(`Text "${text}" is greater than max width ${maxWidth} (width:${txtWidth})`)
if (text.endsWith('...')) text = text.slice(0, -4) // Repeated checks remove 1 char at a time
else text = text.slice(0, -3) // First check remove last 3 chars
text += '...'
txtWidth = ctx.measureText(text).width
console.log(`Checking text "${text}" (width:${txtWidth})`)
}
}
ctx.fillText(text, x, y)
}
const addIcon = (icon, color, fontSize, x, y) => {
ctx.fillStyle = color
ctx.font = `${fontSize} Material Icons Outlined`
ctx.fillText(icon, x, y)
}
// Bg color
ctx.fillStyle = '#232323'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Cover image tiles
const bookCovers = this.yearStats.finishedBooksWithCovers
bookCovers.push(...this.yearStats.booksWithCovers)
let finishedBookCoverImgs = {}
if (bookCovers.length) {
let index = 0
ctx.globalAlpha = 0.25
ctx.save()
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.rotate((-Math.PI / 180) * 25)
ctx.translate(-canvas.width / 2, -canvas.height / 2)
ctx.translate(-130, -120)
for (let x = 0; x < 5; x++) {
for (let y = 0; y < 5; y++) {
const coverIndex = index % bookCovers.length
let libraryItemId = bookCovers[coverIndex]
index++
await new Promise((resolve) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.addEventListener('load', () => {
let sw = img.width
if (img.width > img.height) {
sw = img.height
}
let sx = -(sw - img.width) / 2
let sy = -(sw - img.height) / 2
ctx.drawImage(img, sx, sy, sw, sw, 215 * x, 215 * y, 215, 215)
resolve()
if (this.yearStats.finishedBooksWithCovers.includes(libraryItemId) && !finishedBookCoverImgs[libraryItemId]) {
finishedBookCoverImgs[libraryItemId] = {
img,
sx,
sy,
sw
}
}
})
img.addEventListener('error', () => {
resolve()
})
img.src = this.$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId)
})
}
}
ctx.restore()
}
ctx.globalAlpha = 1
ctx.textBaseline = 'middle'
// Create gradient
const grd1 = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
grd1.addColorStop(0, '#000000aa')
grd1.addColorStop(1, '#cd9d49aa')
ctx.fillStyle = grd1
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Top Abs icon
let tanColor = '#ffdb70'
ctx.fillStyle = tanColor
ctx.font = '42px absicons'
ctx.fillText('\ue900', 15, 36)
// Top text
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51)
// Top left box
createRoundedRect(50, 100, 340, 160)
addText(this.yearStats.numBooksFinished, '64px', 'bold', 'white', '0px', 160, 165)
addText('books finished', '28px', 'normal', tanColor, '0px', 160, 210)
const readIconPath = new Path2D()
readIconPath.addPath(new Path2D('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'), { a: 2, d: 2, e: 100, f: 160 })
ctx.fillStyle = '#ffffff'
ctx.fill(readIconPath)
// Box top right
createRoundedRect(410, 100, 340, 160)
addText(this.$elapsedPrettyExtended(this.yearStats.totalListeningTime, true, false), '40px', 'bold', 'white', '0px', 500, 165)
addText('spent listening', '28px', 'normal', tanColor, '0px', 500, 205)
addIcon('watch_later', 'white', '52px', 440, 180)
// Box bottom left
createRoundedRect(50, 280, 340, 160)
addText(this.yearStats.totalListeningSessions, '64px', 'bold', 'white', '0px', 160, 345)
addText('sessions', '28px', 'normal', tanColor, '1px', 160, 390)
addIcon('headphones', 'white', '52px', 95, 360)
// Box bottom right
createRoundedRect(410, 280, 340, 160)
addText(this.yearStats.numBooksListened, '64px', 'bold', 'white', '0px', 500, 345)
addText('books listened to', '28px', 'normal', tanColor, '0px', 500, 390)
addIcon('local_library', 'white', '52px', 440, 360)
if (!this.variant) {
// Text stats
const topNarrator = this.yearStats.mostListenedNarrator
if (topNarrator) {
addText('TOP NARRATOR', '24px', 'normal', tanColor, '1px', 70, 520)
addText(topNarrator.name, '36px', 'bolder', 'white', '0px', 70, 564, 330)
addText(this.$elapsedPrettyExtended(topNarrator.time, true, false), '24px', 'lighter', 'white', '1px', 70, 599)
}
const topGenre = this.yearStats.topGenres[0]
if (topGenre) {
addText('TOP GENRE', '24px', 'normal', tanColor, '1px', 430, 520)
addText(topGenre.genre, '36px', 'bolder', 'white', '0px', 430, 564, 330)
addText(this.$elapsedPrettyExtended(topGenre.time, true, false), '24px', 'lighter', 'white', '1px', 430, 599)
}
const topAuthor = this.yearStats.topAuthors[0]
if (topAuthor) {
addText('TOP AUTHOR', '24px', 'normal', tanColor, '1px', 70, 670)
addText(topAuthor.name, '36px', 'bolder', 'white', '0px', 70, 714, 330)
addText(this.$elapsedPrettyExtended(topAuthor.time, true, false), '24px', 'lighter', 'white', '1px', 70, 749)
}
if (this.yearStats.mostListenedMonth?.time) {
const jsdate = new Date(this.year, this.yearStats.mostListenedMonth.month, 1)
const monthName = this.$formatJsDate(jsdate, 'LLLL')
addText('TOP MONTH', '24px', 'normal', tanColor, '1px', 430, 670)
addText(monthName, '36px', 'bolder', 'white', '0px', 430, 714, 330)
addText(this.$elapsedPrettyExtended(this.yearStats.mostListenedMonth.time, true, false), '24px', 'lighter', 'white', '1px', 430, 749)
}
} else if (this.variant === 1) {
// Bottom images
finishedBookCoverImgs = Object.values(finishedBookCoverImgs)
if (finishedBookCoverImgs.length > 0) {
ctx.textAlign = 'center'
addText('Some books finished this year...', '28px', 'normal', tanColor, '0px', canvas.width / 2, 530)
for (let i = 0; i < Math.min(5, finishedBookCoverImgs.length); i++) {
let imgToAdd = finishedBookCoverImgs[i]
ctx.drawImage(imgToAdd.img, imgToAdd.sx, imgToAdd.sy, imgToAdd.sw, imgToAdd.sw, 40 + 145 * i, 570, 140, 140)
}
}
} else if (this.variant === 2) {
// Text stats
if (this.yearStats.topAuthors.length) {
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 524)
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 584 + i * 60, 330)
}
}
if (this.yearStats.topGenres.length) {
addText('TOP GENRES', '24px', 'normal', tanColor, '1px', 430, 524)
for (let i = 0; i < this.yearStats.topGenres.length; i++) {
addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 584 + i * 60, 330)
}
}
}
this.canvas = canvas
this.dataUrl = canvas.toDataURL('png')
},
refresh() {
this.init()
},
share() {
this.canvas.toBlob((blob) => {
const file = new File([blob], 'yearinreview.png', { type: blob.type })
const shareData = {
files: [file]
}
if (navigator.canShare(shareData)) {
navigator
.share(shareData)
.then(() => {
console.log('Share success')
})
.catch((error) => {
console.error('Failed to share', error)
if (error.name !== 'AbortError') {
this.$toast.error('Failed to share: ' + error.message)
}
})
} else {
this.$toast.error('Cannot share natively on this device')
}
})
},
async init() {
this.$emit('update:processing', true)
this.yearStats = await this.$axios.$get(`/api/me/stats/year/${this.year}`).catch((err) => {
console.error('Failed to load stats for year', err)
this.$toast.error('Failed to load year stats')
return null
})
await this.initCanvas()
this.$emit('update:processing', false)
}
},
mounted() {
this.init()
}
}
</script>

View File

@@ -0,0 +1,146 @@
<template>
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-1 sm:p-4 mb-4">
<!-- hack to get icon fonts loaded on init -->
<div class="h-0 w-0 overflow-hidden opacity-0">
<span class="material-icons-outlined">close</span>
<span class="abs-icons icon-audiobookshelf" />
</div>
<div class="flex items-center">
<p class="hidden md:block text-xl font-semibold">{{ $getString('HeaderYearReview', [yearInReviewYear]) }}</p>
<div class="hidden md:block flex-grow" />
<ui-btn class="w-full md:w-auto" @click.stop="clickShowYearInReview">{{ showYearInReview ? $strings.LabelYearReviewHide :
$strings.LabelYearReviewShow }}</ui-btn>
</div>
<!-- your year in review -->
<div v-if="showYearInReview">
<div class="w-full h-px bg-slate-200/10 my-4" />
<div class="flex items-center justify-center mb-2 max-w-[800px] mx-auto">
<!-- previous button -->
<ui-btn small :disabled="!yearInReviewVariant || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant--">
<span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
<span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
</ui-btn>
<!-- share button -->
<ui-btn v-if="showShareButton" small :disabled="processingYearInReview" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReview">{{
$strings.ButtonShare }}
</ui-btn>
<div class="flex-grow" />
<p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelPersonalYearReview', [yearInReviewVariant + 1]) }}
</p>
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewVariant + 1 }}</p>
<div class="flex-grow" />
<!-- refresh button -->
<ui-btn small :disabled="processingYearInReview" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReview">
<span class="hidden sm:inline-block">{{ $strings.ButtonRefresh }}</span>
<span class="material-icons sm:!hidden text-lg py-px">refresh</span>
</ui-btn>
<!-- next button -->
<ui-btn small :disabled="yearInReviewVariant >= 2 || processingYearInReview" class="inline-flex items-center font-semibold" @click="yearInReviewVariant++">
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
<span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
</ui-btn>
</div>
<stats-year-in-review ref="yearInReview" :variant="yearInReviewVariant" :year="yearInReviewYear" :processing.sync="processingYearInReview" />
<!-- your year in review short -->
<div class="w-full max-w-[800px] mx-auto my-4">
<!-- share button -->
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewShort" class="inline-flex sm:hidden items-center font-semibold mb-1" @click="shareYearInReviewShort">{{ $strings.ButtonShare }}</ui-btn>
<stats-year-in-review-short ref="yearInReviewShort" :year="yearInReviewYear" :processing.sync="processingYearInReviewShort" />
</div>
<!-- your server in review -->
<div v-if="isAdminOrUp" class="w-full max-w-[800px] mx-auto mb-2 mt-4 border-t pt-4 border-white/10">
<div class="flex items-center justify-center mb-2">
<!-- previous button -->
<ui-btn small :disabled="!yearInReviewServerVariant || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant--">
<span class="material-icons text-lg sm:pr-1 py-px sm:py-0">chevron_left</span>
<span class="hidden sm:inline-block pr-2">{{ $strings.ButtonPrevious }}</span>
</ui-btn>
<!-- share button -->
<ui-btn v-if="showShareButton" small :disabled="processingYearInReviewServer" class="inline-flex sm:hidden items-center font-semibold ml-1 sm:ml-2" @click="shareYearInReviewServer">{{ $strings.ButtonShare }}
</ui-btn>
<div class="flex-grow" />
<p class="hidden sm:block text-lg font-semibold">{{ $getString('LabelServerYearReview', [yearInReviewServerVariant + 1]) }}</p>
<p class="block sm:hidden text-lg font-semibold">{{ yearInReviewServerVariant + 1 }}</p>
<div class="flex-grow" />
<!-- refresh button -->
<ui-btn small :disabled="processingYearInReviewServer" class="inline-flex items-center font-semibold mr-1 sm:mr-2" @click="refreshYearInReviewServer">
<span class="hidden sm:inline-block">{{ $strings.ButtonRefresh }}</span>
<span class="material-icons sm:!hidden text-lg py-px">refresh</span>
</ui-btn>
<!-- next button -->
<ui-btn small :disabled="yearInReviewServerVariant >= 2 || processingYearInReviewServer" class="inline-flex items-center font-semibold" @click="yearInReviewServerVariant++">
<span class="hidden sm:inline-block pl-2">{{ $strings.ButtonNext }}</span>
<span class="material-icons-outlined text-lg sm:pl-1 py-px sm:py-0">chevron_right</span>
</ui-btn>
</div>
</div>
<stats-year-in-review-server v-if="isAdminOrUp" ref="yearInReviewServer" :year="yearInReviewYear" :variant="yearInReviewServerVariant" :processing.sync="processingYearInReviewServer" />
</div>
</div>
</template>
<script>
export default {
data() {
return {
showYearInReview: false,
yearInReviewYear: 0,
yearInReviewVariant: 0,
yearInReviewServerVariant: 0,
processingYearInReview: false,
processingYearInReviewShort: false,
processingYearInReviewServer: false,
showShareButton: false
}
},
computed: {
isAdminOrUp() {
return this.$store.getters['user/getIsAdminOrUp']
}
},
methods: {
shareYearInReviewServer() {
this.$refs.yearInReviewServer.share()
},
shareYearInReview() {
this.$refs.yearInReview.share()
},
shareYearInReviewShort() {
this.$refs.yearInReviewShort.share()
},
refreshYearInReviewServer() {
this.$refs.yearInReviewServer.refresh()
},
refreshYearInReview() {
this.$refs.yearInReview.refresh()
this.$refs.yearInReviewShort.refresh()
},
clickShowYearInReview() {
this.showYearInReview = !this.showYearInReview
}
},
beforeMount() {
this.yearInReviewYear = new Date().getFullYear()
// When not December show previous year
if (new Date().getMonth() < 11) {
this.yearInReviewYear--
}
},
mounted() {
if (typeof navigator.share !== 'undefined' && navigator.share) {
this.showShareButton = true
} else {
console.warn('Navigator.share not supported')
}
}
}
</script>

View File

@@ -0,0 +1,264 @@
<template>
<div>
<div v-if="processing" class="max-w-[800px] h-80 md:h-[800px] mx-auto flex items-center justify-center">
<widgets-loading-spinner />
</div>
<img v-else-if="dataUrl" :src="dataUrl" class="mx-auto" />
</div>
</template>
<script>
export default {
props: {
variant: {
type: Number,
default: 0
},
processing: Boolean,
year: Number
},
data() {
return {
canvas: null,
dataUrl: null,
yearStats: null
}
},
watch: {
variant() {
this.init()
}
},
methods: {
async initCanvas() {
if (!this.yearStats) return
const canvas = document.createElement('canvas')
canvas.width = 800
canvas.height = 800
const ctx = canvas.getContext('2d')
const createRoundedRect = (x, y, w, h) => {
const grd1 = ctx.createLinearGradient(x, y, x + w, y + h)
grd1.addColorStop(0, '#44444455')
grd1.addColorStop(1, '#ffffff11')
ctx.fillStyle = grd1
ctx.strokeStyle = '#C0C0C088'
ctx.beginPath()
ctx.roundRect(x, y, w, h, [20])
ctx.fill()
ctx.stroke()
}
const addText = (text, fontSize, fontWeight, color, letterSpacing, x, y, maxWidth = 0) => {
ctx.fillStyle = color
ctx.font = `${fontWeight} ${fontSize} Source Sans Pro`
ctx.letterSpacing = letterSpacing
// If maxWidth is specified then continue to remove chars until under maxWidth and add ellipsis
if (maxWidth) {
let txtWidth = ctx.measureText(text).width
while (txtWidth > maxWidth) {
console.warn(`Text "${text}" is greater than max width ${maxWidth} (width:${txtWidth})`)
if (text.endsWith('...')) text = text.slice(0, -4) // Repeated checks remove 1 char at a time
else text = text.slice(0, -3) // First check remove last 3 chars
text += '...'
txtWidth = ctx.measureText(text).width
console.log(`Checking text "${text}" (width:${txtWidth})`)
}
}
ctx.fillText(text, x, y)
}
// Bg color
ctx.fillStyle = '#232323'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Cover image tiles
let imgsToAdd = {}
if (this.yearStats.booksAddedWithCovers.length) {
let index = 0
ctx.globalAlpha = 0.25
ctx.save()
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.rotate((-Math.PI / 180) * 25)
ctx.translate(-canvas.width / 2, -canvas.height / 2)
ctx.translate(-130, -120)
for (let x = 0; x < 5; x++) {
for (let y = 0; y < 5; y++) {
const coverIndex = index % this.yearStats.booksAddedWithCovers.length
let libraryItemId = this.yearStats.booksAddedWithCovers[coverIndex]
index++
await new Promise((resolve) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.addEventListener('load', () => {
let sw = img.width
if (img.width > img.height) {
sw = img.height
}
let sx = -(sw - img.width) / 2
let sy = -(sw - img.height) / 2
ctx.drawImage(img, sx, sy, sw, sw, 215 * x, 215 * y, 215, 215)
if (!imgsToAdd[libraryItemId]) {
imgsToAdd[libraryItemId] = {
img,
sx,
sy,
sw
}
}
resolve()
})
img.addEventListener('error', () => {
resolve()
})
img.src = this.$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId)
})
}
}
ctx.restore()
}
ctx.globalAlpha = 1
ctx.textBaseline = 'middle'
// Create gradient
const grd1 = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
grd1.addColorStop(0, '#000000aa')
grd1.addColorStop(1, '#cd9d49aa')
ctx.fillStyle = grd1
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Top Abs icon
let tanColor = '#ffdb70'
ctx.fillStyle = tanColor
ctx.font = '42px absicons'
ctx.fillText('\ue900', 15, 36)
// Top text
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51)
// Top left box
createRoundedRect(40, 100, 230, 100)
ctx.textAlign = 'center'
addText(this.yearStats.numBooksAdded, '48px', 'bold', 'white', '0px', 155, 140)
addText('books added', '18px', 'normal', tanColor, '0px', 155, 170)
// Box top right
createRoundedRect(285, 100, 230, 100)
addText(this.yearStats.numAuthorsAdded, '48px', 'bold', 'white', '0px', 400, 140)
addText('authors added', '18px', 'normal', tanColor, '0px', 400, 170)
// Box bottom left
createRoundedRect(530, 100, 230, 100)
addText(this.yearStats.numListeningSessions, '48px', 'bold', 'white', '0px', 645, 140)
addText('sessions', '18px', 'normal', tanColor, '1px', 645, 170)
// Text stats
if (this.yearStats.totalBooksAddedSize) {
addText('Your book collection grew to...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 260)
addText(this.$bytesPretty(this.yearStats.totalBooksSize), '36px', 'bolder', 'white', '0px', canvas.width / 2, 300)
addText('+' + this.$bytesPretty(this.yearStats.totalBooksAddedSize), '20px', 'lighter', 'white', '0px', canvas.width / 2, 330)
}
if (this.yearStats.totalBooksAddedDuration) {
addText('With a total duration of...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 400)
addText(this.$elapsedPrettyExtended(this.yearStats.totalBooksDuration, true, false), '36px', 'bolder', 'white', '0px', canvas.width / 2, 440)
addText('+' + this.$elapsedPrettyExtended(this.yearStats.totalBooksAddedDuration, true, false), '20px', 'lighter', 'white', '0px', canvas.width / 2, 470)
}
if (!this.variant) {
// Bottom images
imgsToAdd = Object.values(imgsToAdd)
if (imgsToAdd.length > 0) {
addText('Some additions include...', '24px', 'normal', tanColor, '0px', canvas.width / 2, 540)
for (let i = 0; i < Math.min(5, imgsToAdd.length); i++) {
let imgToAdd = imgsToAdd[i]
ctx.drawImage(imgToAdd.img, imgToAdd.sx, imgToAdd.sy, imgToAdd.sw, imgToAdd.sw, 40 + 145 * i, 580, 140, 140)
}
}
} else if (this.variant === 1) {
// Text stats
ctx.textAlign = 'left'
if (this.yearStats.topAuthors.length) {
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 549)
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330)
}
}
if (this.yearStats.topNarrators.length) {
addText('TOP NARRATORS', '24px', 'normal', tanColor, '1px', 430, 549)
for (let i = 0; i < this.yearStats.topNarrators.length; i++) {
addText(this.yearStats.topNarrators[i].name, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330)
}
}
} else if (this.variant === 2) {
// Text stats
ctx.textAlign = 'left'
if (this.yearStats.topAuthors.length) {
addText('TOP AUTHORS', '24px', 'normal', tanColor, '1px', 70, 549)
for (let i = 0; i < this.yearStats.topAuthors.length; i++) {
addText(this.yearStats.topAuthors[i].name, '36px', 'bolder', 'white', '0px', 70, 609 + i * 60, 330)
}
}
if (this.yearStats.topGenres.length) {
addText('TOP GENRES', '24px', 'normal', tanColor, '1px', 430, 549)
for (let i = 0; i < this.yearStats.topGenres.length; i++) {
addText(this.yearStats.topGenres[i].genre, '36px', 'bolder', 'white', '0px', 430, 609 + i * 60, 330)
}
}
}
this.canvas = canvas
this.dataUrl = canvas.toDataURL('png')
},
share() {
this.canvas.toBlob((blob) => {
const file = new File([blob], 'yearinreviewserver.png', { type: blob.type })
const shareData = {
files: [file]
}
if (navigator.canShare(shareData)) {
navigator
.share(shareData)
.then(() => {
console.log('Share success')
})
.catch((error) => {
console.error('Failed to share', error)
if (error.name !== 'AbortError') {
this.$toast.error('Failed to share: ' + error.message)
}
})
} else {
this.$toast.error('Cannot share natively on this device')
}
})
},
refresh() {
this.init()
},
async init() {
this.$emit('update:processing', true)
this.yearStats = await this.$axios.$get(`/api/stats/year/${this.year}`).catch((err) => {
console.error('Failed to load stats for year', err)
this.$toast.error('Failed to load year stats')
return null
})
await this.initCanvas()
this.$emit('update:processing', false)
}
},
mounted() {
this.init()
}
}
</script>

View File

@@ -0,0 +1,194 @@
<template>
<div>
<div v-if="processing" class="max-w-[600px] h-32 sm:h-[200px] flex items-center justify-center">
<widgets-loading-spinner />
</div>
<img v-else-if="dataUrl" :src="dataUrl" />
</div>
</template>
<script>
export default {
props: {
processing: Boolean,
year: Number
},
data() {
return {
canvas: null,
dataUrl: null,
yearStats: null
}
},
methods: {
async initCanvas() {
if (!this.yearStats) return
const canvas = document.createElement('canvas')
canvas.width = 600
canvas.height = 200
const ctx = canvas.getContext('2d')
const createRoundedRect = (x, y, w, h) => {
const grd1 = ctx.createLinearGradient(x, y, x + w, y + h)
grd1.addColorStop(0, '#44444455')
grd1.addColorStop(1, '#ffffff11')
ctx.fillStyle = grd1
ctx.strokeStyle = '#C0C0C088'
ctx.beginPath()
ctx.roundRect(x, y, w, h, [20])
ctx.fill()
ctx.stroke()
}
const addText = (text, fontSize, fontWeight, color, letterSpacing, x, y, maxWidth = 0) => {
ctx.fillStyle = color
ctx.font = `${fontWeight} ${fontSize} Source Sans Pro`
ctx.letterSpacing = letterSpacing
// If maxWidth is specified then continue to remove chars until under maxWidth and add ellipsis
if (maxWidth) {
let txtWidth = ctx.measureText(text).width
while (txtWidth > maxWidth) {
console.warn(`Text "${text}" is greater than max width ${maxWidth} (width:${txtWidth})`)
if (text.endsWith('...')) text = text.slice(0, -4) // Repeated checks remove 1 char at a time
else text = text.slice(0, -3) // First check remove last 3 chars
text += '...'
txtWidth = ctx.measureText(text).width
console.log(`Checking text "${text}" (width:${txtWidth})`)
}
}
ctx.fillText(text, x, y)
}
const addIcon = (icon, color, fontSize, x, y) => {
ctx.fillStyle = color
ctx.font = `${fontSize} Material Icons Outlined`
ctx.fillText(icon, x, y)
}
// Bg color
ctx.fillStyle = '#232323'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Cover image tiles
const bookCovers = this.yearStats.finishedBooksWithCovers
bookCovers.push(...this.yearStats.booksWithCovers)
if (bookCovers.length) {
let index = 0
ctx.globalAlpha = 0.25
ctx.save()
ctx.translate(canvas.width / 2, canvas.height / 2)
ctx.rotate((-Math.PI / 180) * 25)
ctx.translate(-canvas.width / 2, -canvas.height / 2)
ctx.translate(-10, -90)
for (let x = 0; x < 4; x++) {
for (let y = 0; y < 3; y++) {
const coverIndex = index % bookCovers.length
let libraryItemId = bookCovers[coverIndex]
index++
await new Promise((resolve) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.addEventListener('load', () => {
let sw = img.width
if (img.width > img.height) {
sw = img.height
}
let sx = -(sw - img.width) / 2
let sy = -(sw - img.height) / 2
ctx.drawImage(img, sx, sy, sw, sw, 155 * x, 155 * y, 155, 155)
resolve()
})
img.addEventListener('error', () => {
resolve()
})
img.src = this.$store.getters['globals/getLibraryItemCoverSrcById'](libraryItemId)
})
}
}
ctx.restore()
}
ctx.globalAlpha = 1
ctx.textBaseline = 'middle'
// Create gradient
const grd1 = ctx.createLinearGradient(0, 0, canvas.width, canvas.height)
grd1.addColorStop(0, '#000000aa')
grd1.addColorStop(1, '#cd9d49aa')
ctx.fillStyle = grd1
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Top Abs icon
let tanColor = '#ffdb70'
ctx.fillStyle = tanColor
ctx.font = '42px absicons'
ctx.fillText('\ue900', 15, 36)
// Top text
addText('audiobookshelf', '28px', 'normal', tanColor, '0px', 65, 28)
addText(`${this.year} YEAR IN REVIEW`, '18px', 'bold', 'white', '1px', 65, 51)
// Top left box
createRoundedRect(15, 75, 280, 110)
addText(this.yearStats.numBooksFinished, '48px', 'bold', 'white', '0px', 105, 120)
addText('books finished', '20px', 'normal', tanColor, '0px', 105, 155)
const readIconPath = new Path2D()
readIconPath.addPath(new Path2D('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'), { a: 1.5, d: 1.5, e: 55, f: 115 })
ctx.fillStyle = '#ffffff'
ctx.fill(readIconPath)
createRoundedRect(305, 75, 280, 110)
addText(this.yearStats.numBooksListened, '48px', 'bold', 'white', '0px', 400, 120)
addText('books listened to', '20px', 'normal', tanColor, '0px', 400, 155)
addIcon('local_library', 'white', '42px', 345, 130)
this.canvas = canvas
this.dataUrl = canvas.toDataURL('png')
},
share() {
this.canvas.toBlob((blob) => {
const file = new File([blob], 'yearinreviewshort.png', { type: blob.type })
const shareData = {
files: [file]
}
if (navigator.canShare(shareData)) {
navigator
.share(shareData)
.then(() => {
console.log('Share success')
})
.catch((error) => {
console.error('Failed to share', error)
if (error.name !== 'AbortError') {
this.$toast.error('Failed to share: ' + error.message)
}
})
} else {
this.$toast.error('Cannot share natively on this device')
}
})
},
refresh() {
this.init()
},
async init() {
this.$emit('update:processing', true)
this.yearStats = await this.$axios.$get(`/api/me/stats/year/${this.year}`).catch((err) => {
console.error('Failed to load stats for year', err)
this.$toast.error('Failed to load year stats')
return null
})
await this.initCanvas()
this.$emit('update:processing', false)
}
},
mounted() {
this.init()
}
}
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div class="text-center mt-4">
<div class="text-center mt-4 relative">
<div class="flex py-4">
<ui-file-input ref="fileInput" class="mr-2" accept=".audiobookshelf" @change="backupUploaded">{{ $strings.ButtonUploadBackup }}</ui-file-input>
<div class="flex-grow" />
@@ -54,6 +54,10 @@
</div>
</div>
</prompt-dialog>
<div v-if="isApplyingBackup" class="absolute inset-0 w-full h-full flex items-center justify-center bg-black/20 rounded-md">
<ui-loading-indicator />
</div>
</div>
</template>
@@ -64,6 +68,7 @@ export default {
showConfirmApply: false,
selectedBackup: null,
isBackingUp: false,
isApplyingBackup: false,
processing: false,
backups: []
}
@@ -85,19 +90,21 @@ export default {
},
confirm() {
this.showConfirmApply = false
this.isApplyingBackup = true
this.$axios
.$get(`/api/backups/${this.selectedBackup.id}/apply`)
.then(() => {
this.isBackingUp = false
location.replace('/config/backups?backup=1')
})
.catch((error) => {
this.isBackingUp = false
console.error('Failed to apply backup', error)
const errorMsg = error.response.data || this.$strings.ToastBackupRestoreFailed
this.$toast.error(errorMsg)
})
.finally(() => {
this.isApplyingBackup = false
})
},
deleteBackupClick(backup) {
if (confirm(this.$getString('MessageConfirmDeleteBackup', [this.$formatDatetime(backup.createdAt, this.dateFormat, this.timeFormat)]))) {
@@ -180,7 +187,6 @@ export default {
this.loadBackups()
if (this.$route.query.backup) {
this.$toast.success('Backup applied successfully')
this.$router.replace('/config')
}
}
}

View File

@@ -0,0 +1,127 @@
<template>
<div class="min-h-40">
<table v-if="providers.length" id="providers">
<tr>
<th>{{ $strings.LabelName }}</th>
<th>URL</th>
<th>Authorization Header Value</th>
<th class="w-12"></th>
</tr>
<tr v-for="provider in providers" :key="provider.id">
<td class="text-sm">{{ provider.name }}</td>
<td class="text-sm">{{ provider.url }}</td>
<td class="text-sm">
<span v-if="provider.authHeaderValue" class="custom-provider-api-key">{{ provider.authHeaderValue }}</span>
</td>
<td class="py-0">
<div class="h-8 w-8 flex items-center justify-center text-white text-opacity-50 hover:text-error cursor-pointer" @click.stop="removeProvider(provider)">
<button type="button" :aria-label="$strings.ButtonDelete" class="material-icons text-base">delete</button>
</div>
</td>
</tr>
</table>
<div v-else-if="!processing" class="text-center py-8">
<p class="text-lg">No custom metadata providers</p>
</div>
<div v-if="processing" class="absolute inset-0 h-full flex items-center justify-center bg-black/40 rounded-md">
<ui-loading-indicator />
</div>
</div>
</template>
<script>
export default {
props: {
providers: {
type: Array,
default: () => []
},
processing: Boolean
},
data() {
return {}
},
methods: {
removeProvider(provider) {
const payload = {
message: `Are you sure you want remove custom metadata provider "${provider.name}"?`,
callback: (confirmed) => {
if (confirmed) {
this.$emit('update:processing', true)
this.$axios
.$delete(`/api/custom-metadata-providers/${provider.id}`)
.then(() => {
this.$toast.success('Provider removed')
this.$emit('removed', provider.id)
})
.catch((error) => {
console.error('Failed to remove provider', error)
this.$toast.error('Failed to remove provider')
})
.finally(() => {
this.$emit('update:processing', false)
})
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
}
}
}
</script>
<style>
#providers {
table-layout: fixed;
border-collapse: collapse;
border: 1px solid #474747;
width: 100%;
}
#providers td,
#providers th {
/* border: 1px solid #2e2e2e; */
padding: 8px 8px;
text-align: left;
}
#providers td.py-0 {
padding: 0px 8px;
}
#providers tr:nth-child(even) {
background-color: #373838;
}
#providers tr:nth-child(odd) {
background-color: #2f2f2f;
}
#providers tr:hover {
background-color: #444;
}
#providers th {
font-size: 0.8rem;
font-weight: 600;
padding-top: 5px;
padding-bottom: 5px;
background-color: #272727;
}
.custom-provider-api-key {
padding: 1px;
background-color: #272727;
border-radius: 4px;
color: transparent;
transition: color, background-color 0.5s ease;
}
.custom-provider-api-key:hover {
background-color: transparent;
color: white;
}
</style>

View File

@@ -12,7 +12,7 @@
</div>
</div>
<transition name="slide">
<div class="w-full" v-show="showFiles">
<div class="w-full" v-if="showFiles">
<table class="text-sm tracksTable">
<tr>
<th class="text-left px-4">{{ $strings.LabelPath }}</th>
@@ -70,7 +70,7 @@ export default {
},
audioFiles() {
if (this.libraryItem.mediaType === 'podcast') {
return this.libraryItem.media?.episodes.map((ep) => ep.audioFile) || []
return this.libraryItem.media?.episodes.map((ep) => ep.audioFile).filter((af) => af) || []
}
return this.libraryItem.media?.audioFiles || []
},

View File

@@ -1,18 +1,22 @@
<template>
<div class="w-full px-2 py-3 overflow-hidden relative border-b border-white border-opacity-10" @mouseover="mouseover" @mouseleave="mouseleave">
<div v-if="episode" class="flex items-center cursor-pointer" :class="{ 'opacity-70': isSelected || selectionMode }" @click="clickedEpisode">
<div class="flex-grow px-2">
<div :id="`lazy-episode-${index}`" class="w-full h-full cursor-pointer" @mouseover="mouseover" @mouseleave="mouseleave">
<div class="flex" @click="clickedEpisode">
<div class="flex-grow">
<div class="flex items-center">
<span class="text-sm font-semibold">{{ title }}</span>
<widgets-podcast-type-indicator :type="episode.episodeType" />
<span class="text-sm font-semibold">{{ episodeTitle }}</span>
<widgets-podcast-type-indicator :type="episodeType" />
</div>
<p class="text-sm text-gray-200 episode-subtitle mt-1.5 mb-0.5" v-html="subtitle"></p>
<div class="flex justify-between pt-2 max-w-xl">
<p v-if="episode.season" class="text-sm text-gray-300">Season #{{ episode.season }}</p>
<p v-if="episode.episode" class="text-sm text-gray-300">Episode #{{ episode.episode }}</p>
<p v-if="episode.chapters?.length" class="text-sm text-gray-300">{{ episode.chapters.length }} Chapters</p>
<p v-if="publishedAt" class="text-sm text-gray-300">Published {{ $formatDate(publishedAt, dateFormat) }}</p>
<div class="h-10 flex items-center mt-1.5 mb-0.5 overflow-hidden">
<p class="text-sm text-gray-200 line-clamp-2" v-html="episodeSubtitle"></p>
</div>
<div class="h-8 flex items-center">
<div class="w-full inline-flex justify-between max-w-xl">
<p v-if="episode?.season" class="text-sm text-gray-300">Season #{{ episode.season }}</p>
<p v-if="episode?.episode" class="text-sm text-gray-300">Episode #{{ episode.episode }}</p>
<p v-if="episode?.chapters?.length" class="text-sm text-gray-300">{{ episode.chapters.length }} Chapters</p>
<p v-if="publishedAt" class="text-sm text-gray-300">Published {{ $formatDate(publishedAt, dateFormat) }}</p>
</div>
</div>
<div class="flex items-center pt-2">
@@ -37,10 +41,11 @@
<ui-icon-btn v-if="userCanDelete" icon="close" borderless @click="removeClick" />
</div>
</div>
<div v-if="isHovering || isSelected || selectionMode" class="hidden md:block w-12 min-w-12" />
<div v-if="isHovering || isSelected || isSelectionMode" class="hidden md:block w-12 min-w-12" />
</div>
<div v-if="isSelected || selectionMode" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-10 z-10 cursor-pointer" @click.stop="clickedSelectionBg" />
<div class="hidden md:block md:w-12 md:min-w-12 md:-right-0 md:absolute md:top-0 h-full transform transition-transform z-20" :class="!isHovering && !isSelected && !selectionMode ? 'translate-x-24' : 'translate-x-0'">
<div v-if="isSelected || isSelectionMode" class="absolute top-0 left-0 w-full h-full bg-black bg-opacity-10 z-10 cursor-pointer" @click.stop="clickedSelectionBg" />
<div class="hidden md:block md:w-12 md:min-w-12 md:-right-0 md:absolute md:top-0 h-full transform transition-transform z-20" :class="!isHovering && !isSelected && !isSelectionMode ? 'translate-x-24' : 'translate-x-0'">
<div class="flex h-full items-center">
<div class="mx-1">
<ui-checkbox v-model="isSelected" @input="selectedUpdated" checkbox-bg="bg" />
@@ -55,84 +60,91 @@
<script>
export default {
props: {
index: Number,
libraryItemId: String,
episode: {
type: Object,
default: () => {}
},
selectionMode: Boolean
default: () => null
}
},
data() {
return {
isProcessingReadUpdate: false,
processingRemove: false,
isHovering: false,
isSelected: false
isSelected: false,
isSelectionMode: false
}
},
computed: {
store() {
return this.$store || this.$nuxt.$store
},
axios() {
return this.$axios || this.$nuxt.$axios
},
userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate']
return this.store.getters['user/getUserCanUpdate']
},
userCanDelete() {
return this.$store.getters['user/getUserCanDelete']
return this.store.getters['user/getUserCanDelete']
},
audioFile() {
return this.episode.audioFile
episodeId() {
return this.episode?.id || ''
},
title() {
return this.episode.title || ''
episodeTitle() {
return this.episode?.title || ''
},
subtitle() {
return this.episode.subtitle || this.description
episodeSubtitle() {
return this.episode?.subtitle || ''
},
description() {
return this.episode.description || ''
episodeType() {
return this.episode?.episodeType || ''
},
duration() {
return this.$secondsToTimestamp(this.episode.duration)
publishedAt() {
return this.episode?.publishedAt
},
libraryItemIdStreaming() {
return this.$store.getters['getLibraryItemIdStreaming']
},
isStreamingFromDifferentLibrary() {
return this.$store.getters['getIsStreamingFromDifferentLibrary']
},
isStreaming() {
return this.$store.getters['getIsMediaStreaming'](this.libraryItemId, this.episode.id)
},
isQueued() {
return this.$store.getters['getIsMediaQueued'](this.libraryItemId, this.episode.id)
},
streamIsPlaying() {
return this.$store.state.streamIsPlaying && this.isStreaming
dateFormat() {
return this.store.state.serverSettings.dateFormat
},
itemProgress() {
return this.$store.getters['user/getUserMediaProgress'](this.libraryItemId, this.episode.id)
return this.store.getters['user/getUserMediaProgress'](this.libraryItemId, this.episodeId)
},
itemProgressPercent() {
return this.itemProgress ? this.itemProgress.progress : 0
return this.itemProgress?.progress || 0
},
userIsFinished() {
return this.itemProgress ? !!this.itemProgress.isFinished : false
return !!this.itemProgress?.isFinished
},
libraryItemIdStreaming() {
return this.store.getters['getLibraryItemIdStreaming']
},
isStreamingFromDifferentLibrary() {
return this.store.getters['getIsStreamingFromDifferentLibrary']
},
isStreaming() {
return this.store.getters['getIsMediaStreaming'](this.libraryItemId, this.episodeId)
},
isQueued() {
return this.store.getters['getIsMediaQueued'](this.libraryItemId, this.episodeId)
},
streamIsPlaying() {
return this.store.state.streamIsPlaying && this.isStreaming
},
timeRemaining() {
if (this.streamIsPlaying) return 'Playing'
if (!this.itemProgress) return this.$elapsedPretty(this.episode.duration)
if (!this.itemProgress) return this.$elapsedPretty(this.episode?.duration || 0)
if (this.userIsFinished) return 'Finished'
var remaining = Math.floor(this.itemProgress.duration - this.itemProgress.currentTime)
const duration = this.itemProgress.duration || this.episode?.duration || 0
const remaining = Math.floor(duration - this.itemProgress.currentTime)
return `${this.$elapsedPretty(remaining)} left`
},
publishedAt() {
return this.episode.publishedAt
},
dateFormat() {
return this.$store.state.serverSettings.dateFormat
}
},
methods: {
clickAddToPlaylist() {
this.$emit('addToPlaylist', this.episode)
setSelectionMode(isSelectionMode) {
this.isSelectionMode = isSelectionMode
if (!this.isSelectionMode) this.isSelected = false
},
clickedEpisode() {
this.$emit('view', this.episode)
@@ -150,16 +162,23 @@ export default {
mouseleave() {
this.isHovering = false
},
clickEdit() {
this.$emit('edit', this.episode)
},
playClick() {
if (this.streamIsPlaying) {
this.$eventBus.$emit('pause-item')
const eventBus = this.$eventBus || this.$nuxt.$eventBus
eventBus.$emit('pause-item')
} else {
this.$emit('play', this.episode)
}
},
queueBtnClick() {
if (this.isQueued) {
// Remove from queue
this.store.commit('removeItemFromQueue', { libraryItemId: this.libraryItemId, episodeId: this.episodeId })
} else {
// Add to queue
this.$emit('addToQueue', this.episode)
}
},
toggleFinished(confirmed = false) {
if (!this.userIsFinished && this.itemProgressPercent > 0 && !confirmed) {
const payload = {
@@ -171,37 +190,47 @@ export default {
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
this.store.commit('globals/setConfirmPrompt', payload)
return
}
var updatePayload = {
const updatePayload = {
isFinished: !this.userIsFinished
}
this.isProcessingReadUpdate = true
this.$axios
.$patch(`/api/me/progress/${this.libraryItemId}/${this.episode.id}`, updatePayload)
this.axios
.$patch(`/api/me/progress/${this.libraryItemId}/${this.episodeId}`, updatePayload)
.then(() => {
this.isProcessingReadUpdate = false
})
.catch((error) => {
console.error('Failed', error)
this.isProcessingReadUpdate = false
this.$toast.error(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedFailed : this.$strings.ToastItemMarkedAsNotFinishedFailed)
const toast = this.$toast || this.$nuxt.$toast
toast.error(updatePayload.isFinished ? this.$strings.ToastItemMarkedAsFinishedFailed : this.$strings.ToastItemMarkedAsNotFinishedFailed)
})
},
clickAddToPlaylist() {
this.$emit('addToPlaylist', this.episode)
},
clickEdit() {
this.$emit('edit', this.episode)
},
removeClick() {
this.$emit('remove', this.episode)
},
queueBtnClick() {
if (this.isQueued) {
// Remove from queue
this.$store.commit('removeItemFromQueue', { libraryItemId: this.libraryItemId, episodeId: this.episode.id })
} else {
// Add to queue
this.$emit('addToQueue', this.episode)
destroy() {
// destroy the vue listeners, etc
this.$destroy()
// remove the element from the DOM
if (this.$el && this.$el.parentNode) {
this.$el.parentNode.removeChild(this.$el)
} else if (this.$el && this.$el.remove) {
this.$el.remove()
}
}
}
},
mounted() {}
}
</script>
</script>

View File

@@ -1,5 +1,5 @@
<template>
<div class="w-full py-6">
<div id="lazy-episodes-table" class="w-full py-6">
<div class="flex flex-wrap flex-col md:flex-row md:items-center mb-4">
<div class="flex items-center flex-nowrap whitespace-nowrap mb-2 md:mb-0">
<p class="text-lg mb-0 font-semibold">{{ $strings.HeaderEpisodes }}</p>
@@ -18,28 +18,41 @@
<ui-btn :disabled="processing" small class="ml-2 h-9" @click="clearSelected">{{ $strings.ButtonCancel }}</ui-btn>
</template>
<template v-else>
<controls-filter-select v-model="filterKey" :items="filterItems" class="w-36 h-9 md:ml-4" />
<controls-sort-select v-model="sortKey" :descending.sync="sortDesc" :items="sortItems" class="w-44 md:w-48 h-9 ml-1 sm:ml-4" />
<controls-filter-select v-model="filterKey" :items="filterItems" class="w-36 h-9 md:ml-4" @change="filterSortChanged" />
<controls-sort-select v-model="sortKey" :descending.sync="sortDesc" :items="sortItems" class="w-44 md:w-48 h-9 ml-1 sm:ml-4" @change="filterSortChanged" />
<div class="flex-grow md:hidden" />
<ui-context-menu-dropdown v-if="contextMenuItems.length" :items="contextMenuItems" class="ml-1" @action="contextMenuAction" />
</template>
</div>
</div>
<p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p>
<!-- <p v-if="!episodes.length" class="py-4 text-center text-lg">{{ $strings.MessageNoEpisodes }}</p> -->
<div v-if="episodes.length" class="w-full py-3 mx-auto flex">
<form @submit.prevent="submit" class="flex flex-grow">
<ui-text-input v-model="search" @input="inputUpdate" type="search" :placeholder="$strings.PlaceholderSearchEpisode" class="flex-grow mr-2 text-sm md:text-base" />
</form>
</div>
<template v-for="episode in episodesList">
<tables-podcast-episode-table-row ref="episodeRow" :key="episode.id" :episode="episode" :library-item-id="libraryItem.id" :selection-mode="isSelectionMode" class="item" @play="playEpisode" @remove="removeEpisode" @edit="editEpisode" @view="viewEpisode" @selected="episodeSelected" @addToQueue="addEpisodeToQueue" @addToPlaylist="addToPlaylist" />
</template>
<div class="relative min-h-[176px]">
<template v-for="episode in totalEpisodes">
<div :key="episode" :id="`episode-${episode - 1}`" class="w-full h-44 px-2 py-3 overflow-hidden relative border-b border-white/10">
<!-- episode is mounted here -->
</div>
</template>
<div v-if="isSearching" class="w-full h-full absolute inset-0 flex justify-center py-12" :class="{ 'bg-black/50': totalEpisodes }">
<ui-loading-indicator />
</div>
<div v-else-if="!totalEpisodes" class="h-44 flex items-center justify-center">
<p class="text-lg">{{ $strings.MessageNoEpisodes }}</p>
</div>
</div>
<modals-podcast-remove-episode v-model="showPodcastRemoveModal" @input="removeEpisodeModalToggled" :library-item="libraryItem" :episodes="episodesToRemove" @clearSelected="clearSelected" />
</div>
</template>
<script>
import Vue from 'vue'
import LazyEpisodeRow from './LazyEpisodeRow.vue'
export default {
props: {
libraryItem: {
@@ -60,13 +73,21 @@ export default {
processing: false,
search: null,
searchTimeout: null,
searchText: null
searchText: null,
isSearching: false,
totalEpisodes: 0,
episodesPerPage: null,
episodeIndexesMounted: [],
episodeComponentRefs: {},
windowHeight: 0,
episodesTableOffsetTop: 0,
episodeRowHeight: 176
}
},
watch: {
libraryItem: {
handler() {
this.init()
this.refresh()
}
}
},
@@ -194,13 +215,19 @@ export default {
submit() {},
inputUpdate() {
clearTimeout(this.searchTimeout)
this.isSearching = true
let searchStart = this.searchText
this.searchTimeout = setTimeout(() => {
if (!this.search || !this.search.trim()) {
this.isSearching = false
if (!this.search?.trim()) {
this.searchText = ''
return
} else {
this.searchText = this.search.toLowerCase().trim()
}
this.searchText = this.search.toLowerCase().trim()
}, 500)
if (searchStart !== this.searchText) {
this.init()
}
}, 750)
},
contextMenuAction({ action }) {
if (action === 'quick-match-episodes') {
@@ -304,24 +331,30 @@ export default {
if (!val) this.episodesToRemove = []
},
clearSelected() {
const episodeRows = this.$refs.episodeRow
if (episodeRows && episodeRows.length) {
for (const epRow of episodeRows) {
if (epRow) epRow.isSelected = false
}
}
this.selectedEpisodes = []
this.setSelectionModeForEpisodes()
},
removeSelectedEpisodes() {
this.episodesToRemove = this.selectedEpisodes
this.showPodcastRemoveModal = true
},
episodeSelected({ isSelected, episode }) {
let isSelectionModeBefore = this.isSelectionMode
if (isSelected) {
this.selectedEpisodes.push(episode)
} else {
this.selectedEpisodes = this.selectedEpisodes.filter((ep) => ep.id !== episode.id)
}
if (this.isSelectionMode !== isSelectionModeBefore) {
this.setSelectionModeForEpisodes()
}
},
setSelectionModeForEpisodes() {
for (const key in this.episodeComponentRefs) {
if (this.episodeComponentRefs[key]?.setSelectionMode) {
this.episodeComponentRefs[key].setSelectionMode(this.isSelectionMode)
}
}
},
playEpisode(episode) {
const queueItems = []
@@ -367,12 +400,147 @@ export default {
this.$store.commit('globals/setSelectedEpisode', episode)
this.$store.commit('globals/setShowViewPodcastEpisodeModal', true)
},
init() {
destroyEpisodeComponents() {
for (const key in this.episodeComponentRefs) {
if (this.episodeComponentRefs[key]?.destroy) {
this.episodeComponentRefs[key].destroy()
}
}
this.episodeComponentRefs = {}
this.episodeIndexesMounted = []
},
mountEpisode(index) {
const episodeEl = document.getElementById(`episode-${index}`)
if (!episodeEl) {
console.warn('Episode row el not found at ' + index)
return
}
this.episodeIndexesMounted.push(index)
if (this.episodeComponentRefs[index]) {
const episodeComponent = this.episodeComponentRefs[index]
episodeEl.appendChild(episodeComponent.$el)
if (this.isSelectionMode) {
episodeComponent.setSelectionMode(true)
if (this.selectedEpisodes.some((i) => i.id === episodeComponent.episodeId)) {
episodeComponent.isSelected = true
} else {
episodeComponent.isSelected = false
}
} else {
episodeComponent.setSelectionMode(false)
}
} else {
const _this = this
const ComponentClass = Vue.extend(LazyEpisodeRow)
const instance = new ComponentClass({
propsData: {
index,
libraryItemId: this.libraryItem.id,
episode: this.episodesList[index]
},
created() {
this.$on('selected', (payload) => {
_this.episodeSelected(payload)
})
this.$on('view', (payload) => {
_this.viewEpisode(payload)
})
this.$on('play', (payload) => {
_this.playEpisode(payload)
})
this.$on('addToQueue', (payload) => {
_this.addEpisodeToQueue(payload)
})
this.$on('remove', (payload) => {
_this.removeEpisode(payload)
})
this.$on('edit', (payload) => {
_this.editEpisode(payload)
})
this.$on('addToPlaylist', (payload) => {
_this.addToPlaylist(payload)
})
}
})
this.episodeComponentRefs[index] = instance
instance.$mount()
episodeEl.appendChild(instance.$el)
if (this.isSelectionMode) {
instance.setSelectionMode(true)
if (this.selectedEpisodes.some((i) => i.id === this.episodesList[index].id)) {
instance.isSelected = true
}
}
}
},
mountEpisodes(startIndex, endIndex) {
for (let i = startIndex; i < endIndex; i++) {
if (!this.episodeIndexesMounted.includes(i)) {
this.mountEpisode(i)
}
}
},
scroll(evt) {
if (!evt?.target?.scrollTop) return
const scrollTop = Math.max(evt.target.scrollTop - this.episodesTableOffsetTop, 0)
let firstEpisodeIndex = Math.floor(scrollTop / this.episodeRowHeight)
let lastEpisodeIndex = Math.ceil((scrollTop + this.windowHeight) / this.episodeRowHeight)
lastEpisodeIndex = Math.min(this.totalEpisodes - 1, lastEpisodeIndex)
this.episodeIndexesMounted = this.episodeIndexesMounted.filter((_index) => {
if (_index < firstEpisodeIndex || _index >= lastEpisodeIndex) {
const el = document.getElementById(`lazy-episode-${_index}`)
if (el) el.remove()
return false
}
return true
})
this.mountEpisodes(firstEpisodeIndex, lastEpisodeIndex + 1)
},
initListeners() {
const itemPageWrapper = document.getElementById('item-page-wrapper')
if (itemPageWrapper) {
itemPageWrapper.addEventListener('scroll', this.scroll)
}
},
removeListeners() {
const itemPageWrapper = document.getElementById('item-page-wrapper')
if (itemPageWrapper) {
itemPageWrapper.removeEventListener('scroll', this.scroll)
}
},
filterSortChanged() {
this.init()
},
refresh() {
this.episodesCopy = this.episodes.map((ep) => ({ ...ep }))
this.init()
},
init() {
this.destroyEpisodeComponents()
this.totalEpisodes = this.episodesList.length
const lazyEpisodesTableEl = document.getElementById('lazy-episodes-table')
this.episodesTableOffsetTop = (lazyEpisodesTableEl?.offsetTop || 0) + 64
this.windowHeight = window.innerHeight
this.episodesPerPage = Math.ceil(this.windowHeight / this.episodeRowHeight)
this.$nextTick(() => {
this.mountEpisodes(0, Math.min(this.episodesPerPage, this.totalEpisodes))
})
}
},
mounted() {
this.episodesCopy = this.episodes.map((ep) => ({ ...ep }))
this.initListeners()
this.init()
},
beforeDestroy() {
this.removeListeners()
}
}
</script>

View File

@@ -2,7 +2,8 @@
<label class="flex justify-start items-center" :class="!disabled ? 'cursor-pointer' : ''">
<div class="border-2 rounded flex flex-shrink-0 justify-center items-center" :class="wrapperClass">
<input v-model="selected" :disabled="disabled" type="checkbox" class="opacity-0 absolute" :class="!disabled ? 'cursor-pointer' : ''" />
<svg v-if="selected" class="fill-current pointer-events-none" :class="svgClass" viewBox="0 0 20 20"><path d="M0 11l2-2 5 5L18 3l2 2L7 18z" /></svg>
<span v-if="partial" class="material-icons text-base leading-none text-gray-400">remove</span>
<svg v-else-if="selected" class="fill-current pointer-events-none" :class="svgClass" viewBox="0 0 20 20"><path d="M0 11l2-2 5 5L18 3l2 2L7 18z" /></svg>
</div>
<div v-if="label" class="select-none" :class="[labelClassname, disabled ? 'text-gray-400' : 'text-gray-100']">{{ label }}</div>
</label>
@@ -31,7 +32,8 @@ export default {
type: String,
default: ''
},
disabled: Boolean
disabled: Boolean,
partial: Boolean
},
data() {
return {}

View File

@@ -1,6 +1,6 @@
<template>
<div class="relative w-full" v-click-outside="clickOutsideObj">
<p class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
<p v-if="label" class="text-sm font-semibold px-1" :class="disabled ? 'text-gray-300' : ''">{{ label }}</p>
<button type="button" :aria-label="longLabel" :disabled="disabled" class="relative w-full border rounded shadow-sm 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>
@@ -64,7 +64,7 @@ export default {
},
itemsToShow() {
return this.items.map((i) => {
if (typeof i === 'string') {
if (typeof i === 'string' || typeof i === 'number') {
return {
text: i,
value: i

View File

@@ -1,6 +1,6 @@
<template>
<div class="w-full">
<label class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</label>
<label v-if="label" class="px-1 text-sm font-semibold" :class="disabled ? 'text-gray-400' : ''">{{ label }}</label>
<div ref="wrapper" class="relative">
<form @submit.prevent="submitForm">
<div ref="inputWrapper" class="input-wrapper flex-wrap relative w-full shadow-sm flex items-center border border-gray-600 rounded px-2 py-2" :class="disabled ? 'pointer-events-none bg-black-300 text-gray-400' : 'bg-primary'">

View File

@@ -11,13 +11,13 @@
</div>
{{ item }}
</div>
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" style="min-width: 40px; width: 40px" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
</div>
</form>
<ul ref="menu" v-show="showMenu" class="absolute z-60 mt-1 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in itemsToShow">
<li :key="item" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
<li :key="item" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="itemsToShow[selectedMenuItemIndex] === item ? 'text-yellow-300' : ''" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
<div class="flex items-center">
<span class="font-normal ml-3 block truncate">{{ item }}</span>
</div>
@@ -50,7 +50,11 @@ export default {
label: String,
disabled: Boolean,
readonly: Boolean,
showEdit: Boolean
showEdit: Boolean,
menuDisabled: {
type: Boolean,
default: false
}
},
data() {
return {
@@ -58,7 +62,9 @@ export default {
currentSearch: null,
typingTimeout: null,
isFocused: false,
menu: null
menu: null,
filteredItems: null,
selectedMenuItemIndex: null
}
},
watch: {
@@ -77,7 +83,7 @@ export default {
}
},
showMenu() {
return this.isFocused
return this.isFocused && !this.menuDisabled
},
wrapperClass() {
var classes = []
@@ -87,24 +93,63 @@ export default {
return classes.join(' ')
},
itemsToShow() {
if (!this.currentSearch || !this.textInput) {
if (!this.currentSearch || !this.textInput || !this.filteredItems) {
return this.items
}
return this.items.filter((i) => {
var iValue = String(i).toLowerCase()
return iValue.includes(this.currentSearch.toLowerCase())
})
return this.filteredItems
}
},
methods: {
editItem(item) {
this.$emit('edit', item)
},
keydownInput() {
search() {
if (!this.textInput) {
this.filteredItems = null
return
}
this.currentSearch = this.textInput
const results = this.items.filter((i) => {
var iValue = String(i).toLowerCase()
return iValue.includes(this.currentSearch.toLowerCase())
})
this.filteredItems = results || []
},
keydownInput(event) {
let items = this.itemsToShow
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault()
if (!items.length) return
if (event.key === 'ArrowDown') {
if (this.selectedMenuItemIndex === null) {
this.selectedMenuItemIndex = 0
} else {
this.selectedMenuItemIndex = Math.min(this.selectedMenuItemIndex + 1, items.length - 1)
}
} else if (event.key === 'ArrowUp') {
if (this.selectedMenuItemIndex === null) {
this.selectedMenuItemIndex = items.length - 1
} else {
this.selectedMenuItemIndex = Math.max(this.selectedMenuItemIndex - 1, 0)
}
}
this.recalcScroll()
return
} else if (event.key === 'Enter') {
if (this.selectedMenuItemIndex !== null) {
this.clickedOption(event, items[this.selectedMenuItemIndex])
} else {
this.submitForm()
}
return
}
this.selectedMenuItemIndex = null
clearTimeout(this.typingTimeout)
this.typingTimeout = setTimeout(() => {
this.currentSearch = this.textInput
this.search()
}, 100)
this.setInputWidth()
},
@@ -116,6 +161,24 @@ export default {
this.recalcMenuPos()
}, 50)
},
recalcScroll() {
if (!this.menu) return
var menuItems = this.menu.querySelectorAll('li')
if (!menuItems.length) return
var selectedItem = menuItems[this.selectedMenuItemIndex]
if (!selectedItem) return
var menuHeight = this.menu.offsetHeight
var itemHeight = selectedItem.offsetHeight
var itemTop = selectedItem.offsetTop
var itemBottom = itemTop + itemHeight
if (itemBottom > this.menu.scrollTop + menuHeight) {
let menuPaddingBottom = parseFloat(window.getComputedStyle(this.menu).paddingBottom)
this.menu.scrollTop = itemBottom - menuHeight + menuPaddingBottom
} else if (itemTop < this.menu.scrollTop) {
let menuPaddingTop = parseFloat(window.getComputedStyle(this.menu).paddingTop)
this.menu.scrollTop = itemTop - menuPaddingTop
}
},
recalcMenuPos() {
if (!this.menu || !this.$refs.inputWrapper) return
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
@@ -204,7 +267,10 @@ export default {
e.stopPropagation()
e.preventDefault()
}
if (this.$refs.input) this.$refs.input.focus()
if (this.$refs.input) {
this.$refs.input.style.width = '24px'
this.$refs.input.focus()
}
var newSelected = null
if (this.selected.includes(itemValue)) {
@@ -215,6 +281,7 @@ export default {
}
this.textInput = null
this.currentSearch = null
this.selectedMenuItemIndex = null
this.$emit('input', newSelected)
this.$nextTick(() => {
this.recalcMenuPos()
@@ -241,6 +308,7 @@ export default {
this.$emit('newItem', item)
this.textInput = null
this.currentSearch = null
this.selectedMenuItemIndex = null
this.$nextTick(() => {
this.blur()
})
@@ -257,6 +325,7 @@ export default {
} else {
this.insertNewItem(this.textInput)
}
if (this.$refs.input) this.$refs.input.style.width = '24px'
},
scroll() {
this.recalcMenuPos()

View File

@@ -14,13 +14,13 @@
<div v-if="showEdit && !disabled" class="rounded-full cursor-pointer w-6 h-6 mx-0.5 bg-bg flex items-center justify-center">
<span class="material-icons text-white hover:text-success pt-px pr-px" style="font-size: 1.1rem" @click.stop="addItem">add</span>
</div>
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" style="min-width: 40px; width: 40px" class="h-full bg-primary focus:outline-none px-1" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
<input v-show="!readonly" ref="input" v-model="textInput" :disabled="disabled" class="h-full bg-primary focus:outline-none px-1 w-6" @keydown="keydownInput" @focus="inputFocus" @blur="inputBlur" @paste="inputPaste" />
</div>
</form>
<ul ref="menu" v-show="showMenu" class="absolute z-60 w-full bg-bg border border-black-200 shadow-lg max-h-56 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm" role="listbox" aria-labelledby="listbox-label">
<template v-for="item in itemsToShow">
<li :key="item.id" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
<li :key="item.id" class="text-gray-50 select-none relative py-2 pr-9 cursor-pointer hover:bg-black-400" :class="itemsToShow[selectedMenuItemIndex] === item ? 'text-yellow-300' : ''" role="option" @click="clickedOption($event, item)" @mouseup.stop.prevent @mousedown.prevent>
<div class="flex items-center">
<span class="font-normal ml-3 block truncate">{{ item.name }}</span>
</div>
@@ -63,7 +63,8 @@ export default {
typingTimeout: null,
isFocused: false,
menu: null,
items: []
items: [],
selectedMenuItemIndex: null
}
},
watch: {
@@ -122,7 +123,35 @@ export default {
this.items = results || []
},
keydownInput() {
keydownInput(event) {
let items = this.itemsToShow
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
event.preventDefault()
if (!items.length) return
if (event.key === 'ArrowDown') {
if (this.selectedMenuItemIndex === null) {
this.selectedMenuItemIndex = 0
} else {
this.selectedMenuItemIndex = Math.min(this.selectedMenuItemIndex + 1, items.length - 1)
}
} else if (event.key === 'ArrowUp') {
if (this.selectedMenuItemIndex === null) {
this.selectedMenuItemIndex = items.length - 1
} else {
this.selectedMenuItemIndex = Math.max(this.selectedMenuItemIndex - 1, 0)
}
}
this.recalcScroll()
return
} else if (event.key === 'Enter') {
if (this.selectedMenuItemIndex !== null) {
this.clickedOption(event, items[this.selectedMenuItemIndex])
} else {
this.submitForm()
}
return
}
this.selectedMenuItemIndex = null
clearTimeout(this.typingTimeout)
this.typingTimeout = setTimeout(() => {
this.search()
@@ -137,6 +166,24 @@ export default {
this.recalcMenuPos()
}, 50)
},
recalcScroll() {
if (!this.menu) return
var menuItems = this.menu.querySelectorAll('li')
if (!menuItems.length) return
var selectedItem = menuItems[this.selectedMenuItemIndex]
if (!selectedItem) return
var menuHeight = this.menu.offsetHeight
var itemHeight = selectedItem.offsetHeight
var itemTop = selectedItem.offsetTop
var itemBottom = itemTop + itemHeight
if (itemBottom > this.menu.scrollTop + menuHeight) {
let menuPaddingBottom = parseFloat(window.getComputedStyle(this.menu).paddingBottom)
this.menu.scrollTop = itemBottom - menuHeight + menuPaddingBottom
} else if (itemTop < this.menu.scrollTop) {
let menuPaddingTop = parseFloat(window.getComputedStyle(this.menu).paddingTop)
this.menu.scrollTop = itemTop - menuPaddingTop
}
},
recalcMenuPos() {
if (!this.menu || !this.$refs.inputWrapper) return
var boundingBox = this.$refs.inputWrapper.getBoundingClientRect()
@@ -228,7 +275,10 @@ export default {
e.stopPropagation()
e.preventDefault()
}
if (this.$refs.input) this.$refs.input.focus()
if (this.$refs.input) {
this.$refs.input.style.width = '24px'
this.$refs.input.focus()
}
let newSelected = null
if (this.getIsSelected(item.id)) {
@@ -244,6 +294,7 @@ export default {
}
this.textInput = null
this.currentSearch = null
this.selectedMenuItemIndex = null
this.$emit('input', newSelected)
this.$nextTick(() => {
@@ -271,6 +322,7 @@ export default {
this.$emit('newItem', item)
this.textInput = null
this.currentSearch = null
this.selectedMenuItemIndex = null
this.$nextTick(() => {
this.blur()
})
@@ -291,6 +343,7 @@ export default {
name: this.textInput
})
}
if (this.$refs.input) this.$refs.input.style.width = '24px'
},
scroll() {
this.recalcMenuPos()

View File

@@ -1,5 +1,5 @@
<template>
<div>
<div class="default-style">
<p v-if="label" class="px-1 text-sm font-semibold" :class="{ 'text-gray-400': disabled }">
{{ label }}
</p>
@@ -29,31 +29,31 @@ export default {
config() {
return {
toolbar: {
getDefaultHTML: () => ` <div class="trix-button-row">
getDefaultHTML: () => `<div class="trix-button-row">
<span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" data-trix-key="b" title="#{lang.bold}" tabindex="-1">#{lang.bold}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" data-trix-key="i" title="#{lang.italic}" tabindex="-1">#{lang.italic}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-strike" data-trix-attribute="strike" title="#{lang.strike}" tabindex="-1">#{lang.strike}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" title="#{lang.link}" tabindex="-1">#{lang.link}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" data-trix-key="b" title="${this.$strings.LabelFontBold}" tabindex="-1">${this.$strings.LabelFontBold}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" data-trix-key="i" title="${this.$strings.LabelFontItalic}" tabindex="-1">${this.$strings.LabelFontItalic}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-strike" data-trix-attribute="strike" title="${this.$strings.LabelFontStrikethrough}" tabindex="-1">${this.$strings.LabelFontStrikethrough}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" data-trix-action="link" data-trix-key="k" title="${this.$strings.LabelTextEditorLink}" tabindex="-1">${this.$strings.LabelTextEditorLink}</button>
</span>
<span class="trix-button-group trix-button-group--block-tools" data-trix-button-group="block-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-bullet-list" data-trix-attribute="bullet" title="#{lang.bullets}" tabindex="-1">#{lang.bullets}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-number-list" data-trix-attribute="number" title="#{lang.numbers}" tabindex="-1">#{lang.numbers}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-bullet-list" data-trix-attribute="bullet" title="${this.$strings.LabelTextEditorBulletedList}" tabindex="-1">${this.$strings.LabelTextEditorBulletedList}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-number-list" data-trix-attribute="number" title="${this.$strings.LabelTextEditorNumberedList}" tabindex="-1">${this.$strings.LabelTextEditorNumberedList}</button>
</span>
<span class="trix-button-group-spacer"></span>
<span class="trix-button-group trix-button-group--history-tools" data-trix-button-group="history-tools">
<button type="button" class="trix-button trix-button--icon trix-button--icon-undo" data-trix-action="undo" data-trix-key="z" title="#{lang.undo}" tabindex="-1">#{lang.undo}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-redo" data-trix-action="redo" data-trix-key="shift+z" title="#{lang.redo}" tabindex="-1">#{lang.redo}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-undo" data-trix-action="undo" data-trix-key="z" title="${this.$strings.LabelUndo}" tabindex="-1">${this.$strings.LabelUndo}</button>
<button type="button" class="trix-button trix-button--icon trix-button--icon-redo" data-trix-action="redo" data-trix-key="shift+z" title="${this.$strings.LabelRedo}" tabindex="-1">${this.$strings.LabelRedo}</button>
</span>
</div>
<div class="trix-dialogs" data-trix-dialogs>
<div class="trix-dialog trix-dialog--link" data-trix-dialog="href" data-trix-dialog-attribute="href">
<div class="trix-dialog__link-fields">
<input type="url" name="href" class="trix-input trix-input--dialog" placeholder="#{lang.urlPlaceholder}" aria-label="#{lang.url}" required data-trix-input>
<input type="url" name="href" class="trix-input trix-input--dialog" placeholder="" aria-label="URL" required data-trix-input>
<div class="trix-button-group">
<input type="button" class="trix-button trix-button--dialog" value="#{lang.link}" data-trix-method="setAttribute">
<input type="button" class="trix-button trix-button--dialog" value="#{lang.unlink}" data-trix-method="removeAttribute">
<input type="button" class="trix-button trix-button--dialog" value="${this.$strings.LabelTextEditorLink}" data-trix-method="setAttribute">
<input type="button" class="trix-button trix-button--dialog" value="${this.$strings.LabelTextEditorUnlink}" data-trix-method="removeAttribute">
</div>
</div>
</div>

View File

@@ -1,6 +1,6 @@
<template>
<div ref="wrapper" class="relative">
<input :id="inputId" ref="input" v-model="inputValue" :type="actualType" :step="step" :min="min" :readonly="readonly" :disabled="disabled" :placeholder="placeholder" class="rounded bg-primary text-gray-200 focus:border-gray-300 focus:bg-bg focus:outline-none border border-gray-600 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" class="rounded bg-primary text-gray-200 focus:border-gray-300 focus:bg-bg focus:outline-none border border-gray-600 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-icons text-gray-300 cursor-pointer" style="font-size: 1.1rem" @click.stop.prevent="clear">close</span>
</div>
@@ -33,6 +33,7 @@ export default {
textCenter: Boolean,
clearable: Boolean,
inputId: String,
inputName: String,
step: [String, Number],
min: [String, Number]
},
@@ -117,4 +118,4 @@ input:read-only {
input::-webkit-calendar-picker-indicator {
filter: invert(1);
}
</style>
</style>

View File

@@ -15,6 +15,13 @@ export default {
type: String,
default: 'right'
},
/**
* Delay showing the tooltip after X milliseconds of hovering
*/
delayOnShow: {
type: Number,
default: 0
},
disabled: Boolean
},
data() {
@@ -22,7 +29,8 @@ export default {
tooltip: null,
tooltipId: null,
isShowing: false,
hideTimeout: null
hideTimeout: null,
delayOnShowTimeout: null
}
},
watch: {
@@ -59,29 +67,44 @@ export default {
this.tooltip = tooltip
},
setTooltipPosition(tooltip) {
var boxChow = this.$refs.box.getBoundingClientRect()
const boxRect = this.$refs.box.getBoundingClientRect()
const shouldMount = !tooltip.isConnected
var shouldMount = !tooltip.isConnected
// Calculate size of tooltip
if (shouldMount) document.body.appendChild(tooltip)
var { width, height } = tooltip.getBoundingClientRect()
const tooltipRect = tooltip.getBoundingClientRect()
if (shouldMount) tooltip.remove()
var top = 0
var left = 0
// Subtracting scrollbar size
const windowHeight = window.innerHeight - 8
const windowWidth = window.innerWidth - 8
let top = 0
let left = 0
if (this.direction === 'right') {
top = boxChow.top - height / 2 + boxChow.height / 2
left = boxChow.left + boxChow.width + 4
top = Math.max(0, boxRect.top - tooltipRect.height / 2 + boxRect.height / 2)
left = Math.max(0, boxRect.left + boxRect.width + 4)
} else if (this.direction === 'bottom') {
top = boxChow.top + boxChow.height + 4
left = boxChow.left - width / 2 + boxChow.width / 2
top = Math.max(0, boxRect.top + boxRect.height + 4)
left = Math.max(0, boxRect.left - tooltipRect.width / 2 + boxRect.width / 2)
} else if (this.direction === 'top') {
top = boxChow.top - height - 4
left = boxChow.left - width / 2 + boxChow.width / 2
top = Math.max(0, boxRect.top - tooltipRect.height - 4)
left = Math.max(0, boxRect.left - tooltipRect.width / 2 + boxRect.width / 2)
} else if (this.direction === 'left') {
top = boxChow.top - height / 2 + boxChow.height / 2
left = boxChow.left - width - 4
top = Math.max(0, boxRect.top - tooltipRect.height / 2 + boxRect.height / 2)
left = Math.max(0, boxRect.left - tooltipRect.width - 4)
}
// Shift left if tooltip would overflow the window on the right
if (left + tooltipRect.width > windowWidth) {
left -= left + tooltipRect.width - windowWidth
}
// Shift up if tooltip would overflow the window on the bottom
if (top + tooltipRect.height > windowHeight) {
top -= top + tooltipRect.height - windowHeight
}
tooltip.style.top = top + 'px'
tooltip.style.left = left + 'px'
},
@@ -107,15 +130,33 @@ export default {
this.isShowing = false
},
cancelHide() {
if (this.hideTimeout) clearTimeout(this.hideTimeout)
clearTimeout(this.hideTimeout)
},
mouseover() {
if (!this.isShowing) this.showTooltip()
if (this.isShowing || this.disabled) return
if (this.delayOnShow) {
if (this.delayOnShowTimeout) {
// Delay already running
return
}
this.delayOnShowTimeout = setTimeout(() => {
this.showTooltip()
this.delayOnShowTimeout = null
}, this.delayOnShow)
} else {
this.showTooltip()
}
},
mouseleave() {
if (this.isShowing) {
this.hideTimeout = setTimeout(this.hideTooltip, 100)
if (!this.isShowing) {
clearTimeout(this.delayOnShowTimeout)
this.delayOnShowTimeout = null
return
}
this.hideTimeout = setTimeout(this.hideTooltip, 100)
}
},
beforeDestroy() {

View File

@@ -7,7 +7,7 @@
<Nuxt :key="currentLang" />
</div>
<app-stream-container ref="streamContainer" />
<app-media-player-container ref="mediaPlayerContainer" />
<modals-item-edit-modal />
<modals-collections-add-create-modal />
@@ -129,23 +129,23 @@ export default {
this.$eventBus.$emit('socket_init')
},
streamOpen(stream) {
if (this.$refs.streamContainer) this.$refs.streamContainer.streamOpen(stream)
if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamOpen(stream)
},
streamClosed(streamId) {
if (this.$refs.streamContainer) this.$refs.streamContainer.streamClosed(streamId)
if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamClosed(streamId)
},
streamProgress(data) {
if (this.$refs.streamContainer) this.$refs.streamContainer.streamProgress(data)
if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamProgress(data)
},
streamReady() {
if (this.$refs.streamContainer) this.$refs.streamContainer.streamReady()
if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamReady()
},
streamReset(payload) {
if (this.$refs.streamContainer) this.$refs.streamContainer.streamReset(payload)
if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamReset(payload)
},
streamError({ id, errorMessage }) {
this.$toast.error(`Stream Failed: ${errorMessage}`)
if (this.$refs.streamContainer) this.$refs.streamContainer.streamError(id)
if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.streamError(id)
},
libraryAdded(library) {
this.$store.commit('libraries/addUpdate', library)
@@ -247,7 +247,7 @@ export default {
this.multiSessionCurrentSessionId = null
this.$toast.dismiss('multiple-sessions')
}
if (this.$refs.streamContainer) this.$refs.streamContainer.sessionClosedEvent(sessionId)
if (this.$refs.mediaPlayerContainer) this.$refs.mediaPlayerContainer.sessionClosedEvent(sessionId)
},
userMediaProgressUpdate(payload) {
this.$store.commit('user/updateMediaProgress', payload)
@@ -328,6 +328,14 @@ export default {
this.$store.commit('libraries/setEReaderDevices', data.ereaderDevices)
},
customMetadataProviderAdded(provider) {
if (!provider?.id) return
this.$store.commit('scanners/addCustomMetadataProvider', provider)
},
customMetadataProviderRemoved(provider) {
if (!provider?.id) return
this.$store.commit('scanners/removeCustomMetadataProvider', provider)
},
initializeSocket() {
this.socket = this.$nuxtSocket({
name: process.env.NODE_ENV === 'development' ? 'dev' : 'prod',
@@ -406,6 +414,10 @@ export default {
this.socket.on('batch_quickmatch_complete', this.batchQuickMatchComplete)
this.socket.on('admin_message', this.adminMessageEvt)
// Custom metadata provider Listeners
this.socket.on('custom_metadata_provider_added', this.customMetadataProviderAdded)
this.socket.on('custom_metadata_provider_removed', this.customMetadataProviderRemoved)
},
showUpdateToast(versionData) {
var ignoreVersion = localStorage.getItem('ignoreVersion')

View File

@@ -25,11 +25,13 @@ module.exports = {
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' }
{ hid: 'description', name: 'description', content: '' },
{ hid: 'robots', name: 'robots', content: 'noindex' }
],
script: [],
link: [
{ rel: 'icon', type: 'image/x-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/favicon.ico' }
{ rel: 'icon', type: 'image/x-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/favicon.ico' },
{ rel: 'apple-touch-icon', href: (process.env.ROUTER_BASE_PATH || '') + '/ios_icon.png' }
]
},
@@ -39,6 +41,7 @@ module.exports = {
// Global CSS: https://go.nuxtjs.dev/config-css
css: [
'@/assets/tailwind.css',
'@/assets/app.css'
],
@@ -58,9 +61,7 @@ module.exports = {
// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
buildModules: [
// https://go.nuxtjs.dev/tailwindcss
'@nuxtjs/tailwindcss',
'@nuxtjs/pwa',
'@nuxt/postcss8'
'@nuxtjs/pwa'
],
// Modules: https://go.nuxtjs.dev/config-modules
@@ -96,7 +97,7 @@ module.exports = {
meta: {
appleStatusBarStyle: 'black',
name: 'Audiobookshelf',
theme_color: '#373838',
theme_color: '#232323',
mobileAppIOS: true,
nativeUI: true
},
@@ -104,16 +105,16 @@ module.exports = {
name: 'Audiobookshelf',
short_name: 'Audiobookshelf',
display: 'standalone',
background_color: '#373838',
background_color: '#232323',
icons: [
{
src: (process.env.ROUTER_BASE_PATH || '') + '/icon.svg',
sizes: "any"
sizes: 'any'
},
{
src: (process.env.ROUTER_BASE_PATH || '') + '/icon64.png',
type: "image/png",
sizes: "64x64"
src: (process.env.ROUTER_BASE_PATH || '') + '/icon192.png',
type: 'image/png',
sizes: 'any'
}
]
},

26967
client/package-lock.json generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "2.6.0",
"version": "2.8.1",
"buildNumber": 1,
"description": "Self-hosted audiobook and podcast client",
"main": "index.js",
@@ -21,9 +21,9 @@
"cron-parser": "^4.7.1",
"date-fns": "^2.25.0",
"epubjs": "^0.3.88",
"hls.js": "^1.0.7",
"hls.js": "^1.5.7",
"libarchive.js": "^1.3.0",
"nuxt": "^2.15.8",
"nuxt": "^2.17.3",
"nuxt-socket-io": "^1.1.18",
"trix": "^1.3.1",
"v-click-outside": "^3.1.2",
@@ -31,11 +31,9 @@
"vuedraggable": "^2.24.3"
},
"devDependencies": {
"@nuxt/postcss8": "^1.1.3",
"@nuxtjs/pwa": "^3.3.5",
"@nuxtjs/tailwindcss": "^4.2.1",
"autoprefixer": "^10.4.7",
"postcss": "^8.3.6",
"tailwindcss": "^3.1.4"
"tailwindcss": "^3.4.1"
}
}

View File

@@ -19,8 +19,8 @@
<div class="w-full h-px bg-white/10 my-4" />
<p v-if="!isGuest" class="mb-4 text-lg">{{ $strings.HeaderChangePassword }}</p>
<form v-if="!isGuest" @submit.prevent="submitChangePassword">
<p v-if="showChangePasswordForm" class="mb-4 text-lg">{{ $strings.HeaderChangePassword }}</p>
<form v-if="showChangePasswordForm" @submit.prevent="submitChangePassword">
<ui-text-input-with-label v-model="password" :disabled="changingPassword" type="password" :label="$strings.LabelPassword" class="my-2" />
<ui-text-input-with-label v-model="newPassword" :disabled="changingPassword" type="password" :label="$strings.LabelNewPassword" class="my-2" />
<ui-text-input-with-label v-model="confirmPassword" :disabled="changingPassword" type="password" :label="$strings.LabelConfirmPassword" class="my-2" />
@@ -68,6 +68,13 @@ export default {
},
isGuest() {
return this.usertype === 'guest'
},
isPasswordAuthEnabled() {
const activeAuthMethods = this.$store.getters['getServerSetting']('authActiveAuthMethods') || []
return activeAuthMethods.includes('local')
},
showChangePasswordForm() {
return !this.isGuest && this.isPasswordAuthEnabled
}
},
methods: {
@@ -75,19 +82,33 @@ export default {
this.$setLanguageCode(lang)
},
logout() {
var rootSocket = this.$root.socket || {}
const logoutPayload = {
socketId: rootSocket.id
// Disconnect from socket
if (this.$root.socket) {
console.log('Disconnecting from socket', this.$root.socket.id)
this.$root.socket.removeAllListeners()
this.$root.socket.disconnect()
}
this.$axios.$post('/logout', logoutPayload).catch((error) => {
console.error(error)
})
if (localStorage.getItem('token')) {
localStorage.removeItem('token')
}
this.$store.commit('libraries/setUserPlaylists', [])
this.$store.commit('libraries/setCollections', [])
this.$router.push('/login')
this.$axios
.$post('/logout')
.then((logoutPayload) => {
const redirect_url = logoutPayload.redirect_url
if (redirect_url) {
window.location.href = redirect_url
} else {
this.$router.push('/login')
}
})
.catch((error) => {
console.error(error)
})
},
resetForm() {
this.password = null

View File

@@ -142,7 +142,7 @@
</template>
<div class="w-full h-full max-h-full text-sm rounded-lg bg-bg shadow-lg border border-black-300 relative">
<div v-if="!chapterData" class="flex p-20">
<ui-text-input-with-label v-model="asinInput" label="ASIN" />
<ui-text-input-with-label v-model.trim="asinInput" label="ASIN" />
<ui-dropdown v-model="regionInput" :label="$strings.LabelRegion" small :items="audibleRegions" class="w-32 mx-1" />
<ui-btn small color="primary" class="mt-5" @click="findChapters">{{ $strings.ButtonSearch }}</ui-btn>
</div>

View File

@@ -17,7 +17,10 @@
</div>
<p v-if="author.description" class="text-white text-opacity-60 uppercase text-xs mb-2">{{ $strings.LabelDescription }}</p>
<p class="text-white max-w-3xl text-sm leading-5 whitespace-pre-wrap">{{ author.description }}</p>
<p ref="description" id="author-description" class="text-white max-w-3xl text-base whitespace-pre-wrap" :class="{ 'show-full': showFullDescription }">{{ author.description }}</p>
<button v-if="isDescriptionClamped" class="py-0.5 flex items-center text-slate-300 hover:text-white" @click="showFullDescription = !showFullDescription">
{{ showFullDescription ? 'Read less' : 'Read more' }} <span class="material-icons text-xl pl-1">{{ showFullDescription ? 'expand_less' : 'expand_more' }}</span>
</button>
</div>
</div>
@@ -62,7 +65,10 @@ export default {
}
},
data() {
return {}
return {
isDescriptionClamped: false,
showFullDescription: false
}
},
computed: {
streamLibraryItem() {
@@ -82,6 +88,10 @@ export default {
}
},
methods: {
checkDescriptionClamped() {
if (!this.$refs.description) return
this.isDescriptionClamped = this.$refs.description.scrollHeight > this.$refs.description.clientHeight
},
editAuthor() {
this.$store.commit('globals/showEditAuthorModal', this.author)
},
@@ -93,6 +103,7 @@ export default {
series: this.authorSeries,
libraryItems: this.libraryItems
}
this.$nextTick(this.checkDescriptionClamped)
}
},
authorRemoved(author) {
@@ -104,6 +115,7 @@ export default {
},
mounted() {
if (!this.author) this.$router.replace('/')
this.checkDescriptionClamped()
this.$root.socket.on('author_updated', this.authorUpdated)
this.$root.socket.on('author_removed', this.authorRemoved)
@@ -113,4 +125,19 @@ export default {
this.$root.socket.off('author_removed', this.authorRemoved)
}
}
</script>
</script>
<style scoped>
#author-description {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
max-height: 6.25rem;
transition: all 0.3s ease-in-out;
}
#author-description.show-full {
-webkit-line-clamp: unset;
max-height: 999rem;
}
</style>

View File

@@ -20,44 +20,44 @@
<div class="overflow-hidden">
<transition name="slide">
<div v-if="openMapOptions" class="flex flex-wrap">
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 w-1/2">
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.subtitle" />
<ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" :label="$strings.LabelSubtitle" class="mb-4 ml-4" />
<ui-text-input-with-label ref="subtitleInput" v-model="batchDetails.subtitle" :disabled="!selectedBatchUsage.subtitle" :label="$strings.LabelSubtitle" class="mb-5 ml-4" />
</div>
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
<div v-if="!isPodcastLibrary" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.authors" />
<!-- Authors filter only contains authors in this library, uses filter data -->
<ui-multi-select-query-input ref="authorsSelect" v-model="batchDetails.authors" :disabled="!selectedBatchUsage.authors" :label="$strings.LabelAuthors" filter-key="authors" class="mb-4 ml-4" />
<ui-multi-select-query-input ref="authorsSelect" v-model="batchDetails.authors" :disabled="!selectedBatchUsage.authors" :label="$strings.LabelAuthors" filter-key="authors" class="mb-5 ml-4" />
</div>
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 w-1/2">
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.publishedYear" />
<ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" :label="$strings.LabelPublishYear" class="mb-4 ml-4" />
<ui-text-input-with-label ref="publishedYearInput" v-model="batchDetails.publishedYear" :disabled="!selectedBatchUsage.publishedYear" :label="$strings.LabelPublishYear" class="mb-5 ml-4" />
</div>
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
<div v-if="!isPodcastLibrary" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.series" />
<ui-multi-select ref="seriesSelect" v-model="batchDetails.series" :disabled="!selectedBatchUsage.series" :label="$strings.LabelSeries" :items="existingSeriesNames" @newItem="newSeriesItem" @removedItem="removedSeriesItem" class="mb-4 ml-4" />
<ui-multi-select ref="seriesSelect" v-model="batchDetails.series" :disabled="!selectedBatchUsage.series" :label="$strings.LabelSeries" :items="existingSeriesNames" @newItem="newSeriesItem" @removedItem="removedSeriesItem" class="mb-5 ml-4" />
</div>
<div class="flex items-center px-4 w-1/2">
<div class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.genres" />
<ui-multi-select ref="genresSelect" v-model="batchDetails.genres" :disabled="!selectedBatchUsage.genres" :label="$strings.LabelGenres" :items="genreItems" @newItem="newGenreItem" @removedItem="removedGenreItem" class="mb-4 ml-4" />
<ui-multi-select ref="genresSelect" v-model="batchDetails.genres" :disabled="!selectedBatchUsage.genres" :label="$strings.LabelGenres" :items="genreItems" @newItem="newGenreItem" @removedItem="removedGenreItem" class="mb-5 ml-4" />
</div>
<div class="flex items-center px-4 w-1/2">
<div class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.tags" />
<ui-multi-select ref="tagsSelect" v-model="batchDetails.tags" :label="$strings.LabelTags" :disabled="!selectedBatchUsage.tags" :items="tagItems" @newItem="newTagItem" @removedItem="removedTagItem" class="mb-4 ml-4" />
<ui-multi-select ref="tagsSelect" v-model="batchDetails.tags" :label="$strings.LabelTags" :disabled="!selectedBatchUsage.tags" :items="tagItems" @newItem="newTagItem" @removedItem="removedTagItem" class="mb-5 ml-4" />
</div>
<div v-if="!isPodcastLibrary" class="flex items-center px-4 w-1/2">
<div v-if="!isPodcastLibrary" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.narrators" />
<ui-multi-select ref="narratorsSelect" v-model="batchDetails.narrators" :disabled="!selectedBatchUsage.narrators" :label="$strings.LabelNarrators" :items="narratorItems" @newItem="newNarratorItem" @removedItem="removedNarratorItem" class="mb-4 ml-4" />
<ui-multi-select ref="narratorsSelect" v-model="batchDetails.narrators" :disabled="!selectedBatchUsage.narrators" :label="$strings.LabelNarrators" :items="narratorItems" @newItem="newNarratorItem" @removedItem="removedNarratorItem" class="mb-5 ml-4" />
</div>
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 w-1/2">
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.publisher" />
<ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" :label="$strings.LabelPublisher" class="mb-4 ml-4" />
<ui-text-input-with-label ref="publisherInput" v-model="batchDetails.publisher" :disabled="!selectedBatchUsage.publisher" :label="$strings.LabelPublisher" class="mb-5 ml-4" />
</div>
<div v-if="!isMapAppend" class="flex items-center px-4 w-1/2">
<div v-if="!isMapAppend" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.language" />
<ui-text-input-with-label ref="languageInput" v-model="batchDetails.language" :disabled="!selectedBatchUsage.language" :label="$strings.LabelLanguage" class="mb-4 ml-4" />
<ui-text-input-with-label ref="languageInput" v-model="batchDetails.language" :disabled="!selectedBatchUsage.language" :label="$strings.LabelLanguage" class="mb-5 ml-4" />
</div>
<div v-if="!isMapAppend" class="flex items-center px-4 w-1/2">
<div v-if="!isMapAppend" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.explicit" />
<div class="ml-4">
<ui-checkbox
@@ -71,6 +71,20 @@
/>
</div>
</div>
<div v-if="!isPodcastLibrary && !isMapAppend" class="flex items-center px-4 h-18 w-1/2">
<ui-checkbox v-model="selectedBatchUsage.abridged" />
<div class="ml-4">
<ui-checkbox
v-model="batchDetails.abridged"
:label="$strings.LabelAbridged"
:disabled="!selectedBatchUsage.abridged"
:checkbox-bg="!selectedBatchUsage.abridged ? 'bg' : 'primary'"
:check-color="!selectedBatchUsage.abridged ? 'gray-600' : 'green-500'"
border-color="gray-600"
:label-class="!selectedBatchUsage.abridged ? 'pl-2 text-base text-gray-400 font-semibold' : 'pl-2 text-base font-semibold'"
/>
</div>
</div>
<div class="w-full flex items-center justify-end p-4">
<ui-btn color="success" :disabled="!hasSelectedBatchUsage" :padding-x="8" small class="text-base" :loading="isProcessing" @click="mapBatchDetails">{{ $strings.ButtonApply }}</ui-btn>
@@ -139,7 +153,8 @@ export default {
narrators: [],
publisher: null,
language: null,
explicit: false
explicit: false,
abridged: false
},
selectedBatchUsage: {
subtitle: false,
@@ -151,7 +166,8 @@ export default {
narrators: false,
publisher: false,
language: false,
explicit: false
explicit: false,
abridged: false
},
appendableKeys: ['authors', 'genres', 'tags', 'narrators', 'series'],
openMapOptions: false

View File

@@ -1,6 +1,18 @@
<template>
<div id="authentication-settings">
<app-settings-content :header-text="$strings.HeaderAuthentication">
<div class="w-full border border-white/10 rounded-xl p-4 my-4 bg-primary/25">
<div class="flex items-center">
<ui-checkbox v-model="showCustomLoginMessage" checkbox-bg="bg" />
<p class="text-lg pl-4">Custom Message on Login</p>
</div>
<transition name="slide">
<div v-if="showCustomLoginMessage" class="w-full pt-4">
<ui-rich-text-editor v-model="newAuthSettings.authLoginCustomMessage" />
</div>
</transition>
</div>
<div class="w-full border border-white/10 rounded-xl p-4 my-4 bg-primary/25">
<div class="flex items-center">
<ui-checkbox v-model="enableLocalAuth" checkbox-bg="bg" />
@@ -46,6 +58,9 @@
<ui-text-input-with-label ref="openidClientSecret" v-model="newAuthSettings.authOpenIDClientSecret" :disabled="savingSettings" :label="'Client Secret'" class="mb-2" />
<ui-multi-select ref="redirectUris" v-model="newAuthSettings.authOpenIDMobileRedirectURIs" :items="newAuthSettings.authOpenIDMobileRedirectURIs" :label="$strings.LabelMobileRedirectURIs" class="mb-2" :menuDisabled="true" :disabled="savingSettings" />
<p class="pl-4 text-sm text-gray-300 mb-2" v-html="$strings.LabelMobileRedirectURIsDescription" />
<ui-text-input-with-label ref="buttonTextInput" v-model="newAuthSettings.authOpenIDButtonText" :disabled="savingSettings" :label="$strings.LabelButtonText" class="mb-2" />
<div class="flex items-center pt-1 mb-2">
@@ -100,6 +115,7 @@ export default {
return {
enableLocalAuth: false,
enableOpenIDAuth: false,
showCustomLoginMessage: false,
savingSettings: false,
newAuthSettings: {}
}
@@ -187,6 +203,25 @@ export default {
this.$toast.error('Client Secret required')
isValid = false
}
function isValidRedirectURI(uri) {
// Check for somestring://someother/string
const pattern = new RegExp('^\\w+://[\\w\\.-]+(/[\\w\\./-]*)*$', 'i')
return pattern.test(uri)
}
const uris = this.newAuthSettings.authOpenIDMobileRedirectURIs
if (uris.includes('*') && uris.length > 1) {
this.$toast.error('Mobile Redirect URIs: Asterisk (*) must be the only entry if used')
isValid = false
} else {
uris.forEach((uri) => {
if (uri !== '*' && !isValidRedirectURI(uri)) {
this.$toast.error(`Mobile Redirect URIs: Invalid URI ${uri}`)
isValid = false
}
})
}
return isValid
},
async saveSettings() {
@@ -199,6 +234,10 @@ export default {
return
}
if (!this.showCustomLoginMessage || !this.newAuthSettings.authLoginCustomMessage?.trim()) {
this.newAuthSettings.authLoginCustomMessage = null
}
this.newAuthSettings.authActiveAuthMethods = []
if (this.enableLocalAuth) this.newAuthSettings.authActiveAuthMethods.push('local')
if (this.enableOpenIDAuth) this.newAuthSettings.authActiveAuthMethods.push('openid')
@@ -208,7 +247,11 @@ export default {
.$patch('/api/auth-settings', this.newAuthSettings)
.then((data) => {
this.$store.commit('setServerSettings', data.serverSettings)
this.$toast.success('Server settings updated')
if (data.updated) {
this.$toast.success('Server settings updated')
} else {
this.$toast.info(this.$strings.MessageNoUpdatesWereNecessary)
}
})
.catch((error) => {
console.error('Failed to update server settings', error)
@@ -224,6 +267,7 @@ export default {
}
this.enableLocalAuth = this.authMethods.includes('local')
this.enableOpenIDAuth = this.authMethods.includes('openid')
this.showCustomLoginMessage = !!this.authSettings.authLoginCustomMessage
}
},
mounted() {

View File

@@ -178,9 +178,9 @@
</a>
<p class="pl-4 pr-2 text-sm text-yellow-400">
{{ $strings.MessageJoinUsOn }}
<a class="underline" href="https://discord.gg/pJsjuNCKRq" target="_blank">discord</a>
<a class="underline" href="https://discord.gg/HQgCbd6E75" target="_blank">discord</a>
</p>
<a href="https://discord.gg/pJsjuNCKRq" target="_blank" class="text-white hover:text-gray-200 hover:scale-150 hover:rotate-6 transform duration-500">
<a href="https://discord.gg/HQgCbd6E75" target="_blank" class="text-white hover:text-gray-200 hover:scale-150 hover:rotate-6 transform duration-500">
<svg width="31" height="24" viewBox="0 0 71 55" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0)">
<path

View File

@@ -0,0 +1,74 @@
<template>
<div class="relative">
<app-settings-content :header-text="$strings.HeaderCustomMetadataProviders">
<template #header-prefix>
<nuxt-link to="/config/item-metadata-utils" class="w-8 h-8 flex items-center justify-center rounded-full cursor-pointer hover:bg-white hover:bg-opacity-10 text-center mr-2">
<span class="material-icons text-2xl">arrow_back</span>
</nuxt-link>
</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">
<span class="material-icons text-xl w-5 text-gray-200">help_outline</span>
</a>
</ui-tooltip>
<div class="flex-grow" />
<ui-btn color="primary" small @click="setShowAddModal">{{ $strings.ButtonAdd }}</ui-btn>
</template>
<tables-custom-metadata-provider-table :providers="providers" :processing.sync="processing" class="pt-2" @removed="providerRemoved" />
<modals-add-custom-metadata-provider-modal ref="addModal" v-model="showAddModal" @added="providerAdded" />
</app-settings-content>
</div>
</template>
<script>
export default {
async asyncData({ store, redirect }) {
if (!store.getters['user/getIsAdminOrUp']) {
redirect('/')
return
}
return {}
},
data() {
return {
showAddModal: false,
processing: false,
providers: []
}
},
methods: {
providerRemoved(providerId) {
this.providers = this.providers.filter((p) => p.id !== providerId)
},
providerAdded(provider) {
this.providers.push(provider)
},
setShowAddModal() {
this.showAddModal = true
},
loadProviders() {
this.processing = true
this.$axios
.$get('/api/custom-metadata-providers')
.then((res) => {
this.providers = res.providers
})
.catch((error) => {
console.error('Failed', error)
this.$toast.error('Failed to load custom metadata providers')
})
.finally(() => {
this.processing = false
})
}
},
mounted() {
this.loadProviders()
}
}
</script>
<style></style>

View File

@@ -13,6 +13,12 @@
<span class="material-icons">arrow_forward</span>
</div>
</nuxt-link>
<nuxt-link to="/config/item-metadata-utils/custom-metadata-providers" class="block w-full rounded bg-primary/40 hover:bg-primary/60 text-gray-300 hover:text-white p-4 my-2">
<div class="flex justify-between">
<p>{{ $strings.HeaderCustomMetadataProviders }}</p>
<span class="material-icons">arrow_forward</span>
</div>
</nuxt-link>
</app-settings-content>
</div>
</template>

View File

@@ -8,7 +8,7 @@
</div>
<div class="relative">
<div ref="container" class="relative w-full h-full bg-primary border-bg overflow-x-hidden overflow-y-auto text-red shadow-inner rounded-md" style="max-height: 800px; min-height: 550px">
<div ref="container" id="log-container" class="relative w-full h-full bg-primary border-bg overflow-x-hidden overflow-y-auto text-red shadow-inner rounded-md" style="min-height: 550px">
<template v-for="(log, index) in logs">
<div :key="index" class="flex flex-nowrap px-2 py-1 items-start text-sm bg-opacity-10" :class="`bg-${logColors[log.level]}`">
<p class="text-gray-400 w-36 font-mono text-xs">{{ log.timestamp }}</p>
@@ -136,7 +136,15 @@ export default {
this.loadedLogs = this.loadedLogs.slice(-5000)
}
},
init(attempts = 0) {
async loadLoggerData() {
const loggerData = await this.$axios.$get('/api/logger-data').catch((error) => {
console.error('Failed to load logger data', error)
this.$toast.error('Failed to load logger data')
})
this.loadedLogs = loggerData?.currentDailyLogs || []
},
async init(attempts = 0) {
if (!this.$root.socket) {
if (attempts > 10) {
return console.error('Failed to setup socket listeners')
@@ -147,14 +155,11 @@ export default {
return
}
await this.loadLoggerData()
this.newServerSettings = this.serverSettings ? { ...this.serverSettings } : {}
this.$root.socket.on('daily_logs', this.dailyLogsLoaded)
this.$root.socket.on('log', this.logEvtReceived)
this.$root.socket.emit('set_log_listener', this.newServerSettings.logLevel)
this.$root.socket.emit('fetch_daily_logs')
},
dailyLogsLoaded(lines) {
this.loadedLogs = lines
}
},
updated() {
@@ -166,13 +171,15 @@ export default {
beforeDestroy() {
if (!this.$root.socket) return
this.$root.socket.emit('remove_log_listener')
this.$root.socket.off('daily_logs', this.dailyLogsLoaded)
this.$root.socket.off('log', this.logEvtReceived)
}
}
</script>
<style scoped>
#log-container {
height: calc(100vh - 285px);
}
.logmessage {
width: calc(100% - 208px);
}

View File

@@ -5,37 +5,72 @@
<ui-dropdown v-model="selectedUser" :items="userItems" :label="$strings.LabelFilterByUser" small class="max-w-48" @input="updateUserFilter" />
</div>
<div v-if="listeningSessions.length" class="block max-w-full">
<div v-if="listeningSessions.length" class="block max-w-full relative">
<table class="userSessionsTable">
<tr class="bg-primary bg-opacity-40">
<th class="w-48 min-w-48 text-left">{{ $strings.LabelItem }}</th>
<th class="w-20 min-w-20 text-left hidden md:table-cell">{{ $strings.LabelUser }}</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="flex-grow hidden sm:table-cell">{{ $strings.LabelLastUpdate }}</th>
<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="flex-grow text-left" :colspan="7">
<div class="flex items-center">
<p>{{ $getString('MessageSelected', [numSelected]) }}</p>
<div class="flex-grow" />
<ui-btn small color="error" :loading="deletingSessions" @click.stop="removeSessionsClick">{{ $strings.ButtonRemove }}</ui-btn>
</div>
</th>
<th v-if="!numSelected" class="flex-grow sm:flex-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-icons 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-icons 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-icons 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-icons text-base pl-px hidden sm:inline-block">{{ sortDesc ? 'arrow_drop_down' : 'arrow_drop_up' }}</span>
</div>
</th>
<th v-if="!numSelected" class="flex-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-icons 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="cursor-pointer" @click="showSession(session)">
<td class="py-1 max-w-48">
<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 flex-grow sm:flex-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">
<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">
<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">
<td class="hidden sm:table-cell w-32 min-w-32">
<p class="text-xs" v-html="getDeviceInfoString(session.deviceInfo)" />
</td>
<td class="text-center">
<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" @click.stop="clickCurrentTime(session)">
<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">
@@ -45,10 +80,22 @@
</td>
</tr>
</table>
<div class="flex items-center justify-end my-2">
<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">Page {{ currentPage + 1 }} of {{ numPages }}</p>
<ui-icon-btn icon="arrow_forward_ios" :size="7" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" />
<!-- table bottom options -->
<div class="flex items-center my-2">
<div class="flex-grow" />
<div class="hidden sm:inline-flex items-center">
<p class="text-sm whitespace-nowrap">{{ $strings.LabelRowsPerPage }}</p>
<ui-dropdown v-model="itemsPerPage" :items="itemsPerPageOptions" small class="w-24 mx-2" @input="updatedItemsPerPage" />
</div>
<div class="inline-flex items-center">
<p class="text-sm mx-2">Page {{ currentPage + 1 }} of {{ numPages }}</p>
<ui-icon-btn icon="arrow_back_ios_new" :size="9" icon-font-size="1rem" class="mx-1" :disabled="currentPage === 0" @click="prevPage" />
<ui-icon-btn icon="arrow_forward_ios" :size="9" icon-font-size="1rem" class="mx-1" :disabled="currentPage >= numPages - 1" @click="nextPage" />
</div>
</div>
<div v-if="deletingSessions || loading" class="absolute inset-0 w-full h-full flex items-center justify-center">
<ui-loading-indicator />
</div>
</div>
<p v-else class="text-white text-opacity-50">{{ $strings.MessageNoListeningSessions }}</p>
@@ -128,6 +175,7 @@ export default {
},
data() {
return {
loading: false,
showSessionModal: false,
selectedSession: null,
listeningSessions: [],
@@ -138,7 +186,11 @@ export default {
itemsPerPage: 10,
userFilter: null,
selectedUser: '',
processingGoToTimestamp: false
sortBy: 'updatedAt',
sortDesc: true,
processingGoToTimestamp: false,
deletingSessions: false,
itemsPerPageOptions: [10, 25, 50, 100]
}
},
computed: {
@@ -162,9 +214,85 @@ export default {
},
timeFormat() {
return this.$store.state.serverSettings.timeFormat
},
numSelected() {
return this.listeningSessions.filter((s) => s.selected).length
},
isAllSelected: {
get() {
return this.numSelected === this.listeningSessions.length
},
set(val) {
this.setSelectionForAll(val)
}
}
},
methods: {
isSortSelected(column) {
return this.sortBy === column
},
sortColumn(column) {
if (this.sortBy === column) {
this.sortDesc = !this.sortDesc
} else {
this.sortBy = column
}
this.loadSessions(this.currentPage)
},
removeSelectedSessions() {
if (!this.numSelected) return
this.deletingSessions = true
let isAllSessions = this.isAllSelected
const payload = {
sessions: this.listeningSessions.filter((s) => s.selected).map((s) => s.id)
}
this.$axios
.$post(`/api/sessions/batch/delete`, payload)
.then(() => {
this.$toast.success('Sessions removed')
if (isAllSessions) {
// If all sessions were removed from the current page then go to the previous page
if (this.currentPage > 0) {
this.currentPage--
}
this.loadSessions(this.currentPage)
} else {
// Filter out the deleted sessions
this.listeningSessions = this.listeningSessions.filter((ls) => !payload.sessions.includes(ls.id))
}
})
.catch((error) => {
const errorMsg = error.response?.data || 'Failed to remove sessions'
this.$toast.error(errorMsg)
})
.finally(() => {
this.deletingSessions = false
})
},
removeSessionsClick() {
if (!this.numSelected) return
const payload = {
message: this.$getString('MessageConfirmRemoveListeningSessions', [this.numSelected]),
callback: (confirmed) => {
if (confirmed) {
this.removeSelectedSessions()
}
},
type: 'yesNo'
}
this.$store.commit('globals/setConfirmPrompt', payload)
},
setSelectionForAll(val) {
this.listeningSessions = this.listeningSessions.map((s) => {
s.selected = val
return s
})
},
updatedItemsPerPage() {
this.currentPage = 0
this.loadSessions(this.currentPage)
},
closedSession() {
this.loadOpenSessions()
},
@@ -252,6 +380,13 @@ export default {
nextPage() {
this.loadSessions(this.currentPage + 1)
},
clickSessionRow(session) {
if (this.numSelected > 0) {
session.selected = !session.selected
} else {
this.showSession(session)
}
},
showSession(session) {
this.selectedSession = session
this.showSessionModal = true
@@ -274,11 +409,21 @@ export default {
return 'Unknown'
},
async loadSessions(page) {
const userFilterQuery = this.selectedUser ? `&user=${this.selectedUser}` : ''
const data = await this.$axios.$get(`/api/sessions?page=${page}&itemsPerPage=${this.itemsPerPage}${userFilterQuery}`).catch((err) => {
this.loading = true
const urlSearchParams = new URLSearchParams()
urlSearchParams.set('page', page)
urlSearchParams.set('itemsPerPage', this.itemsPerPage)
urlSearchParams.set('sort', this.sortBy)
urlSearchParams.set('desc', this.sortDesc ? '1' : '0')
if (this.selectedUser) {
urlSearchParams.set('user', this.selectedUser)
}
const data = await this.$axios.$get(`/api/sessions?${urlSearchParams.toString()}`).catch((err) => {
console.error('Failed to load listening sessions', err)
return null
})
this.loading = false
if (!data) {
this.$toast.error('Failed to load listening sessions')
return
@@ -287,8 +432,13 @@ export default {
this.numPages = data.numPages
this.total = data.total
this.currentPage = data.page
this.listeningSessions = data.sessions
this.userFilter = data.userFilter
this.listeningSessions = data.sessions.map((ls) => {
return {
...ls,
selected: false
}
})
this.userFilter = data.userId
},
async loadOpenSessions() {
const data = await this.$axios.$get('/api/sessions/open').catch((err) => {
@@ -326,15 +476,18 @@ export default {
.userSessionsTable tr:first-child {
background-color: #272727;
}
.userSessionsTable tr:not(:first-child) {
.userSessionsTable tr:not(:first-child):not(.selected) {
background-color: #373838;
}
.userSessionsTable tr:not(:first-child):nth-child(odd) {
.userSessionsTable tr:not(:first-child):nth-child(odd):not(.selected):not(:hover) {
background-color: #2f2f2f;
}
.userSessionsTable tr:hover:not(:first-child) {
background-color: #474747;
}
.userSessionsTable tr.selected {
background-color: #474747;
}
.userSessionsTable td {
padding: 4px 8px;
}

View File

@@ -1,6 +1,9 @@
<template>
<div>
<app-settings-content :header-text="$strings.HeaderYourStats">
<!-- Year in review banner shown at the top in December and January -->
<stats-year-in-review-banner v-if="showYearInReviewBanner" />
<app-settings-content :header-text="$strings.HeaderYourStats" class="!mb-4">
<div class="flex justify-center">
<div class="flex p-2">
<svg class="hidden sm:block h-14 w-14 lg:h-18 lg:w-18" viewBox="0 0 24 24">
@@ -63,6 +66,9 @@
</div>
<stats-heatmap v-if="listeningStats" :days-listening="listeningStats.days" class="my-2" />
</app-settings-content>
<!-- Year in review banner shown at the bottom Feb - Nov -->
<stats-year-in-review-banner v-if="!showYearInReviewBanner" />
</div>
</template>
@@ -71,7 +77,8 @@ export default {
data() {
return {
listeningStats: null,
windowWidth: 0
windowWidth: 0,
showYearInReviewBanner: false
}
},
watch: {
@@ -119,6 +126,12 @@ export default {
console.error('Failed to load listening sesions', err)
return []
})
let month = new Date().getMonth()
// January and December show year in review banner
if (month === 11 || month === 0) {
this.showYearInReviewBanner = true
}
}
},
mounted() {

View File

@@ -1,6 +1,6 @@
<template>
<div id="page-wrapper" class="bg-bg page overflow-hidden" :class="streamLibraryItem ? 'streaming' : ''">
<div class="w-full h-full overflow-y-auto px-2 py-6 lg:p-8">
<div id="item-page-wrapper" class="w-full h-full overflow-y-auto px-2 py-6 lg:p-8">
<div class="flex flex-col lg:flex-row max-w-6xl mx-auto">
<div class="w-full flex justify-center lg:block lg:w-52" style="min-width: 208px">
<div class="relative group" style="height: fit-content">
@@ -125,7 +125,10 @@
</div>
<div class="my-4 w-full">
<p class="text-base text-gray-100 whitespace-pre-line">{{ description }}</p>
<p ref="description" id="item-description" class="text-base text-gray-100 whitespace-pre-line mb-1" :class="{ 'show-full': showFullDescription }">{{ description }}</p>
<button v-if="isDescriptionClamped" class="py-0.5 flex items-center text-slate-300 hover:text-white" @click="showFullDescription = !showFullDescription">
{{ showFullDescription ? 'Read less' : 'Read more' }} <span class="material-icons text-xl pl-1">{{ showFullDescription ? 'expand_less' : 'expand_more' }}</span>
</button>
</div>
<div v-if="invalidAudioFiles.length" class="bg-error border-red-800 shadow-md p-4">
@@ -136,7 +139,7 @@
<widgets-audiobook-data v-if="tracks.length" :library-item-id="libraryItemId" :is-file="isFile" :media="media" />
<tables-podcast-episodes-table v-if="isPodcast" :library-item="libraryItem" />
<tables-podcast-lazy-episodes-table v-if="isPodcast" :library-item="libraryItem" />
<tables-chapters-table v-if="chapters.length" :library-item="libraryItem" class="mt-6" />
@@ -182,7 +185,9 @@ export default {
podcastFeedEpisodes: [],
episodesDownloading: [],
episodeDownloadsQueued: [],
showBookmarksModal: false
showBookmarksModal: false,
isDescriptionClamped: false,
showFullDescription: false
}
},
computed: {
@@ -596,10 +601,15 @@ export default {
this.$store.commit('setBookshelfBookIds', [])
this.$store.commit('showEditModal', this.libraryItem)
},
checkDescriptionClamped() {
if (!this.$refs.description) return
this.isDescriptionClamped = this.$refs.description.scrollHeight > this.$refs.description.clientHeight
},
libraryItemUpdated(libraryItem) {
if (libraryItem.id === this.libraryItemId) {
console.log('Item was updated', libraryItem)
this.libraryItem = libraryItem
this.$nextTick(this.checkDescriptionClamped)
}
},
clearProgressClick() {
@@ -756,6 +766,8 @@ export default {
}
},
mounted() {
this.checkDescriptionClamped()
this.episodeDownloadsQueued = this.libraryItem.episodeDownloadsQueued || []
this.episodesDownloading = this.libraryItem.episodesDownloading || []
@@ -782,3 +794,18 @@ export default {
}
}
</script>
<style scoped>
#item-description {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
max-height: 6.25rem;
transition: all 0.3s ease-in-out;
}
#item-description.show-full {
-webkit-line-clamp: unset;
max-height: 999rem;
}
</style>

View File

@@ -3,7 +3,7 @@
<app-book-shelf-toolbar page="authors" is-home :authors="authors" />
<div id="bookshelf" class="w-full h-full p-8 overflow-y-auto">
<div class="flex flex-wrap justify-center">
<template v-for="author in authors">
<template v-for="author in authorsSorted">
<cards-author-card :key="author.id" :author="author" :width="160" :height="200" class="p-3" @edit="editAuthor" />
</template>
</div>
@@ -44,6 +44,22 @@ export default {
},
selectedAuthor() {
return this.$store.state.globals.selectedAuthor
},
authorSortBy() {
return this.$store.getters['user/getUserSetting']('authorSortBy') || 'name'
},
authorSortDesc() {
return !!this.$store.getters['user/getUserSetting']('authorSortDesc')
},
authorsSorted() {
const sortProp = this.authorSortBy
const bDesc = this.authorSortDesc ? -1 : 1
return this.authors.sort((a, b) => {
if (typeof a[sortProp] === 'number' && typeof b[sortProp] === 'number') {
return a[sortProp] > b[sortProp] ? bDesc : -bDesc
}
return a[sortProp].localeCompare(b[sortProp], undefined, { sensitivity: 'base' }) * bDesc
})
}
},
methods: {

View File

@@ -8,11 +8,11 @@
<p v-if="!recentEpisodes.length && !processing" class="text-center text-xl">{{ $strings.MessageNoEpisodes }}</p>
<template v-for="(episode, index) in episodesMapped">
<div :key="episode.id" class="flex py-5 cursor-pointer relative" @click.stop="clickEpisode(episode)">
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="96" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="hidden md:block" />
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId, episode.updatedAt)" :width="96" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="hidden md:block" />
<div class="flex-grow pl-4 max-w-2xl">
<!-- mobile -->
<div class="flex md:hidden mb-2">
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId)" :width="48" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="md:hidden" />
<covers-preview-cover :src="$store.getters['globals/getLibraryItemCoverSrcById'](episode.libraryItemId, episode.updatedAt)" :width="48" :book-cover-aspect-ratio="bookCoverAspectRatio" :show-resolution="false" class="md:hidden" />
<div class="flex-grow px-2">
<div class="flex items-center">
<div class="flex" @click.stop>
@@ -45,7 +45,7 @@
<widgets-podcast-type-indicator :type="episode.episodeType" />
</div>
<p class="text-sm text-gray-200 mb-4 episode-subtitle-long" v-html="episode.subtitle || episode.description" />
<p class="text-sm text-gray-200 mb-4 line-clamp-4" v-html="episode.subtitle || episode.description" />
<div class="flex items-center">
<button class="h-8 px-4 border border-white border-opacity-20 hover:bg-white hover:bg-opacity-10 rounded-full flex items-center justify-center cursor-pointer focus:outline-none" :class="episode.progress && episode.progress.isFinished ? 'text-white text-opacity-40' : ''" @click.stop="playClick(episode)">
@@ -54,9 +54,16 @@
<p class="pl-2 pr-1 text-sm font-semibold">{{ getButtonText(episode) }}</p>
</button>
<button v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" class="h-8 w-8 flex justify-center items-center mx-2" :class="playerQueueEpisodeIdMap[episode.id] ? 'text-success' : ''" @click.stop="queueBtnClick(episode)">
<span class="material-icons-outlined text-2xl">{{ playerQueueEpisodeIdMap[episode.id] ? 'playlist_add_check' : 'playlist_add' }}</span>
</button>
<ui-tooltip v-if="libraryItemIdStreaming && !isStreamingFromDifferentLibrary" :text="playerQueueEpisodeIdMap[episode.id] ? $strings.MessageRemoveFromPlayerQueue : $strings.MessageAddToPlayerQueue" :class="playerQueueEpisodeIdMap[episode.id] ? 'text-success' : ''" direction="top">
<ui-icon-btn :icon="playerQueueEpisodeIdMap[episode.id] ? 'playlist_add_check' : 'playlist_play'" borderless @click="queueBtnClick(episode)" />
<!-- <button class="h-8 w-8 flex justify-center items-center mx-2" :class="playerQueueEpisodeIdMap[episode.id] ? 'text-success' : ''" @click.stop="queueBtnClick(episode)">
<span class="material-icons-outlined text-2xl">{{ playerQueueEpisodeIdMap[episode.id] ? 'playlist_add_check' : 'playlist_add' }}</span>
</button> -->
</ui-tooltip>
<ui-tooltip :text="$strings.LabelYourPlaylists" direction="top">
<ui-icon-btn icon="playlist_add" borderless @click="clickAddToPlaylist(episode)" />
</ui-tooltip>
</div>
</div>
@@ -136,6 +143,15 @@ export default {
}
},
methods: {
clickAddToPlaylist(episode) {
// Makeshift libraryItem
const libraryItem = {
id: episode.libraryItemId,
media: episode.podcast
}
this.$store.commit('globals/setSelectedPlaylistItems', [{ libraryItem: libraryItem, episode }])
this.$store.commit('globals/setShowPlaylistsModal', true)
},
async clickEpisode(episode) {
if (this.openingItem) return
this.openingItem = true
@@ -155,7 +171,9 @@ export default {
if (this.episodeIdStreaming === episode.id) return this.streamIsPlaying ? 'Streaming' : 'Play'
if (!episode.progress) return this.$elapsedPretty(episode.duration)
if (episode.progress.isFinished) return 'Finished'
var remaining = Math.floor(episode.progress.duration - episode.progress.currentTime)
const duration = episode.progress.duration || episode.duration
const remaining = Math.floor(duration - episode.progress.currentTime)
return `${this.$elapsedPretty(remaining)} left`
},
playClick(episodeToPlay) {

View File

@@ -45,6 +45,11 @@
<script>
export default {
async asyncData({ params, query, store, app, redirect }) {
// Podcast search/add page is restricted to admins
if (!store.getters['user/getIsAdminOrUp']) {
return redirect(`/library/${params.library}`)
}
var libraryId = params.library
var libraryData = await store.dispatch('libraries/fetch', libraryId)
if (!libraryData) {
@@ -81,6 +86,9 @@ export default {
},
streamLibraryItem() {
return this.$store.state.streamLibraryItem
},
librarySettings() {
return this.$store.getters['libraries/getCurrentLibrarySettings']
}
},
methods: {
@@ -146,7 +154,12 @@ export default {
async submitSearch(term) {
this.processing = true
this.termSearched = ''
let results = await this.$axios.$get(`/api/search/podcast?term=${encodeURIComponent(term)}`).catch((error) => {
const searchParams = new URLSearchParams({
term,
country: this.librarySettings?.podcastSearchRegion || 'us'
})
let results = await this.$axios.$get(`/api/search/podcast?${searchParams.toString()}`).catch((error) => {
console.error('Search request failed', error)
return []
})

View File

@@ -1,6 +1,13 @@
<template>
<div class="w-full h-screen bg-bg">
<div class="w-full flex h-full items-center justify-center">
<div id="page-wrapper" class="w-full h-screen overflow-y-auto">
<div class="absolute z-0 top-0 left-0 px-6 py-3">
<div class="flex items-center">
<img src="~static/icon.svg" alt="Audiobookshelf Logo" class="w-10 min-w-10 h-10" />
<h1 class="text-xl ml-4 hidden lg:block hover:underline">audiobookshelf</h1>
</div>
</div>
<div class="relative z-10 w-full flex h-full items-center justify-center">
<div v-if="criticalError" class="w-full max-w-md rounded border border-error border-opacity-25 bg-error bg-opacity-10 p-4">
<p class="text-center text-lg font-semibold">{{ $strings.MessageServerCouldNotBeReached }}</p>
</div>
@@ -10,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="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" :disabled="processing" class="w-full mb-3 text-sm" />
<ui-text-input-with-label v-model.trim="newRoot.password" label="Password" type="password" :disabled="processing" class="w-full mb-3 text-sm" />
<ui-text-input-with-label v-model.trim="confirmPassword" label="Confirm Password" type="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" />
@@ -23,30 +30,34 @@
</div>
</form>
</div>
<div v-else-if="isInit" class="w-full max-w-md px-8 pb-8 pt-4 -mt-40">
<p class="text-3xl text-white text-center mb-4">{{ $strings.HeaderLogin }}</p>
<div v-else-if="isInit" class="w-full max-w-md px-8 pb-8 pt-4 lg:-mt-40">
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4">
<p class="text-2xl font-semibold text-center text-white mb-4">{{ $strings.HeaderLogin }}</p>
<div class="w-full h-px bg-white bg-opacity-10 my-4" />
<div class="w-full h-px bg-white bg-opacity-10 my-4" />
<p v-if="error" class="text-error text-center py-2">{{ error }}</p>
<p v-if="loginCustomMessage" class="py-2 default-style mb-2" v-html="loginCustomMessage"></p>
<form v-show="login_local" @submit.prevent="submitForm">
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelUsername }}</label>
<ui-text-input v-model="username" :disabled="processing" class="mb-3 w-full" />
<p v-if="error" class="text-error text-center py-2">{{ error }}</p>
<label class="text-xs text-gray-300 uppercase">{{ $strings.LabelPassword }}</label>
<ui-text-input v-model="password" type="password" :disabled="processing" class="w-full mb-3" />
<div class="w-full flex justify-end py-3">
<ui-btn type="submit" :disabled="processing" color="primary" class="leading-none">{{ processing ? 'Checking...' : $strings.ButtonSubmit }}</ui-btn>
<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" />
<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" />
<div class="w-full flex justify-end py-3">
<ui-btn type="submit" :disabled="processing" color="primary" class="leading-none">{{ processing ? 'Checking...' : $strings.ButtonSubmit }}</ui-btn>
</div>
</form>
<div v-if="login_local && login_openid" class="w-full h-px bg-white bg-opacity-10 my-4" />
<div class="w-full flex py-3">
<a v-if="login_openid" :href="openidAuthUri" class="w-full abs-btn outline-none rounded-md shadow-md relative border border-gray-600 text-center bg-primary text-white px-8 py-2 leading-none">
{{ openIDButtonText }}
</a>
</div>
</form>
<div v-if="login_local && login_openid" class="w-full h-px bg-white bg-opacity-10 my-4" />
<div class="w-full flex py-3">
<a v-if="login_openid" :href="openidAuthUri" class="w-full abs-btn outline-none rounded-md shadow-md relative border border-gray-600 text-center bg-primary text-white px-8 py-2 leading-none">
{{ openIDButtonText }}
</a>
</div>
</div>
</div>
@@ -113,6 +124,9 @@ export default {
},
openIDButtonText() {
return this.authFormData?.authOpenIDButtonText || 'Login with OpenId'
},
loginCustomMessage() {
return this.authFormData?.authLoginCustomMessage || null
}
},
methods: {
@@ -276,4 +290,4 @@ export default {
this.checkStatus()
}
}
</script>
</script>

View File

@@ -14,6 +14,20 @@
</div>
</div>
<div v-if="!selectedLibraryIsPodcast" class="flex items-center mb-6">
<label class="flex cursor-pointer pt-4">
<ui-toggle-switch v-model="fetchMetadata.enabled" class="inline-flex" />
<span class="pl-2 text-base">{{ $strings.LabelAutoFetchMetadata }}</span>
</label>
<ui-tooltip :text="$strings.LabelAutoFetchMetadataHelp" class="inline-flex pt-4">
<span class="pl-1 material-icons icon-text text-sm cursor-pointer">info_outlined</span>
</ui-tooltip>
<div class="flex-grow ml-4">
<ui-dropdown v-model="fetchMetadata.provider" :items="providers" :label="$strings.LabelProvider" />
</div>
</div>
<widgets-alert v-if="error" type="error">
<p class="text-lg">{{ error }}</p>
</widgets-alert>
@@ -61,9 +75,7 @@
</widgets-alert>
<!-- Item Upload cards -->
<template v-for="item in items">
<cards-item-upload-card :ref="`itemCard-${item.index}`" :key="item.index" :media-type="selectedLibraryMediaType" :item="item" :processing="processing" @remove="removeItem(item)" />
</template>
<cards-item-upload-card v-for="item in items" :key="item.index" :ref="`itemCard-${item.index}`" :media-type="selectedLibraryMediaType" :item="item" :provider="fetchMetadata.provider" :processing="processing" @remove="removeItem(item)" />
<!-- Upload/Reset btns -->
<div v-show="items.length" class="flex justify-end pb-8 pt-4">
@@ -92,13 +104,18 @@ export default {
selectedLibraryId: null,
selectedFolderId: null,
processing: false,
uploadFinished: false
uploadFinished: false,
fetchMetadata: {
enabled: false,
provider: null
}
}
},
watch: {
selectedLibrary(newVal) {
if (newVal && !this.selectedFolderId) {
this.setDefaultFolder()
this.setMetadataProvider()
}
}
},
@@ -133,6 +150,13 @@ export default {
selectedLibraryIsPodcast() {
return this.selectedLibraryMediaType === 'podcast'
},
providers() {
if (this.selectedLibraryIsPodcast) return this.$store.state.scanners.podcastProviders
return this.$store.state.scanners.providers
},
canFetchMetadata() {
return !this.selectedLibraryIsPodcast && this.fetchMetadata.enabled
},
selectedFolder() {
if (!this.selectedLibrary) return null
return this.selectedLibrary.folders.find((fold) => fold.id === this.selectedFolderId)
@@ -160,12 +184,16 @@ export default {
}
}
this.setDefaultFolder()
this.setMetadataProvider()
},
setDefaultFolder() {
if (!this.selectedFolderId && this.selectedLibrary && this.selectedLibrary.folders.length) {
this.selectedFolderId = this.selectedLibrary.folders[0].id
}
},
setMetadataProvider() {
this.fetchMetadata.provider ||= this.$store.getters['libraries/getLibraryProvider'](this.selectedLibraryId)
},
removeItem(item) {
this.items = this.items.filter((b) => b.index !== item.index)
if (!this.items.length) {
@@ -213,27 +241,49 @@ export default {
var items = e.dataTransfer.items || []
var itemResults = await this.uploadHelpers.getItemsFromDrop(items, this.selectedLibraryMediaType)
this.setResults(itemResults)
this.onItemsSelected(itemResults)
},
inputChanged(e) {
if (!e.target || !e.target.files) return
var _files = Array.from(e.target.files)
if (_files && _files.length) {
var itemResults = this.uploadHelpers.getItemsFromPicker(_files, this.selectedLibraryMediaType)
this.setResults(itemResults)
this.onItemsSelected(itemResults)
}
},
setResults(itemResults) {
onItemsSelected(itemResults) {
if (this.itemSelectionSuccessful(itemResults)) {
// setTimeout ensures the new item ref is attached before this method is called
setTimeout(this.attemptMetadataFetch, 0)
}
},
itemSelectionSuccessful(itemResults) {
console.log('Upload results', itemResults)
if (itemResults.error) {
this.error = itemResults.error
this.items = []
this.ignoredFiles = []
} else {
this.error = ''
this.items = itemResults.items
this.ignoredFiles = itemResults.ignoredFiles
return false
}
console.log('Upload results', itemResults)
this.error = ''
this.items = itemResults.items
this.ignoredFiles = itemResults.ignoredFiles
return true
},
attemptMetadataFetch() {
if (!this.canFetchMetadata) {
return false
}
this.items.forEach((item) => {
let itemRef = this.$refs[`itemCard-${item.index}`]
if (itemRef?.length) {
itemRef[0].fetchMetadata(this.fetchMetadata.provider)
}
})
},
updateItemCardStatus(index, status) {
var ref = this.$refs[`itemCard-${index}`]
@@ -248,8 +298,8 @@ export default {
var form = new FormData()
form.set('title', item.title)
if (!this.selectedLibraryIsPodcast) {
form.set('author', item.author)
form.set('series', item.series)
form.set('author', item.author || '')
form.set('series', item.series || '')
}
form.set('library', this.selectedLibraryId)
form.set('folder', this.selectedFolderId)
@@ -346,6 +396,8 @@ export default {
},
mounted() {
this.selectedLibraryId = this.$store.state.libraries.currentLibraryId
this.setMetadataProvider()
this.setDefaultFolder()
window.addEventListener('dragenter', this.dragenter)
window.addEventListener('dragleave', this.dragleave)
@@ -359,4 +411,4 @@ export default {
window.removeEventListener('drop', this.drop)
}
}
</script>
</script>

View File

@@ -139,11 +139,30 @@ export default class LocalAudioPlayer extends EventEmitter {
}
var hlsOptions = {
startPosition: this.startTime || -1
// No longer needed because token is put in a query string
// xhrSetup: (xhr) => {
// xhr.setRequestHeader('Authorization', `Bearer ${this.token}`)
// }
startPosition: this.startTime || -1,
fragLoadPolicy: {
default: {
maxTimeToFirstByteMs: 10000,
maxLoadTimeMs: 120000,
timeoutRetry: {
maxNumRetry: 4,
retryDelayMs: 0,
maxRetryDelayMs: 0,
},
errorRetry: {
maxNumRetry: 8,
retryDelayMs: 1000,
maxRetryDelayMs: 8000,
shouldRetry: (retryConfig, retryCount, isTimeout, httpStatus, retry) => {
if (httpStatus?.code === 404 && retryConfig?.maxNumRetry > retryCount) {
console.log(`[HLS] Server 404 for fragment retry ${retryCount} of ${retryConfig.maxNumRetry}`)
return true
}
return retry
}
},
}
}
}
this.hlsInstance = new Hls(hlsOptions)
@@ -156,9 +175,15 @@ export default class LocalAudioPlayer extends EventEmitter {
})
this.hlsInstance.on(Hls.Events.ERROR, (e, data) => {
console.error('[HLS] Error', data.type, data.details, data)
if (data.details === Hls.ErrorDetails.BUFFER_STALLED_ERROR) {
console.error('[HLS] BUFFER STALLED ERROR')
} else if (data.details === Hls.ErrorDetails.FRAG_LOAD_ERROR) {
// Only show error if the fragment is not being retried
if (data.errorAction?.action !== 5) {
console.error('[HLS] FRAG LOAD ERROR', data)
}
} else {
console.error('[HLS] Error', data.type, data.details, data)
}
})
this.hlsInstance.on(Hls.Events.DESTROYING, () => {

View File

@@ -10,16 +10,21 @@ const languageCodeMap = {
'de': { label: 'Deutsch', dateFnsLocale: 'de' },
'en-us': { label: 'English', dateFnsLocale: 'enUS' },
'es': { label: 'Español', dateFnsLocale: 'es' },
'et': { label: 'Eesti', dateFnsLocale: 'et' },
'fr': { label: 'Français', dateFnsLocale: 'fr' },
'hr': { label: 'Hrvatski', dateFnsLocale: 'hr' },
'it': { label: 'Italiano', dateFnsLocale: 'it' },
'lt': { label: 'Lietuvių', dateFnsLocale: 'lt' },
'hu': { label: 'Magyar', dateFnsLocale: 'hu' },
'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' },
'sv': { label: 'Svenska', dateFnsLocale: 'sv' },
'vi-vn': { label: 'Tiếng Việt', dateFnsLocale: 'vi' },
'zh-cn': { label: '简体中文 (Simplified Chinese)', dateFnsLocale: 'zhCN' },
'zh-tw': { label: '正體中文 (Traditional Chinese)', dateFnsLocale: 'zhTW' },
}
Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => {
return {
@@ -28,6 +33,18 @@ Vue.prototype.$languageCodeOptions = Object.keys(languageCodeMap).map(code => {
}
})
// iTunes search API uses ISO 3166 country codes: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
const podcastSearchRegionMap = {
'us': { label: 'United States' },
'cn': { label: '中国' }
}
Vue.prototype.$podcastSearchRegionOptions = Object.keys(podcastSearchRegionMap).map(code => {
return {
text: podcastSearchRegionMap[code].label,
value: code
}
})
Vue.prototype.$languageCodes = {
default: defaultCode,
current: defaultCode,
@@ -83,7 +100,7 @@ async function loadi18n(code) {
Vue.prototype.$setDateFnsLocale(languageCodeMap[code].dateFnsLocale)
this.$eventBus.$emit('change-lang', code)
this?.$eventBus?.$emit('change-lang', code)
return true
}

View File

@@ -77,6 +77,7 @@ Vue.prototype.$sanitizeFilename = (filename, colonReplacement = ' - ') => {
.replace(lineBreaks, replacement)
.replace(windowsReservedRe, replacement)
.replace(windowsTrailingRe, replacement)
.replace(/\s+/g, ' ') // Replace consecutive spaces with a single space
// Check if basename is too many bytes
const ext = Path.extname(sanitized) // separate out file extension
@@ -155,14 +156,14 @@ Vue.prototype.$copyToClipboard = (str, ctx) => {
}
function xmlToJson(xml) {
const json = {};
const json = {}
for (const res of xml.matchAll(/(?:<(\w*)(?:\s[^>]*)*>)((?:(?!<\1).)*)(?:<\/\1>)|<(\w*)(?:\s*)*\/>/gm)) {
const key = res[1] || res[3];
const value = res[2] && xmlToJson(res[2]);
json[key] = ((value && Object.keys(value).length) ? value : res[2]) || null;
const key = res[1] || res[3]
const value = res[2] && xmlToJson(res[2])
json[key] = ((value && Object.keys(value).length) ? value : res[2]) || null
}
return json;
return json
}
Vue.prototype.$xmlToJson = xmlToJson

BIN
client/static/ios_icon.png Normal file
View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

2
client/static/robots.txt Normal file
View File

@@ -0,0 +1,2 @@
User-Agent: *
Disallow: /

View File

@@ -99,7 +99,7 @@ export const getters = {
return `http://localhost:3333${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}${raw ? '&raw=1' : ''}`
}
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}`
return `${rootState.routerBasePath}/api/items/${libraryItemId}/cover?token=${userToken}&ts=${lastUpdate}${raw ? '&raw=1' : ''}`
},
getLibraryItemCoverSrcById: (state, getters, rootState, rootGetters) => (libraryItemId, timestamp = null, raw = false) => {
const placeholder = `${rootState.routerBasePath}/book_placeholder.jpg`

View File

@@ -80,13 +80,11 @@ export const actions = {
return state.folders
}
}
console.log('Loading folders')
commit('setFoldersLastUpdate')
return this.$axios
.$get('/api/filesystem')
.then((res) => {
console.log('Settings folders', res)
commit('setFolders', res.directories)
return res.directories
})
@@ -115,19 +113,23 @@ 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 })
if (libraryChanging) {
commit('setCollections', [])
commit('setUserPlaylists', [])
}
commit('addUpdate', library)
commit('setLibraryIssues', issues)
commit('setLibraryFilterData', filterData)
commit('setNumUserPlaylists', numUserPlaylists)
commit('scanners/setCustomMetadataProviders', customMetadataProviders, { root: true })
commit('setCurrentLibrary', libraryId)
if (libraryChanging) {
commit('setCollections', [])
commit('setUserPlaylists', [])
}
return data
})
.catch((error) => {

View File

@@ -71,8 +71,56 @@ export const state = () => ({
]
})
export const getters = {}
export const getters = {
checkBookProviderExists: state => (providerValue) => {
return state.providers.some(p => p.value === providerValue)
},
checkPodcastProviderExists: state => (providerValue) => {
return state.podcastProviders.some(p => p.value === providerValue)
}
}
export const actions = {}
export const mutations = {}
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
}
}
}

View File

@@ -11,7 +11,9 @@ export const state = () => ({
useChapterTrack: false,
seriesSortBy: 'name',
seriesSortDesc: false,
seriesFilterBy: 'all'
seriesFilterBy: 'all',
authorSortBy: 'name',
authorSortDesc: false
}
})

View File

@@ -32,6 +32,8 @@
"ButtonHide": "Skrýt",
"ButtonHome": "Domů",
"ButtonIssues": "Problémy",
"ButtonJumpBackward": "Jump Backward",
"ButtonJumpForward": "Jump Forward",
"ButtonLatest": "Nejnovější",
"ButtonLibrary": "Knihovna",
"ButtonLogout": "Odhlásit",
@@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "Spárovat všechny autory",
"ButtonMatchBooks": "Spárovat Knihy",
"ButtonNevermind": "Nevadí",
"ButtonNext": "Next",
"ButtonNextChapter": "Next Chapter",
"ButtonOk": "Ok",
"ButtonOpenFeed": "Otevřít kanál",
"ButtonOpenManager": "Otevřít správce",
"ButtonPause": "Pause",
"ButtonPlay": "Přehrát",
"ButtonPlaying": "Hraje",
"ButtonPlaylists": "Seznamy skladeb",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "Vyčistit veškerou mezipaměť",
"ButtonPurgeItemsCache": "Vyčistit mezipaměť položek",
"ButtonPurgeMediaProgress": "Vyčistit průběh médií",
@@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "Odstranit z fronty",
"ButtonQuickMatch": "Rychlé přiřazení",
"ButtonRead": "Číst",
"ButtonRefresh": "Refresh",
"ButtonRemove": "Odstranit",
"ButtonRemoveAll": "Odstranit vše",
"ButtonRemoveAllLibraryItems": "Odstranit všechny položky knihovny",
@@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "Vybrat cestu ke složce",
"ButtonSeries": "Série",
"ButtonSetChaptersFromTracks": "Nastavit kapitoly ze stop",
"ButtonShare": "Share",
"ButtonShiftTimes": "Časy posunu",
"ButtonShow": "Zobrazit",
"ButtonStartM4BEncode": "Spustit kódování M4B",
@@ -87,6 +96,9 @@
"ButtonUserEdit": "Upravit uživatelské {0}",
"ButtonViewAll": "Zobrazit vše",
"ButtonYes": "Ano",
"ErrorUploadFetchMetadataAPI": "Error fetching metadata",
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
"ErrorUploadLacksTitle": "Must have a title",
"HeaderAccount": "Účet",
"HeaderAdvanced": "Pokročilé",
"HeaderAppriseNotificationSettings": "Nastavení oznámení Apprise",
@@ -101,6 +113,7 @@
"HeaderCollectionItems": "Položky kolekce",
"HeaderCover": "Obálka",
"HeaderCurrentDownloads": "Aktuální stahování",
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "Podrobnosti",
"HeaderDownloadQueue": "Fronta stahování",
"HeaderEbookFiles": "Soubory elektronických knih",
@@ -171,6 +184,7 @@
"HeaderUpdateDetails": "Aktualizovat podrobnosti",
"HeaderUpdateLibrary": "Aktualizovat knihovnu",
"HeaderUsers": "Uživatelé",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Vaše statistiky",
"LabelAbridged": "Zkráceno",
"LabelAccountType": "Typ účtu",
@@ -196,6 +210,8 @@
"LabelAuthorLastFirst": "Autor (příjmení a jméno)",
"LabelAuthors": "Autoři",
"LabelAutoDownloadEpisodes": "Automaticky stahovat epizody",
"LabelAutoFetchMetadata": "Auto Fetch Metadata",
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
"LabelAutoLaunch": "Auto Launch",
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register",
@@ -266,6 +282,7 @@
"LabelExample": "Příklad",
"LabelExplicit": "Explicitní",
"LabelFeedURL": "URL zdroje",
"LabelFetchingMetadata": "Fetching Metadata",
"LabelFile": "Soubor",
"LabelFileBirthtime": "Čas vzniku souboru",
"LabelFileModified": "Soubor změněn",
@@ -275,8 +292,11 @@
"LabelFinished": "Dokončeno",
"LabelFolder": "Složka",
"LabelFolders": "Složky",
"LabelFontBold": "Bold",
"LabelFontFamily": "Rodina písem",
"LabelFontItalic": "Italic",
"LabelFontScale": "Měřítko písma",
"LabelFontStrikethrough": "Strikethrough",
"LabelFormat": "Formát",
"LabelGenre": "Žánr",
"LabelGenres": "Žánry",
@@ -336,7 +356,11 @@
"LabelMetaTags": "Metaznačky",
"LabelMinute": "Minuta",
"LabelMissing": "Chybějící",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Chybějící díly",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Více",
"LabelMoreInfo": "Více informací",
"LabelName": "Jméno",
@@ -374,11 +398,13 @@
"LabelPermissionsDownload": "Může stahovat",
"LabelPermissionsUpdate": "Může aktualizovat",
"LabelPermissionsUpload": "Může nahrávat",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Cesta k fotografii/URL",
"LabelPlaylists": "Seznamy skladeb",
"LabelPlayMethod": "Metoda přehrávání",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasty",
"LabelPodcastSearchRegion": "Oblast vyhledávání podcastu",
"LabelPodcastType": "Typ podcastu",
"LabelPort": "Port",
"LabelPrefixesToIgnore": "Předpony, které se mají ignorovat (nerozlišují se malá a velká písmena)",
@@ -395,9 +421,11 @@
"LabelRecentlyAdded": "Nedávno přidané",
"LabelRecentSeries": "Nedávné série",
"LabelRecommended": "Doporučeno",
"LabelRedo": "Redo",
"LabelRegion": "Region",
"LabelReleaseDate": "Datum vydání",
"LabelRemoveCover": "Odstranit obálku",
"LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Vlastní e-mail vlastníka",
"LabelRSSFeedCustomOwnerName": "Vlastní jméno vlastníka",
"LabelRSSFeedOpen": "Otevření RSS kanálu",
@@ -416,6 +444,7 @@
"LabelSeries": "Série",
"LabelSeriesName": "Název série",
"LabelSeriesProgress": "Průběh série",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Nastavit jako primární",
"LabelSetEbookAsSupplementary": "Nastavit jako doplňkové",
"LabelSettingsAudiobooksOnly": "Pouze audioknihy",
@@ -437,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Série, které mají jedinou knihu, budou skryty na stránce série a na domovské stránce.",
"LabelSettingsHomePageBookshelfView": "Domovská stránka používá zobrazení police s knihami",
"LabelSettingsLibraryBookshelfView": "Knihovna používá zobrazení police s knihami",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Analzyovat podtitul",
"LabelSettingsParseSubtitlesHelp": "Rozparsovat podtitul z názvů složek audioknih.<br>Podtiul musí být oddělen znakem \" - \"<br>tj. \"Název knihy - Zde Podtitul\" má podtitul \"Zde podtitul\"",
"LabelSettingsPreferMatchedMetadata": "Preferovat spárovaná metadata",
@@ -482,6 +513,10 @@
"LabelTagsAccessibleToUser": "Značky přístupné uživateli",
"LabelTagsNotAccessibleToUser": "Značky nepřístupné uživateli",
"LabelTasks": "Spuštěné Úlohy",
"LabelTextEditorBulletedList": "Bulleted list",
"LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "Numbered list",
"LabelTextEditorUnlink": "Unlink",
"LabelTheme": "Téma",
"LabelThemeDark": "Tmavé",
"LabelThemeLight": "Světlé",
@@ -507,6 +542,7 @@
"LabelTracksSingleTrack": "Jedna stopa",
"LabelType": "Typ",
"LabelUnabridged": "Nezkráceno",
"LabelUndo": "Undo",
"LabelUnknown": "Neznámý",
"LabelUpdateCover": "Aktualizovat obálku",
"LabelUpdateCoverHelp": "Povolit přepsání existujících obálek pro vybrané knihy, pokud je nalezena shoda",
@@ -515,6 +551,7 @@
"LabelUpdateDetailsHelp": "Povolit přepsání existujících údajů o vybraných knihách, když je nalezena shoda",
"LabelUploaderDragAndDrop": "Přetáhnout soubory nebo složky",
"LabelUploaderDropFiles": "Odstranit soubory",
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
"LabelUseChapterTrack": "Použít stopu kapitoly",
"LabelUseFullTrack": "Použít celou stopu",
"LabelUser": "Uživatel",
@@ -526,6 +563,8 @@
"LabelViewQueue": "Zobrazit frontu přehrávače",
"LabelVolume": "Hlasitost",
"LabelWeekdaysToRun": "Dny v týdnu ke spuštění",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Doba trvání vaší audioknihy",
"LabelYourBookmarks": "Vaše záložky",
"LabelYourPlaylists": "Vaše seznamy přehrávání",
@@ -562,6 +601,7 @@
"MessageConfirmRemoveCollection": "Opravdu chcete odstranit kolekci \"{0}\"?",
"MessageConfirmRemoveEpisode": "Opravdu chcete odstranit epizodu \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Opravdu chcete odstranit {0} epizody?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Opravdu chcete odebrat předčítání \"{0}\"?",
"MessageConfirmRemovePlaylist": "Opravdu chcete odstranit svůj playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Opravdu chcete přejmenovat žánr \"{0}\" na \"{1}\" pro všechny položky?",
@@ -641,6 +681,7 @@
"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.",
"MessageSearchResultsFor": "Výsledky hledání pro",
"MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Server je nedostupný",
"MessageSetChaptersFromTracksDescription": "Nastavit kapitoly jako kapitolu a název kapitoly jako název zvukového souboru",
"MessageStartPlaybackAtTime": "Spustit přehrávání pro \"{0}\" v {1}?",
@@ -656,7 +697,6 @@
"NoteChangeRootPassword": "Uživatel root je jediný uživatel, který může mít prázdné heslo",
"NoteChapterEditorTimes": "Poznámka: Čas začátku první kapitoly musí zůstat v 0:00 a čas začátku poslední kapitoly nesmí překročit tuto dobu trvání audioknihy.",
"NoteFolderPicker": "Poznámka: složky, které jsou již namapovány, nebudou zobrazeny",
"NoteFolderPickerDebian": "Poznámka: Výběr složek pro instalaci debianu není plně implementován. Cestu ke své knihovně byste měli zadat přímo.",
"NoteRSSFeedPodcastAppsHttps": "Upozornění: Většina aplikací pro podcasty bude vyžadovat, aby adresa URL kanálu RSS používala protokol HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Upozornění: 1 nebo více epizod nemá datum vydání. Některé podcastové aplikace to vyžadují.",
"NoteUploaderFoldersWithMediaFiles": "Se složkami s multimediálními soubory bude zacházeno jako se samostatnými položkami knihovny.",

View File

@@ -32,6 +32,8 @@
"ButtonHide": "Skjul",
"ButtonHome": "Hjem",
"ButtonIssues": "Problemer",
"ButtonJumpBackward": "Jump Backward",
"ButtonJumpForward": "Jump Forward",
"ButtonLatest": "Seneste",
"ButtonLibrary": "Bibliotek",
"ButtonLogout": "Log ud",
@@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "Match alle forfattere",
"ButtonMatchBooks": "Match bøger",
"ButtonNevermind": "Glem det",
"ButtonNext": "Next",
"ButtonNextChapter": "Next Chapter",
"ButtonOk": "OK",
"ButtonOpenFeed": "Åbn feed",
"ButtonOpenManager": "Åbn manager",
"ButtonPause": "Pause",
"ButtonPlay": "Afspil",
"ButtonPlaying": "Afspiller",
"ButtonPlaylists": "Afspilningslister",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "Ryd al cache",
"ButtonPurgeItemsCache": "Ryd elementcache",
"ButtonPurgeMediaProgress": "Ryd Medieforløb",
@@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "Fjern fra kø",
"ButtonQuickMatch": "Hurtig Match",
"ButtonRead": "Læs",
"ButtonRefresh": "Refresh",
"ButtonRemove": "Fjern",
"ButtonRemoveAll": "Fjern Alle",
"ButtonRemoveAllLibraryItems": "Fjern Alle Bibliotekselementer",
@@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "Vælg Mappen Sti",
"ButtonSeries": "Serie",
"ButtonSetChaptersFromTracks": "Sæt kapitler fra spor",
"ButtonShare": "Share",
"ButtonShiftTimes": "Skift Tider",
"ButtonShow": "Vis",
"ButtonStartM4BEncode": "Start M4B Kode",
@@ -87,6 +96,9 @@
"ButtonUserEdit": "Rediger bruger {0}",
"ButtonViewAll": "Vis Alle",
"ButtonYes": "Ja",
"ErrorUploadFetchMetadataAPI": "Error fetching metadata",
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
"ErrorUploadLacksTitle": "Must have a title",
"HeaderAccount": "Konto",
"HeaderAdvanced": "Avanceret",
"HeaderAppriseNotificationSettings": "Apprise Notifikationsindstillinger",
@@ -101,6 +113,7 @@
"HeaderCollectionItems": "Samlingselementer",
"HeaderCover": "Omslag",
"HeaderCurrentDownloads": "Nuværende Downloads",
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "Detaljer",
"HeaderDownloadQueue": "Download Kø",
"HeaderEbookFiles": "E-bogsfiler",
@@ -171,6 +184,7 @@
"HeaderUpdateDetails": "Opdater Detaljer",
"HeaderUpdateLibrary": "Opdater Bibliotek",
"HeaderUsers": "Brugere",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Dine Statistikker",
"LabelAbridged": "Abridged",
"LabelAccountType": "Kontotype",
@@ -196,6 +210,8 @@
"LabelAuthorLastFirst": "Forfatter (Efternavn, Fornavn)",
"LabelAuthors": "Forfattere",
"LabelAutoDownloadEpisodes": "Auto Download Episoder",
"LabelAutoFetchMetadata": "Auto Fetch Metadata",
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
"LabelAutoLaunch": "Auto Launch",
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register",
@@ -266,6 +282,7 @@
"LabelExample": "Eksempel",
"LabelExplicit": "Eksplisit",
"LabelFeedURL": "Feed URL",
"LabelFetchingMetadata": "Fetching Metadata",
"LabelFile": "Fil",
"LabelFileBirthtime": "Fødselstidspunkt for fil",
"LabelFileModified": "Fil ændret",
@@ -275,8 +292,11 @@
"LabelFinished": "Færdig",
"LabelFolder": "Mappe",
"LabelFolders": "Mapper",
"LabelFontBold": "Bold",
"LabelFontFamily": "Fontfamilie",
"LabelFontItalic": "Italic",
"LabelFontScale": "Skriftstørrelse",
"LabelFontStrikethrough": "Strikethrough",
"LabelFormat": "Format",
"LabelGenre": "Genre",
"LabelGenres": "Genrer",
@@ -336,7 +356,11 @@
"LabelMetaTags": "Meta-tags",
"LabelMinute": "Minut",
"LabelMissing": "Mangler",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Manglende dele",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Mere",
"LabelMoreInfo": "Mere info",
"LabelName": "Navn",
@@ -374,11 +398,13 @@
"LabelPermissionsDownload": "Kan downloade",
"LabelPermissionsUpdate": "Kan opdatere",
"LabelPermissionsUpload": "Kan uploade",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Foto sti/URL",
"LabelPlaylists": "Afspilningslister",
"LabelPlayMethod": "Afspilningsmetode",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts",
"LabelPodcastSearchRegion": "Podcast søgeområde",
"LabelPodcastType": "Podcast type",
"LabelPort": "Port",
"LabelPrefixesToIgnore": "Præfikser der skal ignoreres (skal ikke skelne mellem store og små bogstaver)",
@@ -395,9 +421,11 @@
"LabelRecentlyAdded": "Senest tilføjet",
"LabelRecentSeries": "Seneste serie",
"LabelRecommended": "Anbefalet",
"LabelRedo": "Redo",
"LabelRegion": "Region",
"LabelReleaseDate": "Udgivelsesdato",
"LabelRemoveCover": "Fjern omslag",
"LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Brugerdefineret ejerens e-mail",
"LabelRSSFeedCustomOwnerName": "Brugerdefineret ejerens navn",
"LabelRSSFeedOpen": "Åben RSS-feed",
@@ -416,6 +444,7 @@
"LabelSeries": "Serie",
"LabelSeriesName": "Serienavn",
"LabelSeriesProgress": "Seriefremskridt",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Indstil som primær",
"LabelSetEbookAsSupplementary": "Indstil som supplerende",
"LabelSettingsAudiobooksOnly": "Kun lydbøger",
@@ -437,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Serier med en enkelt bog vil blive skjult fra serie-siden og hjemmesidehylder.",
"LabelSettingsHomePageBookshelfView": "Brug bogreolvisning på startside",
"LabelSettingsLibraryBookshelfView": "Brug bogreolvisning i biblioteket",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Fortolk undertekster",
"LabelSettingsParseSubtitlesHelp": "Udtræk undertekster fra lydbogsmappenavne.<br>Undertitler skal adskilles af \" - \"<br>f.eks. \"Bogtitel - En undertitel her\" har undertitlen \"En undertitel her\"",
"LabelSettingsPreferMatchedMetadata": "Foretræk matchede metadata",
@@ -482,6 +513,10 @@
"LabelTagsAccessibleToUser": "Mærker tilgængelige for bruger",
"LabelTagsNotAccessibleToUser": "Mærker ikke tilgængelige for bruger",
"LabelTasks": "Kører opgaver",
"LabelTextEditorBulletedList": "Bulleted list",
"LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "Numbered list",
"LabelTextEditorUnlink": "Unlink",
"LabelTheme": "Tema",
"LabelThemeDark": "Mørk",
"LabelThemeLight": "Lys",
@@ -507,6 +542,7 @@
"LabelTracksSingleTrack": "Enkeltspors",
"LabelType": "Type",
"LabelUnabridged": "Uforkortet",
"LabelUndo": "Undo",
"LabelUnknown": "Ukendt",
"LabelUpdateCover": "Opdater omslag",
"LabelUpdateCoverHelp": "Tillad overskrivning af eksisterende omslag for de valgte bøger, når der findes en match",
@@ -515,6 +551,7 @@
"LabelUpdateDetailsHelp": "Tillad overskrivning af eksisterende detaljer for de valgte bøger, når der findes en match",
"LabelUploaderDragAndDrop": "Træk og slip filer eller mapper",
"LabelUploaderDropFiles": "Smid filer",
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
"LabelUseChapterTrack": "Brug kapitel-spor",
"LabelUseFullTrack": "Brug fuldt spor",
"LabelUser": "Bruger",
@@ -526,6 +563,8 @@
"LabelViewQueue": "Se afspilningskø",
"LabelVolume": "Volumen",
"LabelWeekdaysToRun": "Ugedage til kørsel",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Din lydbogsvarighed",
"LabelYourBookmarks": "Dine bogmærker",
"LabelYourPlaylists": "Dine spillelister",
@@ -562,6 +601,7 @@
"MessageConfirmRemoveCollection": "Er du sikker på, at du vil fjerne samlingen \"{0}\"?",
"MessageConfirmRemoveEpisode": "Er du sikker på, at du vil fjerne episoden \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Er du sikker på, at du vil fjerne {0} episoder?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Er du sikker på, at du vil fjerne fortælleren \"{0}\"?",
"MessageConfirmRemovePlaylist": "Er du sikker på, at du vil fjerne din spilleliste \"{0}\"?",
"MessageConfirmRenameGenre": "Er du sikker på, at du vil omdøbe genre \"{0}\" til \"{1}\" for alle elementer?",
@@ -641,6 +681,7 @@
"MessageRestoreBackupConfirm": "Er du sikker på, at du vil gendanne sikkerhedskopien oprettet den",
"MessageRestoreBackupWarning": "Gendannelse af en sikkerhedskopi vil overskrive hele databasen, som er placeret på /config, og omslagsbilleder i /metadata/items & /metadata/authors.<br /><br />Sikkerhedskopier ændrer ikke nogen filer i dine biblioteksmapper. Hvis du har aktiveret serverindstillinger for at gemme omslagskunst og metadata i dine biblioteksmapper, sikkerhedskopieres eller overskrives disse ikke.<br /><br />Alle klienter, der bruger din server, opdateres automatisk.",
"MessageSearchResultsFor": "Søgeresultater for",
"MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Serveren kunne ikke nås",
"MessageSetChaptersFromTracksDescription": "Indstil kapitler ved at bruge hver lydfil som et kapitel og kapiteloverskrift som lydfilnavn",
"MessageStartPlaybackAtTime": "Start afspilning for \"{0}\" kl. {1}?",
@@ -656,7 +697,6 @@
"NoteChangeRootPassword": "Root-brugeren er den eneste bruger, der kan have en tom adgangskode",
"NoteChapterEditorTimes": "Bemærk: Første kapitel starttidspunkt skal forblive kl. 0:00, og det sidste kapitel starttidspunkt må ikke overstige denne lydbogs varighed.",
"NoteFolderPicker": "Bemærk: Mapper, der allerede er mappet, vises ikke",
"NoteFolderPickerDebian": "Bemærk: Mappicker for Debian-installationen er ikke fuldt implementeret. Du bør indtaste stien til dit bibliotek direkte.",
"NoteRSSFeedPodcastAppsHttps": "Advarsel: De fleste podcast-apps kræver, at RSS-feedets URL bruger HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Advarsel: En eller flere af dine episoder har ikke en Pub Date. Nogle podcast-apps kræver dette.",
"NoteUploaderFoldersWithMediaFiles": "Mapper med mediefiler håndteres som separate bibliotekselementer.",

View File

@@ -1,17 +1,17 @@
{
"ButtonAdd": "Hinzufügen",
"ButtonAddChapters": "Kapitel hinzufügen",
"ButtonAddDevice": "Add Device",
"ButtonAddLibrary": "Add Library",
"ButtonAddDevice": "Gerät hinzufügen",
"ButtonAddLibrary": "Bibliothek hinzufügen",
"ButtonAddPodcasts": "Podcasts hinzufügen",
"ButtonAddUser": "Add User",
"ButtonAddUser": "Benutzer hinzufügen",
"ButtonAddYourFirstLibrary": "Erstelle deine erste Bibliothek",
"ButtonApply": "Übernehmen",
"ButtonApplyChapters": "Kapitel anwenden",
"ButtonAuthors": "Autoren",
"ButtonBrowseForFolder": "Ordnersuche",
"ButtonCancel": "Abbrechen",
"ButtonCancelEncode": "Abbruch der Verschlüsselung",
"ButtonCancelEncode": "Codierung abbrechen",
"ButtonChangeRootPassword": "Hauptpasswort ändern",
"ButtonCheckAndDownloadNewEpisodes": "Überprüfe & lade neue Episoden herunter",
"ButtonChooseAFolder": "Wähle einen Ordner",
@@ -32,6 +32,8 @@
"ButtonHide": "Ausblenden",
"ButtonHome": "Startseite",
"ButtonIssues": "Probleme",
"ButtonJumpBackward": "Zurück springen",
"ButtonJumpForward": "Vorwärts springen",
"ButtonLatest": "Neuste",
"ButtonLibrary": "Bibliothek",
"ButtonLogout": "Abmelden",
@@ -41,28 +43,34 @@
"ButtonMatchAllAuthors": "Online Metadaten-Abgleich (alle Autoren)",
"ButtonMatchBooks": "Online Metadaten-Abgleich (alle Medien)",
"ButtonNevermind": "Abbrechen",
"ButtonNext": "Vor",
"ButtonNextChapter": "Nächstes Kapitel",
"ButtonOk": "Ok",
"ButtonOpenFeed": "Feed öffnen",
"ButtonOpenManager": "Manager öffnen",
"ButtonPause": "Pause",
"ButtonPlay": "Abspielen",
"ButtonPlaying": "Spielt",
"ButtonPlaylists": "Wiedergabelisten",
"ButtonPurgeAllCache": "Lösche alle Zwischenspeicher",
"ButtonPurgeItemsCache": "Lösche Medien-Zwischenspeicher",
"ButtonPrevious": "Zurück",
"ButtonPreviousChapter": "Vorheriges Kapitel",
"ButtonPurgeAllCache": "Cache leeren",
"ButtonPurgeItemsCache": "Lösche Medien-Cache",
"ButtonPurgeMediaProgress": "Lösche Hörfortschritte",
"ButtonQueueAddItem": "Zur Warteschlange hinzufügen",
"ButtonQueueRemoveItem": "Aus der Warteschlange entfernen",
"ButtonQuickMatch": "Schnellabgleich",
"ButtonRead": "Lese",
"ButtonRead": "Lesen",
"ButtonRefresh": "Neu Laden",
"ButtonRemove": "Löschen",
"ButtonRemoveAll": "Alles löschen",
"ButtonRemoveAllLibraryItems": "Lösche alle Bibliothekseinträge",
"ButtonRemoveFromContinueListening": "Lösche den Eintrag aus der Fortsetzungsliste",
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
"ButtonRemoveFromContinueReading": "Lösche die Serie aus der Lesefortsetzungsliste",
"ButtonRemoveSeriesFromContinueSeries": "Lösche die Serie aus der Serienfortsetzungsliste",
"ButtonReScan": "Neu scannen",
"ButtonReset": "Zurücksetzen",
"ButtonResetToDefault": "Reset to default",
"ButtonResetToDefault": "Zurücksetzen auf Standard",
"ButtonRestore": "Wiederherstellen",
"ButtonSave": "Speichern",
"ButtonSaveAndClose": "Speichern & Schließen",
@@ -70,9 +78,10 @@
"ButtonScan": "Partial-Scan (nur geänderte/neue Medien)",
"ButtonScanLibrary": "Bibliothek scannen",
"ButtonSearch": "Suchen",
"ButtonSelectFolderPath": "Auswahl Ordnerpfad",
"ButtonSelectFolderPath": "Ordnerpfad auswählen",
"ButtonSeries": "Serien",
"ButtonSetChaptersFromTracks": "Kapitelerstellung aus Audiodateien",
"ButtonShare": "Teilen",
"ButtonShiftTimes": "Zeitverschiebung",
"ButtonShow": "Anzeigen",
"ButtonStartM4BEncode": "M4B-Kodierung starten",
@@ -87,6 +96,9 @@
"ButtonUserEdit": "Benutzer {0} bearbeiten",
"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",
"ErrorUploadLacksTitle": "Es muss ein Titel eingegeben werden",
"HeaderAccount": "Konto",
"HeaderAdvanced": "Erweitert",
"HeaderAppriseNotificationSettings": "Apprise Benachrichtigungseinstellungen",
@@ -101,21 +113,22 @@
"HeaderCollectionItems": "Sammlungseinträge",
"HeaderCover": "Titelbild",
"HeaderCurrentDownloads": "Aktuelle Downloads",
"HeaderCustomMetadataProviders": "Benutzerdefinierte Metadata Anbieter",
"HeaderDetails": "Details",
"HeaderDownloadQueue": "Download Warteschlange",
"HeaderEbookFiles": "E-Book Dateien",
"HeaderEmail": "Email",
"HeaderEmailSettings": "Email Einstellungen",
"HeaderEpisodes": "Episoden",
"HeaderEreaderDevices": "Ereader Geräte",
"HeaderEreaderSettings": "Ereader Einstellungen",
"HeaderEreaderDevices": "E-Reader Geräte",
"HeaderEreaderSettings": "E-Reader Einstellungen",
"HeaderFiles": "Dateien",
"HeaderFindChapters": "Kapitel suchen",
"HeaderIgnoredFiles": "Ignorierte Dateien",
"HeaderItemFiles": "Medien-Dateien",
"HeaderItemMetadataUtils": "Metadaten",
"HeaderLastListeningSession": "Letzte Hörsitzung",
"HeaderLatestEpisodes": "Letzte Episoden",
"HeaderLatestEpisodes": "Neueste Episoden",
"HeaderLibraries": "Bibliotheken",
"HeaderLibraryFiles": "Alle Dateien",
"HeaderLibraryStats": "Bibliotheksstatistiken",
@@ -127,7 +140,7 @@
"HeaderManageTags": "Tags verwalten",
"HeaderMapDetails": "Stapelverarbeitung",
"HeaderMatch": "Metadaten",
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
"HeaderMetadataOrderOfPrecedence": "Metadaten Rangfolge",
"HeaderMetadataToEmbed": "Einzubettende Metadaten",
"HeaderNewAccount": "Neues Konto",
"HeaderNewLibrary": "Neue Bibliothek",
@@ -135,9 +148,9 @@
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentifizierung",
"HeaderOpenRSSFeed": "RSS-Feed öffnen",
"HeaderOtherFiles": "Sonstige Dateien",
"HeaderPasswordAuthentication": "Password Authentifizierung",
"HeaderPasswordAuthentication": "Passwort Authentifizierung",
"HeaderPermissions": "Berechtigungen",
"HeaderPlayerQueue": "Spieler Warteschlange",
"HeaderPlayerQueue": "Player Warteschlange",
"HeaderPlaylist": "Wiedergabeliste",
"HeaderPlaylistItems": "Einträge in der Wiedergabeliste",
"HeaderPodcastsToAdd": "Podcasts zum Hinzufügen",
@@ -146,7 +159,7 @@
"HeaderRemoveEpisodes": "Lösche {0} Episoden",
"HeaderRSSFeedGeneral": "RSS Details",
"HeaderRSSFeedIsOpen": "RSS-Feed ist geöffnet",
"HeaderRSSFeeds": "RSS Feeds",
"HeaderRSSFeeds": "RSS-Feeds",
"HeaderSavedMediaProgress": "Gespeicherte Hörfortschritte",
"HeaderSchedule": "Zeitplan",
"HeaderScheduleLibraryScans": "Automatische Bibliotheksscans",
@@ -157,7 +170,7 @@
"HeaderSettingsExperimental": "Experimentelle Funktionen",
"HeaderSettingsGeneral": "Allgemein",
"HeaderSettingsScanner": "Scanner",
"HeaderSleepTimer": "Einschlaf-Timer",
"HeaderSleepTimer": "Sleep-Timer",
"HeaderStatsLargestItems": "Größte Medien",
"HeaderStatsLongestItems": "Längste Medien (h)",
"HeaderStatsMinutesListeningChart": "Hörminuten (letzte 7 Tage)",
@@ -171,6 +184,7 @@
"HeaderUpdateDetails": "Details aktualisieren",
"HeaderUpdateLibrary": "Bibliothek aktualisieren",
"HeaderUsers": "Benutzer",
"HeaderYearReview": "Jahr {0} in Übersicht",
"HeaderYourStats": "Eigene Statistiken",
"LabelAbridged": "Gekürzt",
"LabelAccountType": "Kontoart",
@@ -188,18 +202,20 @@
"LabelAll": "Alle",
"LabelAllUsers": "Alle Benutzer",
"LabelAllUsersExcludingGuests": "Alle Benutzer außer Gästen",
"LabelAllUsersIncludingGuests": "All Benutzer und Gäste",
"LabelAlreadyInYourLibrary": "In der Bibliothek vorhanden",
"LabelAllUsersIncludingGuests": "Alle Benutzer und Gäste",
"LabelAlreadyInYourLibrary": "Bereits in der Bibliothek",
"LabelAppend": "Anhängen",
"LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (Vorname Nachname)",
"LabelAuthorLastFirst": "Autor (Nachname, Vorname)",
"LabelAuthors": "Autoren",
"LabelAutoDownloadEpisodes": "Episoden automatisch herunterladen",
"LabelAutoFetchMetadata": "Automatisches Abholen der Metadaten",
"LabelAutoFetchMetadataHelp": "Abholen der Metadaten von Titel, Autor und Serien, um das Hochladen zu optimieren. Möglicherweise müssen zusätzliche Metadaten nach dem Hochladen abgeglichen werden.",
"LabelAutoLaunch": "Automatischer Start",
"LabelAutoLaunchDescription": "Automatische Weiterleitung zum Authentifizierungsanbieter beim Navigieren zur Anmeldeseite (manueller Überschreibungspfad <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Automatische Registrierung",
"LabelAutoRegisterDescription": "Automatische neue Neutzer anlegen nach dem Einloggen",
"LabelAutoRegisterDescription": "Automatische neue Neutzer anlegen nach dem Registrieren",
"LabelBackToUser": "Zurück zum Benutzer",
"LabelBackupLocation": "Backup-Ort",
"LabelBackupsEnableAutomaticBackups": "Automatische Sicherung aktivieren",
@@ -207,16 +223,16 @@
"LabelBackupsMaxBackupSize": "Maximale Sicherungsgröße (in GB)",
"LabelBackupsMaxBackupSizeHelp": "Zum Schutz vor Fehlkonfigurationen schlagen Sicherungen fehl, wenn sie die konfigurierte Größe überschreiten.",
"LabelBackupsNumberToKeep": "Anzahl der aufzubewahrenden Sicherungen",
"LabelBackupsNumberToKeepHelp": "Es wird immer nur 1 Sicherung auf einmal entfernt. Wenn Sie bereits mehrere Sicherungen als die definierte max. Anzahl haben, sollten Sie diese manuell entfernen.",
"LabelBackupsNumberToKeepHelp": "Es wird immer nur 1 Sicherung auf einmal entfernt. Wenn du bereits mehrere Sicherungen als die definierte max. Anzahl hast, solltest du diese manuell entfernen.",
"LabelBitrate": "Bitrate",
"LabelBooks": "Bücher",
"LabelButtonText": "Button Text",
"LabelChangePassword": "Passwort ändern",
"LabelChannels": "Kanäle",
"LabelChapters": "Kapitel",
"LabelChaptersFound": "gefundene Kapitel",
"LabelChaptersFound": "Gefundene Kapitel",
"LabelChapterTitle": "Kapitelüberschrift",
"LabelClickForMoreInfo": "Click for more info",
"LabelClickForMoreInfo": "Klicken für mehr Informationen",
"LabelClosePlayer": "Player schließen",
"LabelCodec": "Codec",
"LabelCollapseSeries": "Serien zusammenfassen",
@@ -225,7 +241,7 @@
"LabelComplete": "Vollständig",
"LabelConfirmPassword": "Passwort bestätigen",
"LabelContinueListening": "Weiterhören",
"LabelContinueReading": "Lesen fortsetzen",
"LabelContinueReading": "Weiterlesen",
"LabelContinueSeries": "Serien fortsetzen",
"LabelCover": "Titelbild",
"LabelCoverImageURL": "URL des Titelbildes",
@@ -240,23 +256,23 @@
"LabelDeselectAll": "Alles abwählen",
"LabelDevice": "Gerät",
"LabelDeviceInfo": "Geräteinformationen",
"LabelDeviceIsAvailableTo": "Dem Geärt ist es möglich zu ...",
"LabelDeviceIsAvailableTo": "Dem Gerät ist es möglich zu ...",
"LabelDirectory": "Verzeichnis",
"LabelDiscFromFilename": "CD aus dem Dateinamen",
"LabelDiscFromMetadata": "CD aus den Metadaten",
"LabelDiscover": "Entdecken",
"LabelDownload": "Herunterladen",
"LabelDownloadNEpisodes": "Download {0} episodes",
"LabelDownloadNEpisodes": "Download {0} Episoden",
"LabelDuration": "Laufzeit",
"LabelDurationFound": "Gefundene Laufzeit:",
"LabelEbook": "E-Book",
"LabelEbooks": "E-Books",
"LabelEdit": "Bearbeiten",
"LabelEmail": "Email",
"LabelEmailSettingsFromAddress": "Von Address",
"LabelEmailSettingsSecure": "Sicherheit",
"LabelEmailSettingsSecureHelp": "Wenn \"true\", verwendet die Verbindung TLS, wenn sie eine Verbindung zum Server herstellt. Bei \"false\" wird TLS verwendet, wenn der Server die STARTTLS-Erweiterung unterstützt. In den meisten Fällen setzen Sie diesen Wert auf \"true\", wenn Sie eine Verbindung zu Port 465 herstellen. Für Port 587 oder 25 behalten Sie den Wert \"false\" bei. (von nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Test Addresse",
"LabelEmailSettingsFromAddress": "Von Adresse",
"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",
"LabelEmbeddedCover": "Eingebettetes Cover",
"LabelEnable": "Aktivieren",
"LabelEnd": "Ende",
@@ -266,24 +282,28 @@
"LabelExample": "Beispiel",
"LabelExplicit": "Explizit (Altersbeschränkung)",
"LabelFeedURL": "Feed URL",
"LabelFetchingMetadata": "Abholen der Metadaten",
"LabelFile": "Datei",
"LabelFileBirthtime": "Datei erstellt",
"LabelFileModified": "Datei geändert",
"LabelFilename": "Dateiname",
"LabelFilterByUser": "Nach Benutzern filtern",
"LabelFindEpisodes": "Episoden suchen",
"LabelFinished": "beendet",
"LabelFinished": "Beendet",
"LabelFolder": "Ordner",
"LabelFolders": "Verzeichnisse",
"LabelFontBold": "Fett",
"LabelFontFamily": "Schriftfamilie",
"LabelFontItalic": "Kursiv",
"LabelFontScale": "Schriftgröße",
"LabelFontStrikethrough": "Durchgestrichen",
"LabelFormat": "Format",
"LabelGenre": "Kategorie",
"LabelGenres": "Kategorien",
"LabelHardDeleteFile": "Datei dauerhaft löschen",
"LabelHasEbook": "mit E-Book",
"LabelHasSupplementaryEbook": "mit zusätlichem E-Book",
"LabelHighestPriority": "Highest priority",
"LabelHasEbook": "E-Book verfügbar",
"LabelHasSupplementaryEbook": "Ergänzendes E-Book verfügbar",
"LabelHighestPriority": "Höchste Priorität",
"LabelHost": "Host",
"LabelHour": "Stunde",
"LabelIcon": "Symbol",
@@ -305,9 +325,9 @@
"LabelItem": "Medium",
"LabelLanguage": "Sprache",
"LabelLanguageDefaultServer": "Standard-Server-Sprache",
"LabelLastBookAdded": "Zuletzt hinzugefügtes Medium",
"LabelLastBookUpdated": "Zuletzt aktualisiertes Medium",
"LabelLastSeen": "Zuletzt angesehen",
"LabelLastBookAdded": "Zuletzt hinzugefügtes Buch",
"LabelLastBookUpdated": "Zuletzt aktualisiertes Buch",
"LabelLastSeen": "Zuletzt gesehen",
"LabelLastTime": "Letztes Mal",
"LabelLastUpdate": "Letzte Aktualisierung",
"LabelLayout": "Layout",
@@ -324,33 +344,37 @@
"LabelLogLevelDebug": "Fehlersuche",
"LabelLogLevelInfo": "Informationen",
"LabelLogLevelWarn": "Warnungen",
"LabelLookForNewEpisodesAfterDate": "Suchen nach neuen Episoden nach diesem Datum",
"LabelLowestPriority": "Lowest Priority",
"LabelLookForNewEpisodesAfterDate": "Suche nach neuen Episoden nach diesem Datum",
"LabelLowestPriority": "Niedrigste Priorität",
"LabelMatchExistingUsersBy": "Zuordnen existierender Benutzer mit",
"LabelMatchExistingUsersByDescription": "Wird zum Verbinden vorhandener Benutzer verwendet. Sobald die Verbindung hergestellt ist, wird den Benutzern eine eindeutige ID von Ihrem SSO-Anbieter zugeordnet",
"LabelMatchExistingUsersByDescription": "Wird zum Verbinden vorhandener Benutzer verwendet. Sobald die Verbindung hergestellt ist, wird den Benutzern eine eindeutige ID vom SSO-Anbieter zugeordnet",
"LabelMediaPlayer": "Mediaplayer",
"LabelMediaType": "Medientyp",
"LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources",
"LabelMetadataOrderOfPrecedenceDescription": "Höher priorisierte Quellen für Metadaten überschreiben Metadaten aus Quellen die niedriger priorisiert sind.",
"LabelMetadataProvider": "Metadatenanbieter",
"LabelMetaTag": "Meta Schlagwort",
"LabelMetaTags": "Meta Tags",
"LabelMinute": "Minute",
"LabelMissing": "Fehlend",
"LabelMissingEbook": "E-Book fehlt",
"LabelMissingParts": "Fehlende Teile",
"LabelMissingSupplementaryEbook": "Ergänzendes E-Book fehlt",
"LabelMobileRedirectURIs": "Erlaubte Weiterleitungs-URIs für die mobile App",
"LabelMobileRedirectURIsDescription": "Dies ist eine Whitelist gültiger Umleitungs-URIs für mobile Apps. Der Standardwert ist <code>audiobookshelf://oauth</code>, den du entfernen oder durch zusätzliche URIs für die Integration von Drittanbieter-Apps ergänzen kannst. Die Verwendung eines Sternchens (<code>*</code>) als alleiniger Eintrag erlaubt jede URI.",
"LabelMore": "Mehr",
"LabelMoreInfo": "Mehr Info",
"LabelMoreInfo": "Mehr Infos",
"LabelName": "Name",
"LabelNarrator": "Erzähler",
"LabelNarrators": "Erzähler",
"LabelNew": "Neu",
"LabelNewestAuthors": "Neuste Autoren",
"LabelNewestAuthors": "Neueste Autoren",
"LabelNewestEpisodes": "Neueste Episoden",
"LabelNewPassword": "Neues Passwort",
"LabelNextBackupDate": "Nächstes Sicherungsdatum",
"LabelNextScheduledRun": "Nächster planmäßiger Durchlauf",
"LabelNoEpisodesSelected": "Keine Episoden ausgewählt",
"LabelNotes": "Hinweise",
"LabelNotFinished": "nicht beendet",
"LabelNotes": "Notizen",
"LabelNotFinished": "Nicht beendet",
"LabelNotificationAppriseURL": "Apprise URL(s)",
"LabelNotificationAvailableVariables": "Verfügbare Variablen",
"LabelNotificationBodyTemplate": "Textvorlage",
@@ -363,7 +387,7 @@
"LabelNotStarted": "Nicht begonnen",
"LabelNumberOfBooks": "Anzahl der Hörbücher",
"LabelNumberOfEpisodes": "Anzahl der Episoden",
"LabelOpenRSSFeed": "Öffne RSS Feed",
"LabelOpenRSSFeed": "Öffne RSS-Feed",
"LabelOverwrite": "Überschreiben",
"LabelPassword": "Passwort",
"LabelPath": "Pfad",
@@ -374,30 +398,34 @@
"LabelPermissionsDownload": "Herunterladen",
"LabelPermissionsUpdate": "Aktualisieren",
"LabelPermissionsUpload": "Hochladen",
"LabelPersonalYearReview": "Dein Jahr in Übersicht ({0})",
"LabelPhotoPathURL": "Foto Pfad/URL",
"LabelPlaylists": "Wiedergabelisten",
"LabelPlayMethod": "Abspielmethode",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts",
"LabelPodcastSearchRegion": "Podcast-Suchregion",
"LabelPodcastType": "Podcast Typ",
"LabelPort": "Port",
"LabelPrefixesToIgnore": "Zu ignorierende(s) Vorwort(e) (Groß- und Kleinschreibung wird nicht berücksichtigt)",
"LabelPreventIndexing": "Verhindere, dass dein Feed von iTunes- und Google-Podcast-Verzeichnissen indiziert wird",
"LabelPrimaryEbook": "Haupt-E-Book",
"LabelPrimaryEbook": "Primäres E-Book",
"LabelProgress": "Fortschritt",
"LabelProvider": "Anbieter",
"LabelPubDate": "Veröffentlichungsdatum",
"LabelPublisher": "Herausgeber",
"LabelPublishYear": "Jahr",
"LabelRead": "Lesen",
"LabelReadAgain": "Nocheinmal Lesen",
"LabelReadAgain": "Noch einmal Lesen",
"LabelReadEbookWithoutProgress": "E-Book lesen und Fortschritt verwerfen",
"LabelRecentlyAdded": "Kürzlich hinzugefügt",
"LabelRecentSeries": "Aktuelle Serien",
"LabelRecommended": "Empfohlen",
"LabelRedo": "Wiederholen",
"LabelRegion": "Region",
"LabelReleaseDate": "Veröffentlichungsdatum",
"LabelRemoveCover": "Lösche Titelbild",
"LabelRowsPerPage": "Zeilen pro Seite",
"LabelRSSFeedCustomOwnerEmail": "Benutzerdefinierte Eigentümer-E-Mail",
"LabelRSSFeedCustomOwnerName": "Benutzerdefinierter Name des Eigentümers",
"LabelRSSFeedOpen": "RSS Feed Offen",
@@ -405,8 +433,8 @@
"LabelRSSFeedSlug": "RSS Feed Schlagwort",
"LabelRSSFeedURL": "RSS Feed URL",
"LabelSearchTerm": "Begriff suchen",
"LabelSearchTitle": "Titel",
"LabelSearchTitleOrASIN": "Titel oder ASIN",
"LabelSearchTitle": "Titel suchen",
"LabelSearchTitleOrASIN": "Titel oder ASIN suchen",
"LabelSeason": "Staffel",
"LabelSelectAllEpisodes": "Alle Episoden auswählen",
"LabelSelectEpisodesShowing": "{0} ausgewählte Episoden werden angezeigt",
@@ -416,10 +444,11 @@
"LabelSeries": "Serien",
"LabelSeriesName": "Serienname",
"LabelSeriesProgress": "Serienfortschritt",
"LabelSetEbookAsPrimary": "Setzen als Hauptbuch",
"LabelSetEbookAsSupplementary": "Setzen als Ergänzung",
"LabelSettingsAudiobooksOnly": "nur Hörbücher",
"LabelSettingsAudiobooksOnlyHelp": "Wenn Sie diese Einstellung aktivieren, werden E-Book-Dateien ignoriert, es sei denn, sie befinden sich in einem Hörbuchordner. In diesem Fall werden sie als zusätzliche E-Books festgelegt",
"LabelServerYearReview": "Server Jahr in Übersicht ({0})",
"LabelSetEbookAsPrimary": "Als Hauptbuch setzen",
"LabelSetEbookAsSupplementary": "Als Ergänzung setzen",
"LabelSettingsAudiobooksOnly": "Nur Hörbücher",
"LabelSettingsAudiobooksOnlyHelp": "Wenn du diese Einstellung aktivierst, werden E-Book-Dateien ignoriert, es sei denn, sie befinden sich in einem Hörbuchordner. In diesem Fall werden sie als zusätzliche E-Books festgelegt",
"LabelSettingsBookshelfViewHelp": "Skeumorphes Design mit Holzeinlegeböden",
"LabelSettingsChromecastSupport": "Chromecastunterstützung",
"LabelSettingsDateFormat": "Datumsformat",
@@ -430,13 +459,15 @@
"LabelSettingsEnableWatcherForLibrary": "Ordnerüberwachung für die Bibliothek aktivieren",
"LabelSettingsEnableWatcherHelp": "Aktiviert das automatische Hinzufügen/Aktualisieren von Elementen, wenn Dateiänderungen erkannt werden. *Erfordert einen Server-Neustart",
"LabelSettingsExperimentalFeatures": "Experimentelle Funktionen",
"LabelSettingsExperimentalFeaturesHelp": "Funktionen welche sich in der Entwicklung befinden, benötigen Ihr Feedback und Ihre Hilfe beim Testen. Klicken Sie hier, um die Github-Diskussion zu öffnen.",
"LabelSettingsExperimentalFeaturesHelp": "Funktionen welche sich in der Entwicklung befinden, benötigen dein Feedback und deine Hilfe beim Testen. Klicke hier, um die Github-Diskussion zu öffnen.",
"LabelSettingsFindCovers": "Suche Titelbilder",
"LabelSettingsFindCoversHelp": "Wenn Ihr Medium kein eingebettetes Titelbild oder kein Titelbild im Ordner hat, versucht der Scanner, ein Titelbild online zu finden.<br>Hinweis: Dies verlängert die Scandauer",
"LabelSettingsHideSingleBookSeries": "Ausblenden einzelzne Bücher",
"LabelSettingsHideSingleBookSeriesHelp": "Serien, die ein einzelnes Buch enthalten, werden in den Regalen der Serienseite und der Startseite ausgeblendet.",
"LabelSettingsFindCoversHelp": "Wenn dein Medium kein eingebettetes Titelbild oder kein Titelbild im Ordner hat, versucht der Scanner, ein Titelbild online zu finden.<br>Hinweis: Dies verlängert die Scandauer",
"LabelSettingsHideSingleBookSeries": "Ausblenden einzelner Bücher",
"LabelSettingsHideSingleBookSeriesHelp": "Serien, die nur ein einzelnes Buch enthalten, werden auf der Startseite und in der Serienansicht ausgeblendet.",
"LabelSettingsHomePageBookshelfView": "Startseite verwendet die Bücherregalansicht",
"LabelSettingsLibraryBookshelfView": "Bibliothek verwendet die Bücherregalansicht",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Analysiere Untertitel",
"LabelSettingsParseSubtitlesHelp": "Extrahiere den Untertitel von Medium-Ordnernamen.<br>Untertitel müssen vom eigentlichem Titel durch ein \" - \" getrennt sein. <br>Beispiel: \"Titel - Untertitel\"",
"LabelSettingsPreferMatchedMetadata": "Bevorzuge online abgestimmte Metadaten",
@@ -446,7 +477,7 @@
"LabelSettingsSortingIgnorePrefixes": "Vorwort/Artikel beim Sortieren ignorieren",
"LabelSettingsSortingIgnorePrefixesHelp": "Beispiel: für den Artikel \"der\" würde der Mediumtitel \"Der Buchtitel\" als \"Buchtitel, Der\" sortiert werden.",
"LabelSettingsSquareBookCovers": "Benutze quadratische Titelbilder",
"LabelSettingsSquareBookCoversHelp": "Bevorzugen quadratische Titelbilder gegenüber den Standardtielbildern im Verhältnis 1,6:1",
"LabelSettingsSquareBookCoversHelp": "Bevorzuge quadratische Titelbilder gegenüber den Standardtielbildern im Verhältnis 1,6:1",
"LabelSettingsStoreCoversWithItem": "Titelbilder im Medienordner speichern",
"LabelSettingsStoreCoversWithItemHelp": "Standardmäßig werden die Titelbilder in /metadata/items gespeichert. Wenn diese Option aktiviert ist, werden die Titelbilder als jpg Datei in dem gleichen Ordner gespeichert in welchem sich auch das Medium befindet. Es wird immer nur eine Datei mit dem Namen \"cover.jpg\" gespeichert.",
"LabelSettingsStoreMetadataWithItem": "Metadaten als OPF-Datei im Medienordner speichern",
@@ -454,7 +485,7 @@
"LabelSettingsTimeFormat": "Zeitformat",
"LabelShowAll": "Alles anzeigen",
"LabelSize": "Größe",
"LabelSleepTimer": "Einschlaf-Timer",
"LabelSleepTimer": "Sleep-Timer",
"LabelSlug": "URL Teil",
"LabelStart": "Start",
"LabelStarted": "Gestartet",
@@ -467,7 +498,7 @@
"LabelStatsDays": "Tage",
"LabelStatsDaysListened": "Gehörte Tage",
"LabelStatsHours": "Stunden",
"LabelStatsInARow": "nacheinander",
"LabelStatsInARow": "Nacheinander",
"LabelStatsItemsFinished": "Gehörte Medien",
"LabelStatsItemsInLibrary": "Bibliothekseinträge",
"LabelStatsMinutes": "Minuten",
@@ -482,9 +513,13 @@
"LabelTagsAccessibleToUser": "Für Benutzer zugängliche Schlagwörter",
"LabelTagsNotAccessibleToUser": "Für Benutzer nicht zugängliche Schlagwörter",
"LabelTasks": "Laufende Aufgaben",
"LabelTextEditorBulletedList": "Aufzählungsliste",
"LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "nummerierte Liste",
"LabelTextEditorUnlink": "entkoppeln",
"LabelTheme": "Theme",
"LabelThemeDark": "Dark",
"LabelThemeLight": "Light",
"LabelThemeDark": "Dunkel",
"LabelThemeLight": "Hell",
"LabelTimeBase": "Basiszeit",
"LabelTimeListened": "Gehörte Zeit",
"LabelTimeListenedToday": "Heute gehörte Zeit",
@@ -494,12 +529,12 @@
"LabelToolsEmbedMetadata": "Metadaten einbetten",
"LabelToolsEmbedMetadataDescription": "Bettet die Metadaten einschließlich des Titelbildes und der Kapitel in die Audiodatein ein.",
"LabelToolsMakeM4b": "M4B-Datei erstellen",
"LabelToolsMakeM4bDescription": "Erstellt eine M4B-Datei (Endung \".m4b\") welche mehrere mp3-Dateien in einer einzigen Datei inkl. derer Metadaten (Beschreibung, Titelbild, Kapitel, ....) zusammenfasst. M4B-Datei können darüber hinaus Lesezeichen speichern und mit einem Abspielschutz (Passwort) versehen werden.",
"LabelToolsMakeM4bDescription": "Erstellt eine M4B-Datei (Endung \".m4b\") welche mehrere mp3-Dateien in einer einzigen Datei inkl. derer Metadaten (Beschreibung, Titelbild, Kapitel, ...) zusammenfasst. M4B-Datei können darüber hinaus Lesezeichen speichern und mit einem Abspielschutz (Passwort) versehen werden.",
"LabelToolsSplitM4b": "M4B in MP3's aufteilen",
"LabelToolsSplitM4bDescription": "Erstellt aus einer mit Metadaten und nach Kapiteln aufgeteilten M4B-Datei seperate MP3's mit eingebetteten Metadaten, Coverbild und Kapiteln.",
"LabelTotalDuration": "Gesamtdauer",
"LabelTotalTimeListened": "Gehörte Gesamtzeit",
"LabelTrackFromFilename": "Titel von Dateiname",
"LabelTrackFromFilename": "Titel aus Dateiname",
"LabelTrackFromMetadata": "Titel aus Metadaten",
"LabelTracks": "Dateien",
"LabelTracksMultiTrack": "Mehrfachdatei",
@@ -507,14 +542,16 @@
"LabelTracksSingleTrack": "Einzeldatei",
"LabelType": "Typ",
"LabelUnabridged": "Ungekürzt",
"LabelUndo": "Rückgängig machen",
"LabelUnknown": "Unbekannt",
"LabelUpdateCover": "Titelbild aktualisieren",
"LabelUpdateCoverHelp": "Erlaube das Überschreiben bestehender Titelbilder für die ausgewählten Hörbücher wenn eine Übereinstimmung gefunden wird",
"LabelUpdateCoverHelp": "Erlaube das Überschreiben bestehender Titelbilder für die ausgewählten Hörbücher, wenn eine Übereinstimmung gefunden wird",
"LabelUpdatedAt": "Aktualisiert am",
"LabelUpdateDetails": "Details aktualisieren",
"LabelUpdateDetailsHelp": "Erlaube das Überschreiben bestehender Details für die ausgewählten Hörbücher wenn eine Übereinstimmung gefunden wird",
"LabelUpdateDetailsHelp": "Erlaube das Überschreiben bestehender Details für die ausgewählten Hörbücher, wenn eine Übereinstimmung gefunden wird",
"LabelUploaderDragAndDrop": "Ziehen und Ablegen von Dateien oder Ordnern",
"LabelUploaderDropFiles": "Dateien löschen",
"LabelUploaderItemFetchMetadataHelp": "Automatisches Aktualisieren von Titel, Autor und Serie",
"LabelUseChapterTrack": "Kapiteldatei verwenden",
"LabelUseFullTrack": "Gesamte Datei verwenden",
"LabelUser": "Benutzer",
@@ -523,17 +560,19 @@
"LabelVersion": "Version",
"LabelViewBookmarks": "Lesezeichen anzeigen",
"LabelViewChapters": "Kapitel anzeigen",
"LabelViewQueue": "Spieler-Warteschlange anzeigen",
"LabelVolume": "Volumen",
"LabelViewQueue": "Player-Warteschlange anzeigen",
"LabelVolume": "Lautstärke",
"LabelWeekdaysToRun": "Wochentage für die Ausführung",
"LabelYourAudiobookDuration": "Laufzeit Ihres Mediums",
"LabelYearReviewHide": "Verstecke Jahr in Übersicht",
"LabelYearReviewShow": "Zeige Jahr in Übersicht",
"LabelYourAudiobookDuration": "Laufzeit deines Mediums",
"LabelYourBookmarks": "Lesezeichen",
"LabelYourPlaylists": "Eigene Wiedergabelisten",
"LabelYourProgress": "Fortschritt",
"MessageAddToPlayerQueue": "Zur Abspielwarteliste hinzufügen",
"MessageAppriseDescription": "Um diese Funktion nutzen zu können, müssen Sie 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ürden Sie <code>http://192.168.1.1:8337/notify</code> eingeben.",
"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.",
"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.",
"MessageBatchQuickMatchDescription": "Der Schnellabgleich versucht, fehlende Titelbilder und Metadaten für die ausgewählten Artikel hinzuzufügen. Aktivieren Sie die nachstehenden Optionen, damit der Schnellabgleich vorhandene Titelbilder und/oder Metadaten überschreiben kann.",
"MessageBatchQuickMatchDescription": "Der Schnellabgleich versucht, fehlende Titelbilder und Metadaten für die ausgewählten Artikel hinzuzufügen. Aktiviere die nachstehenden Optionen, damit der Schnellabgleich vorhandene Titelbilder und/oder Metadaten überschreiben kann.",
"MessageBookshelfNoCollections": "Es wurden noch keine Sammlungen erstellt",
"MessageBookshelfNoResultsForFilter": "Keine Ergebnisse für Filter \"{0}: {1}\"",
"MessageBookshelfNoRSSFeeds": "Keine RSS-Feeds geöffnet",
@@ -544,56 +583,57 @@
"MessageChapterErrorStartLtPrev": "Ungültige Kapitelstartzeit: Kapitelanfang < Kapitelanfang vorheriges Kapitel (Kapitelanfang liegt zeitlich vor dem Beginn des vorherigen Kapitels -> Lösung: Kapitelanfang >= Startzeit des vorherigen Kapitels)",
"MessageChapterStartIsAfter": "Ungültige Kapitelstartzeit: Kapitelanfang > Mediumende (Kapitelanfang liegt nach dem Ende des Mediums)",
"MessageCheckingCron": "Überprüfe Cron...",
"MessageConfirmCloseFeed": "Feed wird geschlossen! Sind Sie sicher?",
"MessageConfirmDeleteBackup": "Sicherung für {0} wird gelöscht! Sind Sie sicher?",
"MessageConfirmDeleteFile": "Datei wird vom System gelöscht! Sind Sie sicher?",
"MessageConfirmDeleteLibrary": "Bibliothek \"{0}\" wird dauerhaft gelöscht! Sind Sie sicher?",
"MessageConfirmDeleteLibraryItem": "Bibliothekselement wird aus der Datenbank + Festplatte gelöscht? Sind Sie sicher?",
"MessageConfirmDeleteLibraryItems": "{0} Bibliothekselemente werden aus der Datenbank + Festplatte gelöscht? Sind Sie sicher?",
"MessageConfirmDeleteSession": "Sitzung wird gelöscht! Sind Sie sicher?",
"MessageConfirmForceReScan": "Scanvorgang erzwingen! Sind Sie sicher?",
"MessageConfirmMarkAllEpisodesFinished": "Alle Episoden werden als abgeschlossen markiert! Sind Sie sicher?",
"MessageConfirmMarkAllEpisodesNotFinished": "Alle Episoden werden als nicht abgeschlossen markiert! Sind Sie sicher?",
"MessageConfirmMarkSeriesFinished": "Alle Medien dieser Reihe werden als abgeschlossen markiert! Sind Sie sicher?",
"MessageConfirmMarkSeriesNotFinished": "Alle Medien dieser Reihe werden als nicht abgeschlossen markiert! Sind Sie sicher?",
"MessageConfirmQuickEmbed": "Warnung! Audiodateien werden bei der Schnelleinbettung nicht gesichert! Achten Sie darauf, dass Sie eine Sicherungskopie der Audiodateien besitzen. <br><br>Möchten Sie fortfahren?",
"MessageConfirmRemoveAllChapters": "Alle Kapitel werden entfernt! Sind Sie sicher?",
"MessageConfirmRemoveAuthor": "Autor \"{0}\" wird enfernt! Sind Sie sicher?",
"MessageConfirmRemoveCollection": "Sammlung \"{0}\" wird gelöscht! Sind Sie sicher?",
"MessageConfirmRemoveEpisode": "Episode \"{0}\" wird geloscht! Sind Sie sicher?",
"MessageConfirmRemoveEpisodes": "{0} Episoden werden gelöscht! Sind Sie sicher?",
"MessageConfirmRemoveNarrator": "Erzähler \"{0}\" wird gelöscht! Sind Sie sicher?",
"MessageConfirmRemovePlaylist": "Wiedergabeliste \"{0}\" wird entfernt! Sind Sie sicher?",
"MessageConfirmRenameGenre": "Kategorie \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Sind Sie sicher?",
"MessageConfirmCloseFeed": "Feed wird geschlossen! Bist du dir sicher?",
"MessageConfirmDeleteBackup": "Sicherung für {0} wird gelöscht! Bist du dir sicher?",
"MessageConfirmDeleteFile": "Datei wird vom System gelöscht! Bist du dir sicher?",
"MessageConfirmDeleteLibrary": "Bibliothek \"{0}\" wird dauerhaft gelöscht! Bist du dir sicher?",
"MessageConfirmDeleteLibraryItem": "Bibliothekselement wird aus der Datenbank + Festplatte gelöscht? Bist du dir sicher?",
"MessageConfirmDeleteLibraryItems": "{0} Bibliothekselemente werden aus der Datenbank + Festplatte gelöscht? Bist du dir sicher?",
"MessageConfirmDeleteSession": "Sitzung wird gelöscht! Bist du dir sicher?",
"MessageConfirmForceReScan": "Scanvorgang erzwingen! Bist du dir sicher?",
"MessageConfirmMarkAllEpisodesFinished": "Alle Episoden werden als abgeschlossen markiert! Bist du dir sicher?",
"MessageConfirmMarkAllEpisodesNotFinished": "Alle Episoden werden als nicht abgeschlossen markiert! Bist du dir sicher?",
"MessageConfirmMarkSeriesFinished": "Alle Medien dieser Reihe werden als abgeschlossen markiert! Bist du dir sicher?",
"MessageConfirmMarkSeriesNotFinished": "Alle Medien dieser Reihe werden als nicht abgeschlossen markiert! Bist du dir sicher?",
"MessageConfirmQuickEmbed": "Warnung! Audiodateien werden bei der Schnelleinbettung nicht gesichert! Achte darauf, dass du eine Sicherungskopie der Audiodateien besitzt. <br><br>Möchtest du fortfahren?",
"MessageConfirmRemoveAllChapters": "Alle Kapitel werden entfernt! Bist du dir sicher?",
"MessageConfirmRemoveAuthor": "Autor \"{0}\" wird enfernt! Bist du dir sicher?",
"MessageConfirmRemoveCollection": "Sammlung \"{0}\" wird gelöscht! Bist du dir sicher?",
"MessageConfirmRemoveEpisode": "Episode \"{0}\" wird geloscht! Bist du dir sicher?",
"MessageConfirmRemoveEpisodes": "{0} Episoden werden gelöscht! Bist du dir sicher?",
"MessageConfirmRemoveListeningSessions": "Bist du dir sicher, dass du {0} Hörsitzungen enfernen möchtest?",
"MessageConfirmRemoveNarrator": "Erzähler \"{0}\" wird gelöscht! Bist du dir sicher?",
"MessageConfirmRemovePlaylist": "Wiedergabeliste \"{0}\" wird entfernt! Bist du dir sicher?",
"MessageConfirmRenameGenre": "Kategorie \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Bist du dir sicher?",
"MessageConfirmRenameGenreMergeNote": "Hinweis: Kategorie existiert bereits -> Kategorien werden zusammengelegt.",
"MessageConfirmRenameGenreWarning": "Warnung! Ein ähnliche Kategorie mit einem anderen Wortlaut existiert bereits: \"{0}\".",
"MessageConfirmRenameTag": "Tag \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Sind Sie sicher?",
"MessageConfirmRenameTag": "Tag \"{0}\" in \"{1}\" für alle Hörbücher/Podcasts werden umbenannt! Bist du dir sicher?",
"MessageConfirmRenameTagMergeNote": "Hinweis: Tag existiert bereits -> Tags werden zusammengelegt.",
"MessageConfirmRenameTagWarning": "Warnung! Ein ähnlicher Tag mit einem anderen Wortlaut existiert bereits: \"{0}\".",
"MessageConfirmReScanLibraryItems": "{0} Elemente werden erneut gescannt! Sind Sie sicher?",
"MessageConfirmSendEbookToDevice": "{0} E-Book \"{1}\" werden auf das Gerät \"{2}\" gesendet! Sind Sie sicher?",
"MessageDownloadingEpisode": "Episode herunterladen",
"MessageDragFilesIntoTrackOrder": "Verschieben Sie die Dateien in die richtige Reihenfolge",
"MessageConfirmReScanLibraryItems": "{0} Elemente werden erneut gescannt! Bist du dir sicher?",
"MessageConfirmSendEbookToDevice": "{0} E-Book \"{1}\" wird auf das Gerät \"{2}\" gesendet! Bist du dir sicher?",
"MessageDownloadingEpisode": "Episode wird heruntergeladen",
"MessageDragFilesIntoTrackOrder": "Verschiebe die Dateien in die richtige Reihenfolge",
"MessageEmbedFinished": "Einbettung abgeschlossen!",
"MessageEpisodesQueuedForDownload": "{0} Episode(n) in der Warteschlange zum Herunterladen",
"MessageFeedURLWillBe": "Feed-URL wird {0} sein",
"MessageFetching": "Abrufen...",
"MessageForceReScanDescription": "durchsucht alle Dateien neu, wie bei einem frischen Scan. ID3-Tags von Audiodateien, OPF-Dateien und Textdateien werden neu durchsucht.",
"MessageForceReScanDescription": "Durchsucht alle Dateien erneut, wie bei einem frischen Scan. ID3-Tags von Audiodateien, OPF-Dateien und Textdateien werden neu durchsucht.",
"MessageImportantNotice": "Wichtiger Hinweis!",
"MessageInsertChapterBelow": "Kapitel unten einfügen",
"MessageItemsSelected": "{0} ausgewählte Medien",
"MessageItemsUpdated": "{0} Medien aktualisiert",
"MessageJoinUsOn": "Besuchen Sie uns auf",
"MessageJoinUsOn": "Besuche uns auf",
"MessageListeningSessionsInTheLastYear": "{0} Ereignisse im letzten Jahr",
"MessageLoading": "Laden...",
"MessageLoadingFolders": "Lade Ordner...",
"MessageM4BFailed": "M4B fehlgeschlagen!",
"MessageM4BFinished": "M4B beendet!",
"MessageMapChapterTitles": "Zuordnen von Kapiteltiteln zu Ihren vorhandenen Medienkapiteln ohne Anpassung der Zeitangaben",
"MessageMapChapterTitles": "Zuordnen von Kapiteltiteln zu deinen vorhandenen Medienkapiteln ohne Anpassung der Zeitangaben",
"MessageMarkAllEpisodesFinished": "Alle Episoden als beendet markieren",
"MessageMarkAllEpisodesNotFinished": "Alle Episoden als ungehört markieren",
"MessageMarkAsFinished": "Als beendet markieren",
"MessageMarkAsNotFinished": "Als nicht abgeschlossen markieren",
"MessageMarkAsNotFinished": "Als nicht beendet markieren",
"MessageMatchBooksDescription": "Es wird versucht die Bücher in der Bibliothek mit einem Buch des ausgewählten Suchanbieters abzugleichen um leere Details und das Titelbild auszufüllen. Vorhandene Details werden nicht überschrieben.",
"MessageNoAudioTracks": "Keine Audiodateien",
"MessageNoAuthors": "Keine Autoren",
@@ -626,7 +666,7 @@
"MessageNoUpdateNecessary": "Keine Aktualisierung erforderlich",
"MessageNoUpdatesWereNecessary": "Keine Aktualisierungen waren notwendig",
"MessageNoUserPlaylists": "Keine Wiedergabelisten vorhanden",
"MessageOr": "oder",
"MessageOr": "Oder",
"MessagePauseChapter": "Kapitelwiedergabe pausieren",
"MessagePlayChapter": "Kapitelanfang anhören",
"MessagePlaylistCreateFromCollection": "Erstelle eine Wiedergabeliste aus der Sammlung",
@@ -635,12 +675,13 @@
"MessageRemoveChapter": "Kapitel löschen",
"MessageRemoveEpisodes": "Entferne {0} Episode(n)",
"MessageRemoveFromPlayerQueue": "Aus der Abspielwarteliste löschen",
"MessageRemoveUserWarning": "Benutzer \"{0}\" wird dauerhaft gelöscht! Sind Sie sicher?",
"MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und Beiträge leisten auf",
"MessageResetChaptersConfirm": "Kapitel und vorgenommenen Änderungen werden zurückgesetzt und rückgängig gemacht! Sind Sie sicher?",
"MessageRestoreBackupConfirm": "Sind Sie sicher, dass Sie die Sicherung wiederherstellen wollen, 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 Ihren Bibliotheksordnern verändert. Wenn Sie die Servereinstellungen aktiviert haben, um Cover und Metadaten in Ihren Bibliotheksordnern zu speichern, werden diese nicht gesichert oder überschrieben.<br /><br />Alle Clients, die Ihren Server nutzen, werden automatisch aktualisiert.",
"MessageRemoveUserWarning": "Benutzer \"{0}\" wird dauerhaft gelöscht! Bist du dir sicher?",
"MessageReportBugsAndContribute": "Fehler melden, Funktionen anfordern und mitwirken",
"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.",
"MessageSearchResultsFor": "Suchergebnisse für",
"MessageSelected": "{0} ausgewählt",
"MessageServerCouldNotBeReached": "Server kann nicht erreicht werden",
"MessageSetChaptersFromTracksDescription": "Kaitelerstellung basiert auf den existierenden einzelnen Audiodateien. Pro existierende Audiodatei wird 1 Kapitel erstellt, wobei deren Kapitelname aus dem Audiodateinamen extrahiert wird",
"MessageStartPlaybackAtTime": "Start der Wiedergabe für \"{0}\" bei {1}?",
@@ -651,16 +692,15 @@
"MessageValidCronExpression": "Gültiger Cron-Ausdruck",
"MessageWatcherIsDisabledGlobally": "Überwachung ist in den Servereinstellungen global deaktiviert",
"MessageXLibraryIsEmpty": "{0} Bibliothek ist leer!",
"MessageYourAudiobookDurationIsLonger": "Die Dauer Ihres Mediums ist länger als die gefundene Dauer",
"MessageYourAudiobookDurationIsShorter": "Die Dauer Ihres Mediums ist kürzer als die gefundene Dauer",
"MessageYourAudiobookDurationIsLonger": "Die Dauer deines Mediums ist länger als die gefundene Dauer",
"MessageYourAudiobookDurationIsShorter": "Die Dauer deines Mediums ist kürzer als die gefundene Dauer",
"NoteChangeRootPassword": "Der Root-Benutzer (Hauptbenutzer) ist der einzige Benutzer, der ein leeres Passwort haben kann",
"NoteChapterEditorTimes": "Hinweis: Die Anfangszeit des ersten Kapitels muss bei 0:00 beginnen und die Anfangszeit des letzten Kapitels darf die Dauer des Mediums nicht überschreiten.",
"NoteFolderPicker": "Hinweis: Bereits zugeordnete Ordner werden nicht angezeigt.",
"NoteFolderPickerDebian": "Hinweis: Der Ordnerauswahldialog für die Debian-Installation ist nicht vollständig implementiert. Sie sollten den Pfad zu Ihrer Bibliothek direkt eingeben.",
"NoteRSSFeedPodcastAppsHttps": "Warnung: Die meisten Podcast-Apps verlangen, dass die URL des RSS-Feeds HTTPS verwendet.",
"NoteRSSFeedPodcastAppsPubDate": "Warnung: 1 oder mehrere Ihrer Episoden haben kein Veröffentlichungsdatum. Einige Podcast-Apps verlangen dies.",
"NoteRSSFeedPodcastAppsPubDate": "Warnung: 1 oder mehrere deiner Episoden haben kein Veröffentlichungsdatum. Einige Podcast-Apps verlangen dies.",
"NoteUploaderFoldersWithMediaFiles": "Ordner mit Mediendateien werden als separate Bibliothekselemente behandelt.",
"NoteUploaderOnlyAudioFiles": "Wenn Sie nur Audiodateien hochladen, wird jede Audiodatei als ein separates Medium behandelt.",
"NoteUploaderOnlyAudioFiles": "Wenn du nur Audiodateien hochlädst, wird jede Audiodatei als ein separates Medium behandelt.",
"NoteUploaderUnsupportedFiles": "Nicht unterstützte Dateien werden ignoriert. Bei der Auswahl oder dem Löschen eines Ordners werden andere Dateien, die sich nicht in einem Elementordner befinden, ignoriert.",
"PlaceholderNewCollection": "Neuer Sammlungsname",
"PlaceholderNewFolderPath": "Neuer Ordnerpfad",
@@ -728,7 +768,7 @@
"ToastRSSFeedCloseFailed": "RSS-Feed konnte nicht geschlossen werden",
"ToastRSSFeedCloseSuccess": "RSS-Feed geschlossen",
"ToastSendEbookToDeviceFailed": "E-Book konnte nicht auf Gerät übertragen werden",
"ToastSendEbookToDeviceSuccess": "E-Book an Gerät senden \"{0}\"",
"ToastSendEbookToDeviceSuccess": "E-Book an Gerät \"{0}\" gesendet",
"ToastSeriesUpdateFailed": "Aktualisierung der Serien fehlgeschlagen",
"ToastSeriesUpdateSuccess": "Serien aktualisiert",
"ToastSessionDeleteFailed": "Sitzung konnte nicht gelöscht werden",

View File

@@ -32,6 +32,8 @@
"ButtonHide": "Hide",
"ButtonHome": "Home",
"ButtonIssues": "Issues",
"ButtonJumpBackward": "Jump Backward",
"ButtonJumpForward": "Jump Forward",
"ButtonLatest": "Latest",
"ButtonLibrary": "Library",
"ButtonLogout": "Logout",
@@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "Match All Authors",
"ButtonMatchBooks": "Match Books",
"ButtonNevermind": "Nevermind",
"ButtonNext": "Next",
"ButtonNextChapter": "Next Chapter",
"ButtonOk": "Ok",
"ButtonOpenFeed": "Open Feed",
"ButtonOpenManager": "Open Manager",
"ButtonPause": "Pause",
"ButtonPlay": "Play",
"ButtonPlaying": "Playing",
"ButtonPlaylists": "Playlists",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "Purge All Cache",
"ButtonPurgeItemsCache": "Purge Items Cache",
"ButtonPurgeMediaProgress": "Purge Media Progress",
@@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "Remove from queue",
"ButtonQuickMatch": "Quick Match",
"ButtonRead": "Read",
"ButtonRefresh": "Refresh",
"ButtonRemove": "Remove",
"ButtonRemoveAll": "Remove All",
"ButtonRemoveAllLibraryItems": "Remove All Library Items",
@@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "Select Folder Path",
"ButtonSeries": "Series",
"ButtonSetChaptersFromTracks": "Set chapters from tracks",
"ButtonShare": "Share",
"ButtonShiftTimes": "Shift Times",
"ButtonShow": "Show",
"ButtonStartM4BEncode": "Start M4B Encode",
@@ -87,6 +96,9 @@
"ButtonUserEdit": "Edit user {0}",
"ButtonViewAll": "View All",
"ButtonYes": "Yes",
"ErrorUploadFetchMetadataAPI": "Error fetching metadata",
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
"ErrorUploadLacksTitle": "Must have a title",
"HeaderAccount": "Account",
"HeaderAdvanced": "Advanced",
"HeaderAppriseNotificationSettings": "Apprise Notification Settings",
@@ -101,6 +113,7 @@
"HeaderCollectionItems": "Collection Items",
"HeaderCover": "Cover",
"HeaderCurrentDownloads": "Current Downloads",
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "Details",
"HeaderDownloadQueue": "Download Queue",
"HeaderEbookFiles": "Ebook Files",
@@ -171,6 +184,7 @@
"HeaderUpdateDetails": "Update Details",
"HeaderUpdateLibrary": "Update Library",
"HeaderUsers": "Users",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Your Stats",
"LabelAbridged": "Abridged",
"LabelAccountType": "Account Type",
@@ -196,6 +210,8 @@
"LabelAuthorLastFirst": "Author (Last, First)",
"LabelAuthors": "Authors",
"LabelAutoDownloadEpisodes": "Auto Download Episodes",
"LabelAutoFetchMetadata": "Auto Fetch Metadata",
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
"LabelAutoLaunch": "Auto Launch",
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register",
@@ -266,6 +282,7 @@
"LabelExample": "Example",
"LabelExplicit": "Explicit",
"LabelFeedURL": "Feed URL",
"LabelFetchingMetadata": "Fetching Metadata",
"LabelFile": "File",
"LabelFileBirthtime": "File Birthtime",
"LabelFileModified": "File Modified",
@@ -275,8 +292,11 @@
"LabelFinished": "Finished",
"LabelFolder": "Folder",
"LabelFolders": "Folders",
"LabelFontBold": "Bold",
"LabelFontFamily": "Font family",
"LabelFontItalic": "Italic",
"LabelFontScale": "Font scale",
"LabelFontStrikethrough": "Strikethrough",
"LabelFormat": "Format",
"LabelGenre": "Genre",
"LabelGenres": "Genres",
@@ -336,7 +356,11 @@
"LabelMetaTags": "Meta Tags",
"LabelMinute": "Minute",
"LabelMissing": "Missing",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Missing Parts",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "More",
"LabelMoreInfo": "More Info",
"LabelName": "Name",
@@ -374,11 +398,13 @@
"LabelPermissionsDownload": "Can Download",
"LabelPermissionsUpdate": "Can Update",
"LabelPermissionsUpload": "Can Upload",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Photo Path/URL",
"LabelPlaylists": "Playlists",
"LabelPlayMethod": "Play Method",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts",
"LabelPodcastSearchRegion": "Podcast search region",
"LabelPodcastType": "Podcast Type",
"LabelPort": "Port",
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
@@ -395,9 +421,11 @@
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRecommended": "Recommended",
"LabelRedo": "Redo",
"LabelRegion": "Region",
"LabelReleaseDate": "Release Date",
"LabelRemoveCover": "Remove cover",
"LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedOpen": "RSS Feed Open",
@@ -416,6 +444,7 @@
"LabelSeries": "Series",
"LabelSeriesName": "Series Name",
"LabelSeriesProgress": "Series Progress",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Set as primary",
"LabelSetEbookAsSupplementary": "Set as supplementary",
"LabelSettingsAudiobooksOnly": "Audiobooks only",
@@ -437,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
"LabelSettingsHomePageBookshelfView": "Home page use bookshelf view",
"LabelSettingsLibraryBookshelfView": "Library use bookshelf view",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Parse subtitles",
"LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by \" - \"<br>i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"",
"LabelSettingsPreferMatchedMetadata": "Prefer matched metadata",
@@ -482,6 +513,10 @@
"LabelTagsAccessibleToUser": "Tags Accessible to User",
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
"LabelTasks": "Tasks Running",
"LabelTextEditorBulletedList": "Bulleted list",
"LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "Numbered list",
"LabelTextEditorUnlink": "Unlink",
"LabelTheme": "Theme",
"LabelThemeDark": "Dark",
"LabelThemeLight": "Light",
@@ -507,6 +542,7 @@
"LabelTracksSingleTrack": "Single-track",
"LabelType": "Type",
"LabelUnabridged": "Unabridged",
"LabelUndo": "Undo",
"LabelUnknown": "Unknown",
"LabelUpdateCover": "Update Cover",
"LabelUpdateCoverHelp": "Allow overwriting of existing covers for the selected books when a match is located",
@@ -515,6 +551,7 @@
"LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located",
"LabelUploaderDragAndDrop": "Drag & drop files or folders",
"LabelUploaderDropFiles": "Drop files",
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
"LabelUseChapterTrack": "Use chapter track",
"LabelUseFullTrack": "Use full track",
"LabelUser": "User",
@@ -526,6 +563,8 @@
"LabelViewQueue": "View player queue",
"LabelVolume": "Volume",
"LabelWeekdaysToRun": "Weekdays to run",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Your audiobook duration",
"LabelYourBookmarks": "Your Bookmarks",
"LabelYourPlaylists": "Your Playlists",
@@ -562,6 +601,7 @@
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
@@ -641,6 +681,7 @@
"MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on",
"MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.",
"MessageSearchResultsFor": "Search results for",
"MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Server could not be reached",
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
"MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?",
@@ -656,7 +697,6 @@
"NoteChangeRootPassword": "Root user is the only user that can have an empty password",
"NoteChapterEditorTimes": "Note: First chapter start time must remain at 0:00 and the last chapter start time cannot exceed this audiobooks duration.",
"NoteFolderPicker": "Note: folders already mapped will not be shown",
"NoteFolderPickerDebian": "Note: Folder picker for the debian install is not fully implemented. You should enter the path to your library directly.",
"NoteRSSFeedPodcastAppsHttps": "Warning: Most podcast apps will require the RSS feed URL is using HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.",
"NoteUploaderFoldersWithMediaFiles": "Folders with media files will be handled as separate library items.",

View File

@@ -1,11 +1,11 @@
{
"ButtonAdd": "Agregar",
"ButtonAddChapters": "Agregar Capitulo",
"ButtonAddDevice": "Add Device",
"ButtonAddLibrary": "Add Library",
"ButtonAddDevice": "Agregar Dispositivo",
"ButtonAddLibrary": "Crear Biblioteca",
"ButtonAddPodcasts": "Agregar Podcasts",
"ButtonAddUser": "Add User",
"ButtonAddYourFirstLibrary": "Agrega tu Primera Biblioteca",
"ButtonAddUser": "Crear Usuario",
"ButtonAddYourFirstLibrary": "Crea tu Primera Biblioteca",
"ButtonApply": "Aplicar",
"ButtonApplyChapters": "Aplicar Capítulos",
"ButtonAuthors": "Autores",
@@ -32,6 +32,8 @@
"ButtonHide": "Esconder",
"ButtonHome": "Inicio",
"ButtonIssues": "Problemas",
"ButtonJumpBackward": "Retroceder",
"ButtonJumpForward": "Adelantar",
"ButtonLatest": "Últimos",
"ButtonLibrary": "Biblioteca",
"ButtonLogout": "Cerrar Sesión",
@@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "Encontrar Todos los Autores",
"ButtonMatchBooks": "Encontrar Libros",
"ButtonNevermind": "Olvidar",
"ButtonNext": "Next",
"ButtonNextChapter": "Siguiente Capítulo",
"ButtonOk": "Ok",
"ButtonOpenFeed": "Abrir Fuente",
"ButtonOpenManager": "Abrir Editor",
"ButtonPause": "Pausar",
"ButtonPlay": "Reproducir",
"ButtonPlaying": "Reproduciendo",
"ButtonPlaylists": "Listas de Reproducción",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Capítulo Anterior",
"ButtonPurgeAllCache": "Purgar Todo el Cache",
"ButtonPurgeItemsCache": "Purgar Elementos de Cache",
"ButtonPurgeMediaProgress": "Purgar Progreso de Multimedia",
@@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "Remover de la Fila",
"ButtonQuickMatch": "Encontrar Rápido",
"ButtonRead": "Leer",
"ButtonRefresh": "Refresh",
"ButtonRemove": "Remover",
"ButtonRemoveAll": "Remover Todos",
"ButtonRemoveAllLibraryItems": "Remover Todos los Elementos de la Biblioteca",
@@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "Seleccionar Ruta de Carpeta",
"ButtonSeries": "Series",
"ButtonSetChaptersFromTracks": "Seleccionar Capítulos Según las Pistas",
"ButtonShare": "Share",
"ButtonShiftTimes": "Desplazar Tiempos",
"ButtonShow": "Mostrar",
"ButtonStartM4BEncode": "Iniciar Codificación M4B",
@@ -87,12 +96,15 @@
"ButtonUserEdit": "Editar Usuario {0}",
"ButtonViewAll": "Ver Todos",
"ButtonYes": "Aceptar",
"ErrorUploadFetchMetadataAPI": "Error obteniendo metadatos",
"ErrorUploadFetchMetadataNoResults": "No se pudo obtener metadatos - Intenta actualizar el título y/o autor",
"ErrorUploadLacksTitle": "Se debe tener título",
"HeaderAccount": "Cuenta",
"HeaderAdvanced": "Avanzado",
"HeaderAppriseNotificationSettings": "Ajustes de Notificaciones de Apprise",
"HeaderAudiobookTools": "Herramientas de Gestión de Archivos de Audiolibro",
"HeaderAudioTracks": "Pistas de Audio",
"HeaderAuthentication": "Authentication",
"HeaderAuthentication": "Autenticación",
"HeaderBackups": "Respaldos",
"HeaderChangePassword": "Cambiar Contraseña",
"HeaderChapters": "Capítulos",
@@ -101,6 +113,7 @@
"HeaderCollectionItems": "Elementos en la Colección",
"HeaderCover": "Portada",
"HeaderCurrentDownloads": "Descargando Actualmente",
"HeaderCustomMetadataProviders": "Proveedores de metadatos personalizados",
"HeaderDetails": "Detalles",
"HeaderDownloadQueue": "Lista de Descarga",
"HeaderEbookFiles": "Archivos de Ebook",
@@ -127,15 +140,15 @@
"HeaderManageTags": "Administrar Etiquetas",
"HeaderMapDetails": "Asignar Detalles",
"HeaderMatch": "Encontrar",
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
"HeaderMetadataOrderOfPrecedence": "Orden de precedencia de metadatos",
"HeaderMetadataToEmbed": "Metadatos para Insertar",
"HeaderNewAccount": "Nueva Cuenta",
"HeaderNewLibrary": "Nueva Biblioteca",
"HeaderNotifications": "Notificaciones",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
"HeaderOpenIDConnectAuthentication": "Autenticación OpenID Connect",
"HeaderOpenRSSFeed": "Abrir fuente RSS",
"HeaderOtherFiles": "Otros Archivos",
"HeaderPasswordAuthentication": "Password Authentication",
"HeaderPasswordAuthentication": "Autenticación por contraseña",
"HeaderPermissions": "Permisos",
"HeaderPlayerQueue": "Fila del Reproductor",
"HeaderPlaylist": "Lista de Reproducción",
@@ -171,6 +184,7 @@
"HeaderUpdateDetails": "Actualizar Detalles",
"HeaderUpdateLibrary": "Actualizar Biblioteca",
"HeaderUsers": "Usuarios",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Tus Estadísticas",
"LabelAbridged": "Abreviado",
"LabelAccountType": "Tipo de Cuenta",
@@ -184,11 +198,11 @@
"LabelAddToCollectionBatch": "Se Añadieron {0} Libros a la Colección",
"LabelAddToPlaylist": "Añadido a la Lista de Reproducción",
"LabelAddToPlaylistBatch": "Se Añadieron {0} Artículos a la Lista de Reproducción",
"LabelAdminUsersOnly": "Admin users only",
"LabelAdminUsersOnly": "Solamente usuarios administradores",
"LabelAll": "Todos",
"LabelAllUsers": "Todos los Usuarios",
"LabelAllUsersExcludingGuests": "All users excluding guests",
"LabelAllUsersIncludingGuests": "All users including guests",
"LabelAllUsersExcludingGuests": "Todos los usuarios excepto invitados",
"LabelAllUsersIncludingGuests": "Todos los usuarios e invitados",
"LabelAlreadyInYourLibrary": "Ya en la Biblioteca",
"LabelAppend": "Adjuntar",
"LabelAuthor": "Autor",
@@ -196,10 +210,12 @@
"LabelAuthorLastFirst": "Autor (Apellido, Nombre)",
"LabelAuthors": "Autores",
"LabelAutoDownloadEpisodes": "Descargar Episodios Automáticamente",
"LabelAutoLaunch": "Auto Launch",
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register",
"LabelAutoRegisterDescription": "Automatically create new users after logging in",
"LabelAutoFetchMetadata": "Actualizar Metadatos Automáticamente",
"LabelAutoFetchMetadataHelp": "Obtiene metadatos de título, autor y serie para agilizar la carga. Es posible que haya que cotejar metadatos adicionales después de la carga.",
"LabelAutoLaunch": "Lanzamiento automático",
"LabelAutoLaunchDescription": "Redirigir al proveedor de autenticación automáticamente al navegar a la página de inicio de sesión (ruta de sobreescritura manual <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Registro automático",
"LabelAutoRegisterDescription": "Crear usuarios automáticamente tras iniciar sesión",
"LabelBackToUser": "Regresar a Usuario",
"LabelBackupLocation": "Ubicación del Respaldo",
"LabelBackupsEnableAutomaticBackups": "Habilitar Respaldo Automático",
@@ -210,13 +226,13 @@
"LabelBackupsNumberToKeepHelp": "Solamente 1 respaldo se removerá a la vez. Si tiene mas respaldos guardados, debe removerlos manualmente.",
"LabelBitrate": "Bitrate",
"LabelBooks": "Libros",
"LabelButtonText": "Button Text",
"LabelButtonText": "Texto del botón",
"LabelChangePassword": "Cambiar Contraseña",
"LabelChannels": "Canales",
"LabelChapters": "Capítulos",
"LabelChaptersFound": "Capítulo Encontrado",
"LabelChapterTitle": "Titulo del Capítulo",
"LabelClickForMoreInfo": "Click for more info",
"LabelClickForMoreInfo": "Click para más información",
"LabelClosePlayer": "Cerrar Reproductor",
"LabelCodec": "Codec",
"LabelCollapseSeries": "Colapsar Serie",
@@ -235,12 +251,12 @@
"LabelCurrently": "En este momento:",
"LabelCustomCronExpression": "Expresión de Cron Personalizada:",
"LabelDatetime": "Hora y Fecha",
"LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
"LabelDeleteFromFileSystemCheckbox": "Eliminar archivos del sistema (desmarcar para eliminar sólo de la base de datos)",
"LabelDescription": "Descripción",
"LabelDeselectAll": "Deseleccionar Todos",
"LabelDevice": "Dispositivo",
"LabelDeviceInfo": "Información de Dispositivo",
"LabelDeviceIsAvailableTo": "Device is available to...",
"LabelDeviceIsAvailableTo": "El dispositivo está disponible para...",
"LabelDirectory": "Directorio",
"LabelDiscFromFilename": "Disco a partir del Nombre del Archivo",
"LabelDiscFromMetadata": "Disco a partir de Metadata",
@@ -266,6 +282,7 @@
"LabelExample": "Ejemplo",
"LabelExplicit": "Explicito",
"LabelFeedURL": "Fuente de URL",
"LabelFetchingMetadata": "Obteniendo metadatos",
"LabelFile": "Archivo",
"LabelFileBirthtime": "Archivo Creado en",
"LabelFileModified": "Archivo modificado",
@@ -275,20 +292,23 @@
"LabelFinished": "Terminado",
"LabelFolder": "Carpeta",
"LabelFolders": "Carpetas",
"LabelFontBold": "Negrilla",
"LabelFontFamily": "Familia tipográfica",
"LabelFontItalic": "Itálica",
"LabelFontScale": "Tamaño de Fuente",
"LabelFontStrikethrough": "Tachado",
"LabelFormat": "Formato",
"LabelGenre": "Genero",
"LabelGenres": "Géneros",
"LabelHardDeleteFile": "Eliminar Definitivamente",
"LabelHasEbook": "Tiene Ebook",
"LabelHasSupplementaryEbook": "Tiene Ebook Suplementario",
"LabelHighestPriority": "Highest priority",
"LabelHighestPriority": "Mayor prioridad",
"LabelHost": "Host",
"LabelHour": "Hora",
"LabelIcon": "Icono",
"LabelImageURLFromTheWeb": "Image URL from the web",
"LabelIncludeInTracklist": "Incluir en Tracklist",
"LabelImageURLFromTheWeb": "URL de la imagen",
"LabelIncludeInTracklist": "Incluir en la Tracklist",
"LabelIncomplete": "Incompleto",
"LabelInProgress": "En Proceso",
"LabelInterval": "Intervalo",
@@ -325,18 +345,22 @@
"LabelLogLevelInfo": "Información",
"LabelLogLevelWarn": "Advertencia",
"LabelLookForNewEpisodesAfterDate": "Buscar Nuevos Episodios a partir de esta Fecha",
"LabelLowestPriority": "Lowest Priority",
"LabelMatchExistingUsersBy": "Match existing users by",
"LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
"LabelLowestPriority": "Menor prioridad",
"LabelMatchExistingUsersBy": "Emparejar a los usuarios existentes por",
"LabelMatchExistingUsersByDescription": "Se utiliza para conectar usuarios existentes. Una vez conectados, los usuarios serán emparejados por un identificador único de su proveedor de SSO",
"LabelMediaPlayer": "Reproductor de Medios",
"LabelMediaType": "Tipo de Multimedia",
"LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources",
"LabelMetadataProvider": "Proveedor de Metadata",
"LabelMetaTag": "Meta Tag",
"LabelMetaTags": "Meta Tags",
"LabelMetadataOrderOfPrecedenceDescription": "Las fuentes de metadatos de mayor prioridad prevalecerán sobre las de menor prioridad",
"LabelMetadataProvider": "Proveedor de Metadatos",
"LabelMetaTag": "Metaetiqueta",
"LabelMetaTags": "Metaetiquetas",
"LabelMinute": "Minuto",
"LabelMissing": "Ausente",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Partes Ausentes",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "URIs de redirección a móviles permitidos",
"LabelMobileRedirectURIsDescription": "Esta es una lista de URIs válidos para redireccionamiento de apps móviles. La URI por defecto es <code>audiobookshelf://oauth</code>, la cual puedes remover or corroborar con URIs adicionales para la integración con apps de terceros. Utilizando un asterisco (<code>*</code>) como el único punto de entrada permite cualquier URI.",
"LabelMore": "Más",
"LabelMoreInfo": "Más Información",
"LabelName": "Nombre",
@@ -374,11 +398,13 @@
"LabelPermissionsDownload": "Puede Descargar",
"LabelPermissionsUpdate": "Puede Actualizar",
"LabelPermissionsUpload": "Puede Subir",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Ruta de Acceso/URL de Foto",
"LabelPlaylists": "Lista de Reproducción",
"LabelPlayMethod": "Método de Reproducción",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts",
"LabelPodcastSearchRegion": "Región de búsqueda de podcasts",
"LabelPodcastType": "Tipo Podcast",
"LabelPort": "Puerto",
"LabelPrefixesToIgnore": "Prefijos para Ignorar (no distingue entre mayúsculas y minúsculas.)",
@@ -395,9 +421,11 @@
"LabelRecentlyAdded": "Agregado Recientemente",
"LabelRecentSeries": "Series Recientes",
"LabelRecommended": "Recomendados",
"LabelRedo": "Rehacer",
"LabelRegion": "Región",
"LabelReleaseDate": "Fecha de Estreno",
"LabelRemoveCover": "Remover Portada",
"LabelRowsPerPage": "Filas por página",
"LabelRSSFeedCustomOwnerEmail": "Email de dueño personalizado",
"LabelRSSFeedCustomOwnerName": "Nombre de dueño personalizado",
"LabelRSSFeedOpen": "Fuente RSS Abierta",
@@ -410,12 +438,13 @@
"LabelSeason": "Temporada",
"LabelSelectAllEpisodes": "Seleccionar todos los episodios",
"LabelSelectEpisodesShowing": "Seleccionar los {0} episodios visibles",
"LabelSelectUsers": "Select users",
"LabelSelectUsers": "Seleccionar usuarios",
"LabelSendEbookToDevice": "Enviar Ebook a...",
"LabelSequence": "Secuencia",
"LabelSeries": "Series",
"LabelSeriesName": "Nombre de la Serie",
"LabelSeriesProgress": "Progreso de la Serie",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Establecer como primario",
"LabelSetEbookAsSupplementary": "Establecer como suplementario",
"LabelSettingsAudiobooksOnly": "Sólo Audiolibros",
@@ -437,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Las series con un solo libro no aparecerán en la página de series ni la repisa para series de la página principal.",
"LabelSettingsHomePageBookshelfView": "Usar la vista de librero en la página principal",
"LabelSettingsLibraryBookshelfView": "Usar la vista de librero en la biblioteca",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Extraer Subtítulos",
"LabelSettingsParseSubtitlesHelp": "Extraer subtítulos de los nombres de las carpetas de los audiolibros.<br>Los subtítulos deben estar separados por \" - \"<br>Por ejemplo: \"Ejemplo de Título - Subtítulo Aquí\" tiene el subtítulo \"Subtítulo Aquí\"",
"LabelSettingsPreferMatchedMetadata": "Preferir metadatos encontrados",
@@ -482,10 +513,14 @@
"LabelTagsAccessibleToUser": "Etiquetas Accessibles al Usuario",
"LabelTagsNotAccessibleToUser": "Etiquetas no Accesibles al Usuario",
"LabelTasks": "Tareas Corriendo",
"LabelTextEditorBulletedList": "Lista con viñetas",
"LabelTextEditorLink": "Enlazar",
"LabelTextEditorNumberedList": "Lista numerada",
"LabelTextEditorUnlink": "Desenlazar",
"LabelTheme": "Tema",
"LabelThemeDark": "Oscuro",
"LabelThemeLight": "Claro",
"LabelTimeBase": "Time Base",
"LabelTimeBase": "Tiempo Base",
"LabelTimeListened": "Tiempo Escuchando",
"LabelTimeListenedToday": "Tiempo Escuchando Hoy",
"LabelTimeRemaining": "{0} restante",
@@ -507,6 +542,7 @@
"LabelTracksSingleTrack": "Una pista",
"LabelType": "Tipo",
"LabelUnabridged": "No Abreviado",
"LabelUndo": "Deshacer",
"LabelUnknown": "Desconocido",
"LabelUpdateCover": "Actualizar Portada",
"LabelUpdateCoverHelp": "Permitir sobrescribir las portadas existentes de los libros seleccionados cuando sean encontradas.",
@@ -515,6 +551,7 @@
"LabelUpdateDetailsHelp": "Permitir sobrescribir detalles existentes de los libros seleccionados cuando sean encontrados",
"LabelUploaderDragAndDrop": "Arrastre y suelte archivos o carpetas",
"LabelUploaderDropFiles": "Suelte los Archivos",
"LabelUploaderItemFetchMetadataHelp": "Buscar título, autor y series automáticamente",
"LabelUseChapterTrack": "Usar pista por capitulo",
"LabelUseFullTrack": "Usar pista completa",
"LabelUser": "Usuario",
@@ -526,6 +563,8 @@
"LabelViewQueue": "Ver Fila del Reproductor",
"LabelVolume": "Volumen",
"LabelWeekdaysToRun": "Correr en Días de la Semana",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Duración de tu Audiolibro",
"LabelYourBookmarks": "Tus Marcadores",
"LabelYourPlaylists": "Tus Listas",
@@ -548,20 +587,21 @@
"MessageConfirmDeleteBackup": "¿Está seguro de que desea eliminar el respaldo {0}?",
"MessageConfirmDeleteFile": "Esto eliminará el archivo de su sistema de archivos. ¿Está seguro?",
"MessageConfirmDeleteLibrary": "¿Está seguro de que desea eliminar permanentemente la biblioteca \"{0}\"?",
"MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?",
"MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?",
"MessageConfirmDeleteLibraryItem": "Esto removerá la librería de la base de datos y archivos en tu sistema. ¿Estás seguro?",
"MessageConfirmDeleteLibraryItems": "Esto removerá {0} elemento(s) de la librería en base de datos y archivos en tu sistema. ¿Estás seguro?",
"MessageConfirmDeleteSession": "¿Está seguro de que desea eliminar esta sesión?",
"MessageConfirmForceReScan": "¿Está seguro de que desea forzar un re-escaneo?",
"MessageConfirmMarkAllEpisodesFinished": "¿Está seguro de que desea marcar todos los episodios como terminados?",
"MessageConfirmMarkAllEpisodesNotFinished": "¿Está seguro de que desea marcar todos los episodios como no terminados?",
"MessageConfirmMarkSeriesFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como terminados?",
"MessageConfirmMarkSeriesNotFinished": "¿Está seguro de que desea marcar todos los libros en esta serie como no terminados?",
"MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?",
"MessageConfirmQuickEmbed": "¡Advertencia! La integración rápida no realiza copias de seguridad a ninguno de tus archivos de audio. Asegúrate de haber realizado una copia de los mismos previamente. <br><br>¿Deseas continuar?",
"MessageConfirmRemoveAllChapters": "¿Está seguro de que desea remover todos los capitulos?",
"MessageConfirmRemoveAuthor": "¿Está seguro de que desea remover el autor \"{0}\"?",
"MessageConfirmRemoveCollection": "¿Está seguro de que desea remover la colección \"{0}\"?",
"MessageConfirmRemoveEpisode": "¿Está seguro de que desea remover el episodio \"{0}\"?",
"MessageConfirmRemoveEpisodes": "¿Está seguro de que desea remover {0} episodios?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "¿Está seguro de que desea remover el narrador \"{0}\"?",
"MessageConfirmRemovePlaylist": "¿Está seguro de que desea remover la lista de reproducción \"{0}\"?",
"MessageConfirmRenameGenre": "¿Está seguro de que desea renombrar el genero \"{0}\" a \"{1}\" de todos los elementos?",
@@ -570,7 +610,7 @@
"MessageConfirmRenameTag": "¿Está seguro de que desea renombrar la etiqueta \"{0}\" a \"{1}\" de todos los elementos?",
"MessageConfirmRenameTagMergeNote": "Nota: Esta etiqueta ya existe, por lo que se fusionarán.",
"MessageConfirmRenameTagWarning": "Advertencia! Una etiqueta similar ya existe \"{0}\".",
"MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?",
"MessageConfirmReScanLibraryItems": "¿Estás seguro de querer re escanear {0} elemento(s)?",
"MessageConfirmSendEbookToDevice": "¿Está seguro de que enviar {0} ebook(s) \"{1}\" al dispositivo \"{2}\"?",
"MessageDownloadingEpisode": "Descargando Capitulo",
"MessageDragFilesIntoTrackOrder": "Arrastra los archivos al orden correcto de las pistas.",
@@ -641,6 +681,7 @@
"MessageRestoreBackupConfirm": "¿Está seguro de que desea para restaurar del respaldo creado en",
"MessageRestoreBackupWarning": "Restaurar sobrescribirá toda la base de datos localizada en /config y las imágenes de portadas en /metadata/items y /metadata/authors.<br /><br />El respaldo no modifica ningún archivo en las carpetas de su biblioteca. Si ha habilitado la opción del servidor para almacenar portadas y metadata en las carpetas de su biblioteca, esos archivos no se respaldan o sobrescriben.<br /><br />Todos los clientes que usen su servidor se actualizarán automáticamente.",
"MessageSearchResultsFor": "Resultados de la búsqueda de",
"MessageSelected": "{0} seleccionado(s)",
"MessageServerCouldNotBeReached": "No se pudo establecer la conexión con el servidor",
"MessageSetChaptersFromTracksDescription": "Establecer capítulos usando cada archivo de audio como un capítulo y el título del capítulo como el nombre del archivo de audio",
"MessageStartPlaybackAtTime": "Iniciar reproducción para \"{0}\" en {1}?",
@@ -656,7 +697,6 @@
"NoteChangeRootPassword": "El usuario Root es el único usuario que puede no tener una contraseña",
"NoteChapterEditorTimes": "Nota: El tiempo de inicio del primer capítulo debe permanecer en 0:00, y el tiempo de inicio del último capítulo no puede exceder la duración del audiolibro.",
"NoteFolderPicker": "Nota: Las carpetas ya asignadas no se mostrarán",
"NoteFolderPickerDebian": "Nota: El selector de archivos no está completamente implementado para instalaciones en Debian. Deberá ingresar la ruta de la carpeta de su biblioteca directamente.",
"NoteRSSFeedPodcastAppsHttps": "Advertencia: La mayoría de las aplicaciones de podcast requieren que la URL de la fuente RSS use HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Advertencia: 1 o más de sus episodios no tienen fecha de publicación. Algunas aplicaciones de podcast lo requieren.",
"NoteUploaderFoldersWithMediaFiles": "Las carpetas con archivos multimedia se manejarán como elementos separados en la biblioteca.",

781
client/strings/et.json Normal file
View File

@@ -0,0 +1,781 @@
{
"ButtonAdd": "Lisa",
"ButtonAddChapters": "Lisa peatükid",
"ButtonAddDevice": "Lisa seade",
"ButtonAddLibrary": "Lisa raamatukogu",
"ButtonAddPodcasts": "Lisa podcastid",
"ButtonAddUser": "Lisa kasutaja",
"ButtonAddYourFirstLibrary": "Lisa oma esimene raamatukogu",
"ButtonApply": "Rakenda",
"ButtonApplyChapters": "Rakenda peatükid",
"ButtonAuthors": "Autorid",
"ButtonBrowseForFolder": "Sirvi kausta",
"ButtonCancel": "Tühista",
"ButtonCancelEncode": "Tühista kodeerimine",
"ButtonChangeRootPassword": "Muuda põhiparooli",
"ButtonCheckAndDownloadNewEpisodes": "Kontrolli ja laadi alla uued episoodid",
"ButtonChooseAFolder": "Vali kaust",
"ButtonChooseFiles": "Vali failid",
"ButtonClearFilter": "Tühista filter",
"ButtonCloseFeed": "Sulge voog",
"ButtonCollections": "Kogud",
"ButtonConfigureScanner": "Konfigureeri skanner",
"ButtonCreate": "Loo",
"ButtonCreateBackup": "Loo varundus",
"ButtonDelete": "Kustuta",
"ButtonDownloadQueue": "Järjekord",
"ButtonEdit": "Muuda",
"ButtonEditChapters": "Muuda peatükke",
"ButtonEditPodcast": "Muuda podcasti",
"ButtonForceReScan": "Sunnitud uuestiskaneerimine",
"ButtonFullPath": "Täielik asukoht",
"ButtonHide": "Peida",
"ButtonHome": "Avaleht",
"ButtonIssues": "Probleemid",
"ButtonJumpBackward": "Hüppa tagasi",
"ButtonJumpForward": "Hüppa edasi",
"ButtonLatest": "Uusim",
"ButtonLibrary": "Raamatukogu",
"ButtonLogout": "Logi välja",
"ButtonLookup": "Otsi",
"ButtonManageTracks": "Halda lugusid",
"ButtonMapChapterTitles": "Kaardista peatükkide pealkirjad",
"ButtonMatchAllAuthors": "Sobita kõik autorid",
"ButtonMatchBooks": "Sobita raamatud",
"ButtonNevermind": "Pole tähtis",
"ButtonNext": "Next",
"ButtonNextChapter": "Järgmine peatükk",
"ButtonOk": "Ok",
"ButtonOpenFeed": "Ava voog",
"ButtonOpenManager": "Ava haldur",
"ButtonPause": "Peata",
"ButtonPlay": "Mängi",
"ButtonPlaying": "Mängib",
"ButtonPlaylists": "Esitusloendid",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Eelmine peatükk",
"ButtonPurgeAllCache": "Tühjenda kogu vahemälu",
"ButtonPurgeItemsCache": "Tühjenda esemete vahemälu",
"ButtonPurgeMediaProgress": "Tühjenda meedia edenemine",
"ButtonQueueAddItem": "Lisa järjekorda",
"ButtonQueueRemoveItem": "Eemalda järjekorrast",
"ButtonQuickMatch": "Kiire sobitamine",
"ButtonRead": "Loe",
"ButtonRefresh": "Refresh",
"ButtonRemove": "Eemalda",
"ButtonRemoveAll": "Eemalda kõik",
"ButtonRemoveAllLibraryItems": "Eemalda kõik raamatukogu esemed",
"ButtonRemoveFromContinueListening": "Eemalda jätkake kuulamisest",
"ButtonRemoveFromContinueReading": "Eemalda jätkake lugemisest",
"ButtonRemoveSeriesFromContinueSeries": "Eemalda seeria jätkamisest",
"ButtonReScan": "Uuestiskaneeri",
"ButtonReset": "Lähtesta",
"ButtonResetToDefault": "Lähtesta vaikeseade",
"ButtonRestore": "Taasta",
"ButtonSave": "Salvesta",
"ButtonSaveAndClose": "Salvesta ja sulge",
"ButtonSaveTracklist": "Salvesta lugude loend",
"ButtonScan": "Skanneeri",
"ButtonScanLibrary": "Skanneeri raamatukogu",
"ButtonSearch": "Otsi",
"ButtonSelectFolderPath": "Vali kaustatee",
"ButtonSeries": "Sarjad",
"ButtonSetChaptersFromTracks": "Määra peatükid lugudest",
"ButtonShare": "Share",
"ButtonShiftTimes": "Nihke ajad",
"ButtonShow": "Näita",
"ButtonStartM4BEncode": "Alusta M4B kodeerimist",
"ButtonStartMetadataEmbed": "Alusta metaandmete lisamist",
"ButtonSubmit": "Esita",
"ButtonTest": "Test",
"ButtonUpload": "Lae üles",
"ButtonUploadBackup": "Lae üles varundus",
"ButtonUploadCover": "Lae üles ümbris",
"ButtonUploadOPMLFile": "Lae üles OPML-fail",
"ButtonUserDelete": "Kustuta kasutaja {0}",
"ButtonUserEdit": "Muuda kasutajat {0}",
"ButtonViewAll": "Vaata kõiki",
"ButtonYes": "Jah",
"ErrorUploadFetchMetadataAPI": "Viga metaandmete hankimisel",
"ErrorUploadFetchMetadataNoResults": "Ei saanud metaandmeid hankida - proovi tiitlit ja/või autorit uuendada",
"ErrorUploadLacksTitle": "Peab olema pealkiri",
"HeaderAccount": "Konto",
"HeaderAdvanced": "Täpsem",
"HeaderAppriseNotificationSettings": "Apprise teavitamise seaded",
"HeaderAudiobookTools": "Heliraamatu failihaldustööriistad",
"HeaderAudioTracks": "Helirajad",
"HeaderAuthentication": "Autentimine",
"HeaderBackups": "Varukoopiad",
"HeaderChangePassword": "Muuda parooli",
"HeaderChapters": "Peatükid",
"HeaderChooseAFolder": "Vali kaust",
"HeaderCollection": "Kogu",
"HeaderCollectionItems": "Kogu esemed",
"HeaderCover": "Ümbris",
"HeaderCurrentDownloads": "Praegused allalaadimised",
"HeaderCustomMetadataProviders": "Kohandatud metaandmete pakkujad",
"HeaderDetails": "Detailid",
"HeaderDownloadQueue": "Allalaadimise järjekord",
"HeaderEbookFiles": "E-raamatute failid",
"HeaderEmail": "E-post",
"HeaderEmailSettings": "E-posti seaded",
"HeaderEpisodes": "Episoodid",
"HeaderEreaderDevices": "E-lugerite seadmed",
"HeaderEreaderSettings": "E-lugerite seadistused",
"HeaderFiles": "Failid",
"HeaderFindChapters": "Leia peatükid",
"HeaderIgnoredFiles": "Ignoreeritud failid",
"HeaderItemFiles": "Esemete failid",
"HeaderItemMetadataUtils": "Eseme metaandmete tööriistad",
"HeaderLastListeningSession": "Viimane kuulamissessioon",
"HeaderLatestEpisodes": "Viimased episoodid",
"HeaderLibraries": "Raamatukogud",
"HeaderLibraryFiles": "Raamatukogu failid",
"HeaderLibraryStats": "Raamatukogu statistika",
"HeaderListeningSessions": "Kuulamissessioonid",
"HeaderListeningStats": "Kuulamise statistika",
"HeaderLogin": "Logi sisse",
"HeaderLogs": "Logid",
"HeaderManageGenres": "Halda žanre",
"HeaderManageTags": "Halda silte",
"HeaderMapDetails": "Kaardi detailid",
"HeaderMatch": "Sobita",
"HeaderMetadataOrderOfPrecedence": "Metaandmete eelnevusjärjestus",
"HeaderMetadataToEmbed": "Manusta metaandmed",
"HeaderNewAccount": "Uus konto",
"HeaderNewLibrary": "Uus raamatukogu",
"HeaderNotifications": "Teatised",
"HeaderOpenIDConnectAuthentication": "OpenID Connect autentimine",
"HeaderOpenRSSFeed": "Ava RSS-voog",
"HeaderOtherFiles": "Muud failid",
"HeaderPasswordAuthentication": "Parooli autentimine",
"HeaderPermissions": "Õigused",
"HeaderPlayerQueue": "Mängija järjekord",
"HeaderPlaylist": "Mänguloend",
"HeaderPlaylistItems": "Mänguloendi esemed",
"HeaderPodcastsToAdd": "Lisatavad podcastid",
"HeaderPreviewCover": "Eelvaate kaas",
"HeaderRemoveEpisode": "Eemalda episood",
"HeaderRemoveEpisodes": "Eemalda {0} episoodi",
"HeaderRSSFeedGeneral": "RSS-i üksikasjad",
"HeaderRSSFeedIsOpen": "RSS-voog on avatud",
"HeaderRSSFeeds": "RSS-vooged",
"HeaderSavedMediaProgress": "Salvestatud meedia edenemine",
"HeaderSchedule": "Ajakava",
"HeaderScheduleLibraryScans": "Ajasta automaatsed raamatukogu skaneerimised",
"HeaderSession": "Sessioon",
"HeaderSetBackupSchedule": "Määra varunduse ajakava",
"HeaderSettings": "Seaded",
"HeaderSettingsDisplay": "Kuva",
"HeaderSettingsExperimental": "Katsetusfunktsioonid",
"HeaderSettingsGeneral": "Üldised",
"HeaderSettingsScanner": "Skanner",
"HeaderSleepTimer": "Uinaku taimer",
"HeaderStatsLargestItems": "Suurimad esemed",
"HeaderStatsLongestItems": "Kõige pikemad esemed (tunnid)",
"HeaderStatsMinutesListeningChart": "Kuulamise minutid (viimased 7 päeva)",
"HeaderStatsRecentSessions": "Hiljutised sessioonid",
"HeaderStatsTop10Authors": "Top 10 autorit",
"HeaderStatsTop5Genres": "Top 5 žanrit",
"HeaderTableOfContents": "Sisukord",
"HeaderTools": "Tööriistad",
"HeaderUpdateAccount": "Uuenda kontot",
"HeaderUpdateAuthor": "Uuenda autorit",
"HeaderUpdateDetails": "Uuenda detaile",
"HeaderUpdateLibrary": "Uuenda raamatukogu",
"HeaderUsers": "Kasutajad",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Sinu statistika",
"LabelAbridged": "Kärbitud",
"LabelAccountType": "Konto tüüp",
"LabelAccountTypeAdmin": "Administraator",
"LabelAccountTypeGuest": "Külaline",
"LabelAccountTypeUser": "Kasutaja",
"LabelActivity": "Tegevus",
"LabelAdded": "Lisatud",
"LabelAddedAt": "Lisatud",
"LabelAddToCollection": "Lisa kogusse",
"LabelAddToCollectionBatch": "Lisa {0} raamatut kogusse",
"LabelAddToPlaylist": "Lisa mänguloendisse",
"LabelAddToPlaylistBatch": "Lisa {0} eset mänguloendisse",
"LabelAdminUsersOnly": "Ainult administraatorid",
"LabelAll": "Kõik",
"LabelAllUsers": "Kõik kasutajad",
"LabelAllUsersExcludingGuests": "Kõik kasutajad, välja arvatud külalised",
"LabelAllUsersIncludingGuests": "Kõik kasutajad, kaasa arvatud külalised",
"LabelAlreadyInYourLibrary": "Juba teie raamatukogus",
"LabelAppend": "Lisa",
"LabelAuthor": "Autor",
"LabelAuthorFirstLast": "Autor (Eesnimi Perekonnanimi)",
"LabelAuthorLastFirst": "Autor (Perekonnanimi, Eesnimi)",
"LabelAuthors": "Autorid",
"LabelAutoDownloadEpisodes": "Automaatne episoodide allalaadimine",
"LabelAutoFetchMetadata": "Automaatne metaandmete hankimine",
"LabelAutoFetchMetadataHelp": "Toob tiitli, autori ja seeria metaandmed üleslaadimise hõlbustamiseks. Lisametaandmed võivad pärast üleslaadimist vajada vastavust.",
"LabelAutoLaunch": "Automaatne käivitamine",
"LabelAutoLaunchDescription": "Suunab automaatselt autentimist pakkuvale teenusele, kui navigeeritakse sisselogimislehele (käsitsi ülekirjutuse tee <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Automaatne registreerimine",
"LabelAutoRegisterDescription": "Loo uued kasutajad automaatselt sisselogimisel",
"LabelBackToUser": "Tagasi kasutajale",
"LabelBackupLocation": "Varukoopia asukoht",
"LabelBackupsEnableAutomaticBackups": "Luba automaatsed varukoopiad",
"LabelBackupsEnableAutomaticBackupsHelp": "Varukoopiad salvestatakse /metadata/backups kausta",
"LabelBackupsMaxBackupSize": "Maksimaalne varukoopia suurus (GB-des)",
"LabelBackupsMaxBackupSizeHelp": "Kaitsena valesti seadistamise vastu ebaõnnestuvad varukoopiad, kui need ületavad seadistatud suuruse.",
"LabelBackupsNumberToKeep": "Varukoopiate arv, mida hoida",
"LabelBackupsNumberToKeepHelp": "Ühel ajal eemaldatakse ainult 1 varukoopia, seega kui teil on juba rohkem varukoopiaid kui siin määratud, peaksite need käsitsi eemaldama.",
"LabelBitrate": "Bittkiirus",
"LabelBooks": "Raamatud",
"LabelButtonText": "Nupu tekst",
"LabelChangePassword": "Muuda parooli",
"LabelChannels": "Kanalid",
"LabelChapters": "Peatükid",
"LabelChaptersFound": "peatükid leitud",
"LabelChapterTitle": "Peatüki pealkiri",
"LabelClickForMoreInfo": "Klõpsa lisateabe saamiseks",
"LabelClosePlayer": "Sulge mängija",
"LabelCodec": "Kodek",
"LabelCollapseSeries": "Ahenda seeria",
"LabelCollection": "Kogu",
"LabelCollections": "Kogud",
"LabelComplete": "Valmis",
"LabelConfirmPassword": "Kinnita parool",
"LabelContinueListening": "Jätka kuulamist",
"LabelContinueReading": "Jätka lugemist",
"LabelContinueSeries": "Jätka seeriat",
"LabelCover": "Ümbris",
"LabelCoverImageURL": "Ümbrise pildi URL",
"LabelCreatedAt": "Loodud",
"LabelCronExpression": "Croni valem",
"LabelCurrent": "Praegune",
"LabelCurrently": "Praegu:",
"LabelCustomCronExpression": "Kohandatud Croni valem:",
"LabelDatetime": "Kuupäev ja kellaaeg",
"LabelDeleteFromFileSystemCheckbox": "Kustuta failisüsteemist (ärge märkige seda ära, et eemaldada ainult andmebaasist)",
"LabelDescription": "Kirjeldus",
"LabelDeselectAll": "Tühista kõigi valimine",
"LabelDevice": "Seade",
"LabelDeviceInfo": "Seadme info",
"LabelDeviceIsAvailableTo": "Seade on saadaval kasutajale...",
"LabelDirectory": "Kataloog",
"LabelDiscFromFilename": "Ketas failinimest",
"LabelDiscFromMetadata": "Ketas metaandmetest",
"LabelDiscover": "Avasta",
"LabelDownload": "Lae alla",
"LabelDownloadNEpisodes": "Lae alla {0} episoodi",
"LabelDuration": "Kestus",
"LabelDurationFound": "Leitud kestus:",
"LabelEbook": "E-raamat",
"LabelEbooks": "E-raamatud",
"LabelEdit": "Muuda",
"LabelEmail": "E-post",
"LabelEmailSettingsFromAddress": "Saatja aadress",
"LabelEmailSettingsSecure": "Turvaline",
"LabelEmailSettingsSecureHelp": "Kui see on tõene, kasutab ühendus serveriga ühenduse loomisel TLS-i. Kui see on väär, kasutatakse TLS-i, kui server toetab STARTTLS-i laiendust. Enamikul juhtudest seadke see väärtus tõeks, kui ühendate pordile 465. Pordi 587 või 25 korral hoidke seda väär. (nodemailer.com/smtp/#authentication)",
"LabelEmailSettingsTestAddress": "Testi aadress",
"LabelEmbeddedCover": "Manustatud kaas",
"LabelEnable": "Luba",
"LabelEnd": "Lõpp",
"LabelEpisode": "Episood",
"LabelEpisodeTitle": "Episoodi pealkiri",
"LabelEpisodeType": "Episoodi tüüp",
"LabelExample": "Näide",
"LabelExplicit": "Vulgaarne",
"LabelFeedURL": "Voogu URL",
"LabelFetchingMetadata": "Metaandmete hankimine",
"LabelFile": "Fail",
"LabelFileBirthtime": "Faili sünniaeg",
"LabelFileModified": "Faili muudetud",
"LabelFilename": "Failinimi",
"LabelFilterByUser": "Filtri alusel kasutaja järgi",
"LabelFindEpisodes": "Otsi episoodid",
"LabelFinished": "Lõpetatud",
"LabelFolder": "Kaust",
"LabelFolders": "Kataloogid",
"LabelFontBold": "Paks",
"LabelFontFamily": "Fondi pere",
"LabelFontItalic": "Kaldkiri",
"LabelFontScale": "Fondi suurus",
"LabelFontStrikethrough": "Üle joonitud",
"LabelFormat": "Vorming",
"LabelGenre": "Žanr",
"LabelGenres": "Žanrid",
"LabelHardDeleteFile": "Faili lõplik kustutamine",
"LabelHasEbook": "On e-raamat",
"LabelHasSupplementaryEbook": "On täiendav e-raamat",
"LabelHighestPriority": "Kõrgeim prioriteet",
"LabelHost": "Host",
"LabelHour": "Tund",
"LabelIcon": "Ikoon",
"LabelImageURLFromTheWeb": "Pildi URL veebist",
"LabelIncludeInTracklist": "Kaasa jälgimisloendis",
"LabelIncomplete": "Puudulik",
"LabelInProgress": "Pooleli",
"LabelInterval": "Intervall",
"LabelIntervalCustomDailyWeekly": "Kohandatud päevane/nädalane",
"LabelIntervalEvery12Hours": "Iga 12 tunni tagant",
"LabelIntervalEvery15Minutes": "Iga 15 minuti tagant",
"LabelIntervalEvery2Hours": "Iga 2 tunni tagant",
"LabelIntervalEvery30Minutes": "Iga 30 minuti tagant",
"LabelIntervalEvery6Hours": "Iga 6 tunni tagant",
"LabelIntervalEveryDay": "Iga päev",
"LabelIntervalEveryHour": "Iga tunni tagant",
"LabelInvalidParts": "Vigased osad",
"LabelInvert": "Pööra ümber",
"LabelItem": "Kirje",
"LabelLanguage": "Keel",
"LabelLanguageDefaultServer": "Vaikeserveri keel",
"LabelLastBookAdded": "Viimati lisatud raamat",
"LabelLastBookUpdated": "Viimati uuendatud raamat",
"LabelLastSeen": "Viimati nähtud",
"LabelLastTime": "Viimati aeg",
"LabelLastUpdate": "Viimane uuendus",
"LabelLayout": "Paigutus",
"LabelLayoutSinglePage": "Üks lehekülg",
"LabelLayoutSplitPage": "Jagatud lehekülg",
"LabelLess": "Vähem",
"LabelLibrariesAccessibleToUser": "Kasutajale ligipääsetavad raamatukogud",
"LabelLibrary": "Raamatukogu",
"LabelLibraryItem": "Raamatukogu kirje",
"LabelLibraryName": "Raamatukogu nimi",
"LabelLimit": "Piirang",
"LabelLineSpacing": "Joonevahe",
"LabelListenAgain": "Kuula uuesti",
"LabelLogLevelDebug": "Silumine",
"LabelLogLevelInfo": "Teave",
"LabelLogLevelWarn": "Hoiatus",
"LabelLookForNewEpisodesAfterDate": "Otsi uusi episoodid pärast seda kuupäeva",
"LabelLowestPriority": "Madalaim prioriteet",
"LabelMatchExistingUsersBy": "Sobita olemasolevad kasutajad",
"LabelMatchExistingUsersByDescription": "Kasutatakse olemasolevate kasutajate ühendamiseks. Ühendatud kasutajaid sobitatakse teie SSO pakkuja unikaalse ID järgi.",
"LabelMediaPlayer": "Meediapleier",
"LabelMediaType": "Meedia tüüp",
"LabelMetadataOrderOfPrecedenceDescription": "Kõrgema prioriteediga metaandmete allikad võtavad üle madalama prioriteediga metaandmete allikad",
"LabelMetadataProvider": "Metaandmete pakkuja",
"LabelMetaTag": "Meta märge",
"LabelMetaTags": "Meta märgendid",
"LabelMinute": "Minut",
"LabelMissing": "Puudub",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Puuduvad osad",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Lubatud mobiilile suunamise URI-d",
"LabelMobileRedirectURIsDescription": "See on mobiilirakenduste jaoks kehtivate suunamise URI-de lubatud nimekiri. Vaikimisi on selleks <code>audiobookshelf://oauth</code>, mida saate eemaldada või täiendada täiendavate URI-dega kolmanda osapoole rakenduste integreerimiseks. Tärni (<code>*</code>) ainukese kirjena kasutamine võimaldab mis tahes URI-d.",
"LabelMore": "Rohkem",
"LabelMoreInfo": "Rohkem infot",
"LabelName": "Nimi",
"LabelNarrator": "Jutustaja",
"LabelNarrators": "Jutustajad",
"LabelNew": "Uus",
"LabelNewestAuthors": "Uusimad autorid",
"LabelNewestEpisodes": "Uusimad episoodid",
"LabelNewPassword": "Uus parool",
"LabelNextBackupDate": "Järgmine varukoopia kuupäev",
"LabelNextScheduledRun": "Järgmine ajakava järgmine",
"LabelNoEpisodesSelected": "Episoodid pole valitud",
"LabelNotes": "Märkused",
"LabelNotFinished": "Ei ole lõpetatud",
"LabelNotificationAppriseURL": "Apprise URL-id",
"LabelNotificationAvailableVariables": "Saadaolevad muutujad",
"LabelNotificationBodyTemplate": "Keha mall",
"LabelNotificationEvent": "Teavituse sündmus",
"LabelNotificationsMaxFailedAttempts": "Maksimaalsed ebaõnnestunud katsed",
"LabelNotificationsMaxFailedAttemptsHelp": "Teatised keelatakse, kui need ebaõnnestuvad nii palju kordi",
"LabelNotificationsMaxQueueSize": "Teavituste sündmuste maksimaalne järjekorra suurus",
"LabelNotificationsMaxQueueSizeHelp": "Sündmused on piiratud 1 sekundiga. Sündmusi ignoreeritakse, kui järjekord on maksimumsuuruses. See takistab teavituste rämpsposti.",
"LabelNotificationTitleTemplate": "Pealkirja mall",
"LabelNotStarted": "Pole alustatud",
"LabelNumberOfBooks": "Raamatute arv",
"LabelNumberOfEpisodes": "Episoodide arv",
"LabelOpenRSSFeed": "Ava RSS voog",
"LabelOverwrite": "Kirjuta üle",
"LabelPassword": "Parool",
"LabelPath": "Asukoht",
"LabelPermissionsAccessAllLibraries": "Saab ligi kõikidele raamatukogudele",
"LabelPermissionsAccessAllTags": "Saab ligi kõikidele siltidele",
"LabelPermissionsAccessExplicitContent": "Saab ligi vulgaarsele sisule",
"LabelPermissionsDelete": "Saab kustutada",
"LabelPermissionsDownload": "Saab alla laadida",
"LabelPermissionsUpdate": "Saab uuendada",
"LabelPermissionsUpload": "Saab üles laadida",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Foto tee/URL",
"LabelPlaylists": "Mänguloendid",
"LabelPlayMethod": "Esitusmeetod",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcastid",
"LabelPodcastSearchRegion": "Podcasti otsingu piirkond",
"LabelPodcastType": "Podcasti tüüp",
"LabelPort": "Port",
"LabelPrefixesToIgnore": "Eiramiseks eesliited (tõstutundetu)",
"LabelPreventIndexing": "Vältige oma voogu indekseerimist iTunes'i ja Google podcasti kataloogides",
"LabelPrimaryEbook": "Esmane e-raamat",
"LabelProgress": "Edenemine",
"LabelProvider": "Pakkuja",
"LabelPubDate": "Avaldamise kuupäev",
"LabelPublisher": "Kirjastaja",
"LabelPublishYear": "Aasta avaldamine",
"LabelRead": "Lugenud",
"LabelReadAgain": "Loe uuesti",
"LabelReadEbookWithoutProgress": "Lugege e-raamatut ilma edenemist säilitamata",
"LabelRecentlyAdded": "Hiljuti lisatud",
"LabelRecentSeries": "Hiljutised seeriad",
"LabelRecommended": "Soovitatud",
"LabelRedo": "Tee uuesti",
"LabelRegion": "Piirkond",
"LabelReleaseDate": "Väljalaske kuupäev",
"LabelRemoveCover": "Eemalda ümbris",
"LabelRowsPerPage": "Rida lehe kohta",
"LabelRSSFeedCustomOwnerEmail": "Kohandatud omaniku e-post",
"LabelRSSFeedCustomOwnerName": "Kohandatud omaniku nimi",
"LabelRSSFeedOpen": "Ava RSS voog",
"LabelRSSFeedPreventIndexing": "Vältige indekseerimist",
"LabelRSSFeedSlug": "RSS voog Slug",
"LabelRSSFeedURL": "RSS voog URL",
"LabelSearchTerm": "Otsingutermin",
"LabelSearchTitle": "Otsi pealkirja",
"LabelSearchTitleOrASIN": "Otsi pealkirja või ASIN-i",
"LabelSeason": "Hooaeg",
"LabelSelectAllEpisodes": "Vali kõik episoodid",
"LabelSelectEpisodesShowing": "Valige {0} näidatavat episoodi",
"LabelSelectUsers": "Valige kasutajad",
"LabelSendEbookToDevice": "Saada e-raamat seadmele...",
"LabelSequence": "Järjestus",
"LabelSeries": "Seeria",
"LabelSeriesName": "Seeria nimi",
"LabelSeriesProgress": "Seeria edenemine",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Määra peamiseks",
"LabelSetEbookAsSupplementary": "Määra täiendavaks",
"LabelSettingsAudiobooksOnly": "Ainult heliraamatud",
"LabelSettingsAudiobooksOnlyHelp": "Selle seadistuse lubamine eirab e-raamatute faile, välja arvatud juhul, kui need on heliraamatu kaustas, kus need seatakse täiendavate e-raamatutena",
"LabelSettingsBookshelfViewHelp": "Skeumorfne kujundus puidust riiulitega",
"LabelSettingsChromecastSupport": "Chromecasti tugi",
"LabelSettingsDateFormat": "Kuupäeva vorming",
"LabelSettingsDisableWatcher": "Keela vaatamine",
"LabelSettingsDisableWatcherForLibrary": "Keela kaustavaatamine raamatukogu jaoks",
"LabelSettingsDisableWatcherHelp": "Keelab automaatse lisamise/uuendamise, kui failimuudatusi tuvastatakse. *Nõuab serveri taaskäivitamist",
"LabelSettingsEnableWatcher": "Luba vaatamine",
"LabelSettingsEnableWatcherForLibrary": "Luba kaustavaatamine raamatukogu jaoks",
"LabelSettingsEnableWatcherHelp": "Lubab automaatset lisamist/uuendamist, kui tuvastatakse failimuudatused. *Nõuab serveri taaskäivitamist",
"LabelSettingsExperimentalFeatures": "Eksperimentaalsed funktsioonid",
"LabelSettingsExperimentalFeaturesHelp": "Arengus olevad funktsioonid, mis vajavad teie tagasisidet ja abi testimisel. Klõpsake GitHubi arutelu avamiseks.",
"LabelSettingsFindCovers": "Leia ümbrised",
"LabelSettingsFindCoversHelp": "Kui teie heliraamatul pole sisseehitatud ümbrist ega ümbrise pilti kaustas, proovib skanner leida ümbrist.<br>Märkus: see pikendab skaneerimisaega",
"LabelSettingsHideSingleBookSeries": "Peida üksikute raamatute seeriad",
"LabelSettingsHideSingleBookSeriesHelp": "Ühe raamatuga seeriaid peidetakse seeria lehelt ja avalehe riiulitelt.",
"LabelSettingsHomePageBookshelfView": "Avaleht kasutage raamatukoguvaadet",
"LabelSettingsLibraryBookshelfView": "Raamatukogu kasutamiseks kasutage raamatukoguvaadet",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Lugege subtiitreid",
"LabelSettingsParseSubtitlesHelp": "Eraldage subtiitrid heliraamatu kaustade nimedest.<br>Subtiitrid peavad olema eraldatud \" - \".<br>Näiteks: \"Raamatu pealkiri - Siin on alapealkiri\" alapealkiri on \"Siin on alapealkiri\"",
"LabelSettingsPreferMatchedMetadata": "Eelista sobitatud metaandmeid",
"LabelSettingsPreferMatchedMetadataHelp": "Sobitatud andmed kirjutavad Kiir Sobitamise kasutamisel üle üksikasjad.",
"LabelSettingsSkipMatchingBooksWithASIN": "Jätke ASIN-iga sobituvad raamatud vahele",
"LabelSettingsSkipMatchingBooksWithISBN": "Jätke ISBN-iga sobituvad raamatud vahele",
"LabelSettingsSortingIgnorePrefixes": "Ignoreeri eesliiteid sortimisel",
"LabelSettingsSortingIgnorePrefixesHelp": "nt. eesliidet \"the\" kasutades raamatu pealkiri \"The Book Title\" sorteeritakse \"Book Title, The\"",
"LabelSettingsSquareBookCovers": "Kasutage ruudukujulisi raamatu kaasi",
"LabelSettingsSquareBookCoversHelp": "Eelistage ruudukujulisi kaasi tavaliste 1.6:1 raamatu ümbrise asemel",
"LabelSettingsStoreCoversWithItem": "Salvesta kaaned üksusega",
"LabelSettingsStoreCoversWithItemHelp": "Vaikimisi salvestatakse kaaned /metadata/items kausta. Selle seadistuse lubamine salvestab kaaned teie raamatukogu üksuse kausta. Hoitakse ainult ühte faili nimega \"kaas\"",
"LabelSettingsStoreMetadataWithItem": "Salvesta metaandmed üksusega",
"LabelSettingsStoreMetadataWithItemHelp": "Vaikimisi salvestatakse metaandmed /metadata/items kausta. Selle seadistuse lubamine salvestab metaandmed teie raamatukogu üksuse kaustadesse",
"LabelSettingsTimeFormat": "Kellaaja vorming",
"LabelShowAll": "Näita kõiki",
"LabelSize": "Suurus",
"LabelSleepTimer": "Uinaku taimer",
"LabelSlug": "Slug",
"LabelStart": "Alusta",
"LabelStarted": "Alustatud",
"LabelStartedAt": "Alustatud",
"LabelStartTime": "Alustamise aeg",
"LabelStatsAudioTracks": "Audiojäljed",
"LabelStatsAuthors": "Autorid",
"LabelStatsBestDay": "Parim päev",
"LabelStatsDailyAverage": "Päevane keskmine",
"LabelStatsDays": "Päevad",
"LabelStatsDaysListened": "Kuulatud päevad",
"LabelStatsHours": "Tunnid",
"LabelStatsInARow": "järjest",
"LabelStatsItemsFinished": "Lõpetatud üksused",
"LabelStatsItemsInLibrary": "Üksused raamatukogus",
"LabelStatsMinutes": "minutit",
"LabelStatsMinutesListening": "Kuulamise minutid",
"LabelStatsOverallDays": "Kokku päevad",
"LabelStatsOverallHours": "Kokku tunnid",
"LabelStatsWeekListening": "Nädala kuulamine",
"LabelSubtitle": "Alapealkiri",
"LabelSupportedFileTypes": "Toetatud failitüübid",
"LabelTag": "Silt",
"LabelTags": "Sildid",
"LabelTagsAccessibleToUser": "Kasutajale kättesaadavad sildid",
"LabelTagsNotAccessibleToUser": "Kasutajale mittekättesaadavad sildid",
"LabelTasks": "Käimasolevad ülesanded",
"LabelTextEditorBulletedList": "Punktloend",
"LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "Numberloend",
"LabelTextEditorUnlink": "Eemalda link",
"LabelTheme": "Teema",
"LabelThemeDark": "Tume",
"LabelThemeLight": "Hele",
"LabelTimeBase": "Aja alus",
"LabelTimeListened": "Kuulatud aeg",
"LabelTimeListenedToday": "Täna kuulatud aeg",
"LabelTimeRemaining": "{0} jäänud",
"LabelTimeToShift": "Nihutamiseks sekundites kuluv aeg",
"LabelTitle": "Pealkiri",
"LabelToolsEmbedMetadata": "Manusta metaandmed",
"LabelToolsEmbedMetadataDescription": "Manusta metaandmed helifailidesse, sealhulgas kaanepilt ja peatükid.",
"LabelToolsMakeM4b": "Loo M4B heliraamatu fail",
"LabelToolsMakeM4bDescription": "Loo .M4B heliraamatu fail, kuhu on manustatud metaandmed, kaanepilt ja peatükid.",
"LabelToolsSplitM4b": "Jaga M4B MP3-deks",
"LabelToolsSplitM4bDescription": "Loo MP3-d M4B-st peatükkide kaupa, kus on manustatud metaandmed, kaanepilt ja peatükid.",
"LabelTotalDuration": "Kogukestus",
"LabelTotalTimeListened": "Kogu kuulatud aeg",
"LabelTrackFromFilename": "Jälg nimest",
"LabelTrackFromMetadata": "Jälg metaandmetest",
"LabelTracks": "Jäljed",
"LabelTracksMultiTrack": "Mitmejälg",
"LabelTracksNone": "Ühtegi jälgimist",
"LabelTracksSingleTrack": "Üksikjälg",
"LabelType": "Tüüp",
"LabelUnabridged": "Täismahus",
"LabelUndo": "Võta tagasi",
"LabelUnknown": "Tundmatu",
"LabelUpdateCover": "Uuenda kaant",
"LabelUpdateCoverHelp": "Luba üle kirjutamine olemasolevate kaante jaoks valitud raamatutele, kui leitakse sobivus",
"LabelUpdatedAt": "Uuendatud",
"LabelUpdateDetails": "Uuenda üksikasju",
"LabelUpdateDetailsHelp": "Luba üle kirjutamine olemasolevate üksikasjade jaoks valitud raamatutele, kui leitakse sobivus",
"LabelUploaderDragAndDrop": "Lohista ja aseta faile või kaustu",
"LabelUploaderDropFiles": "Aseta failid",
"LabelUploaderItemFetchMetadataHelp": "Hangi automaatselt pealkiri, autor ja seeria",
"LabelUseChapterTrack": "Kasuta peatüki jälge",
"LabelUseFullTrack": "Kasuta täielikku jälge",
"LabelUser": "Kasutaja",
"LabelUsername": "Kasutajanimi",
"LabelValue": "Väärtus",
"LabelVersion": "Versioon",
"LabelViewBookmarks": "Vaata järjehoidjaid",
"LabelViewChapters": "Vaata peatükke",
"LabelViewQueue": "Vaata esitusjärjekorda",
"LabelVolume": "Heli tugevus",
"LabelWeekdaysToRun": "Päevad nädalas käivitamiseks",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Teie heliraamatu kestus",
"LabelYourBookmarks": "Teie järjehoidjad",
"LabelYourPlaylists": "Teie esitusloendid",
"LabelYourProgress": "Teie edenemine",
"MessageAddToPlayerQueue": "Lisa esitusjärjekorda",
"MessageAppriseDescription": "Selle funktsiooni kasutamiseks peate käivitama <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> eksemplari või API, mis töötleb samu päringuid. <br />Apprise API URL peaks olema täielik URL-rada teatise saatmiseks, näiteks kui teie API eksemplar töötab aadressil <code>http://192.168.1.1:8337</code>, siis peaksite sisestama <code>http://192.168.1.1:8337/notify</code>.",
"MessageBackupsDescription": "Varukoopiad hõlmavad kasutajaid, kasutajate edenemist, raamatukogu üksikasju, serveri seadeid ja kaustades <code>/metadata/items</code> ja <code>/metadata/authors</code> salvestatud pilte. Varukoopiad ei hõlma ühtegi teie raamatukogu kaustades olevat faili.",
"MessageBatchQuickMatchDescription": "Kiire sobitamine üritab lisada valitud üksustele puuduvad kaaned ja metaandmed. Luba allpool olevad valikud, et lubada Kiire sobitamine'il üle kirjutada olemasolevaid kaasi ja/või metaandmeid.",
"MessageBookshelfNoCollections": "Te pole veel ühtegi kogumit teinud",
"MessageBookshelfNoResultsForFilter": "Filtrile \"{0}: {1}\" pole tulemusi",
"MessageBookshelfNoRSSFeeds": "Ühtegi RSS-i voogu pole avatud",
"MessageBookshelfNoSeries": "Teil pole ühtegi seeriat",
"MessageChapterEndIsAfter": "Peatüki lõpp on pärast teie heliraamatu lõppu",
"MessageChapterErrorFirstNotZero": "Esimene peatükk peab algama 0-st",
"MessageChapterErrorStartGteDuration": "Vigane algusaeg peab olema väiksem kui heliraamatu kestus",
"MessageChapterErrorStartLtPrev": "Vigane algusaeg peab olema suurem või võrdne eelneva peatüki algusajaga",
"MessageChapterStartIsAfter": "Peatüki algus on pärast teie heliraamatu lõppu",
"MessageCheckingCron": "Croni kontrollimine...",
"MessageConfirmCloseFeed": "Olete kindel, et soovite selle voo sulgeda?",
"MessageConfirmDeleteBackup": "Olete kindel, et soovite varukoopia kustutada {0} kohta?",
"MessageConfirmDeleteFile": "See kustutab faili teie failisüsteemist. Olete kindel?",
"MessageConfirmDeleteLibrary": "Olete kindel, et soovite raamatukogu \"{0}\" lõplikult kustutada?",
"MessageConfirmDeleteLibraryItem": "See kustutab raamatukogu üksuse andmebaasist ja failisüsteemist. Olete kindel?",
"MessageConfirmDeleteLibraryItems": "See kustutab {0} raamatukogu üksust andmebaasist ja failisüsteemist. Olete kindel?",
"MessageConfirmDeleteSession": "Olete kindel, et soovite selle seansi kustutada?",
"MessageConfirmForceReScan": "Olete kindel, et soovite jõuga uuesti skannida?",
"MessageConfirmMarkAllEpisodesFinished": "Olete kindel, et soovite kõik episoodid lõpetatuks märkida?",
"MessageConfirmMarkAllEpisodesNotFinished": "Olete kindel, et soovite kõik episoodid mitte lõpetatuks märkida?",
"MessageConfirmMarkSeriesFinished": "Olete kindel, et soovite selle seeria kõik raamatud lõpetatuks märkida?",
"MessageConfirmMarkSeriesNotFinished": "Olete kindel, et soovite selle seeria kõik raamatud mitte lõpetatuks märkida?",
"MessageConfirmQuickEmbed": "Hoiatus! Quick Embed ei tee varukoopiaid teie helifailidest. Veenduge, et teil oleks varukoopia oma helifailidest. <br><br>Kas soovite jätkata?",
"MessageConfirmRemoveAllChapters": "Olete kindel, et soovite eemaldada kõik peatükid?",
"MessageConfirmRemoveAuthor": "Olete kindel, et soovite autori \"{0}\" eemaldada?",
"MessageConfirmRemoveCollection": "Olete kindel, et soovite kogumi \"{0}\" eemaldada?",
"MessageConfirmRemoveEpisode": "Olete kindel, et soovite episoodi \"{0}\" eemaldada?",
"MessageConfirmRemoveEpisodes": "Olete kindel, et soovite eemaldada {0} episoodi?",
"MessageConfirmRemoveListeningSessions": "Olete kindel, et soovite eemaldada {0} kuulamise sessiooni?",
"MessageConfirmRemoveNarrator": "Olete kindel, et soovite jutustaja \"{0}\" eemaldada?",
"MessageConfirmRemovePlaylist": "Olete kindel, et soovite eemaldada oma esitusloendi \"{0}\"?",
"MessageConfirmRenameGenre": "Olete kindel, et soovite žanri \"{0}\" ümber nimetada kujule \"{1}\" kõikidele üksustele?",
"MessageConfirmRenameGenreMergeNote": "Märkus: See žanr on juba olemas, nii et need ühendatakse.",
"MessageConfirmRenameGenreWarning": "Hoiatus! Sarnane žanr erineva puhvriga on juba olemas \"{0}\".",
"MessageConfirmRenameTag": "Olete kindel, et soovite silti \"{0}\" ümber nimetada kujule \"{1}\" kõikidele üksustele?",
"MessageConfirmRenameTagMergeNote": "Märkus: See silt on juba olemas, nii et need ühendatakse.",
"MessageConfirmRenameTagWarning": "Hoiatus! Sarnane silt erineva puhvriga on juba olemas \"{0}\".",
"MessageConfirmReScanLibraryItems": "Olete kindel, et soovite uuesti skannida {0} üksust?",
"MessageConfirmSendEbookToDevice": "Olete kindel, et soovite saata {0} e-raamatu \"{1}\" seadmesse \"{2}\"?",
"MessageDownloadingEpisode": "Episoodi allalaadimine",
"MessageDragFilesIntoTrackOrder": "Lohistage failid õigesse järjekorda",
"MessageEmbedFinished": "Manustamine lõpetatud!",
"MessageEpisodesQueuedForDownload": "{0} Episood(i) on allalaadimiseks järjekorras",
"MessageFeedURLWillBe": "Toite URL saab olema {0}",
"MessageFetching": "Hangitakse...",
"MessageForceReScanDescription": "skaneerib kõik failid uuesti nagu värsket skannimist. Heli faili ID3 silte, OPF faile ja tekstifaile skaneeritakse uuesti.",
"MessageImportantNotice": "Oluline märkus!",
"MessageInsertChapterBelow": "Sisesta peatükk allapoole",
"MessageItemsSelected": "{0} Valitud üksust",
"MessageItemsUpdated": "{0} Üksust on uuendatud",
"MessageJoinUsOn": "Liitu meiega",
"MessageListeningSessionsInTheLastYear": "Kuulamissessioone viimase aasta jooksul: {0}",
"MessageLoading": "Laadimine...",
"MessageLoadingFolders": "Kaustade laadimine...",
"MessageM4BFailed": "M4B ebaõnnestus!",
"MessageM4BFinished": "M4B lõpetatud!",
"MessageMapChapterTitles": "Kaarda peatükkide pealkirjad olemasolevatele heliraamatu peatükkidele, ajatempe ei muudeta",
"MessageMarkAllEpisodesFinished": "Märgi kõik episoodid lõpetatuks",
"MessageMarkAllEpisodesNotFinished": "Märgi kõik episoodid mitte lõpetatuks",
"MessageMarkAsFinished": "Märgi lõpetatuks",
"MessageMarkAsNotFinished": "Märgi mitte lõpetatuks",
"MessageMatchBooksDescription": "üritab raamatuid raamatukogus sobitada otsingupakkujast leitud raamatuga ning täita tühjad üksikasjad ja kaas. Ei üle kirjuta üksikasju.",
"MessageNoAudioTracks": "Ühtegi helijälge pole",
"MessageNoAuthors": "Ühtegi autori pole",
"MessageNoBackups": "Ühtegi varukoopia pole",
"MessageNoBookmarks": "Ühtegi järjehoidjat pole",
"MessageNoChapters": "Ühtegi peatükki pole",
"MessageNoCollections": "Ühtegi kogumit pole",
"MessageNoCoversFound": "Ühtegi kaant pole leitud",
"MessageNoDescription": "Kirjeldust pole",
"MessageNoDownloadsInProgress": "Praegu allalaadimisi pole",
"MessageNoDownloadsQueued": "Pole järjekorras allalaadimisi",
"MessageNoEpisodeMatchesFound": "Ühtegi episoodi vastet pole leitud",
"MessageNoEpisodes": "Ühtegi episoodi pole",
"MessageNoFoldersAvailable": "Ühtegi kausta pole saadaval",
"MessageNoGenres": "Ühtegi žanrit pole",
"MessageNoIssues": "Ühtegi probleemi pole",
"MessageNoItems": "Ühtegi üksust pole",
"MessageNoItemsFound": "Ühtegi üksust pole leitud",
"MessageNoListeningSessions": "Ühtegi kuulamissessiooni pole",
"MessageNoLogs": "Ühtegi logi pole",
"MessageNoMediaProgress": "Ühtegi meediaprogressi pole",
"MessageNoNotifications": "Ühtegi teavitust pole",
"MessageNoPodcastsFound": "Ühtegi podcasti pole leitud",
"MessageNoResults": "Ühtegi tulemust pole",
"MessageNoSearchResultsFor": "Otsingutulemusi pole märksõna kohta: \"{0}\"",
"MessageNoSeries": "Ühtegi seeriat pole",
"MessageNoTags": "Ühtegi silti pole",
"MessageNoTasksRunning": "Ühtegi käimasolevat ülesannet pole",
"MessageNotYetImplemented": "Pole veel ellu viidud",
"MessageNoUpdateNecessary": "Ühtegi värskendust pole vaja",
"MessageNoUpdatesWereNecessary": "Ühtegi värskendust polnud vaja",
"MessageNoUserPlaylists": "Teil pole ühtegi esitusloendit",
"MessageOr": "või",
"MessagePauseChapter": "Peata peatüki esitamine",
"MessagePlayChapter": "Kuula peatüki algust",
"MessagePlaylistCreateFromCollection": "Loo esitusloend kogumist",
"MessagePodcastHasNoRSSFeedForMatching": "Podcastil pole sobitamiseks RSS-voogu",
"MessageQuickMatchDescription": "täidab tühjad üksikasjad ja kaaned raamatukogus esimese otsingutulemusega rakendusest '{0}'. Ei üle kirjuta üksikasju, välja arvatud juhul, kui serveri sätetes on lubatud 'Eelista sobitatud metaandmeid'.",
"MessageRemoveChapter": "Eemalda peatükk",
"MessageRemoveEpisodes": "Eemalda {0} episood(i)",
"MessageRemoveFromPlayerQueue": "Eemalda esitusjärjekorrast",
"MessageRemoveUserWarning": "Olete kindel, et soovite kasutaja \"{0}\" lõplikult kustutada?",
"MessageReportBugsAndContribute": "Raporteeri vigu, palu funktsioone ja aita kaasa",
"MessageResetChaptersConfirm": "Olete kindel, et soovite peatükkide lähtestada ja tehtud muudatused tagasi võtta?",
"MessageRestoreBackupConfirm": "Olete kindel, et soovite taastada varukoopia, mis loodi",
"MessageRestoreBackupWarning": "Varukoopia taastamine kirjutab üle kogu /config ja /metadata/items & /metadata/authors kaustas oleva andmebaasi. <br /><br />Varukoopiad ei muuda teie raamatukogukaustades olevaid faile. Kui olete lubanud serveri sätetel salvestada kaane kunsti ja metaandmed teie raamatukogu kaustadesse, siis neid ei varundata ega kirjutata üle.<br /><br />Kõik teie serveri kasutavad kliendid värskendatakse automaatselt.",
"MessageSearchResultsFor": "Otsingutulemused märksõnale",
"MessageSelected": "{0} valitud",
"MessageServerCouldNotBeReached": "Serveriga ei saanud ühendust luua",
"MessageSetChaptersFromTracksDescription": "Määrake peatükid, kasutades iga helifaili peatükina ja peatüki pealkirjana helifaili nime",
"MessageStartPlaybackAtTime": "Alustage \"{0}\" esitamist kell {1}?",
"MessageThinking": "Mõtlen...",
"MessageUploaderItemFailed": "Üleslaadimine ebaõnnestus",
"MessageUploaderItemSuccess": "Edukalt üles laaditud!",
"MessageUploading": "Üles laadimine...",
"MessageValidCronExpression": "Kehtiv cron-väljend",
"MessageWatcherIsDisabledGlobally": "Vaatleja on ülemaailmselt keelatud serveri sätetes",
"MessageXLibraryIsEmpty": "{0} raamatukogu on tühi!",
"MessageYourAudiobookDurationIsLonger": "Teie heliraamatu kestus on pikem kui leitud kestus",
"MessageYourAudiobookDurationIsShorter": "Teie heliraamatu kestus on lühem kui leitud kestus",
"NoteChangeRootPassword": "Root kasutajal võib olla ainus kasutaja, kellel võib olla tühi parool",
"NoteChapterEditorTimes": "Märkus: Esimese peatüki algusaeg peab jääma 0:00 ja viimase peatüki algusaeg ei tohi ületada selle heliraamatu kestust.",
"NoteFolderPicker": "Märkus: juba kaardistatud kaustu ei kuvata",
"NoteRSSFeedPodcastAppsHttps": "Hoiatus: Enamik podcasti rakendusi nõuab, et RSS-voogu URL kasutaks HTTPS-i",
"NoteRSSFeedPodcastAppsPubDate": "Hoiatus: Üks või mitu teie episoodi ei sisalda publikatsioonikuupäeva. Mõned podcasti rakendused nõuavad seda.",
"NoteUploaderFoldersWithMediaFiles": "Kaustu, kus on meediat, käsitletakse eraldi raamatukogu üksustena.",
"NoteUploaderOnlyAudioFiles": "Kui laadite üles ainult helifaile, käsitletakse iga helifaili eraldi heliraamatuna.",
"NoteUploaderUnsupportedFiles": "Toetamata failid jäetakse tähelepanuta. Kausta valimisel või lohistamisel jäetakse tähelepanuta muud failid, mis pole üksuse kaustas.",
"PlaceholderNewCollection": "Uue kogumi nimi",
"PlaceholderNewFolderPath": "Uus kausta tee",
"PlaceholderNewPlaylist": "Uue esitusloendi nimi",
"PlaceholderSearch": "Otsi...",
"PlaceholderSearchEpisode": "Otsi episoodi...",
"ToastAccountUpdateFailed": "Konto värskendamine ebaõnnestus",
"ToastAccountUpdateSuccess": "Konto on värskendatud",
"ToastAuthorImageRemoveFailed": "Pildi eemaldamine ebaõnnestus",
"ToastAuthorImageRemoveSuccess": "Autori pilt on eemaldatud",
"ToastAuthorUpdateFailed": "Autori värskendamine ebaõnnestus",
"ToastAuthorUpdateMerged": "Autor liidetud",
"ToastAuthorUpdateSuccess": "Autor värskendatud",
"ToastAuthorUpdateSuccessNoImageFound": "Autor värskendatud (pilti ei leitud)",
"ToastBackupCreateFailed": "Varukoopia loomine ebaõnnestus",
"ToastBackupCreateSuccess": "Varukoopia loodud",
"ToastBackupDeleteFailed": "Varukoopia kustutamine ebaõnnestus",
"ToastBackupDeleteSuccess": "Varukoopia kustutatud",
"ToastBackupRestoreFailed": "Varukoopia taastamine ebaõnnestus",
"ToastBackupUploadFailed": "Varukoopia üles laadimine ebaõnnestus",
"ToastBackupUploadSuccess": "Varukoopia üles laaditud",
"ToastBatchUpdateFailed": "Partii värskendamine ebaõnnestus",
"ToastBatchUpdateSuccess": "Partii värskendamine õnnestus",
"ToastBookmarkCreateFailed": "Järjehoidja loomine ebaõnnestus",
"ToastBookmarkCreateSuccess": "Järjehoidja lisatud",
"ToastBookmarkRemoveFailed": "Järjehoidja eemaldamine ebaõnnestus",
"ToastBookmarkRemoveSuccess": "Järjehoidja eemaldatud",
"ToastBookmarkUpdateFailed": "Järjehoidja värskendamine ebaõnnestus",
"ToastBookmarkUpdateSuccess": "Järjehoidja värskendatud",
"ToastChaptersHaveErrors": "Peatükkidel on vigu",
"ToastChaptersMustHaveTitles": "Peatükkidel peab olema pealkiri",
"ToastCollectionItemsRemoveFailed": "Üksuse(te) eemaldamine kogumist ebaõnnestus",
"ToastCollectionItemsRemoveSuccess": "Üksus(ed) eemaldatud kogumist",
"ToastCollectionRemoveFailed": "Kogumi eemaldamine ebaõnnestus",
"ToastCollectionRemoveSuccess": "Kogum eemaldatud",
"ToastCollectionUpdateFailed": "Kogumi värskendamine ebaõnnestus",
"ToastCollectionUpdateSuccess": "Kogum värskendatud",
"ToastItemCoverUpdateFailed": "Üksuse kaane värskendamine ebaõnnestus",
"ToastItemCoverUpdateSuccess": "Üksuse kaas värskendatud",
"ToastItemDetailsUpdateFailed": "Üksuse üksikasjade värskendamine ebaõnnestus",
"ToastItemDetailsUpdateSuccess": "Üksuse üksikasjad värskendatud",
"ToastItemDetailsUpdateUnneeded": "Üksuse üksikasjade värskendamine pole vajalik",
"ToastItemMarkedAsFinishedFailed": "Märgistamine kui lõpetatud ebaõnnestus",
"ToastItemMarkedAsFinishedSuccess": "Üksus märgitud kui lõpetatud",
"ToastItemMarkedAsNotFinishedFailed": "Märgistamine kui mitte lõpetatud ebaõnnestus",
"ToastItemMarkedAsNotFinishedSuccess": "Üksus märgitud kui mitte lõpetatud",
"ToastLibraryCreateFailed": "Raamatukogu loomine ebaõnnestus",
"ToastLibraryCreateSuccess": "Raamatukogu \"{0}\" loodud",
"ToastLibraryDeleteFailed": "Raamatukogu kustutamine ebaõnnestus",
"ToastLibraryDeleteSuccess": "Raamatukogu kustutatud",
"ToastLibraryScanFailedToStart": "Skanneerimine ei käivitunud",
"ToastLibraryScanStarted": "Raamatukogu skaneerimine alustatud",
"ToastLibraryUpdateFailed": "Raamatukogu värskendamine ebaõnnestus",
"ToastLibraryUpdateSuccess": "Raamatukogu \"{0}\" värskendatud",
"ToastPlaylistCreateFailed": "Esitusloendi loomine ebaõnnestus",
"ToastPlaylistCreateSuccess": "Esitusloend loodud",
"ToastPlaylistRemoveFailed": "Esitusloendi eemaldamine ebaõnnestus",
"ToastPlaylistRemoveSuccess": "Esitusloend eemaldatud",
"ToastPlaylistUpdateFailed": "Esitusloendi värskendamine ebaõnnestus",
"ToastPlaylistUpdateSuccess": "Esitusloend värskendatud",
"ToastPodcastCreateFailed": "Podcasti loomine ebaõnnestus",
"ToastPodcastCreateSuccess": "Podcast loodud edukalt",
"ToastRemoveItemFromCollectionFailed": "Üksuse eemaldamine kogumist ebaõnnestus",
"ToastRemoveItemFromCollectionSuccess": "Üksus eemaldatud kogumist",
"ToastRSSFeedCloseFailed": "RSS-voogu sulgemine ebaõnnestus",
"ToastRSSFeedCloseSuccess": "RSS-voog suletud",
"ToastSendEbookToDeviceFailed": "E-raamatu saatmine seadmesse ebaõnnestus",
"ToastSendEbookToDeviceSuccess": "E-raamat saadetud seadmesse \"{0}\"",
"ToastSeriesUpdateFailed": "Sarja värskendamine ebaõnnestus",
"ToastSeriesUpdateSuccess": "Sarja värskendamine õnnestus",
"ToastSessionDeleteFailed": "Seansi kustutamine ebaõnnestus",
"ToastSessionDeleteSuccess": "Sessioon kustutatud",
"ToastSocketConnected": "Pesa ühendatud",
"ToastSocketDisconnected": "Pesa ühendus katkenud",
"ToastSocketFailedToConnect": "Pesa ühendamine ebaõnnestus",
"ToastUserDeleteFailed": "Kasutaja kustutamine ebaõnnestus",
"ToastUserDeleteSuccess": "Kasutaja kustutatud"
}

View File

@@ -1,13 +1,13 @@
{
"ButtonAdd": "Ajouter",
"ButtonAddChapters": "Ajouter le chapitre",
"ButtonAddDevice": "Add Device",
"ButtonAddLibrary": "Add Library",
"ButtonAddDevice": "Ajouter un appareil",
"ButtonAddLibrary": "Ajouter une bibliothèque",
"ButtonAddPodcasts": "Ajouter des podcasts",
"ButtonAddUser": "Add User",
"ButtonAddUser": "Ajouter un utilisateur",
"ButtonAddYourFirstLibrary": "Ajouter votre première bibliothèque",
"ButtonApply": "Appliquer",
"ButtonApplyChapters": "Appliquer les chapitres",
"ButtonApplyChapters": "Appliquer aux chapitres",
"ButtonAuthors": "Auteurs",
"ButtonBrowseForFolder": "Naviguer vers le répertoire",
"ButtonCancel": "Annuler",
@@ -32,6 +32,8 @@
"ButtonHide": "Cacher",
"ButtonHome": "Accueil",
"ButtonIssues": "Parutions",
"ButtonJumpBackward": "Retour",
"ButtonJumpForward": "Avancer",
"ButtonLatest": "Dernière version",
"ButtonLibrary": "Bibliothèque",
"ButtonLogout": "Me déconnecter",
@@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "Chercher tous les auteurs",
"ButtonMatchBooks": "Chercher les livres",
"ButtonNevermind": "Non merci",
"ButtonNext": "Suivant",
"ButtonNextChapter": "Chapitre suivant",
"ButtonOk": "Ok",
"ButtonOpenFeed": "Ouvrir le flux",
"ButtonOpenManager": "Ouvrir le gestionnaire",
"ButtonPause": "Pause",
"ButtonPlay": "Écouter",
"ButtonPlaying": "En lecture",
"ButtonPlaylists": "Listes de lecture",
"ButtonPrevious": "Précédent",
"ButtonPreviousChapter": "Chapitre précédent",
"ButtonPurgeAllCache": "Purger le cache",
"ButtonPurgeItemsCache": "Purger le cache des articles",
"ButtonPurgeMediaProgress": "Purger la progression des médias",
@@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "Supprimer de la liste de lecture",
"ButtonQuickMatch": "Recherche rapide",
"ButtonRead": "Lire",
"ButtonRefresh": "Rafraîchir",
"ButtonRemove": "Supprimer",
"ButtonRemoveAll": "Supprimer tout",
"ButtonRemoveAllLibraryItems": "Supprimer tous les articles de la bibliothèque",
@@ -62,7 +70,7 @@
"ButtonRemoveSeriesFromContinueSeries": "Ne plus continuer à écouter la série",
"ButtonReScan": "Nouvelle analyse",
"ButtonReset": "Réinitialiser",
"ButtonResetToDefault": "Reset to default",
"ButtonResetToDefault": "Réinitialiser aux valeurs par défaut",
"ButtonRestore": "Rétablir",
"ButtonSave": "Sauvegarder",
"ButtonSaveAndClose": "Sauvegarder et Fermer",
@@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "Sélectionner le chemin du dossier",
"ButtonSeries": "Séries",
"ButtonSetChaptersFromTracks": "Positionner les chapitres par rapports aux pistes",
"ButtonShare": "Partager",
"ButtonShiftTimes": "Décaler lhorodatage du livre",
"ButtonShow": "Afficher",
"ButtonStartM4BEncode": "Démarrer lencodage M4B",
@@ -83,14 +92,17 @@
"ButtonUploadBackup": "Téléverser une sauvegarde",
"ButtonUploadCover": "Téléverser une couverture",
"ButtonUploadOPMLFile": "Téléverser un fichier OPML",
"ButtonUserDelete": "Effacer lutilisateur {0}",
"ButtonUserDelete": "Supprimer lutilisateur {0}",
"ButtonUserEdit": "Modifier lutilisateur {0}",
"ButtonViewAll": "Afficher tout",
"ButtonYes": "Oui",
"ErrorUploadFetchMetadataAPI": "Erreur lors de la récupération des métadonnées",
"ErrorUploadFetchMetadataNoResults": "Impossible de récupérer les métadonnées - essayez de mettre à jour le titre et/ou lauteur.",
"ErrorUploadLacksTitle": "Doit avoir un titre",
"HeaderAccount": "Compte",
"HeaderAdvanced": "Avancé",
"HeaderAppriseNotificationSettings": "Configuration des Notifications Apprise",
"HeaderAudiobookTools": "Outils de Gestion de Fichier Audiobook",
"HeaderAppriseNotificationSettings": "Configuration des notifications Apprise",
"HeaderAudiobookTools": "Outils de gestion de fichiers de livres audio",
"HeaderAudioTracks": "Pistes audio",
"HeaderAuthentication": "Authentication",
"HeaderBackups": "Sauvegardes",
@@ -98,9 +110,10 @@
"HeaderChapters": "Chapitres",
"HeaderChooseAFolder": "Choisir un dossier",
"HeaderCollection": "Collection",
"HeaderCollectionItems": "Entrées de la Collection",
"HeaderCollectionItems": "Entrées de la collection",
"HeaderCover": "Couverture",
"HeaderCurrentDownloads": "Téléchargements en cours",
"HeaderCustomMetadataProviders": "Fournisseurs de métadonnées personnalisés",
"HeaderDetails": "Détails",
"HeaderDownloadQueue": "File dattente de téléchargements",
"HeaderEbookFiles": "Fichier des livres numériques",
@@ -111,10 +124,10 @@
"HeaderEreaderSettings": "Options Ereader",
"HeaderFiles": "Fichiers",
"HeaderFindChapters": "Trouver les chapitres",
"HeaderIgnoredFiles": "Fichiers Ignorés",
"HeaderItemFiles": "Fichiers des Articles",
"HeaderIgnoredFiles": "Fichiers ignorés",
"HeaderItemFiles": "Fichiers des articles",
"HeaderItemMetadataUtils": "Outils de gestion des métadonnées",
"HeaderLastListeningSession": "Dernière Session découte",
"HeaderLastListeningSession": "Dernière session découte",
"HeaderLatestEpisodes": "Dernier épisodes",
"HeaderLibraries": "Bibliothèque",
"HeaderLibraryFiles": "Fichier de bibliothèque",
@@ -127,15 +140,15 @@
"HeaderManageTags": "Gérer les étiquettes",
"HeaderMapDetails": "Édition en masse",
"HeaderMatch": "Chercher",
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
"HeaderMetadataToEmbed": "Métadonnée à intégrer",
"HeaderMetadataOrderOfPrecedence": "Ordre de priorité des métadonnées",
"HeaderMetadataToEmbed": "Métadonnées à intégrer",
"HeaderNewAccount": "Nouveau compte",
"HeaderNewLibrary": "Nouvelle bibliothèque",
"HeaderNotifications": "Notifications",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
"HeaderOpenRSSFeed": "Ouvrir Flux RSS",
"HeaderOpenIDConnectAuthentication": "Authentification via OpenID Connect",
"HeaderOpenRSSFeed": "Ouvrir un flux RSS",
"HeaderOtherFiles": "Autres fichiers",
"HeaderPasswordAuthentication": "Password Authentication",
"HeaderPasswordAuthentication": "Authentification par mot de passe",
"HeaderPermissions": "Permissions",
"HeaderPlayerQueue": "Liste découte",
"HeaderPlaylist": "Liste de lecture",
@@ -151,7 +164,7 @@
"HeaderSchedule": "Programmation",
"HeaderScheduleLibraryScans": "Analyse automatique de la bibliothèque",
"HeaderSession": "Session",
"HeaderSetBackupSchedule": "Activer la Sauvegarde Automatique",
"HeaderSetBackupSchedule": "Activer la sauvegarde automatique",
"HeaderSettings": "Paramètres",
"HeaderSettingsDisplay": "Affichage",
"HeaderSettingsExperimental": "Fonctionnalités expérimentales",
@@ -171,6 +184,7 @@
"HeaderUpdateDetails": "Mettre à jour les détails",
"HeaderUpdateLibrary": "Mettre à jour la bibliothèque",
"HeaderUsers": "Utilisateurs",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Vos statistiques",
"LabelAbridged": "Version courte",
"LabelAccountType": "Type de compte",
@@ -184,11 +198,11 @@
"LabelAddToCollectionBatch": "Ajout de {0} livres à la lollection",
"LabelAddToPlaylist": "Ajouter à la liste de lecture",
"LabelAddToPlaylistBatch": "{0} éléments ajoutés à la liste de lecture",
"LabelAdminUsersOnly": "Admin users only",
"LabelAdminUsersOnly": "Administrateurs uniquement",
"LabelAll": "Tout",
"LabelAllUsers": "Tous les utilisateurs",
"LabelAllUsersExcludingGuests": "All users excluding guests",
"LabelAllUsersIncludingGuests": "All users including guests",
"LabelAllUsersExcludingGuests": "Tous les utilisateurs à lexception des invités",
"LabelAllUsersIncludingGuests": "Tous les utilisateurs, y compris les invités",
"LabelAlreadyInYourLibrary": "Déjà dans la bibliothèque",
"LabelAppend": "Ajouter",
"LabelAuthor": "Auteur",
@@ -196,27 +210,29 @@
"LabelAuthorLastFirst": "Auteur (Nom, Prénom)",
"LabelAuthors": "Auteurs",
"LabelAutoDownloadEpisodes": "Téléchargement automatique dépisode",
"LabelAutoLaunch": "Auto Launch",
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register",
"LabelAutoRegisterDescription": "Automatically create new users after logging in",
"LabelBackToUser": "Revenir à lUtilisateur",
"LabelBackupLocation": "Backup Location",
"LabelAutoFetchMetadata": "Recherche automatique de métadonnées",
"LabelAutoFetchMetadataHelp": "Récupère les métadonnées du titre, de lauteur et de la série pour simplifier le téléchargement. Il se peut que des métadonnées supplémentaires doivent être ajoutées après le téléchargement.",
"LabelAutoLaunch": "Lancement automatique",
"LabelAutoLaunchDescription": "Redirection automatique vers le fournisseur d'authentification lors de la navigation vers la page de connexion (chemin de remplacement manuel <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Enregistrement automatique",
"LabelAutoRegisterDescription": "Créer automatiquement de nouveaux utilisateurs après la connexion",
"LabelBackToUser": "Retour à lutilisateur",
"LabelBackupLocation": "Emplacement de la sauvegarde",
"LabelBackupsEnableAutomaticBackups": "Activer les sauvegardes automatiques",
"LabelBackupsEnableAutomaticBackupsHelp": "Sauvegardes Enregistrées dans /metadata/backups",
"LabelBackupsEnableAutomaticBackupsHelp": "Sauvegardes enregistrées dans /metadata/backups",
"LabelBackupsMaxBackupSize": "Taille maximale de la sauvegarde (en Go)",
"LabelBackupsMaxBackupSizeHelp": "Afin de prévenir les mauvaises configuration, la sauvegarde échouera si elle excède la taille limite.",
"LabelBackupsNumberToKeep": "Nombre de sauvegardes à maintenir",
"LabelBackupsNumberToKeepHelp": "Une seule sauvegarde sera effacée à la fois. Si vous avez plus de sauvegardes à effacer, vous devrez le faire manuellement.",
"LabelBackupsNumberToKeep": "Nombre de sauvegardes à conserver",
"LabelBackupsNumberToKeepHelp": "Seule une sauvegarde sera supprimée à la fois. Si vous avez déjà plus de sauvegardes à effacer, vous devez les supprimer manuellement.",
"LabelBitrate": "Bitrate",
"LabelBooks": "Livres",
"LabelButtonText": "Button Text",
"LabelButtonText": "Texte du bouton",
"LabelChangePassword": "Modifier le mot de passe",
"LabelChannels": "Canaux",
"LabelChapters": "Chapitres",
"LabelChaptersFound": "Chapitres trouvés",
"LabelChapterTitle": "Titres du chapitre",
"LabelClickForMoreInfo": "Click for more info",
"LabelChaptersFound": "chapitres trouvés",
"LabelChapterTitle": "Titre du chapitre",
"LabelClickForMoreInfo": "Cliquez ici pour plus dinformations",
"LabelClosePlayer": "Fermer le lecteur",
"LabelCodec": "Codec",
"LabelCollapseSeries": "Réduire les séries",
@@ -230,20 +246,20 @@
"LabelCover": "Couverture",
"LabelCoverImageURL": "URL vers limage de couverture",
"LabelCreatedAt": "Créé le",
"LabelCronExpression": "Expression Cron",
"LabelCurrent": "Courrant",
"LabelCurrently": "En ce moment :",
"LabelCustomCronExpression": "Expression cron personnalisée:",
"LabelDatetime": "Datetime",
"LabelDeleteFromFileSystemCheckbox": "Delete from file system (uncheck to only remove from database)",
"LabelCronExpression": "Expression cron",
"LabelCurrent": "Actuel",
"LabelCurrently": "Actuellement :",
"LabelCustomCronExpression": "Expression cron personnalisée :",
"LabelDatetime": "Date",
"LabelDeleteFromFileSystemCheckbox": "Supprimer du système de fichiers (décocher pour ne supprimer que de la base de données)",
"LabelDescription": "Description",
"LabelDeselectAll": "Tout déselectionner",
"LabelDevice": "Appareil",
"LabelDeviceInfo": "Détail de lappareil",
"LabelDeviceIsAvailableTo": "Device is available to...",
"LabelDeviceIsAvailableTo": "Lappareil est disponible pour…",
"LabelDirectory": "Répertoire",
"LabelDiscFromFilename": "Disque depuis le fichier",
"LabelDiscFromMetadata": "Disque depuis les métadonnées",
"LabelDiscFromFilename": "Depuis le fichier",
"LabelDiscFromMetadata": "Depuis les métadonnées",
"LabelDiscover": "Découvrir",
"LabelDownload": "Téléchargement",
"LabelDownloadNEpisodes": "Télécharger {0} épisode(s)",
@@ -266,33 +282,37 @@
"LabelExample": "Exemple",
"LabelExplicit": "Restriction",
"LabelFeedURL": "URL du flux",
"LabelFetchingMetadata": "Récupération des métadonnées",
"LabelFile": "Fichier",
"LabelFileBirthtime": "Création du fichier",
"LabelFileModified": "Modification du fichier",
"LabelFilename": "Nom de fichier",
"LabelFilterByUser": "Filtrer par lutilisateur",
"LabelFilterByUser": "Filtrer par utilisateur",
"LabelFindEpisodes": "Trouver des épisodes",
"LabelFinished": "Fini(e)",
"LabelFinished": "Terminé le",
"LabelFolder": "Dossier",
"LabelFolders": "Dossiers",
"LabelFontFamily": "Famille de polices",
"LabelFontBold": "Gras",
"LabelFontFamily": "Polices de caractères",
"LabelFontItalic": "Italique",
"LabelFontScale": "Taille de la police de caractère",
"LabelFontStrikethrough": "Barrer",
"LabelFormat": "Format",
"LabelGenre": "Genre",
"LabelGenres": "Genres",
"LabelHardDeleteFile": "Suppression du fichier",
"LabelHasEbook": "Dispose dun livre numérique",
"LabelHasSupplementaryEbook": "Dispose dun livre numérique supplémentaire",
"LabelHighestPriority": "Highest priority",
"LabelHighestPriority": "Priorité la plus élevée",
"LabelHost": "Hôte",
"LabelHour": "Heure",
"LabelIcon": "Icone",
"LabelImageURLFromTheWeb": "Image URL from the web",
"LabelIncludeInTracklist": "Inclure dans la liste des pistes",
"LabelIcon": "Icône",
"LabelImageURLFromTheWeb": "URL de limage à partir du web",
"LabelIncludeInTracklist": "Inclure dans la liste de lecture",
"LabelIncomplete": "Incomplet",
"LabelInProgress": "En cours",
"LabelInterval": "Intervalle",
"LabelIntervalCustomDailyWeekly": "Journalier / Hebdomadaire personnalisé",
"LabelIntervalCustomDailyWeekly": "Personnaliser quotidiennement / hebdomadairement",
"LabelIntervalEvery12Hours": "Toutes les 12 heures",
"LabelIntervalEvery15Minutes": "Toutes les 15 minutes",
"LabelIntervalEvery2Hours": "Toutes les 2 heures",
@@ -325,20 +345,24 @@
"LabelLogLevelInfo": "Info",
"LabelLogLevelWarn": "Warn",
"LabelLookForNewEpisodesAfterDate": "Chercher de nouveaux épisode après cette date",
"LabelLowestPriority": "Lowest Priority",
"LabelMatchExistingUsersBy": "Match existing users by",
"LabelMatchExistingUsersByDescription": "Used for connecting existing users. Once connected, users will be matched by a unique id from your SSO provider",
"LabelLowestPriority": "Priorité la plus basse",
"LabelMatchExistingUsersBy": "Faire correspondre les utilisateurs existants par",
"LabelMatchExistingUsersByDescription": "Utilisé pour connecter les utilisateurs existants. Une fois connectés, les utilisateurs seront associés à un identifiant unique provenant de votre fournisseur SSO.",
"LabelMediaPlayer": "Lecteur multimédia",
"LabelMediaType": "Type de média",
"LabelMetadataOrderOfPrecedenceDescription": "Higher priority metadata sources will override lower priority metadata sources",
"LabelMetadataOrderOfPrecedenceDescription": "Les sources de métadonnées ayant une priorité plus élevée auront la priorité sur celles ayant une priorité moins élevée.",
"LabelMetadataProvider": "Fournisseur de métadonnées",
"LabelMetaTag": "Etiquette de métadonnée",
"LabelMetaTags": "Etiquettes de métadonnée",
"LabelMetaTag": "Balise de métadonnée",
"LabelMetaTags": "Balises de métadonnée",
"LabelMinute": "Minute",
"LabelMissing": "Manquant",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Parties manquantes",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "URI de redirection mobile autorisés",
"LabelMobileRedirectURIsDescription": "Il s'agit d'une liste blanche dURI de redirection valides pour les applications mobiles. Celui par défaut est <code>audiobookshelf://oauth</code>, que vous pouvez supprimer ou compléter avec des URIs supplémentaires pour l'intégration d'applications tierces. Lutilisation dun astérisque (<code>*</code>) comme seule entrée autorise nimporte quel URI.",
"LabelMore": "Plus",
"LabelMoreInfo": "Plus dinfo",
"LabelMoreInfo": "Plus dinformations",
"LabelName": "Nom",
"LabelNarrator": "Narrateur",
"LabelNarrators": "Narrateurs",
@@ -350,7 +374,7 @@
"LabelNextScheduledRun": "Prochain lancement prévu",
"LabelNoEpisodesSelected": "Aucun épisode sélectionné",
"LabelNotes": "Notes",
"LabelNotFinished": "Non terminé(e)",
"LabelNotFinished": "Non terminé",
"LabelNotificationAppriseURL": "URL(s) dApprise",
"LabelNotificationAvailableVariables": "Variables disponibles",
"LabelNotificationBodyTemplate": "Modèle de Message",
@@ -359,10 +383,10 @@
"LabelNotificationsMaxFailedAttemptsHelp": "La notification est abandonnée une fois ce seuil atteint",
"LabelNotificationsMaxQueueSize": "Nombres de notifications maximum à mettre en attente",
"LabelNotificationsMaxQueueSizeHelp": "La limite de notification est de un évènement par seconde. Les notifications seront ignorées si la file dattente est à son maximum. Cela empêche un flot trop important.",
"LabelNotificationTitleTemplate": "Modèle de Titre",
"LabelNotStarted": "Non Démarré(e)",
"LabelNumberOfBooks": "Nombre de Livres",
"LabelNumberOfEpisodes": "Nombre dEpisodes",
"LabelNotificationTitleTemplate": "Modèle de titre",
"LabelNotStarted": "Pas commencé",
"LabelNumberOfBooks": "Nombre de livres",
"LabelNumberOfEpisodes": "Nombre dépisodes",
"LabelOpenRSSFeed": "Ouvrir le flux RSS",
"LabelOverwrite": "Écraser",
"LabelPassword": "Mot de passe",
@@ -374,11 +398,13 @@
"LabelPermissionsDownload": "Peut télécharger",
"LabelPermissionsUpdate": "Peut mettre à jour",
"LabelPermissionsUpload": "Peut téléverser",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Chemin / URL des photos",
"LabelPlaylists": "Listes de lecture",
"LabelPlayMethod": "Méthode découte",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts",
"LabelPodcastSearchRegion": "Région de recherche de podcasts",
"LabelPodcastType": "Type de Podcast",
"LabelPort": "Port",
"LabelPrefixesToIgnore": "Préfixes à Ignorer (Insensible à la Casse)",
@@ -395,14 +421,16 @@
"LabelRecentlyAdded": "Derniers ajouts",
"LabelRecentSeries": "Séries récentes",
"LabelRecommended": "Recommandé",
"LabelRedo": "Refaire",
"LabelRegion": "Région",
"LabelReleaseDate": "Date de parution",
"LabelRemoveCover": "Supprimer la couverture",
"LabelRowsPerPage": "Lignes par page",
"LabelRSSFeedCustomOwnerEmail": "Courriel du propriétaire personnalisé",
"LabelRSSFeedCustomOwnerName": "Nom propriétaire personnalisé",
"LabelRSSFeedOpen": "Flux RSS ouvert",
"LabelRSSFeedPreventIndexing": "Empêcher lindexation",
"LabelRSSFeedSlug": "Identificateur dadresse du Flux RSS ",
"LabelRSSFeedSlug": "Balise URL du flux RSS",
"LabelRSSFeedURL": "Adresse du flux RSS",
"LabelSearchTerm": "Terme de recherche",
"LabelSearchTitle": "Titre de recherche",
@@ -410,41 +438,44 @@
"LabelSeason": "Saison",
"LabelSelectAllEpisodes": "Sélectionner tous les épisodes",
"LabelSelectEpisodesShowing": "Sélectionner {0} episode(s) en cours",
"LabelSelectUsers": "Select users",
"LabelSendEbookToDevice": "Envoyer le livre numérique à...",
"LabelSelectUsers": "Sélectionner les utilisateurs",
"LabelSendEbookToDevice": "Envoyer le livre numérique à",
"LabelSequence": "Séquence",
"LabelSeries": "Séries",
"LabelSeriesName": "Nom de la série",
"LabelSeriesProgress": "Progression de séries",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Définir comme principale",
"LabelSetEbookAsSupplementary": "Définir comme supplémentaire",
"LabelSettingsAudiobooksOnly": "Livres audios seulement",
"LabelSettingsAudiobooksOnlyHelp": "Lactivation de ce paramètre ignorera les fichiers “ ebook ”, à moins quils ne se trouvent dans un dossier de livres audio, auquel cas ils seront définis comme des livres numériques supplémentaires.",
"LabelSettingsAudiobooksOnlyHelp": "L'activation de ce paramètre ignorera les fichiers de type « livre numériques », sauf s'ils se trouvent dans un dossier spécifique , auquel cas ils seront définis comme des livres numériques supplémentaires.",
"LabelSettingsBookshelfViewHelp": "Interface skeuomorphique avec une étagère en bois",
"LabelSettingsChromecastSupport": "Support du Chromecast",
"LabelSettingsDateFormat": "Format de date",
"LabelSettingsDisableWatcher": "Désactiver la surveillance",
"LabelSettingsDisableWatcherForLibrary": "Désactiver la surveillance des dossiers pour la bibliothèque",
"LabelSettingsDisableWatcherHelp": "Désactive la mise à jour automatique lorsque des modifications de fichiers sont détectées. *Nécessite le redémarrage du serveur",
"LabelSettingsDisableWatcherHelp": "Désactive la mise à jour automatique lorsque des modifications de fichiers sont détectées. * nécessite le redémarrage du serveur",
"LabelSettingsEnableWatcher": "Activer la veille",
"LabelSettingsEnableWatcherForLibrary": "Activer la surveillance des dossiers pour la bibliothèque",
"LabelSettingsEnableWatcherHelp": "Active la mise à jour automatique automatique lorsque des modifications de fichiers sont détectées. *Nécessite le redémarrage du serveur",
"LabelSettingsEnableWatcherHelp": "Active la mise à jour automatique automatique lorsque des modifications de fichiers sont détectées. * nécessite le redémarrage du serveur",
"LabelSettingsExperimentalFeatures": "Fonctionnalités expérimentales",
"LabelSettingsExperimentalFeaturesHelp": "Fonctionnalités en cours de développement sur lesquelles nous attendons votre retour et expérience. Cliquez pour ouvrir la discussion Github.",
"LabelSettingsExperimentalFeaturesHelp": "Fonctionnalités en cours de développement sur lesquelles nous attendons votre retour et expérience. Cliquez pour ouvrir la discussion GitHub.",
"LabelSettingsFindCovers": "Chercher des couvertures de livre",
"LabelSettingsFindCoversHelp": "Si votre livre audio ne possède pas de couverture intégrée ou une image de couverture dans le dossier, lanalyseur tentera de récupérer une couverture.<br>Attention, cela peut augmenter le temps danalyse.",
"LabelSettingsHideSingleBookSeries": "Masquer les séries de livres uniques",
"LabelSettingsHideSingleBookSeriesHelp": "Les séries qui ne comportent quun seul livre seront masquées sur la page de la série et sur les étagères de la page daccueil.",
"LabelSettingsHomePageBookshelfView": "La page daccueil utilise la vue étagère",
"LabelSettingsLibraryBookshelfView": "La bibliothèque utilise la vue étagère",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Analyser les sous-titres",
"LabelSettingsParseSubtitlesHelp": "Extrait les sous-titres depuis le dossier du Livre Audio.<br>Les sous-titres doivent être séparés par « - »<br>i.e. « Titre du Livre - Ceci est un sous-titre » aura le sous-titre « Ceci est un sous-titre »",
"LabelSettingsParseSubtitlesHelp": "Extrait les sous-titres depuis le dossier du livre audio.<br>Les sous-titres doivent être séparés par « - »<br>cest-à-dire : « Titre du livre - Ceci est un sous-titre » aura le sous-titre « Ceci est un sous-titre »",
"LabelSettingsPreferMatchedMetadata": "Préférer les métadonnées par correspondance",
"LabelSettingsPreferMatchedMetadataHelp": "Les métadonnées par correspondance écrase les détails de larticle lors dune recherche par correspondance rapide. Par défaut, la recherche par correspondance rapide ne comblera que les éléments manquant.",
"LabelSettingsSkipMatchingBooksWithASIN": "Ignorer la recherche par correspondance sur les livres ayant déjà un ASIN",
"LabelSettingsSkipMatchingBooksWithISBN": "Ignorer la recherche par correspondance sur les livres ayant déjà un ISBN",
"LabelSettingsSortingIgnorePrefixes": "Ignorer les préfixes lors du tri",
"LabelSettingsSortingIgnorePrefixesHelp": "i.e. pour le préfixe « le », le livre avec pour titre « Le Titre du Livre » sera trié en tant que « Titre du Livre, Le »",
"LabelSettingsSortingIgnorePrefixesHelp": "cest-à-dire : pour le préfixe « le », le livre avec pour titre « Le Titre du Livre » sera trié en tant que « Titre du Livre, Le »",
"LabelSettingsSquareBookCovers": "Utiliser des couvertures carrées",
"LabelSettingsSquareBookCoversHelp": "Préférer les couvertures carrées par rapport aux couvertures standards de ratio 1.6:1.",
"LabelSettingsStoreCoversWithItem": "Enregistrer la couverture avec les articles",
@@ -452,61 +483,66 @@
"LabelSettingsStoreMetadataWithItem": "Enregistrer les Métadonnées avec les articles",
"LabelSettingsStoreMetadataWithItemHelp": "Par défaut, les métadonnées sont enregistrées dans /metadata/items",
"LabelSettingsTimeFormat": "Format dheure",
"LabelShowAll": "Afficher Tout",
"LabelShowAll": "Tout afficher",
"LabelSize": "Taille",
"LabelSleepTimer": "Minuterie",
"LabelSlug": "Slug",
"LabelSlug": "Balise",
"LabelStart": "Démarrer",
"LabelStarted": "Démarré",
"LabelStartedAt": "Démarré à",
"LabelStartTime": "Heure de Démarrage",
"LabelStartTime": "Heure de démarrage",
"LabelStatsAudioTracks": "Pistes Audios",
"LabelStatsAuthors": "Auteurs",
"LabelStatsBestDay": "Meilleur Jour",
"LabelStatsDailyAverage": "Moyenne Journalière",
"LabelStatsBestDay": "Meilleur jour",
"LabelStatsDailyAverage": "Moyenne journalière",
"LabelStatsDays": "Jours",
"LabelStatsDaysListened": "Jours découte",
"LabelStatsHours": "Heures",
"LabelStatsInARow": "daffilé(s)",
"LabelStatsInARow": "daffilée(s)",
"LabelStatsItemsFinished": "Articles terminés",
"LabelStatsItemsInLibrary": "Articles dans la Bibliothèque",
"LabelStatsItemsInLibrary": "Articles dans la bibliothèque",
"LabelStatsMinutes": "minutes",
"LabelStatsMinutesListening": "Minutes découte",
"LabelStatsOverallDays": "Jours au total",
"LabelStatsOverallHours": "Heures au total",
"LabelStatsOverallDays": "Nombre total de jours",
"LabelStatsOverallHours": "Nombre total d'heures",
"LabelStatsWeekListening": "Écoute de la semaine",
"LabelSubtitle": "Sous-Titre",
"LabelSubtitle": "Sous-titre",
"LabelSupportedFileTypes": "Types de fichiers supportés",
"LabelTag": "Étiquette",
"LabelTags": "Étiquettes",
"LabelTagsAccessibleToUser": "Étiquettes accessibles à lutilisateur",
"LabelTagsNotAccessibleToUser": "Étiquettes non accessibles à lutilisateur",
"LabelTasks": "Tâches en cours",
"LabelTextEditorBulletedList": "Liste à puces",
"LabelTextEditorLink": "Lien",
"LabelTextEditorNumberedList": "Liste numérotée",
"LabelTextEditorUnlink": "Dissocier",
"LabelTheme": "Thème",
"LabelThemeDark": "Sombre",
"LabelThemeLight": "Clair",
"LabelTimeBase": "Base de temps",
"LabelTimeListened": "Temps découte",
"LabelTimeListenedToday": "Nombres découtes Aujourdhui",
"LabelTimeListenedToday": "Nombres découtes aujourdhui",
"LabelTimeRemaining": "{0} restantes",
"LabelTimeToShift": "Temps de décalage en secondes",
"LabelTitle": "Titre",
"LabelToolsEmbedMetadata": "Métadonnées Intégrées",
"LabelToolsEmbedMetadata": "Métadonnées intégrées",
"LabelToolsEmbedMetadataDescription": "Intègre les métadonnées au fichier audio avec la couverture et les chapitres.",
"LabelToolsMakeM4b": "Créer un fichier Livre Audio M4B",
"LabelToolsMakeM4bDescription": "Génère un fichier Livre Audio .M4B avec intégration des métadonnées, image de couverture et les chapitres.",
"LabelToolsMakeM4b": "Créer un fichier livre audio M4B",
"LabelToolsMakeM4bDescription": "Générer un fichier de livre audio .M4B avec des métadonnées intégrées, une image de couverture et des chapitres.",
"LabelToolsSplitM4b": "Scinde le fichier M4B en fichiers MP3",
"LabelToolsSplitM4bDescription": "Créer plusieurs fichier MP3 à partir du découpage par chapitre, en incluant les métadonnées, limage de couverture et les chapitres.",
"LabelTotalDuration": "Durée Totale",
"LabelTotalDuration": "Durée totale",
"LabelTotalTimeListened": "Temps découte total",
"LabelTrackFromFilename": "Piste depuis le fichier",
"LabelTrackFromMetadata": "Piste depuis les métadonnées",
"LabelTracks": "Pistes",
"LabelTracksMultiTrack": "Piste multiple",
"LabelTracksNone": "No tracks",
"LabelTracksNone": "Aucune piste",
"LabelTracksSingleTrack": "Piste simple",
"LabelType": "Type",
"LabelUnabridged": "Version intégrale",
"LabelUndo": "Annuler",
"LabelUnknown": "Inconnu",
"LabelUpdateCover": "Mettre à jour la couverture",
"LabelUpdateCoverHelp": "Autoriser la mise à jour de la couverture existante lorsquune correspondance est trouvée",
@@ -515,8 +551,9 @@
"LabelUpdateDetailsHelp": "Autoriser la mise à jour des détails existants lorsquune correspondance est trouvée",
"LabelUploaderDragAndDrop": "Glisser et déposer des fichiers ou dossiers",
"LabelUploaderDropFiles": "Déposer des fichiers",
"LabelUploaderItemFetchMetadataHelp": "Récupérer automatiquement le titre, lauteur et la série",
"LabelUseChapterTrack": "Utiliser la piste du chapitre",
"LabelUseFullTrack": "Utiliser la piste Complète",
"LabelUseFullTrack": "Utiliser la piste complète",
"LabelUser": "Utilisateur",
"LabelUsername": "Nom dutilisateur",
"LabelValue": "Valeur",
@@ -526,19 +563,21 @@
"LabelViewQueue": "Afficher la liste de lecture",
"LabelVolume": "Volume",
"LabelWeekdaysToRun": "Jours de la semaine à exécuter",
"LabelYearReviewHide": "Masquer le bilan de lannée",
"LabelYearReviewShow": "Afficher le bilan de lannée",
"LabelYourAudiobookDuration": "Durée de vos livres audios",
"LabelYourBookmarks": "Vos signets",
"LabelYourPlaylists": "Vos listes de lecture",
"LabelYourProgress": "Votre progression",
"MessageAddToPlayerQueue": "Ajouter en file dattente",
"MessageAppriseDescription": "Nécessite une instance d<a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API Apprise</a> pour utiliser cette fonctionnalité ou une api qui prend en charge les mêmes requêtes. <br />lURL de lAPI Apprise doit comprendre le chemin complet pour envoyer la notification. Par exemple, si votre instance écoute sur <code>http://192.168.1.1:8337</code> alors vous devez mettre <code>http://192.168.1.1:8337/notify</code>.",
"MessageAppriseDescription": "Nécessite une instance d<a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">API Apprise</a> pour utiliser cette fonctionnalité ou une api qui prend en charge les mêmes requêtes.<br>LURL de lAPI Apprise doit comprendre le chemin complet pour envoyer la notification. Par exemple, si votre instance écoute sur <code>http://192.168.1.1:8337</code> alors vous devez mettre <code>http://192.168.1.1:8337/notify</code>.",
"MessageBackupsDescription": "Les sauvegardes incluent les utilisateurs, la progression de lecture par utilisateur, les détails des articles des bibliothèques, les paramètres du serveur et les images sauvegardées. Les sauvegardes nincluent pas les fichiers de votre bibliothèque.",
"MessageBatchQuickMatchDescription": "La recherche par correspondance rapide tentera dajouter les couvertures et les métadonnées manquantes pour les articles sélectionnés. Activer loption suivante pour autoriser la recherche par correspondance à écraser les données existantes.",
"MessageBookshelfNoCollections": "Vous navez pas encore de collections",
"MessageBookshelfNoResultsForFilter": "Aucun résultat pour le filtre « {0}: {1} »",
"MessageBookshelfNoResultsForFilter": "Aucun résultat pour le filtre « {0} : {1} »",
"MessageBookshelfNoRSSFeeds": "Aucun flux RSS nest ouvert",
"MessageBookshelfNoSeries": "Vous navez aucune série",
"MessageChapterEndIsAfter": "Le Chapitre Fin est situé à la fin de votre Livre Audio",
"MessageChapterEndIsAfter": "La fin du chapitre se situe après la fin de votre livre audio.",
"MessageChapterErrorFirstNotZero": "Le premier capitre doit débuter à 0",
"MessageChapterErrorStartGteDuration": "Horodatage invalide car il doit débuter avant la fin du livre",
"MessageChapterErrorStartLtPrev": "Horodatage invalide car il doit débuter au moins après le précédent chapitre",
@@ -548,20 +587,21 @@
"MessageConfirmDeleteBackup": "Êtes-vous sûr de vouloir supprimer la sauvegarde de « {0} » ?",
"MessageConfirmDeleteFile": "Cela supprimera le fichier de votre système de fichiers. Êtes-vous sûr ?",
"MessageConfirmDeleteLibrary": "Êtes-vous sûr de vouloir supprimer définitivement la bibliothèque « {0} » ?",
"MessageConfirmDeleteLibraryItem": "This will delete the library item from the database and your file system. Are you sure?",
"MessageConfirmDeleteLibraryItems": "This will delete {0} library items from the database and your file system. Are you sure?",
"MessageConfirmDeleteLibraryItem": "Cette opération supprimera lélément de la base de données et de votre système de fichiers. Êtes-vous sûr ?",
"MessageConfirmDeleteLibraryItems": "Cette opération supprimera {0} éléments de la base de données et de votre système de fichiers. Êtes-vous sûr ?",
"MessageConfirmDeleteSession": "Êtes-vous sûr de vouloir supprimer cette session ?",
"MessageConfirmForceReScan": "Êtes-vous sûr de vouloir lancer une analyse forcée ?",
"MessageConfirmMarkAllEpisodesFinished": "Êtes-vous sûr de marquer tous les épisodes comme terminés ?",
"MessageConfirmMarkAllEpisodesNotFinished": "Êtes-vous sûr de vouloir marquer tous les épisodes comme non terminés ?",
"MessageConfirmMarkSeriesFinished": "Êtes-vous sûr de vouloir marquer tous les livres de cette série comme terminées ?",
"MessageConfirmMarkSeriesNotFinished": "Êtes-vous sûr de vouloir marquer tous les livres de cette série comme comme non terminés ?",
"MessageConfirmQuickEmbed": "Warning! Quick embed will not backup your audio files. Make sure that you have a backup of your audio files. <br><br>Would you like to continue?",
"MessageConfirmQuickEmbed": "Attention ! Lintégration rapide ne sauvegardera pas vos fichiers audio. Assurez-vous davoir effectuer une sauvegarde de vos fichiers audio.<br><br>Souhaitez-vous continuer ?",
"MessageConfirmRemoveAllChapters": "Êtes-vous sûr de vouloir supprimer tous les chapitres ?",
"MessageConfirmRemoveAuthor": "Are you sure you want to remove author \"{0}\"?",
"MessageConfirmRemoveCollection": "Êtes-vous sûr de vouloir supprimer la collection « {0} » ?",
"MessageConfirmRemoveEpisode": "Êtes-vous sûr de vouloir supprimer lépisode « {0} » ?",
"MessageConfirmRemoveEpisodes": "Êtes-vous sûr de vouloir supprimer {0} épisodes ?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Êtes-vous sûr de vouloir supprimer le narrateur « {0} » ?",
"MessageConfirmRemovePlaylist": "Êtes-vous sûr de vouloir supprimer la liste de lecture « {0} » ?",
"MessageConfirmRenameGenre": "Êtes-vous sûr de vouloir renommer le genre « {0} » en « {1} » pour tous les articles ?",
@@ -570,16 +610,16 @@
"MessageConfirmRenameTag": "Êtes-vous sûr de vouloir renommer létiquette « {0} » en « {1} » pour tous les articles ?",
"MessageConfirmRenameTagMergeNote": "Information: Cette étiquette existe déjà et sera fusionnée.",
"MessageConfirmRenameTagWarning": "Attention ! Une étiquette similaire avec une casse différente existe déjà « {0} ».",
"MessageConfirmReScanLibraryItems": "Are you sure you want to re-scan {0} items?",
"MessageConfirmReScanLibraryItems": "Êtes-vous sûr de vouloir re-analyser {0} éléments ?",
"MessageConfirmSendEbookToDevice": "Êtes-vous sûr de vouloir envoyer le livre numérique {0} « {1} » à lappareil « {2} »?",
"MessageDownloadingEpisode": "Téléchargement de lépisode",
"MessageDragFilesIntoTrackOrder": "Faire glisser les fichiers dans lordre correct",
"MessageDragFilesIntoTrackOrder": "Faites glisser les fichiers dans lordre correct des pistes",
"MessageEmbedFinished": "Intégration terminée !",
"MessageEpisodesQueuedForDownload": "{0} épisode(s) mis en file pour téléchargement",
"MessageFeedURLWillBe": "lURL du flux sera {0}",
"MessageFeedURLWillBe": "LURL du flux sera {0}",
"MessageFetching": "Récupération…",
"MessageForceReScanDescription": "Analysera tous les fichiers de nouveau. Les étiquettes ID3 des fichiers audios, fichiers OPF, et les fichiers textes seront analysés comme sils étaient nouveaux.",
"MessageImportantNotice": "Information Importante !",
"MessageForceReScanDescription": "analysera de nouveau tous les fichiers. Les étiquettes ID3 des fichiers audio, les fichiers OPF et les fichiers texte seront analysés comme sils étaient nouveaux.",
"MessageImportantNotice": "Information importante !",
"MessageInsertChapterBelow": "Insérer le chapitre ci-dessous",
"MessageItemsSelected": "{0} articles sélectionnés",
"MessageItemsUpdated": "{0} articles mis à jour",
@@ -635,12 +675,13 @@
"MessageRemoveChapter": "Supprimer le chapitre",
"MessageRemoveEpisodes": "Suppression de {0} épisode(s)",
"MessageRemoveFromPlayerQueue": "Supprimer de la liste découte",
"MessageRemoveUserWarning": "Êtes-vous certain de vouloir supprimer définitivement lutilisateur « {0} » ?",
"MessageRemoveUserWarning": "Êtes-vous sûr de vouloir supprimer définitivement lutilisateur « {0} » ?",
"MessageReportBugsAndContribute": "Remonter des anomalies, demander des fonctionnalités et contribuer sur",
"MessageResetChaptersConfirm": "Êtes-vous certain de vouloir réinitialiser les chapitres et annuler les changements effectués ?",
"MessageRestoreBackupConfirm": "Êtes-vous certain de vouloir restaurer la sauvegarde créée le",
"MessageRestoreBackupWarning": "Restaurer la sauvegarde écrasera la base de donnée située dans le dossier /config ainsi que les images sur /metadata/items et /metadata/authors.<br /><br />Les sauvegardes ne touchent pas aux fichiers de la bibliothèque. Si vous avez activé le paramètre pour sauvegarder les métadonnées et les images de couverture dans le même dossier que les fichiers, ceux-ci ne ni sauvegardés, ni écrasés lors de la restauration.<br /><br />Tous les clients utilisant votre serveur seront automatiquement mis à jour.",
"MessageResetChaptersConfirm": "Êtes-vous sûr de vouloir réinitialiser les chapitres et annuler les changements effectués ?",
"MessageRestoreBackupConfirm": "Êtes-vous sûr de vouloir restaurer la sauvegarde créée le",
"MessageRestoreBackupWarning": "Restaurer la sauvegarde écrasera la base de donnée située dans le dossier /config ainsi que les images sur /metadata/items et /metadata/authors.<br><br>Les sauvegardes ne touchent pas aux fichiers de la bibliothèque. Si vous avez activé le paramètre pour sauvegarder les métadonnées et les images de couverture dans le même dossier que les fichiers, ceux-ci ne ni sauvegardés, ni écrasés lors de la restauration.<br><br>Tous les clients utilisant votre serveur seront automatiquement mis à jour.",
"MessageSearchResultsFor": "Résultats de recherche pour",
"MessageSelected": "{0} sélectionnés",
"MessageServerCouldNotBeReached": "Serveur inaccessible",
"MessageSetChaptersFromTracksDescription": "Positionne un chapitre par fichier audio, avec le titre du fichier comme titre de chapitre",
"MessageStartPlaybackAtTime": "Démarrer la lecture pour « {0} » à {1} ?",
@@ -651,12 +692,11 @@
"MessageValidCronExpression": "Expression cron valide",
"MessageWatcherIsDisabledGlobally": "La surveillance est désactivée par un paramètre global du serveur",
"MessageXLibraryIsEmpty": "La bibliothèque {0} est vide !",
"MessageYourAudiobookDurationIsLonger": "La durée de votre Livre Audio est plus longue que la durée trouvée",
"MessageYourAudiobookDurationIsShorter": "La durée de votre Livre Audio est plus courte que la durée trouvée",
"MessageYourAudiobookDurationIsLonger": "La durée de votre livre audio est plus longue que la durée trouvée",
"MessageYourAudiobookDurationIsShorter": "La durée de votre livre audio est plus courte que la durée trouvée",
"NoteChangeRootPassword": "seul lutilisateur « root » peut utiliser un mot de passe vide",
"NoteChapterEditorTimes": "Information : lhorodatage du premier chapitre doit être à 0:00 et celui du dernier chapitre ne peut se situer au-delà de la durée du Livre Audio.",
"NoteFolderPicker": "Information : Les dossiers déjà surveillés ne sont pas affichés",
"NoteFolderPickerDebian": "Information : La sélection de dossier sur une installation debian nest pas finalisée. Merci de renseigner le chemin complet vers votre bibliothèque manuellement.",
"NoteChapterEditorTimes": "Information : lhorodatage du premier chapitre doit être à 0:00 et celui du dernier chapitre ne peut se situer au-delà de la durée du livre audio.",
"NoteFolderPicker": "Information : les dossiers déjà surveillés ne sont pas affichés",
"NoteRSSFeedPodcastAppsHttps": "Attention : la majorité des application de podcast nécessite une adresse de flux en HTTPS.",
"NoteRSSFeedPodcastAppsPubDate": "Attention : un ou plusieurs de vos épisodes ne possèdent pas de date de publication. Certaines applications de podcast le requièrent.",
"NoteUploaderFoldersWithMediaFiles": "Les dossiers contenant des fichiers multimédias seront traités comme des éléments distincts de la bibliothèque.",
@@ -665,8 +705,8 @@
"PlaceholderNewCollection": "Nom de la nouvelle collection",
"PlaceholderNewFolderPath": "Nouveau chemin de dossier",
"PlaceholderNewPlaylist": "Nouveau nom de liste de lecture",
"PlaceholderSearch": "Recherche...",
"PlaceholderSearchEpisode": "Recherche dépisode...",
"PlaceholderSearch": "Recherche",
"PlaceholderSearchEpisode": "Recherche dépisode",
"ToastAccountUpdateFailed": "Échec de la mise à jour du compte",
"ToastAccountUpdateSuccess": "Compte mis à jour",
"ToastAuthorImageRemoveFailed": "Échec de la suppression de limage",

View File

@@ -1,10 +1,10 @@
{
"ButtonAdd": "ઉમેરો",
"ButtonAddChapters": "પ્રકરણો ઉમેરો",
"ButtonAddDevice": "Add Device",
"ButtonAddLibrary": "Add Library",
"ButtonAddDevice": "ઉપકરણ ઉમેરો",
"ButtonAddLibrary": "પુસ્તકાલય ઉમેરો",
"ButtonAddPodcasts": "પોડકાસ્ટ ઉમેરો",
"ButtonAddUser": "Add User",
"ButtonAddUser": "વપરાશકર્તા ઉમેરો",
"ButtonAddYourFirstLibrary": "તમારી પ્રથમ પુસ્તકાલય ઉમેરો",
"ButtonApply": "લાગુ કરો",
"ButtonApplyChapters": "પ્રકરણો લાગુ કરો",
@@ -32,6 +32,8 @@
"ButtonHide": "છુપાવો",
"ButtonHome": "ઘર",
"ButtonIssues": "સમસ્યાઓ",
"ButtonJumpBackward": "Jump Backward",
"ButtonJumpForward": "Jump Forward",
"ButtonLatest": "નવીનતમ",
"ButtonLibrary": "પુસ્તકાલય",
"ButtonLogout": "લૉગ આઉટ",
@@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "બધા મેળ ખાતા લેખકો શોધો",
"ButtonMatchBooks": "મેળ ખાતી પુસ્તકો શોધો",
"ButtonNevermind": "કંઈ વાંધો નહીં",
"ButtonNext": "Next",
"ButtonNextChapter": "Next Chapter",
"ButtonOk": "ઓકે",
"ButtonOpenFeed": "ફીડ ખોલો",
"ButtonOpenManager": "મેનેજર ખોલો",
"ButtonPause": "Pause",
"ButtonPlay": "ચલાવો",
"ButtonPlaying": "ચલાવી રહ્યું છે",
"ButtonPlaylists": "પ્લેલિસ્ટ",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "બધો Cache કાઢી નાખો",
"ButtonPurgeItemsCache": "વસ્તુઓનો Cache કાઢી નાખો",
"ButtonPurgeMediaProgress": "બધું સાંભળ્યું કાઢી નાખો",
@@ -54,15 +61,16 @@
"ButtonQueueRemoveItem": "કતારથી કાઢી નાખો",
"ButtonQuickMatch": "ઝડપી મેળ ખવડાવો",
"ButtonRead": "વાંચો",
"ButtonRefresh": "Refresh",
"ButtonRemove": "કાઢી નાખો",
"ButtonRemoveAll": "બધું કાઢી નાખો",
"ButtonRemoveAllLibraryItems": "બધું પુસ્તકાલય વસ્તુઓ કાઢી નાખો",
"ButtonRemoveFromContinueListening": "સાંભળતી પુસ્તકો માંથી કાઢી નાખો",
"ButtonRemoveFromContinueReading": "Remove from Continue Reading",
"ButtonRemoveFromContinueReading": "સાંભળતી પુસ્તકો માંથી કાઢી નાખો",
"ButtonRemoveSeriesFromContinueSeries": "સાંભળતી સિરીઝ માંથી કાઢી નાખો",
"ButtonReScan": "ફરીથી સ્કેન કરો",
"ButtonReset": "રીસેટ કરો",
"ButtonResetToDefault": "Reset to default",
"ButtonResetToDefault": "ડિફોલ્ટ પર રીસેટ કરો",
"ButtonRestore": "પુનઃસ્થાપિત કરો",
"ButtonSave": "સાચવો",
"ButtonSaveAndClose": "સાચવો અને બંધ કરો",
@@ -73,12 +81,13 @@
"ButtonSelectFolderPath": "ફોલ્ડર પથ પસંદ કરો",
"ButtonSeries": "સિરીઝ",
"ButtonSetChaptersFromTracks": "ટ્રેક્સથી પ્રકરણો સેટ કરો",
"ButtonShare": "Share",
"ButtonShiftTimes": "સમય શિફ્ટ કરો",
"ButtonShow": "બતાવો",
"ButtonStartM4BEncode": "M4B એન્કોડ શરૂ કરો",
"ButtonStartMetadataEmbed": "મેટાડેટા એમ્બેડ શરૂ કરો",
"ButtonSubmit": "સબમિટ કરો",
"ButtonTest": "Test",
"ButtonTest": "પરખ કરો",
"ButtonUpload": "અપલોડ કરો",
"ButtonUploadBackup": "બેકઅપ અપલોડ કરો",
"ButtonUploadCover": "કવર અપલોડ કરો",
@@ -87,81 +96,85 @@
"ButtonUserEdit": "વપરાશકર્તા {0} સંપાદિત કરો",
"ButtonViewAll": "બધું જુઓ",
"ButtonYes": "હા",
"ErrorUploadFetchMetadataAPI": "Error fetching metadata",
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
"ErrorUploadLacksTitle": "Must have a title",
"HeaderAccount": "એકાઉન્ટ",
"HeaderAdvanced": "અડ્વાન્સડ",
"HeaderAppriseNotificationSettings": "Apprise સૂચના સેટિંગ્સ",
"HeaderAudiobookTools": "Audiobook File Management Tools",
"HeaderAudioTracks": "Audio Tracks",
"HeaderAudiobookTools": "ઓડિયોબુક ફાઇલ વ્યવસ્થાપન ટૂલ્સ",
"HeaderAudioTracks": "ઓડિયો ટ્રેક્સ",
"HeaderAuthentication": "Authentication",
"HeaderBackups": "Backups",
"HeaderChangePassword": "Change Password",
"HeaderChapters": "Chapters",
"HeaderChooseAFolder": "Choose a Folder",
"HeaderCollection": "Collection",
"HeaderCollectionItems": "Collection Items",
"HeaderCover": "Cover",
"HeaderCurrentDownloads": "Current Downloads",
"HeaderDetails": "Details",
"HeaderDownloadQueue": "Download Queue",
"HeaderEbookFiles": "Ebook Files",
"HeaderEmail": "Email",
"HeaderEmailSettings": "Email Settings",
"HeaderEpisodes": "Episodes",
"HeaderEreaderDevices": "Ereader Devices",
"HeaderEreaderSettings": "Ereader Settings",
"HeaderFiles": "Files",
"HeaderFindChapters": "Find Chapters",
"HeaderIgnoredFiles": "Ignored Files",
"HeaderItemFiles": "Item Files",
"HeaderItemMetadataUtils": "Item Metadata Utils",
"HeaderLastListeningSession": "Last Listening Session",
"HeaderLatestEpisodes": "Latest episodes",
"HeaderLibraries": "Libraries",
"HeaderLibraryFiles": "Library Files",
"HeaderLibraryStats": "Library Stats",
"HeaderListeningSessions": "Listening Sessions",
"HeaderListeningStats": "Listening Stats",
"HeaderLogin": "Login",
"HeaderLogs": "Logs",
"HeaderManageGenres": "Manage Genres",
"HeaderManageTags": "Manage Tags",
"HeaderMapDetails": "Map details",
"HeaderMatch": "Match",
"HeaderMetadataOrderOfPrecedence": "Metadata order of precedence",
"HeaderMetadataToEmbed": "Metadata to embed",
"HeaderNewAccount": "New Account",
"HeaderNewLibrary": "New Library",
"HeaderNotifications": "Notifications",
"HeaderBackups": "બેકઅપ્સ",
"HeaderChangePassword": "પાસવર્ડ બદલો",
"HeaderChapters": "પ્રકરણો",
"HeaderChooseAFolder": "ફોલ્ડર પસંદ કરો",
"HeaderCollection": "સંગ્રહ",
"HeaderCollectionItems": "સંગ્રહ વસ્તુઓ",
"HeaderCover": "આવરણ",
"HeaderCurrentDownloads": "વર્તમાન ડાઉનલોડ્સ",
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "વિગતો",
"HeaderDownloadQueue": "ડાઉનલોડ કતાર",
"HeaderEbookFiles": "ઇબુક ફાઇલો",
"HeaderEmail": "ઈમેલ",
"HeaderEmailSettings": "ઈમેલ સેટિંગ્સ",
"HeaderEpisodes": "એપિસોડ્સ",
"HeaderEreaderDevices": "ઇરીડર ઉપકરણો",
"HeaderEreaderSettings": "ઇરીડર સેટિંગ્સ",
"HeaderFiles": "ફાઇલો",
"HeaderFindChapters": "પ્રકરણો શોધો",
"HeaderIgnoredFiles": "અવગણેલી ફાઇલો",
"HeaderItemFiles": "વાસ્તુ ની ફાઈલો",
"HeaderItemMetadataUtils": "વસ્તુ મેટાડેટા સાધનો",
"HeaderLastListeningSession": "છેલ્લી સાંભળતી સેશન",
"HeaderLatestEpisodes": "નવીનતમ એપિસોડ્સ",
"HeaderLibraries": "પુસ્તકાલયો",
"HeaderLibraryFiles": "પુસ્તકાલય ફાઇલો",
"HeaderLibraryStats": "પુસ્તકાલય આંકડા",
"HeaderListeningSessions": "સાંભળતી સેશન્સ",
"HeaderListeningStats": "સાંભળતી આંકડા",
"HeaderLogin": "લોગિન",
"HeaderLogs": "લોગ્સ",
"HeaderManageGenres": "જાતિઓ મેનેજ કરો",
"HeaderManageTags": "ટેગ્સ મેનેજ કરો",
"HeaderMapDetails": "વિગતો મેપ કરો",
"HeaderMatch": "મેળ ખાતી શોધો",
"HeaderMetadataOrderOfPrecedence": "મેટાડેટા પ્રાધાન્યતાનો ક્રમ",
"HeaderMetadataToEmbed": "એમ્બેડ કરવા માટે મેટાડેટા",
"HeaderNewAccount": "નવું એકાઉન્ટ",
"HeaderNewLibrary": "નવી પુસ્તકાલય",
"HeaderNotifications": "સૂચનાઓ",
"HeaderOpenIDConnectAuthentication": "OpenID Connect Authentication",
"HeaderOpenRSSFeed": "Open RSS Feed",
"HeaderOtherFiles": "Other Files",
"HeaderOpenRSSFeed": "RSS ફીડ ખોલો",
"HeaderOtherFiles": "અન્ય ફાઇલો",
"HeaderPasswordAuthentication": "Password Authentication",
"HeaderPermissions": "Permissions",
"HeaderPlayerQueue": "Player Queue",
"HeaderPlaylist": "Playlist",
"HeaderPlaylistItems": "Playlist Items",
"HeaderPodcastsToAdd": "Podcasts to Add",
"HeaderPreviewCover": "Preview Cover",
"HeaderRemoveEpisode": "Remove Episode",
"HeaderRemoveEpisodes": "Remove {0} Episodes",
"HeaderRSSFeedGeneral": "RSS Details",
"HeaderRSSFeedIsOpen": "RSS Feed is Open",
"HeaderRSSFeeds": "RSS Feeds",
"HeaderSavedMediaProgress": "Saved Media Progress",
"HeaderSchedule": "Schedule",
"HeaderScheduleLibraryScans": "Schedule Automatic Library Scans",
"HeaderSession": "Session",
"HeaderSetBackupSchedule": "Set Backup Schedule",
"HeaderSettings": "Settings",
"HeaderSettingsDisplay": "Display",
"HeaderSettingsExperimental": "Experimental Features",
"HeaderSettingsGeneral": "General",
"HeaderSettingsScanner": "Scanner",
"HeaderSleepTimer": "Sleep Timer",
"HeaderStatsLargestItems": "Largest Items",
"HeaderStatsLongestItems": "Longest Items (hrs)",
"HeaderStatsMinutesListeningChart": "Minutes Listening (last 7 days)",
"HeaderStatsRecentSessions": "Recent Sessions",
"HeaderPermissions": "પરવાનગીઓ",
"HeaderPlayerQueue": "પ્લેયર કતાર",
"HeaderPlaylist": "પ્લેલિસ્ટ",
"HeaderPlaylistItems": "પ્લેલિસ્ટ ની વસ્તુઓ",
"HeaderPodcastsToAdd": "ઉમેરવા માટે પોડકાસ્ટ્સ",
"HeaderPreviewCover": "પૂર્વાવલોકન કવર",
"HeaderRemoveEpisode": "એપિસોડ કાઢી નાખો",
"HeaderRemoveEpisodes": "{0} એપિસોડ્સ કાઢી નાખો",
"HeaderRSSFeedGeneral": "સામાન્ય RSS ફીડ",
"HeaderRSSFeedIsOpen": "RSS ફીડ ખોલેલી છે",
"HeaderRSSFeeds": "RSS ફીડ્સ",
"HeaderSavedMediaProgress": "સાચવેલ મીડિયા પ્રગતિ",
"HeaderSchedule": "સમયપત્રક",
"HeaderScheduleLibraryScans": "પુસ્તકાલય સ્કેન સમયપત્રક",
"HeaderSession": "સેશન",
"HeaderSetBackupSchedule": "બેકઅપ સમયપત્રક સેટ કરો",
"HeaderSettings": "સેટિંગ્સ",
"HeaderSettingsDisplay": "ડિસ્પ્લે સેટિંગ્સ",
"HeaderSettingsExperimental": "પ્રયોગશીલ સેટિંગ્સ",
"HeaderSettingsGeneral": "સામાન્ય સેટિંગ્સ",
"HeaderSettingsScanner": "સ્કેનર સેટિંગ્સ",
"HeaderSleepTimer": "સ્લીપ ટાઈમર",
"HeaderStatsLargestItems": "સૌથી મોટી વસ્તુઓ",
"HeaderStatsLongestItems": "સૌથી લાંબી વસ્તુઓ (કલાક)",
"HeaderStatsMinutesListeningChart": "સાંભળવાની મિનિટ (છેલ્લા ૭ દિવસ)",
"HeaderStatsRecentSessions": "છેલ્લી સાંભળતી સેશન્સ",
"HeaderStatsTop10Authors": "Top 10 Authors",
"HeaderStatsTop5Genres": "Top 5 Genres",
"HeaderTableOfContents": "Table of Contents",
@@ -171,6 +184,7 @@
"HeaderUpdateDetails": "Update Details",
"HeaderUpdateLibrary": "Update Library",
"HeaderUsers": "Users",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Your Stats",
"LabelAbridged": "Abridged",
"LabelAccountType": "Account Type",
@@ -196,6 +210,8 @@
"LabelAuthorLastFirst": "Author (Last, First)",
"LabelAuthors": "Authors",
"LabelAutoDownloadEpisodes": "Auto Download Episodes",
"LabelAutoFetchMetadata": "Auto Fetch Metadata",
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
"LabelAutoLaunch": "Auto Launch",
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register",
@@ -266,6 +282,7 @@
"LabelExample": "Example",
"LabelExplicit": "Explicit",
"LabelFeedURL": "Feed URL",
"LabelFetchingMetadata": "Fetching Metadata",
"LabelFile": "File",
"LabelFileBirthtime": "File Birthtime",
"LabelFileModified": "File Modified",
@@ -275,8 +292,11 @@
"LabelFinished": "Finished",
"LabelFolder": "Folder",
"LabelFolders": "Folders",
"LabelFontBold": "Bold",
"LabelFontFamily": "ફોન્ટ કુટુંબ",
"LabelFontItalic": "Italic",
"LabelFontScale": "Font scale",
"LabelFontStrikethrough": "Strikethrough",
"LabelFormat": "Format",
"LabelGenre": "Genre",
"LabelGenres": "Genres",
@@ -336,7 +356,11 @@
"LabelMetaTags": "Meta Tags",
"LabelMinute": "Minute",
"LabelMissing": "Missing",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Missing Parts",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "More",
"LabelMoreInfo": "More Info",
"LabelName": "Name",
@@ -374,11 +398,13 @@
"LabelPermissionsDownload": "Can Download",
"LabelPermissionsUpdate": "Can Update",
"LabelPermissionsUpload": "Can Upload",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Photo Path/URL",
"LabelPlaylists": "Playlists",
"LabelPlayMethod": "Play Method",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts",
"LabelPodcastSearchRegion": "પોડકાસ્ટ શોધ પ્રદેશ",
"LabelPodcastType": "Podcast Type",
"LabelPort": "Port",
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
@@ -395,9 +421,11 @@
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRecommended": "Recommended",
"LabelRedo": "Redo",
"LabelRegion": "Region",
"LabelReleaseDate": "Release Date",
"LabelRemoveCover": "Remove cover",
"LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedOpen": "RSS Feed Open",
@@ -416,6 +444,7 @@
"LabelSeries": "Series",
"LabelSeriesName": "Series Name",
"LabelSeriesProgress": "Series Progress",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Set as primary",
"LabelSetEbookAsSupplementary": "Set as supplementary",
"LabelSettingsAudiobooksOnly": "Audiobooks only",
@@ -437,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
"LabelSettingsHomePageBookshelfView": "Home page use bookshelf view",
"LabelSettingsLibraryBookshelfView": "Library use bookshelf view",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Parse subtitles",
"LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by \" - \"<br>i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"",
"LabelSettingsPreferMatchedMetadata": "Prefer matched metadata",
@@ -482,6 +513,10 @@
"LabelTagsAccessibleToUser": "Tags Accessible to User",
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
"LabelTasks": "Tasks Running",
"LabelTextEditorBulletedList": "Bulleted list",
"LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "Numbered list",
"LabelTextEditorUnlink": "Unlink",
"LabelTheme": "Theme",
"LabelThemeDark": "Dark",
"LabelThemeLight": "Light",
@@ -507,6 +542,7 @@
"LabelTracksSingleTrack": "Single-track",
"LabelType": "Type",
"LabelUnabridged": "Unabridged",
"LabelUndo": "Undo",
"LabelUnknown": "Unknown",
"LabelUpdateCover": "Update Cover",
"LabelUpdateCoverHelp": "Allow overwriting of existing covers for the selected books when a match is located",
@@ -515,6 +551,7 @@
"LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located",
"LabelUploaderDragAndDrop": "Drag & drop files or folders",
"LabelUploaderDropFiles": "Drop files",
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
"LabelUseChapterTrack": "Use chapter track",
"LabelUseFullTrack": "Use full track",
"LabelUser": "User",
@@ -526,6 +563,8 @@
"LabelViewQueue": "View player queue",
"LabelVolume": "Volume",
"LabelWeekdaysToRun": "Weekdays to run",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Your audiobook duration",
"LabelYourBookmarks": "Your Bookmarks",
"LabelYourPlaylists": "Your Playlists",
@@ -562,6 +601,7 @@
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
@@ -641,6 +681,7 @@
"MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on",
"MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.",
"MessageSearchResultsFor": "Search results for",
"MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Server could not be reached",
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
"MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?",
@@ -656,7 +697,6 @@
"NoteChangeRootPassword": "Root user is the only user that can have an empty password",
"NoteChapterEditorTimes": "Note: First chapter start time must remain at 0:00 and the last chapter start time cannot exceed this audiobooks duration.",
"NoteFolderPicker": "Note: folders already mapped will not be shown",
"NoteFolderPickerDebian": "Note: Folder picker for the debian install is not fully implemented. You should enter the path to your library directly.",
"NoteRSSFeedPodcastAppsHttps": "Warning: Most podcast apps will require the RSS feed URL is using HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.",
"NoteUploaderFoldersWithMediaFiles": "Folders with media files will be handled as separate library items.",

View File

@@ -32,6 +32,8 @@
"ButtonHide": "छुपाएं",
"ButtonHome": "घर",
"ButtonIssues": "समस्याएं",
"ButtonJumpBackward": "Jump Backward",
"ButtonJumpForward": "Jump Forward",
"ButtonLatest": "नवीनतम",
"ButtonLibrary": "पुस्तकालय",
"ButtonLogout": "लॉग आउट",
@@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "सभी लेखकों को तलाश करें",
"ButtonMatchBooks": "संबंधित पुस्तकों का मिलान करें",
"ButtonNevermind": "कोई बात नहीं",
"ButtonNext": "Next",
"ButtonNextChapter": "Next Chapter",
"ButtonOk": "ठीक है",
"ButtonOpenFeed": "फ़ीड खोलें",
"ButtonOpenManager": "मैनेजर खोलें",
"ButtonPause": "Pause",
"ButtonPlay": "चलाएँ",
"ButtonPlaying": "चल रही है",
"ButtonPlaylists": "प्लेलिस्ट्स",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "सभी Cache मिटाएं",
"ButtonPurgeItemsCache": "आइटम Cache मिटाएं",
"ButtonPurgeMediaProgress": "अभी तक सुना हुआ सब हटा दे",
@@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "कतार से हटाएं",
"ButtonQuickMatch": "जल्दी से समानता की तलाश करें",
"ButtonRead": "पढ़ लिया",
"ButtonRefresh": "Refresh",
"ButtonRemove": "हटाएं",
"ButtonRemoveAll": "सभी हटाएं",
"ButtonRemoveAllLibraryItems": "पुस्तकालय की सभी आइटम हटाएं",
@@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "फ़ोल्डर का पथ चुनें",
"ButtonSeries": "सीरीज",
"ButtonSetChaptersFromTracks": "ट्रैक्स से अध्याय बनाएं",
"ButtonShare": "Share",
"ButtonShiftTimes": "समय खिसकाए",
"ButtonShow": "दिखाएं",
"ButtonStartM4BEncode": "M4B एन्कोडिंग शुरू करें",
@@ -87,6 +96,9 @@
"ButtonUserEdit": "उपयोगकर्ता {0} को संपादित करें",
"ButtonViewAll": "सभी को देखें",
"ButtonYes": "हाँ",
"ErrorUploadFetchMetadataAPI": "Error fetching metadata",
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
"ErrorUploadLacksTitle": "Must have a title",
"HeaderAccount": "खाता",
"HeaderAdvanced": "विकसित",
"HeaderAppriseNotificationSettings": "Apprise अधिसूचना सेटिंग्स",
@@ -101,6 +113,7 @@
"HeaderCollectionItems": "Collection Items",
"HeaderCover": "Cover",
"HeaderCurrentDownloads": "Current Downloads",
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "Details",
"HeaderDownloadQueue": "Download Queue",
"HeaderEbookFiles": "Ebook Files",
@@ -171,6 +184,7 @@
"HeaderUpdateDetails": "Update Details",
"HeaderUpdateLibrary": "Update Library",
"HeaderUsers": "Users",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Your Stats",
"LabelAbridged": "Abridged",
"LabelAccountType": "Account Type",
@@ -196,6 +210,8 @@
"LabelAuthorLastFirst": "Author (Last, First)",
"LabelAuthors": "Authors",
"LabelAutoDownloadEpisodes": "Auto Download Episodes",
"LabelAutoFetchMetadata": "Auto Fetch Metadata",
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
"LabelAutoLaunch": "Auto Launch",
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register",
@@ -266,6 +282,7 @@
"LabelExample": "Example",
"LabelExplicit": "Explicit",
"LabelFeedURL": "Feed URL",
"LabelFetchingMetadata": "Fetching Metadata",
"LabelFile": "File",
"LabelFileBirthtime": "File Birthtime",
"LabelFileModified": "File Modified",
@@ -275,8 +292,11 @@
"LabelFinished": "Finished",
"LabelFolder": "Folder",
"LabelFolders": "Folders",
"LabelFontBold": "Bold",
"LabelFontFamily": "फुहारा परिवार",
"LabelFontItalic": "Italic",
"LabelFontScale": "Font scale",
"LabelFontStrikethrough": "Strikethrough",
"LabelFormat": "Format",
"LabelGenre": "Genre",
"LabelGenres": "Genres",
@@ -336,7 +356,11 @@
"LabelMetaTags": "Meta Tags",
"LabelMinute": "Minute",
"LabelMissing": "Missing",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Missing Parts",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "More",
"LabelMoreInfo": "More Info",
"LabelName": "Name",
@@ -374,11 +398,13 @@
"LabelPermissionsDownload": "Can Download",
"LabelPermissionsUpdate": "Can Update",
"LabelPermissionsUpload": "Can Upload",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Photo Path/URL",
"LabelPlaylists": "Playlists",
"LabelPlayMethod": "Play Method",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts",
"LabelPodcastSearchRegion": "पॉडकास्ट खोज क्षेत्र",
"LabelPodcastType": "Podcast Type",
"LabelPort": "Port",
"LabelPrefixesToIgnore": "Prefixes to Ignore (case insensitive)",
@@ -395,9 +421,11 @@
"LabelRecentlyAdded": "Recently Added",
"LabelRecentSeries": "Recent Series",
"LabelRecommended": "Recommended",
"LabelRedo": "Redo",
"LabelRegion": "Region",
"LabelReleaseDate": "Release Date",
"LabelRemoveCover": "Remove cover",
"LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedOpen": "RSS Feed Open",
@@ -416,6 +444,7 @@
"LabelSeries": "Series",
"LabelSeriesName": "Series Name",
"LabelSeriesProgress": "Series Progress",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Set as primary",
"LabelSetEbookAsSupplementary": "Set as supplementary",
"LabelSettingsAudiobooksOnly": "Audiobooks only",
@@ -437,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
"LabelSettingsHomePageBookshelfView": "Home page use bookshelf view",
"LabelSettingsLibraryBookshelfView": "Library use bookshelf view",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Parse subtitles",
"LabelSettingsParseSubtitlesHelp": "Extract subtitles from audiobook folder names.<br>Subtitle must be seperated by \" - \"<br>i.e. \"Book Title - A Subtitle Here\" has the subtitle \"A Subtitle Here\"",
"LabelSettingsPreferMatchedMetadata": "Prefer matched metadata",
@@ -482,6 +513,10 @@
"LabelTagsAccessibleToUser": "Tags Accessible to User",
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
"LabelTasks": "Tasks Running",
"LabelTextEditorBulletedList": "Bulleted list",
"LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "Numbered list",
"LabelTextEditorUnlink": "Unlink",
"LabelTheme": "Theme",
"LabelThemeDark": "Dark",
"LabelThemeLight": "Light",
@@ -507,6 +542,7 @@
"LabelTracksSingleTrack": "Single-track",
"LabelType": "Type",
"LabelUnabridged": "Unabridged",
"LabelUndo": "Undo",
"LabelUnknown": "Unknown",
"LabelUpdateCover": "Update Cover",
"LabelUpdateCoverHelp": "Allow overwriting of existing covers for the selected books when a match is located",
@@ -515,6 +551,7 @@
"LabelUpdateDetailsHelp": "Allow overwriting of existing details for the selected books when a match is located",
"LabelUploaderDragAndDrop": "Drag & drop files or folders",
"LabelUploaderDropFiles": "Drop files",
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
"LabelUseChapterTrack": "Use chapter track",
"LabelUseFullTrack": "Use full track",
"LabelUser": "User",
@@ -526,6 +563,8 @@
"LabelViewQueue": "View player queue",
"LabelVolume": "Volume",
"LabelWeekdaysToRun": "Weekdays to run",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Your audiobook duration",
"LabelYourBookmarks": "Your Bookmarks",
"LabelYourPlaylists": "Your Playlists",
@@ -562,6 +601,7 @@
"MessageConfirmRemoveCollection": "Are you sure you want to remove collection \"{0}\"?",
"MessageConfirmRemoveEpisode": "Are you sure you want to remove episode \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Are you sure you want to remove {0} episodes?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
@@ -641,6 +681,7 @@
"MessageRestoreBackupConfirm": "Are you sure you want to restore the backup created on",
"MessageRestoreBackupWarning": "Restoring a backup will overwrite the entire database located at /config and cover images in /metadata/items & /metadata/authors.<br /><br />Backups do not modify any files in your library folders. If you have enabled server settings to store cover art and metadata in your library folders then those are not backed up or overwritten.<br /><br />All clients using your server will be automatically refreshed.",
"MessageSearchResultsFor": "Search results for",
"MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Server could not be reached",
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
"MessageStartPlaybackAtTime": "Start playback for \"{0}\" at {1}?",
@@ -656,7 +697,6 @@
"NoteChangeRootPassword": "रूट user is the only user that can have an empty password",
"NoteChapterEditorTimes": "Note: First chapter start time must remain at 0:00 and the last chapter start time cannot exceed this audiobooks duration.",
"NoteFolderPicker": "Note: folders already mapped will not be shown",
"NoteFolderPickerDebian": "Note: Folder picker for the debian install is not fully implemented. You should enter the path to your library directly.",
"NoteRSSFeedPodcastAppsHttps": "Warning: Most podcast apps will require the RSS feed URL is using HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Warning: 1 or more of your episodes do not have a Pub Date. Some podcast apps require this.",
"NoteUploaderFoldersWithMediaFiles": "Folders with media files will be handled as separate library items.",

View File

@@ -32,6 +32,8 @@
"ButtonHide": "Sakrij",
"ButtonHome": "Početna stranica",
"ButtonIssues": "Problemi",
"ButtonJumpBackward": "Jump Backward",
"ButtonJumpForward": "Jump Forward",
"ButtonLatest": "Najnovije",
"ButtonLibrary": "Biblioteka",
"ButtonLogout": "Odjavi se",
@@ -41,12 +43,17 @@
"ButtonMatchAllAuthors": "Matchaj sve autore",
"ButtonMatchBooks": "Matchaj knjige",
"ButtonNevermind": "Nije bitno",
"ButtonNext": "Next",
"ButtonNextChapter": "Next Chapter",
"ButtonOk": "Ok",
"ButtonOpenFeed": "Otvori feed",
"ButtonOpenManager": "Otvori menadžera",
"ButtonPause": "Pause",
"ButtonPlay": "Pokreni",
"ButtonPlaying": "Playing",
"ButtonPlaylists": "Playlists",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Previous Chapter",
"ButtonPurgeAllCache": "Isprazni sav cache",
"ButtonPurgeItemsCache": "Isprazni Items Cache",
"ButtonPurgeMediaProgress": "Purge Media Progress",
@@ -54,6 +61,7 @@
"ButtonQueueRemoveItem": "Remove from queue",
"ButtonQuickMatch": "Brzi match",
"ButtonRead": "Pročitaj",
"ButtonRefresh": "Refresh",
"ButtonRemove": "Ukloni",
"ButtonRemoveAll": "Ukloni sve",
"ButtonRemoveAllLibraryItems": "Ukloni sve stvari iz biblioteke",
@@ -73,6 +81,7 @@
"ButtonSelectFolderPath": "Odaberi putanju do folder",
"ButtonSeries": "Serije",
"ButtonSetChaptersFromTracks": "Set chapters from tracks",
"ButtonShare": "Share",
"ButtonShiftTimes": "Pomakni vremena",
"ButtonShow": "Prikaži",
"ButtonStartM4BEncode": "Pokreni M4B kodiranje",
@@ -87,6 +96,9 @@
"ButtonUserEdit": "Edit user {0}",
"ButtonViewAll": "Prikaži sve",
"ButtonYes": "Da",
"ErrorUploadFetchMetadataAPI": "Error fetching metadata",
"ErrorUploadFetchMetadataNoResults": "Could not fetch metadata - try updating title and/or author",
"ErrorUploadLacksTitle": "Must have a title",
"HeaderAccount": "Korisnički račun",
"HeaderAdvanced": "Napredno",
"HeaderAppriseNotificationSettings": "Apprise Notification Settings",
@@ -101,6 +113,7 @@
"HeaderCollectionItems": "Stvari u kolekciji",
"HeaderCover": "Cover",
"HeaderCurrentDownloads": "Current Downloads",
"HeaderCustomMetadataProviders": "Custom Metadata Providers",
"HeaderDetails": "Detalji",
"HeaderDownloadQueue": "Download Queue",
"HeaderEbookFiles": "Ebook Files",
@@ -171,6 +184,7 @@
"HeaderUpdateDetails": "Aktualiziraj detalje",
"HeaderUpdateLibrary": "Aktualiziraj biblioteku",
"HeaderUsers": "Korinici",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Tvoja statistika",
"LabelAbridged": "Abridged",
"LabelAccountType": "Vrsta korisničkog računa",
@@ -196,6 +210,8 @@
"LabelAuthorLastFirst": "Author (Last, First)",
"LabelAuthors": "Autori",
"LabelAutoDownloadEpisodes": "Automatski preuzmi epizode",
"LabelAutoFetchMetadata": "Auto Fetch Metadata",
"LabelAutoFetchMetadataHelp": "Fetches metadata for title, author, and series to streamline uploading. Additional metadata may have to be matched after upload.",
"LabelAutoLaunch": "Auto Launch",
"LabelAutoLaunchDescription": "Redirect to the auth provider automatically when navigating to the login page (manual override path <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Auto Register",
@@ -266,6 +282,7 @@
"LabelExample": "Example",
"LabelExplicit": "Explicit",
"LabelFeedURL": "Feed URL",
"LabelFetchingMetadata": "Fetching Metadata",
"LabelFile": "Datoteka",
"LabelFileBirthtime": "File Birthtime",
"LabelFileModified": "File Modified",
@@ -275,8 +292,11 @@
"LabelFinished": "Finished",
"LabelFolder": "Folder",
"LabelFolders": "Folderi",
"LabelFontBold": "Bold",
"LabelFontFamily": "Font family",
"LabelFontItalic": "Italic",
"LabelFontScale": "Font scale",
"LabelFontStrikethrough": "Strikethrough",
"LabelFormat": "Format",
"LabelGenre": "Genre",
"LabelGenres": "Žanrovi",
@@ -336,7 +356,11 @@
"LabelMetaTags": "Meta Tags",
"LabelMinute": "Minuta",
"LabelMissing": "Nedostaje",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Nedostajali dijelovi",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Allowed Mobile Redirect URIs",
"LabelMobileRedirectURIsDescription": "This is a whitelist of valid redirect URIs for mobile apps. The default one is <code>audiobookshelf://oauth</code>, which you can remove or supplement with additional URIs for third-party app integration. Using an asterisk (<code>*</code>) as the sole entry permits any URI.",
"LabelMore": "Više",
"LabelMoreInfo": "More Info",
"LabelName": "Ime",
@@ -374,11 +398,13 @@
"LabelPermissionsDownload": "Smije preuzimati",
"LabelPermissionsUpdate": "Smije aktualizirati",
"LabelPermissionsUpload": "Smije uploadati",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Slika putanja/URL",
"LabelPlaylists": "Playlists",
"LabelPlayMethod": "Vrsta reprodukcije",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcasts",
"LabelPodcastSearchRegion": "Područje pretrage podcasta",
"LabelPodcastType": "Podcast Type",
"LabelPort": "Port",
"LabelPrefixesToIgnore": "Prefiksi za ignorirati (mala i velika slova nisu bitna)",
@@ -395,9 +421,11 @@
"LabelRecentlyAdded": "Nedavno dodano",
"LabelRecentSeries": "Nedavne serije",
"LabelRecommended": "Recommended",
"LabelRedo": "Redo",
"LabelRegion": "Regija",
"LabelReleaseDate": "Datum izlaska",
"LabelRemoveCover": "Remove cover",
"LabelRowsPerPage": "Rows per page",
"LabelRSSFeedCustomOwnerEmail": "Custom owner Email",
"LabelRSSFeedCustomOwnerName": "Custom owner Name",
"LabelRSSFeedOpen": "RSS Feed Open",
@@ -416,6 +444,7 @@
"LabelSeries": "Serije",
"LabelSeriesName": "Ime serije",
"LabelSeriesProgress": "Series Progress",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Set as primary",
"LabelSetEbookAsSupplementary": "Set as supplementary",
"LabelSettingsAudiobooksOnly": "Audiobooks only",
@@ -437,6 +466,8 @@
"LabelSettingsHideSingleBookSeriesHelp": "Series that have a single book will be hidden from the series page and home page shelves.",
"LabelSettingsHomePageBookshelfView": "Koristi bookshelf pogled za početnu stranicu",
"LabelSettingsLibraryBookshelfView": "Koristi bookshelf pogled za biblioteku",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Parsaj podnapise",
"LabelSettingsParseSubtitlesHelp": "Izvadi podnapise iz imena od audiobook foldera.<br>Podnapis mora biti odvojen sa \" - \"<br>npr. \"Ime knjige - Podnapis ovdje\" ima podnapis \"Podnapis ovdje\"",
"LabelSettingsPreferMatchedMetadata": "Preferiraj matchane metapodatke",
@@ -482,6 +513,10 @@
"LabelTagsAccessibleToUser": "Tags dostupni korisniku",
"LabelTagsNotAccessibleToUser": "Tags not Accessible to User",
"LabelTasks": "Tasks Running",
"LabelTextEditorBulletedList": "Bulleted list",
"LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "Numbered list",
"LabelTextEditorUnlink": "Unlink",
"LabelTheme": "Theme",
"LabelThemeDark": "Dark",
"LabelThemeLight": "Light",
@@ -507,6 +542,7 @@
"LabelTracksSingleTrack": "Single-track",
"LabelType": "Tip",
"LabelUnabridged": "Unabridged",
"LabelUndo": "Undo",
"LabelUnknown": "Nepoznato",
"LabelUpdateCover": "Aktualiziraj Cover",
"LabelUpdateCoverHelp": "Dozvoli postavljanje novog covera za odabrane knjige nakon što je match pronađen.",
@@ -515,6 +551,7 @@
"LabelUpdateDetailsHelp": "Dozvoli postavljanje novih detalja za odabrane knjige nakon što je match pronađen",
"LabelUploaderDragAndDrop": "Drag & Drop datoteke ili foldere",
"LabelUploaderDropFiles": "Ubaci datoteke",
"LabelUploaderItemFetchMetadataHelp": "Automatically fetch title, author, and series",
"LabelUseChapterTrack": "Koristi poglavlja track",
"LabelUseFullTrack": "Koristi cijeli track",
"LabelUser": "Korisnik",
@@ -526,6 +563,8 @@
"LabelViewQueue": "View player queue",
"LabelVolume": "Volume",
"LabelWeekdaysToRun": "Radnih dana da radi",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Tvoje trajanje audiobooka",
"LabelYourBookmarks": "Tvoje knjižne oznake",
"LabelYourPlaylists": "Your Playlists",
@@ -562,6 +601,7 @@
"MessageConfirmRemoveCollection": "AJeste li sigurni da želite obrisati kolekciju \"{0}\"?",
"MessageConfirmRemoveEpisode": "Jeste li sigurni da želite obrisati epizodu \"{0}\"?",
"MessageConfirmRemoveEpisodes": "Jeste li sigurni da želite obrisati {0} epizoda/-u?",
"MessageConfirmRemoveListeningSessions": "Are you sure you want to remove {0} listening sessions?",
"MessageConfirmRemoveNarrator": "Are you sure you want to remove narrator \"{0}\"?",
"MessageConfirmRemovePlaylist": "Are you sure you want to remove your playlist \"{0}\"?",
"MessageConfirmRenameGenre": "Are you sure you want to rename genre \"{0}\" to \"{1}\" for all items?",
@@ -641,6 +681,7 @@
"MessageRestoreBackupConfirm": "Jeste li sigurni da želite povratiti backup kreiran",
"MessageRestoreBackupWarning": "Povračanje backupa će zamijeniti postoječu bazu podataka u /config i slike covera u /metadata/items i /metadata/authors.<br /><br />Backups ne modificiraju nikakve datoteke u folderu od biblioteke. Ako imate uključene server postavke da spremate cover i metapodtake u folderu od biblioteke, onda oni neće biti backupani ili overwritten.<br /><br />Svi klijenti koji koriste tvoj server će biti automatski osvježeni.",
"MessageSearchResultsFor": "Traži rezultate za",
"MessageSelected": "{0} selected",
"MessageServerCouldNotBeReached": "Server ne može biti kontaktiran",
"MessageSetChaptersFromTracksDescription": "Set chapters using each audio file as a chapter and chapter title as the audio file name",
"MessageStartPlaybackAtTime": "Pokreni reprodukciju za \"{0}\" na {1}?",
@@ -656,7 +697,6 @@
"NoteChangeRootPassword": "Root korisnik je jedini korisnik koji može imati praznu lozinku",
"NoteChapterEditorTimes": "Bilješka: Prvo početno vrijeme poglavlja mora ostati na 0:00 i posljednje vrijeme poglavlja ne smije preći vrijeme trajanja ove audio knjige.",
"NoteFolderPicker": "Bilješka: več mapirani folderi neće biti prikazani",
"NoteFolderPickerDebian": "Bilješka: Folder picker za debian instalaciju nije potpuno implementiran. Trebate unjeti direktnu putanju do biblioteke.",
"NoteRSSFeedPodcastAppsHttps": "Upozorenje: Večina podcasta će trebati RSS feed URL koji koristi HTTPS",
"NoteRSSFeedPodcastAppsPubDate": "Upozorenje: 1 ili više vaših epizoda nemaju datum objavljivanja. Neke podcast aplikacije zahtjevaju to.",
"NoteUploaderFoldersWithMediaFiles": "Folderi sa media datotekama će biti tretirane kao odvojene stavke u biblioteki.",

781
client/strings/hu.json Normal file
View File

@@ -0,0 +1,781 @@
{
"ButtonAdd": "Hozzáadás",
"ButtonAddChapters": "Fejezetek hozzáadása",
"ButtonAddDevice": "Eszköz hozzáadása",
"ButtonAddLibrary": "Könyvtár hozzáadása",
"ButtonAddPodcasts": "Podcastok hozzáadása",
"ButtonAddUser": "Felhasználó hozzáadása",
"ButtonAddYourFirstLibrary": "Az első könyvtár hozzáadása",
"ButtonApply": "Alkalmaz",
"ButtonApplyChapters": "Fejezetek alkalmazása",
"ButtonAuthors": "Szerzők",
"ButtonBrowseForFolder": "Mappa keresése",
"ButtonCancel": "Mégse",
"ButtonCancelEncode": "Kódolás megszakítása",
"ButtonChangeRootPassword": "Gyökérjelszó megváltoztatása",
"ButtonCheckAndDownloadNewEpisodes": "Új epizódok ellenőrzése és letöltése",
"ButtonChooseAFolder": "Válassz egy mappát",
"ButtonChooseFiles": "Fájlok kiválasztása",
"ButtonClearFilter": "Szűrő törlése",
"ButtonCloseFeed": "Hírcsatorna bezárása",
"ButtonCollections": "Gyűjtemény",
"ButtonConfigureScanner": "Szkenner konfigurálása",
"ButtonCreate": "Létrehozás",
"ButtonCreateBackup": "Biztonsági másolat készítése",
"ButtonDelete": "Törlés",
"ButtonDownloadQueue": "Sor",
"ButtonEdit": "Szerkesztés",
"ButtonEditChapters": "Fejezetek szerkesztése",
"ButtonEditPodcast": "Podcast szerkesztése",
"ButtonForceReScan": "Újraszkennelés kényszerítése",
"ButtonFullPath": "Teljes útvonal",
"ButtonHide": "Elrejtés",
"ButtonHome": "Kezdőlap",
"ButtonIssues": "Problémák",
"ButtonJumpBackward": "Ugrás vissza",
"ButtonJumpForward": "Ugrás előre",
"ButtonLatest": "Legújabb",
"ButtonLibrary": "Könyvtár",
"ButtonLogout": "Kijelentkezés",
"ButtonLookup": "Keresés",
"ButtonManageTracks": "Sávok kezelése",
"ButtonMapChapterTitles": "Fejezetcímek hozzárendelése",
"ButtonMatchAllAuthors": "Minden szerző egyeztetése",
"ButtonMatchBooks": "Könyvek egyeztetése",
"ButtonNevermind": "Mindegy",
"ButtonNext": "Next",
"ButtonNextChapter": "Következő fejezet",
"ButtonOk": "Oké",
"ButtonOpenFeed": "Hírcsatorna megnyitása",
"ButtonOpenManager": "Kezelő megnyitása",
"ButtonPause": "Szünet",
"ButtonPlay": "Lejátszás",
"ButtonPlaying": "Lejátszás folyamatban",
"ButtonPlaylists": "Lejátszási listák",
"ButtonPrevious": "Previous",
"ButtonPreviousChapter": "Előző fejezet",
"ButtonPurgeAllCache": "Összes gyorsítótár törlése",
"ButtonPurgeItemsCache": "Elemek gyorsítótárának törlése",
"ButtonPurgeMediaProgress": "Médialejátszás állapotának törlése",
"ButtonQueueAddItem": "Hozzáadás a sorhoz",
"ButtonQueueRemoveItem": "Eltávolítás a sorból",
"ButtonQuickMatch": "Gyors egyeztetés",
"ButtonRead": "Olvasás",
"ButtonRefresh": "Refresh",
"ButtonRemove": "Eltávolítás",
"ButtonRemoveAll": "Összes eltávolítása",
"ButtonRemoveAllLibraryItems": "Összes könyvtárelem eltávolítása",
"ButtonRemoveFromContinueListening": "Eltávolítás a Folytatás hallgatásából",
"ButtonRemoveFromContinueReading": "Eltávolítás a Folytatás olvasásából",
"ButtonRemoveSeriesFromContinueSeries": "Sorozat eltávolítása a Folytatás sorozatokból",
"ButtonReScan": "Újraszkennelés",
"ButtonReset": "Visszaállítás",
"ButtonResetToDefault": "Alapértelmezésre állítás",
"ButtonRestore": "Visszaállítás",
"ButtonSave": "Mentés",
"ButtonSaveAndClose": "Mentés és bezárás",
"ButtonSaveTracklist": "Sávlista mentése",
"ButtonScan": "Szkennelés",
"ButtonScanLibrary": "Könyvtár szkennelése",
"ButtonSearch": "Keresés",
"ButtonSelectFolderPath": "Mappa útvonalának kiválasztása",
"ButtonSeries": "Sorozatok",
"ButtonSetChaptersFromTracks": "Fejezetek beállítása sávokból",
"ButtonShare": "Share",
"ButtonShiftTimes": "Idők eltolása",
"ButtonShow": "Megjelenítés",
"ButtonStartM4BEncode": "M4B kódolás indítása",
"ButtonStartMetadataEmbed": "Metaadatok beágyazásának indítása",
"ButtonSubmit": "Beküldés",
"ButtonTest": "Teszt",
"ButtonUpload": "Feltöltés",
"ButtonUploadBackup": "Biztonsági másolat feltöltése",
"ButtonUploadCover": "Borító feltöltése",
"ButtonUploadOPMLFile": "OPML fájl feltöltése",
"ButtonUserDelete": "Felhasználó törlése {0}",
"ButtonUserEdit": "Felhasználó szerkesztése {0}",
"ButtonViewAll": "Összes megtekintése",
"ButtonYes": "Igen",
"ErrorUploadFetchMetadataAPI": "Hiba a metaadatok lekérésekor",
"ErrorUploadFetchMetadataNoResults": "Nem sikerült a metaadatok lekérése - próbálja meg frissíteni a címet és/vagy a szerzőt",
"ErrorUploadLacksTitle": "Cím szükséges",
"HeaderAccount": "Fiók",
"HeaderAdvanced": "Haladó",
"HeaderAppriseNotificationSettings": "Apprise értesítési beállítások",
"HeaderAudiobookTools": "Hangoskönyv fájlkezelő eszközök",
"HeaderAudioTracks": "Audiósávok",
"HeaderAuthentication": "Hitelesítés",
"HeaderBackups": "Biztonsági másolatok",
"HeaderChangePassword": "Jelszó megváltoztatása",
"HeaderChapters": "Fejezetek",
"HeaderChooseAFolder": "Válasszon egy mappát",
"HeaderCollection": "Gyűjtemény",
"HeaderCollectionItems": "Gyűjtemény elemek",
"HeaderCover": "Borító",
"HeaderCurrentDownloads": "Jelenlegi letöltések",
"HeaderCustomMetadataProviders": "Egyéni metaadat-szolgáltatók",
"HeaderDetails": "Részletek",
"HeaderDownloadQueue": "Letöltési sor",
"HeaderEbookFiles": "E-könyv fájlok",
"HeaderEmail": "E-mail",
"HeaderEmailSettings": "E-mail beállítások",
"HeaderEpisodes": "Epizódok",
"HeaderEreaderDevices": "E-olvasó eszközök",
"HeaderEreaderSettings": "E-olvasó beállítások",
"HeaderFiles": "Fájlok",
"HeaderFindChapters": "Fejezetek keresése",
"HeaderIgnoredFiles": "Figyelmen kívül hagyott fájlok",
"HeaderItemFiles": "Elemfájlok",
"HeaderItemMetadataUtils": "Elem metaadat eszközök",
"HeaderLastListeningSession": "Utolsó hallgatási munkamenet",
"HeaderLatestEpisodes": "Legújabb epizódok",
"HeaderLibraries": "Könyvtárak",
"HeaderLibraryFiles": "Könyvtárfájlok",
"HeaderLibraryStats": "Könyvtár statisztikák",
"HeaderListeningSessions": "Hallgatási munkamenetek",
"HeaderListeningStats": "Hallgatási statisztikák",
"HeaderLogin": "Bejelentkezés",
"HeaderLogs": "Naplók",
"HeaderManageGenres": "Műfajok kezelése",
"HeaderManageTags": "Címkék kezelése",
"HeaderMapDetails": "Részletek hozzárendelése",
"HeaderMatch": "Egyeztetés",
"HeaderMetadataOrderOfPrecedence": "Metaadatok előnyben részesítési sorrendje",
"HeaderMetadataToEmbed": "Beágyazandó metaadatok",
"HeaderNewAccount": "Új fiók",
"HeaderNewLibrary": "Új könyvtár",
"HeaderNotifications": "Értesítések",
"HeaderOpenIDConnectAuthentication": "OpenID Connect hitelesítés",
"HeaderOpenRSSFeed": "RSS hírcsatorna megnyitása",
"HeaderOtherFiles": "Egyéb fájlok",
"HeaderPasswordAuthentication": "Jelszó hitelesítés",
"HeaderPermissions": "Engedélyek",
"HeaderPlayerQueue": "Lejátszó sor",
"HeaderPlaylist": "Lejátszási lista",
"HeaderPlaylistItems": "Lejátszási lista elemek",
"HeaderPodcastsToAdd": "Hozzáadandó podcastok",
"HeaderPreviewCover": "Borító előnézete",
"HeaderRemoveEpisode": "Epizód eltávolítása",
"HeaderRemoveEpisodes": "{0} epizód eltávolítása",
"HeaderRSSFeedGeneral": "RSS részletek",
"HeaderRSSFeedIsOpen": "RSS hírcsatorna nyitva",
"HeaderRSSFeeds": "RSS hírcsatornák",
"HeaderSavedMediaProgress": "Mentett médialejátszási állapot",
"HeaderSchedule": "Ütemezés",
"HeaderScheduleLibraryScans": "Könyvtárak automatikus szkennelésének ütemezése",
"HeaderSession": "Munkamenet",
"HeaderSetBackupSchedule": "Biztonsági másolatok ütemezésének beállítása",
"HeaderSettings": "Beállítások",
"HeaderSettingsDisplay": "Kijelző",
"HeaderSettingsExperimental": "Kísérleti funkciók",
"HeaderSettingsGeneral": "Általános",
"HeaderSettingsScanner": "Szkenner",
"HeaderSleepTimer": "Alvásidőzítő",
"HeaderStatsLargestItems": "Legnagyobb elemek",
"HeaderStatsLongestItems": "Leghosszabb elemek (órákban)",
"HeaderStatsMinutesListeningChart": "Hallgatási percek (az utolsó 7 napban)",
"HeaderStatsRecentSessions": "Legutóbbi munkamenetek",
"HeaderStatsTop10Authors": "Top 10 szerzők",
"HeaderStatsTop5Genres": "Top 5 műfajok",
"HeaderTableOfContents": "Tartalomjegyzék",
"HeaderTools": "Eszközök",
"HeaderUpdateAccount": "Fiók frissítése",
"HeaderUpdateAuthor": "Szerző frissítése",
"HeaderUpdateDetails": "Részletek frissítése",
"HeaderUpdateLibrary": "Könyvtár frissítése",
"HeaderUsers": "Felhasználók",
"HeaderYearReview": "Year {0} in Review",
"HeaderYourStats": "Saját statisztikák",
"LabelAbridged": "Tömörített",
"LabelAccountType": "Fióktípus",
"LabelAccountTypeAdmin": "Admin",
"LabelAccountTypeGuest": "Vendég",
"LabelAccountTypeUser": "Felhasználó",
"LabelActivity": "Tevékenység",
"LabelAdded": "Hozzáadva",
"LabelAddedAt": "Hozzáadás ideje",
"LabelAddToCollection": "Hozzáadás a gyűjteményhez",
"LabelAddToCollectionBatch": "{0} könyv hozzáadása a gyűjteményhez",
"LabelAddToPlaylist": "Hozzáadás a lejátszási listához",
"LabelAddToPlaylistBatch": "{0} elem hozzáadása a lejátszási listához",
"LabelAdminUsersOnly": "Csak admin felhasználók",
"LabelAll": "Minden",
"LabelAllUsers": "Minden felhasználó",
"LabelAllUsersExcludingGuests": "Minden felhasználó, vendégek kivételével",
"LabelAllUsersIncludingGuests": "Minden felhasználó, beleértve a vendégeket is",
"LabelAlreadyInYourLibrary": "Már a könyvtárában van",
"LabelAppend": "Hozzáfűzés",
"LabelAuthor": "Szerző",
"LabelAuthorFirstLast": "Szerző (Keresztnév Vezetéknév)",
"LabelAuthorLastFirst": "Szerző (Vezetéknév, Keresztnév)",
"LabelAuthors": "Szerzők",
"LabelAutoDownloadEpisodes": "Epizódok automatikus letöltése",
"LabelAutoFetchMetadata": "Metaadatok automatikus lekérése",
"LabelAutoFetchMetadataHelp": "Cím, szerző és sorozat metaadatok automatikus lekérése a feltöltés megkönnyítése érdekében. További metaadatok egyeztetése szükséges lehet a feltöltés után.",
"LabelAutoLaunch": "Automatikus indítás",
"LabelAutoLaunchDescription": "Automatikus átirányítás az hitelesítő szolgáltatóhoz a bejelentkezési oldalra navigáláskor (kézi felülbírálás útvonala <code>/login?autoLaunch=0</code>)",
"LabelAutoRegister": "Automatikus regisztráció",
"LabelAutoRegisterDescription": "Új felhasználók automatikus létrehozása bejelentkezés után",
"LabelBackToUser": "Vissza a felhasználóhoz",
"LabelBackupLocation": "Biztonsági másolat helye",
"LabelBackupsEnableAutomaticBackups": "Automatikus biztonsági másolatok engedélyezése",
"LabelBackupsEnableAutomaticBackupsHelp": "Biztonsági másolatok mentése a /metadata/backups mappába",
"LabelBackupsMaxBackupSize": "Maximális biztonsági másolat méret (GB-ban)",
"LabelBackupsMaxBackupSizeHelp": "A rossz konfiguráció elleni védelem érdekében a biztonsági másolatok meghiúsulnak, ha meghaladják a beállított méretet.",
"LabelBackupsNumberToKeep": "Megtartandó biztonsági másolatok száma",
"LabelBackupsNumberToKeepHelp": "Egyszerre csak 1 biztonsági másolat kerül eltávolításra, tehát ha már több biztonsági másolat van, mint ez a szám, akkor manuálisan kell eltávolítani őket.",
"LabelBitrate": "Bitráta",
"LabelBooks": "Könyvek",
"LabelButtonText": "Gomb szövege",
"LabelChangePassword": "Jelszó megváltoztatása",
"LabelChannels": "Csatornák",
"LabelChapters": "Fejezetek",
"LabelChaptersFound": "fejezet található",
"LabelChapterTitle": "Fejezet címe",
"LabelClickForMoreInfo": "További információkért kattintson",
"LabelClosePlayer": "Lejátszó bezárása",
"LabelCodec": "Kodek",
"LabelCollapseSeries": "Sorozat összecsukása",
"LabelCollection": "Gyűjtemény",
"LabelCollections": "Gyűjtemények",
"LabelComplete": "Teljes",
"LabelConfirmPassword": "Jelszó megerősítése",
"LabelContinueListening": "Hallgatás folytatása",
"LabelContinueReading": "Olvasás folytatása",
"LabelContinueSeries": "Sorozat folytatása",
"LabelCover": "Borító",
"LabelCoverImageURL": "Borítókép URL",
"LabelCreatedAt": "Létrehozás ideje",
"LabelCronExpression": "Cron kifejezés",
"LabelCurrent": "Jelenlegi",
"LabelCurrently": "Jelenleg:",
"LabelCustomCronExpression": "Egyéni Cron kifejezés:",
"LabelDatetime": "Dátumidő",
"LabelDeleteFromFileSystemCheckbox": "Törlés a fájlrendszerről (ne jelölje be, ha csak az adatbázisból szeretné eltávolítani)",
"LabelDescription": "Leírás",
"LabelDeselectAll": "Minden kijelölés megszüntetése",
"LabelDevice": "Eszköz",
"LabelDeviceInfo": "Eszköz információ",
"LabelDeviceIsAvailableTo": "Eszköz elérhető a következő számára...",
"LabelDirectory": "Könyvtár",
"LabelDiscFromFilename": "Lemez a fájlnévből",
"LabelDiscFromMetadata": "Lemez a metaadatokból",
"LabelDiscover": "Felfedezés",
"LabelDownload": "Letöltés",
"LabelDownloadNEpisodes": "{0} epizód letöltése",
"LabelDuration": "Időtartam",
"LabelDurationFound": "Megtalált időtartam:",
"LabelEbook": "E-könyv",
"LabelEbooks": "E-könyvek",
"LabelEdit": "Szerkesztés",
"LabelEmail": "E-mail",
"LabelEmailSettingsFromAddress": "Feladó címe",
"LabelEmailSettingsSecure": "Biztonságos",
"LabelEmailSettingsSecureHelp": "Ha igaz, a kapcsolat TLS-t használ a szerverhez való csatlakozáskor. Ha hamis, akkor TLS-t használ, ha a szerver támogatja a STARTTLS kiterjesztést. A legtöbb esetben állítsa ezt az értéket igazra, ha a 465-ös portra csatlakozik. A 587-es vagy 25-ös port esetében tartsa hamis értéken. (a nodemailer.com/smtp/#authentication oldalról)",
"LabelEmailSettingsTestAddress": "Teszt cím",
"LabelEmbeddedCover": "Beágyazott borító",
"LabelEnable": "Engedélyezés",
"LabelEnd": "Vége",
"LabelEpisode": "Epizód",
"LabelEpisodeTitle": "Epizód címe",
"LabelEpisodeType": "Epizód típusa",
"LabelExample": "Példa",
"LabelExplicit": "Explicit",
"LabelFeedURL": "Hírcsatorna URL",
"LabelFetchingMetadata": "Metaadatok lekérése",
"LabelFile": "Fájl",
"LabelFileBirthtime": "Fájl létrehozásának ideje",
"LabelFileModified": "Fájl módosításának ideje",
"LabelFilename": "Fájlnév",
"LabelFilterByUser": "Szűrés felhasználó szerint",
"LabelFindEpisodes": "Epizódok keresése",
"LabelFinished": "Befejezett",
"LabelFolder": "Mappa",
"LabelFolders": "Mappák",
"LabelFontBold": "Félkövér",
"LabelFontFamily": "Betűtípus család",
"LabelFontItalic": "Dőlt",
"LabelFontScale": "Betűméret skála",
"LabelFontStrikethrough": "Áthúzott",
"LabelFormat": "Formátum",
"LabelGenre": "Műfaj",
"LabelGenres": "Műfajok",
"LabelHardDeleteFile": "Fájl végleges törlése",
"LabelHasEbook": "Van e-könyve",
"LabelHasSupplementaryEbook": "Van kiegészítő e-könyve",
"LabelHighestPriority": "Legmagasabb prioritás",
"LabelHost": "Hoszt",
"LabelHour": "Óra",
"LabelIcon": "Ikon",
"LabelImageURLFromTheWeb": "Kép URL a weben",
"LabelIncludeInTracklist": "Beleértve a sávlistába",
"LabelIncomplete": "Befejezetlen",
"LabelInProgress": "Folyamatban",
"LabelInterval": "Intervallum",
"LabelIntervalCustomDailyWeekly": "Egyéni napi/heti",
"LabelIntervalEvery12Hours": "Minden 12 órában",
"LabelIntervalEvery15Minutes": "Minden 15 percben",
"LabelIntervalEvery2Hours": "Minden 2 órában",
"LabelIntervalEvery30Minutes": "Minden 30 percben",
"LabelIntervalEvery6Hours": "Minden 6 órában",
"LabelIntervalEveryDay": "Minden nap",
"LabelIntervalEveryHour": "Minden órában",
"LabelInvalidParts": "Érvénytelen részek",
"LabelInvert": "Megfordítás",
"LabelItem": "Elem",
"LabelLanguage": "Nyelv",
"LabelLanguageDefaultServer": "Szerver alapértelmezett nyelve",
"LabelLastBookAdded": "Utolsó hozzáadott könyv",
"LabelLastBookUpdated": "Utolsó frissített könyv",
"LabelLastSeen": "Utolsó látogatás",
"LabelLastTime": "Utolsó alkalom",
"LabelLastUpdate": "Utolsó frissítés",
"LabelLayout": "Elrendezés",
"LabelLayoutSinglePage": "Egyoldalas",
"LabelLayoutSplitPage": "Kétoldalas",
"LabelLess": "Kevesebb",
"LabelLibrariesAccessibleToUser": "A felhasználó számára elérhető könyvtárak",
"LabelLibrary": "Könyvtár",
"LabelLibraryItem": "Könyvtári elem",
"LabelLibraryName": "Könyvtár neve",
"LabelLimit": "Korlát",
"LabelLineSpacing": "Sorköz",
"LabelListenAgain": "Újrahallgatás",
"LabelLogLevelDebug": "Debug",
"LabelLogLevelInfo": "Információ",
"LabelLogLevelWarn": "Figyelmeztetés",
"LabelLookForNewEpisodesAfterDate": "Új epizódok keresése ezen a dátum után",
"LabelLowestPriority": "Legalacsonyabb prioritás",
"LabelMatchExistingUsersBy": "Meglévő felhasználók egyeztetése",
"LabelMatchExistingUsersByDescription": "Meglévő felhasználók összekapcsolására használt. Egyszer összekapcsolva, a felhasználók egyedülálló azonosítóval lesznek egyeztetve az Ön SSO szolgáltatójától",
"LabelMediaPlayer": "Médialejátszó",
"LabelMediaType": "Média típus",
"LabelMetadataOrderOfPrecedenceDescription": "A magasabb prioritású metaadat-források felülírják az alacsonyabb prioritásúakat",
"LabelMetadataProvider": "Metaadat-szolgáltató",
"LabelMetaTag": "Meta címke",
"LabelMetaTags": "Meta címkék",
"LabelMinute": "Perc",
"LabelMissing": "Hiányzó",
"LabelMissingEbook": "Has no ebook",
"LabelMissingParts": "Hiányzó részek",
"LabelMissingSupplementaryEbook": "Has no supplementary ebook",
"LabelMobileRedirectURIs": "Engedélyezett mobil átirányítási URI-k",
"LabelMobileRedirectURIsDescription": "Ez egy fehérlista az érvényes mobilalkalmazás-átirányítási URI-k számára. Az alapértelmezett <code>audiobookshelf://oauth</code>, amely eltávolítható vagy kiegészíthető további URI-kkal harmadik féltől származó alkalmazásintegráció érdekében. Ha az egyetlen bejegyzés egy csillag (<code>*</code>), akkor bármely URI engedélyezett.",
"LabelMore": "Több",
"LabelMoreInfo": "További információ",
"LabelName": "Név",
"LabelNarrator": "Előadó",
"LabelNarrators": "Előadók",
"LabelNew": "Új",
"LabelNewestAuthors": "Legújabb szerzők",
"LabelNewestEpisodes": "Legújabb epizódok",
"LabelNewPassword": "Új jelszó",
"LabelNextBackupDate": "Következő biztonsági másolat dátuma",
"LabelNextScheduledRun": "Következő ütemezett futtatás",
"LabelNoEpisodesSelected": "Nincsenek kiválasztott epizódok",
"LabelNotes": "Megjegyzések",
"LabelNotFinished": "Nem befejezett",
"LabelNotificationAppriseURL": "Apprise URL(ek)",
"LabelNotificationAvailableVariables": "Elérhető változók",
"LabelNotificationBodyTemplate": "Törzs sablon",
"LabelNotificationEvent": "Értesítési esemény",
"LabelNotificationsMaxFailedAttempts": "Maximális sikertelen próbálkozások",
"LabelNotificationsMaxFailedAttemptsHelp": "Az értesítések akkor kerülnek letiltásra, ha ennyiszer nem sikerül elküldeni őket",
"LabelNotificationsMaxQueueSize": "Maximális értesítési események sorának mérete",
"LabelNotificationsMaxQueueSizeHelp": "Az események korlátozva vannak, hogy másodpercenként 1-szer történjenek. Ha a sor maximális méretű, akkor az események figyelmen kívül lesznek hagyva. Ez megakadályozza az értesítések spamelését.",
"LabelNotificationTitleTemplate": "Cím sablon",
"LabelNotStarted": "Nem indult el",
"LabelNumberOfBooks": "Könyvek száma",
"LabelNumberOfEpisodes": "Epizódok száma",
"LabelOpenRSSFeed": "RSS hírcsatorna megnyitása",
"LabelOverwrite": "Felülírás",
"LabelPassword": "Jelszó",
"LabelPath": "Útvonal",
"LabelPermissionsAccessAllLibraries": "Hozzáférhet az összes könyvtárhoz",
"LabelPermissionsAccessAllTags": "Hozzáférhet az összes címkéhez",
"LabelPermissionsAccessExplicitContent": "Hozzáférhet explicit tartalomhoz",
"LabelPermissionsDelete": "Törölhet",
"LabelPermissionsDownload": "Letölthet",
"LabelPermissionsUpdate": "Frissíthet",
"LabelPermissionsUpload": "Feltölthet",
"LabelPersonalYearReview": "Your Year in Review ({0})",
"LabelPhotoPathURL": "Fénykép útvonal/URL",
"LabelPlaylists": "Lejátszási listák",
"LabelPlayMethod": "Lejátszási módszer",
"LabelPodcast": "Podcast",
"LabelPodcasts": "Podcastok",
"LabelPodcastSearchRegion": "Podcast keresési régió",
"LabelPodcastType": "Podcast típus",
"LabelPort": "Port",
"LabelPrefixesToIgnore": "Figyelmen kívül hagyandó előtagok (nem érzékeny a kis- és nagybetűkre)",
"LabelPreventIndexing": "A hírcsatorna indexelésének megakadályozása az iTunes és a Google podcast könyvtáraiban",
"LabelPrimaryEbook": "Elsődleges e-könyv",
"LabelProgress": "Haladás",
"LabelProvider": "Szolgáltató",
"LabelPubDate": "Kiadás dátuma",
"LabelPublisher": "Kiadó",
"LabelPublishYear": "Kiadás éve",
"LabelRead": "Olvasás",
"LabelReadAgain": "Újraolvasás",
"LabelReadEbookWithoutProgress": "E-könyv olvasása haladás nélkül",
"LabelRecentlyAdded": "Nemrég hozzáadva",
"LabelRecentSeries": "Legutóbbi sorozatok",
"LabelRecommended": "Ajánlott",
"LabelRedo": "Újra",
"LabelRegion": "Régió",
"LabelReleaseDate": "Megjelenés dátuma",
"LabelRemoveCover": "Borító eltávolítása",
"LabelRowsPerPage": "Sorok száma oldalanként",
"LabelRSSFeedCustomOwnerEmail": "Egyéni tulajdonos e-mail",
"LabelRSSFeedCustomOwnerName": "Egyéni tulajdonos neve",
"LabelRSSFeedOpen": "RSS hírcsatorna nyitva",
"LabelRSSFeedPreventIndexing": "Indexelés megakadályozása",
"LabelRSSFeedSlug": "RSS hírcsatorna slug",
"LabelRSSFeedURL": "RSS hírcsatorna URL",
"LabelSearchTerm": "Keresési kifejezés",
"LabelSearchTitle": "Cím keresése",
"LabelSearchTitleOrASIN": "Cím vagy ASIN keresése",
"LabelSeason": "Évad",
"LabelSelectAllEpisodes": "Összes epizód kiválasztása",
"LabelSelectEpisodesShowing": "Kiválasztás {0} megjelenített epizód",
"LabelSelectUsers": "Felhasználók kiválasztása",
"LabelSendEbookToDevice": "E-könyv küldése...",
"LabelSequence": "Sorozat",
"LabelSeries": "Sorozat",
"LabelSeriesName": "Sorozat neve",
"LabelSeriesProgress": "Sorozat haladása",
"LabelServerYearReview": "Server Year in Review ({0})",
"LabelSetEbookAsPrimary": "Beállítás elsődlegesként",
"LabelSetEbookAsSupplementary": "Beállítás kiegészítőként",
"LabelSettingsAudiobooksOnly": "Csak hangoskönyvek",
"LabelSettingsAudiobooksOnlyHelp": "Ennek a beállításnak az engedélyezése figyelmen kívül hagyja az e-könyv fájlokat, kivéve, ha azok egy hangoskönyv mappában vannak, ebben az esetben kiegészítő e-könyvként lesznek beállítva",
"LabelSettingsBookshelfViewHelp": "Skeuomorfikus dizájn fa polcokkal",
"LabelSettingsChromecastSupport": "Chromecast támogatás",
"LabelSettingsDateFormat": "Dátumformátum",
"LabelSettingsDisableWatcher": "Figyelő letiltása",
"LabelSettingsDisableWatcherForLibrary": "Mappafigyelő letiltása a könyvtárban",
"LabelSettingsDisableWatcherHelp": "Letiltja az automatikus elem hozzáadás/frissítés funkciót, amikor fájlváltozásokat észlel. *Szerver újraindítása szükséges",
"LabelSettingsEnableWatcher": "Figyelő engedélyezése",
"LabelSettingsEnableWatcherForLibrary": "Mappafigyelő engedélyezése a könyvtárban",
"LabelSettingsEnableWatcherHelp": "Engedélyezi az automatikus elem hozzáadás/frissítés funkciót, amikor fájlváltozásokat észlel. *Szerver újraindítása szükséges",
"LabelSettingsExperimentalFeatures": "Kísérleti funkciók",
"LabelSettingsExperimentalFeaturesHelp": "Fejlesztés alatt álló funkciók, amelyek visszajelzésre és tesztelésre szorulnak. Kattintson a github megbeszélés megnyitásához.",
"LabelSettingsFindCovers": "Borítók keresése",
"LabelSettingsFindCoversHelp": "Ha a hangoskönyvnek nincs beágyazott borítója vagy borítóképe a mappában, a szkenner megpróbálja megtalálni a borítót.<br>Megjegyzés: Ez meghosszabbítja a szkennelési időt",
"LabelSettingsHideSingleBookSeries": "Egykönyves sorozatok elrejtése",
"LabelSettingsHideSingleBookSeriesHelp": "A csak egy könyvet tartalmazó sorozatok el lesznek rejtve a sorozatok oldalról és a kezdőlap polcairól.",
"LabelSettingsHomePageBookshelfView": "Kezdőlap használja a könyvespolc nézetet",
"LabelSettingsLibraryBookshelfView": "Könyvtár használja a könyvespolc nézetet",
"LabelSettingsOnlyShowLaterBooksInContinueSeries": "Skip earlier books in Continue Series",
"LabelSettingsOnlyShowLaterBooksInContinueSeriesHelp": "The Continue Series home page shelf shows the first book not started in series that have at least one book finished and no books in progress. Enabling this setting will continue series from the furthest completed book instead of the first book not started.",
"LabelSettingsParseSubtitles": "Feliratok elemzése",
"LabelSettingsParseSubtitlesHelp": "Feliratok kinyerése a hangoskönyv mappaneveiből.<br>A feliratnak el kell különülnie egy \" - \" jellel<br>például: \"Könyv címe - Egy felirat itt\" esetén a felirat \"Egy felirat itt\"",
"LabelSettingsPreferMatchedMetadata": "Preferált egyeztetett metaadatok",
"LabelSettingsPreferMatchedMetadataHelp": "Az egyeztetett adatok felülírják az elem részleteit a Gyors egyeztetés használatakor. Alapértelmezés szerint a Gyors egyeztetés csak a hiányzó részleteket tölti ki.",
"LabelSettingsSkipMatchingBooksWithASIN": "Már ASIN-nel rendelkező könyvek egyeztetésének kihagyása",
"LabelSettingsSkipMatchingBooksWithISBN": "Már ISBN-nel rendelkező könyvek egyeztetésének kihagyása",
"LabelSettingsSortingIgnorePrefixes": "Előtagok figyelmen kívül hagyása rendezéskor",
"LabelSettingsSortingIgnorePrefixesHelp": "például az \"a\" előtag esetén a \"A könyv címe\" könyv címe \"Könyv címe, A\" szerint rendeződik",
"LabelSettingsSquareBookCovers": "Négyzet alakú könyvborítók használata",
"LabelSettingsSquareBookCoversHelp": "Négyzet alakú borítók használata az 1,6:1 arányú standard könyvborítók helyett",
"LabelSettingsStoreCoversWithItem": "Borítók tárolása az elemmel",
"LabelSettingsStoreCoversWithItemHelp": "Alapértelmezés szerint a borítók a /metadata/items mappában vannak tárolva, ennek a beállításnak az engedélyezése a borítókat a könyvtári elem mappájában tárolja. Csak egy \"cover\" nevű fájl lesz megtartva",
"LabelSettingsStoreMetadataWithItem": "Metaadatok tárolása az elemmel",
"LabelSettingsStoreMetadataWithItemHelp": "Alapértelmezés szerint a metaadatfájlok a /metadata/items mappában vannak tárolva, ennek a beállításnak az engedélyezése a metaadatfájlokat a könyvtári elem mappáiban tárolja",
"LabelSettingsTimeFormat": "Időformátum",
"LabelShowAll": "Mindent mutat",
"LabelSize": "Méret",
"LabelSleepTimer": "Alvásidőzítő",
"LabelSlug": "Rövid cím",
"LabelStart": "Kezdés",
"LabelStarted": "Elkezdődött",
"LabelStartedAt": "Kezdés ideje",
"LabelStartTime": "Kezdési idő",
"LabelStatsAudioTracks": "Audiósávok",
"LabelStatsAuthors": "Szerzők",
"LabelStatsBestDay": "Legjobb nap",
"LabelStatsDailyAverage": "Napi átlag",
"LabelStatsDays": "Napok",
"LabelStatsDaysListened": "Hallgatott napok",
"LabelStatsHours": "Órák",
"LabelStatsInARow": "egymás után",
"LabelStatsItemsFinished": "Befejezett elemek",
"LabelStatsItemsInLibrary": "Elemek a könyvtárban",
"LabelStatsMinutes": "percek",
"LabelStatsMinutesListening": "Hallgatási percek",
"LabelStatsOverallDays": "Összes nap",
"LabelStatsOverallHours": "Összes óra",
"LabelStatsWeekListening": "Heti hallgatás",
"LabelSubtitle": "Felirat",
"LabelSupportedFileTypes": "Támogatott fájltípusok",
"LabelTag": "Címke",
"LabelTags": "Címkék",
"LabelTagsAccessibleToUser": "A felhasználó számára elérhető címkék",
"LabelTagsNotAccessibleToUser": "A felhasználó számára nem elérhető címkék",
"LabelTasks": "Futó feladatok",
"LabelTextEditorBulletedList": "Pontozott lista",
"LabelTextEditorLink": "Link",
"LabelTextEditorNumberedList": "Számozott lista",
"LabelTextEditorUnlink": "Link eltávolítása",
"LabelTheme": "Téma",
"LabelThemeDark": "Sötét",
"LabelThemeLight": "Világos",
"LabelTimeBase": "Időalap",
"LabelTimeListened": "Hallgatott idő",
"LabelTimeListenedToday": "Ma hallgatott idő",
"LabelTimeRemaining": "{0} maradt",
"LabelTimeToShift": "Eltolás ideje másodpercben",
"LabelTitle": "Cím",
"LabelToolsEmbedMetadata": "Metaadatok beágyazása",
"LabelToolsEmbedMetadataDescription": "Metaadatok beágyazása az audiofájlokba, beleértve a borítóképet és a fejezeteket.",
"LabelToolsMakeM4b": "M4B Hangoskönyv fájl készítése",
"LabelToolsMakeM4bDescription": ".M4B hangoskönyv fájl generálása beágyazott metaadatokkal, borítóképpel és fejezetekkel.",
"LabelToolsSplitM4b": "M4B felosztása MP3-ra",
"LabelToolsSplitM4bDescription": "MP3 fájlok létrehozása egy M4B-ből, fejezetenként felosztva, beágyazott metaadatokkal, borítóképpel és fejezetekkel.",
"LabelTotalDuration": "Teljes időtartam",
"LabelTotalTimeListened": "Teljes hallgatási idő",
"LabelTrackFromFilename": "Sáv a fájlnévből",
"LabelTrackFromMetadata": "Sáv a metaadatokból",
"LabelTracks": "Sávok",
"LabelTracksMultiTrack": "Többsávos",
"LabelTracksNone": "Nincsenek sávok",
"LabelTracksSingleTrack": "Egysávos",
"LabelType": "Típus",
"LabelUnabridged": "Nem tömörített",
"LabelUndo": "Visszavonás",
"LabelUnknown": "Ismeretlen",
"LabelUpdateCover": "Borító frissítése",
"LabelUpdateCoverHelp": "Lehetővé teszi a meglévő borítók felülírását a kiválasztott könyveknél, amikor találatot talál",
"LabelUpdatedAt": "Frissítve",
"LabelUpdateDetails": "Részletek frissítése",
"LabelUpdateDetailsHelp": "Lehetővé teszi a meglévő részletek felülírását a kiválasztott könyveknél, amikor találatot talál",
"LabelUploaderDragAndDrop": "Fájlok vagy mappák húzása és elengedése",
"LabelUploaderDropFiles": "Fájlok elengedése",
"LabelUploaderItemFetchMetadataHelp": "Cím, szerző és sorozat automatikus lekérése",
"LabelUseChapterTrack": "Fejezetsáv használata",
"LabelUseFullTrack": "Teljes sáv használata",
"LabelUser": "Felhasználó",
"LabelUsername": "Felhasználónév",
"LabelValue": "Érték",
"LabelVersion": "Verzió",
"LabelViewBookmarks": "Könyvjelzők megtekintése",
"LabelViewChapters": "Fejezetek megtekintése",
"LabelViewQueue": "Lejátszó sor megtekintése",
"LabelVolume": "Hangerő",
"LabelWeekdaysToRun": "Futás napjai",
"LabelYearReviewHide": "Hide Year in Review",
"LabelYearReviewShow": "See Year in Review",
"LabelYourAudiobookDuration": "Hangoskönyv időtartama",
"LabelYourBookmarks": "Könyvjelzőid",
"LabelYourPlaylists": "Lejátszási listáid",
"LabelYourProgress": "Haladásod",
"MessageAddToPlayerQueue": "Hozzáadás a lejátszó sorhoz",
"MessageAppriseDescription": "Ennek a funkció használatához futtatnia kell egy <a href=\"https://github.com/caronc/apprise-api\" target=\"_blank\">Apprise API</a> példányt vagy egy olyan API-t, amely kezeli ezeket a kéréseket. <br />Az Apprise API URL-nek a teljes URL útvonalat kell tartalmaznia az értesítés elküldéséhez, például, ha az API példánya a <code>http://192.168.1.1:8337</code> címen szolgáltatva, akkor <code>http://192.168.1.1:8337/notify</code> értéket kell megadnia.",
"MessageBackupsDescription": "A biztonsági másolatok tartalmazzák a felhasználókat, a felhasználói haladást, a könyvtári elem részleteit, a szerver beállításait és a képeket, amelyek a <code>/metadata/items</code> és <code>/metadata/authors</code> mappákban vannak tárolva. A biztonsági másolatok <strong>nem</strong> tartalmazzák a könyvtári mappákban tárolt fájlokat.",
"MessageBatchQuickMatchDescription": "A Gyors egyeztetés megpróbálja hozzáadni a hiányzó borítókat és metaadatokat a kiválasztott elemekhez. Engedélyezze az alábbi opciókat, hogy a Gyors egyeztetés felülírhassa a meglévő borítókat és/vagy metaadatokat.",
"MessageBookshelfNoCollections": "Még nem készített gyűjteményeket",
"MessageBookshelfNoResultsForFilter": "Nincs eredmény a \"{0}: {1}\" szűrőre",
"MessageBookshelfNoRSSFeeds": "Nincsenek nyitott RSS hírcsatornák",
"MessageBookshelfNoSeries": "Nincsenek sorozatai",
"MessageChapterEndIsAfter": "A fejezet vége a hangoskönyv végét követi",
"MessageChapterErrorFirstNotZero": "Az első fejezetnek 0:00-kor kell kezdődnie",
"MessageChapterErrorStartGteDuration": "Érvénytelen kezdési idő, kevesebbnek kell lennie, mint a hangoskönyv időtartama",
"MessageChapterErrorStartLtPrev": "Érvénytelen kezdési idő, nagyobbnak kell lennie, mint az előző fejezet kezdési ideje",
"MessageChapterStartIsAfter": "A fejezet kezdete a hangoskönyv végét követi",
"MessageCheckingCron": "Cron ellenőrzése...",
"MessageConfirmCloseFeed": "Biztosan be szeretné zárni ezt a hírcsatornát?",
"MessageConfirmDeleteBackup": "Biztosan törölni szeretné a(z) {0} biztonsági másolatot?",
"MessageConfirmDeleteFile": "Ez törölni fogja a fájlt a fájlrendszerből. Biztos benne?",
"MessageConfirmDeleteLibrary": "Biztosan véglegesen törölni szeretné a(z) \"{0}\" könyvtárat?",
"MessageConfirmDeleteLibraryItem": "Ez eltávolítja a könyvtári elemet az adatbázisból és a fájlrendszerből. Biztos benne?",
"MessageConfirmDeleteLibraryItems": "Ez eltávolítja a(z) {0} könyvtári elemet az adatbázisból és a fájlrendszerből. Biztos benne?",
"MessageConfirmDeleteSession": "Biztosan törölni szeretné ezt a munkamenetet?",
"MessageConfirmForceReScan": "Biztosan kényszeríteni szeretné az újraszkennelést?",
"MessageConfirmMarkAllEpisodesFinished": "Biztosan meg szeretné jelölni az összes epizódot befejezettnek?",
"MessageConfirmMarkAllEpisodesNotFinished": "Biztosan meg szeretné jelölni az összes epizódot nem befejezettnek?",
"MessageConfirmMarkSeriesFinished": "Biztosan meg szeretné jelölni a sorozat összes könyvét befejezettnek?",
"MessageConfirmMarkSeriesNotFinished": "Biztosan meg szeretné jelölni a sorozat összes könyvét nem befejezettnek?",
"MessageConfirmQuickEmbed": "Figyelem! A Gyors beágyazás nem készít biztonsági másolatot az audiofájlokról. Győződjön meg arról, hogy van biztonsági másolata az audiofájlokról. <br><br>Szeretné folytatni?",
"MessageConfirmRemoveAllChapters": "Biztosan eltávolítja az összes fejezetet?",
"MessageConfirmRemoveAuthor": "Biztosan eltávolítja a(z) \"{0}\" szerzőt?",
"MessageConfirmRemoveCollection": "Biztosan eltávolítja a(z) \"{0}\" gyűjteményt?",
"MessageConfirmRemoveEpisode": "Biztosan eltávolítja a(z) \"{0}\" epizódot?",
"MessageConfirmRemoveEpisodes": "Biztosan eltávolítja a(z) {0} epizódot?",
"MessageConfirmRemoveListeningSessions": "Biztosan eltávolítja a(z) {0} hallgatási munkamenetet?",
"MessageConfirmRemoveNarrator": "Biztosan eltávolítja a(z) \"{0}\" előadót?",
"MessageConfirmRemovePlaylist": "Biztosan eltávolítja a(z) \"{0}\" lejátszási listáját?",
"MessageConfirmRenameGenre": "Biztosan át szeretné nevezni a(z) \"{0}\" műfajt \"{1}\"-re az összes elemnél?",
"MessageConfirmRenameGenreMergeNote": "Megjegyzés: Ez a műfaj már létezik, így össze lesznek vonva.",
"MessageConfirmRenameGenreWarning": "Figyelem! Egy hasonló, de eltérő nagybetűkkel rendelkező műfaj már létezik \"{0}\".",
"MessageConfirmRenameTag": "Biztosan át szeretné nevezni a(z) \"{0}\" címkét \"{1}\"-re az összes elemnél?",
"MessageConfirmRenameTagMergeNote": "Megjegyzés: Ez a címke már létezik, így össze lesznek vonva.",
"MessageConfirmRenameTagWarning": "Figyelem! Egy hasonló, de eltérő nagybetűkkel rendelkező címke már létezik \"{0}\".",
"MessageConfirmReScanLibraryItems": "Biztosan újra szeretné szkennelni a(z) {0} elemet?",
"MessageConfirmSendEbookToDevice": "Biztosan el szeretné küldeni a(z) {0} e-könyvet a(z) \"{1}\" eszközre?",
"MessageDownloadingEpisode": "Epizód letöltése",
"MessageDragFilesIntoTrackOrder": "Húzza a fájlokat a helyes sávrendbe",
"MessageEmbedFinished": "Beágyazás befejeződött!",
"MessageEpisodesQueuedForDownload": "{0} Epizód letöltésre várakozik",
"MessageFeedURLWillBe": "A hírcsatorna URL-je {0} lesz",
"MessageFetching": "Lekérés...",
"MessageForceReScanDescription": "minden fájlt újra szkennel, mint egy friss szkennelés. Az audiofájlok ID3 címkéi, OPF fájlok és szövegfájlok újként lesznek szkennelve.",
"MessageImportantNotice": "Fontos közlemény!",
"MessageInsertChapterBelow": "Fejezet beszúrása alulra",
"MessageItemsSelected": "{0} kiválasztott elem",
"MessageItemsUpdated": "{0} frissített elem",
"MessageJoinUsOn": "Csatlakozzon hozzánk: ",
"MessageListeningSessionsInTheLastYear": "{0} hallgatási munkamenet az elmúlt évben",
"MessageLoading": "Betöltés...",
"MessageLoadingFolders": "Mappák betöltése...",
"MessageM4BFailed": "M4B sikertelen!",
"MessageM4BFinished": "M4B befejeződött!",
"MessageMapChapterTitles": "Fejezetcímek hozzárendelése a meglévő hangoskönyv fejezeteihez anélkül, hogy az időbélyegeket módosítaná",
"MessageMarkAllEpisodesFinished": "Az összes epizód megjelölése befejezettnek",
"MessageMarkAllEpisodesNotFinished": "Az összes epizód megjelölése nem befejezettnek",
"MessageMarkAsFinished": "Megjelölés befejezettnek",
"MessageMarkAsNotFinished": "Megjelölés nem befejezettnek",
"MessageMatchBooksDescription": "megpróbálja egyeztetni a könyvtár könyveit egy kiválasztott keresési szolgáltató könyvével, és kitölti az üres részleteket és a borítót. Nem írja felül a részleteket.",
"MessageNoAudioTracks": "Nincsenek audiósávok",
"MessageNoAuthors": "Nincsenek szerzők",
"MessageNoBackups": "Nincsenek biztonsági másolatok",
"MessageNoBookmarks": "Nincsenek könyvjelzők",
"MessageNoChapters": "Nincsenek fejezetek",
"MessageNoCollections": "Nincsenek gyűjtemények",
"MessageNoCoversFound": "Nem találhatóak borítók",
"MessageNoDescription": "Nincs leírás",
"MessageNoDownloadsInProgress": "Jelenleg nincsenek folyamatban lévő letöltések",
"MessageNoDownloadsQueued": "Nincsenek várakozó letöltések",
"MessageNoEpisodeMatchesFound": "Nincs találat az epizódokra",
"MessageNoEpisodes": "Nincsenek epizódok",
"MessageNoFoldersAvailable": "Nincsenek elérhető mappák",
"MessageNoGenres": "Nincsenek műfajok",
"MessageNoIssues": "Nincsenek problémák",
"MessageNoItems": "Nincsenek elemek",
"MessageNoItemsFound": "Nem találhatóak elemek",
"MessageNoListeningSessions": "Nincsenek hallgatási munkamenetek",
"MessageNoLogs": "Nincsenek naplók",
"MessageNoMediaProgress": "Nincs előrehaladás a médialejátszásban",
"MessageNoNotifications": "Nincsenek értesítések",
"MessageNoPodcastsFound": "Nem találhatóak podcastok",
"MessageNoResults": "Nincsenek eredmények",
"MessageNoSearchResultsFor": "Nincs keresési eredmény erre: \"{0}\"",
"MessageNoSeries": "Nincsenek sorozatok",
"MessageNoTags": "Nincsenek címkék",
"MessageNoTasksRunning": "Nincsenek futó feladatok",
"MessageNotYetImplemented": "Még nem implementált",
"MessageNoUpdateNecessary": "Nincs szükség frissítésre",
"MessageNoUpdatesWereNecessary": "Nem volt szükség frissítésekre",
"MessageNoUserPlaylists": "Nincsenek felhasználói lejátszási listák",
"MessageOr": "vagy",
"MessagePauseChapter": "Fejezet lejátszásának szüneteltetése",
"MessagePlayChapter": "Fejezet elejének meghallgatása",
"MessagePlaylistCreateFromCollection": "Lejátszási lista létrehozása gyűjteményből",
"MessagePodcastHasNoRSSFeedForMatching": "A podcastnak nincs RSS hírcsatorna URL-je az egyeztetéshez",
"MessageQuickMatchDescription": "Üres elem részletek és borító feltöltése az első találati eredménnyel a(z) '{0}'-ból. Nem írja felül a részleteket, kivéve, ha a 'Preferált egyeztetett metaadatok' szerverbeállítás engedélyezve van.",
"MessageRemoveChapter": "Fejezet eltávolítása",
"MessageRemoveEpisodes": "Epizód(ok) eltávolítása: {0}",
"MessageRemoveFromPlayerQueue": "Eltávolítás a lejátszási sorból",
"MessageRemoveUserWarning": "Biztosan véglegesen törölni szeretné a(z) \"{0}\" felhasználót?",
"MessageReportBugsAndContribute": "Hibák jelentése, funkciók kérése és hozzájárulás itt:",
"MessageResetChaptersConfirm": "Biztosan alaphelyzetbe szeretné állítani a fejezeteket és visszavonni a módosításokat?",
"MessageRestoreBackupConfirm": "Biztosan vissza szeretné állítani a biztonsági másolatot, amely ekkor készült:",
"MessageRestoreBackupWarning": "A biztonsági mentés visszaállítása felülírja az egész adatbázist, amely a /config mappában található, valamint a borítóképeket a /metadata/items és /metadata/authors mappákban.<br /><br />A biztonsági mentések nem módosítják a könyvtár mappáiban található fájlokat. Ha engedélyezte a szerverbeállításokat a borítóképek és a metaadatok könyvtármappákban való tárolására, akkor ezek nem kerülnek biztonsági mentésre vagy felülírásra.<br /><br />A szerver használó összes kliens automatikusan frissül.",
"MessageSearchResultsFor": "Keresési eredmények",
"MessageSelected": "{0} kiválasztva",
"MessageServerCouldNotBeReached": "A szervert nem lehet elérni",
"MessageSetChaptersFromTracksDescription": "Fejezetek beállítása minden egyes hangfájlt egy fejezetként használva, és a fejezet címét a hangfájl neveként",
"MessageStartPlaybackAtTime": "\"{0}\" lejátszásának kezdése {1} -tól?",
"MessageThinking": "Gondolkodás...",
"MessageUploaderItemFailed": "A feltöltés sikertelen",
"MessageUploaderItemSuccess": "Sikeresen feltöltve!",
"MessageUploading": "Feltöltés...",
"MessageValidCronExpression": "Érvényes cron kifejezés",
"MessageWatcherIsDisabledGlobally": "A megfigyelő globálisan le van tiltva a szerver beállításokban",
"MessageXLibraryIsEmpty": "{0} könyvtár üres!",
"MessageYourAudiobookDurationIsLonger": "Az Ön hangoskönyvének hossza hosszabb, mint a talált időtartam",
"MessageYourAudiobookDurationIsShorter": "Az Ön hangoskönyvének hossza rövidebb, mint a talált időtartam",
"NoteChangeRootPassword": "A Root felhasználó az egyetlen felhasználó, akinek lehet üres jelszava",
"NoteChapterEditorTimes": "Megjegyzés: Az első fejezet kezdőidejének 0:00 kell lennie, és az utolsó fejezet kezdőideje nem haladhatja meg a hangoskönyv időtartamát.",
"NoteFolderPicker": "Megjegyzés: azok a mappák, amelyek már hozzá vannak rendelve, nem jelennek meg",
"NoteRSSFeedPodcastAppsHttps": "Figyelem: A legtöbb podcast alkalmazás megköveteli, hogy az RSS feed URL HTTPS-t használjon",
"NoteRSSFeedPodcastAppsPubDate": "Figyelem: Az egy vagy több epizódnak nincs Közzétételi dátuma. Néhány podcast alkalmazás ezt megköveteli.",
"NoteUploaderFoldersWithMediaFiles": "A médiafájlokat tartalmazó mappák külön könyvtári tételekként lesznek kezelve.",
"NoteUploaderOnlyAudioFiles": "Ha csak hangfájlokat tölt fel, akkor minden egyes hangfájl külön hangoskönyvként lesz kezelve.",
"NoteUploaderUnsupportedFiles": "A nem támogatott fájlok figyelmen kívül hagyásra kerülnek. Mappa kiválasztása vagy elengedésekor az elem mappáján kívüli egyéb fájlok figyelmen kívül lesznek hagyva.",
"PlaceholderNewCollection": "Új gyűjtemény neve",
"PlaceholderNewFolderPath": "Új mappa útvonala",
"PlaceholderNewPlaylist": "Új lejátszási lista neve",
"PlaceholderSearch": "Keresés..",
"PlaceholderSearchEpisode": "Epizód keresése..",
"ToastAccountUpdateFailed": "A fiók frissítése sikertelen",
"ToastAccountUpdateSuccess": "Fiók frissítve",
"ToastAuthorImageRemoveFailed": "A kép eltávolítása sikertelen",
"ToastAuthorImageRemoveSuccess": "Szerző képe eltávolítva",
"ToastAuthorUpdateFailed": "A szerző frissítése sikertelen",
"ToastAuthorUpdateMerged": "Szerző összevonva",
"ToastAuthorUpdateSuccess": "Szerző frissítve",
"ToastAuthorUpdateSuccessNoImageFound": "Szerző frissítve (nem található kép)",
"ToastBackupCreateFailed": "A biztonsági mentés létrehozása sikertelen",
"ToastBackupCreateSuccess": "Biztonsági mentés létrehozva",
"ToastBackupDeleteFailed": "A biztonsági mentés törlése sikertelen",
"ToastBackupDeleteSuccess": "Biztonsági mentés törölve",
"ToastBackupRestoreFailed": "A biztonsági mentés visszaállítása sikertelen",
"ToastBackupUploadFailed": "A biztonsági mentés feltöltése sikertelen",
"ToastBackupUploadSuccess": "Biztonsági mentés feltöltve",
"ToastBatchUpdateFailed": "Kötegelt frissítés sikertelen",
"ToastBatchUpdateSuccess": "Kötegelt frissítés sikeres",
"ToastBookmarkCreateFailed": "Könyvjelző létrehozása sikertelen",
"ToastBookmarkCreateSuccess": "Könyvjelző hozzáadva",
"ToastBookmarkRemoveFailed": "Könyvjelző eltávolítása sikertelen",
"ToastBookmarkRemoveSuccess": "Könyvjelző eltávolítva",
"ToastBookmarkUpdateFailed": "Könyvjelző frissítése sikertelen",
"ToastBookmarkUpdateSuccess": "Könyvjelző frissítve",
"ToastChaptersHaveErrors": "A fejezetek hibákat tartalmaznak",
"ToastChaptersMustHaveTitles": "A fejezeteknek címekkel kell rendelkezniük",
"ToastCollectionItemsRemoveFailed": "Elem(ek) eltávolítása a gyűjteményből sikertelen",
"ToastCollectionItemsRemoveSuccess": "Elem(ek) eltávolítva a gyűjteményből",
"ToastCollectionRemoveFailed": "Gyűjtemény eltávolítása sikertelen",
"ToastCollectionRemoveSuccess": "Gyűjtemény eltávolítva",
"ToastCollectionUpdateFailed": "Gyűjtemény frissítése sikertelen",
"ToastCollectionUpdateSuccess": "Gyűjtemény frissítve",
"ToastItemCoverUpdateFailed": "Elem borítójának frissítése sikertelen",
"ToastItemCoverUpdateSuccess": "Elem borítója frissítve",
"ToastItemDetailsUpdateFailed": "Elem részleteinek frissítése sikertelen",
"ToastItemDetailsUpdateSuccess": "Elem részletei frissítve",
"ToastItemDetailsUpdateUnneeded": "Nincsenek szükséges frissítések a tétel részletein",
"ToastItemMarkedAsFinishedFailed": "Megjelölés Befejezettként sikertelen",
"ToastItemMarkedAsFinishedSuccess": "Elem megjelölve Befejezettként",
"ToastItemMarkedAsNotFinishedFailed": "Nem sikerült Nem Befejezettként megjelölni az elemet",
"ToastItemMarkedAsNotFinishedSuccess": "Elem megjelölve Nem Befejezettként",
"ToastLibraryCreateFailed": "Könyvtár létrehozása sikertelen",
"ToastLibraryCreateSuccess": "\"{0}\" könyvtár létrehozva",
"ToastLibraryDeleteFailed": "Könyvtár törlése sikertelen",
"ToastLibraryDeleteSuccess": "Könyvtár törölve",
"ToastLibraryScanFailedToStart": "A beolvasás elindítása sikertelen",
"ToastLibraryScanStarted": "Könyvtár beolvasása elindítva",
"ToastLibraryUpdateFailed": "Könyvtár frissítése sikertelen",
"ToastLibraryUpdateSuccess": "\"{0}\" könyvtár frissítve",
"ToastPlaylistCreateFailed": "Lejátszási lista létrehozása sikertelen",
"ToastPlaylistCreateSuccess": "Lejátszási lista létrehozva",
"ToastPlaylistRemoveFailed": "Lejátszási lista eltávolítása sikertelen",
"ToastPlaylistRemoveSuccess": "Lejátszási lista eltávolítva",
"ToastPlaylistUpdateFailed": "Lejátszási lista frissítése sikertelen",
"ToastPlaylistUpdateSuccess": "Lejátszási lista frissítve",
"ToastPodcastCreateFailed": "Podcast létrehozása sikertelen",
"ToastPodcastCreateSuccess": "Podcast sikeresen létrehozva",
"ToastRemoveItemFromCollectionFailed": "Tétel eltávolítása a gyűjteményből sikertelen",
"ToastRemoveItemFromCollectionSuccess": "Tétel eltávolítva a gyűjteményből",
"ToastRSSFeedCloseFailed": "RSS feed bezárása sikertelen",
"ToastRSSFeedCloseSuccess": "RSS feed bezárva",
"ToastSendEbookToDeviceFailed": "E-könyv küldése az eszközre sikertelen",
"ToastSendEbookToDeviceSuccess": "E-könyv elküldve az eszközre \"{0}\"",
"ToastSeriesUpdateFailed": "Sorozat frissítése sikertelen",
"ToastSeriesUpdateSuccess": "Sorozat frissítése sikeres",
"ToastSessionDeleteFailed": "Munkamenet törlése sikertelen",
"ToastSessionDeleteSuccess": "Munkamenet törölve",
"ToastSocketConnected": "Socket csatlakoztatva",
"ToastSocketDisconnected": "Socket lecsatlakoztatva",
"ToastSocketFailedToConnect": "A Socket csatlakoztatása sikertelen",
"ToastUserDeleteFailed": "Felhasználó törlése sikertelen",
"ToastUserDeleteSuccess": "Felhasználó törölve"
}

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